@oneuptime/common 14.0.7 → 14.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (440) hide show
  1. package/Models/DatabaseModels/Incident.ts +267 -1
  2. package/Models/DatabaseModels/IncidentCustomField.ts +202 -0
  3. package/Models/DatabaseModels/IncidentEpisode.ts +30 -0
  4. package/Models/DatabaseModels/IncidentTemplate.ts +126 -1
  5. package/Models/DatabaseModels/StatusPage.ts +55 -0
  6. package/Models/DatabaseModels/StatusPageSubscriber.ts +128 -0
  7. package/Server/API/IncidentAPI.ts +162 -0
  8. package/Server/API/StatusPageAPI.ts +487 -171
  9. package/Server/API/StatusPageSubscriberAPI.ts +63 -15
  10. package/Server/Infrastructure/Postgres/SchemaMigrations/1795300000000-SkipHiddenIncidentCreatedNotifications.ts +49 -0
  11. package/Server/Infrastructure/Postgres/SchemaMigrations/1795400000000-AddIncidentStatusPageScope.ts +108 -0
  12. package/Server/Infrastructure/Postgres/SchemaMigrations/1795500000000-AddIncidentTemplateStatusPageScopeFlag.ts +33 -0
  13. package/Server/Infrastructure/Postgres/SchemaMigrations/1795600000000-AddStatusPageSubscriberUnsubscribeToken.ts +51 -0
  14. package/Server/Infrastructure/Postgres/SchemaMigrations/1795700000000-AddStatusPageSubscriberIsAddedByTeam.ts +33 -0
  15. package/Server/Infrastructure/Postgres/SchemaMigrations/1795800000000-AddIncidentCustomFieldCreateAndNotificationSettings.ts +168 -0
  16. package/Server/Infrastructure/Postgres/SchemaMigrations/1795900000000-AddSubscriberNotificationClaimedAt.ts +44 -0
  17. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +14 -0
  18. package/Server/Infrastructure/Semaphore.ts +10 -0
  19. package/Server/Middleware/IdentityRateLimit.ts +108 -3
  20. package/Server/Middleware/SubscriberNotificationPreviewRateLimit.ts +237 -0
  21. package/Server/Middleware/SubscriberNotificationTestSendRateLimit.ts +304 -0
  22. package/Server/Services/DatabaseService.ts +93 -10
  23. package/Server/Services/IncidentCustomFieldService.ts +276 -1
  24. package/Server/Services/IncidentEpisodePublicNoteService.ts +28 -0
  25. package/Server/Services/IncidentEpisodeService.ts +15 -0
  26. package/Server/Services/IncidentEpisodeStateTimelineService.ts +27 -1
  27. package/Server/Services/IncidentPublicNoteService.ts +30 -1
  28. package/Server/Services/IncidentService.ts +1535 -65
  29. package/Server/Services/IncidentStateTimelineService.ts +13 -0
  30. package/Server/Services/IncidentTemplateService.ts +109 -0
  31. package/Server/Services/ScheduledMaintenancePublicNoteService.ts +30 -1
  32. package/Server/Services/ScheduledMaintenanceService.ts +68 -18
  33. package/Server/Services/SmsService.ts +10 -0
  34. package/Server/Services/StatusPageService.ts +65 -8
  35. package/Server/Services/StatusPageSubscriberNotificationTemplateService.ts +299 -37
  36. package/Server/Services/StatusPageSubscriberNotificationTemplateStatusPageService.ts +129 -0
  37. package/Server/Services/StatusPageSubscriberService.ts +1161 -64
  38. package/Server/Types/Markdown.ts +153 -6
  39. package/Server/Types/Workflow/Components/Email.ts +61 -2
  40. package/Server/Utils/CustomField/CustomFieldRename.ts +378 -0
  41. package/Server/Utils/DataSource/EgressGuard.ts +66 -21
  42. package/Server/Utils/Mail/PinnedSmtpSocket.ts +130 -0
  43. package/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.ts +10 -0
  44. package/Server/Utils/SSRFProtection.ts +76 -16
  45. package/Server/Utils/StatusPage/IncidentStatusPageScope.ts +948 -0
  46. package/Server/Utils/StatusPage/IncidentSubscriberAudienceBuilder.ts +656 -0
  47. package/Server/Utils/StatusPage/IncidentTemplateVariableBuilder.ts +805 -0
  48. package/Server/Utils/StatusPage/StatusPageExclusion.ts +18 -0
  49. package/Server/Utils/StatusPage/StatusPageReadAccess.ts +125 -0
  50. package/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeNotice.ts +151 -0
  51. package/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeToken.ts +70 -0
  52. package/Server/Utils/StatusPage/SubscriberIncidentEmailBuilder.ts +421 -0
  53. package/Server/Utils/StatusPage/SubscriberNotificationClaim.ts +107 -0
  54. package/Server/Utils/StatusPage/SubscriberNotificationDeliveryRecord.ts +1049 -0
  55. package/Server/Utils/StatusPage/SubscriberNotificationFanOut.ts +180 -0
  56. package/Server/Utils/StatusPage/SubscriberNotificationPreviewBuilder.ts +839 -0
  57. package/Server/Utils/StatusPage/SubscriberNotificationResendAccess.ts +393 -0
  58. package/Server/Utils/StatusPage/SubscriberNotificationRunLimit.ts +246 -0
  59. package/Server/Utils/StatusPage/SubscriberNotificationTiming.ts +133 -0
  60. package/Server/Utils/StatusPage/SubscriberTemplateIncidentRecordAccess.ts +215 -0
  61. package/Server/Utils/StatusPageResource.ts +51 -20
  62. package/Server/Utils/VM/VMAPI.ts +1 -0
  63. package/Server/Utils/VM/VMRunner.ts +10 -0
  64. package/Server/Utils/Workflow/OAuth2TokenClient.ts +10 -0
  65. package/Server/Views/Partials/SensitiveUrlToken.ejs +8 -5
  66. package/Tests/App/Accounts/RegisterVerifyEmail.test.tsx +748 -0
  67. package/Tests/App/Accounts/ResendVerificationEmail.test.tsx +1222 -0
  68. package/Tests/App/Accounts/VerificationEmailResendUtils.test.ts +797 -0
  69. package/Tests/App/Accounts/VerifyEmailPending.test.tsx +538 -0
  70. package/Tests/App/Accounts/VerifyEmailResend.test.tsx +779 -0
  71. package/Tests/App/Dashboard/CustomFieldsSettingsIncidentOptions.test.tsx +357 -0
  72. package/Tests/App/Dashboard/EventNotes.test.tsx +776 -0
  73. package/Tests/App/Dashboard/EventNotesPages.test.tsx +119 -0
  74. package/Tests/App/Dashboard/EventNotesUtil.test.ts +116 -17
  75. package/Tests/App/Dashboard/EventOverviewPages.test.tsx +242 -5
  76. package/Tests/App/Dashboard/IncidentChangeStateNotifyDefault.test.tsx +91 -1
  77. package/Tests/App/Dashboard/IncidentCreateDetailsStep.test.tsx +643 -0
  78. package/Tests/App/Dashboard/IncidentCreateFromAlerts.test.tsx +8 -1
  79. package/Tests/App/Dashboard/IncidentCreateStatusPageScope.test.tsx +601 -0
  80. package/Tests/App/Dashboard/IncidentCustomFieldTemplateVariables.test.tsx +386 -0
  81. package/Tests/App/Dashboard/IncidentNoteTemplatePlaceholders.test.tsx +124 -0
  82. package/Tests/App/Dashboard/IncidentNoteTemplateVariablesLoader.test.ts +305 -0
  83. package/Tests/App/Dashboard/IncidentOverviewNotifyDefault.test.tsx +112 -0
  84. package/Tests/App/Dashboard/IncidentSettingsRenotifyOnPublish.test.tsx +674 -0
  85. package/Tests/App/Dashboard/IncidentSettingsStatusPageScope.test.tsx +1051 -0
  86. package/Tests/App/Dashboard/IncidentTemplateCustomFields.test.tsx +384 -0
  87. package/Tests/App/Dashboard/IncidentTemplateStatusPageScopeCard.test.tsx +215 -0
  88. package/Tests/App/Dashboard/PublicNotePagesNotifyDefault.test.tsx +113 -1
  89. package/Tests/App/Dashboard/SubscriberAudienceSummary.test.tsx +512 -0
  90. package/Tests/App/Dashboard/SubscriberNotificationPreviewModal.test.tsx +837 -0
  91. package/Tests/App/Dashboard/SubscriberNotificationStatusLabel.test.tsx +88 -0
  92. package/Tests/App/Dashboard/SubscriberNotificationStatusResend.test.tsx +473 -0
  93. package/Tests/App/Dashboard/SubscriberTemplateLivePreview.test.tsx +196 -0
  94. package/Tests/App/Dashboard/TeamAddedSubscribersUnsubscribedNotice.test.tsx +248 -0
  95. package/Tests/App/SensitiveUrlTokenBootstrap.test.ts +64 -3
  96. package/Tests/App/StatusPage/StatusPageUnsubscribePage.test.tsx +412 -0
  97. package/Tests/Models/IncidentCustomFieldApiSchemaContract.test.ts +217 -0
  98. package/Tests/Models/IncidentCustomFieldCreateSettingsColumns.test.ts +252 -0
  99. package/Tests/Models/StatusPageSubscriberUnsubscribeColumns.test.ts +218 -0
  100. package/Tests/Server/API/ExpiredSessionRouteGuards.test.ts +7 -0
  101. package/Tests/Server/API/IncidentSubscriberAudienceAPI.test.ts +1408 -0
  102. package/Tests/Server/API/StatusPageIncidentScope.test.ts +1868 -0
  103. package/Tests/Server/API/StatusPageIncidentScopePostgres.test.ts +790 -0
  104. package/Tests/Server/API/StatusPageManageSubscriptionRecipient.test.ts +10 -1
  105. package/Tests/Server/API/StatusPageManageSubscriptionTemplateVariables.test.ts +206 -18
  106. package/Tests/Server/API/StatusPagePostmortemVisibility.test.ts +14 -2
  107. package/Tests/Server/API/StatusPageSubscriberCreateResponse.test.ts +217 -0
  108. package/Tests/Server/API/StatusPageSubscriberOwnership.test.ts +103 -2
  109. package/Tests/Server/API/StatusPageUnsubscribeAPI.test.ts +396 -0
  110. package/Tests/Server/Infrastructure/Postgres/AddIncidentStatusPageScopeMigration.test.ts +395 -0
  111. package/Tests/Server/Infrastructure/Postgres/AddIncidentTemplateStatusPageScopeFlagMigration.test.ts +150 -0
  112. package/Tests/Server/Infrastructure/Postgres/AddSubscriberNotificationClaimedAtMigration.test.ts +156 -0
  113. package/Tests/Server/Infrastructure/Postgres/SkipHiddenIncidentCreatedNotificationsMigration.test.ts +410 -0
  114. package/Tests/Server/Middleware/IdentityRateLimit.test.ts +551 -1
  115. package/Tests/Server/Middleware/SubscriberNotificationPreviewRateLimit.test.ts +238 -0
  116. package/Tests/Server/Middleware/SubscriberNotificationTestSendRateLimit.test.ts +245 -0
  117. package/Tests/Server/Services/AddStatusPageSubscriberUnsubscribeTokenMigration.test.ts +152 -0
  118. package/Tests/Server/Services/CrossProjectRelationWriteGuard.test.ts +221 -36
  119. package/Tests/Server/Services/DatabaseServiceCompareAndSetColumns.test.ts +191 -0
  120. package/Tests/Server/Services/IncidentCreatedRenotifyOnPublish.test.ts +681 -0
  121. package/Tests/Server/Services/IncidentCreatedResend.test.ts +775 -0
  122. package/Tests/Server/Services/IncidentCustomFieldRenamePostgres.test.ts +713 -0
  123. package/Tests/Server/Services/IncidentCustomFieldService.test.ts +751 -0
  124. package/Tests/Server/Services/IncidentCustomFieldValueValidation.test.ts +556 -0
  125. package/Tests/Server/Services/IncidentStatusPageScope.test.ts +2971 -0
  126. package/Tests/Server/Services/IncidentStatusPageScopeFeed.test.ts +131 -0
  127. package/Tests/Server/Services/IncidentStatusPageScopePostgres.test.ts +625 -0
  128. package/Tests/Server/Services/IncidentTemplateCustomFieldsCopy.test.ts +311 -0
  129. package/Tests/Server/Services/IncidentTemplateStatusPageScope.test.ts +363 -0
  130. package/Tests/Server/Services/PublicNoteEditFeedEventType.test.ts +213 -0
  131. package/Tests/Server/Services/PublicNoteResendAccess.test.ts +994 -0
  132. package/Tests/Server/Services/ScheduledMaintenanceSubscriberTemplateFormats.test.ts +289 -9
  133. package/Tests/Server/Services/SmsServiceFailIfNotSent.test.ts +67 -0
  134. package/Tests/Server/Services/StatusPageReportIncidentCounts.test.ts +610 -0
  135. package/Tests/Server/Services/StatusPageSubscriberAudienceCounts.test.ts +201 -0
  136. package/Tests/Server/Services/StatusPageSubscriberAudienceCountsPostgres.test.ts +271 -0
  137. package/Tests/Server/Services/StatusPageSubscriberConfirmationTemplateEscaping.test.ts +200 -0
  138. package/Tests/Server/Services/StatusPageSubscriberNotificationLogRedaction.test.ts +54 -0
  139. package/Tests/Server/Services/StatusPageSubscriberNotificationTemplateCompile.test.ts +37 -0
  140. package/Tests/Server/Services/StatusPageSubscriberNotificationTemplateIncidentRecordAccess.test.ts +772 -0
  141. package/Tests/Server/Services/StatusPageSubscriberNotificationTemplateLookup.test.ts +506 -0
  142. package/Tests/Server/Services/StatusPageSubscriberNotificationTemplateLookupPostgres.test.ts +261 -0
  143. package/Tests/Server/Services/StatusPageSubscriberReport.test.ts +21 -5
  144. package/Tests/Server/Services/StatusPageSubscriberUnsubscribe.test.ts +1618 -0
  145. package/Tests/Server/Services/StatusPageSubscriberUnsubscribeLinkSenders.test.ts +373 -0
  146. package/Tests/Server/Services/StatusPageSubscriberUnsubscribePostgres.test.ts +388 -0
  147. package/Tests/Server/Services/SubscriberNotificationInFlightRequeue.test.ts +500 -0
  148. package/Tests/Server/Types/MarkdownEmailLinks.test.ts +320 -0
  149. package/Tests/Server/Types/Workflow/Components/Email.test.ts +491 -1
  150. package/Tests/Server/Types/Workflow/Components/EmailSmtpPinning.test.ts +398 -0
  151. package/Tests/Server/Utils/CustomField/CustomFieldRename.test.ts +586 -0
  152. package/Tests/Server/Utils/DataSource/EgressGuard.test.ts +246 -10
  153. package/Tests/Server/Utils/DataSource/EgressGuardDeploymentConfig.test.ts +226 -0
  154. package/Tests/Server/Utils/DataSource/EgressGuardPinning.test.ts +131 -0
  155. package/Tests/Server/Utils/DataSource/HttpFetch.test.ts +4 -0
  156. package/Tests/Server/Utils/EgressPolicyEnvironment.ts +45 -0
  157. package/Tests/Server/Utils/Mail/PinnedSmtpSocket.test.ts +409 -0
  158. package/Tests/Server/Utils/Monitor/MonitorCriteriaObservationBuilderUnits.test.ts +23 -0
  159. package/Tests/Server/Utils/SSRFProtection.test.ts +4 -0
  160. package/Tests/Server/Utils/SSRFProtectionCloudServiceAddresses.test.ts +4 -0
  161. package/Tests/Server/Utils/SSRFProtectionPrivateNetwork.test.ts +4 -0
  162. package/Tests/Server/Utils/SSRFProtectionResolutionDetail.test.ts +314 -0
  163. package/Tests/Server/Utils/StatusPage/IncidentStatusPageScope.test.ts +1699 -0
  164. package/Tests/Server/Utils/StatusPage/IncidentStatusPageScopeQueriesPostgres.test.ts +508 -0
  165. package/Tests/Server/Utils/StatusPage/IncidentSubscriberAudienceCallerPermission.test.ts +246 -0
  166. package/Tests/Server/Utils/StatusPage/IncidentTemplateVariableBuilder.test.ts +1348 -0
  167. package/Tests/Server/Utils/StatusPage/StatusPageReadAccess.test.ts +221 -0
  168. package/Tests/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeNotice.test.ts +132 -0
  169. package/Tests/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeToken.test.ts +155 -0
  170. package/Tests/Server/Utils/StatusPage/SubscriberIncidentEmailBuilder.test.ts +708 -0
  171. package/Tests/Server/Utils/StatusPage/SubscriberNotificationClaim.test.ts +255 -0
  172. package/Tests/Server/Utils/StatusPage/SubscriberNotificationClaimPostgres.test.ts +528 -0
  173. package/Tests/Server/Utils/StatusPage/SubscriberNotificationDeliveryRecord.test.ts +1238 -0
  174. package/Tests/Server/Utils/StatusPage/SubscriberNotificationFanOut.test.ts +472 -0
  175. package/Tests/Server/Utils/StatusPage/SubscriberNotificationPreviewBuilder.test.ts +1168 -0
  176. package/Tests/Server/Utils/StatusPage/SubscriberNotificationRunLimit.test.ts +275 -0
  177. package/Tests/Server/Utils/StatusPage/SubscriberNotificationTiming.test.ts +131 -0
  178. package/Tests/Server/Utils/StatusPageResource.test.ts +98 -0
  179. package/Tests/Server/Utils/VM/VMRunnerPrivateNetworkWiring.test.ts +17 -0
  180. package/Tests/Server/Utils/VM/VMRunnerSsrf.test.ts +75 -0
  181. package/Tests/Types/CustomField/CustomFieldSavedViews.test.ts +247 -0
  182. package/Tests/Types/CustomField/CustomFieldTemplateValues.test.ts +186 -0
  183. package/Tests/Types/CustomField/CustomFieldTypeCorrespondence.test.ts +48 -0
  184. package/Tests/Types/CustomField/CustomFieldValueFormat.test.ts +215 -0
  185. package/Tests/Types/CustomField/CustomFieldValueValidator.test.ts +343 -0
  186. package/Tests/Types/CustomField/CustomFieldVariableKey.test.ts +364 -0
  187. package/Tests/Types/SafeHtml.test.ts +102 -0
  188. package/Tests/Types/StatusPage/IncidentCreatedRenotify.test.ts +419 -0
  189. package/Tests/Types/StatusPage/IncidentCreatedResend.test.ts +118 -0
  190. package/Tests/Types/StatusPage/IncidentScopeAddedPagesNotification.test.ts +558 -0
  191. package/Tests/Types/StatusPage/IncidentSubscriberAudience.test.ts +284 -0
  192. package/Tests/Types/StatusPage/StatusPageSubscriberUnsubscribe.test.ts +341 -0
  193. package/Tests/Types/StatusPage/SubscriberNotificationPreview.test.ts +196 -0
  194. package/Tests/Types/StatusPage/SubscriberNotificationResend.test.ts +201 -0
  195. package/Tests/Types/StatusPage/SubscriberNotificationTemplateCompiler.test.ts +277 -0
  196. package/Tests/Types/StatusPage/SubscriberNotificationTemplatePreview.test.ts +179 -0
  197. package/Tests/Types/StatusPage/SubscriberNotificationTemplateVariables.test.ts +324 -0
  198. package/Tests/Types/StatusPage/SubscriberUpdateNotification.test.ts +60 -13
  199. package/Tests/UI/Components/CustomFields/CustomFieldFormFields.test.ts +432 -0
  200. package/Tests/UI/Components/CustomFields/CustomFieldModelFormFields.test.tsx +685 -0
  201. package/Tests/UI/Components/CustomFields/CustomFieldsDetailOrder.test.tsx +469 -0
  202. package/Tests/UI/Components/Forms/ModelFormCustomFieldInputs.test.tsx +325 -0
  203. package/Tests/UI/Components/Forms/ModelFormDropdownOptionsRefused.test.tsx +358 -0
  204. package/Tests/UI/Components/ModelTable/CustomFieldColumns.test.tsx +22 -0
  205. package/Tests/UI/Monitor/NetworkPathView.test.tsx +289 -0
  206. package/Tests/Utils/Incident/IncidentNoteTemplateVariables.test.ts +623 -0
  207. package/Tests/Utils/Markdown/MarkdownEscape.test.ts +89 -0
  208. package/Tests/Utils/ProbeNetworkFailureUtil.test.ts +992 -0
  209. package/Tests/Utils/RequestFailedDetails.test.ts +575 -0
  210. package/Types/CustomField/CustomFieldOrder.ts +55 -0
  211. package/Types/CustomField/CustomFieldSavedViews.ts +225 -0
  212. package/Types/CustomField/CustomFieldTemplateMerge.ts +71 -0
  213. package/Types/CustomField/CustomFieldType.ts +14 -0
  214. package/Types/CustomField/CustomFieldValueFormat.ts +161 -0
  215. package/Types/CustomField/CustomFieldValueValidator.ts +488 -0
  216. package/Types/CustomField/CustomFieldVariableKey.ts +211 -0
  217. package/Types/SafeHtml.ts +93 -0
  218. package/Types/StatusPage/IncidentCreatedRenotify.ts +224 -0
  219. package/Types/StatusPage/IncidentCreatedResend.ts +108 -0
  220. package/Types/StatusPage/IncidentScopeAddedPagesNotification.ts +343 -0
  221. package/Types/StatusPage/IncidentSubscriberAudience.ts +332 -0
  222. package/Types/StatusPage/StatusPageSubscriberUnsubscribe.ts +405 -0
  223. package/Types/StatusPage/SubscriberNotificationInterruption.ts +32 -0
  224. package/Types/StatusPage/SubscriberNotificationPreview.ts +322 -0
  225. package/Types/StatusPage/SubscriberNotificationResend.ts +132 -0
  226. package/Types/StatusPage/SubscriberNotificationTemplateCompiler.ts +180 -0
  227. package/Types/StatusPage/SubscriberNotificationTemplatePreview.ts +301 -0
  228. package/Types/StatusPage/SubscriberNotificationTemplateVariables.ts +217 -4
  229. package/Types/StatusPage/SubscriberUpdateNotification.ts +32 -11
  230. package/UI/Components/CustomFields/CustomFieldFormFields.ts +317 -0
  231. package/UI/Components/CustomFields/CustomFieldModelFormFields.tsx +395 -0
  232. package/UI/Components/CustomFields/CustomFieldsDetail.tsx +95 -55
  233. package/UI/Components/Forms/ModelForm.tsx +70 -8
  234. package/UI/Components/Forms/Validation.ts +1 -1
  235. package/UI/Components/ModelFormModal/ModelFormModal.tsx +2 -4
  236. package/UI/Components/ModelTable/AnalyticsModelTable.tsx +2 -6
  237. package/UI/Components/ModelTable/BaseModelTable.tsx +9 -8
  238. package/UI/Components/ModelTable/CustomFieldColumns.tsx +14 -1
  239. package/UI/Components/ModelTable/ModelTable.tsx +6 -7
  240. package/UI/Utils/SensitiveUrlToken.ts +10 -5
  241. package/Utils/API.ts +247 -2
  242. package/Utils/Incident/IncidentNoteTemplateVariables.ts +304 -0
  243. package/Utils/Markdown/MarkdownEscape.ts +65 -0
  244. package/Utils/ProbeNetworkFailureUtil.ts +673 -0
  245. package/build/dist/Models/DatabaseModels/Incident.js +267 -1
  246. package/build/dist/Models/DatabaseModels/Incident.js.map +1 -1
  247. package/build/dist/Models/DatabaseModels/IncidentCustomField.js +207 -1
  248. package/build/dist/Models/DatabaseModels/IncidentCustomField.js.map +1 -1
  249. package/build/dist/Models/DatabaseModels/IncidentEpisode.js +31 -0
  250. package/build/dist/Models/DatabaseModels/IncidentEpisode.js.map +1 -1
  251. package/build/dist/Models/DatabaseModels/IncidentTemplate.js +124 -1
  252. package/build/dist/Models/DatabaseModels/IncidentTemplate.js.map +1 -1
  253. package/build/dist/Models/DatabaseModels/StatusPage.js +56 -0
  254. package/build/dist/Models/DatabaseModels/StatusPage.js.map +1 -1
  255. package/build/dist/Models/DatabaseModels/StatusPageSubscriber.js +131 -0
  256. package/build/dist/Models/DatabaseModels/StatusPageSubscriber.js.map +1 -1
  257. package/build/dist/Server/API/IncidentAPI.js +100 -1
  258. package/build/dist/Server/API/IncidentAPI.js.map +1 -1
  259. package/build/dist/Server/API/StatusPageAPI.js +417 -176
  260. package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
  261. package/build/dist/Server/API/StatusPageSubscriberAPI.js +52 -13
  262. package/build/dist/Server/API/StatusPageSubscriberAPI.js.map +1 -1
  263. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795300000000-SkipHiddenIncidentCreatedNotifications.js +41 -0
  264. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795300000000-SkipHiddenIncidentCreatedNotifications.js.map +1 -0
  265. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795400000000-AddIncidentStatusPageScope.js +57 -0
  266. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795400000000-AddIncidentStatusPageScope.js.map +1 -0
  267. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795500000000-AddIncidentTemplateStatusPageScopeFlag.js +23 -0
  268. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795500000000-AddIncidentTemplateStatusPageScopeFlag.js.map +1 -0
  269. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795600000000-AddStatusPageSubscriberUnsubscribeToken.js +40 -0
  270. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795600000000-AddStatusPageSubscriberUnsubscribeToken.js.map +1 -0
  271. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795700000000-AddStatusPageSubscriberIsAddedByTeam.js +26 -0
  272. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795700000000-AddStatusPageSubscriberIsAddedByTeam.js.map +1 -0
  273. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795800000000-AddIncidentCustomFieldCreateAndNotificationSettings.js +89 -0
  274. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795800000000-AddIncidentCustomFieldCreateAndNotificationSettings.js.map +1 -0
  275. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795900000000-AddSubscriberNotificationClaimedAt.js +29 -0
  276. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795900000000-AddSubscriberNotificationClaimedAt.js.map +1 -0
  277. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +14 -0
  278. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  279. package/build/dist/Server/Infrastructure/Semaphore.js +3 -0
  280. package/build/dist/Server/Infrastructure/Semaphore.js.map +1 -1
  281. package/build/dist/Server/Middleware/IdentityRateLimit.js +89 -3
  282. package/build/dist/Server/Middleware/IdentityRateLimit.js.map +1 -1
  283. package/build/dist/Server/Middleware/SubscriberNotificationPreviewRateLimit.js +149 -0
  284. package/build/dist/Server/Middleware/SubscriberNotificationPreviewRateLimit.js.map +1 -0
  285. package/build/dist/Server/Middleware/SubscriberNotificationTestSendRateLimit.js +190 -0
  286. package/build/dist/Server/Middleware/SubscriberNotificationTestSendRateLimit.js.map +1 -0
  287. package/build/dist/Server/Services/DatabaseService.js +56 -7
  288. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  289. package/build/dist/Server/Services/IncidentCustomFieldService.js +204 -8
  290. package/build/dist/Server/Services/IncidentCustomFieldService.js.map +1 -1
  291. package/build/dist/Server/Services/IncidentEpisodePublicNoteService.js +24 -0
  292. package/build/dist/Server/Services/IncidentEpisodePublicNoteService.js.map +1 -1
  293. package/build/dist/Server/Services/IncidentEpisodeService.js +14 -0
  294. package/build/dist/Server/Services/IncidentEpisodeService.js.map +1 -1
  295. package/build/dist/Server/Services/IncidentEpisodeStateTimelineService.js +26 -0
  296. package/build/dist/Server/Services/IncidentEpisodeStateTimelineService.js.map +1 -1
  297. package/build/dist/Server/Services/IncidentPublicNoteService.js +26 -1
  298. package/build/dist/Server/Services/IncidentPublicNoteService.js.map +1 -1
  299. package/build/dist/Server/Services/IncidentService.js +1052 -0
  300. package/build/dist/Server/Services/IncidentService.js.map +1 -1
  301. package/build/dist/Server/Services/IncidentStateTimelineService.js +12 -0
  302. package/build/dist/Server/Services/IncidentStateTimelineService.js.map +1 -1
  303. package/build/dist/Server/Services/IncidentTemplateService.js +83 -0
  304. package/build/dist/Server/Services/IncidentTemplateService.js.map +1 -1
  305. package/build/dist/Server/Services/ScheduledMaintenancePublicNoteService.js +26 -1
  306. package/build/dist/Server/Services/ScheduledMaintenancePublicNoteService.js.map +1 -1
  307. package/build/dist/Server/Services/ScheduledMaintenanceService.js +47 -17
  308. package/build/dist/Server/Services/ScheduledMaintenanceService.js.map +1 -1
  309. package/build/dist/Server/Services/SmsService.js +2 -22
  310. package/build/dist/Server/Services/SmsService.js.map +1 -1
  311. package/build/dist/Server/Services/StatusPageService.js +47 -9
  312. package/build/dist/Server/Services/StatusPageService.js.map +1 -1
  313. package/build/dist/Server/Services/StatusPageSubscriberNotificationTemplateService.js +237 -32
  314. package/build/dist/Server/Services/StatusPageSubscriberNotificationTemplateService.js.map +1 -1
  315. package/build/dist/Server/Services/StatusPageSubscriberNotificationTemplateStatusPageService.js +109 -0
  316. package/build/dist/Server/Services/StatusPageSubscriberNotificationTemplateStatusPageService.js.map +1 -1
  317. package/build/dist/Server/Services/StatusPageSubscriberService.js +925 -99
  318. package/build/dist/Server/Services/StatusPageSubscriberService.js.map +1 -1
  319. package/build/dist/Server/Types/Markdown.js +110 -6
  320. package/build/dist/Server/Types/Markdown.js.map +1 -1
  321. package/build/dist/Server/Types/Workflow/Components/Email.js +55 -6
  322. package/build/dist/Server/Types/Workflow/Components/Email.js.map +1 -1
  323. package/build/dist/Server/Utils/CustomField/CustomFieldRename.js +211 -0
  324. package/build/dist/Server/Utils/CustomField/CustomFieldRename.js.map +1 -0
  325. package/build/dist/Server/Utils/DataSource/EgressGuard.js +41 -12
  326. package/build/dist/Server/Utils/DataSource/EgressGuard.js.map +1 -1
  327. package/build/dist/Server/Utils/Mail/PinnedSmtpSocket.js +101 -0
  328. package/build/dist/Server/Utils/Mail/PinnedSmtpSocket.js.map +1 -0
  329. package/build/dist/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.js +9 -0
  330. package/build/dist/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.js.map +1 -1
  331. package/build/dist/Server/Utils/SSRFProtection.js +48 -6
  332. package/build/dist/Server/Utils/SSRFProtection.js.map +1 -1
  333. package/build/dist/Server/Utils/StatusPage/IncidentStatusPageScope.js +631 -0
  334. package/build/dist/Server/Utils/StatusPage/IncidentStatusPageScope.js.map +1 -0
  335. package/build/dist/Server/Utils/StatusPage/IncidentSubscriberAudienceBuilder.js +403 -0
  336. package/build/dist/Server/Utils/StatusPage/IncidentSubscriberAudienceBuilder.js.map +1 -0
  337. package/build/dist/Server/Utils/StatusPage/IncidentTemplateVariableBuilder.js +451 -0
  338. package/build/dist/Server/Utils/StatusPage/IncidentTemplateVariableBuilder.js.map +1 -0
  339. package/build/dist/Server/Utils/StatusPage/StatusPageExclusion.js +13 -0
  340. package/build/dist/Server/Utils/StatusPage/StatusPageExclusion.js.map +1 -0
  341. package/build/dist/Server/Utils/StatusPage/StatusPageReadAccess.js +91 -0
  342. package/build/dist/Server/Utils/StatusPage/StatusPageReadAccess.js.map +1 -0
  343. package/build/dist/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeNotice.js +101 -0
  344. package/build/dist/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeNotice.js.map +1 -0
  345. package/build/dist/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeToken.js +47 -0
  346. package/build/dist/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeToken.js.map +1 -0
  347. package/build/dist/Server/Utils/StatusPage/SubscriberIncidentEmailBuilder.js +273 -0
  348. package/build/dist/Server/Utils/StatusPage/SubscriberIncidentEmailBuilder.js.map +1 -0
  349. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationClaim.js +73 -0
  350. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationClaim.js.map +1 -0
  351. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationDeliveryRecord.js +722 -0
  352. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationDeliveryRecord.js.map +1 -0
  353. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationFanOut.js +102 -0
  354. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationFanOut.js.map +1 -0
  355. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationPreviewBuilder.js +527 -0
  356. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationPreviewBuilder.js.map +1 -0
  357. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationResendAccess.js +278 -0
  358. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationResendAccess.js.map +1 -0
  359. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationRunLimit.js +137 -0
  360. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationRunLimit.js.map +1 -0
  361. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationTiming.js +103 -0
  362. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationTiming.js.map +1 -0
  363. package/build/dist/Server/Utils/StatusPage/SubscriberTemplateIncidentRecordAccess.js +114 -0
  364. package/build/dist/Server/Utils/StatusPage/SubscriberTemplateIncidentRecordAccess.js.map +1 -0
  365. package/build/dist/Server/Utils/StatusPageResource.js +39 -7
  366. package/build/dist/Server/Utils/StatusPageResource.js.map +1 -1
  367. package/build/dist/Server/Utils/VM/VMAPI.js.map +1 -1
  368. package/build/dist/Server/Utils/VM/VMRunner.js +1 -0
  369. package/build/dist/Server/Utils/VM/VMRunner.js.map +1 -1
  370. package/build/dist/Server/Utils/Workflow/OAuth2TokenClient.js +3 -0
  371. package/build/dist/Server/Utils/Workflow/OAuth2TokenClient.js.map +1 -1
  372. package/build/dist/Types/CustomField/CustomFieldOrder.js +36 -0
  373. package/build/dist/Types/CustomField/CustomFieldOrder.js.map +1 -0
  374. package/build/dist/Types/CustomField/CustomFieldSavedViews.js +135 -0
  375. package/build/dist/Types/CustomField/CustomFieldSavedViews.js.map +1 -0
  376. package/build/dist/Types/CustomField/CustomFieldTemplateMerge.js +27 -0
  377. package/build/dist/Types/CustomField/CustomFieldTemplateMerge.js.map +1 -0
  378. package/build/dist/Types/CustomField/CustomFieldType.js +12 -0
  379. package/build/dist/Types/CustomField/CustomFieldType.js.map +1 -1
  380. package/build/dist/Types/CustomField/CustomFieldValueFormat.js +126 -0
  381. package/build/dist/Types/CustomField/CustomFieldValueFormat.js.map +1 -0
  382. package/build/dist/Types/CustomField/CustomFieldValueValidator.js +280 -0
  383. package/build/dist/Types/CustomField/CustomFieldValueValidator.js.map +1 -0
  384. package/build/dist/Types/CustomField/CustomFieldVariableKey.js +150 -0
  385. package/build/dist/Types/CustomField/CustomFieldVariableKey.js.map +1 -0
  386. package/build/dist/Types/SafeHtml.js +84 -0
  387. package/build/dist/Types/SafeHtml.js.map +1 -0
  388. package/build/dist/Types/StatusPage/IncidentCreatedRenotify.js +117 -0
  389. package/build/dist/Types/StatusPage/IncidentCreatedRenotify.js.map +1 -0
  390. package/build/dist/Types/StatusPage/IncidentCreatedResend.js +86 -0
  391. package/build/dist/Types/StatusPage/IncidentCreatedResend.js.map +1 -0
  392. package/build/dist/Types/StatusPage/IncidentScopeAddedPagesNotification.js +246 -0
  393. package/build/dist/Types/StatusPage/IncidentScopeAddedPagesNotification.js.map +1 -0
  394. package/build/dist/Types/StatusPage/IncidentSubscriberAudience.js +183 -0
  395. package/build/dist/Types/StatusPage/IncidentSubscriberAudience.js.map +1 -0
  396. package/build/dist/Types/StatusPage/StatusPageSubscriberUnsubscribe.js +277 -0
  397. package/build/dist/Types/StatusPage/StatusPageSubscriberUnsubscribe.js.map +1 -0
  398. package/build/dist/Types/StatusPage/SubscriberNotificationInterruption.js +27 -0
  399. package/build/dist/Types/StatusPage/SubscriberNotificationInterruption.js.map +1 -0
  400. package/build/dist/Types/StatusPage/SubscriberNotificationPreview.js +190 -0
  401. package/build/dist/Types/StatusPage/SubscriberNotificationPreview.js.map +1 -0
  402. package/build/dist/Types/StatusPage/SubscriberNotificationResend.js +102 -0
  403. package/build/dist/Types/StatusPage/SubscriberNotificationResend.js.map +1 -0
  404. package/build/dist/Types/StatusPage/SubscriberNotificationTemplateCompiler.js +97 -0
  405. package/build/dist/Types/StatusPage/SubscriberNotificationTemplateCompiler.js.map +1 -0
  406. package/build/dist/Types/StatusPage/SubscriberNotificationTemplatePreview.js +175 -0
  407. package/build/dist/Types/StatusPage/SubscriberNotificationTemplatePreview.js.map +1 -0
  408. package/build/dist/Types/StatusPage/SubscriberNotificationTemplateVariables.js +144 -4
  409. package/build/dist/Types/StatusPage/SubscriberNotificationTemplateVariables.js.map +1 -1
  410. package/build/dist/Types/StatusPage/SubscriberUpdateNotification.js +29 -9
  411. package/build/dist/Types/StatusPage/SubscriberUpdateNotification.js.map +1 -1
  412. package/build/dist/UI/Components/CustomFields/CustomFieldFormFields.js +169 -0
  413. package/build/dist/UI/Components/CustomFields/CustomFieldFormFields.js.map +1 -0
  414. package/build/dist/UI/Components/CustomFields/CustomFieldModelFormFields.js +212 -0
  415. package/build/dist/UI/Components/CustomFields/CustomFieldModelFormFields.js.map +1 -0
  416. package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js +64 -49
  417. package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js.map +1 -1
  418. package/build/dist/UI/Components/Forms/ModelForm.js +39 -5
  419. package/build/dist/UI/Components/Forms/ModelForm.js.map +1 -1
  420. package/build/dist/UI/Components/Forms/Validation.js +1 -1
  421. package/build/dist/UI/Components/Forms/Validation.js.map +1 -1
  422. package/build/dist/UI/Components/ModelFormModal/ModelFormModal.js.map +1 -1
  423. package/build/dist/UI/Components/ModelTable/AnalyticsModelTable.js.map +1 -1
  424. package/build/dist/UI/Components/ModelTable/BaseModelTable.js +2 -2
  425. package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
  426. package/build/dist/UI/Components/ModelTable/CustomFieldColumns.js +13 -1
  427. package/build/dist/UI/Components/ModelTable/CustomFieldColumns.js.map +1 -1
  428. package/build/dist/UI/Components/ModelTable/ModelTable.js +1 -1
  429. package/build/dist/UI/Components/ModelTable/ModelTable.js.map +1 -1
  430. package/build/dist/UI/Utils/SensitiveUrlToken.js +10 -5
  431. package/build/dist/UI/Utils/SensitiveUrlToken.js.map +1 -1
  432. package/build/dist/Utils/API.js +177 -1
  433. package/build/dist/Utils/API.js.map +1 -1
  434. package/build/dist/Utils/Incident/IncidentNoteTemplateVariables.js +155 -0
  435. package/build/dist/Utils/Incident/IncidentNoteTemplateVariables.js.map +1 -0
  436. package/build/dist/Utils/Markdown/MarkdownEscape.js +48 -0
  437. package/build/dist/Utils/Markdown/MarkdownEscape.js.map +1 -1
  438. package/build/dist/Utils/ProbeNetworkFailureUtil.js +460 -0
  439. package/build/dist/Utils/ProbeNetworkFailureUtil.js.map +1 -0
  440. package/package.json +1 -1
