@oneuptime/common 14.0.6 → 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.
- package/Models/DatabaseModels/Incident.ts +267 -1
- package/Models/DatabaseModels/IncidentCustomField.ts +202 -0
- package/Models/DatabaseModels/IncidentEpisode.ts +30 -0
- package/Models/DatabaseModels/IncidentTemplate.ts +126 -1
- package/Models/DatabaseModels/InventoryItem.ts +23 -12
- package/Models/DatabaseModels/StatusPage.ts +55 -0
- package/Models/DatabaseModels/StatusPageSubscriber.ts +128 -0
- package/Server/API/IncidentAPI.ts +162 -0
- package/Server/API/StatusPageAPI.ts +487 -171
- package/Server/API/StatusPageSubscriberAPI.ts +63 -15
- package/Server/Infrastructure/Postgres/SchemaMigrations/1795200000000-AddInventoryItemProjectEntityKeyIndex.ts +217 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1795300000000-SkipHiddenIncidentCreatedNotifications.ts +49 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1795400000000-AddIncidentStatusPageScope.ts +108 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1795500000000-AddIncidentTemplateStatusPageScopeFlag.ts +33 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1795600000000-AddStatusPageSubscriberUnsubscribeToken.ts +51 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1795700000000-AddStatusPageSubscriberIsAddedByTeam.ts +33 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1795800000000-AddIncidentCustomFieldCreateAndNotificationSettings.ts +168 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1795900000000-AddSubscriberNotificationClaimedAt.ts +44 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +16 -0
- package/Server/Infrastructure/Semaphore.ts +10 -0
- package/Server/Middleware/IdentityRateLimit.ts +108 -3
- package/Server/Middleware/SubscriberNotificationPreviewRateLimit.ts +237 -0
- package/Server/Middleware/SubscriberNotificationTestSendRateLimit.ts +304 -0
- package/Server/Services/AlertService.ts +32 -36
- package/Server/Services/DatabaseService.ts +93 -10
- package/Server/Services/IncidentAlertService.ts +609 -2
- package/Server/Services/IncidentCustomFieldService.ts +276 -1
- package/Server/Services/IncidentEpisodePublicNoteService.ts +28 -0
- package/Server/Services/IncidentEpisodeService.ts +15 -0
- package/Server/Services/IncidentEpisodeStateTimelineService.ts +27 -1
- package/Server/Services/IncidentPublicNoteService.ts +30 -1
- package/Server/Services/IncidentService.ts +1663 -103
- package/Server/Services/IncidentStateTimelineService.ts +13 -0
- package/Server/Services/IncidentTemplateService.ts +109 -0
- package/Server/Services/InventoryItemService.ts +38 -0
- package/Server/Services/IoTDeviceCredentialService.ts +13 -4
- package/Server/Services/ScheduledMaintenancePublicNoteService.ts +30 -1
- package/Server/Services/ScheduledMaintenanceService.ts +118 -64
- package/Server/Services/SmsService.ts +10 -0
- package/Server/Services/StatusPageService.ts +65 -8
- package/Server/Services/StatusPageSubscriberNotificationTemplateService.ts +299 -37
- package/Server/Services/StatusPageSubscriberNotificationTemplateStatusPageService.ts +129 -0
- package/Server/Services/StatusPageSubscriberService.ts +1161 -64
- package/Server/Services/UserNotificationRuleService.ts +55 -53
- package/Server/Services/WorkspaceNotificationSummaryService.ts +38 -20
- package/Server/Types/Markdown.ts +153 -6
- package/Server/Types/Workflow/Components/Email.ts +61 -2
- package/Server/Utils/AffectedResources/LinkedAffectedResources.ts +576 -0
- package/Server/Utils/Alert/AlertStateChangeAuthorization.ts +129 -0
- package/Server/Utils/CustomField/CustomFieldRename.ts +378 -0
- package/Server/Utils/DataSource/EgressGuard.ts +66 -21
- package/Server/Utils/Inventory/InventoryOverviewAggregation.ts +111 -0
- package/Server/Utils/Mail/PinnedSmtpSocket.ts +130 -0
- package/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.ts +10 -0
- package/Server/Utils/Response.ts +31 -0
- package/Server/Utils/SSRFProtection.ts +76 -16
- package/Server/Utils/StatusPage/IncidentStatusPageScope.ts +948 -0
- package/Server/Utils/StatusPage/IncidentSubscriberAudienceBuilder.ts +656 -0
- package/Server/Utils/StatusPage/IncidentTemplateVariableBuilder.ts +805 -0
- package/Server/Utils/StatusPage/StatusPageExclusion.ts +18 -0
- package/Server/Utils/StatusPage/StatusPageReadAccess.ts +125 -0
- package/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeNotice.ts +151 -0
- package/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeToken.ts +70 -0
- package/Server/Utils/StatusPage/SubscriberIncidentEmailBuilder.ts +421 -0
- package/Server/Utils/StatusPage/SubscriberNotificationClaim.ts +107 -0
- package/Server/Utils/StatusPage/SubscriberNotificationDeliveryRecord.ts +1049 -0
- package/Server/Utils/StatusPage/SubscriberNotificationFanOut.ts +180 -0
- package/Server/Utils/StatusPage/SubscriberNotificationPreviewBuilder.ts +839 -0
- package/Server/Utils/StatusPage/SubscriberNotificationResendAccess.ts +393 -0
- package/Server/Utils/StatusPage/SubscriberNotificationRunLimit.ts +246 -0
- package/Server/Utils/StatusPage/SubscriberNotificationTiming.ts +133 -0
- package/Server/Utils/StatusPage/SubscriberTemplateIncidentRecordAccess.ts +215 -0
- package/Server/Utils/StatusPageResource.ts +51 -20
- package/Server/Utils/Topology/TopologyConcurrencyLimiter.ts +229 -0
- package/Server/Utils/Topology/TopologyQueries.ts +1350 -0
- package/Server/Utils/Topology/TopologyRequest.ts +463 -0
- package/Server/Utils/Topology/TopologyResponseCache.ts +183 -0
- package/Server/Utils/Topology/TopologySql.ts +1024 -0
- package/Server/Utils/VM/VMAPI.ts +1 -0
- package/Server/Utils/VM/VMRunner.ts +10 -0
- package/Server/Utils/Workflow/OAuth2TokenClient.ts +10 -0
- package/Server/Views/Partials/SensitiveUrlToken.ejs +8 -5
- package/Tests/App/Accounts/RegisterVerifyEmail.test.tsx +748 -0
- package/Tests/App/Accounts/ResendVerificationEmail.test.tsx +1222 -0
- package/Tests/App/Accounts/VerificationEmailResendUtils.test.ts +797 -0
- package/Tests/App/Accounts/VerifyEmailPending.test.tsx +538 -0
- package/Tests/App/Accounts/VerifyEmailResend.test.tsx +779 -0
- package/Tests/App/Dashboard/AcknowledgeAlertsOnDeclare.test.ts +369 -0
- package/Tests/App/Dashboard/AffectedResourcesSlo.test.tsx +1 -1
- package/Tests/App/Dashboard/AffectedResourcesSummaryStep.test.tsx +510 -123
- package/Tests/App/Dashboard/AlertsTableAffectedResources.test.tsx +490 -0
- package/Tests/App/Dashboard/ChangeAlertStateDeclareIncident.test.tsx +886 -0
- package/Tests/App/Dashboard/CreateReviewStepRoleSummary.test.tsx +1250 -0
- package/Tests/App/Dashboard/CreateReviewStepSelectionSummary.test.tsx +738 -0
- package/Tests/App/Dashboard/CustomFieldsSettingsIncidentOptions.test.tsx +357 -0
- package/Tests/App/Dashboard/DeclareIncidentFromAlert.test.ts +417 -0
- package/Tests/App/Dashboard/EntityDetailPanel.test.tsx +1948 -229
- package/Tests/App/Dashboard/EntityDetailPanelLifecycle.test.tsx +493 -0
- package/Tests/App/Dashboard/EntityDetailPanelNavigation.test.tsx +439 -0
- package/Tests/App/Dashboard/EventNotes.test.tsx +776 -0
- package/Tests/App/Dashboard/EventNotesPages.test.tsx +119 -0
- package/Tests/App/Dashboard/EventNotesUtil.test.ts +116 -17
- package/Tests/App/Dashboard/EventOverviewPages.test.tsx +534 -6
- package/Tests/App/Dashboard/EventStatusPanel.test.tsx +798 -2
- package/Tests/App/Dashboard/FetchStateAndSeverityElements.test.tsx +669 -0
- package/Tests/App/Dashboard/FetchUsers.test.tsx +230 -0
- package/Tests/App/Dashboard/IncidentChangeStateNotifyDefault.test.tsx +91 -1
- package/Tests/App/Dashboard/IncidentCreateAcknowledgeAlerts.test.tsx +1611 -0
- package/Tests/App/Dashboard/IncidentCreateDetailsStep.test.tsx +643 -0
- package/Tests/App/Dashboard/IncidentCreateFromAlerts.test.tsx +8 -1
- package/Tests/App/Dashboard/IncidentCreateStatusPageScope.test.tsx +601 -0
- package/Tests/App/Dashboard/IncidentCustomFieldTemplateVariables.test.tsx +386 -0
- package/Tests/App/Dashboard/IncidentNoteTemplatePlaceholders.test.tsx +124 -0
- package/Tests/App/Dashboard/IncidentNoteTemplateVariablesLoader.test.ts +305 -0
- package/Tests/App/Dashboard/IncidentOverviewNotifyDefault.test.tsx +112 -0
- package/Tests/App/Dashboard/IncidentSettingsRenotifyOnPublish.test.tsx +674 -0
- package/Tests/App/Dashboard/IncidentSettingsStatusPageScope.test.tsx +1051 -0
- package/Tests/App/Dashboard/IncidentTemplateCustomFields.test.tsx +384 -0
- package/Tests/App/Dashboard/IncidentTemplateStatusPageScopeCard.test.tsx +215 -0
- package/Tests/App/Dashboard/InfrastructureExplorer.test.tsx +1365 -57
- package/Tests/App/Dashboard/InfrastructureGraph.test.tsx +964 -23
- package/Tests/App/Dashboard/InventoryRelationships.test.tsx +885 -0
- package/Tests/App/Dashboard/PublicNotePagesNotifyDefault.test.tsx +113 -1
- package/Tests/App/Dashboard/ServiceMapDatabaseLink.test.tsx +201 -15
- package/Tests/App/Dashboard/ServiceMapGraph.test.tsx +508 -36
- package/Tests/App/Dashboard/SubscriberAudienceSummary.test.tsx +512 -0
- package/Tests/App/Dashboard/SubscriberNotificationPreviewModal.test.tsx +837 -0
- package/Tests/App/Dashboard/SubscriberNotificationStatusLabel.test.tsx +88 -0
- package/Tests/App/Dashboard/SubscriberNotificationStatusResend.test.tsx +473 -0
- package/Tests/App/Dashboard/SubscriberTemplateLivePreview.test.tsx +196 -0
- package/Tests/App/Dashboard/TeamAddedSubscribersUnsubscribedNotice.test.tsx +248 -0
- package/Tests/App/Dashboard/TopologyDataLifecycle.test.tsx +583 -0
- package/Tests/App/Dashboard/TopologyDataLoading.test.tsx +982 -218
- package/Tests/App/Dashboard/TopologyPageNavigation.test.tsx +818 -114
- package/Tests/App/SensitiveUrlTokenBootstrap.test.ts +64 -3
- package/Tests/App/StatusPage/StatusPageUnsubscribePage.test.tsx +412 -0
- package/Tests/Models/IncidentCustomFieldApiSchemaContract.test.ts +217 -0
- package/Tests/Models/IncidentCustomFieldCreateSettingsColumns.test.ts +252 -0
- package/Tests/Models/StatusPageSubscriberUnsubscribeColumns.test.ts +218 -0
- package/Tests/Server/API/ExpiredSessionRouteGuards.test.ts +7 -0
- package/Tests/Server/API/IncidentSubscriberAudienceAPI.test.ts +1408 -0
- package/Tests/Server/API/StatusPageIncidentScope.test.ts +1868 -0
- package/Tests/Server/API/StatusPageIncidentScopePostgres.test.ts +790 -0
- package/Tests/Server/API/StatusPageManageSubscriptionRecipient.test.ts +10 -1
- package/Tests/Server/API/StatusPageManageSubscriptionTemplateVariables.test.ts +206 -18
- package/Tests/Server/API/StatusPagePostmortemVisibility.test.ts +14 -2
- package/Tests/Server/API/StatusPageSubscriberCreateResponse.test.ts +217 -0
- package/Tests/Server/API/StatusPageSubscriberOwnership.test.ts +103 -2
- package/Tests/Server/API/StatusPageUnsubscribeAPI.test.ts +396 -0
- package/Tests/Server/Infrastructure/Postgres/AddIncidentStatusPageScopeMigration.test.ts +395 -0
- package/Tests/Server/Infrastructure/Postgres/AddIncidentTemplateStatusPageScopeFlagMigration.test.ts +150 -0
- package/Tests/Server/Infrastructure/Postgres/AddSubscriberNotificationClaimedAtMigration.test.ts +156 -0
- package/Tests/Server/Infrastructure/Postgres/RenameInventoryItemMigration.test.ts +90 -21
- package/Tests/Server/Infrastructure/Postgres/SkipHiddenIncidentCreatedNotificationsMigration.test.ts +410 -0
- package/Tests/Server/Middleware/IdentityRateLimit.test.ts +551 -1
- package/Tests/Server/Middleware/SubscriberNotificationPreviewRateLimit.test.ts +238 -0
- package/Tests/Server/Middleware/SubscriberNotificationTestSendRateLimit.test.ts +245 -0
- package/Tests/Server/Services/AddStatusPageSubscriberUnsubscribeTokenMigration.test.ts +152 -0
- package/Tests/Server/Services/CrossProjectRelationWriteGuard.test.ts +221 -36
- package/Tests/Server/Services/DatabaseServiceCompareAndSetColumns.test.ts +191 -0
- package/Tests/Server/Services/DomainService.test.ts +525 -0
- package/Tests/Server/Services/IncidentAlertAcknowledgeOnDeclare.test.ts +2630 -0
- package/Tests/Server/Services/IncidentAlertPostgres.test.ts +1625 -1
- package/Tests/Server/Services/IncidentCreatedRenotifyOnPublish.test.ts +681 -0
- package/Tests/Server/Services/IncidentCreatedResend.test.ts +775 -0
- package/Tests/Server/Services/IncidentCustomFieldRenamePostgres.test.ts +713 -0
- package/Tests/Server/Services/IncidentCustomFieldService.test.ts +751 -0
- package/Tests/Server/Services/IncidentCustomFieldValueValidation.test.ts +556 -0
- package/Tests/Server/Services/IncidentDeclareAcknowledgeAlerts.test.ts +2189 -0
- package/Tests/Server/Services/IncidentStatusPageScope.test.ts +2971 -0
- package/Tests/Server/Services/IncidentStatusPageScopeFeed.test.ts +131 -0
- package/Tests/Server/Services/IncidentStatusPageScopePostgres.test.ts +625 -0
- package/Tests/Server/Services/IncidentTemplateCustomFieldsCopy.test.ts +311 -0
- package/Tests/Server/Services/IncidentTemplateStatusPageScope.test.ts +363 -0
- package/Tests/Server/Services/InventoryItemServiceOverviewCounts.test.ts +345 -0
- package/Tests/Server/Services/InventoryOverviewPostgres.test.ts +254 -0
- package/Tests/Server/Services/IoTDeviceCredentialService.test.ts +819 -0
- package/Tests/Server/Services/LinkedAffectedResourcesCreatedFeed.test.ts +420 -0
- package/Tests/Server/Services/PublicNoteEditFeedEventType.test.ts +213 -0
- package/Tests/Server/Services/PublicNoteResendAccess.test.ts +994 -0
- package/Tests/Server/Services/ScheduledMaintenanceSubscriberTemplateFormats.test.ts +289 -9
- package/Tests/Server/Services/SloAffectedResourceCreatedFeed.test.ts +92 -36
- package/Tests/Server/Services/SmsServiceFailIfNotSent.test.ts +67 -0
- package/Tests/Server/Services/StatusPageReportIncidentCounts.test.ts +610 -0
- package/Tests/Server/Services/StatusPageSubscriberAudienceCounts.test.ts +201 -0
- package/Tests/Server/Services/StatusPageSubscriberAudienceCountsPostgres.test.ts +271 -0
- package/Tests/Server/Services/StatusPageSubscriberConfirmationTemplateEscaping.test.ts +200 -0
- package/Tests/Server/Services/StatusPageSubscriberNotificationLogRedaction.test.ts +54 -0
- package/Tests/Server/Services/StatusPageSubscriberNotificationTemplateCompile.test.ts +37 -0
- package/Tests/Server/Services/StatusPageSubscriberNotificationTemplateIncidentRecordAccess.test.ts +772 -0
- package/Tests/Server/Services/StatusPageSubscriberNotificationTemplateLookup.test.ts +506 -0
- package/Tests/Server/Services/StatusPageSubscriberNotificationTemplateLookupPostgres.test.ts +261 -0
- package/Tests/Server/Services/StatusPageSubscriberReport.test.ts +21 -5
- package/Tests/Server/Services/StatusPageSubscriberUnsubscribe.test.ts +1618 -0
- package/Tests/Server/Services/StatusPageSubscriberUnsubscribeLinkSenders.test.ts +373 -0
- package/Tests/Server/Services/StatusPageSubscriberUnsubscribePostgres.test.ts +388 -0
- package/Tests/Server/Services/SubscriberNotificationInFlightRequeue.test.ts +500 -0
- package/Tests/Server/Types/MarkdownEmailLinks.test.ts +320 -0
- package/Tests/Server/Types/Workflow/Components/Email.test.ts +491 -1
- package/Tests/Server/Types/Workflow/Components/EmailSmtpPinning.test.ts +398 -0
- package/Tests/Server/Utils/AffectedResources/LinkedAffectedResources.test.ts +858 -0
- package/Tests/Server/Utils/Alert/AlertStateChangeAuthorization.test.ts +1172 -0
- package/Tests/Server/Utils/CustomField/CustomFieldRename.test.ts +586 -0
- package/Tests/Server/Utils/DataSource/EgressGuard.test.ts +246 -10
- package/Tests/Server/Utils/DataSource/EgressGuardDeploymentConfig.test.ts +226 -0
- package/Tests/Server/Utils/DataSource/EgressGuardPinning.test.ts +131 -0
- package/Tests/Server/Utils/DataSource/HttpFetch.test.ts +4 -0
- package/Tests/Server/Utils/EgressPolicyEnvironment.ts +45 -0
- package/Tests/Server/Utils/Mail/PinnedSmtpSocket.test.ts +409 -0
- package/Tests/Server/Utils/Monitor/MonitorCriteriaObservationBuilderUnits.test.ts +23 -0
- package/Tests/Server/Utils/SSRFProtection.test.ts +4 -0
- package/Tests/Server/Utils/SSRFProtectionCloudServiceAddresses.test.ts +4 -0
- package/Tests/Server/Utils/SSRFProtectionPrivateNetwork.test.ts +4 -0
- package/Tests/Server/Utils/SSRFProtectionResolutionDetail.test.ts +314 -0
- package/Tests/Server/Utils/StatusPage/IncidentStatusPageScope.test.ts +1699 -0
- package/Tests/Server/Utils/StatusPage/IncidentStatusPageScopeQueriesPostgres.test.ts +508 -0
- package/Tests/Server/Utils/StatusPage/IncidentSubscriberAudienceCallerPermission.test.ts +246 -0
- package/Tests/Server/Utils/StatusPage/IncidentTemplateVariableBuilder.test.ts +1348 -0
- package/Tests/Server/Utils/StatusPage/StatusPageReadAccess.test.ts +221 -0
- package/Tests/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeNotice.test.ts +132 -0
- package/Tests/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeToken.test.ts +155 -0
- package/Tests/Server/Utils/StatusPage/SubscriberIncidentEmailBuilder.test.ts +708 -0
- package/Tests/Server/Utils/StatusPage/SubscriberNotificationClaim.test.ts +255 -0
- package/Tests/Server/Utils/StatusPage/SubscriberNotificationClaimPostgres.test.ts +528 -0
- package/Tests/Server/Utils/StatusPage/SubscriberNotificationDeliveryRecord.test.ts +1238 -0
- package/Tests/Server/Utils/StatusPage/SubscriberNotificationFanOut.test.ts +472 -0
- package/Tests/Server/Utils/StatusPage/SubscriberNotificationPreviewBuilder.test.ts +1168 -0
- package/Tests/Server/Utils/StatusPage/SubscriberNotificationRunLimit.test.ts +275 -0
- package/Tests/Server/Utils/StatusPage/SubscriberNotificationTiming.test.ts +131 -0
- package/Tests/Server/Utils/StatusPageResource.test.ts +98 -0
- package/Tests/Server/Utils/Topology/InventoryItemEntityKeyIndexMigration.test.ts +386 -0
- package/Tests/Server/Utils/Topology/InventoryItemEntityKeyIndexMigrationPostgres.test.ts +430 -0
- package/Tests/Server/Utils/Topology/TopologyActivityParityPostgres.test.ts +279 -0
- package/Tests/Server/Utils/Topology/TopologyConcurrencyLimiter.test.ts +534 -0
- package/Tests/Server/Utils/Topology/TopologyEstateGenerator.ts +624 -0
- package/Tests/Server/Utils/Topology/TopologyJsonResponse.test.ts +90 -0
- package/Tests/Server/Utils/Topology/TopologyQueries.test.ts +1634 -0
- package/Tests/Server/Utils/Topology/TopologyQueriesDifferentialPostgres.test.ts +683 -0
- package/Tests/Server/Utils/Topology/TopologyQueriesPostgres.test.ts +2624 -0
- package/Tests/Server/Utils/Topology/TopologyReference.ts +1018 -0
- package/Tests/Server/Utils/Topology/TopologyRequest.test.ts +643 -0
- package/Tests/Server/Utils/Topology/TopologyResponseCache.test.ts +305 -0
- package/Tests/Server/Utils/Topology/TopologySql.test.ts +973 -0
- package/Tests/Server/Utils/VM/VMRunnerPrivateNetworkWiring.test.ts +17 -0
- package/Tests/Server/Utils/VM/VMRunnerSsrf.test.ts +75 -0
- package/Tests/Types/CustomField/CustomFieldSavedViews.test.ts +247 -0
- package/Tests/Types/CustomField/CustomFieldTemplateValues.test.ts +186 -0
- package/Tests/Types/CustomField/CustomFieldTypeCorrespondence.test.ts +48 -0
- package/Tests/Types/CustomField/CustomFieldValueFormat.test.ts +215 -0
- package/Tests/Types/CustomField/CustomFieldValueValidator.test.ts +343 -0
- package/Tests/Types/CustomField/CustomFieldVariableKey.test.ts +364 -0
- package/Tests/Types/SafeHtml.test.ts +102 -0
- package/Tests/Types/StatusPage/IncidentCreatedRenotify.test.ts +419 -0
- package/Tests/Types/StatusPage/IncidentCreatedResend.test.ts +118 -0
- package/Tests/Types/StatusPage/IncidentScopeAddedPagesNotification.test.ts +558 -0
- package/Tests/Types/StatusPage/IncidentSubscriberAudience.test.ts +284 -0
- package/Tests/Types/StatusPage/StatusPageSubscriberUnsubscribe.test.ts +341 -0
- package/Tests/Types/StatusPage/SubscriberNotificationPreview.test.ts +196 -0
- package/Tests/Types/StatusPage/SubscriberNotificationResend.test.ts +201 -0
- package/Tests/Types/StatusPage/SubscriberNotificationTemplateCompiler.test.ts +277 -0
- package/Tests/Types/StatusPage/SubscriberNotificationTemplatePreview.test.ts +179 -0
- package/Tests/Types/StatusPage/SubscriberNotificationTemplateVariables.test.ts +324 -0
- package/Tests/Types/StatusPage/SubscriberUpdateNotification.test.ts +60 -13
- package/Tests/Types/Topology/TopologyTypeRules.test.ts +176 -0
- package/Tests/UI/Components/CustomFields/CustomFieldFormFields.test.ts +432 -0
- package/Tests/UI/Components/CustomFields/CustomFieldModelFormFields.test.tsx +685 -0
- package/Tests/UI/Components/CustomFields/CustomFieldsDetailOrder.test.tsx +469 -0
- package/Tests/UI/Components/Forms/ModelFormCustomFieldInputs.test.tsx +325 -0
- package/Tests/UI/Components/Forms/ModelFormDropdownOptionsRefused.test.tsx +358 -0
- package/Tests/UI/Components/ModelTable/CustomFieldColumns.test.tsx +22 -0
- package/Tests/UI/Monitor/NetworkPathView.test.tsx +289 -0
- package/Tests/Utils/Incident/IncidentFromAlerts.test.ts +302 -0
- package/Tests/Utils/Incident/IncidentNoteTemplateVariables.test.ts +623 -0
- package/Tests/Utils/Markdown/MarkdownEscape.test.ts +89 -0
- package/Tests/Utils/ProbeNetworkFailureUtil.test.ts +992 -0
- package/Tests/Utils/RequestFailedDetails.test.ts +575 -0
- package/Types/CustomField/CustomFieldOrder.ts +55 -0
- package/Types/CustomField/CustomFieldSavedViews.ts +225 -0
- package/Types/CustomField/CustomFieldTemplateMerge.ts +71 -0
- package/Types/CustomField/CustomFieldType.ts +14 -0
- package/Types/CustomField/CustomFieldValueFormat.ts +161 -0
- package/Types/CustomField/CustomFieldValueValidator.ts +488 -0
- package/Types/CustomField/CustomFieldVariableKey.ts +211 -0
- package/Types/Incident/IncidentAlertLink.ts +19 -0
- package/Types/SafeHtml.ts +93 -0
- package/Types/StatusPage/IncidentCreatedRenotify.ts +224 -0
- package/Types/StatusPage/IncidentCreatedResend.ts +108 -0
- package/Types/StatusPage/IncidentScopeAddedPagesNotification.ts +343 -0
- package/Types/StatusPage/IncidentSubscriberAudience.ts +332 -0
- package/Types/StatusPage/StatusPageSubscriberUnsubscribe.ts +405 -0
- package/Types/StatusPage/SubscriberNotificationInterruption.ts +32 -0
- package/Types/StatusPage/SubscriberNotificationPreview.ts +322 -0
- package/Types/StatusPage/SubscriberNotificationResend.ts +132 -0
- package/Types/StatusPage/SubscriberNotificationTemplateCompiler.ts +180 -0
- package/Types/StatusPage/SubscriberNotificationTemplatePreview.ts +301 -0
- package/Types/StatusPage/SubscriberNotificationTemplateVariables.ts +217 -4
- package/Types/StatusPage/SubscriberUpdateNotification.ts +32 -11
- package/Types/Telemetry/InventoryLiveness.ts +26 -0
- package/Types/Topology/TopologyApi.ts +428 -0
- package/Types/Topology/TopologyTypeRules.ts +191 -0
- package/UI/Components/CustomFields/CustomFieldFormFields.ts +317 -0
- package/UI/Components/CustomFields/CustomFieldModelFormFields.tsx +395 -0
- package/UI/Components/CustomFields/CustomFieldsDetail.tsx +95 -55
- package/UI/Components/Forms/ModelForm.tsx +70 -8
- package/UI/Components/Forms/Validation.ts +1 -1
- package/UI/Components/ModelFormModal/ModelFormModal.tsx +2 -4
- package/UI/Components/ModelTable/AnalyticsModelTable.tsx +2 -6
- package/UI/Components/ModelTable/BaseModelTable.tsx +9 -8
- package/UI/Components/ModelTable/CustomFieldColumns.tsx +14 -1
- package/UI/Components/ModelTable/ModelTable.tsx +6 -7
- package/UI/Utils/SensitiveUrlToken.ts +10 -5
- package/Utils/API.ts +247 -2
- package/Utils/Incident/IncidentFromAlerts.ts +78 -0
- package/Utils/Incident/IncidentNoteTemplateVariables.ts +304 -0
- package/Utils/Markdown/MarkdownEscape.ts +65 -0
- package/Utils/ProbeNetworkFailureUtil.ts +673 -0
- package/build/dist/Models/DatabaseModels/Incident.js +267 -1
- package/build/dist/Models/DatabaseModels/Incident.js.map +1 -1
- package/build/dist/Models/DatabaseModels/IncidentCustomField.js +207 -1
- package/build/dist/Models/DatabaseModels/IncidentCustomField.js.map +1 -1
- package/build/dist/Models/DatabaseModels/IncidentEpisode.js +31 -0
- package/build/dist/Models/DatabaseModels/IncidentEpisode.js.map +1 -1
- package/build/dist/Models/DatabaseModels/IncidentTemplate.js +124 -1
- package/build/dist/Models/DatabaseModels/IncidentTemplate.js.map +1 -1
- package/build/dist/Models/DatabaseModels/InventoryItem.js +20 -11
- package/build/dist/Models/DatabaseModels/InventoryItem.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPage.js +56 -0
- package/build/dist/Models/DatabaseModels/StatusPage.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPageSubscriber.js +131 -0
- package/build/dist/Models/DatabaseModels/StatusPageSubscriber.js.map +1 -1
- package/build/dist/Server/API/IncidentAPI.js +100 -1
- package/build/dist/Server/API/IncidentAPI.js.map +1 -1
- package/build/dist/Server/API/StatusPageAPI.js +417 -176
- package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
- package/build/dist/Server/API/StatusPageSubscriberAPI.js +52 -13
- package/build/dist/Server/API/StatusPageSubscriberAPI.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795200000000-AddInventoryItemProjectEntityKeyIndex.js +165 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795200000000-AddInventoryItemProjectEntityKeyIndex.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795300000000-SkipHiddenIncidentCreatedNotifications.js +41 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795300000000-SkipHiddenIncidentCreatedNotifications.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795400000000-AddIncidentStatusPageScope.js +57 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795400000000-AddIncidentStatusPageScope.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795500000000-AddIncidentTemplateStatusPageScopeFlag.js +23 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795500000000-AddIncidentTemplateStatusPageScopeFlag.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795600000000-AddStatusPageSubscriberUnsubscribeToken.js +40 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795600000000-AddStatusPageSubscriberUnsubscribeToken.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795700000000-AddStatusPageSubscriberIsAddedByTeam.js +26 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795700000000-AddStatusPageSubscriberIsAddedByTeam.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795800000000-AddIncidentCustomFieldCreateAndNotificationSettings.js +89 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795800000000-AddIncidentCustomFieldCreateAndNotificationSettings.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795900000000-AddSubscriberNotificationClaimedAt.js +29 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795900000000-AddSubscriberNotificationClaimedAt.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +16 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Infrastructure/Semaphore.js +3 -0
- package/build/dist/Server/Infrastructure/Semaphore.js.map +1 -1
- package/build/dist/Server/Middleware/IdentityRateLimit.js +89 -3
- package/build/dist/Server/Middleware/IdentityRateLimit.js.map +1 -1
- package/build/dist/Server/Middleware/SubscriberNotificationPreviewRateLimit.js +149 -0
- package/build/dist/Server/Middleware/SubscriberNotificationPreviewRateLimit.js.map +1 -0
- package/build/dist/Server/Middleware/SubscriberNotificationTestSendRateLimit.js +190 -0
- package/build/dist/Server/Middleware/SubscriberNotificationTestSendRateLimit.js.map +1 -0
- package/build/dist/Server/Services/AlertService.js +21 -33
- package/build/dist/Server/Services/AlertService.js.map +1 -1
- package/build/dist/Server/Services/DatabaseService.js +56 -7
- package/build/dist/Server/Services/DatabaseService.js.map +1 -1
- package/build/dist/Server/Services/IncidentAlertService.js +457 -3
- package/build/dist/Server/Services/IncidentAlertService.js.map +1 -1
- package/build/dist/Server/Services/IncidentCustomFieldService.js +204 -8
- package/build/dist/Server/Services/IncidentCustomFieldService.js.map +1 -1
- package/build/dist/Server/Services/IncidentEpisodePublicNoteService.js +24 -0
- package/build/dist/Server/Services/IncidentEpisodePublicNoteService.js.map +1 -1
- package/build/dist/Server/Services/IncidentEpisodeService.js +14 -0
- package/build/dist/Server/Services/IncidentEpisodeService.js.map +1 -1
- package/build/dist/Server/Services/IncidentEpisodeStateTimelineService.js +26 -0
- package/build/dist/Server/Services/IncidentEpisodeStateTimelineService.js.map +1 -1
- package/build/dist/Server/Services/IncidentPublicNoteService.js +26 -1
- package/build/dist/Server/Services/IncidentPublicNoteService.js.map +1 -1
- package/build/dist/Server/Services/IncidentService.js +1148 -42
- package/build/dist/Server/Services/IncidentService.js.map +1 -1
- package/build/dist/Server/Services/IncidentStateTimelineService.js +12 -0
- package/build/dist/Server/Services/IncidentStateTimelineService.js.map +1 -1
- package/build/dist/Server/Services/IncidentTemplateService.js +83 -0
- package/build/dist/Server/Services/IncidentTemplateService.js.map +1 -1
- package/build/dist/Server/Services/InventoryItemService.js +31 -0
- package/build/dist/Server/Services/InventoryItemService.js.map +1 -1
- package/build/dist/Server/Services/IoTDeviceCredentialService.js +12 -5
- package/build/dist/Server/Services/IoTDeviceCredentialService.js.map +1 -1
- package/build/dist/Server/Services/ScheduledMaintenancePublicNoteService.js +26 -1
- package/build/dist/Server/Services/ScheduledMaintenancePublicNoteService.js.map +1 -1
- package/build/dist/Server/Services/ScheduledMaintenanceService.js +84 -57
- package/build/dist/Server/Services/ScheduledMaintenanceService.js.map +1 -1
- package/build/dist/Server/Services/SmsService.js +2 -22
- package/build/dist/Server/Services/SmsService.js.map +1 -1
- package/build/dist/Server/Services/StatusPageService.js +47 -9
- package/build/dist/Server/Services/StatusPageService.js.map +1 -1
- package/build/dist/Server/Services/StatusPageSubscriberNotificationTemplateService.js +237 -32
- package/build/dist/Server/Services/StatusPageSubscriberNotificationTemplateService.js.map +1 -1
- package/build/dist/Server/Services/StatusPageSubscriberNotificationTemplateStatusPageService.js +109 -0
- package/build/dist/Server/Services/StatusPageSubscriberNotificationTemplateStatusPageService.js.map +1 -1
- package/build/dist/Server/Services/StatusPageSubscriberService.js +925 -99
- package/build/dist/Server/Services/StatusPageSubscriberService.js.map +1 -1
- package/build/dist/Server/Services/UserNotificationRuleService.js +55 -51
- package/build/dist/Server/Services/UserNotificationRuleService.js.map +1 -1
- package/build/dist/Server/Services/WorkspaceNotificationSummaryService.js +42 -25
- package/build/dist/Server/Services/WorkspaceNotificationSummaryService.js.map +1 -1
- package/build/dist/Server/Types/Markdown.js +110 -6
- package/build/dist/Server/Types/Markdown.js.map +1 -1
- package/build/dist/Server/Types/Workflow/Components/Email.js +55 -6
- package/build/dist/Server/Types/Workflow/Components/Email.js.map +1 -1
- package/build/dist/Server/Utils/AffectedResources/LinkedAffectedResources.js +405 -0
- package/build/dist/Server/Utils/AffectedResources/LinkedAffectedResources.js.map +1 -0
- package/build/dist/Server/Utils/Alert/AlertStateChangeAuthorization.js +112 -0
- package/build/dist/Server/Utils/Alert/AlertStateChangeAuthorization.js.map +1 -0
- package/build/dist/Server/Utils/CustomField/CustomFieldRename.js +211 -0
- package/build/dist/Server/Utils/CustomField/CustomFieldRename.js.map +1 -0
- package/build/dist/Server/Utils/DataSource/EgressGuard.js +41 -12
- package/build/dist/Server/Utils/DataSource/EgressGuard.js.map +1 -1
- package/build/dist/Server/Utils/Inventory/InventoryOverviewAggregation.js +81 -0
- package/build/dist/Server/Utils/Inventory/InventoryOverviewAggregation.js.map +1 -0
- package/build/dist/Server/Utils/Mail/PinnedSmtpSocket.js +101 -0
- package/build/dist/Server/Utils/Mail/PinnedSmtpSocket.js.map +1 -0
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.js +9 -0
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.js.map +1 -1
- package/build/dist/Server/Utils/Response.js +30 -0
- package/build/dist/Server/Utils/Response.js.map +1 -1
- package/build/dist/Server/Utils/SSRFProtection.js +48 -6
- package/build/dist/Server/Utils/SSRFProtection.js.map +1 -1
- package/build/dist/Server/Utils/StatusPage/IncidentStatusPageScope.js +631 -0
- package/build/dist/Server/Utils/StatusPage/IncidentStatusPageScope.js.map +1 -0
- package/build/dist/Server/Utils/StatusPage/IncidentSubscriberAudienceBuilder.js +403 -0
- package/build/dist/Server/Utils/StatusPage/IncidentSubscriberAudienceBuilder.js.map +1 -0
- package/build/dist/Server/Utils/StatusPage/IncidentTemplateVariableBuilder.js +451 -0
- package/build/dist/Server/Utils/StatusPage/IncidentTemplateVariableBuilder.js.map +1 -0
- package/build/dist/Server/Utils/StatusPage/StatusPageExclusion.js +13 -0
- package/build/dist/Server/Utils/StatusPage/StatusPageExclusion.js.map +1 -0
- package/build/dist/Server/Utils/StatusPage/StatusPageReadAccess.js +91 -0
- package/build/dist/Server/Utils/StatusPage/StatusPageReadAccess.js.map +1 -0
- package/build/dist/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeNotice.js +101 -0
- package/build/dist/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeNotice.js.map +1 -0
- package/build/dist/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeToken.js +47 -0
- package/build/dist/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeToken.js.map +1 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberIncidentEmailBuilder.js +273 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberIncidentEmailBuilder.js.map +1 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberNotificationClaim.js +73 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberNotificationClaim.js.map +1 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberNotificationDeliveryRecord.js +722 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberNotificationDeliveryRecord.js.map +1 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberNotificationFanOut.js +102 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberNotificationFanOut.js.map +1 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberNotificationPreviewBuilder.js +527 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberNotificationPreviewBuilder.js.map +1 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberNotificationResendAccess.js +278 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberNotificationResendAccess.js.map +1 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberNotificationRunLimit.js +137 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberNotificationRunLimit.js.map +1 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberNotificationTiming.js +103 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberNotificationTiming.js.map +1 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberTemplateIncidentRecordAccess.js +114 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberTemplateIncidentRecordAccess.js.map +1 -0
- package/build/dist/Server/Utils/StatusPageResource.js +39 -7
- package/build/dist/Server/Utils/StatusPageResource.js.map +1 -1
- package/build/dist/Server/Utils/Topology/TopologyConcurrencyLimiter.js +178 -0
- package/build/dist/Server/Utils/Topology/TopologyConcurrencyLimiter.js.map +1 -0
- package/build/dist/Server/Utils/Topology/TopologyQueries.js +884 -0
- package/build/dist/Server/Utils/Topology/TopologyQueries.js.map +1 -0
- package/build/dist/Server/Utils/Topology/TopologyRequest.js +275 -0
- package/build/dist/Server/Utils/Topology/TopologyRequest.js.map +1 -0
- package/build/dist/Server/Utils/Topology/TopologyResponseCache.js +146 -0
- package/build/dist/Server/Utils/Topology/TopologyResponseCache.js.map +1 -0
- package/build/dist/Server/Utils/Topology/TopologySql.js +688 -0
- package/build/dist/Server/Utils/Topology/TopologySql.js.map +1 -0
- package/build/dist/Server/Utils/VM/VMAPI.js.map +1 -1
- package/build/dist/Server/Utils/VM/VMRunner.js +1 -0
- package/build/dist/Server/Utils/VM/VMRunner.js.map +1 -1
- package/build/dist/Server/Utils/Workflow/OAuth2TokenClient.js +3 -0
- package/build/dist/Server/Utils/Workflow/OAuth2TokenClient.js.map +1 -1
- package/build/dist/Types/CustomField/CustomFieldOrder.js +36 -0
- package/build/dist/Types/CustomField/CustomFieldOrder.js.map +1 -0
- package/build/dist/Types/CustomField/CustomFieldSavedViews.js +135 -0
- package/build/dist/Types/CustomField/CustomFieldSavedViews.js.map +1 -0
- package/build/dist/Types/CustomField/CustomFieldTemplateMerge.js +27 -0
- package/build/dist/Types/CustomField/CustomFieldTemplateMerge.js.map +1 -0
- package/build/dist/Types/CustomField/CustomFieldType.js +12 -0
- package/build/dist/Types/CustomField/CustomFieldType.js.map +1 -1
- package/build/dist/Types/CustomField/CustomFieldValueFormat.js +126 -0
- package/build/dist/Types/CustomField/CustomFieldValueFormat.js.map +1 -0
- package/build/dist/Types/CustomField/CustomFieldValueValidator.js +280 -0
- package/build/dist/Types/CustomField/CustomFieldValueValidator.js.map +1 -0
- package/build/dist/Types/CustomField/CustomFieldVariableKey.js +150 -0
- package/build/dist/Types/CustomField/CustomFieldVariableKey.js.map +1 -0
- package/build/dist/Types/Incident/IncidentAlertLink.js +17 -0
- package/build/dist/Types/Incident/IncidentAlertLink.js.map +1 -1
- package/build/dist/Types/SafeHtml.js +84 -0
- package/build/dist/Types/SafeHtml.js.map +1 -0
- package/build/dist/Types/StatusPage/IncidentCreatedRenotify.js +117 -0
- package/build/dist/Types/StatusPage/IncidentCreatedRenotify.js.map +1 -0
- package/build/dist/Types/StatusPage/IncidentCreatedResend.js +86 -0
- package/build/dist/Types/StatusPage/IncidentCreatedResend.js.map +1 -0
- package/build/dist/Types/StatusPage/IncidentScopeAddedPagesNotification.js +246 -0
- package/build/dist/Types/StatusPage/IncidentScopeAddedPagesNotification.js.map +1 -0
- package/build/dist/Types/StatusPage/IncidentSubscriberAudience.js +183 -0
- package/build/dist/Types/StatusPage/IncidentSubscriberAudience.js.map +1 -0
- package/build/dist/Types/StatusPage/StatusPageSubscriberUnsubscribe.js +277 -0
- package/build/dist/Types/StatusPage/StatusPageSubscriberUnsubscribe.js.map +1 -0
- package/build/dist/Types/StatusPage/SubscriberNotificationInterruption.js +27 -0
- package/build/dist/Types/StatusPage/SubscriberNotificationInterruption.js.map +1 -0
- package/build/dist/Types/StatusPage/SubscriberNotificationPreview.js +190 -0
- package/build/dist/Types/StatusPage/SubscriberNotificationPreview.js.map +1 -0
- package/build/dist/Types/StatusPage/SubscriberNotificationResend.js +102 -0
- package/build/dist/Types/StatusPage/SubscriberNotificationResend.js.map +1 -0
- package/build/dist/Types/StatusPage/SubscriberNotificationTemplateCompiler.js +97 -0
- package/build/dist/Types/StatusPage/SubscriberNotificationTemplateCompiler.js.map +1 -0
- package/build/dist/Types/StatusPage/SubscriberNotificationTemplatePreview.js +175 -0
- package/build/dist/Types/StatusPage/SubscriberNotificationTemplatePreview.js.map +1 -0
- package/build/dist/Types/StatusPage/SubscriberNotificationTemplateVariables.js +144 -4
- package/build/dist/Types/StatusPage/SubscriberNotificationTemplateVariables.js.map +1 -1
- package/build/dist/Types/StatusPage/SubscriberUpdateNotification.js +29 -9
- package/build/dist/Types/StatusPage/SubscriberUpdateNotification.js.map +1 -1
- package/build/dist/Types/Telemetry/InventoryLiveness.js +23 -0
- package/build/dist/Types/Telemetry/InventoryLiveness.js.map +1 -1
- package/build/dist/Types/Topology/TopologyApi.js +55 -0
- package/build/dist/Types/Topology/TopologyApi.js.map +1 -0
- package/build/dist/Types/Topology/TopologyTypeRules.js +165 -0
- package/build/dist/Types/Topology/TopologyTypeRules.js.map +1 -0
- package/build/dist/UI/Components/CustomFields/CustomFieldFormFields.js +169 -0
- package/build/dist/UI/Components/CustomFields/CustomFieldFormFields.js.map +1 -0
- package/build/dist/UI/Components/CustomFields/CustomFieldModelFormFields.js +212 -0
- package/build/dist/UI/Components/CustomFields/CustomFieldModelFormFields.js.map +1 -0
- package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js +64 -49
- package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js.map +1 -1
- package/build/dist/UI/Components/Forms/ModelForm.js +39 -5
- package/build/dist/UI/Components/Forms/ModelForm.js.map +1 -1
- package/build/dist/UI/Components/Forms/Validation.js +1 -1
- package/build/dist/UI/Components/Forms/Validation.js.map +1 -1
- package/build/dist/UI/Components/ModelFormModal/ModelFormModal.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/AnalyticsModelTable.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js +2 -2
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/CustomFieldColumns.js +13 -1
- package/build/dist/UI/Components/ModelTable/CustomFieldColumns.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/ModelTable.js +1 -1
- package/build/dist/UI/Components/ModelTable/ModelTable.js.map +1 -1
- package/build/dist/UI/Utils/SensitiveUrlToken.js +10 -5
- package/build/dist/UI/Utils/SensitiveUrlToken.js.map +1 -1
- package/build/dist/Utils/API.js +177 -1
- package/build/dist/Utils/API.js.map +1 -1
- package/build/dist/Utils/Incident/IncidentFromAlerts.js +38 -0
- package/build/dist/Utils/Incident/IncidentFromAlerts.js.map +1 -1
- package/build/dist/Utils/Incident/IncidentNoteTemplateVariables.js +155 -0
- package/build/dist/Utils/Incident/IncidentNoteTemplateVariables.js.map +1 -0
- package/build/dist/Utils/Markdown/MarkdownEscape.js +48 -0
- package/build/dist/Utils/Markdown/MarkdownEscape.js.map +1 -1
- package/build/dist/Utils/ProbeNetworkFailureUtil.js +460 -0
- package/build/dist/Utils/ProbeNetworkFailureUtil.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,1222 @@
|
|
|
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 userEvent from "@testing-library/user-event";
|
|
18
|
+
import { UserEvent } from "@testing-library/user-event/dist/types/setup/setup";
|
|
19
|
+
import { Mock, SpyInstance } from "jest-mock";
|
|
20
|
+
import React from "react";
|
|
21
|
+
import { MemoryRouter } from "react-router-dom";
|
|
22
|
+
import HTTPErrorResponse from "../../../Types/API/HTTPErrorResponse";
|
|
23
|
+
import HTTPResponse from "../../../Types/API/HTTPResponse";
|
|
24
|
+
import Headers from "../../../Types/API/Headers";
|
|
25
|
+
import APIException from "../../../Types/Exception/ApiException";
|
|
26
|
+
import { JSONObject } from "../../../Types/JSON";
|
|
27
|
+
import Button, { ButtonStyleType } from "../../../UI/Components/Button/Button";
|
|
28
|
+
import API from "../../../UI/Utils/API/API";
|
|
29
|
+
import Navigation from "../../../UI/Utils/Navigation";
|
|
30
|
+
import i18n from "../../../../App/FeatureSet/Accounts/src/Utils/i18n";
|
|
31
|
+
import ResendVerificationEmail, {
|
|
32
|
+
ComponentProps,
|
|
33
|
+
} from "../../../../App/FeatureSet/Accounts/src/Components/ResendVerificationEmail/ResendVerificationEmail";
|
|
34
|
+
|
|
35
|
+
/*
|
|
36
|
+
* The "Resend verification email" button, rendered for real with the
|
|
37
|
+
* Accounts translations and only the network mocked.
|
|
38
|
+
*
|
|
39
|
+
* WHY THE CLOCK IS FAKE. The countdown is the whole point of this component:
|
|
40
|
+
* a button that is disabled for exactly as long as the server will refuse,
|
|
41
|
+
* with the wait written next to it. A test that waited on the real clock
|
|
42
|
+
* would either take minutes or assert on whatever second it happened to land
|
|
43
|
+
* in. The clock only moves here when a test says so, one whole second at a
|
|
44
|
+
* time, so every countdown below is an exact string.
|
|
45
|
+
*
|
|
46
|
+
* WHY waitFor IS NOT USED. With fake timers, Testing Library's waitFor
|
|
47
|
+
* advances the clock between polls. A mocked request settles in a handful of
|
|
48
|
+
* microtasks, so settle() flushes those inside act() instead and the
|
|
49
|
+
* countdown stays where the test put it.
|
|
50
|
+
*
|
|
51
|
+
* WHY THE ENGLISH SENTENCES. The component's whole surface comes out of t();
|
|
52
|
+
* asserting on the English copy from Locales/en.json means a renamed or
|
|
53
|
+
* missing key fails here instead of shipping as a dotted path.
|
|
54
|
+
*/
|
|
55
|
+
|
|
56
|
+
const RESEND_TOKEN: string =
|
|
57
|
+
"v1.eyJ2IjoxLCJ1IjoiMzMzMyJ9.c2lnbmF0dXJlLXNpZ25hdHVyZS1zaWduYXR1cmUtc2ln";
|
|
58
|
+
const VERIFICATION_TOKEN: string = "8f2d1c3a-4b5e-4f60-8a71-92b3c4d5e6f7";
|
|
59
|
+
|
|
60
|
+
// Straight out of en.json.
|
|
61
|
+
const BUTTON: string = "Resend verification email";
|
|
62
|
+
const SENT: string =
|
|
63
|
+
"We've sent a new verification link. It can take a minute or two to arrive.";
|
|
64
|
+
const WAIT: string =
|
|
65
|
+
"A verification email was sent recently. You can request another one when the timer runs out.";
|
|
66
|
+
const ALREADY_VERIFIED: string =
|
|
67
|
+
"Your email address is already verified. You can sign in now.";
|
|
68
|
+
const FAILED: string =
|
|
69
|
+
"We couldn't send a new verification email. Please try again in a moment.";
|
|
70
|
+
const CONTINUE_TO_SIGN_IN: string = "Continue to sign in";
|
|
71
|
+
|
|
72
|
+
// The server's curated refusals (flat locale keys in en.json).
|
|
73
|
+
const INVALID_REQUEST: string =
|
|
74
|
+
"This verification request is no longer valid. Sign in with your email and password and we will send you a new verification link.";
|
|
75
|
+
const BLOCKED: string =
|
|
76
|
+
"Your account has been blocked. Please contact your administrator.";
|
|
77
|
+
const RATE_LIMITED: string =
|
|
78
|
+
"Too many requests for a new verification email. Please try again later.";
|
|
79
|
+
const UNAVAILABLE: string =
|
|
80
|
+
"Unable to send a verification email right now. Please try again shortly.";
|
|
81
|
+
|
|
82
|
+
type CountdownFunction = (time: string) => string;
|
|
83
|
+
|
|
84
|
+
const countdown: CountdownFunction = (time: string): string => {
|
|
85
|
+
return `You can request another email in ${time}.`;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
type Answer = HTTPResponse<JSONObject> | HTTPErrorResponse;
|
|
89
|
+
|
|
90
|
+
interface Deferred {
|
|
91
|
+
promise: Promise<Answer>;
|
|
92
|
+
resolve: (answer: Answer) => void;
|
|
93
|
+
reject: (error: unknown) => void;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
type DeferredFunction = () => Deferred;
|
|
97
|
+
|
|
98
|
+
const deferred: DeferredFunction = (): Deferred => {
|
|
99
|
+
let resolve: (answer: Answer) => void = () => {};
|
|
100
|
+
let reject: (error: unknown) => void = () => {};
|
|
101
|
+
const promise: Promise<Answer> = new Promise<Answer>(
|
|
102
|
+
(
|
|
103
|
+
onResolve: (answer: Answer) => void,
|
|
104
|
+
onReject: (error: unknown) => void,
|
|
105
|
+
): void => {
|
|
106
|
+
resolve = onResolve;
|
|
107
|
+
reject = onReject;
|
|
108
|
+
},
|
|
109
|
+
);
|
|
110
|
+
return { promise, resolve, reject };
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
type ResendAnswerFunction = (body: JSONObject) => HTTPResponse<JSONObject>;
|
|
114
|
+
|
|
115
|
+
const ok: ResendAnswerFunction = (
|
|
116
|
+
body: JSONObject,
|
|
117
|
+
): HTTPResponse<JSONObject> => {
|
|
118
|
+
return new HTTPResponse<JSONObject>(200, body, {});
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
type SecondsAnswerFunction = (seconds: number) => HTTPResponse<JSONObject>;
|
|
122
|
+
|
|
123
|
+
const sent: SecondsAnswerFunction = (
|
|
124
|
+
seconds: number,
|
|
125
|
+
): HTTPResponse<JSONObject> => {
|
|
126
|
+
return ok({
|
|
127
|
+
emailSent: true,
|
|
128
|
+
alreadyVerified: false,
|
|
129
|
+
retryAfterSeconds: seconds,
|
|
130
|
+
});
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const coolingDown: SecondsAnswerFunction = (
|
|
134
|
+
seconds: number,
|
|
135
|
+
): HTTPResponse<JSONObject> => {
|
|
136
|
+
return ok({
|
|
137
|
+
emailSent: false,
|
|
138
|
+
alreadyVerified: false,
|
|
139
|
+
retryAfterSeconds: seconds,
|
|
140
|
+
});
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const alreadyVerified: () => HTTPResponse<JSONObject> =
|
|
144
|
+
(): HTTPResponse<JSONObject> => {
|
|
145
|
+
return ok({
|
|
146
|
+
emailSent: false,
|
|
147
|
+
alreadyVerified: true,
|
|
148
|
+
retryAfterSeconds: 0,
|
|
149
|
+
});
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
type RefusalFunction = (
|
|
153
|
+
statusCode: number,
|
|
154
|
+
message: string,
|
|
155
|
+
headers?: Headers,
|
|
156
|
+
) => HTTPErrorResponse;
|
|
157
|
+
|
|
158
|
+
const refusal: RefusalFunction = (
|
|
159
|
+
statusCode: number,
|
|
160
|
+
message: string,
|
|
161
|
+
headers: Headers = {},
|
|
162
|
+
): HTTPErrorResponse => {
|
|
163
|
+
return new HTTPErrorResponse(statusCode, { message: message }, headers);
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
interface PostedRequest {
|
|
167
|
+
url: string;
|
|
168
|
+
data: unknown;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
let posted: Array<PostedRequest> = [];
|
|
172
|
+
|
|
173
|
+
/*
|
|
174
|
+
* What the next POST /resend-verification-email answers with. A function so
|
|
175
|
+
* a test can hand back a pending promise, or throw like a dropped
|
|
176
|
+
* connection does.
|
|
177
|
+
*/
|
|
178
|
+
let answerResend: () => Promise<Answer> = async (): Promise<Answer> => {
|
|
179
|
+
return sent(60);
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
let consoleErrors: SpyInstance<typeof console.error>;
|
|
183
|
+
|
|
184
|
+
/*
|
|
185
|
+
* Printed once per run by Testing Library 13 itself on React 18.3, whatever
|
|
186
|
+
* the component does. Every other console error fails the test.
|
|
187
|
+
*/
|
|
188
|
+
const TEST_UTILS_ACT_DEPRECATION: string =
|
|
189
|
+
"`ReactDOMTestUtils.act` is deprecated";
|
|
190
|
+
|
|
191
|
+
type SettleFunction = () => Promise<void>;
|
|
192
|
+
|
|
193
|
+
/*
|
|
194
|
+
* Lets a mocked request run to completion inside act(). A handful of
|
|
195
|
+
* microtasks is all the request chain takes; twenty leaves room to spare
|
|
196
|
+
* without ever touching the (fake) clock.
|
|
197
|
+
*/
|
|
198
|
+
const settle: SettleFunction = async (): Promise<void> => {
|
|
199
|
+
await act(async () => {
|
|
200
|
+
for (let tick: number = 0; tick < 20; tick++) {
|
|
201
|
+
await Promise.resolve();
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
type AdvanceFunction = (milliseconds: number) => void;
|
|
207
|
+
|
|
208
|
+
const advance: AdvanceFunction = (milliseconds: number): void => {
|
|
209
|
+
act(() => {
|
|
210
|
+
jest.advanceTimersByTime(milliseconds);
|
|
211
|
+
});
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
type RenderFunction = (
|
|
215
|
+
props?: Partial<ComponentProps>,
|
|
216
|
+
) => ReturnType<typeof render>;
|
|
217
|
+
|
|
218
|
+
const renderButton: RenderFunction = (
|
|
219
|
+
props: Partial<ComponentProps> = {},
|
|
220
|
+
): ReturnType<typeof render> => {
|
|
221
|
+
return render(
|
|
222
|
+
<MemoryRouter>
|
|
223
|
+
<ResendVerificationEmail
|
|
224
|
+
credential={{ resendToken: RESEND_TOKEN }}
|
|
225
|
+
{...props}
|
|
226
|
+
/>
|
|
227
|
+
</MemoryRouter>,
|
|
228
|
+
);
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
type ElementFunction = () => HTMLElement;
|
|
232
|
+
|
|
233
|
+
const button: ElementFunction = (): HTMLElement => {
|
|
234
|
+
return screen.getByTestId("resend-verification-email");
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
const status: ElementFunction = (): HTMLElement => {
|
|
238
|
+
return screen.getByTestId("resend-verification-email-status");
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
type TextFunction = () => string | null;
|
|
242
|
+
|
|
243
|
+
const countdownText: TextFunction = (): string | null => {
|
|
244
|
+
const element: HTMLElement | null = screen.queryByTestId(
|
|
245
|
+
"resend-verification-email-countdown",
|
|
246
|
+
);
|
|
247
|
+
return element ? element.textContent : null;
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
type ClickFunction = () => Promise<void>;
|
|
251
|
+
|
|
252
|
+
/*
|
|
253
|
+
* A plain DOM click: it does not move focus, which is what a click on a
|
|
254
|
+
* button that is about to disable itself amounts to in a real browser.
|
|
255
|
+
*/
|
|
256
|
+
const clickResend: ClickFunction = async (): Promise<void> => {
|
|
257
|
+
fireEvent.click(button());
|
|
258
|
+
await settle();
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
type SetupUserFunction = () => UserEvent;
|
|
262
|
+
|
|
263
|
+
const setupUser: SetupUserFunction = (): UserEvent => {
|
|
264
|
+
return userEvent.setup({
|
|
265
|
+
delay: null,
|
|
266
|
+
advanceTimers: jest.advanceTimersByTime,
|
|
267
|
+
});
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
type ExpectButtonLabelFunction = () => void;
|
|
271
|
+
|
|
272
|
+
/*
|
|
273
|
+
* The label never carries the countdown. A label that changes every second
|
|
274
|
+
* is re-announced by some screen readers every second, and the name a voice
|
|
275
|
+
* control user says to press it would keep changing under them.
|
|
276
|
+
*/
|
|
277
|
+
const expectFixedButtonLabel: ExpectButtonLabelFunction = (): void => {
|
|
278
|
+
expect(button()).toHaveAccessibleName(BUTTON);
|
|
279
|
+
expect(button()).toHaveTextContent(BUTTON);
|
|
280
|
+
expect(button().textContent).not.toMatch(/\d/);
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
/*
|
|
284
|
+
* WHY THE BUTTON'S CLASSES ARE PINNED. The shared Button's OUTLINE style is
|
|
285
|
+
* nothing but two custom classes, btn-outline-secondary and
|
|
286
|
+
* background-very-light-Gray500-on-hover, and no stylesheet the Accounts app
|
|
287
|
+
* loads defines either -- an OUTLINE button there renders as bare text with
|
|
288
|
+
* no border. NORMAL is plain Tailwind (a gray border on white), which the
|
|
289
|
+
* Accounts build does ship, so it is the one that looks like a button.
|
|
290
|
+
*
|
|
291
|
+
* NORMAL also carries md:w-auto and md:ml-3 for dialog footers. The inline
|
|
292
|
+
* width and margin are what keep it full width and flush with the card on a
|
|
293
|
+
* wide screen, where those md: classes would otherwise win over w-full.
|
|
294
|
+
*/
|
|
295
|
+
const ACCOUNTS_BUTTON_CLASSES: string =
|
|
296
|
+
"w-full justify-center gap-1 disabled:cursor-not-allowed disabled:opacity-60";
|
|
297
|
+
|
|
298
|
+
// Only Button's NORMAL branch renders these; OUTLINE replaces them wholesale.
|
|
299
|
+
const NORMAL_STYLE_CLASSES: Array<string> = ["border-gray-300", "bg-white"];
|
|
300
|
+
|
|
301
|
+
// OUTLINE's classes, undefined in every stylesheet Accounts loads.
|
|
302
|
+
const OUTLINE_STYLE_CLASSES: Array<string> = [
|
|
303
|
+
"btn-outline-secondary",
|
|
304
|
+
"background-very-light-Gray500-on-hover",
|
|
305
|
+
];
|
|
306
|
+
|
|
307
|
+
type ExpectButtonStyleFunction = (element: HTMLElement) => void;
|
|
308
|
+
|
|
309
|
+
const expectAccountsButtonStyle: ExpectButtonStyleFunction = (
|
|
310
|
+
element: HTMLElement,
|
|
311
|
+
): void => {
|
|
312
|
+
expect(element).toHaveClass(...ACCOUNTS_BUTTON_CLASSES.split(" "));
|
|
313
|
+
expect(element).toHaveStyle({ width: "100%", marginLeft: "0px" });
|
|
314
|
+
|
|
315
|
+
for (const className of NORMAL_STYLE_CLASSES) {
|
|
316
|
+
expect(element).toHaveClass(className);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
for (const className of OUTLINE_STYLE_CLASSES) {
|
|
320
|
+
expect(element).not.toHaveClass(className);
|
|
321
|
+
}
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
describe("ResendVerificationEmail", () => {
|
|
325
|
+
beforeEach(() => {
|
|
326
|
+
jest.useFakeTimers();
|
|
327
|
+
jest.setSystemTime(new Date("2026-03-02T10:00:00.000Z"));
|
|
328
|
+
|
|
329
|
+
posted = [];
|
|
330
|
+
answerResend = async (): Promise<Answer> => {
|
|
331
|
+
return sent(60);
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
consoleErrors = jest.spyOn(console, "error");
|
|
335
|
+
|
|
336
|
+
jest.spyOn(Navigation, "navigate").mockImplementation(() => {});
|
|
337
|
+
jest
|
|
338
|
+
.spyOn(API, "post")
|
|
339
|
+
.mockImplementation(
|
|
340
|
+
async (
|
|
341
|
+
options: Parameters<typeof API.post>[0],
|
|
342
|
+
): Promise<HTTPResponse<JSONObject>> => {
|
|
343
|
+
const url: string = options.url.toString();
|
|
344
|
+
posted.push({ url: url, data: options.data });
|
|
345
|
+
|
|
346
|
+
if (!url.endsWith("/resend-verification-email")) {
|
|
347
|
+
throw new Error(`Unexpected request to ${url}`);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
return (await answerResend()) as HTTPResponse<JSONObject>;
|
|
351
|
+
},
|
|
352
|
+
);
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
afterEach(async () => {
|
|
356
|
+
cleanup();
|
|
357
|
+
|
|
358
|
+
const reportedErrors: Array<string> = consoleErrors.mock.calls
|
|
359
|
+
.map((call: Array<unknown>) => {
|
|
360
|
+
return call.map(String).join(" ");
|
|
361
|
+
})
|
|
362
|
+
.filter((message: string) => {
|
|
363
|
+
return !message.includes(TEST_UTILS_ACT_DEPRECATION);
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
// Restored before asserting, so one failure cannot leak into the next test.
|
|
367
|
+
jest.useRealTimers();
|
|
368
|
+
jest.restoreAllMocks();
|
|
369
|
+
|
|
370
|
+
if (i18n.language !== "en") {
|
|
371
|
+
await i18n.changeLanguage("en");
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Nothing this component does may leave React complaining.
|
|
375
|
+
expect(reportedErrors).toEqual([]);
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
describe("the countdown", () => {
|
|
379
|
+
test("with no wait the button is ready and nothing counts down", () => {
|
|
380
|
+
renderButton();
|
|
381
|
+
|
|
382
|
+
expect(button()).toBeEnabled();
|
|
383
|
+
expectFixedButtonLabel();
|
|
384
|
+
expect(countdownText()).toBeNull();
|
|
385
|
+
expect(
|
|
386
|
+
screen.queryByTestId("resend-verification-email-status"),
|
|
387
|
+
).not.toBeInTheDocument();
|
|
388
|
+
expect(jest.getTimerCount()).toBe(0);
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
test("an initial wait disables the button and counts down beside it, never inside it", () => {
|
|
392
|
+
renderButton({ initialCooldownSeconds: 5 });
|
|
393
|
+
|
|
394
|
+
expect(button()).toBeDisabled();
|
|
395
|
+
expectFixedButtonLabel();
|
|
396
|
+
expect(countdownText()).toBe(countdown("0:05"));
|
|
397
|
+
|
|
398
|
+
const countdownElement: HTMLElement = screen.getByTestId(
|
|
399
|
+
"resend-verification-email-countdown",
|
|
400
|
+
);
|
|
401
|
+
expect(button()).not.toContainElement(countdownElement);
|
|
402
|
+
expect(countdownElement.tagName).toBe("P");
|
|
403
|
+
// Not a live region: a countdown read out every second drowns out the page.
|
|
404
|
+
expect(countdownElement).not.toHaveAttribute("aria-live");
|
|
405
|
+
expect(countdownElement).not.toHaveAttribute("role");
|
|
406
|
+
expect(countdownElement).toHaveClass("tabular-nums");
|
|
407
|
+
|
|
408
|
+
for (const [elapsed, time] of [
|
|
409
|
+
[1000, "0:04"],
|
|
410
|
+
[1000, "0:03"],
|
|
411
|
+
[1000, "0:02"],
|
|
412
|
+
[1000, "0:01"],
|
|
413
|
+
] as const) {
|
|
414
|
+
advance(elapsed);
|
|
415
|
+
expect(countdownText()).toBe(countdown(time));
|
|
416
|
+
expect(button()).toBeDisabled();
|
|
417
|
+
expectFixedButtonLabel();
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
advance(1000);
|
|
421
|
+
|
|
422
|
+
expect(button()).toBeEnabled();
|
|
423
|
+
expectFixedButtonLabel();
|
|
424
|
+
expect(countdownText()).toBeNull();
|
|
425
|
+
// The ticking stops once there is nothing left to count.
|
|
426
|
+
expect(jest.getTimerCount()).toBe(0);
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
test("does not move between whole seconds", () => {
|
|
430
|
+
renderButton({ initialCooldownSeconds: 3 });
|
|
431
|
+
|
|
432
|
+
advance(999);
|
|
433
|
+
expect(countdownText()).toBe(countdown("0:03"));
|
|
434
|
+
|
|
435
|
+
advance(1);
|
|
436
|
+
expect(countdownText()).toBe(countdown("0:02"));
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
test("uses the initial wait as given, with no extra second", () => {
|
|
440
|
+
renderButton({ initialCooldownSeconds: 60 });
|
|
441
|
+
|
|
442
|
+
expect(countdownText()).toBe(countdown("1:00"));
|
|
443
|
+
|
|
444
|
+
advance(59_000);
|
|
445
|
+
expect(countdownText()).toBe(countdown("0:01"));
|
|
446
|
+
|
|
447
|
+
advance(1000);
|
|
448
|
+
expect(button()).toBeEnabled();
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
test("shows minutes and seconds for a longer wait", () => {
|
|
452
|
+
renderButton({ initialCooldownSeconds: 725 });
|
|
453
|
+
|
|
454
|
+
expect(countdownText()).toBe(countdown("12:05"));
|
|
455
|
+
|
|
456
|
+
advance(5000);
|
|
457
|
+
expect(countdownText()).toBe(countdown("12:00"));
|
|
458
|
+
|
|
459
|
+
advance(60_000);
|
|
460
|
+
expect(countdownText()).toBe(countdown("11:00"));
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
test("rounds a fractional initial wait up", () => {
|
|
464
|
+
renderButton({ initialCooldownSeconds: 2.2 });
|
|
465
|
+
|
|
466
|
+
expect(countdownText()).toBe(countdown("0:03"));
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
test.each([
|
|
470
|
+
["zero", 0],
|
|
471
|
+
["negative", -30],
|
|
472
|
+
["NaN", Number.NaN],
|
|
473
|
+
["infinite", Number.POSITIVE_INFINITY],
|
|
474
|
+
])(
|
|
475
|
+
"treats a %s initial wait as none",
|
|
476
|
+
(_label: string, seconds: number) => {
|
|
477
|
+
renderButton({ initialCooldownSeconds: seconds });
|
|
478
|
+
|
|
479
|
+
expect(button()).toBeEnabled();
|
|
480
|
+
expect(countdownText()).toBeNull();
|
|
481
|
+
},
|
|
482
|
+
);
|
|
483
|
+
|
|
484
|
+
test("caps an absurd initial wait at one day", () => {
|
|
485
|
+
renderButton({ initialCooldownSeconds: 30 * 24 * 60 * 60 });
|
|
486
|
+
|
|
487
|
+
expect(countdownText()).toBe(countdown("1440:00"));
|
|
488
|
+
expect(button()).toBeDisabled();
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
test("recomputes from the clock, so a throttled tab catches up at once", () => {
|
|
492
|
+
renderButton({ initialCooldownSeconds: 30 });
|
|
493
|
+
|
|
494
|
+
/*
|
|
495
|
+
* A background tab can go a long time without an interval firing.
|
|
496
|
+
* Moving the wall clock without firing any timer, then letting one
|
|
497
|
+
* tick through, must land on the true remaining time rather than one
|
|
498
|
+
* second less than before.
|
|
499
|
+
*/
|
|
500
|
+
jest.setSystemTime(Date.now() + 20_000);
|
|
501
|
+
advance(1000);
|
|
502
|
+
|
|
503
|
+
expect(countdownText()).toBe(countdown("0:09"));
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
test("a click during the wait sends nothing", async () => {
|
|
507
|
+
renderButton({ initialCooldownSeconds: 10 });
|
|
508
|
+
|
|
509
|
+
await clickResend();
|
|
510
|
+
|
|
511
|
+
expect(posted).toHaveLength(0);
|
|
512
|
+
expect(
|
|
513
|
+
screen.queryByTestId("resend-verification-email-status"),
|
|
514
|
+
).not.toBeInTheDocument();
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
test("unmounting stops the countdown", () => {
|
|
518
|
+
const { unmount } = renderButton({ initialCooldownSeconds: 30 });
|
|
519
|
+
|
|
520
|
+
expect(jest.getTimerCount()).toBe(1);
|
|
521
|
+
|
|
522
|
+
unmount();
|
|
523
|
+
|
|
524
|
+
expect(jest.getTimerCount()).toBe(0);
|
|
525
|
+
|
|
526
|
+
// A tick after unmount would be a state update on a dead component.
|
|
527
|
+
act(() => {
|
|
528
|
+
jest.advanceTimersByTime(60_000);
|
|
529
|
+
});
|
|
530
|
+
});
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
describe("sending", () => {
|
|
534
|
+
test("posts the resend token to /resend-verification-email", async () => {
|
|
535
|
+
renderButton();
|
|
536
|
+
|
|
537
|
+
await clickResend();
|
|
538
|
+
|
|
539
|
+
expect(posted).toEqual([
|
|
540
|
+
{
|
|
541
|
+
url: expect.stringMatching(/\/resend-verification-email$/),
|
|
542
|
+
data: { data: { resendToken: RESEND_TOKEN } },
|
|
543
|
+
},
|
|
544
|
+
]);
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
test("posts a verification token the same way", async () => {
|
|
548
|
+
renderButton({ credential: { verificationToken: VERIFICATION_TOKEN } });
|
|
549
|
+
|
|
550
|
+
await clickResend();
|
|
551
|
+
|
|
552
|
+
expect(posted).toEqual([
|
|
553
|
+
{
|
|
554
|
+
url: expect.stringMatching(/\/resend-verification-email$/),
|
|
555
|
+
data: { data: { verificationToken: VERIFICATION_TOKEN } },
|
|
556
|
+
},
|
|
557
|
+
]);
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
test("a double click sends exactly one request", async () => {
|
|
561
|
+
/*
|
|
562
|
+
* The second click of a double click lands while the first request is
|
|
563
|
+
* still on the wire -- which is the case the guard exists for.
|
|
564
|
+
*/
|
|
565
|
+
const pending: Deferred = deferred();
|
|
566
|
+
answerResend = (): Promise<Answer> => {
|
|
567
|
+
return pending.promise;
|
|
568
|
+
};
|
|
569
|
+
renderButton();
|
|
570
|
+
const user: UserEvent = setupUser();
|
|
571
|
+
|
|
572
|
+
await act(async () => {
|
|
573
|
+
await user.dblClick(button());
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
expect(posted).toHaveLength(1);
|
|
577
|
+
|
|
578
|
+
await act(async () => {
|
|
579
|
+
pending.resolve(sent(60));
|
|
580
|
+
});
|
|
581
|
+
await settle();
|
|
582
|
+
|
|
583
|
+
expect(posted).toHaveLength(1);
|
|
584
|
+
expect(status()).toHaveTextContent(SENT);
|
|
585
|
+
expect(button()).toBeDisabled();
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
test("two clicks that land before React re-renders still send one request", async () => {
|
|
589
|
+
const pending: Deferred = deferred();
|
|
590
|
+
answerResend = (): Promise<Answer> => {
|
|
591
|
+
return pending.promise;
|
|
592
|
+
};
|
|
593
|
+
renderButton();
|
|
594
|
+
|
|
595
|
+
const element: HTMLElement = button();
|
|
596
|
+
|
|
597
|
+
/*
|
|
598
|
+
* Both clicks inside one act(): React has not re-rendered the button as
|
|
599
|
+
* disabled in between, so only the ref can stop the second one.
|
|
600
|
+
*/
|
|
601
|
+
act(() => {
|
|
602
|
+
element.click();
|
|
603
|
+
element.click();
|
|
604
|
+
element.click();
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
expect(posted).toHaveLength(1);
|
|
608
|
+
|
|
609
|
+
await act(async () => {
|
|
610
|
+
pending.resolve(sent(60));
|
|
611
|
+
});
|
|
612
|
+
await settle();
|
|
613
|
+
|
|
614
|
+
expect(posted).toHaveLength(1);
|
|
615
|
+
expect(status()).toHaveTextContent(SENT);
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
test("the button stays disabled and says it is busy while the request runs", async () => {
|
|
619
|
+
const pending: Deferred = deferred();
|
|
620
|
+
answerResend = (): Promise<Answer> => {
|
|
621
|
+
return pending.promise;
|
|
622
|
+
};
|
|
623
|
+
renderButton();
|
|
624
|
+
|
|
625
|
+
await clickResend();
|
|
626
|
+
|
|
627
|
+
expect(button()).toBeDisabled();
|
|
628
|
+
expect(button()).toHaveAttribute("aria-disabled", "true");
|
|
629
|
+
expectFixedButtonLabel();
|
|
630
|
+
expect(countdownText()).toBeNull();
|
|
631
|
+
|
|
632
|
+
await act(async () => {
|
|
633
|
+
pending.resolve(coolingDown(5));
|
|
634
|
+
});
|
|
635
|
+
await settle();
|
|
636
|
+
|
|
637
|
+
expect(button()).toBeDisabled();
|
|
638
|
+
expect(countdownText()).toBe(countdown("0:06"));
|
|
639
|
+
});
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
describe("what the server answered", () => {
|
|
643
|
+
test("Sent: a success message and the server's cooldown, plus a second of slack", async () => {
|
|
644
|
+
renderButton();
|
|
645
|
+
|
|
646
|
+
await clickResend();
|
|
647
|
+
|
|
648
|
+
const alert: HTMLElement = status();
|
|
649
|
+
expect(alert).toHaveTextContent(SENT);
|
|
650
|
+
expect(alert).toHaveAttribute("role", "alert");
|
|
651
|
+
expect(alert).toHaveClass("border-emerald-200");
|
|
652
|
+
expect(alert).toHaveClass("mt-4");
|
|
653
|
+
expect(alert).toHaveClass("text-start");
|
|
654
|
+
|
|
655
|
+
/*
|
|
656
|
+
* 60 seconds from the server, shown as 1:01: the server rounds and
|
|
657
|
+
* measures against its own timestamps, so a click at exactly 1:00
|
|
658
|
+
* could land early and buy nothing but another refusal.
|
|
659
|
+
*/
|
|
660
|
+
expect(countdownText()).toBe(countdown("1:01"));
|
|
661
|
+
expect(button()).toBeDisabled();
|
|
662
|
+
expectFixedButtonLabel();
|
|
663
|
+
|
|
664
|
+
advance(60_000);
|
|
665
|
+
expect(countdownText()).toBe(countdown("0:01"));
|
|
666
|
+
expect(button()).toBeDisabled();
|
|
667
|
+
|
|
668
|
+
advance(1000);
|
|
669
|
+
expect(button()).toBeEnabled();
|
|
670
|
+
expect(countdownText()).toBeNull();
|
|
671
|
+
// The confirmation stays up after the wait is over.
|
|
672
|
+
expect(status()).toHaveTextContent(SENT);
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
test("Sent with no usable wait falls back to the default minute", async () => {
|
|
676
|
+
answerResend = async (): Promise<Answer> => {
|
|
677
|
+
return ok({ emailSent: true });
|
|
678
|
+
};
|
|
679
|
+
renderButton();
|
|
680
|
+
|
|
681
|
+
await clickResend();
|
|
682
|
+
|
|
683
|
+
expect(status()).toHaveTextContent(SENT);
|
|
684
|
+
expect(countdownText()).toBe(countdown("1:01"));
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
test("CoolingDown: an informational message and the wait that is left", async () => {
|
|
688
|
+
answerResend = async (): Promise<Answer> => {
|
|
689
|
+
return coolingDown(42);
|
|
690
|
+
};
|
|
691
|
+
renderButton();
|
|
692
|
+
|
|
693
|
+
await clickResend();
|
|
694
|
+
|
|
695
|
+
expect(status()).toHaveTextContent(WAIT);
|
|
696
|
+
expect(status()).toHaveClass("border-blue-200");
|
|
697
|
+
expect(countdownText()).toBe(countdown("0:43"));
|
|
698
|
+
expect(button()).toBeDisabled();
|
|
699
|
+
|
|
700
|
+
advance(43_000);
|
|
701
|
+
expect(button()).toBeEnabled();
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
test("a request after the wait is over goes out and starts a new wait", async () => {
|
|
705
|
+
renderButton({ initialCooldownSeconds: 2 });
|
|
706
|
+
|
|
707
|
+
advance(2000);
|
|
708
|
+
await clickResend();
|
|
709
|
+
|
|
710
|
+
expect(posted).toHaveLength(1);
|
|
711
|
+
expect(countdownText()).toBe(countdown("1:01"));
|
|
712
|
+
|
|
713
|
+
advance(61_000);
|
|
714
|
+
answerResend = async (): Promise<Answer> => {
|
|
715
|
+
return coolingDown(30);
|
|
716
|
+
};
|
|
717
|
+
await clickResend();
|
|
718
|
+
|
|
719
|
+
expect(posted).toHaveLength(2);
|
|
720
|
+
expect(status()).toHaveTextContent(WAIT);
|
|
721
|
+
expect(countdownText()).toBe(countdown("0:31"));
|
|
722
|
+
});
|
|
723
|
+
|
|
724
|
+
test("AlreadyVerified: the button gives way to a focused sign-in link", async () => {
|
|
725
|
+
const onAlreadyVerified: Mock<() => void> = jest.fn<() => void>();
|
|
726
|
+
answerResend = async (): Promise<Answer> => {
|
|
727
|
+
return alreadyVerified();
|
|
728
|
+
};
|
|
729
|
+
renderButton({ onAlreadyVerified: onAlreadyVerified });
|
|
730
|
+
|
|
731
|
+
await clickResend();
|
|
732
|
+
|
|
733
|
+
expect(status()).toHaveTextContent(ALREADY_VERIFIED);
|
|
734
|
+
expect(status()).toHaveClass("border-emerald-200");
|
|
735
|
+
expect(
|
|
736
|
+
screen.queryByTestId("resend-verification-email"),
|
|
737
|
+
).not.toBeInTheDocument();
|
|
738
|
+
expect(countdownText()).toBeNull();
|
|
739
|
+
|
|
740
|
+
const signIn: HTMLElement = screen.getByTestId(
|
|
741
|
+
"resend-verification-email-sign-in",
|
|
742
|
+
);
|
|
743
|
+
expect(signIn).toBe(
|
|
744
|
+
screen.getByRole("link", { name: CONTINUE_TO_SIGN_IN }),
|
|
745
|
+
);
|
|
746
|
+
expect(signIn).toHaveAttribute("href", "/accounts/login");
|
|
747
|
+
expect(signIn).toHaveClass("bg-indigo-600");
|
|
748
|
+
expect(signIn).toHaveFocus();
|
|
749
|
+
|
|
750
|
+
expect(onAlreadyVerified).toHaveBeenCalledTimes(1);
|
|
751
|
+
expect(jest.getTimerCount()).toBe(0);
|
|
752
|
+
});
|
|
753
|
+
|
|
754
|
+
test("AlreadyVerified wins even while a wait was showing", async () => {
|
|
755
|
+
renderButton({ initialCooldownSeconds: 1 });
|
|
756
|
+
advance(1000);
|
|
757
|
+
|
|
758
|
+
answerResend = async (): Promise<Answer> => {
|
|
759
|
+
return alreadyVerified();
|
|
760
|
+
};
|
|
761
|
+
await clickResend();
|
|
762
|
+
|
|
763
|
+
expect(countdownText()).toBeNull();
|
|
764
|
+
expect(
|
|
765
|
+
screen.getByTestId("resend-verification-email-sign-in"),
|
|
766
|
+
).toHaveFocus();
|
|
767
|
+
});
|
|
768
|
+
|
|
769
|
+
test("AlreadyVerified works without an onAlreadyVerified callback", async () => {
|
|
770
|
+
answerResend = async (): Promise<Answer> => {
|
|
771
|
+
return alreadyVerified();
|
|
772
|
+
};
|
|
773
|
+
renderButton();
|
|
774
|
+
|
|
775
|
+
await clickResend();
|
|
776
|
+
|
|
777
|
+
expect(status()).toHaveTextContent(ALREADY_VERIFIED);
|
|
778
|
+
expect(
|
|
779
|
+
screen.getByTestId("resend-verification-email-sign-in"),
|
|
780
|
+
).toBeInTheDocument();
|
|
781
|
+
});
|
|
782
|
+
|
|
783
|
+
test("the sign-in link navigates in the app on a plain click", async () => {
|
|
784
|
+
answerResend = async (): Promise<Answer> => {
|
|
785
|
+
return alreadyVerified();
|
|
786
|
+
};
|
|
787
|
+
renderButton();
|
|
788
|
+
await clickResend();
|
|
789
|
+
|
|
790
|
+
const signIn: HTMLElement = screen.getByTestId(
|
|
791
|
+
"resend-verification-email-sign-in",
|
|
792
|
+
);
|
|
793
|
+
|
|
794
|
+
const plainClickAllowed: boolean = fireEvent.click(signIn);
|
|
795
|
+
|
|
796
|
+
expect(plainClickAllowed).toBe(false);
|
|
797
|
+
expect(Navigation.navigate).toHaveBeenCalledTimes(1);
|
|
798
|
+
expect(
|
|
799
|
+
jest.mocked(Navigation.navigate).mock.calls[0]![0].toString(),
|
|
800
|
+
).toBe("/accounts/login");
|
|
801
|
+
});
|
|
802
|
+
|
|
803
|
+
test.each([
|
|
804
|
+
["ctrl", { ctrlKey: true }],
|
|
805
|
+
["meta", { metaKey: true }],
|
|
806
|
+
["shift", { shiftKey: true }],
|
|
807
|
+
["alt", { altKey: true }],
|
|
808
|
+
])(
|
|
809
|
+
"the sign-in link leaves a %s-click to the browser",
|
|
810
|
+
async (_label: string, modifiers: MouseEventInit) => {
|
|
811
|
+
answerResend = async (): Promise<Answer> => {
|
|
812
|
+
return alreadyVerified();
|
|
813
|
+
};
|
|
814
|
+
renderButton();
|
|
815
|
+
await clickResend();
|
|
816
|
+
|
|
817
|
+
/*
|
|
818
|
+
* Whether the page left the click to the browser is read last in the
|
|
819
|
+
* event's path; the default is then prevented anyway, because jsdom
|
|
820
|
+
* cannot open a new tab and says so on the console.
|
|
821
|
+
*/
|
|
822
|
+
let leftToTheBrowser: boolean | null = null;
|
|
823
|
+
const recordDefault: (event: Event) => void = (event: Event): void => {
|
|
824
|
+
leftToTheBrowser = !event.defaultPrevented;
|
|
825
|
+
event.preventDefault();
|
|
826
|
+
};
|
|
827
|
+
window.addEventListener("click", recordDefault);
|
|
828
|
+
|
|
829
|
+
try {
|
|
830
|
+
fireEvent.click(
|
|
831
|
+
screen.getByTestId("resend-verification-email-sign-in"),
|
|
832
|
+
modifiers,
|
|
833
|
+
);
|
|
834
|
+
} finally {
|
|
835
|
+
window.removeEventListener("click", recordDefault);
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
expect(leftToTheBrowser).toBe(true);
|
|
839
|
+
expect(Navigation.navigate).not.toHaveBeenCalled();
|
|
840
|
+
},
|
|
841
|
+
);
|
|
842
|
+
});
|
|
843
|
+
|
|
844
|
+
describe("refusals and failures", () => {
|
|
845
|
+
test("429 with Retry-After: the wait message and that wait, not the limiter's text", async () => {
|
|
846
|
+
answerResend = async (): Promise<Answer> => {
|
|
847
|
+
return refusal(429, RATE_LIMITED, { "retry-after": "120" });
|
|
848
|
+
};
|
|
849
|
+
renderButton();
|
|
850
|
+
|
|
851
|
+
await clickResend();
|
|
852
|
+
|
|
853
|
+
expect(status()).toHaveTextContent(WAIT);
|
|
854
|
+
expect(status()).toHaveClass("border-blue-200");
|
|
855
|
+
expect(status()).not.toHaveTextContent(RATE_LIMITED);
|
|
856
|
+
expect(countdownText()).toBe(countdown("2:01"));
|
|
857
|
+
expect(button()).toBeDisabled();
|
|
858
|
+
|
|
859
|
+
advance(121_000);
|
|
860
|
+
expect(button()).toBeEnabled();
|
|
861
|
+
});
|
|
862
|
+
|
|
863
|
+
test("429 thrown rather than returned is handled the same way", async () => {
|
|
864
|
+
answerResend = async (): Promise<Answer> => {
|
|
865
|
+
throw refusal(429, RATE_LIMITED, { "Retry-After": "15" });
|
|
866
|
+
};
|
|
867
|
+
renderButton();
|
|
868
|
+
|
|
869
|
+
await clickResend();
|
|
870
|
+
|
|
871
|
+
expect(status()).toHaveTextContent(WAIT);
|
|
872
|
+
expect(countdownText()).toBe(countdown("0:16"));
|
|
873
|
+
});
|
|
874
|
+
|
|
875
|
+
test.each([
|
|
876
|
+
["no Retry-After", {}],
|
|
877
|
+
["an unreadable Retry-After", { "retry-after": "soon" }],
|
|
878
|
+
["a zero Retry-After", { "retry-after": "0" }],
|
|
879
|
+
])(
|
|
880
|
+
"429 with %s waits the default minute",
|
|
881
|
+
async (_label: string, headers: Headers) => {
|
|
882
|
+
answerResend = async (): Promise<Answer> => {
|
|
883
|
+
return refusal(429, RATE_LIMITED, headers);
|
|
884
|
+
};
|
|
885
|
+
renderButton();
|
|
886
|
+
|
|
887
|
+
await clickResend();
|
|
888
|
+
|
|
889
|
+
expect(status()).toHaveTextContent(WAIT);
|
|
890
|
+
expect(countdownText()).toBe(countdown("1:01"));
|
|
891
|
+
},
|
|
892
|
+
);
|
|
893
|
+
|
|
894
|
+
test("429 with a huge Retry-After is capped at a day", async () => {
|
|
895
|
+
answerResend = async (): Promise<Answer> => {
|
|
896
|
+
return refusal(429, RATE_LIMITED, { "retry-after": "999999999" });
|
|
897
|
+
};
|
|
898
|
+
renderButton();
|
|
899
|
+
|
|
900
|
+
await clickResend();
|
|
901
|
+
|
|
902
|
+
expect(countdownText()).toBe(countdown("1440:01"));
|
|
903
|
+
});
|
|
904
|
+
|
|
905
|
+
test("400: the server's curated message, and the button can be tried again", async () => {
|
|
906
|
+
answerResend = async (): Promise<Answer> => {
|
|
907
|
+
return refusal(400, INVALID_REQUEST);
|
|
908
|
+
};
|
|
909
|
+
renderButton();
|
|
910
|
+
|
|
911
|
+
await clickResend();
|
|
912
|
+
|
|
913
|
+
expect(status()).toHaveTextContent(INVALID_REQUEST);
|
|
914
|
+
expect(status()).toHaveClass("border-red-200");
|
|
915
|
+
expect(status()).not.toHaveTextContent(FAILED);
|
|
916
|
+
expect(countdownText()).toBeNull();
|
|
917
|
+
expect(button()).toBeEnabled();
|
|
918
|
+
});
|
|
919
|
+
|
|
920
|
+
test("400 thrown rather than returned is handled the same way", async () => {
|
|
921
|
+
answerResend = async (): Promise<Answer> => {
|
|
922
|
+
throw refusal(400, INVALID_REQUEST);
|
|
923
|
+
};
|
|
924
|
+
renderButton();
|
|
925
|
+
|
|
926
|
+
await clickResend();
|
|
927
|
+
|
|
928
|
+
expect(status()).toHaveTextContent(INVALID_REQUEST);
|
|
929
|
+
});
|
|
930
|
+
|
|
931
|
+
test("403 for a blocked account shows the server's message", async () => {
|
|
932
|
+
answerResend = async (): Promise<Answer> => {
|
|
933
|
+
return refusal(403, BLOCKED);
|
|
934
|
+
};
|
|
935
|
+
renderButton();
|
|
936
|
+
|
|
937
|
+
await clickResend();
|
|
938
|
+
|
|
939
|
+
expect(status()).toHaveTextContent(BLOCKED);
|
|
940
|
+
expect(status()).toHaveClass("border-red-200");
|
|
941
|
+
});
|
|
942
|
+
|
|
943
|
+
test("a curated refusal is shown in the reader's language", async () => {
|
|
944
|
+
await act(async () => {
|
|
945
|
+
await i18n.changeLanguage("fa");
|
|
946
|
+
});
|
|
947
|
+
answerResend = async (): Promise<Answer> => {
|
|
948
|
+
return refusal(400, INVALID_REQUEST);
|
|
949
|
+
};
|
|
950
|
+
renderButton();
|
|
951
|
+
|
|
952
|
+
await clickResend();
|
|
953
|
+
|
|
954
|
+
const persian: string = i18n.t(INVALID_REQUEST, {
|
|
955
|
+
keySeparator: false,
|
|
956
|
+
nsSeparator: false,
|
|
957
|
+
});
|
|
958
|
+
expect(persian).not.toBe(INVALID_REQUEST);
|
|
959
|
+
expect(status()).toHaveTextContent(persian);
|
|
960
|
+
expect(status()).not.toHaveTextContent(INVALID_REQUEST);
|
|
961
|
+
});
|
|
962
|
+
|
|
963
|
+
test.each([
|
|
964
|
+
[500, "Server Error"],
|
|
965
|
+
[502, "Bad Gateway"],
|
|
966
|
+
[503, UNAVAILABLE],
|
|
967
|
+
[504, "Gateway Timeout"],
|
|
968
|
+
])(
|
|
969
|
+
"%p: the localized failure copy, not the server's text",
|
|
970
|
+
async (statusCode: number, message: string) => {
|
|
971
|
+
answerResend = async (): Promise<Answer> => {
|
|
972
|
+
return refusal(statusCode, message);
|
|
973
|
+
};
|
|
974
|
+
renderButton();
|
|
975
|
+
|
|
976
|
+
await clickResend();
|
|
977
|
+
|
|
978
|
+
expect(status()).toHaveTextContent(FAILED);
|
|
979
|
+
expect(status()).toHaveClass("border-red-200");
|
|
980
|
+
expect(status()).not.toHaveTextContent(message);
|
|
981
|
+
expect(button()).toBeEnabled();
|
|
982
|
+
expect(countdownText()).toBeNull();
|
|
983
|
+
},
|
|
984
|
+
);
|
|
985
|
+
|
|
986
|
+
test.each([
|
|
987
|
+
["a network error", new APIException("Network Error")],
|
|
988
|
+
["a plain error", new Error("socket hang up")],
|
|
989
|
+
["a thrown string", "offline"],
|
|
990
|
+
])(
|
|
991
|
+
"%s: the localized failure copy",
|
|
992
|
+
async (_label: string, failure: unknown) => {
|
|
993
|
+
answerResend = async (): Promise<Answer> => {
|
|
994
|
+
throw failure;
|
|
995
|
+
};
|
|
996
|
+
renderButton();
|
|
997
|
+
|
|
998
|
+
await clickResend();
|
|
999
|
+
|
|
1000
|
+
expect(status()).toHaveTextContent(FAILED);
|
|
1001
|
+
expect(status()).not.toHaveTextContent("Network Error");
|
|
1002
|
+
expect(button()).toBeEnabled();
|
|
1003
|
+
},
|
|
1004
|
+
);
|
|
1005
|
+
|
|
1006
|
+
test("an answer it cannot read is a failure, never 'sent'", async () => {
|
|
1007
|
+
answerResend = async (): Promise<Answer> => {
|
|
1008
|
+
return ok({ ok: true });
|
|
1009
|
+
};
|
|
1010
|
+
renderButton();
|
|
1011
|
+
|
|
1012
|
+
await clickResend();
|
|
1013
|
+
|
|
1014
|
+
expect(status()).toHaveTextContent(FAILED);
|
|
1015
|
+
expect(status()).not.toHaveTextContent(SENT);
|
|
1016
|
+
expect(countdownText()).toBeNull();
|
|
1017
|
+
});
|
|
1018
|
+
|
|
1019
|
+
test("the last message is cleared as soon as a new request starts", async () => {
|
|
1020
|
+
answerResend = async (): Promise<Answer> => {
|
|
1021
|
+
return refusal(500, "Server Error");
|
|
1022
|
+
};
|
|
1023
|
+
renderButton();
|
|
1024
|
+
|
|
1025
|
+
await clickResend();
|
|
1026
|
+
expect(status()).toHaveTextContent(FAILED);
|
|
1027
|
+
|
|
1028
|
+
const pending: Deferred = deferred();
|
|
1029
|
+
answerResend = (): Promise<Answer> => {
|
|
1030
|
+
return pending.promise;
|
|
1031
|
+
};
|
|
1032
|
+
await clickResend();
|
|
1033
|
+
|
|
1034
|
+
expect(
|
|
1035
|
+
screen.queryByTestId("resend-verification-email-status"),
|
|
1036
|
+
).not.toBeInTheDocument();
|
|
1037
|
+
expect(screen.queryByText(FAILED)).not.toBeInTheDocument();
|
|
1038
|
+
|
|
1039
|
+
await act(async () => {
|
|
1040
|
+
pending.resolve(sent(60));
|
|
1041
|
+
});
|
|
1042
|
+
await settle();
|
|
1043
|
+
|
|
1044
|
+
expect(screen.getAllByRole("alert")).toHaveLength(1);
|
|
1045
|
+
expect(status()).toHaveTextContent(SENT);
|
|
1046
|
+
expect(posted).toHaveLength(2);
|
|
1047
|
+
});
|
|
1048
|
+
|
|
1049
|
+
test("a repeated outcome is a fresh alert, so it is announced again", async () => {
|
|
1050
|
+
answerResend = async (): Promise<Answer> => {
|
|
1051
|
+
return refusal(500, "Server Error");
|
|
1052
|
+
};
|
|
1053
|
+
renderButton();
|
|
1054
|
+
|
|
1055
|
+
await clickResend();
|
|
1056
|
+
const first: HTMLElement = status();
|
|
1057
|
+
|
|
1058
|
+
await clickResend();
|
|
1059
|
+
const second: HTMLElement = status();
|
|
1060
|
+
|
|
1061
|
+
expect(second).toHaveTextContent(FAILED);
|
|
1062
|
+
expect(second).not.toBe(first);
|
|
1063
|
+
});
|
|
1064
|
+
});
|
|
1065
|
+
|
|
1066
|
+
describe("focus", () => {
|
|
1067
|
+
test("moves to the result when nothing else holds focus", async () => {
|
|
1068
|
+
renderButton();
|
|
1069
|
+
|
|
1070
|
+
expect(document.body).toHaveFocus();
|
|
1071
|
+
|
|
1072
|
+
await clickResend();
|
|
1073
|
+
|
|
1074
|
+
const wrapper: HTMLElement | null = status().parentElement;
|
|
1075
|
+
expect(wrapper).toHaveAttribute("tabindex", "-1");
|
|
1076
|
+
expect(wrapper).toHaveFocus();
|
|
1077
|
+
});
|
|
1078
|
+
|
|
1079
|
+
test("moves to the result of a failure as well", async () => {
|
|
1080
|
+
answerResend = async (): Promise<Answer> => {
|
|
1081
|
+
throw new APIException("Network Error");
|
|
1082
|
+
};
|
|
1083
|
+
renderButton();
|
|
1084
|
+
|
|
1085
|
+
await clickResend();
|
|
1086
|
+
|
|
1087
|
+
expect(status().parentElement).toHaveFocus();
|
|
1088
|
+
});
|
|
1089
|
+
|
|
1090
|
+
test("never pulls someone out of a field they moved to meanwhile", async () => {
|
|
1091
|
+
const pending: Deferred = deferred();
|
|
1092
|
+
answerResend = (): Promise<Answer> => {
|
|
1093
|
+
return pending.promise;
|
|
1094
|
+
};
|
|
1095
|
+
render(
|
|
1096
|
+
<MemoryRouter>
|
|
1097
|
+
<ResendVerificationEmail credential={{ resendToken: RESEND_TOKEN }} />
|
|
1098
|
+
<input data-testid="elsewhere" aria-label="Elsewhere" />
|
|
1099
|
+
</MemoryRouter>,
|
|
1100
|
+
);
|
|
1101
|
+
|
|
1102
|
+
await clickResend();
|
|
1103
|
+
|
|
1104
|
+
const elsewhere: HTMLElement = screen.getByTestId("elsewhere");
|
|
1105
|
+
act(() => {
|
|
1106
|
+
elsewhere.focus();
|
|
1107
|
+
});
|
|
1108
|
+
|
|
1109
|
+
await act(async () => {
|
|
1110
|
+
pending.resolve(sent(60));
|
|
1111
|
+
});
|
|
1112
|
+
await settle();
|
|
1113
|
+
|
|
1114
|
+
expect(status()).toHaveTextContent(SENT);
|
|
1115
|
+
expect(elsewhere).toHaveFocus();
|
|
1116
|
+
});
|
|
1117
|
+
});
|
|
1118
|
+
|
|
1119
|
+
describe("lifecycle", () => {
|
|
1120
|
+
test("applies className to the root", () => {
|
|
1121
|
+
const { container } = renderButton({ className: "mt-6" });
|
|
1122
|
+
|
|
1123
|
+
expect(container.firstElementChild).toHaveClass("mt-6");
|
|
1124
|
+
expect(container.firstElementChild).toContainElement(button());
|
|
1125
|
+
});
|
|
1126
|
+
|
|
1127
|
+
test("an answer that arrives after unmount changes nothing and warns about nothing", async () => {
|
|
1128
|
+
const pending: Deferred = deferred();
|
|
1129
|
+
answerResend = (): Promise<Answer> => {
|
|
1130
|
+
return pending.promise;
|
|
1131
|
+
};
|
|
1132
|
+
const { unmount } = renderButton();
|
|
1133
|
+
|
|
1134
|
+
await clickResend();
|
|
1135
|
+
unmount();
|
|
1136
|
+
|
|
1137
|
+
await act(async () => {
|
|
1138
|
+
pending.resolve(sent(60));
|
|
1139
|
+
});
|
|
1140
|
+
await settle();
|
|
1141
|
+
|
|
1142
|
+
advance(120_000);
|
|
1143
|
+
expect(jest.getTimerCount()).toBe(0);
|
|
1144
|
+
});
|
|
1145
|
+
|
|
1146
|
+
test("no timers are left behind once a wait started by a click is unmounted", async () => {
|
|
1147
|
+
const { unmount } = renderButton();
|
|
1148
|
+
|
|
1149
|
+
await clickResend();
|
|
1150
|
+
expect(jest.getTimerCount()).toBe(1);
|
|
1151
|
+
|
|
1152
|
+
unmount();
|
|
1153
|
+
expect(jest.getTimerCount()).toBe(0);
|
|
1154
|
+
});
|
|
1155
|
+
});
|
|
1156
|
+
|
|
1157
|
+
describe("appearance", () => {
|
|
1158
|
+
test("is a full-width NORMAL button, never an OUTLINE one", () => {
|
|
1159
|
+
renderButton();
|
|
1160
|
+
|
|
1161
|
+
expect(button()).toBeEnabled();
|
|
1162
|
+
expectAccountsButtonStyle(button());
|
|
1163
|
+
});
|
|
1164
|
+
|
|
1165
|
+
test("keeps the same look while it waits and while it sends", async () => {
|
|
1166
|
+
renderButton({ initialCooldownSeconds: 5 });
|
|
1167
|
+
|
|
1168
|
+
expect(button()).toBeDisabled();
|
|
1169
|
+
expectAccountsButtonStyle(button());
|
|
1170
|
+
|
|
1171
|
+
advance(5000);
|
|
1172
|
+
const pending: Deferred = deferred();
|
|
1173
|
+
answerResend = (): Promise<Answer> => {
|
|
1174
|
+
return pending.promise;
|
|
1175
|
+
};
|
|
1176
|
+
await clickResend();
|
|
1177
|
+
|
|
1178
|
+
expect(button()).toBeDisabled();
|
|
1179
|
+
expectAccountsButtonStyle(button());
|
|
1180
|
+
|
|
1181
|
+
await act(async () => {
|
|
1182
|
+
pending.resolve(sent(60));
|
|
1183
|
+
});
|
|
1184
|
+
await settle();
|
|
1185
|
+
});
|
|
1186
|
+
|
|
1187
|
+
test("the check above fails for an OUTLINE button with the same classes and style", () => {
|
|
1188
|
+
/*
|
|
1189
|
+
* A control, so the check cannot pass by accident: the shared Button
|
|
1190
|
+
* in OUTLINE style, given exactly what the component passes, keeps
|
|
1191
|
+
* w-full, justify-center and the inline width -- those come from the
|
|
1192
|
+
* caller -- but loses NORMAL's border and background to the two
|
|
1193
|
+
* classes Accounts has no CSS for.
|
|
1194
|
+
*/
|
|
1195
|
+
render(
|
|
1196
|
+
<Button
|
|
1197
|
+
buttonStyle={ButtonStyleType.OUTLINE}
|
|
1198
|
+
title={BUTTON}
|
|
1199
|
+
dataTestId="outline-control"
|
|
1200
|
+
className={ACCOUNTS_BUTTON_CLASSES}
|
|
1201
|
+
style={{ width: "100%", marginLeft: 0 }}
|
|
1202
|
+
/>,
|
|
1203
|
+
);
|
|
1204
|
+
|
|
1205
|
+
const outline: HTMLElement = screen.getByTestId("outline-control");
|
|
1206
|
+
expect(outline).toHaveClass("w-full", "justify-center");
|
|
1207
|
+
expect(outline).toHaveStyle({ width: "100%", marginLeft: "0px" });
|
|
1208
|
+
|
|
1209
|
+
for (const className of NORMAL_STYLE_CLASSES) {
|
|
1210
|
+
expect(outline).not.toHaveClass(className);
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
for (const className of OUTLINE_STYLE_CLASSES) {
|
|
1214
|
+
expect(outline).toHaveClass(className);
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
expect(() => {
|
|
1218
|
+
expectAccountsButtonStyle(outline);
|
|
1219
|
+
}).toThrow();
|
|
1220
|
+
});
|
|
1221
|
+
});
|
|
1222
|
+
});
|