@@ -0,0 +1,779 @@
1
+ import "@testing-library/jest-dom";
2
+ import {
3
+ afterEach,
4
+ beforeEach,
5
+ describe,
6
+ expect,
7
+ jest,
8
+ test,
9
+ } from "@jest/globals";
10
+ import {
11
+ act,
12
+ cleanup,
13
+ fireEvent,
14
+ render,
15
+ screen,
16
+ } from "@testing-library/react";
17
+ import { SpyInstance } from "jest-mock";
18
+ import React from "react";
19
+ import { MemoryRouter } from "react-router-dom";
20
+ import EmailVerificationToken from "../../../Models/DatabaseModels/EmailVerificationToken";
21
+ import HTTPErrorResponse from "../../../Types/API/HTTPErrorResponse";
22
+ import HTTPResponse from "../../../Types/API/HTTPResponse";
23
+ import APIException from "../../../Types/Exception/ApiException";
24
+ import { JSONObject } from "../../../Types/JSON";
25
+ import API from "../../../UI/Utils/API/API";
26
+ import ModelAPI, {
27
+ ModelAPIHttpResponse,
28
+ } from "../../../UI/Utils/ModelAPI/ModelAPI";
29
+ import Navigation from "../../../UI/Utils/Navigation";
30
+ import SensitiveUrlToken from "../../../UI/Utils/SensitiveUrlToken";
31
+ import i18n from "../../../../App/FeatureSet/Accounts/src/Utils/i18n";
32
+ import VerifyEmailPage from "../../../../App/FeatureSet/Accounts/src/Pages/VerifyEmail";
33
+
34
+ /*
35
+ * The page a verification link opens, when the link no longer works.
36
+ *
37
+ * Before this, every failure ended on "Sorry, something went wrong!" and the
38
+ * server's advice to sign in for a new link -- even when the server had not
39
+ * really answered, and even though the link that failed is itself enough to
40
+ * ask for a new one. These tests drive the real page with only the network
41
+ * and the URL token mocked, and pin the three ways it can now end:
42
+ *
43
+ * - the link was turned down (4xx) and is a well-formed token: say so, and
44
+ * offer "Resend verification email" with that token as the credential;
45
+ * - the link was turned down but could never have been a real token: say
46
+ * so, with the server's own advice, and no button that can only fail;
47
+ * - the server did not answer properly (5xx, network): nothing is known
48
+ * about the link, so offer to try the SAME link again.
49
+ *
50
+ * The clock is fake so the resend button's countdown never ticks outside
51
+ * act() while a test is still running.
52
+ */
53
+
54
+ const LINK_TOKEN: string = "8f2d1c3a-4b5e-4f60-8a71-92b3c4d5e6f7";
55
+ const MALFORMED_TOKEN: string = "not-a-verification-token";
56
+
57
+ // Straight out of en.json.
58
+ const SUCCESS_TITLE: string = "Your email is verified.";
59
+ const SUCCESS_DESCRIPTION: string =
60
+ "Thank you for verifying your email. You can now log in to OneUptime.";
61
+ const CONTINUE_TO_SIGN_IN: string = "Continue to sign in";
62
+ const LINK_INVALID_TITLE: string = "This verification link is no longer valid";
63
+ const REQUEST_NEW_LINK: string =
64
+ "If your link has expired or stopped working, we can email you a new one.";
65
+ const ERROR_TITLE: string = "Sorry, something went wrong!";
66
+ const TRY_AGAIN: string = "Try again";
67
+ const RETURN_TO_SIGN_IN: string = "Return to sign in?";
68
+ const LOGIN_LINK: string = "Login.";
69
+ const RESEND_BUTTON: string = "Resend verification email";
70
+ const SENT: string =
71
+ "We've sent a new verification link. It can take a minute or two to arrive.";
72
+ const ALREADY_VERIFIED: string =
73
+ "Your email address is already verified. You can sign in now.";
74
+ const INVALID_REQUEST: string =
75
+ "This verification request is no longer valid. Sign in with your email and password and we will send you a new verification link.";
76
+
77
+ // What /verify-email says about a link it turns down.
78
+ const LINK_EXPIRED: string =
79
+ "Link expired. Please try to log in and we will resend you another link which you should be able to verify email with.";
80
+ const LINK_INVALID: string =
81
+ "Invalid link. Please try to log in and we will resend you another link which you should be able to verify email with.";
82
+
83
+ /*
84
+ * Console noise that has nothing to do with this page, and is ignored for
85
+ * that reason; every other console error fails the test.
86
+ *
87
+ * - Testing Library 13 on React 18.3 prints the first one once per run.
88
+ * - ModelAPI pulls in UI/Utils/Telemetry, whose zone.js swaps the global
89
+ * Promise. React's "was this act() awaited?" check runs on that Promise
90
+ * while `await` uses the native one, so the first properly awaited
91
+ * act(async) in the file is reported as not awaited.
92
+ */
93
+ const KNOWN_CONSOLE_NOISE: Array<string> = [
94
+ "`ReactDOMTestUtils.act` is deprecated",
95
+ "You called act(async () => ...) without await",
96
+ ];
97
+
98
+ type VerifyAnswer = () => Promise<ModelAPIHttpResponse<EmailVerificationToken>>;
99
+
100
+ let linkToken: string = LINK_TOKEN;
101
+
102
+ /*
103
+ * What each POST /verify-email answers with, in order. The last one repeats,
104
+ * so a test only lists as many as it cares about.
105
+ */
106
+ let verifyAnswers: Array<VerifyAnswer> = [];
107
+
108
+ let resendAnswer: () => Promise<HTTPResponse<JSONObject>> = async (): Promise<
109
+ HTTPResponse<JSONObject>
110
+ > => {
111
+ return new HTTPResponse<JSONObject>(
112
+ 200,
113
+ { emailSent: true, alreadyVerified: false, retryAfterSeconds: 60 },
114
+ {},
115
+ );
116
+ };
117
+
118
+ let resendPosts: Array<unknown> = [];
119
+ let consoleErrors: SpyInstance<typeof console.error>;
120
+ let createOrUpdate: SpyInstance<typeof ModelAPI.createOrUpdate>;
121
+
122
+ const verified: VerifyAnswer = async (): Promise<
123
+ ModelAPIHttpResponse<EmailVerificationToken>
124
+ > => {
125
+ return new ModelAPIHttpResponse<EmailVerificationToken>(200, {}, {});
126
+ };
127
+
128
+ type RefuseFunction = (statusCode: number, message: string) => VerifyAnswer;
129
+
130
+ const refuse: RefuseFunction = (
131
+ statusCode: number,
132
+ message: string,
133
+ ): VerifyAnswer => {
134
+ return async (): Promise<ModelAPIHttpResponse<EmailVerificationToken>> => {
135
+ // ModelAPI.createOrUpdate throws the error response it got back.
136
+ throw new HTTPErrorResponse(statusCode, { message: message }, {});
137
+ };
138
+ };
139
+
140
+ const networkFailure: VerifyAnswer = async (): Promise<
141
+ ModelAPIHttpResponse<EmailVerificationToken>
142
+ > => {
143
+ throw new APIException("Network Error");
144
+ };
145
+
146
+ type SettleFunction = () => Promise<void>;
147
+
148
+ const settle: SettleFunction = async (): Promise<void> => {
149
+ await act(async () => {
150
+ for (let tick: number = 0; tick < 20; tick++) {
151
+ await Promise.resolve();
152
+ }
153
+ });
154
+ };
155
+
156
+ type RenderPageFunction = () => Promise<void>;
157
+
158
+ const renderPage: RenderPageFunction = async (): Promise<void> => {
159
+ render(
160
+ <MemoryRouter>
161
+ <VerifyEmailPage />
162
+ </MemoryRouter>,
163
+ );
164
+ await settle();
165
+ };
166
+
167
+ type ElementFunction = () => HTMLElement;
168
+
169
+ const errorBlock: ElementFunction = (): HTMLElement => {
170
+ return screen.getByTestId("verify-email-error");
171
+ };
172
+
173
+ const heading: ElementFunction = (): HTMLElement => {
174
+ return screen.getByRole("heading", { level: 1 });
175
+ };
176
+
177
+ type ExpectFooterFunction = () => void;
178
+
179
+ const expectSignInFooter: ExpectFooterFunction = (): void => {
180
+ const loginLink: HTMLElement = screen.getByRole("link", {
181
+ name: LOGIN_LINK,
182
+ });
183
+ expect(loginLink).toHaveAttribute("href", "/accounts/login");
184
+ expect(loginLink.closest("p")).toHaveTextContent(
185
+ `${RETURN_TO_SIGN_IN} ${LOGIN_LINK}`,
186
+ );
187
+ };
188
+
189
+ type SwitchLanguageFunction = () => Promise<void>;
190
+
191
+ const switchToPersian: SwitchLanguageFunction = async (): Promise<void> => {
192
+ await act(async () => {
193
+ await i18n.changeLanguage("fa");
194
+ });
195
+ };
196
+
197
+ type PersianFunction = (key: string, english: string) => string;
198
+
199
+ /*
200
+ * The Persian copy for a key, out of fa.json. A missing Persian string would
201
+ * silently fall back to the English one and prove nothing, so it must be
202
+ * neither the English copy nor the bare key, and must be in Persian script.
203
+ */
204
+ const persian: PersianFunction = (key: string, english: string): string => {
205
+ const text: string = i18n.t(key);
206
+
207
+ expect(text).not.toBe(english);
208
+ expect(text).not.toBe(key);
209
+ expect(text).toMatch(/[؀-ۿ]/);
210
+
211
+ return text;
212
+ };
213
+
214
+ type CardFunction = (testId: string) => HTMLElement | null;
215
+
216
+ // The card around a view: the element that carries the page's direction.
217
+ const cardAround: CardFunction = (testId: string): HTMLElement | null => {
218
+ return screen.getByTestId(testId).closest<HTMLElement>("[dir]");
219
+ };
220
+
221
+ describe("Verify email page", () => {
222
+ beforeEach(() => {
223
+ jest.useFakeTimers();
224
+ jest.setSystemTime(new Date("2026-03-02T10:00:00.000Z"));
225
+
226
+ linkToken = LINK_TOKEN;
227
+ verifyAnswers = [verified];
228
+ resendPosts = [];
229
+ resendAnswer = async (): Promise<HTTPResponse<JSONObject>> => {
230
+ return new HTTPResponse<JSONObject>(
231
+ 200,
232
+ { emailSent: true, alreadyVerified: false, retryAfterSeconds: 60 },
233
+ {},
234
+ );
235
+ };
236
+
237
+ consoleErrors = jest.spyOn(console, "error");
238
+
239
+ jest.spyOn(Navigation, "navigate").mockImplementation(() => {});
240
+ jest.spyOn(SensitiveUrlToken, "read").mockImplementation((): string => {
241
+ return linkToken;
242
+ });
243
+ jest.spyOn(SensitiveUrlToken, "clear").mockImplementation(() => {});
244
+
245
+ let verifyCalls: number = 0;
246
+ createOrUpdate = jest
247
+ .spyOn(ModelAPI, "createOrUpdate")
248
+ .mockImplementation(
249
+ async (): Promise<ModelAPIHttpResponse<EmailVerificationToken>> => {
250
+ const answer: VerifyAnswer =
251
+ verifyAnswers[Math.min(verifyCalls, verifyAnswers.length - 1)]!;
252
+ verifyCalls++;
253
+ return await answer();
254
+ },
255
+ ) as unknown as SpyInstance<typeof ModelAPI.createOrUpdate>;
256
+
257
+ jest
258
+ .spyOn(API, "post")
259
+ .mockImplementation(
260
+ async (
261
+ options: Parameters<typeof API.post>[0],
262
+ ): Promise<HTTPResponse<JSONObject>> => {
263
+ const url: string = options.url.toString();
264
+
265
+ if (!url.endsWith("/resend-verification-email")) {
266
+ throw new Error(`Unexpected request to ${url}`);
267
+ }
268
+
269
+ resendPosts.push(options.data);
270
+ return await resendAnswer();
271
+ },
272
+ );
273
+ });
274
+
275
+ afterEach(async () => {
276
+ cleanup();
277
+
278
+ const reportedErrors: Array<string> = consoleErrors.mock.calls
279
+ .map((call: Array<unknown>) => {
280
+ return call.map(String).join(" ");
281
+ })
282
+ .filter((message: string) => {
283
+ return !KNOWN_CONSOLE_NOISE.some((noise: string) => {
284
+ return message.includes(noise);
285
+ });
286
+ });
287
+
288
+ jest.useRealTimers();
289
+ jest.restoreAllMocks();
290
+
291
+ // Language is shared by every test in the file: always put English back.
292
+ if (i18n.language !== "en") {
293
+ await i18n.changeLanguage("en");
294
+ }
295
+
296
+ expect(reportedErrors).toEqual([]);
297
+ });
298
+
299
+ describe("a link that works", () => {
300
+ test("verifies the token from the URL and shows the success view, as before", async () => {
301
+ await renderPage();
302
+
303
+ expect(createOrUpdate).toHaveBeenCalledTimes(1);
304
+ const request: Parameters<typeof ModelAPI.createOrUpdate>[0] =
305
+ createOrUpdate.mock.calls[0]![0];
306
+ expect((request.model as EmailVerificationToken).token?.toString()).toBe(
307
+ LINK_TOKEN,
308
+ );
309
+ expect(request.requestOptions?.overrideRequestUrl?.toString()).toMatch(
310
+ /\/verify-email$/,
311
+ );
312
+
313
+ const success: HTMLElement = screen.getByTestId("verify-email-success");
314
+ expect(heading()).toHaveTextContent(SUCCESS_TITLE);
315
+ expect(success).toContainElement(heading());
316
+ expect(success).toHaveTextContent(SUCCESS_DESCRIPTION);
317
+
318
+ const next: HTMLElement = screen.getByRole("link", {
319
+ name: CONTINUE_TO_SIGN_IN,
320
+ });
321
+ expect(next).toHaveAttribute("href", "/accounts/login");
322
+
323
+ // The spent token must not outlive the page.
324
+ expect(SensitiveUrlToken.clear).toHaveBeenCalledTimes(1);
325
+
326
+ expect(
327
+ screen.queryByTestId("verify-email-error"),
328
+ ).not.toBeInTheDocument();
329
+ expect(
330
+ screen.queryByTestId("resend-verification-email"),
331
+ ).not.toBeInTheDocument();
332
+ expect(
333
+ screen.queryByRole("link", { name: LOGIN_LINK }),
334
+ ).not.toBeInTheDocument();
335
+ expect(resendPosts).toHaveLength(0);
336
+ });
337
+
338
+ test("lays the card out left-to-right in English", async () => {
339
+ await renderPage();
340
+
341
+ const card: HTMLElement | null = screen
342
+ .getByTestId("verify-email-success")
343
+ .closest("[dir]");
344
+ expect(card).toHaveAttribute("dir", "ltr");
345
+ });
346
+ });
347
+
348
+ describe("a link the server turned down", () => {
349
+ test.each([
350
+ ["expired", LINK_EXPIRED],
351
+ ["unknown", LINK_INVALID],
352
+ ])(
353
+ "an %s link offers a new one instead of the server's sign-in advice",
354
+ async (_label: string, serverMessage: string) => {
355
+ verifyAnswers = [refuse(400, serverMessage)];
356
+
357
+ await renderPage();
358
+
359
+ const block: HTMLElement = errorBlock();
360
+ expect(heading()).toHaveTextContent(LINK_INVALID_TITLE);
361
+ expect(block).toContainElement(heading());
362
+ expect(block).toHaveTextContent(REQUEST_NEW_LINK);
363
+ expect(screen.queryByText(serverMessage)).not.toBeInTheDocument();
364
+ expect(document.body).not.toHaveTextContent("Please try to log in");
365
+ expect(screen.queryByText(ERROR_TITLE)).not.toBeInTheDocument();
366
+ expect(
367
+ screen.queryByTestId("verify-email-try-again"),
368
+ ).not.toBeInTheDocument();
369
+
370
+ const resend: HTMLElement = screen.getByTestId(
371
+ "resend-verification-email",
372
+ );
373
+ expect(block).toContainElement(resend);
374
+ expect(resend).toHaveAccessibleName(RESEND_BUTTON);
375
+ // Nothing has been sent from this page yet, so there is no wait.
376
+ expect(resend).toBeEnabled();
377
+
378
+ expectSignInFooter();
379
+
380
+ // Kept: the failed link is still the credential for a new one.
381
+ expect(SensitiveUrlToken.clear).not.toHaveBeenCalled();
382
+ expect(
383
+ screen.queryByTestId("verify-email-success"),
384
+ ).not.toBeInTheDocument();
385
+ },
386
+ );
387
+
388
+ test("the resend button asks for a new link with the token from the URL", async () => {
389
+ verifyAnswers = [refuse(400, LINK_EXPIRED)];
390
+ await renderPage();
391
+
392
+ fireEvent.click(screen.getByTestId("resend-verification-email"));
393
+ await settle();
394
+
395
+ expect(resendPosts).toEqual([
396
+ { data: { verificationToken: LINK_TOKEN } },
397
+ ]);
398
+ expect(
399
+ screen.getByTestId("resend-verification-email-status"),
400
+ ).toHaveTextContent(SENT);
401
+ expect(screen.getByTestId("resend-verification-email")).toBeDisabled();
402
+ expect(
403
+ screen.getByTestId("resend-verification-email-countdown"),
404
+ ).toHaveTextContent("You can request another email in 1:01.");
405
+
406
+ // Asking for a new link does not re-run verification.
407
+ expect(createOrUpdate).toHaveBeenCalledTimes(1);
408
+ });
409
+
410
+ test("uses the token read when the page opened, even if it is gone from storage by then", async () => {
411
+ verifyAnswers = [refuse(400, LINK_EXPIRED)];
412
+ await renderPage();
413
+
414
+ linkToken = "";
415
+ fireEvent.click(screen.getByTestId("resend-verification-email"));
416
+ await settle();
417
+
418
+ expect(resendPosts).toEqual([
419
+ { data: { verificationToken: LINK_TOKEN } },
420
+ ]);
421
+ });
422
+
423
+ test("a refused resend shows the server's curated reason", async () => {
424
+ verifyAnswers = [refuse(400, LINK_EXPIRED)];
425
+ resendAnswer = async (): Promise<HTTPResponse<JSONObject>> => {
426
+ return new HTTPErrorResponse(
427
+ 400,
428
+ { message: INVALID_REQUEST },
429
+ {},
430
+ ) as unknown as HTTPResponse<JSONObject>;
431
+ };
432
+ await renderPage();
433
+
434
+ fireEvent.click(screen.getByTestId("resend-verification-email"));
435
+ await settle();
436
+
437
+ expect(
438
+ screen.getByTestId("resend-verification-email-status"),
439
+ ).toHaveTextContent(INVALID_REQUEST);
440
+ expect(heading()).toHaveTextContent(LINK_INVALID_TITLE);
441
+ });
442
+
443
+ test("an account that turns out to be verified flips the page to the success view", async () => {
444
+ verifyAnswers = [refuse(400, LINK_EXPIRED)];
445
+ resendAnswer = async (): Promise<HTTPResponse<JSONObject>> => {
446
+ return new HTTPResponse<JSONObject>(
447
+ 200,
448
+ { emailSent: false, alreadyVerified: true, retryAfterSeconds: 0 },
449
+ {},
450
+ );
451
+ };
452
+ await renderPage();
453
+
454
+ fireEvent.click(screen.getByTestId("resend-verification-email"));
455
+ await settle();
456
+
457
+ expect(screen.getByTestId("verify-email-success")).toBeInTheDocument();
458
+ expect(
459
+ screen.queryByTestId("verify-email-error"),
460
+ ).not.toBeInTheDocument();
461
+ expect(heading()).toHaveTextContent(SUCCESS_TITLE);
462
+ // Focus follows, or it is left on a button that no longer exists.
463
+ expect(heading()).toHaveFocus();
464
+ expect(
465
+ screen.getByRole("link", { name: CONTINUE_TO_SIGN_IN }),
466
+ ).toHaveAttribute("href", "/accounts/login");
467
+ expect(
468
+ screen.queryByTestId("resend-verification-email"),
469
+ ).not.toBeInTheDocument();
470
+ expect(screen.queryByText(ALREADY_VERIFIED)).not.toBeInTheDocument();
471
+ expect(
472
+ screen.queryByRole("link", { name: LOGIN_LINK }),
473
+ ).not.toBeInTheDocument();
474
+
475
+ // Nothing left for the token to do.
476
+ expect(SensitiveUrlToken.clear).toHaveBeenCalledTimes(1);
477
+ expect(createOrUpdate).toHaveBeenCalledTimes(1);
478
+ });
479
+
480
+ test.each([
481
+ ["a malformed token", MALFORMED_TOKEN],
482
+ ["a token with a trailing character", `${LINK_TOKEN}x`],
483
+ ["no token at all", ""],
484
+ ])(
485
+ "%s gets the server's advice and no button that can only fail",
486
+ async (_label: string, token: string) => {
487
+ linkToken = token;
488
+ verifyAnswers = [refuse(400, LINK_INVALID)];
489
+
490
+ await renderPage();
491
+
492
+ const block: HTMLElement = errorBlock();
493
+ expect(heading()).toHaveTextContent(LINK_INVALID_TITLE);
494
+ expect(block).toHaveTextContent(LINK_INVALID);
495
+ expect(block).not.toHaveTextContent(REQUEST_NEW_LINK);
496
+ expect(
497
+ screen.queryByTestId("resend-verification-email"),
498
+ ).not.toBeInTheDocument();
499
+ expect(
500
+ screen.queryByTestId("verify-email-try-again"),
501
+ ).not.toBeInTheDocument();
502
+ expectSignInFooter();
503
+ },
504
+ );
505
+
506
+ test("any 4xx is a problem with the link, not the server", async () => {
507
+ verifyAnswers = [refuse(404, "Not found")];
508
+
509
+ await renderPage();
510
+
511
+ expect(heading()).toHaveTextContent(LINK_INVALID_TITLE);
512
+ expect(screen.getByTestId("resend-verification-email")).toBeEnabled();
513
+ });
514
+
515
+ test("the error card is left-to-right in English too", async () => {
516
+ verifyAnswers = [refuse(400, LINK_EXPIRED)];
517
+ await renderPage();
518
+
519
+ expect(errorBlock().closest("[dir]")).toHaveAttribute("dir", "ltr");
520
+ });
521
+ });
522
+
523
+ describe("a server that did not answer properly", () => {
524
+ test.each([
525
+ ["a 500", refuse(500, "Server Error"), "Server Error"],
526
+ ["a 503", refuse(503, "Service Unavailable"), "Service Unavailable"],
527
+ ["a network error", networkFailure, "Network Error"],
528
+ ])(
529
+ "%s says so and offers to try the same link again",
530
+ async (_label: string, answer: VerifyAnswer, message: string) => {
531
+ verifyAnswers = [answer];
532
+
533
+ await renderPage();
534
+
535
+ const block: HTMLElement = errorBlock();
536
+ expect(heading()).toHaveTextContent(ERROR_TITLE);
537
+ expect(block).toHaveTextContent(message);
538
+ expect(screen.queryByText(LINK_INVALID_TITLE)).not.toBeInTheDocument();
539
+ expect(
540
+ screen.queryByTestId("resend-verification-email"),
541
+ ).not.toBeInTheDocument();
542
+
543
+ const retry: HTMLElement = screen.getByTestId("verify-email-try-again");
544
+ expect(retry).toHaveAccessibleName(TRY_AGAIN);
545
+ expect(retry).toBeEnabled();
546
+ expect(block).toContainElement(retry);
547
+
548
+ expectSignInFooter();
549
+ expect(SensitiveUrlToken.clear).not.toHaveBeenCalled();
550
+ },
551
+ );
552
+
553
+ test("a 502 is shown as a connection problem", async () => {
554
+ verifyAnswers = [refuse(502, "Bad Gateway")];
555
+
556
+ await renderPage();
557
+
558
+ expect(errorBlock()).toHaveTextContent(
559
+ "Error connecting to server. Please try again in few minutes.",
560
+ );
561
+ expect(screen.getByTestId("verify-email-try-again")).toBeInTheDocument();
562
+ });
563
+
564
+ test("Try again re-runs verification with the same link and can succeed", async () => {
565
+ verifyAnswers = [refuse(500, "Server Error"), verified];
566
+ await renderPage();
567
+
568
+ expect(createOrUpdate).toHaveBeenCalledTimes(1);
569
+
570
+ fireEvent.click(screen.getByTestId("verify-email-try-again"));
571
+ await settle();
572
+
573
+ expect(createOrUpdate).toHaveBeenCalledTimes(2);
574
+ expect(
575
+ (
576
+ createOrUpdate.mock.calls[1]![0].model as EmailVerificationToken
577
+ ).token?.toString(),
578
+ ).toBe(LINK_TOKEN);
579
+
580
+ expect(screen.getByTestId("verify-email-success")).toBeInTheDocument();
581
+ expect(heading()).toHaveTextContent(SUCCESS_TITLE);
582
+ expect(
583
+ screen.queryByTestId("verify-email-error"),
584
+ ).not.toBeInTheDocument();
585
+ expect(SensitiveUrlToken.clear).toHaveBeenCalledTimes(1);
586
+ });
587
+
588
+ test("Try again shows the loader while it runs", async () => {
589
+ let finishRetry: () => void = () => {};
590
+ verifyAnswers = [
591
+ networkFailure,
592
+ (): Promise<ModelAPIHttpResponse<EmailVerificationToken>> => {
593
+ return new Promise<ModelAPIHttpResponse<EmailVerificationToken>>(
594
+ (
595
+ resolve: (
596
+ value: ModelAPIHttpResponse<EmailVerificationToken>,
597
+ ) => void,
598
+ ): void => {
599
+ finishRetry = (): void => {
600
+ resolve(
601
+ new ModelAPIHttpResponse<EmailVerificationToken>(200, {}, {}),
602
+ );
603
+ };
604
+ },
605
+ );
606
+ },
607
+ ];
608
+ await renderPage();
609
+
610
+ fireEvent.click(screen.getByTestId("verify-email-try-again"));
611
+ await settle();
612
+
613
+ expect(
614
+ screen.queryByTestId("verify-email-error"),
615
+ ).not.toBeInTheDocument();
616
+ expect(
617
+ screen.queryByTestId("verify-email-success"),
618
+ ).not.toBeInTheDocument();
619
+
620
+ await act(async () => {
621
+ finishRetry();
622
+ });
623
+ await settle();
624
+
625
+ expect(screen.getByTestId("verify-email-success")).toBeInTheDocument();
626
+ });
627
+
628
+ test("a retry that fails again stays on the error, ready for another try", async () => {
629
+ verifyAnswers = [networkFailure];
630
+ await renderPage();
631
+
632
+ fireEvent.click(screen.getByTestId("verify-email-try-again"));
633
+ await settle();
634
+
635
+ expect(createOrUpdate).toHaveBeenCalledTimes(2);
636
+ expect(heading()).toHaveTextContent(ERROR_TITLE);
637
+ expect(screen.getByTestId("verify-email-try-again")).toBeEnabled();
638
+ });
639
+
640
+ test("a retry that the server now turns down becomes a link problem", async () => {
641
+ verifyAnswers = [
642
+ refuse(503, "Service Unavailable"),
643
+ refuse(400, LINK_EXPIRED),
644
+ ];
645
+ await renderPage();
646
+
647
+ fireEvent.click(screen.getByTestId("verify-email-try-again"));
648
+ await settle();
649
+
650
+ expect(heading()).toHaveTextContent(LINK_INVALID_TITLE);
651
+ expect(screen.getByTestId("resend-verification-email")).toBeEnabled();
652
+ expect(
653
+ screen.queryByTestId("verify-email-try-again"),
654
+ ).not.toBeInTheDocument();
655
+ });
656
+ });
657
+
658
+ /*
659
+ * The card takes its direction from the language, not a hard-coded "ltr":
660
+ * in Persian every view of it -- success, a link problem, a server that did
661
+ * not answer -- must lay out right-to-left and speak Persian.
662
+ */
663
+ describe("in a right-to-left language", () => {
664
+ test("the success card is right-to-left, in Persian", async () => {
665
+ await switchToPersian();
666
+ await renderPage();
667
+
668
+ expect(cardAround("verify-email-success")).toHaveAttribute("dir", "rtl");
669
+
670
+ const title: string = persian("verifyEmail.successTitle", SUCCESS_TITLE);
671
+ expect(heading()).toHaveTextContent(title);
672
+ expect(heading()).not.toHaveTextContent(SUCCESS_TITLE);
673
+ expect(screen.getByTestId("verify-email-success")).toHaveTextContent(
674
+ persian("verifyEmail.successDescription", SUCCESS_DESCRIPTION),
675
+ );
676
+ expect(
677
+ screen.getByRole("link", {
678
+ name: persian("verifyEmail.continueToSignIn", CONTINUE_TO_SIGN_IN),
679
+ }),
680
+ ).toHaveAttribute("href", "/accounts/login");
681
+ });
682
+
683
+ test("the card for a link that was turned down is right-to-left, in Persian", async () => {
684
+ verifyAnswers = [refuse(400, LINK_EXPIRED)];
685
+ await switchToPersian();
686
+ await renderPage();
687
+
688
+ expect(cardAround("verify-email-error")).toHaveAttribute("dir", "rtl");
689
+
690
+ const title: string = persian(
691
+ "verifyEmail.linkInvalidTitle",
692
+ LINK_INVALID_TITLE,
693
+ );
694
+ expect(heading()).toHaveTextContent(title);
695
+ expect(heading()).not.toHaveTextContent(LINK_INVALID_TITLE);
696
+ expect(errorBlock()).toHaveTextContent(
697
+ persian("verifyEmail.requestNewLinkDescription", REQUEST_NEW_LINK),
698
+ );
699
+
700
+ const resend: HTMLElement = screen.getByTestId(
701
+ "resend-verification-email",
702
+ );
703
+ expect(errorBlock()).toContainElement(resend);
704
+ expect(resend).toHaveAccessibleName(
705
+ persian("resendVerificationEmail.button", RESEND_BUTTON),
706
+ );
707
+
708
+ expect(
709
+ screen.getByRole("link", {
710
+ name: persian("verifyEmail.loginLink", LOGIN_LINK),
711
+ }),
712
+ ).toHaveAttribute("href", "/accounts/login");
713
+ });
714
+
715
+ test("the card for a link that could never work is right-to-left, in Persian", async () => {
716
+ linkToken = MALFORMED_TOKEN;
717
+ verifyAnswers = [refuse(400, LINK_INVALID)];
718
+ await switchToPersian();
719
+ await renderPage();
720
+
721
+ expect(cardAround("verify-email-error")).toHaveAttribute("dir", "rtl");
722
+ expect(heading()).toHaveTextContent(
723
+ persian("verifyEmail.linkInvalidTitle", LINK_INVALID_TITLE),
724
+ );
725
+ expect(
726
+ screen.queryByTestId("resend-verification-email"),
727
+ ).not.toBeInTheDocument();
728
+ });
729
+
730
+ test("the card for a server that did not answer is right-to-left, in Persian", async () => {
731
+ verifyAnswers = [refuse(500, "Server Error")];
732
+ await switchToPersian();
733
+ await renderPage();
734
+
735
+ expect(cardAround("verify-email-error")).toHaveAttribute("dir", "rtl");
736
+
737
+ const title: string = persian("verifyEmail.errorTitle", ERROR_TITLE);
738
+ expect(heading()).toHaveTextContent(title);
739
+ expect(heading()).not.toHaveTextContent(ERROR_TITLE);
740
+
741
+ const retry: HTMLElement = screen.getByTestId("verify-email-try-again");
742
+ expect(errorBlock()).toContainElement(retry);
743
+ expect(retry).toHaveAccessibleName(
744
+ persian("verifyEmail.tryAgain", TRY_AGAIN),
745
+ );
746
+ });
747
+
748
+ test("a card that flips to success after a resend stays right-to-left", async () => {
749
+ verifyAnswers = [refuse(400, LINK_EXPIRED)];
750
+ resendAnswer = async (): Promise<HTTPResponse<JSONObject>> => {
751
+ return new HTTPResponse<JSONObject>(
752
+ 200,
753
+ { emailSent: false, alreadyVerified: true, retryAfterSeconds: 0 },
754
+ {},
755
+ );
756
+ };
757
+ await switchToPersian();
758
+ await renderPage();
759
+
760
+ fireEvent.click(screen.getByTestId("resend-verification-email"));
761
+ await settle();
762
+
763
+ expect(cardAround("verify-email-success")).toHaveAttribute("dir", "rtl");
764
+ expect(heading()).toHaveTextContent(
765
+ persian("verifyEmail.successTitle", SUCCESS_TITLE),
766
+ );
767
+ expect(heading()).toHaveFocus();
768
+ });
769
+
770
+ test("English is back for the next test", async () => {
771
+ verifyAnswers = [refuse(400, LINK_EXPIRED)];
772
+ await renderPage();
773
+
774
+ expect(i18n.language).toBe("en");
775
+ expect(heading()).toHaveTextContent(LINK_INVALID_TITLE);
776
+ expect(cardAround("verify-email-error")).toHaveAttribute("dir", "ltr");
777
+ });
778
+ });
779
+ });