From 7029ca78bd19d2493ccd777c8848c878b2a1d630 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9rgio=20Silveira?= Date: Mon, 16 Dec 2024 17:03:39 +0100 Subject: [PATCH 01/15] moved all files into EventStore.Client project --- Directory.Build.props | 4 +-- EventStore.Client.sln.DotSettings | 1 + src/Directory.Build.props | 10 +++--- .../Certificates/X509Certificates.cs | 0 .../{ => Core}/ChannelBaseExtensions.cs | 0 .../{ => Core}/ChannelCache.cs | 0 .../{ => Core}/ChannelFactory.cs | 0 .../{ => Core}/ChannelInfo.cs | 0 .../{ => Core}/ChannelSelector.cs | 0 .../{ => Core}/ClusterMessage.cs | 0 .../Common/AsyncStreamReaderExtensions.cs | 0 .../{ => Core}/Common/Constants.cs | 0 .../Diagnostics/ActivitySourceExtensions.cs | 0 .../ActivityTagsCollectionExtensions.cs | 0 .../Diagnostics/Core/ActivityExtensions.cs | 0 .../Common/Diagnostics/Core/ActivityStatus.cs | 0 .../Core/ActivityStatusCodeHelper.cs | 0 .../Core/ActivityTagsCollectionExtensions.cs | 0 .../Diagnostics/Core/ExceptionExtensions.cs | 0 .../Core/Telemetry/TelemetryTags.cs | 0 .../Core/Tracing/TracingConstants.cs | 0 .../Core/Tracing/TracingMetadata.cs | 0 .../Diagnostics/EventMetadataExtensions.cs | 0 .../EventStoreClientDiagnostics.cs | 0 .../Diagnostics/Telemetry/TelemetryTags.cs | 0 .../Diagnostics/Tracing/TracingConstants.cs | 0 .../Common/EnumerableTaskExtensions.cs | 0 .../{ => Core}/Common/EpochExtensions.cs | 0 .../Common/EventStoreCallOptions.cs | 0 .../{ => Core}/Common/MetadataExtensions.cs | 0 .../{ => Core}/Common/Shims/Index.cs | 0 .../{ => Core}/Common/Shims/IsExternalInit.cs | 0 .../{ => Core}/Common/Shims/Range.cs | 0 .../Common/Shims/TaskCompletionSource.cs | 0 .../{ => Core}/Common/protos/code.proto | 0 .../{ => Core}/Common/protos/gossip.proto | 0 .../{ => Core}/Common/protos/operations.proto | 0 .../protos/persistentsubscriptions.proto | 0 .../Common/protos/projectionmanagement.proto | 0 .../Common/protos/serverfeatures.proto | 0 .../{ => Core}/Common/protos/shared.proto | 0 .../{ => Core}/Common/protos/status.proto | 0 .../{ => Core}/Common/protos/streams.proto | 0 .../Common/protos/usermanagement.proto | 0 .../DefaultRequestVersionHandler.cs | 0 .../{ => Core}/EndPointExtensions.cs | 0 src/EventStore.Client/{ => Core}/EventData.cs | 0 .../{ => Core}/EventRecord.cs | 0 .../{ => Core}/EventStoreClientBase.cs | 0 .../EventStoreClientConnectivitySettings.cs | 0 .../EventStoreClientOperationOptions.cs | 0 ...entStoreClientSettings.ConnectionString.cs | 0 .../{ => Core}/EventStoreClientSettings.cs | 0 .../{ => Core}/EventTypeFilter.cs | 0 .../Exceptions/AccessDeniedException.cs | 0 .../ConnectionStringParseException.cs | 0 .../ConnectionString/DuplicateKeyException.cs | 0 .../InvalidClientCertificateException.cs | 0 .../ConnectionString/InvalidHostException.cs | 0 .../InvalidKeyValuePairException.cs | 0 .../InvalidSchemeException.cs | 0 .../InvalidSettingException.cs | 0 .../InvalidUserCredentialsException.cs | 0 .../ConnectionString/NoSchemeException.cs | 0 .../Exceptions/DiscoveryException.cs | 0 .../Exceptions/NotAuthenticatedException.cs | 0 .../Exceptions/NotLeaderException.cs | 0 ...equiredMetadataPropertyMissingException.cs | 0 .../Exceptions/ScavengeNotFoundException.cs | 0 .../Exceptions/StreamDeletedException.cs | 0 .../Exceptions/StreamNotFoundException.cs | 0 .../Exceptions/UserNotFoundException.cs | 0 .../WrongExpectedVersionException.cs | 0 src/EventStore.Client/{ => Core}/FromAll.cs | 0 .../{ => Core}/FromStream.cs | 0 .../{ => Core}/GossipChannelSelector.cs | 0 .../{ => Core}/GrpcGossipClient.cs | 0 .../GrpcServerCapabilitiesClient.cs | 0 src/EventStore.Client/{ => Core}/HashCode.cs | 0 .../{ => Core}/HttpFallback.cs | 0 .../{ => Core}/IChannelSelector.cs | 0 .../{ => Core}/IEventFilter.cs | 0 .../{ => Core}/IGossipClient.cs | 0 src/EventStore.Client/{ => Core}/IPosition.cs | 0 .../{ => Core}/IServerCapabilitiesClient.cs | 0 .../Interceptors/ConnectionNameInterceptor.cs | 0 .../Interceptors/ReportLeaderInterceptor.cs | 0 .../Interceptors/TypedExceptionInterceptor.cs | 0 .../{ => Core}/NodePreference.cs | 0 .../{ => Core}/NodePreferenceComparers.cs | 0 .../{ => Core}/NodeSelector.cs | 0 src/EventStore.Client/{ => Core}/Position.cs | 0 .../{ => Core}/PrefixFilterExpression.cs | 0 .../{ => Core}/ReconnectionRequired.cs | 0 .../{ => Core}/RegularFilterExpression.cs | 0 .../{ => Core}/ResolvedEvent.cs | 0 .../{ => Core}/ServerCapabilities.cs | 0 .../{ => Core}/SharingProvider.cs | 0 .../{ => Core}/SingleNodeChannelSelector.cs | 0 .../{ => Core}/SingleNodeHttpHandler.cs | 0 .../{ => Core}/StreamFilter.cs | 0 .../{ => Core}/StreamIdentifier.cs | 0 .../{ => Core}/StreamPosition.cs | 0 .../{ => Core}/StreamRevision.cs | 0 .../{ => Core}/StreamState.cs | 0 .../{ => Core}/SubscriptionDroppedReason.cs | 0 .../{ => Core}/SystemRoles.cs | 0 .../{ => Core}/SystemStreams.cs | 0 .../{ => Core}/TaskExtensions.cs | 0 .../{ => Core}/UserCredentials.cs | 0 src/EventStore.Client/{ => Core}/Uuid.cs | 0 .../EventStore.Client.csproj | 35 ++++++++++++++++--- .../EventStore.Client.csproj.DotSettings | 4 ++- .../Operations}/DatabaseScavengeResult.cs | 0 .../EventStoreOperationsClient.Admin.cs | 0 .../EventStoreOperationsClient.Scavenge.cs | 0 .../Operations}/EventStoreOperationsClient.cs | 0 ...ationsClientServiceCollectionExtensions.cs | 0 .../Operations}/ScavengeResult.cs | 0 ...orePersistentSubscriptionsClient.Create.cs | 0 ...orePersistentSubscriptionsClient.Delete.cs | 0 ...StorePersistentSubscriptionsClient.Info.cs | 0 ...StorePersistentSubscriptionsClient.List.cs | 0 ...StorePersistentSubscriptionsClient.Read.cs | 0 ...sistentSubscriptionsClient.ReplayParked.cs | 0 ...entSubscriptionsClient.RestartSubsystem.cs | 0 ...orePersistentSubscriptionsClient.Update.cs | 0 ...EventStorePersistentSubscriptionsClient.cs | 0 ...SubscriptionsClientCollectionExtensions.cs | 0 .../MaximumSubscribersReachedException.cs | 0 .../PersistentSubscription.cs | 0 ...entSubscriptionDroppedByServerException.cs | 0 .../PersistentSubscriptionExtraStatistic.cs | 0 .../PersistentSubscriptionInfo.cs | 0 .../PersistentSubscriptionMessage.cs | 0 .../PersistentSubscriptionNakEventAction.cs | 0 ...PersistentSubscriptionNotFoundException.cs | 0 .../PersistentSubscriptionSettings.cs | 0 .../SystemConsumerStrategies.cs | 0 ...StoreProjectionManagementClient.Control.cs | 0 ...tStoreProjectionManagementClient.Create.cs | 0 ...ntStoreProjectionManagementClient.State.cs | 0 ...reProjectionManagementClient.Statistics.cs | 0 ...tStoreProjectionManagementClient.Update.cs | 0 .../EventStoreProjectionManagementClient.cs | 0 ...ionManagementClientCollectionExtensions.cs | 0 .../ProjectionDetails.cs | 0 .../Streams}/ConditionalWriteResult.cs | 0 .../Streams}/ConditionalWriteStatus.cs | 0 .../Streams}/DeadLine.cs | 0 .../Streams}/DeleteResult.cs | 0 .../Streams}/Direction.cs | 0 .../Streams}/EventStoreClient.Append.cs | 0 .../Streams}/EventStoreClient.Delete.cs | 0 .../Streams}/EventStoreClient.Metadata.cs | 0 .../Streams}/EventStoreClient.Read.cs | 0 .../EventStoreClient.Subscriptions.cs | 0 .../Streams}/EventStoreClient.Tombstone.cs | 0 .../Streams}/EventStoreClient.cs | 0 .../Streams}/EventStoreClientExtensions.cs | 0 ...tStoreClientServiceCollectionExtensions.cs | 0 .../Streams}/IWriteResult.cs | 0 .../Streams}/InvalidTransactionException.cs | 0 .../MaximumAppendSizeExceededException.cs | 0 .../Streams}/ReadState.cs | 0 .../Streams}/StreamAcl.cs | 0 .../Streams}/StreamAclJsonConverter.cs | 0 .../Streams}/StreamMessage.cs | 0 .../Streams}/StreamMetadata.cs | 0 .../Streams}/StreamMetadataJsonConverter.cs | 0 .../Streams}/StreamMetadataResult.cs | 0 .../Streams}/StreamSubscription.cs | 0 .../Streams}/Streams/AppendReq.cs | 0 .../Streams}/Streams/BatchAppendReq.cs | 0 .../Streams}/Streams/BatchAppendResp.cs | 0 .../Streams}/Streams/DeleteReq.cs | 0 .../Streams}/Streams/ReadReq.cs | 0 .../Streams}/Streams/TombstoneReq.cs | 0 .../Streams}/SubscriptionFilterOptions.cs | 0 .../Streams}/SuccessResult.cs | 0 .../Streams}/SystemEventTypes.cs | 0 .../Streams}/SystemMetadata.cs | 0 .../Streams}/SystemSettings.cs | 0 .../Streams}/SystemSettingsJsonConverter.cs | 0 .../Streams}/WriteResultExtensions.cs | 0 .../Streams}/WrongExpectedVersionResult.cs | 0 .../EventStoreUserManagementClient.cs | 0 ...serManagementClientCollectionExtensions.cs | 0 .../EventStoreUserManagerClientExtensions.cs | 0 .../UserManagement}/UserDetails.cs | 0 190 files changed, 41 insertions(+), 13 deletions(-) rename src/EventStore.Client/{ => Core}/Certificates/X509Certificates.cs (100%) rename src/EventStore.Client/{ => Core}/ChannelBaseExtensions.cs (100%) rename src/EventStore.Client/{ => Core}/ChannelCache.cs (100%) rename src/EventStore.Client/{ => Core}/ChannelFactory.cs (100%) rename src/EventStore.Client/{ => Core}/ChannelInfo.cs (100%) rename src/EventStore.Client/{ => Core}/ChannelSelector.cs (100%) rename src/EventStore.Client/{ => Core}/ClusterMessage.cs (100%) rename src/EventStore.Client/{ => Core}/Common/AsyncStreamReaderExtensions.cs (100%) rename src/EventStore.Client/{ => Core}/Common/Constants.cs (100%) rename src/EventStore.Client/{ => Core}/Common/Diagnostics/ActivitySourceExtensions.cs (100%) rename src/EventStore.Client/{ => Core}/Common/Diagnostics/ActivityTagsCollectionExtensions.cs (100%) rename src/EventStore.Client/{ => Core}/Common/Diagnostics/Core/ActivityExtensions.cs (100%) rename src/EventStore.Client/{ => Core}/Common/Diagnostics/Core/ActivityStatus.cs (100%) rename src/EventStore.Client/{ => Core}/Common/Diagnostics/Core/ActivityStatusCodeHelper.cs (100%) rename src/EventStore.Client/{ => Core}/Common/Diagnostics/Core/ActivityTagsCollectionExtensions.cs (100%) rename src/EventStore.Client/{ => Core}/Common/Diagnostics/Core/ExceptionExtensions.cs (100%) rename src/EventStore.Client/{ => Core}/Common/Diagnostics/Core/Telemetry/TelemetryTags.cs (100%) rename src/EventStore.Client/{ => Core}/Common/Diagnostics/Core/Tracing/TracingConstants.cs (100%) rename src/EventStore.Client/{ => Core}/Common/Diagnostics/Core/Tracing/TracingMetadata.cs (100%) rename src/EventStore.Client/{ => Core}/Common/Diagnostics/EventMetadataExtensions.cs (100%) rename src/EventStore.Client/{ => Core}/Common/Diagnostics/EventStoreClientDiagnostics.cs (100%) rename src/EventStore.Client/{ => Core}/Common/Diagnostics/Telemetry/TelemetryTags.cs (100%) rename src/EventStore.Client/{ => Core}/Common/Diagnostics/Tracing/TracingConstants.cs (100%) rename src/EventStore.Client/{ => Core}/Common/EnumerableTaskExtensions.cs (100%) rename src/EventStore.Client/{ => Core}/Common/EpochExtensions.cs (100%) rename src/EventStore.Client/{ => Core}/Common/EventStoreCallOptions.cs (100%) rename src/EventStore.Client/{ => Core}/Common/MetadataExtensions.cs (100%) rename src/EventStore.Client/{ => Core}/Common/Shims/Index.cs (100%) rename src/EventStore.Client/{ => Core}/Common/Shims/IsExternalInit.cs (100%) rename src/EventStore.Client/{ => Core}/Common/Shims/Range.cs (100%) rename src/EventStore.Client/{ => Core}/Common/Shims/TaskCompletionSource.cs (100%) rename src/EventStore.Client/{ => Core}/Common/protos/code.proto (100%) rename src/EventStore.Client/{ => Core}/Common/protos/gossip.proto (100%) rename src/EventStore.Client/{ => Core}/Common/protos/operations.proto (100%) rename src/EventStore.Client/{ => Core}/Common/protos/persistentsubscriptions.proto (100%) rename src/EventStore.Client/{ => Core}/Common/protos/projectionmanagement.proto (100%) rename src/EventStore.Client/{ => Core}/Common/protos/serverfeatures.proto (100%) rename src/EventStore.Client/{ => Core}/Common/protos/shared.proto (100%) rename src/EventStore.Client/{ => Core}/Common/protos/status.proto (100%) rename src/EventStore.Client/{ => Core}/Common/protos/streams.proto (100%) rename src/EventStore.Client/{ => Core}/Common/protos/usermanagement.proto (100%) rename src/EventStore.Client/{ => Core}/DefaultRequestVersionHandler.cs (100%) rename src/EventStore.Client/{ => Core}/EndPointExtensions.cs (100%) rename src/EventStore.Client/{ => Core}/EventData.cs (100%) rename src/EventStore.Client/{ => Core}/EventRecord.cs (100%) rename src/EventStore.Client/{ => Core}/EventStoreClientBase.cs (100%) rename src/EventStore.Client/{ => Core}/EventStoreClientConnectivitySettings.cs (100%) rename src/EventStore.Client/{ => Core}/EventStoreClientOperationOptions.cs (100%) rename src/EventStore.Client/{ => Core}/EventStoreClientSettings.ConnectionString.cs (100%) rename src/EventStore.Client/{ => Core}/EventStoreClientSettings.cs (100%) rename src/EventStore.Client/{ => Core}/EventTypeFilter.cs (100%) rename src/EventStore.Client/{ => Core}/Exceptions/AccessDeniedException.cs (100%) rename src/EventStore.Client/{ => Core}/Exceptions/ConnectionString/ConnectionStringParseException.cs (100%) rename src/EventStore.Client/{ => Core}/Exceptions/ConnectionString/DuplicateKeyException.cs (100%) rename src/EventStore.Client/{ => Core}/Exceptions/ConnectionString/InvalidClientCertificateException.cs (100%) rename src/EventStore.Client/{ => Core}/Exceptions/ConnectionString/InvalidHostException.cs (100%) rename src/EventStore.Client/{ => Core}/Exceptions/ConnectionString/InvalidKeyValuePairException.cs (100%) rename src/EventStore.Client/{ => Core}/Exceptions/ConnectionString/InvalidSchemeException.cs (100%) rename src/EventStore.Client/{ => Core}/Exceptions/ConnectionString/InvalidSettingException.cs (100%) rename src/EventStore.Client/{ => Core}/Exceptions/ConnectionString/InvalidUserCredentialsException.cs (100%) rename src/EventStore.Client/{ => Core}/Exceptions/ConnectionString/NoSchemeException.cs (100%) rename src/EventStore.Client/{ => Core}/Exceptions/DiscoveryException.cs (100%) rename src/EventStore.Client/{ => Core}/Exceptions/NotAuthenticatedException.cs (100%) rename src/EventStore.Client/{ => Core}/Exceptions/NotLeaderException.cs (100%) rename src/EventStore.Client/{ => Core}/Exceptions/RequiredMetadataPropertyMissingException.cs (100%) rename src/EventStore.Client/{ => Core}/Exceptions/ScavengeNotFoundException.cs (100%) rename src/EventStore.Client/{ => Core}/Exceptions/StreamDeletedException.cs (100%) rename src/EventStore.Client/{ => Core}/Exceptions/StreamNotFoundException.cs (100%) rename src/EventStore.Client/{ => Core}/Exceptions/UserNotFoundException.cs (100%) rename src/EventStore.Client/{ => Core}/Exceptions/WrongExpectedVersionException.cs (100%) rename src/EventStore.Client/{ => Core}/FromAll.cs (100%) rename src/EventStore.Client/{ => Core}/FromStream.cs (100%) rename src/EventStore.Client/{ => Core}/GossipChannelSelector.cs (100%) rename src/EventStore.Client/{ => Core}/GrpcGossipClient.cs (100%) rename src/EventStore.Client/{ => Core}/GrpcServerCapabilitiesClient.cs (100%) rename src/EventStore.Client/{ => Core}/HashCode.cs (100%) rename src/EventStore.Client/{ => Core}/HttpFallback.cs (100%) rename src/EventStore.Client/{ => Core}/IChannelSelector.cs (100%) rename src/EventStore.Client/{ => Core}/IEventFilter.cs (100%) rename src/EventStore.Client/{ => Core}/IGossipClient.cs (100%) rename src/EventStore.Client/{ => Core}/IPosition.cs (100%) rename src/EventStore.Client/{ => Core}/IServerCapabilitiesClient.cs (100%) rename src/EventStore.Client/{ => Core}/Interceptors/ConnectionNameInterceptor.cs (100%) rename src/EventStore.Client/{ => Core}/Interceptors/ReportLeaderInterceptor.cs (100%) rename src/EventStore.Client/{ => Core}/Interceptors/TypedExceptionInterceptor.cs (100%) rename src/EventStore.Client/{ => Core}/NodePreference.cs (100%) rename src/EventStore.Client/{ => Core}/NodePreferenceComparers.cs (100%) rename src/EventStore.Client/{ => Core}/NodeSelector.cs (100%) rename src/EventStore.Client/{ => Core}/Position.cs (100%) rename src/EventStore.Client/{ => Core}/PrefixFilterExpression.cs (100%) rename src/EventStore.Client/{ => Core}/ReconnectionRequired.cs (100%) rename src/EventStore.Client/{ => Core}/RegularFilterExpression.cs (100%) rename src/EventStore.Client/{ => Core}/ResolvedEvent.cs (100%) rename src/EventStore.Client/{ => Core}/ServerCapabilities.cs (100%) rename src/EventStore.Client/{ => Core}/SharingProvider.cs (100%) rename src/EventStore.Client/{ => Core}/SingleNodeChannelSelector.cs (100%) rename src/EventStore.Client/{ => Core}/SingleNodeHttpHandler.cs (100%) rename src/EventStore.Client/{ => Core}/StreamFilter.cs (100%) rename src/EventStore.Client/{ => Core}/StreamIdentifier.cs (100%) rename src/EventStore.Client/{ => Core}/StreamPosition.cs (100%) rename src/EventStore.Client/{ => Core}/StreamRevision.cs (100%) rename src/EventStore.Client/{ => Core}/StreamState.cs (100%) rename src/EventStore.Client/{ => Core}/SubscriptionDroppedReason.cs (100%) rename src/EventStore.Client/{ => Core}/SystemRoles.cs (100%) rename src/EventStore.Client/{ => Core}/SystemStreams.cs (100%) rename src/EventStore.Client/{ => Core}/TaskExtensions.cs (100%) rename src/EventStore.Client/{ => Core}/UserCredentials.cs (100%) rename src/EventStore.Client/{ => Core}/Uuid.cs (100%) rename src/{EventStore.Client.Operations => EventStore.Client/Operations}/DatabaseScavengeResult.cs (100%) rename src/{EventStore.Client.Operations => EventStore.Client/Operations}/EventStoreOperationsClient.Admin.cs (100%) rename src/{EventStore.Client.Operations => EventStore.Client/Operations}/EventStoreOperationsClient.Scavenge.cs (100%) rename src/{EventStore.Client.Operations => EventStore.Client/Operations}/EventStoreOperationsClient.cs (100%) rename src/{EventStore.Client.Operations => EventStore.Client/Operations}/EventStoreOperationsClientServiceCollectionExtensions.cs (100%) rename src/{EventStore.Client.Operations => EventStore.Client/Operations}/ScavengeResult.cs (100%) rename src/{EventStore.Client.PersistentSubscriptions => EventStore.Client/PersistentSubscriptions}/EventStorePersistentSubscriptionsClient.Create.cs (100%) rename src/{EventStore.Client.PersistentSubscriptions => EventStore.Client/PersistentSubscriptions}/EventStorePersistentSubscriptionsClient.Delete.cs (100%) rename src/{EventStore.Client.PersistentSubscriptions => EventStore.Client/PersistentSubscriptions}/EventStorePersistentSubscriptionsClient.Info.cs (100%) rename src/{EventStore.Client.PersistentSubscriptions => EventStore.Client/PersistentSubscriptions}/EventStorePersistentSubscriptionsClient.List.cs (100%) rename src/{EventStore.Client.PersistentSubscriptions => EventStore.Client/PersistentSubscriptions}/EventStorePersistentSubscriptionsClient.Read.cs (100%) rename src/{EventStore.Client.PersistentSubscriptions => EventStore.Client/PersistentSubscriptions}/EventStorePersistentSubscriptionsClient.ReplayParked.cs (100%) rename src/{EventStore.Client.PersistentSubscriptions => EventStore.Client/PersistentSubscriptions}/EventStorePersistentSubscriptionsClient.RestartSubsystem.cs (100%) rename src/{EventStore.Client.PersistentSubscriptions => EventStore.Client/PersistentSubscriptions}/EventStorePersistentSubscriptionsClient.Update.cs (100%) rename src/{EventStore.Client.PersistentSubscriptions => EventStore.Client/PersistentSubscriptions}/EventStorePersistentSubscriptionsClient.cs (100%) rename src/{EventStore.Client.PersistentSubscriptions => EventStore.Client/PersistentSubscriptions}/EventStorePersistentSubscriptionsClientCollectionExtensions.cs (100%) rename src/{EventStore.Client.PersistentSubscriptions => EventStore.Client/PersistentSubscriptions}/MaximumSubscribersReachedException.cs (100%) rename src/{EventStore.Client.PersistentSubscriptions => EventStore.Client/PersistentSubscriptions}/PersistentSubscription.cs (100%) rename src/{EventStore.Client.PersistentSubscriptions => EventStore.Client/PersistentSubscriptions}/PersistentSubscriptionDroppedByServerException.cs (100%) rename src/{EventStore.Client.PersistentSubscriptions => EventStore.Client/PersistentSubscriptions}/PersistentSubscriptionExtraStatistic.cs (100%) rename src/{EventStore.Client.PersistentSubscriptions => EventStore.Client/PersistentSubscriptions}/PersistentSubscriptionInfo.cs (100%) rename src/{EventStore.Client.PersistentSubscriptions => EventStore.Client/PersistentSubscriptions}/PersistentSubscriptionMessage.cs (100%) rename src/{EventStore.Client.PersistentSubscriptions => EventStore.Client/PersistentSubscriptions}/PersistentSubscriptionNakEventAction.cs (100%) rename src/{EventStore.Client.PersistentSubscriptions => EventStore.Client/PersistentSubscriptions}/PersistentSubscriptionNotFoundException.cs (100%) rename src/{EventStore.Client.PersistentSubscriptions => EventStore.Client/PersistentSubscriptions}/PersistentSubscriptionSettings.cs (100%) rename src/{EventStore.Client.PersistentSubscriptions => EventStore.Client/PersistentSubscriptions}/SystemConsumerStrategies.cs (100%) rename src/{EventStore.Client.ProjectionManagement => EventStore.Client/ProjectionManagement}/EventStoreProjectionManagementClient.Control.cs (100%) rename src/{EventStore.Client.ProjectionManagement => EventStore.Client/ProjectionManagement}/EventStoreProjectionManagementClient.Create.cs (100%) rename src/{EventStore.Client.ProjectionManagement => EventStore.Client/ProjectionManagement}/EventStoreProjectionManagementClient.State.cs (100%) rename src/{EventStore.Client.ProjectionManagement => EventStore.Client/ProjectionManagement}/EventStoreProjectionManagementClient.Statistics.cs (100%) rename src/{EventStore.Client.ProjectionManagement => EventStore.Client/ProjectionManagement}/EventStoreProjectionManagementClient.Update.cs (100%) rename src/{EventStore.Client.ProjectionManagement => EventStore.Client/ProjectionManagement}/EventStoreProjectionManagementClient.cs (100%) rename src/{EventStore.Client.ProjectionManagement => EventStore.Client/ProjectionManagement}/EventStoreProjectionManagementClientCollectionExtensions.cs (100%) rename src/{EventStore.Client.ProjectionManagement => EventStore.Client/ProjectionManagement}/ProjectionDetails.cs (100%) rename src/{EventStore.Client.Streams => EventStore.Client/Streams}/ConditionalWriteResult.cs (100%) rename src/{EventStore.Client.Streams => EventStore.Client/Streams}/ConditionalWriteStatus.cs (100%) rename src/{EventStore.Client.Streams => EventStore.Client/Streams}/DeadLine.cs (100%) rename src/{EventStore.Client.Streams => EventStore.Client/Streams}/DeleteResult.cs (100%) rename src/{EventStore.Client.Streams => EventStore.Client/Streams}/Direction.cs (100%) rename src/{EventStore.Client.Streams => EventStore.Client/Streams}/EventStoreClient.Append.cs (100%) rename src/{EventStore.Client.Streams => EventStore.Client/Streams}/EventStoreClient.Delete.cs (100%) rename src/{EventStore.Client.Streams => EventStore.Client/Streams}/EventStoreClient.Metadata.cs (100%) rename src/{EventStore.Client.Streams => EventStore.Client/Streams}/EventStoreClient.Read.cs (100%) rename src/{EventStore.Client.Streams => EventStore.Client/Streams}/EventStoreClient.Subscriptions.cs (100%) rename src/{EventStore.Client.Streams => EventStore.Client/Streams}/EventStoreClient.Tombstone.cs (100%) rename src/{EventStore.Client.Streams => EventStore.Client/Streams}/EventStoreClient.cs (100%) rename src/{EventStore.Client.Streams => EventStore.Client/Streams}/EventStoreClientExtensions.cs (100%) rename src/{EventStore.Client.Streams => EventStore.Client/Streams}/EventStoreClientServiceCollectionExtensions.cs (100%) rename src/{EventStore.Client.Streams => EventStore.Client/Streams}/IWriteResult.cs (100%) rename src/{EventStore.Client.Streams => EventStore.Client/Streams}/InvalidTransactionException.cs (100%) rename src/{EventStore.Client.Streams => EventStore.Client/Streams}/MaximumAppendSizeExceededException.cs (100%) rename src/{EventStore.Client.Streams => EventStore.Client/Streams}/ReadState.cs (100%) rename src/{EventStore.Client.Streams => EventStore.Client/Streams}/StreamAcl.cs (100%) rename src/{EventStore.Client.Streams => EventStore.Client/Streams}/StreamAclJsonConverter.cs (100%) rename src/{EventStore.Client.Streams => EventStore.Client/Streams}/StreamMessage.cs (100%) rename src/{EventStore.Client.Streams => EventStore.Client/Streams}/StreamMetadata.cs (100%) rename src/{EventStore.Client.Streams => EventStore.Client/Streams}/StreamMetadataJsonConverter.cs (100%) rename src/{EventStore.Client.Streams => EventStore.Client/Streams}/StreamMetadataResult.cs (100%) rename src/{EventStore.Client.Streams => EventStore.Client/Streams}/StreamSubscription.cs (100%) rename src/{EventStore.Client.Streams => EventStore.Client/Streams}/Streams/AppendReq.cs (100%) rename src/{EventStore.Client.Streams => EventStore.Client/Streams}/Streams/BatchAppendReq.cs (100%) rename src/{EventStore.Client.Streams => EventStore.Client/Streams}/Streams/BatchAppendResp.cs (100%) rename src/{EventStore.Client.Streams => EventStore.Client/Streams}/Streams/DeleteReq.cs (100%) rename src/{EventStore.Client.Streams => EventStore.Client/Streams}/Streams/ReadReq.cs (100%) rename src/{EventStore.Client.Streams => EventStore.Client/Streams}/Streams/TombstoneReq.cs (100%) rename src/{EventStore.Client.Streams => EventStore.Client/Streams}/SubscriptionFilterOptions.cs (100%) rename src/{EventStore.Client.Streams => EventStore.Client/Streams}/SuccessResult.cs (100%) rename src/{EventStore.Client.Streams => EventStore.Client/Streams}/SystemEventTypes.cs (100%) rename src/{EventStore.Client.Streams => EventStore.Client/Streams}/SystemMetadata.cs (100%) rename src/{EventStore.Client.Streams => EventStore.Client/Streams}/SystemSettings.cs (100%) rename src/{EventStore.Client.Streams => EventStore.Client/Streams}/SystemSettingsJsonConverter.cs (100%) rename src/{EventStore.Client.Streams => EventStore.Client/Streams}/WriteResultExtensions.cs (100%) rename src/{EventStore.Client.Streams => EventStore.Client/Streams}/WrongExpectedVersionResult.cs (100%) rename src/{EventStore.Client.UserManagement => EventStore.Client/UserManagement}/EventStoreUserManagementClient.cs (100%) rename src/{EventStore.Client.UserManagement => EventStore.Client/UserManagement}/EventStoreUserManagementClientCollectionExtensions.cs (100%) rename src/{EventStore.Client.UserManagement => EventStore.Client/UserManagement}/EventStoreUserManagerClientExtensions.cs (100%) rename src/{EventStore.Client.UserManagement => EventStore.Client/UserManagement}/UserDetails.cs (100%) diff --git a/Directory.Build.props b/Directory.Build.props index dd2dd9339..8dbeee46e 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,6 +1,6 @@ - net48;net6.0;net7.0;net8.0 + net48;net6.0;net7.0;net8.0;net9.0 true enable enable @@ -21,4 +21,4 @@ - + \ No newline at end of file diff --git a/EventStore.Client.sln.DotSettings b/EventStore.Client.sln.DotSettings index f97c72463..e12ac8bcf 100644 --- a/EventStore.Client.sln.DotSettings +++ b/EventStore.Client.sln.DotSettings @@ -390,6 +390,7 @@ True True True + True True True True diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 58ec83eff..28c9a1dfe 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -1,6 +1,6 @@ - + EventStore.Client @@ -16,7 +16,7 @@ EventStore.Client.Grpc.$(PackageIdSuffix) ../EventStore.Client/Common/protos/$(PackageIdSuffix.ToLower()).proto - + @@ -38,7 +38,7 @@ - + all @@ -50,7 +50,7 @@ - + @@ -68,4 +68,4 @@ - + \ No newline at end of file diff --git a/src/EventStore.Client/Certificates/X509Certificates.cs b/src/EventStore.Client/Core/Certificates/X509Certificates.cs similarity index 100% rename from src/EventStore.Client/Certificates/X509Certificates.cs rename to src/EventStore.Client/Core/Certificates/X509Certificates.cs diff --git a/src/EventStore.Client/ChannelBaseExtensions.cs b/src/EventStore.Client/Core/ChannelBaseExtensions.cs similarity index 100% rename from src/EventStore.Client/ChannelBaseExtensions.cs rename to src/EventStore.Client/Core/ChannelBaseExtensions.cs diff --git a/src/EventStore.Client/ChannelCache.cs b/src/EventStore.Client/Core/ChannelCache.cs similarity index 100% rename from src/EventStore.Client/ChannelCache.cs rename to src/EventStore.Client/Core/ChannelCache.cs diff --git a/src/EventStore.Client/ChannelFactory.cs b/src/EventStore.Client/Core/ChannelFactory.cs similarity index 100% rename from src/EventStore.Client/ChannelFactory.cs rename to src/EventStore.Client/Core/ChannelFactory.cs diff --git a/src/EventStore.Client/ChannelInfo.cs b/src/EventStore.Client/Core/ChannelInfo.cs similarity index 100% rename from src/EventStore.Client/ChannelInfo.cs rename to src/EventStore.Client/Core/ChannelInfo.cs diff --git a/src/EventStore.Client/ChannelSelector.cs b/src/EventStore.Client/Core/ChannelSelector.cs similarity index 100% rename from src/EventStore.Client/ChannelSelector.cs rename to src/EventStore.Client/Core/ChannelSelector.cs diff --git a/src/EventStore.Client/ClusterMessage.cs b/src/EventStore.Client/Core/ClusterMessage.cs similarity index 100% rename from src/EventStore.Client/ClusterMessage.cs rename to src/EventStore.Client/Core/ClusterMessage.cs diff --git a/src/EventStore.Client/Common/AsyncStreamReaderExtensions.cs b/src/EventStore.Client/Core/Common/AsyncStreamReaderExtensions.cs similarity index 100% rename from src/EventStore.Client/Common/AsyncStreamReaderExtensions.cs rename to src/EventStore.Client/Core/Common/AsyncStreamReaderExtensions.cs diff --git a/src/EventStore.Client/Common/Constants.cs b/src/EventStore.Client/Core/Common/Constants.cs similarity index 100% rename from src/EventStore.Client/Common/Constants.cs rename to src/EventStore.Client/Core/Common/Constants.cs diff --git a/src/EventStore.Client/Common/Diagnostics/ActivitySourceExtensions.cs b/src/EventStore.Client/Core/Common/Diagnostics/ActivitySourceExtensions.cs similarity index 100% rename from src/EventStore.Client/Common/Diagnostics/ActivitySourceExtensions.cs rename to src/EventStore.Client/Core/Common/Diagnostics/ActivitySourceExtensions.cs diff --git a/src/EventStore.Client/Common/Diagnostics/ActivityTagsCollectionExtensions.cs b/src/EventStore.Client/Core/Common/Diagnostics/ActivityTagsCollectionExtensions.cs similarity index 100% rename from src/EventStore.Client/Common/Diagnostics/ActivityTagsCollectionExtensions.cs rename to src/EventStore.Client/Core/Common/Diagnostics/ActivityTagsCollectionExtensions.cs diff --git a/src/EventStore.Client/Common/Diagnostics/Core/ActivityExtensions.cs b/src/EventStore.Client/Core/Common/Diagnostics/Core/ActivityExtensions.cs similarity index 100% rename from src/EventStore.Client/Common/Diagnostics/Core/ActivityExtensions.cs rename to src/EventStore.Client/Core/Common/Diagnostics/Core/ActivityExtensions.cs diff --git a/src/EventStore.Client/Common/Diagnostics/Core/ActivityStatus.cs b/src/EventStore.Client/Core/Common/Diagnostics/Core/ActivityStatus.cs similarity index 100% rename from src/EventStore.Client/Common/Diagnostics/Core/ActivityStatus.cs rename to src/EventStore.Client/Core/Common/Diagnostics/Core/ActivityStatus.cs diff --git a/src/EventStore.Client/Common/Diagnostics/Core/ActivityStatusCodeHelper.cs b/src/EventStore.Client/Core/Common/Diagnostics/Core/ActivityStatusCodeHelper.cs similarity index 100% rename from src/EventStore.Client/Common/Diagnostics/Core/ActivityStatusCodeHelper.cs rename to src/EventStore.Client/Core/Common/Diagnostics/Core/ActivityStatusCodeHelper.cs diff --git a/src/EventStore.Client/Common/Diagnostics/Core/ActivityTagsCollectionExtensions.cs b/src/EventStore.Client/Core/Common/Diagnostics/Core/ActivityTagsCollectionExtensions.cs similarity index 100% rename from src/EventStore.Client/Common/Diagnostics/Core/ActivityTagsCollectionExtensions.cs rename to src/EventStore.Client/Core/Common/Diagnostics/Core/ActivityTagsCollectionExtensions.cs diff --git a/src/EventStore.Client/Common/Diagnostics/Core/ExceptionExtensions.cs b/src/EventStore.Client/Core/Common/Diagnostics/Core/ExceptionExtensions.cs similarity index 100% rename from src/EventStore.Client/Common/Diagnostics/Core/ExceptionExtensions.cs rename to src/EventStore.Client/Core/Common/Diagnostics/Core/ExceptionExtensions.cs diff --git a/src/EventStore.Client/Common/Diagnostics/Core/Telemetry/TelemetryTags.cs b/src/EventStore.Client/Core/Common/Diagnostics/Core/Telemetry/TelemetryTags.cs similarity index 100% rename from src/EventStore.Client/Common/Diagnostics/Core/Telemetry/TelemetryTags.cs rename to src/EventStore.Client/Core/Common/Diagnostics/Core/Telemetry/TelemetryTags.cs diff --git a/src/EventStore.Client/Common/Diagnostics/Core/Tracing/TracingConstants.cs b/src/EventStore.Client/Core/Common/Diagnostics/Core/Tracing/TracingConstants.cs similarity index 100% rename from src/EventStore.Client/Common/Diagnostics/Core/Tracing/TracingConstants.cs rename to src/EventStore.Client/Core/Common/Diagnostics/Core/Tracing/TracingConstants.cs diff --git a/src/EventStore.Client/Common/Diagnostics/Core/Tracing/TracingMetadata.cs b/src/EventStore.Client/Core/Common/Diagnostics/Core/Tracing/TracingMetadata.cs similarity index 100% rename from src/EventStore.Client/Common/Diagnostics/Core/Tracing/TracingMetadata.cs rename to src/EventStore.Client/Core/Common/Diagnostics/Core/Tracing/TracingMetadata.cs diff --git a/src/EventStore.Client/Common/Diagnostics/EventMetadataExtensions.cs b/src/EventStore.Client/Core/Common/Diagnostics/EventMetadataExtensions.cs similarity index 100% rename from src/EventStore.Client/Common/Diagnostics/EventMetadataExtensions.cs rename to src/EventStore.Client/Core/Common/Diagnostics/EventMetadataExtensions.cs diff --git a/src/EventStore.Client/Common/Diagnostics/EventStoreClientDiagnostics.cs b/src/EventStore.Client/Core/Common/Diagnostics/EventStoreClientDiagnostics.cs similarity index 100% rename from src/EventStore.Client/Common/Diagnostics/EventStoreClientDiagnostics.cs rename to src/EventStore.Client/Core/Common/Diagnostics/EventStoreClientDiagnostics.cs diff --git a/src/EventStore.Client/Common/Diagnostics/Telemetry/TelemetryTags.cs b/src/EventStore.Client/Core/Common/Diagnostics/Telemetry/TelemetryTags.cs similarity index 100% rename from src/EventStore.Client/Common/Diagnostics/Telemetry/TelemetryTags.cs rename to src/EventStore.Client/Core/Common/Diagnostics/Telemetry/TelemetryTags.cs diff --git a/src/EventStore.Client/Common/Diagnostics/Tracing/TracingConstants.cs b/src/EventStore.Client/Core/Common/Diagnostics/Tracing/TracingConstants.cs similarity index 100% rename from src/EventStore.Client/Common/Diagnostics/Tracing/TracingConstants.cs rename to src/EventStore.Client/Core/Common/Diagnostics/Tracing/TracingConstants.cs diff --git a/src/EventStore.Client/Common/EnumerableTaskExtensions.cs b/src/EventStore.Client/Core/Common/EnumerableTaskExtensions.cs similarity index 100% rename from src/EventStore.Client/Common/EnumerableTaskExtensions.cs rename to src/EventStore.Client/Core/Common/EnumerableTaskExtensions.cs diff --git a/src/EventStore.Client/Common/EpochExtensions.cs b/src/EventStore.Client/Core/Common/EpochExtensions.cs similarity index 100% rename from src/EventStore.Client/Common/EpochExtensions.cs rename to src/EventStore.Client/Core/Common/EpochExtensions.cs diff --git a/src/EventStore.Client/Common/EventStoreCallOptions.cs b/src/EventStore.Client/Core/Common/EventStoreCallOptions.cs similarity index 100% rename from src/EventStore.Client/Common/EventStoreCallOptions.cs rename to src/EventStore.Client/Core/Common/EventStoreCallOptions.cs diff --git a/src/EventStore.Client/Common/MetadataExtensions.cs b/src/EventStore.Client/Core/Common/MetadataExtensions.cs similarity index 100% rename from src/EventStore.Client/Common/MetadataExtensions.cs rename to src/EventStore.Client/Core/Common/MetadataExtensions.cs diff --git a/src/EventStore.Client/Common/Shims/Index.cs b/src/EventStore.Client/Core/Common/Shims/Index.cs similarity index 100% rename from src/EventStore.Client/Common/Shims/Index.cs rename to src/EventStore.Client/Core/Common/Shims/Index.cs diff --git a/src/EventStore.Client/Common/Shims/IsExternalInit.cs b/src/EventStore.Client/Core/Common/Shims/IsExternalInit.cs similarity index 100% rename from src/EventStore.Client/Common/Shims/IsExternalInit.cs rename to src/EventStore.Client/Core/Common/Shims/IsExternalInit.cs diff --git a/src/EventStore.Client/Common/Shims/Range.cs b/src/EventStore.Client/Core/Common/Shims/Range.cs similarity index 100% rename from src/EventStore.Client/Common/Shims/Range.cs rename to src/EventStore.Client/Core/Common/Shims/Range.cs diff --git a/src/EventStore.Client/Common/Shims/TaskCompletionSource.cs b/src/EventStore.Client/Core/Common/Shims/TaskCompletionSource.cs similarity index 100% rename from src/EventStore.Client/Common/Shims/TaskCompletionSource.cs rename to src/EventStore.Client/Core/Common/Shims/TaskCompletionSource.cs diff --git a/src/EventStore.Client/Common/protos/code.proto b/src/EventStore.Client/Core/Common/protos/code.proto similarity index 100% rename from src/EventStore.Client/Common/protos/code.proto rename to src/EventStore.Client/Core/Common/protos/code.proto diff --git a/src/EventStore.Client/Common/protos/gossip.proto b/src/EventStore.Client/Core/Common/protos/gossip.proto similarity index 100% rename from src/EventStore.Client/Common/protos/gossip.proto rename to src/EventStore.Client/Core/Common/protos/gossip.proto diff --git a/src/EventStore.Client/Common/protos/operations.proto b/src/EventStore.Client/Core/Common/protos/operations.proto similarity index 100% rename from src/EventStore.Client/Common/protos/operations.proto rename to src/EventStore.Client/Core/Common/protos/operations.proto diff --git a/src/EventStore.Client/Common/protos/persistentsubscriptions.proto b/src/EventStore.Client/Core/Common/protos/persistentsubscriptions.proto similarity index 100% rename from src/EventStore.Client/Common/protos/persistentsubscriptions.proto rename to src/EventStore.Client/Core/Common/protos/persistentsubscriptions.proto diff --git a/src/EventStore.Client/Common/protos/projectionmanagement.proto b/src/EventStore.Client/Core/Common/protos/projectionmanagement.proto similarity index 100% rename from src/EventStore.Client/Common/protos/projectionmanagement.proto rename to src/EventStore.Client/Core/Common/protos/projectionmanagement.proto diff --git a/src/EventStore.Client/Common/protos/serverfeatures.proto b/src/EventStore.Client/Core/Common/protos/serverfeatures.proto similarity index 100% rename from src/EventStore.Client/Common/protos/serverfeatures.proto rename to src/EventStore.Client/Core/Common/protos/serverfeatures.proto diff --git a/src/EventStore.Client/Common/protos/shared.proto b/src/EventStore.Client/Core/Common/protos/shared.proto similarity index 100% rename from src/EventStore.Client/Common/protos/shared.proto rename to src/EventStore.Client/Core/Common/protos/shared.proto diff --git a/src/EventStore.Client/Common/protos/status.proto b/src/EventStore.Client/Core/Common/protos/status.proto similarity index 100% rename from src/EventStore.Client/Common/protos/status.proto rename to src/EventStore.Client/Core/Common/protos/status.proto diff --git a/src/EventStore.Client/Common/protos/streams.proto b/src/EventStore.Client/Core/Common/protos/streams.proto similarity index 100% rename from src/EventStore.Client/Common/protos/streams.proto rename to src/EventStore.Client/Core/Common/protos/streams.proto diff --git a/src/EventStore.Client/Common/protos/usermanagement.proto b/src/EventStore.Client/Core/Common/protos/usermanagement.proto similarity index 100% rename from src/EventStore.Client/Common/protos/usermanagement.proto rename to src/EventStore.Client/Core/Common/protos/usermanagement.proto diff --git a/src/EventStore.Client/DefaultRequestVersionHandler.cs b/src/EventStore.Client/Core/DefaultRequestVersionHandler.cs similarity index 100% rename from src/EventStore.Client/DefaultRequestVersionHandler.cs rename to src/EventStore.Client/Core/DefaultRequestVersionHandler.cs diff --git a/src/EventStore.Client/EndPointExtensions.cs b/src/EventStore.Client/Core/EndPointExtensions.cs similarity index 100% rename from src/EventStore.Client/EndPointExtensions.cs rename to src/EventStore.Client/Core/EndPointExtensions.cs diff --git a/src/EventStore.Client/EventData.cs b/src/EventStore.Client/Core/EventData.cs similarity index 100% rename from src/EventStore.Client/EventData.cs rename to src/EventStore.Client/Core/EventData.cs diff --git a/src/EventStore.Client/EventRecord.cs b/src/EventStore.Client/Core/EventRecord.cs similarity index 100% rename from src/EventStore.Client/EventRecord.cs rename to src/EventStore.Client/Core/EventRecord.cs diff --git a/src/EventStore.Client/EventStoreClientBase.cs b/src/EventStore.Client/Core/EventStoreClientBase.cs similarity index 100% rename from src/EventStore.Client/EventStoreClientBase.cs rename to src/EventStore.Client/Core/EventStoreClientBase.cs diff --git a/src/EventStore.Client/EventStoreClientConnectivitySettings.cs b/src/EventStore.Client/Core/EventStoreClientConnectivitySettings.cs similarity index 100% rename from src/EventStore.Client/EventStoreClientConnectivitySettings.cs rename to src/EventStore.Client/Core/EventStoreClientConnectivitySettings.cs diff --git a/src/EventStore.Client/EventStoreClientOperationOptions.cs b/src/EventStore.Client/Core/EventStoreClientOperationOptions.cs similarity index 100% rename from src/EventStore.Client/EventStoreClientOperationOptions.cs rename to src/EventStore.Client/Core/EventStoreClientOperationOptions.cs diff --git a/src/EventStore.Client/EventStoreClientSettings.ConnectionString.cs b/src/EventStore.Client/Core/EventStoreClientSettings.ConnectionString.cs similarity index 100% rename from src/EventStore.Client/EventStoreClientSettings.ConnectionString.cs rename to src/EventStore.Client/Core/EventStoreClientSettings.ConnectionString.cs diff --git a/src/EventStore.Client/EventStoreClientSettings.cs b/src/EventStore.Client/Core/EventStoreClientSettings.cs similarity index 100% rename from src/EventStore.Client/EventStoreClientSettings.cs rename to src/EventStore.Client/Core/EventStoreClientSettings.cs diff --git a/src/EventStore.Client/EventTypeFilter.cs b/src/EventStore.Client/Core/EventTypeFilter.cs similarity index 100% rename from src/EventStore.Client/EventTypeFilter.cs rename to src/EventStore.Client/Core/EventTypeFilter.cs diff --git a/src/EventStore.Client/Exceptions/AccessDeniedException.cs b/src/EventStore.Client/Core/Exceptions/AccessDeniedException.cs similarity index 100% rename from src/EventStore.Client/Exceptions/AccessDeniedException.cs rename to src/EventStore.Client/Core/Exceptions/AccessDeniedException.cs diff --git a/src/EventStore.Client/Exceptions/ConnectionString/ConnectionStringParseException.cs b/src/EventStore.Client/Core/Exceptions/ConnectionString/ConnectionStringParseException.cs similarity index 100% rename from src/EventStore.Client/Exceptions/ConnectionString/ConnectionStringParseException.cs rename to src/EventStore.Client/Core/Exceptions/ConnectionString/ConnectionStringParseException.cs diff --git a/src/EventStore.Client/Exceptions/ConnectionString/DuplicateKeyException.cs b/src/EventStore.Client/Core/Exceptions/ConnectionString/DuplicateKeyException.cs similarity index 100% rename from src/EventStore.Client/Exceptions/ConnectionString/DuplicateKeyException.cs rename to src/EventStore.Client/Core/Exceptions/ConnectionString/DuplicateKeyException.cs diff --git a/src/EventStore.Client/Exceptions/ConnectionString/InvalidClientCertificateException.cs b/src/EventStore.Client/Core/Exceptions/ConnectionString/InvalidClientCertificateException.cs similarity index 100% rename from src/EventStore.Client/Exceptions/ConnectionString/InvalidClientCertificateException.cs rename to src/EventStore.Client/Core/Exceptions/ConnectionString/InvalidClientCertificateException.cs diff --git a/src/EventStore.Client/Exceptions/ConnectionString/InvalidHostException.cs b/src/EventStore.Client/Core/Exceptions/ConnectionString/InvalidHostException.cs similarity index 100% rename from src/EventStore.Client/Exceptions/ConnectionString/InvalidHostException.cs rename to src/EventStore.Client/Core/Exceptions/ConnectionString/InvalidHostException.cs diff --git a/src/EventStore.Client/Exceptions/ConnectionString/InvalidKeyValuePairException.cs b/src/EventStore.Client/Core/Exceptions/ConnectionString/InvalidKeyValuePairException.cs similarity index 100% rename from src/EventStore.Client/Exceptions/ConnectionString/InvalidKeyValuePairException.cs rename to src/EventStore.Client/Core/Exceptions/ConnectionString/InvalidKeyValuePairException.cs diff --git a/src/EventStore.Client/Exceptions/ConnectionString/InvalidSchemeException.cs b/src/EventStore.Client/Core/Exceptions/ConnectionString/InvalidSchemeException.cs similarity index 100% rename from src/EventStore.Client/Exceptions/ConnectionString/InvalidSchemeException.cs rename to src/EventStore.Client/Core/Exceptions/ConnectionString/InvalidSchemeException.cs diff --git a/src/EventStore.Client/Exceptions/ConnectionString/InvalidSettingException.cs b/src/EventStore.Client/Core/Exceptions/ConnectionString/InvalidSettingException.cs similarity index 100% rename from src/EventStore.Client/Exceptions/ConnectionString/InvalidSettingException.cs rename to src/EventStore.Client/Core/Exceptions/ConnectionString/InvalidSettingException.cs diff --git a/src/EventStore.Client/Exceptions/ConnectionString/InvalidUserCredentialsException.cs b/src/EventStore.Client/Core/Exceptions/ConnectionString/InvalidUserCredentialsException.cs similarity index 100% rename from src/EventStore.Client/Exceptions/ConnectionString/InvalidUserCredentialsException.cs rename to src/EventStore.Client/Core/Exceptions/ConnectionString/InvalidUserCredentialsException.cs diff --git a/src/EventStore.Client/Exceptions/ConnectionString/NoSchemeException.cs b/src/EventStore.Client/Core/Exceptions/ConnectionString/NoSchemeException.cs similarity index 100% rename from src/EventStore.Client/Exceptions/ConnectionString/NoSchemeException.cs rename to src/EventStore.Client/Core/Exceptions/ConnectionString/NoSchemeException.cs diff --git a/src/EventStore.Client/Exceptions/DiscoveryException.cs b/src/EventStore.Client/Core/Exceptions/DiscoveryException.cs similarity index 100% rename from src/EventStore.Client/Exceptions/DiscoveryException.cs rename to src/EventStore.Client/Core/Exceptions/DiscoveryException.cs diff --git a/src/EventStore.Client/Exceptions/NotAuthenticatedException.cs b/src/EventStore.Client/Core/Exceptions/NotAuthenticatedException.cs similarity index 100% rename from src/EventStore.Client/Exceptions/NotAuthenticatedException.cs rename to src/EventStore.Client/Core/Exceptions/NotAuthenticatedException.cs diff --git a/src/EventStore.Client/Exceptions/NotLeaderException.cs b/src/EventStore.Client/Core/Exceptions/NotLeaderException.cs similarity index 100% rename from src/EventStore.Client/Exceptions/NotLeaderException.cs rename to src/EventStore.Client/Core/Exceptions/NotLeaderException.cs diff --git a/src/EventStore.Client/Exceptions/RequiredMetadataPropertyMissingException.cs b/src/EventStore.Client/Core/Exceptions/RequiredMetadataPropertyMissingException.cs similarity index 100% rename from src/EventStore.Client/Exceptions/RequiredMetadataPropertyMissingException.cs rename to src/EventStore.Client/Core/Exceptions/RequiredMetadataPropertyMissingException.cs diff --git a/src/EventStore.Client/Exceptions/ScavengeNotFoundException.cs b/src/EventStore.Client/Core/Exceptions/ScavengeNotFoundException.cs similarity index 100% rename from src/EventStore.Client/Exceptions/ScavengeNotFoundException.cs rename to src/EventStore.Client/Core/Exceptions/ScavengeNotFoundException.cs diff --git a/src/EventStore.Client/Exceptions/StreamDeletedException.cs b/src/EventStore.Client/Core/Exceptions/StreamDeletedException.cs similarity index 100% rename from src/EventStore.Client/Exceptions/StreamDeletedException.cs rename to src/EventStore.Client/Core/Exceptions/StreamDeletedException.cs diff --git a/src/EventStore.Client/Exceptions/StreamNotFoundException.cs b/src/EventStore.Client/Core/Exceptions/StreamNotFoundException.cs similarity index 100% rename from src/EventStore.Client/Exceptions/StreamNotFoundException.cs rename to src/EventStore.Client/Core/Exceptions/StreamNotFoundException.cs diff --git a/src/EventStore.Client/Exceptions/UserNotFoundException.cs b/src/EventStore.Client/Core/Exceptions/UserNotFoundException.cs similarity index 100% rename from src/EventStore.Client/Exceptions/UserNotFoundException.cs rename to src/EventStore.Client/Core/Exceptions/UserNotFoundException.cs diff --git a/src/EventStore.Client/Exceptions/WrongExpectedVersionException.cs b/src/EventStore.Client/Core/Exceptions/WrongExpectedVersionException.cs similarity index 100% rename from src/EventStore.Client/Exceptions/WrongExpectedVersionException.cs rename to src/EventStore.Client/Core/Exceptions/WrongExpectedVersionException.cs diff --git a/src/EventStore.Client/FromAll.cs b/src/EventStore.Client/Core/FromAll.cs similarity index 100% rename from src/EventStore.Client/FromAll.cs rename to src/EventStore.Client/Core/FromAll.cs diff --git a/src/EventStore.Client/FromStream.cs b/src/EventStore.Client/Core/FromStream.cs similarity index 100% rename from src/EventStore.Client/FromStream.cs rename to src/EventStore.Client/Core/FromStream.cs diff --git a/src/EventStore.Client/GossipChannelSelector.cs b/src/EventStore.Client/Core/GossipChannelSelector.cs similarity index 100% rename from src/EventStore.Client/GossipChannelSelector.cs rename to src/EventStore.Client/Core/GossipChannelSelector.cs diff --git a/src/EventStore.Client/GrpcGossipClient.cs b/src/EventStore.Client/Core/GrpcGossipClient.cs similarity index 100% rename from src/EventStore.Client/GrpcGossipClient.cs rename to src/EventStore.Client/Core/GrpcGossipClient.cs diff --git a/src/EventStore.Client/GrpcServerCapabilitiesClient.cs b/src/EventStore.Client/Core/GrpcServerCapabilitiesClient.cs similarity index 100% rename from src/EventStore.Client/GrpcServerCapabilitiesClient.cs rename to src/EventStore.Client/Core/GrpcServerCapabilitiesClient.cs diff --git a/src/EventStore.Client/HashCode.cs b/src/EventStore.Client/Core/HashCode.cs similarity index 100% rename from src/EventStore.Client/HashCode.cs rename to src/EventStore.Client/Core/HashCode.cs diff --git a/src/EventStore.Client/HttpFallback.cs b/src/EventStore.Client/Core/HttpFallback.cs similarity index 100% rename from src/EventStore.Client/HttpFallback.cs rename to src/EventStore.Client/Core/HttpFallback.cs diff --git a/src/EventStore.Client/IChannelSelector.cs b/src/EventStore.Client/Core/IChannelSelector.cs similarity index 100% rename from src/EventStore.Client/IChannelSelector.cs rename to src/EventStore.Client/Core/IChannelSelector.cs diff --git a/src/EventStore.Client/IEventFilter.cs b/src/EventStore.Client/Core/IEventFilter.cs similarity index 100% rename from src/EventStore.Client/IEventFilter.cs rename to src/EventStore.Client/Core/IEventFilter.cs diff --git a/src/EventStore.Client/IGossipClient.cs b/src/EventStore.Client/Core/IGossipClient.cs similarity index 100% rename from src/EventStore.Client/IGossipClient.cs rename to src/EventStore.Client/Core/IGossipClient.cs diff --git a/src/EventStore.Client/IPosition.cs b/src/EventStore.Client/Core/IPosition.cs similarity index 100% rename from src/EventStore.Client/IPosition.cs rename to src/EventStore.Client/Core/IPosition.cs diff --git a/src/EventStore.Client/IServerCapabilitiesClient.cs b/src/EventStore.Client/Core/IServerCapabilitiesClient.cs similarity index 100% rename from src/EventStore.Client/IServerCapabilitiesClient.cs rename to src/EventStore.Client/Core/IServerCapabilitiesClient.cs diff --git a/src/EventStore.Client/Interceptors/ConnectionNameInterceptor.cs b/src/EventStore.Client/Core/Interceptors/ConnectionNameInterceptor.cs similarity index 100% rename from src/EventStore.Client/Interceptors/ConnectionNameInterceptor.cs rename to src/EventStore.Client/Core/Interceptors/ConnectionNameInterceptor.cs diff --git a/src/EventStore.Client/Interceptors/ReportLeaderInterceptor.cs b/src/EventStore.Client/Core/Interceptors/ReportLeaderInterceptor.cs similarity index 100% rename from src/EventStore.Client/Interceptors/ReportLeaderInterceptor.cs rename to src/EventStore.Client/Core/Interceptors/ReportLeaderInterceptor.cs diff --git a/src/EventStore.Client/Interceptors/TypedExceptionInterceptor.cs b/src/EventStore.Client/Core/Interceptors/TypedExceptionInterceptor.cs similarity index 100% rename from src/EventStore.Client/Interceptors/TypedExceptionInterceptor.cs rename to src/EventStore.Client/Core/Interceptors/TypedExceptionInterceptor.cs diff --git a/src/EventStore.Client/NodePreference.cs b/src/EventStore.Client/Core/NodePreference.cs similarity index 100% rename from src/EventStore.Client/NodePreference.cs rename to src/EventStore.Client/Core/NodePreference.cs diff --git a/src/EventStore.Client/NodePreferenceComparers.cs b/src/EventStore.Client/Core/NodePreferenceComparers.cs similarity index 100% rename from src/EventStore.Client/NodePreferenceComparers.cs rename to src/EventStore.Client/Core/NodePreferenceComparers.cs diff --git a/src/EventStore.Client/NodeSelector.cs b/src/EventStore.Client/Core/NodeSelector.cs similarity index 100% rename from src/EventStore.Client/NodeSelector.cs rename to src/EventStore.Client/Core/NodeSelector.cs diff --git a/src/EventStore.Client/Position.cs b/src/EventStore.Client/Core/Position.cs similarity index 100% rename from src/EventStore.Client/Position.cs rename to src/EventStore.Client/Core/Position.cs diff --git a/src/EventStore.Client/PrefixFilterExpression.cs b/src/EventStore.Client/Core/PrefixFilterExpression.cs similarity index 100% rename from src/EventStore.Client/PrefixFilterExpression.cs rename to src/EventStore.Client/Core/PrefixFilterExpression.cs diff --git a/src/EventStore.Client/ReconnectionRequired.cs b/src/EventStore.Client/Core/ReconnectionRequired.cs similarity index 100% rename from src/EventStore.Client/ReconnectionRequired.cs rename to src/EventStore.Client/Core/ReconnectionRequired.cs diff --git a/src/EventStore.Client/RegularFilterExpression.cs b/src/EventStore.Client/Core/RegularFilterExpression.cs similarity index 100% rename from src/EventStore.Client/RegularFilterExpression.cs rename to src/EventStore.Client/Core/RegularFilterExpression.cs diff --git a/src/EventStore.Client/ResolvedEvent.cs b/src/EventStore.Client/Core/ResolvedEvent.cs similarity index 100% rename from src/EventStore.Client/ResolvedEvent.cs rename to src/EventStore.Client/Core/ResolvedEvent.cs diff --git a/src/EventStore.Client/ServerCapabilities.cs b/src/EventStore.Client/Core/ServerCapabilities.cs similarity index 100% rename from src/EventStore.Client/ServerCapabilities.cs rename to src/EventStore.Client/Core/ServerCapabilities.cs diff --git a/src/EventStore.Client/SharingProvider.cs b/src/EventStore.Client/Core/SharingProvider.cs similarity index 100% rename from src/EventStore.Client/SharingProvider.cs rename to src/EventStore.Client/Core/SharingProvider.cs diff --git a/src/EventStore.Client/SingleNodeChannelSelector.cs b/src/EventStore.Client/Core/SingleNodeChannelSelector.cs similarity index 100% rename from src/EventStore.Client/SingleNodeChannelSelector.cs rename to src/EventStore.Client/Core/SingleNodeChannelSelector.cs diff --git a/src/EventStore.Client/SingleNodeHttpHandler.cs b/src/EventStore.Client/Core/SingleNodeHttpHandler.cs similarity index 100% rename from src/EventStore.Client/SingleNodeHttpHandler.cs rename to src/EventStore.Client/Core/SingleNodeHttpHandler.cs diff --git a/src/EventStore.Client/StreamFilter.cs b/src/EventStore.Client/Core/StreamFilter.cs similarity index 100% rename from src/EventStore.Client/StreamFilter.cs rename to src/EventStore.Client/Core/StreamFilter.cs diff --git a/src/EventStore.Client/StreamIdentifier.cs b/src/EventStore.Client/Core/StreamIdentifier.cs similarity index 100% rename from src/EventStore.Client/StreamIdentifier.cs rename to src/EventStore.Client/Core/StreamIdentifier.cs diff --git a/src/EventStore.Client/StreamPosition.cs b/src/EventStore.Client/Core/StreamPosition.cs similarity index 100% rename from src/EventStore.Client/StreamPosition.cs rename to src/EventStore.Client/Core/StreamPosition.cs diff --git a/src/EventStore.Client/StreamRevision.cs b/src/EventStore.Client/Core/StreamRevision.cs similarity index 100% rename from src/EventStore.Client/StreamRevision.cs rename to src/EventStore.Client/Core/StreamRevision.cs diff --git a/src/EventStore.Client/StreamState.cs b/src/EventStore.Client/Core/StreamState.cs similarity index 100% rename from src/EventStore.Client/StreamState.cs rename to src/EventStore.Client/Core/StreamState.cs diff --git a/src/EventStore.Client/SubscriptionDroppedReason.cs b/src/EventStore.Client/Core/SubscriptionDroppedReason.cs similarity index 100% rename from src/EventStore.Client/SubscriptionDroppedReason.cs rename to src/EventStore.Client/Core/SubscriptionDroppedReason.cs diff --git a/src/EventStore.Client/SystemRoles.cs b/src/EventStore.Client/Core/SystemRoles.cs similarity index 100% rename from src/EventStore.Client/SystemRoles.cs rename to src/EventStore.Client/Core/SystemRoles.cs diff --git a/src/EventStore.Client/SystemStreams.cs b/src/EventStore.Client/Core/SystemStreams.cs similarity index 100% rename from src/EventStore.Client/SystemStreams.cs rename to src/EventStore.Client/Core/SystemStreams.cs diff --git a/src/EventStore.Client/TaskExtensions.cs b/src/EventStore.Client/Core/TaskExtensions.cs similarity index 100% rename from src/EventStore.Client/TaskExtensions.cs rename to src/EventStore.Client/Core/TaskExtensions.cs diff --git a/src/EventStore.Client/UserCredentials.cs b/src/EventStore.Client/Core/UserCredentials.cs similarity index 100% rename from src/EventStore.Client/UserCredentials.cs rename to src/EventStore.Client/Core/UserCredentials.cs diff --git a/src/EventStore.Client/Uuid.cs b/src/EventStore.Client/Core/Uuid.cs similarity index 100% rename from src/EventStore.Client/Uuid.cs rename to src/EventStore.Client/Core/Uuid.cs diff --git a/src/EventStore.Client/EventStore.Client.csproj b/src/EventStore.Client/EventStore.Client.csproj index 47a116602..f783a93cc 100644 --- a/src/EventStore.Client/EventStore.Client.csproj +++ b/src/EventStore.Client/EventStore.Client.csproj @@ -18,16 +18,41 @@ - + + + + + + + - + + + + + MSBuild:Compile + + + MSBuild:Compile + + + MSBuild:Compile + + + MSBuild:Compile + + + MSBuild:Compile + + diff --git a/src/EventStore.Client/EventStore.Client.csproj.DotSettings b/src/EventStore.Client/EventStore.Client.csproj.DotSettings index 9b89548c6..ae68911e8 100644 --- a/src/EventStore.Client/EventStore.Client.csproj.DotSettings +++ b/src/EventStore.Client/EventStore.Client.csproj.DotSettings @@ -1,3 +1,5 @@  True - True \ No newline at end of file + True + True + True \ No newline at end of file diff --git a/src/EventStore.Client.Operations/DatabaseScavengeResult.cs b/src/EventStore.Client/Operations/DatabaseScavengeResult.cs similarity index 100% rename from src/EventStore.Client.Operations/DatabaseScavengeResult.cs rename to src/EventStore.Client/Operations/DatabaseScavengeResult.cs diff --git a/src/EventStore.Client.Operations/EventStoreOperationsClient.Admin.cs b/src/EventStore.Client/Operations/EventStoreOperationsClient.Admin.cs similarity index 100% rename from src/EventStore.Client.Operations/EventStoreOperationsClient.Admin.cs rename to src/EventStore.Client/Operations/EventStoreOperationsClient.Admin.cs diff --git a/src/EventStore.Client.Operations/EventStoreOperationsClient.Scavenge.cs b/src/EventStore.Client/Operations/EventStoreOperationsClient.Scavenge.cs similarity index 100% rename from src/EventStore.Client.Operations/EventStoreOperationsClient.Scavenge.cs rename to src/EventStore.Client/Operations/EventStoreOperationsClient.Scavenge.cs diff --git a/src/EventStore.Client.Operations/EventStoreOperationsClient.cs b/src/EventStore.Client/Operations/EventStoreOperationsClient.cs similarity index 100% rename from src/EventStore.Client.Operations/EventStoreOperationsClient.cs rename to src/EventStore.Client/Operations/EventStoreOperationsClient.cs diff --git a/src/EventStore.Client.Operations/EventStoreOperationsClientServiceCollectionExtensions.cs b/src/EventStore.Client/Operations/EventStoreOperationsClientServiceCollectionExtensions.cs similarity index 100% rename from src/EventStore.Client.Operations/EventStoreOperationsClientServiceCollectionExtensions.cs rename to src/EventStore.Client/Operations/EventStoreOperationsClientServiceCollectionExtensions.cs diff --git a/src/EventStore.Client.Operations/ScavengeResult.cs b/src/EventStore.Client/Operations/ScavengeResult.cs similarity index 100% rename from src/EventStore.Client.Operations/ScavengeResult.cs rename to src/EventStore.Client/Operations/ScavengeResult.cs diff --git a/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Create.cs b/src/EventStore.Client/PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Create.cs similarity index 100% rename from src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Create.cs rename to src/EventStore.Client/PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Create.cs diff --git a/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Delete.cs b/src/EventStore.Client/PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Delete.cs similarity index 100% rename from src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Delete.cs rename to src/EventStore.Client/PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Delete.cs diff --git a/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Info.cs b/src/EventStore.Client/PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Info.cs similarity index 100% rename from src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Info.cs rename to src/EventStore.Client/PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Info.cs diff --git a/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.List.cs b/src/EventStore.Client/PersistentSubscriptions/EventStorePersistentSubscriptionsClient.List.cs similarity index 100% rename from src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.List.cs rename to src/EventStore.Client/PersistentSubscriptions/EventStorePersistentSubscriptionsClient.List.cs diff --git a/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Read.cs b/src/EventStore.Client/PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Read.cs similarity index 100% rename from src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Read.cs rename to src/EventStore.Client/PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Read.cs diff --git a/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.ReplayParked.cs b/src/EventStore.Client/PersistentSubscriptions/EventStorePersistentSubscriptionsClient.ReplayParked.cs similarity index 100% rename from src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.ReplayParked.cs rename to src/EventStore.Client/PersistentSubscriptions/EventStorePersistentSubscriptionsClient.ReplayParked.cs diff --git a/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.RestartSubsystem.cs b/src/EventStore.Client/PersistentSubscriptions/EventStorePersistentSubscriptionsClient.RestartSubsystem.cs similarity index 100% rename from src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.RestartSubsystem.cs rename to src/EventStore.Client/PersistentSubscriptions/EventStorePersistentSubscriptionsClient.RestartSubsystem.cs diff --git a/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Update.cs b/src/EventStore.Client/PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Update.cs similarity index 100% rename from src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Update.cs rename to src/EventStore.Client/PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Update.cs diff --git a/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.cs b/src/EventStore.Client/PersistentSubscriptions/EventStorePersistentSubscriptionsClient.cs similarity index 100% rename from src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.cs rename to src/EventStore.Client/PersistentSubscriptions/EventStorePersistentSubscriptionsClient.cs diff --git a/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClientCollectionExtensions.cs b/src/EventStore.Client/PersistentSubscriptions/EventStorePersistentSubscriptionsClientCollectionExtensions.cs similarity index 100% rename from src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClientCollectionExtensions.cs rename to src/EventStore.Client/PersistentSubscriptions/EventStorePersistentSubscriptionsClientCollectionExtensions.cs diff --git a/src/EventStore.Client.PersistentSubscriptions/MaximumSubscribersReachedException.cs b/src/EventStore.Client/PersistentSubscriptions/MaximumSubscribersReachedException.cs similarity index 100% rename from src/EventStore.Client.PersistentSubscriptions/MaximumSubscribersReachedException.cs rename to src/EventStore.Client/PersistentSubscriptions/MaximumSubscribersReachedException.cs diff --git a/src/EventStore.Client.PersistentSubscriptions/PersistentSubscription.cs b/src/EventStore.Client/PersistentSubscriptions/PersistentSubscription.cs similarity index 100% rename from src/EventStore.Client.PersistentSubscriptions/PersistentSubscription.cs rename to src/EventStore.Client/PersistentSubscriptions/PersistentSubscription.cs diff --git a/src/EventStore.Client.PersistentSubscriptions/PersistentSubscriptionDroppedByServerException.cs b/src/EventStore.Client/PersistentSubscriptions/PersistentSubscriptionDroppedByServerException.cs similarity index 100% rename from src/EventStore.Client.PersistentSubscriptions/PersistentSubscriptionDroppedByServerException.cs rename to src/EventStore.Client/PersistentSubscriptions/PersistentSubscriptionDroppedByServerException.cs diff --git a/src/EventStore.Client.PersistentSubscriptions/PersistentSubscriptionExtraStatistic.cs b/src/EventStore.Client/PersistentSubscriptions/PersistentSubscriptionExtraStatistic.cs similarity index 100% rename from src/EventStore.Client.PersistentSubscriptions/PersistentSubscriptionExtraStatistic.cs rename to src/EventStore.Client/PersistentSubscriptions/PersistentSubscriptionExtraStatistic.cs diff --git a/src/EventStore.Client.PersistentSubscriptions/PersistentSubscriptionInfo.cs b/src/EventStore.Client/PersistentSubscriptions/PersistentSubscriptionInfo.cs similarity index 100% rename from src/EventStore.Client.PersistentSubscriptions/PersistentSubscriptionInfo.cs rename to src/EventStore.Client/PersistentSubscriptions/PersistentSubscriptionInfo.cs diff --git a/src/EventStore.Client.PersistentSubscriptions/PersistentSubscriptionMessage.cs b/src/EventStore.Client/PersistentSubscriptions/PersistentSubscriptionMessage.cs similarity index 100% rename from src/EventStore.Client.PersistentSubscriptions/PersistentSubscriptionMessage.cs rename to src/EventStore.Client/PersistentSubscriptions/PersistentSubscriptionMessage.cs diff --git a/src/EventStore.Client.PersistentSubscriptions/PersistentSubscriptionNakEventAction.cs b/src/EventStore.Client/PersistentSubscriptions/PersistentSubscriptionNakEventAction.cs similarity index 100% rename from src/EventStore.Client.PersistentSubscriptions/PersistentSubscriptionNakEventAction.cs rename to src/EventStore.Client/PersistentSubscriptions/PersistentSubscriptionNakEventAction.cs diff --git a/src/EventStore.Client.PersistentSubscriptions/PersistentSubscriptionNotFoundException.cs b/src/EventStore.Client/PersistentSubscriptions/PersistentSubscriptionNotFoundException.cs similarity index 100% rename from src/EventStore.Client.PersistentSubscriptions/PersistentSubscriptionNotFoundException.cs rename to src/EventStore.Client/PersistentSubscriptions/PersistentSubscriptionNotFoundException.cs diff --git a/src/EventStore.Client.PersistentSubscriptions/PersistentSubscriptionSettings.cs b/src/EventStore.Client/PersistentSubscriptions/PersistentSubscriptionSettings.cs similarity index 100% rename from src/EventStore.Client.PersistentSubscriptions/PersistentSubscriptionSettings.cs rename to src/EventStore.Client/PersistentSubscriptions/PersistentSubscriptionSettings.cs diff --git a/src/EventStore.Client.PersistentSubscriptions/SystemConsumerStrategies.cs b/src/EventStore.Client/PersistentSubscriptions/SystemConsumerStrategies.cs similarity index 100% rename from src/EventStore.Client.PersistentSubscriptions/SystemConsumerStrategies.cs rename to src/EventStore.Client/PersistentSubscriptions/SystemConsumerStrategies.cs diff --git a/src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.Control.cs b/src/EventStore.Client/ProjectionManagement/EventStoreProjectionManagementClient.Control.cs similarity index 100% rename from src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.Control.cs rename to src/EventStore.Client/ProjectionManagement/EventStoreProjectionManagementClient.Control.cs diff --git a/src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.Create.cs b/src/EventStore.Client/ProjectionManagement/EventStoreProjectionManagementClient.Create.cs similarity index 100% rename from src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.Create.cs rename to src/EventStore.Client/ProjectionManagement/EventStoreProjectionManagementClient.Create.cs diff --git a/src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.State.cs b/src/EventStore.Client/ProjectionManagement/EventStoreProjectionManagementClient.State.cs similarity index 100% rename from src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.State.cs rename to src/EventStore.Client/ProjectionManagement/EventStoreProjectionManagementClient.State.cs diff --git a/src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.Statistics.cs b/src/EventStore.Client/ProjectionManagement/EventStoreProjectionManagementClient.Statistics.cs similarity index 100% rename from src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.Statistics.cs rename to src/EventStore.Client/ProjectionManagement/EventStoreProjectionManagementClient.Statistics.cs diff --git a/src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.Update.cs b/src/EventStore.Client/ProjectionManagement/EventStoreProjectionManagementClient.Update.cs similarity index 100% rename from src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.Update.cs rename to src/EventStore.Client/ProjectionManagement/EventStoreProjectionManagementClient.Update.cs diff --git a/src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.cs b/src/EventStore.Client/ProjectionManagement/EventStoreProjectionManagementClient.cs similarity index 100% rename from src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.cs rename to src/EventStore.Client/ProjectionManagement/EventStoreProjectionManagementClient.cs diff --git a/src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClientCollectionExtensions.cs b/src/EventStore.Client/ProjectionManagement/EventStoreProjectionManagementClientCollectionExtensions.cs similarity index 100% rename from src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClientCollectionExtensions.cs rename to src/EventStore.Client/ProjectionManagement/EventStoreProjectionManagementClientCollectionExtensions.cs diff --git a/src/EventStore.Client.ProjectionManagement/ProjectionDetails.cs b/src/EventStore.Client/ProjectionManagement/ProjectionDetails.cs similarity index 100% rename from src/EventStore.Client.ProjectionManagement/ProjectionDetails.cs rename to src/EventStore.Client/ProjectionManagement/ProjectionDetails.cs diff --git a/src/EventStore.Client.Streams/ConditionalWriteResult.cs b/src/EventStore.Client/Streams/ConditionalWriteResult.cs similarity index 100% rename from src/EventStore.Client.Streams/ConditionalWriteResult.cs rename to src/EventStore.Client/Streams/ConditionalWriteResult.cs diff --git a/src/EventStore.Client.Streams/ConditionalWriteStatus.cs b/src/EventStore.Client/Streams/ConditionalWriteStatus.cs similarity index 100% rename from src/EventStore.Client.Streams/ConditionalWriteStatus.cs rename to src/EventStore.Client/Streams/ConditionalWriteStatus.cs diff --git a/src/EventStore.Client.Streams/DeadLine.cs b/src/EventStore.Client/Streams/DeadLine.cs similarity index 100% rename from src/EventStore.Client.Streams/DeadLine.cs rename to src/EventStore.Client/Streams/DeadLine.cs diff --git a/src/EventStore.Client.Streams/DeleteResult.cs b/src/EventStore.Client/Streams/DeleteResult.cs similarity index 100% rename from src/EventStore.Client.Streams/DeleteResult.cs rename to src/EventStore.Client/Streams/DeleteResult.cs diff --git a/src/EventStore.Client.Streams/Direction.cs b/src/EventStore.Client/Streams/Direction.cs similarity index 100% rename from src/EventStore.Client.Streams/Direction.cs rename to src/EventStore.Client/Streams/Direction.cs diff --git a/src/EventStore.Client.Streams/EventStoreClient.Append.cs b/src/EventStore.Client/Streams/EventStoreClient.Append.cs similarity index 100% rename from src/EventStore.Client.Streams/EventStoreClient.Append.cs rename to src/EventStore.Client/Streams/EventStoreClient.Append.cs diff --git a/src/EventStore.Client.Streams/EventStoreClient.Delete.cs b/src/EventStore.Client/Streams/EventStoreClient.Delete.cs similarity index 100% rename from src/EventStore.Client.Streams/EventStoreClient.Delete.cs rename to src/EventStore.Client/Streams/EventStoreClient.Delete.cs diff --git a/src/EventStore.Client.Streams/EventStoreClient.Metadata.cs b/src/EventStore.Client/Streams/EventStoreClient.Metadata.cs similarity index 100% rename from src/EventStore.Client.Streams/EventStoreClient.Metadata.cs rename to src/EventStore.Client/Streams/EventStoreClient.Metadata.cs diff --git a/src/EventStore.Client.Streams/EventStoreClient.Read.cs b/src/EventStore.Client/Streams/EventStoreClient.Read.cs similarity index 100% rename from src/EventStore.Client.Streams/EventStoreClient.Read.cs rename to src/EventStore.Client/Streams/EventStoreClient.Read.cs diff --git a/src/EventStore.Client.Streams/EventStoreClient.Subscriptions.cs b/src/EventStore.Client/Streams/EventStoreClient.Subscriptions.cs similarity index 100% rename from src/EventStore.Client.Streams/EventStoreClient.Subscriptions.cs rename to src/EventStore.Client/Streams/EventStoreClient.Subscriptions.cs diff --git a/src/EventStore.Client.Streams/EventStoreClient.Tombstone.cs b/src/EventStore.Client/Streams/EventStoreClient.Tombstone.cs similarity index 100% rename from src/EventStore.Client.Streams/EventStoreClient.Tombstone.cs rename to src/EventStore.Client/Streams/EventStoreClient.Tombstone.cs diff --git a/src/EventStore.Client.Streams/EventStoreClient.cs b/src/EventStore.Client/Streams/EventStoreClient.cs similarity index 100% rename from src/EventStore.Client.Streams/EventStoreClient.cs rename to src/EventStore.Client/Streams/EventStoreClient.cs diff --git a/src/EventStore.Client.Streams/EventStoreClientExtensions.cs b/src/EventStore.Client/Streams/EventStoreClientExtensions.cs similarity index 100% rename from src/EventStore.Client.Streams/EventStoreClientExtensions.cs rename to src/EventStore.Client/Streams/EventStoreClientExtensions.cs diff --git a/src/EventStore.Client.Streams/EventStoreClientServiceCollectionExtensions.cs b/src/EventStore.Client/Streams/EventStoreClientServiceCollectionExtensions.cs similarity index 100% rename from src/EventStore.Client.Streams/EventStoreClientServiceCollectionExtensions.cs rename to src/EventStore.Client/Streams/EventStoreClientServiceCollectionExtensions.cs diff --git a/src/EventStore.Client.Streams/IWriteResult.cs b/src/EventStore.Client/Streams/IWriteResult.cs similarity index 100% rename from src/EventStore.Client.Streams/IWriteResult.cs rename to src/EventStore.Client/Streams/IWriteResult.cs diff --git a/src/EventStore.Client.Streams/InvalidTransactionException.cs b/src/EventStore.Client/Streams/InvalidTransactionException.cs similarity index 100% rename from src/EventStore.Client.Streams/InvalidTransactionException.cs rename to src/EventStore.Client/Streams/InvalidTransactionException.cs diff --git a/src/EventStore.Client.Streams/MaximumAppendSizeExceededException.cs b/src/EventStore.Client/Streams/MaximumAppendSizeExceededException.cs similarity index 100% rename from src/EventStore.Client.Streams/MaximumAppendSizeExceededException.cs rename to src/EventStore.Client/Streams/MaximumAppendSizeExceededException.cs diff --git a/src/EventStore.Client.Streams/ReadState.cs b/src/EventStore.Client/Streams/ReadState.cs similarity index 100% rename from src/EventStore.Client.Streams/ReadState.cs rename to src/EventStore.Client/Streams/ReadState.cs diff --git a/src/EventStore.Client.Streams/StreamAcl.cs b/src/EventStore.Client/Streams/StreamAcl.cs similarity index 100% rename from src/EventStore.Client.Streams/StreamAcl.cs rename to src/EventStore.Client/Streams/StreamAcl.cs diff --git a/src/EventStore.Client.Streams/StreamAclJsonConverter.cs b/src/EventStore.Client/Streams/StreamAclJsonConverter.cs similarity index 100% rename from src/EventStore.Client.Streams/StreamAclJsonConverter.cs rename to src/EventStore.Client/Streams/StreamAclJsonConverter.cs diff --git a/src/EventStore.Client.Streams/StreamMessage.cs b/src/EventStore.Client/Streams/StreamMessage.cs similarity index 100% rename from src/EventStore.Client.Streams/StreamMessage.cs rename to src/EventStore.Client/Streams/StreamMessage.cs diff --git a/src/EventStore.Client.Streams/StreamMetadata.cs b/src/EventStore.Client/Streams/StreamMetadata.cs similarity index 100% rename from src/EventStore.Client.Streams/StreamMetadata.cs rename to src/EventStore.Client/Streams/StreamMetadata.cs diff --git a/src/EventStore.Client.Streams/StreamMetadataJsonConverter.cs b/src/EventStore.Client/Streams/StreamMetadataJsonConverter.cs similarity index 100% rename from src/EventStore.Client.Streams/StreamMetadataJsonConverter.cs rename to src/EventStore.Client/Streams/StreamMetadataJsonConverter.cs diff --git a/src/EventStore.Client.Streams/StreamMetadataResult.cs b/src/EventStore.Client/Streams/StreamMetadataResult.cs similarity index 100% rename from src/EventStore.Client.Streams/StreamMetadataResult.cs rename to src/EventStore.Client/Streams/StreamMetadataResult.cs diff --git a/src/EventStore.Client.Streams/StreamSubscription.cs b/src/EventStore.Client/Streams/StreamSubscription.cs similarity index 100% rename from src/EventStore.Client.Streams/StreamSubscription.cs rename to src/EventStore.Client/Streams/StreamSubscription.cs diff --git a/src/EventStore.Client.Streams/Streams/AppendReq.cs b/src/EventStore.Client/Streams/Streams/AppendReq.cs similarity index 100% rename from src/EventStore.Client.Streams/Streams/AppendReq.cs rename to src/EventStore.Client/Streams/Streams/AppendReq.cs diff --git a/src/EventStore.Client.Streams/Streams/BatchAppendReq.cs b/src/EventStore.Client/Streams/Streams/BatchAppendReq.cs similarity index 100% rename from src/EventStore.Client.Streams/Streams/BatchAppendReq.cs rename to src/EventStore.Client/Streams/Streams/BatchAppendReq.cs diff --git a/src/EventStore.Client.Streams/Streams/BatchAppendResp.cs b/src/EventStore.Client/Streams/Streams/BatchAppendResp.cs similarity index 100% rename from src/EventStore.Client.Streams/Streams/BatchAppendResp.cs rename to src/EventStore.Client/Streams/Streams/BatchAppendResp.cs diff --git a/src/EventStore.Client.Streams/Streams/DeleteReq.cs b/src/EventStore.Client/Streams/Streams/DeleteReq.cs similarity index 100% rename from src/EventStore.Client.Streams/Streams/DeleteReq.cs rename to src/EventStore.Client/Streams/Streams/DeleteReq.cs diff --git a/src/EventStore.Client.Streams/Streams/ReadReq.cs b/src/EventStore.Client/Streams/Streams/ReadReq.cs similarity index 100% rename from src/EventStore.Client.Streams/Streams/ReadReq.cs rename to src/EventStore.Client/Streams/Streams/ReadReq.cs diff --git a/src/EventStore.Client.Streams/Streams/TombstoneReq.cs b/src/EventStore.Client/Streams/Streams/TombstoneReq.cs similarity index 100% rename from src/EventStore.Client.Streams/Streams/TombstoneReq.cs rename to src/EventStore.Client/Streams/Streams/TombstoneReq.cs diff --git a/src/EventStore.Client.Streams/SubscriptionFilterOptions.cs b/src/EventStore.Client/Streams/SubscriptionFilterOptions.cs similarity index 100% rename from src/EventStore.Client.Streams/SubscriptionFilterOptions.cs rename to src/EventStore.Client/Streams/SubscriptionFilterOptions.cs diff --git a/src/EventStore.Client.Streams/SuccessResult.cs b/src/EventStore.Client/Streams/SuccessResult.cs similarity index 100% rename from src/EventStore.Client.Streams/SuccessResult.cs rename to src/EventStore.Client/Streams/SuccessResult.cs diff --git a/src/EventStore.Client.Streams/SystemEventTypes.cs b/src/EventStore.Client/Streams/SystemEventTypes.cs similarity index 100% rename from src/EventStore.Client.Streams/SystemEventTypes.cs rename to src/EventStore.Client/Streams/SystemEventTypes.cs diff --git a/src/EventStore.Client.Streams/SystemMetadata.cs b/src/EventStore.Client/Streams/SystemMetadata.cs similarity index 100% rename from src/EventStore.Client.Streams/SystemMetadata.cs rename to src/EventStore.Client/Streams/SystemMetadata.cs diff --git a/src/EventStore.Client.Streams/SystemSettings.cs b/src/EventStore.Client/Streams/SystemSettings.cs similarity index 100% rename from src/EventStore.Client.Streams/SystemSettings.cs rename to src/EventStore.Client/Streams/SystemSettings.cs diff --git a/src/EventStore.Client.Streams/SystemSettingsJsonConverter.cs b/src/EventStore.Client/Streams/SystemSettingsJsonConverter.cs similarity index 100% rename from src/EventStore.Client.Streams/SystemSettingsJsonConverter.cs rename to src/EventStore.Client/Streams/SystemSettingsJsonConverter.cs diff --git a/src/EventStore.Client.Streams/WriteResultExtensions.cs b/src/EventStore.Client/Streams/WriteResultExtensions.cs similarity index 100% rename from src/EventStore.Client.Streams/WriteResultExtensions.cs rename to src/EventStore.Client/Streams/WriteResultExtensions.cs diff --git a/src/EventStore.Client.Streams/WrongExpectedVersionResult.cs b/src/EventStore.Client/Streams/WrongExpectedVersionResult.cs similarity index 100% rename from src/EventStore.Client.Streams/WrongExpectedVersionResult.cs rename to src/EventStore.Client/Streams/WrongExpectedVersionResult.cs diff --git a/src/EventStore.Client.UserManagement/EventStoreUserManagementClient.cs b/src/EventStore.Client/UserManagement/EventStoreUserManagementClient.cs similarity index 100% rename from src/EventStore.Client.UserManagement/EventStoreUserManagementClient.cs rename to src/EventStore.Client/UserManagement/EventStoreUserManagementClient.cs diff --git a/src/EventStore.Client.UserManagement/EventStoreUserManagementClientCollectionExtensions.cs b/src/EventStore.Client/UserManagement/EventStoreUserManagementClientCollectionExtensions.cs similarity index 100% rename from src/EventStore.Client.UserManagement/EventStoreUserManagementClientCollectionExtensions.cs rename to src/EventStore.Client/UserManagement/EventStoreUserManagementClientCollectionExtensions.cs diff --git a/src/EventStore.Client.UserManagement/EventStoreUserManagerClientExtensions.cs b/src/EventStore.Client/UserManagement/EventStoreUserManagerClientExtensions.cs similarity index 100% rename from src/EventStore.Client.UserManagement/EventStoreUserManagerClientExtensions.cs rename to src/EventStore.Client/UserManagement/EventStoreUserManagerClientExtensions.cs diff --git a/src/EventStore.Client.UserManagement/UserDetails.cs b/src/EventStore.Client/UserManagement/UserDetails.cs similarity index 100% rename from src/EventStore.Client.UserManagement/UserDetails.cs rename to src/EventStore.Client/UserManagement/UserDetails.cs From 2cdf1f002a1d23532bd12ffe4cbfd5b96db3658e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9rgio=20Silveira?= Date: Wed, 18 Dec 2024 17:07:03 +0100 Subject: [PATCH 02/15] moved files into main project and deleted remaining empty projects dropped support for net6.0 and net7.0 added some helpers to fluentdocker playing around the idea of keeping the container on tests --- Directory.Build.props | 5 +- EventStore.Client.sln | 35 --- src/Directory.Build.props | 50 ++--- ...ore.Client.Extensions.OpenTelemetry.csproj | 4 + .../EventStore.Client.Operations.csproj | 6 - ...tore.Client.PersistentSubscriptions.csproj | 6 - ...ntStore.Client.ProjectionManagement.csproj | 6 - .../EventStore.Client.Streams.csproj | 6 - .../EventStore.Client.UserManagement.csproj | 9 - ...e.Client.UserManagement.csproj.DotSettings | 2 - .../Core/StreamIdentifier.cs | 4 +- src/EventStore.Client/Core/Uuid.cs | 6 +- .../Core/{Common => }/protos/code.proto | 0 .../Core/{Common => }/protos/gossip.proto | 0 .../Core/{Common => }/protos/operations.proto | 0 .../protos/persistentsubscriptions.proto | 0 .../protos/projectionmanagement.proto | 0 .../{Common => }/protos/serverfeatures.proto | 0 .../Core/{Common => }/protos/shared.proto | 0 .../Core/{Common => }/protos/status.proto | 0 .../Core/{Common => }/protos/streams.proto | 0 .../{Common => }/protos/usermanagement.proto | 0 .../EventStore.Client.csproj | 66 +++--- test/Directory.Build.props | 19 +- .../EventStore.Client.Operations.Tests.csproj | 4 +- ...lient.PersistentSubscriptions.Tests.csproj | 6 +- .../EventStore.Client.Plugins.Tests.csproj | 4 +- ...e.Client.ProjectionManagement.Tests.csproj | 2 +- .../EventStore.Client.Streams.Tests.csproj | 10 +- .../EventStore.Client.Tests.Common.csproj | 44 ++-- .../Fixtures/EventStoreTestNode.cs | 33 +-- .../FluentDockerBuilderExtensions.cs | 64 ++++++ .../FluentDockerServiceExtensions.cs | 26 +++ .../EventStore.Client.Tests.csproj | 14 +- .../GrpcServerCapabilitiesClientTests.cs | 204 +++++++++--------- ...ntStore.Client.UserManagement.Tests.csproj | 4 +- 36 files changed, 317 insertions(+), 322 deletions(-) delete mode 100644 src/EventStore.Client.Operations/EventStore.Client.Operations.csproj delete mode 100644 src/EventStore.Client.PersistentSubscriptions/EventStore.Client.PersistentSubscriptions.csproj delete mode 100644 src/EventStore.Client.ProjectionManagement/EventStore.Client.ProjectionManagement.csproj delete mode 100644 src/EventStore.Client.Streams/EventStore.Client.Streams.csproj delete mode 100644 src/EventStore.Client.UserManagement/EventStore.Client.UserManagement.csproj delete mode 100644 src/EventStore.Client.UserManagement/EventStore.Client.UserManagement.csproj.DotSettings rename src/EventStore.Client/Core/{Common => }/protos/code.proto (100%) rename src/EventStore.Client/Core/{Common => }/protos/gossip.proto (100%) rename src/EventStore.Client/Core/{Common => }/protos/operations.proto (100%) rename src/EventStore.Client/Core/{Common => }/protos/persistentsubscriptions.proto (100%) rename src/EventStore.Client/Core/{Common => }/protos/projectionmanagement.proto (100%) rename src/EventStore.Client/Core/{Common => }/protos/serverfeatures.proto (100%) rename src/EventStore.Client/Core/{Common => }/protos/shared.proto (100%) rename src/EventStore.Client/Core/{Common => }/protos/status.proto (100%) rename src/EventStore.Client/Core/{Common => }/protos/streams.proto (100%) rename src/EventStore.Client/Core/{Common => }/protos/usermanagement.proto (100%) diff --git a/Directory.Build.props b/Directory.Build.props index 8dbeee46e..52c0d73a5 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,11 +1,10 @@ - net48;net6.0;net7.0;net8.0;net9.0 + net48;net8.0 true enable enable true - true preview Debug @@ -13,8 +12,6 @@ pdbonly true - 2.60.0 - 2.60.0 diff --git a/EventStore.Client.sln b/EventStore.Client.sln index 72ec92f54..373aa58a2 100644 --- a/EventStore.Client.sln +++ b/EventStore.Client.sln @@ -5,16 +5,6 @@ VisualStudioVersion = 15.0.26124.0 MinimumVisualStudioVersion = 15.0.26124.0 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{EA59C1CB-16DA-4F68-AF8A-642A969B4CF8}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventStore.Client.UserManagement", "src\EventStore.Client.UserManagement\EventStore.Client.UserManagement.csproj", "{D3744A86-DD35-4104-AAEE-84B79062C4A2}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventStore.Client.Streams", "src\EventStore.Client.Streams\EventStore.Client.Streams.csproj", "{362C0CF9-81C9-400F-94C7-A8B190ECE94A}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventStore.Client.Operations", "src\EventStore.Client.Operations\EventStore.Client.Operations.csproj", "{6EFB980F-C72C-40F1-9232-479BE5617580}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventStore.Client.ProjectionManagement", "src\EventStore.Client.ProjectionManagement\EventStore.Client.ProjectionManagement.csproj", "{B275EF4B-80D3-4205-9C7A-337A2A2BD99C}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventStore.Client.PersistentSubscriptions", "src\EventStore.Client.PersistentSubscriptions\EventStore.Client.PersistentSubscriptions.csproj", "{66FDEE78-836E-49F0-A4DD-BF7539C34E54}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventStore.Client", "src\EventStore.Client\EventStore.Client.csproj", "{8853D875-4A8E-450B-A1BE-9CEF8BEDABC3}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{C51F2C69-45A9-4D0D-A708-4FC319D5D340}" @@ -46,26 +36,6 @@ Global HideSolutionNode = FALSE EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {D3744A86-DD35-4104-AAEE-84B79062C4A2}.Debug|x64.ActiveCfg = Debug|Any CPU - {D3744A86-DD35-4104-AAEE-84B79062C4A2}.Debug|x64.Build.0 = Debug|Any CPU - {D3744A86-DD35-4104-AAEE-84B79062C4A2}.Release|x64.ActiveCfg = Release|Any CPU - {D3744A86-DD35-4104-AAEE-84B79062C4A2}.Release|x64.Build.0 = Release|Any CPU - {362C0CF9-81C9-400F-94C7-A8B190ECE94A}.Debug|x64.ActiveCfg = Debug|Any CPU - {362C0CF9-81C9-400F-94C7-A8B190ECE94A}.Debug|x64.Build.0 = Debug|Any CPU - {362C0CF9-81C9-400F-94C7-A8B190ECE94A}.Release|x64.ActiveCfg = Release|Any CPU - {362C0CF9-81C9-400F-94C7-A8B190ECE94A}.Release|x64.Build.0 = Release|Any CPU - {6EFB980F-C72C-40F1-9232-479BE5617580}.Debug|x64.ActiveCfg = Debug|Any CPU - {6EFB980F-C72C-40F1-9232-479BE5617580}.Debug|x64.Build.0 = Debug|Any CPU - {6EFB980F-C72C-40F1-9232-479BE5617580}.Release|x64.ActiveCfg = Release|Any CPU - {6EFB980F-C72C-40F1-9232-479BE5617580}.Release|x64.Build.0 = Release|Any CPU - {B275EF4B-80D3-4205-9C7A-337A2A2BD99C}.Debug|x64.ActiveCfg = Debug|Any CPU - {B275EF4B-80D3-4205-9C7A-337A2A2BD99C}.Debug|x64.Build.0 = Debug|Any CPU - {B275EF4B-80D3-4205-9C7A-337A2A2BD99C}.Release|x64.ActiveCfg = Release|Any CPU - {B275EF4B-80D3-4205-9C7A-337A2A2BD99C}.Release|x64.Build.0 = Release|Any CPU - {66FDEE78-836E-49F0-A4DD-BF7539C34E54}.Debug|x64.ActiveCfg = Debug|Any CPU - {66FDEE78-836E-49F0-A4DD-BF7539C34E54}.Debug|x64.Build.0 = Debug|Any CPU - {66FDEE78-836E-49F0-A4DD-BF7539C34E54}.Release|x64.ActiveCfg = Release|Any CPU - {66FDEE78-836E-49F0-A4DD-BF7539C34E54}.Release|x64.Build.0 = Release|Any CPU {8853D875-4A8E-450B-A1BE-9CEF8BEDABC3}.Debug|x64.ActiveCfg = Debug|Any CPU {8853D875-4A8E-450B-A1BE-9CEF8BEDABC3}.Debug|x64.Build.0 = Debug|Any CPU {8853D875-4A8E-450B-A1BE-9CEF8BEDABC3}.Release|x64.ActiveCfg = Release|Any CPU @@ -108,11 +78,6 @@ Global {7D929D45-F1D9-462B-BE49-84BEC11D5039}.Release|x64.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution - {D3744A86-DD35-4104-AAEE-84B79062C4A2} = {EA59C1CB-16DA-4F68-AF8A-642A969B4CF8} - {362C0CF9-81C9-400F-94C7-A8B190ECE94A} = {EA59C1CB-16DA-4F68-AF8A-642A969B4CF8} - {6EFB980F-C72C-40F1-9232-479BE5617580} = {EA59C1CB-16DA-4F68-AF8A-642A969B4CF8} - {B275EF4B-80D3-4205-9C7A-337A2A2BD99C} = {EA59C1CB-16DA-4F68-AF8A-642A969B4CF8} - {66FDEE78-836E-49F0-A4DD-BF7539C34E54} = {EA59C1CB-16DA-4F68-AF8A-642A969B4CF8} {8853D875-4A8E-450B-A1BE-9CEF8BEDABC3} = {EA59C1CB-16DA-4F68-AF8A-642A969B4CF8} {8F8548D6-694C-4BAE-9EF3-A020140E04C7} = {C51F2C69-45A9-4D0D-A708-4FC319D5D340} {4BA2E05E-6B45-47C3-9001-8B039244ECA9} = {C51F2C69-45A9-4D0D-A708-4FC319D5D340} diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 28c9a1dfe..15441ca5d 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -2,35 +2,20 @@ + true EventStore.Client + Kurrent.Client - - - - - - - - $(MSBuildProjectName.Remove(0,18)) - EventStore.Client.Grpc.$(PackageIdSuffix) - ../EventStore.Client/Common/protos/$(PackageIdSuffix.ToLower()).proto - - - - - - - ouro.png LICENSE.md - https://eventstore.com + https://kurrent.io false https://eventstore.com/blog/ - eventstore client grpc - Event Store Ltd - Copyright 2012-$([System.DateTime]::Today.Year.ToString()) Event Store Ltd + kurrent eventstore client grpc + Kurrent Ltd + Copyright 2012-$([System.DateTime]::Today.Year.ToString()) Kurrent Ltd v @@ -40,28 +25,25 @@ - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - all - runtime; build; native; contentfiles; analyzers - + + + + + + + + + + - - - - - diff --git a/src/EventStore.Client.Extensions.OpenTelemetry/EventStore.Client.Extensions.OpenTelemetry.csproj b/src/EventStore.Client.Extensions.OpenTelemetry/EventStore.Client.Extensions.OpenTelemetry.csproj index e32bbacc7..ef5c6aa35 100644 --- a/src/EventStore.Client.Extensions.OpenTelemetry/EventStore.Client.Extensions.OpenTelemetry.csproj +++ b/src/EventStore.Client.Extensions.OpenTelemetry/EventStore.Client.Extensions.OpenTelemetry.csproj @@ -13,4 +13,8 @@ + + + + diff --git a/src/EventStore.Client.Operations/EventStore.Client.Operations.csproj b/src/EventStore.Client.Operations/EventStore.Client.Operations.csproj deleted file mode 100644 index 37299e1a1..000000000 --- a/src/EventStore.Client.Operations/EventStore.Client.Operations.csproj +++ /dev/null @@ -1,6 +0,0 @@ - - - - The GRPC client API for Event Store Operations, e.g., Scavenging. Get the open source or commercial versions of Event Store server from https://eventstore.com/ - - diff --git a/src/EventStore.Client.PersistentSubscriptions/EventStore.Client.PersistentSubscriptions.csproj b/src/EventStore.Client.PersistentSubscriptions/EventStore.Client.PersistentSubscriptions.csproj deleted file mode 100644 index 405a77405..000000000 --- a/src/EventStore.Client.PersistentSubscriptions/EventStore.Client.PersistentSubscriptions.csproj +++ /dev/null @@ -1,6 +0,0 @@ - - - - The GRPC client API for Event Store Persistent Subscriptions. Get the open source or commercial versions of Event Store server from https://eventstore.com/ - - diff --git a/src/EventStore.Client.ProjectionManagement/EventStore.Client.ProjectionManagement.csproj b/src/EventStore.Client.ProjectionManagement/EventStore.Client.ProjectionManagement.csproj deleted file mode 100644 index 678656cff..000000000 --- a/src/EventStore.Client.ProjectionManagement/EventStore.Client.ProjectionManagement.csproj +++ /dev/null @@ -1,6 +0,0 @@ - - - - The GRPC client API for managing Event Store Projections. Get the open source or commercial versions of Event Store server from https://eventstore.com/ - - diff --git a/src/EventStore.Client.Streams/EventStore.Client.Streams.csproj b/src/EventStore.Client.Streams/EventStore.Client.Streams.csproj deleted file mode 100644 index c878127a5..000000000 --- a/src/EventStore.Client.Streams/EventStore.Client.Streams.csproj +++ /dev/null @@ -1,6 +0,0 @@ - - - - The GRPC client API for Event Store Streams. Get the open source or commercial versions of Event Store server from https://eventstore.com/ - - diff --git a/src/EventStore.Client.UserManagement/EventStore.Client.UserManagement.csproj b/src/EventStore.Client.UserManagement/EventStore.Client.UserManagement.csproj deleted file mode 100644 index 2c9308e2e..000000000 --- a/src/EventStore.Client.UserManagement/EventStore.Client.UserManagement.csproj +++ /dev/null @@ -1,9 +0,0 @@ - - - - The GRPC client API for managing users in Event Store. Get the open source or commercial versions of Event Store server from https://eventstore.com/ - - - - - diff --git a/src/EventStore.Client.UserManagement/EventStore.Client.UserManagement.csproj.DotSettings b/src/EventStore.Client.UserManagement/EventStore.Client.UserManagement.csproj.DotSettings deleted file mode 100644 index 1183b3a73..000000000 --- a/src/EventStore.Client.UserManagement/EventStore.Client.UserManagement.csproj.DotSettings +++ /dev/null @@ -1,2 +0,0 @@ - - True \ No newline at end of file diff --git a/src/EventStore.Client/Core/StreamIdentifier.cs b/src/EventStore.Client/Core/StreamIdentifier.cs index 73e0ee25e..e77286166 100644 --- a/src/EventStore.Client/Core/StreamIdentifier.cs +++ b/src/EventStore.Client/Core/StreamIdentifier.cs @@ -3,7 +3,7 @@ namespace EventStore.Client { #pragma warning disable 1591 - public partial class StreamIdentifier { + internal partial class StreamIdentifier { private string? _cached; public static implicit operator string?(StreamIdentifier? source) { @@ -25,4 +25,4 @@ public partial class StreamIdentifier { public static implicit operator StreamIdentifier(string source) => new() {StreamName = ByteString.CopyFromUtf8(source)}; } -} +} \ No newline at end of file diff --git a/src/EventStore.Client/Core/Uuid.cs b/src/EventStore.Client/Core/Uuid.cs index 6c96b1e2d..0a38cc766 100644 --- a/src/EventStore.Client/Core/Uuid.cs +++ b/src/EventStore.Client/Core/Uuid.cs @@ -51,7 +51,7 @@ namespace EventStore.Client { /// /// /// - public static Uuid FromDto(UUID dto) => + internal static Uuid FromDto(UUID dto) => dto == null ? throw new ArgumentNullException(nameof(dto)) : dto.ValueCase switch { @@ -106,7 +106,7 @@ private Uuid(long msb, long lsb) { /// Converts the to the gRPC wire format. /// /// - public UUID ToDto() => + internal UUID ToDto() => new UUID { Structured = new UUID.Types.Structured { LeastSignificantBits = _lsb, @@ -200,4 +200,4 @@ private bool TryWriteGuidBytes(Guid value, Span destination) #endif } } -} +} \ No newline at end of file diff --git a/src/EventStore.Client/Core/Common/protos/code.proto b/src/EventStore.Client/Core/protos/code.proto similarity index 100% rename from src/EventStore.Client/Core/Common/protos/code.proto rename to src/EventStore.Client/Core/protos/code.proto diff --git a/src/EventStore.Client/Core/Common/protos/gossip.proto b/src/EventStore.Client/Core/protos/gossip.proto similarity index 100% rename from src/EventStore.Client/Core/Common/protos/gossip.proto rename to src/EventStore.Client/Core/protos/gossip.proto diff --git a/src/EventStore.Client/Core/Common/protos/operations.proto b/src/EventStore.Client/Core/protos/operations.proto similarity index 100% rename from src/EventStore.Client/Core/Common/protos/operations.proto rename to src/EventStore.Client/Core/protos/operations.proto diff --git a/src/EventStore.Client/Core/Common/protos/persistentsubscriptions.proto b/src/EventStore.Client/Core/protos/persistentsubscriptions.proto similarity index 100% rename from src/EventStore.Client/Core/Common/protos/persistentsubscriptions.proto rename to src/EventStore.Client/Core/protos/persistentsubscriptions.proto diff --git a/src/EventStore.Client/Core/Common/protos/projectionmanagement.proto b/src/EventStore.Client/Core/protos/projectionmanagement.proto similarity index 100% rename from src/EventStore.Client/Core/Common/protos/projectionmanagement.proto rename to src/EventStore.Client/Core/protos/projectionmanagement.proto diff --git a/src/EventStore.Client/Core/Common/protos/serverfeatures.proto b/src/EventStore.Client/Core/protos/serverfeatures.proto similarity index 100% rename from src/EventStore.Client/Core/Common/protos/serverfeatures.proto rename to src/EventStore.Client/Core/protos/serverfeatures.proto diff --git a/src/EventStore.Client/Core/Common/protos/shared.proto b/src/EventStore.Client/Core/protos/shared.proto similarity index 100% rename from src/EventStore.Client/Core/Common/protos/shared.proto rename to src/EventStore.Client/Core/protos/shared.proto diff --git a/src/EventStore.Client/Core/Common/protos/status.proto b/src/EventStore.Client/Core/protos/status.proto similarity index 100% rename from src/EventStore.Client/Core/Common/protos/status.proto rename to src/EventStore.Client/Core/protos/status.proto diff --git a/src/EventStore.Client/Core/Common/protos/streams.proto b/src/EventStore.Client/Core/protos/streams.proto similarity index 100% rename from src/EventStore.Client/Core/Common/protos/streams.proto rename to src/EventStore.Client/Core/protos/streams.proto diff --git a/src/EventStore.Client/Core/Common/protos/usermanagement.proto b/src/EventStore.Client/Core/protos/usermanagement.proto similarity index 100% rename from src/EventStore.Client/Core/Common/protos/usermanagement.proto rename to src/EventStore.Client/Core/protos/usermanagement.proto diff --git a/src/EventStore.Client/EventStore.Client.csproj b/src/EventStore.Client/EventStore.Client.csproj index f783a93cc..6aa82bad0 100644 --- a/src/EventStore.Client/EventStore.Client.csproj +++ b/src/EventStore.Client/EventStore.Client.csproj @@ -7,54 +7,46 @@ - - - - - - + + + + - + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + - - - - - - - - - - - - - - MSBuild:Compile - - - MSBuild:Compile - - - MSBuild:Compile - - - MSBuild:Compile - - - MSBuild:Compile - + + + + + + + + - + + + + + + + + diff --git a/test/Directory.Build.props b/test/Directory.Build.props index 151f65d09..0d2e0dec9 100644 --- a/test/Directory.Build.props +++ b/test/Directory.Build.props @@ -3,21 +3,24 @@ true - xUnit1031 + xUnit1031;NU1903 + false + false - - - + + + - - + + all runtime; build; native; contentfiles; analyzers + @@ -31,10 +34,10 @@ - + - + \ No newline at end of file diff --git a/test/EventStore.Client.Operations.Tests/EventStore.Client.Operations.Tests.csproj b/test/EventStore.Client.Operations.Tests/EventStore.Client.Operations.Tests.csproj index d43bdceb5..f80438c6b 100644 --- a/test/EventStore.Client.Operations.Tests/EventStore.Client.Operations.Tests.csproj +++ b/test/EventStore.Client.Operations.Tests/EventStore.Client.Operations.Tests.csproj @@ -1,6 +1,6 @@ - + - + \ No newline at end of file diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/EventStore.Client.PersistentSubscriptions.Tests.csproj b/test/EventStore.Client.PersistentSubscriptions.Tests/EventStore.Client.PersistentSubscriptions.Tests.csproj index 04cf6634b..0287e82db 100644 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/EventStore.Client.PersistentSubscriptions.Tests.csproj +++ b/test/EventStore.Client.PersistentSubscriptions.Tests/EventStore.Client.PersistentSubscriptions.Tests.csproj @@ -1,9 +1,9 @@  - + - + - + \ No newline at end of file diff --git a/test/EventStore.Client.Plugins.Tests/EventStore.Client.Plugins.Tests.csproj b/test/EventStore.Client.Plugins.Tests/EventStore.Client.Plugins.Tests.csproj index ee7d48512..dac52c701 100644 --- a/test/EventStore.Client.Plugins.Tests/EventStore.Client.Plugins.Tests.csproj +++ b/test/EventStore.Client.Plugins.Tests/EventStore.Client.Plugins.Tests.csproj @@ -1,6 +1,6 @@  - + - + \ No newline at end of file diff --git a/test/EventStore.Client.ProjectionManagement.Tests/EventStore.Client.ProjectionManagement.Tests.csproj b/test/EventStore.Client.ProjectionManagement.Tests/EventStore.Client.ProjectionManagement.Tests.csproj index 31381e212..dac52c701 100644 --- a/test/EventStore.Client.ProjectionManagement.Tests/EventStore.Client.ProjectionManagement.Tests.csproj +++ b/test/EventStore.Client.ProjectionManagement.Tests/EventStore.Client.ProjectionManagement.Tests.csproj @@ -1,6 +1,6 @@  - + \ No newline at end of file diff --git a/test/EventStore.Client.Streams.Tests/EventStore.Client.Streams.Tests.csproj b/test/EventStore.Client.Streams.Tests/EventStore.Client.Streams.Tests.csproj index e0cf4e59d..3e9700e08 100644 --- a/test/EventStore.Client.Streams.Tests/EventStore.Client.Streams.Tests.csproj +++ b/test/EventStore.Client.Streams.Tests/EventStore.Client.Streams.Tests.csproj @@ -1,9 +1,9 @@  - - CS0612;xUnit1031 - + + CS0612;xUnit1031 + - + - + \ No newline at end of file diff --git a/test/EventStore.Client.Tests.Common/EventStore.Client.Tests.Common.csproj b/test/EventStore.Client.Tests.Common/EventStore.Client.Tests.Common.csproj index f666f3871..5745f4665 100644 --- a/test/EventStore.Client.Tests.Common/EventStore.Client.Tests.Common.csproj +++ b/test/EventStore.Client.Tests.Common/EventStore.Client.Tests.Common.csproj @@ -4,39 +4,33 @@ - - - - - - - + - - - - - - - - + + + + + + + + - - + + - - - - - - - + + + + + + + @@ -69,4 +63,4 @@ - + \ No newline at end of file diff --git a/test/EventStore.Client.Tests.Common/Fixtures/EventStoreTestNode.cs b/test/EventStore.Client.Tests.Common/Fixtures/EventStoreTestNode.cs index 7f1da49d0..b4c2d4bb2 100644 --- a/test/EventStore.Client.Tests.Common/Fixtures/EventStoreTestNode.cs +++ b/test/EventStore.Client.Tests.Common/Fixtures/EventStoreTestNode.cs @@ -14,16 +14,16 @@ namespace EventStore.Client.Tests; public class EventStoreTestNode(EventStoreFixtureOptions? options = null) : TestContainerService { - + static readonly NetworkPortProvider NetworkPortProvider = new(NetworkPortProvider.DefaultEsdbPort); - + EventStoreFixtureOptions Options { get; } = options ?? DefaultOptions(); public static EventStoreFixtureOptions DefaultOptions() { const string connString = "esdb://admin:changeit@localhost:{port}/?tlsVerifyCert=false"; - + var port = NetworkPortProvider.NextAvailablePort; - + var defaultSettings = EventStoreClientSettings .Create(connString.Replace("{port}", $"{port}")) .With(x => x.LoggerFactory = new SerilogLoggerFactory(Log.Logger)) @@ -32,6 +32,7 @@ public static EventStoreFixtureOptions DefaultOptions() { .With(x => x.ConnectivitySettings.DiscoveryInterval = FromSeconds(1)); var defaultEnvironment = new Dictionary(GlobalEnvironment.Variables) { + ["EVENTSTORE_TELEMETRY_OPTOUT"] = "true", ["EVENTSTORE_MEM_DB"] = "true", ["EVENTSTORE_CHUNK_SIZE"] = (1024 * 1024 * 1024).ToString(), ["EVENTSTORE_CERTIFICATE_FILE"] = "/etc/eventstore/certs/node/node.crt", @@ -39,6 +40,7 @@ public static EventStoreFixtureOptions DefaultOptions() { ["EVENTSTORE_STREAM_EXISTENCE_FILTER_SIZE"] = "10000", ["EVENTSTORE_STREAM_INFO_CACHE_CAPACITY"] = "10000", ["EVENTSTORE_ENABLE_ATOM_PUB_OVER_HTTP"] = "true", + ["EVENTSTORE_LOG_LEVEL"] = "Default", // required to use serilog settings ["EVENTSTORE_DISABLE_LOG_FILE"] = "true", ["EVENTSTORE_ADVERTISE_HTTP_PORT_TO_CLIENT_AS"] = $"{NetworkPortProvider.DefaultEsdbPort}" }; @@ -55,7 +57,7 @@ public static EventStoreFixtureOptions DefaultOptions() { else defaultEnvironment["EVENTSTORE_ADVERTISE_HTTP_PORT_TO_CLIENT_AS"] = $"{port}"; } - + return new(defaultSettings, defaultEnvironment); } @@ -75,9 +77,16 @@ protected override ContainerBuilder Configure() { .UseContainer() .UseImage(Options.Environment["ES_DOCKER_IMAGE"]) .WithName(containerName) + .WithPublicEndpointResolver() .WithEnvironment(env) .MountVolume(certsPath, "/etc/eventstore/certs", MountType.ReadOnly) - .ExposePort(port, 2113); + .ExposePort(port, 2113) + //.KeepContainer().KeepRunning().ReuseIfExists() + .WaitUntilReadyWithConstantBackoff(1_000, 60, service => { + var output = service.ExecuteCommand("curl -o - -I http://admin:changeit@localhost:2113/health/live"); + if (!output.Success) + throw new Exception(output.Error); + }); } /// @@ -114,7 +123,7 @@ await Policy.Handle() /// class NetworkPortProvider(int port = 2114) { public const int DefaultEsdbPort = 2113; - + static readonly SemaphoreSlim Semaphore = new(1, 1); public async Task GetNextAvailablePort(TimeSpan delay = default) { @@ -123,13 +132,13 @@ public async Task GetNextAvailablePort(TimeSpan delay = default) { return port; await Semaphore.WaitAsync(); - + try { using var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); - + while (true) { var nexPort = Interlocked.Increment(ref port); - + try { await socket.ConnectAsync(IPAddress.Any, nexPort); } @@ -137,7 +146,7 @@ public async Task GetNextAvailablePort(TimeSpan delay = default) { if (ex.SocketErrorCode is SocketError.ConnectionRefused or not SocketError.IsConnected) { return nexPort; } - + await Task.Delay(delay); } finally { @@ -155,4 +164,4 @@ public async Task GetNextAvailablePort(TimeSpan delay = default) { } public int NextAvailablePort => GetNextAvailablePort(FromMilliseconds(100)).GetAwaiter().GetResult(); -} +} \ No newline at end of file diff --git a/test/EventStore.Client.Tests.Common/FluentDocker/FluentDockerBuilderExtensions.cs b/test/EventStore.Client.Tests.Common/FluentDocker/FluentDockerBuilderExtensions.cs index 782ab7696..b41c031c9 100644 --- a/test/EventStore.Client.Tests.Common/FluentDocker/FluentDockerBuilderExtensions.cs +++ b/test/EventStore.Client.Tests.Common/FluentDocker/FluentDockerBuilderExtensions.cs @@ -1,8 +1,12 @@ using System.Reflection; using Ductus.FluentDocker.Builders; +using Ductus.FluentDocker.Common; using Ductus.FluentDocker.Model.Compose; using Ductus.FluentDocker.Services; using Ductus.FluentDocker.Services.Impl; +using Polly; +using Polly.Contrib.WaitAndRetry; +using static System.TimeSpan; namespace EventStore.Client.Tests.FluentDocker; @@ -21,4 +25,64 @@ public static DockerComposeConfig Configuration(this ICompositeService service) (DockerComposeConfig)typeof(DockerComposeCompositeService) .GetProperty("Config", BindingFlags.NonPublic | BindingFlags.Instance)! .GetValue(service)!; +} + +[PublicAPI] +public static class FluentDockerContainerBuilderExtensions { + public static ContainerBuilder WithEnvironment(this ContainerBuilder builder, Dictionary environment) => + builder.WithEnvironment(environment.Select(pair => $"{pair.Key}={pair.Value}").ToArray()); + + public static ContainerBuilder WaitUntilReady(this ContainerBuilder builder, IEnumerable sleepDurations, Action action) => + builder.Wait("", (service, _) => { + var result = Policy + .Handle() + .WaitAndRetry(sleepDurations) + .ExecuteAndCapture(() => action(service)); + + if (result.Outcome == OutcomeType.Successful) + return 0; + + throw result.FinalException is FluentDockerException + ? result.FinalException + : new FluentDockerException($"Service {service.Name} not ready: {result.FinalException.Message}"); + }); + + public static ContainerBuilder WaitUntilReadyWithConstantBackoff( + this ContainerBuilder builder, TimeSpan delay, int retryCount, Action action + ) => builder.WaitUntilReady(Backoff.ConstantBackoff(delay, retryCount), action); + + public static ContainerBuilder WaitUntilReadyWithExponentialBackoff( + this ContainerBuilder builder, TimeSpan delay, int retryCount, Action action + ) => builder.WaitUntilReady(Backoff.ExponentialBackoff(delay, retryCount), action); + + public static ContainerBuilder WaitUntilReadyWithConstantBackoff( + this ContainerBuilder builder, int delayMs, int retryCount, Action action + ) => builder.WaitUntilReadyWithConstantBackoff(FromMilliseconds(delayMs), retryCount, action); + + public static ContainerBuilder WaitUntilReadyWithExponentialBackoff( + this ContainerBuilder builder, int delayMs, int retryCount, Action action + ) => builder.WaitUntilReadyWithExponentialBackoff(FromMilliseconds(delayMs), retryCount, action); + + public static ContainerBuilder WaitUntilReadyAsync(this ContainerBuilder builder, IEnumerable sleepDurations, Func action) => + builder.WaitUntilReady(sleepDurations, service => { + var valueTask = action(service); + if (!valueTask.IsCompletedSuccessfully) + valueTask.AsTask().GetAwaiter().GetResult(); + }); + + public static ContainerBuilder WaitUntilReadyWithConstantBackoffAsync( + this ContainerBuilder builder, TimeSpan delay, int retryCount, Func action + ) => builder.WaitUntilReadyAsync(Backoff.ConstantBackoff(delay, retryCount, fastFirst: true), action); + + public static ContainerBuilder WaitUntilReadyWithExponentialBackoffAsync( + this ContainerBuilder builder, TimeSpan delay, int retryCount, Func action + ) => builder.WaitUntilReadyAsync(Backoff.ExponentialBackoff(delay, retryCount, fastFirst: true), action); + + public static ContainerBuilder WaitUntilReadyWithConstantBackoffAsync( + this ContainerBuilder builder, int delayMs, int retryCount, Func action + ) => builder.WaitUntilReadyWithConstantBackoffAsync(FromMilliseconds(delayMs), retryCount, action); + + public static ContainerBuilder WaitUntilReadyWithExponentialBackoffAsync( + this ContainerBuilder builder, int delayMs, int retryCount, Func action + ) => builder.WaitUntilReadyWithExponentialBackoffAsync(FromMilliseconds(delayMs), retryCount, action); } \ No newline at end of file diff --git a/test/EventStore.Client.Tests.Common/FluentDocker/FluentDockerServiceExtensions.cs b/test/EventStore.Client.Tests.Common/FluentDocker/FluentDockerServiceExtensions.cs index c4773cede..3fab79a6e 100644 --- a/test/EventStore.Client.Tests.Common/FluentDocker/FluentDockerServiceExtensions.cs +++ b/test/EventStore.Client.Tests.Common/FluentDocker/FluentDockerServiceExtensions.cs @@ -1,8 +1,12 @@ #pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously +using System.Net; +using Ductus.FluentDocker.Builders; +using Ductus.FluentDocker.Commands; using Ductus.FluentDocker.Common; using Ductus.FluentDocker.Model.Containers; using Ductus.FluentDocker.Services; +using Ductus.FluentDocker.Services.Extensions; namespace EventStore.Client.Tests.FluentDocker; @@ -60,3 +64,25 @@ public static async Task WaitUntilNodesAreHealthy(this ICompositeService service await WaitUntilNodesAreHealthy(service, serviceNamePrefix, cts.Token); } } + +public static class FluentDockerContainerServiceExtensions { + // IPAddress.Any defaults to IPAddress.Loopback + public static IPEndPoint GetPublicEndpoint(this IContainerService service, string portAndProtocol) { + var endpoint = service.ToHostExposedEndpoint(portAndProtocol); + return endpoint.Address.Equals(IPAddress.Any) ? new IPEndPoint(IPAddress.Loopback, endpoint.Port) : endpoint; + } + + public static IPEndPoint GetPublicEndpoint(this IContainerService service, int port) => + service.GetPublicEndpoint($"{port}/tcp"); + + public static ContainerBuilder WithPublicEndpointResolver(this ContainerBuilder builder) => + builder.UseCustomResolver((endpoints, portAndProtocol, dockerUri) => { + var endpoint = endpoints.ToHostPort(portAndProtocol, dockerUri); + return endpoint.Address.Equals(IPAddress.Any) ? new IPEndPoint(IPAddress.Loopback, endpoint.Port) : endpoint; + }); + + public static CommandResponse> ExecuteCommand(this IContainerService service, string command) { + var config = service.GetConfiguration(); + return service.DockerHost.Execute(config.Id, command, service.Certificates); + } +} \ No newline at end of file diff --git a/test/EventStore.Client.Tests/EventStore.Client.Tests.csproj b/test/EventStore.Client.Tests/EventStore.Client.Tests.csproj index 494e5e243..c4456169e 100644 --- a/test/EventStore.Client.Tests/EventStore.Client.Tests.csproj +++ b/test/EventStore.Client.Tests/EventStore.Client.Tests.csproj @@ -4,23 +4,17 @@ - - - - - - - + - + - + all runtime; build; native; contentfiles; analyzers @@ -29,4 +23,4 @@ - + \ No newline at end of file diff --git a/test/EventStore.Client.Tests/GrpcServerCapabilitiesClientTests.cs b/test/EventStore.Client.Tests/GrpcServerCapabilitiesClientTests.cs index 2b13cf13b..258f51cd6 100644 --- a/test/EventStore.Client.Tests/GrpcServerCapabilitiesClientTests.cs +++ b/test/EventStore.Client.Tests/GrpcServerCapabilitiesClientTests.cs @@ -1,102 +1,102 @@ -#if NET -using System.Net; -using EventStore.Client.ServerFeatures; -using Grpc.Core; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.TestHost; -using Microsoft.Extensions.DependencyInjection; - -namespace EventStore.Client.Tests; - -public class GrpcServerCapabilitiesClientTests { - public static IEnumerable ExpectedResultsCases() { - yield return new object?[] { new SupportedMethods(), new ServerCapabilities() }; - yield return new object?[] { - new SupportedMethods { - Methods = { - new SupportedMethod { - ServiceName = "event_store.client.streams.streams", - MethodName = "batchappend" - } - } - }, - new ServerCapabilities(true) - }; - - yield return new object?[] { - new SupportedMethods { - Methods = { - new SupportedMethod { - ServiceName = "event_store.client.persistent_subscriptions.persistentsubscriptions", - MethodName = "read", - Features = { - "all" - } - } - } - }, - new ServerCapabilities(SupportsPersistentSubscriptionsToAll: true) - }; - - yield return new object?[] { - new SupportedMethods { - Methods = { - new SupportedMethod { - ServiceName = "event_store.client.persistent_subscriptions.persistentsubscriptions", - MethodName = "read" - } - } - }, - new ServerCapabilities() - }; - } - - [Theory] - [MemberData(nameof(ExpectedResultsCases))] - internal async Task GetAsyncReturnsExpectedResults( - SupportedMethods supportedMethods, - ServerCapabilities expected - ) { - using var kestrel = new TestServer( - new WebHostBuilder() - .ConfigureServices( - services => services - .AddRouting() - .AddGrpc().Services - .AddSingleton(new FakeServerFeatures(supportedMethods)) - ) - .Configure( - app => app - .UseRouting() - .UseEndpoints(ep => ep.MapGrpcService()) - ) - ); - - var sut = new GrpcServerCapabilitiesClient(new()); - - var actual = - await sut.GetAsync( - ChannelFactory - .CreateChannel( - new() { - CreateHttpMessageHandler = kestrel.CreateHandler - }, - new DnsEndPoint("localhost", 80) - ) - .CreateCallInvoker(), - default - ); - - Assert.Equal(expected, actual); - } - - class FakeServerFeatures : ServerFeatures.ServerFeatures.ServerFeaturesBase { - readonly SupportedMethods _supportedMethods; - - public FakeServerFeatures(SupportedMethods supportedMethods) => _supportedMethods = supportedMethods; - - public override Task GetSupportedMethods(Empty request, ServerCallContext context) => Task.FromResult(_supportedMethods); - } -} -#endif +// #if NET +// using System.Net; +// using EventStore.Client.ServerFeatures; +// using Grpc.Core; +// using Microsoft.AspNetCore.Builder; +// using Microsoft.AspNetCore.Hosting; +// using Microsoft.AspNetCore.TestHost; +// using Microsoft.Extensions.DependencyInjection; +// +// namespace EventStore.Client.Tests; +// +// public class GrpcServerCapabilitiesClientTests { +// public static IEnumerable ExpectedResultsCases() { +// yield return new object?[] { new SupportedMethods(), new ServerCapabilities() }; +// yield return new object?[] { +// new SupportedMethods { +// Methods = { +// new SupportedMethod { +// ServiceName = "event_store.client.streams.streams", +// MethodName = "batchappend" +// } +// } +// }, +// new ServerCapabilities(true) +// }; +// +// yield return new object?[] { +// new SupportedMethods { +// Methods = { +// new SupportedMethod { +// ServiceName = "event_store.client.persistent_subscriptions.persistentsubscriptions", +// MethodName = "read", +// Features = { +// "all" +// } +// } +// } +// }, +// new ServerCapabilities(SupportsPersistentSubscriptionsToAll: true) +// }; +// +// yield return new object?[] { +// new SupportedMethods { +// Methods = { +// new SupportedMethod { +// ServiceName = "event_store.client.persistent_subscriptions.persistentsubscriptions", +// MethodName = "read" +// } +// } +// }, +// new ServerCapabilities() +// }; +// } +// +// [Theory] +// [MemberData(nameof(ExpectedResultsCases))] +// internal async Task GetAsyncReturnsExpectedResults( +// SupportedMethods supportedMethods, +// ServerCapabilities expected +// ) { +// using var kestrel = new TestServer( +// new WebHostBuilder() +// .ConfigureServices( +// services => services +// .AddRouting() +// .AddGrpc().Services +// .AddSingleton(new FakeServerFeatures(supportedMethods)) +// ) +// .Configure( +// app => app +// .UseRouting() +// .UseEndpoints(ep => ep.MapGrpcService()) +// ) +// ); +// +// var sut = new GrpcServerCapabilitiesClient(new()); +// +// var actual = +// await sut.GetAsync( +// ChannelFactory +// .CreateChannel( +// new() { +// CreateHttpMessageHandler = kestrel.CreateHandler +// }, +// new DnsEndPoint("localhost", 80) +// ) +// .CreateCallInvoker(), +// default +// ); +// +// Assert.Equal(expected, actual); +// } +// +// class FakeServerFeatures : ServerFeatures.ServerFeatures.ServerFeaturesBase { +// readonly SupportedMethods _supportedMethods; +// +// public FakeServerFeatures(SupportedMethods supportedMethods) => _supportedMethods = supportedMethods; +// +// public override Task GetSupportedMethods(Empty request, ServerCallContext context) => Task.FromResult(_supportedMethods); +// } +// } +// #endif \ No newline at end of file diff --git a/test/EventStore.Client.UserManagement.Tests/EventStore.Client.UserManagement.Tests.csproj b/test/EventStore.Client.UserManagement.Tests/EventStore.Client.UserManagement.Tests.csproj index abaf5e7bf..d4b52e67f 100644 --- a/test/EventStore.Client.UserManagement.Tests/EventStore.Client.UserManagement.Tests.csproj +++ b/test/EventStore.Client.UserManagement.Tests/EventStore.Client.UserManagement.Tests.csproj @@ -4,9 +4,9 @@ EventStore.Client.Tests - + - + \ No newline at end of file From bd6b10ff3b9c2e7db3cfe83035502a2a0e8ee737 Mon Sep 17 00:00:00 2001 From: William Chong Date: Fri, 20 Dec 2024 12:05:41 +0400 Subject: [PATCH 03/15] Remove unused target frameworks and pass certificate in healthcheck --- .github/workflows/base.yml | 4 +- .github/workflows/publish.yml | 10 +--- samples/Samples.sln | 21 ------- samples/Samples.sln.DotSettings | 1 + .../appending-events/appending-events.csproj | 3 - .../connecting-to-a-cluster.csproj | 3 - .../connecting-to-a-single-node.csproj | 3 - samples/diagnostics/diagnostics.csproj | 9 ++- .../persistent-subscriptions.csproj | 2 - .../projection-management.csproj | 3 +- samples/quick-start/quick-start.csproj | 3 - samples/reading-events/reading-events.csproj | 6 +- .../secure-with-tls/secure-with-tls.csproj | 4 +- .../server-side-filtering.csproj | 5 +- .../setting-up-dependency-injection.csproj | 5 +- .../subscribing-to-streams.csproj | 5 +- .../user-certificates.csproj | 3 +- ...ore.Client.Extensions.OpenTelemetry.csproj | 2 +- .../EventStore.Client.Tests.Common.csproj | 16 ++--- .../Fixtures/EventStoreTestNode.cs | 59 +++++++++---------- .../GlobalEnvironment.cs | 4 +- 21 files changed, 57 insertions(+), 114 deletions(-) diff --git a/.github/workflows/base.yml b/.github/workflows/base.yml index 752de25fb..20cfe91d7 100644 --- a/.github/workflows/base.yml +++ b/.github/workflows/base.yml @@ -25,7 +25,7 @@ jobs: strategy: fail-fast: false matrix: - framework: [ net6.0, net7.0, net8.0 ] + framework: [ net8.0 ] os: [ ubuntu-latest ] configuration: [ release ] runs-on: ${{ matrix.os }} @@ -52,8 +52,6 @@ jobs: uses: actions/setup-dotnet@v3 with: dotnet-version: | - 6.0.x - 7.0.x 8.0.x - name: Run Tests shell: bash diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 598be309d..42535a11d 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -14,7 +14,7 @@ jobs: strategy: fail-fast: false matrix: - framework: [ net6.0, net7.0, net8.0 ] + framework: [ net8.0 ] os: [ ubuntu-latest, windows-latest ] runs-on: ${{ matrix.os }} name: scan-vulnerabilities/${{ matrix.os }}/${{ matrix.framework }} @@ -25,8 +25,6 @@ jobs: uses: actions/setup-dotnet@v3 with: dotnet-version: | - 6.0.x - 7.0.x 8.0.x - name: Scan for Vulnerabilities shell: bash @@ -100,7 +98,7 @@ jobs: strategy: fail-fast: false matrix: - framework: [ net6.0, net7.0, net8.0 ] + framework: [ net8.0 ] os: [ ubuntu-latest, windows-latest ] configuration: [ release ] runs-on: ${{ matrix.os }} @@ -115,8 +113,6 @@ jobs: uses: actions/setup-dotnet@v3 with: dotnet-version: | - 6.0.x - 7.0.x 8.0.x - name: Compile shell: bash @@ -167,8 +163,6 @@ jobs: uses: actions/setup-dotnet@v3 with: dotnet-version: | - 6.0.x - 7.0.x 8.0.x - name: Dotnet Pack shell: bash diff --git a/samples/Samples.sln b/samples/Samples.sln index 79c979342..d3c46d188 100644 --- a/samples/Samples.sln +++ b/samples/Samples.sln @@ -7,8 +7,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "quick-start", "quick-start\ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "client", "client", "{EBB93BBC-42A7-48E4-B1EA-0EA3953D347C}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EventStore.Client.Streams", "..\src\EventStore.Client.Streams\EventStore.Client.Streams.csproj", "{A32CE3CB-AB71-45C6-A1B2-CD94BE2D2B28}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EventStore.Client", "..\src\EventStore.Client\EventStore.Client.csproj", "{A71A13F7-8480-4E48-B88D-A2364F7C95B6}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "connecting-to-a-cluster", "connecting-to-a-cluster\connecting-to-a-cluster.csproj", "{C4CA324A-450D-4621-82F1-B4ECD18216B6}" @@ -29,10 +27,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "server-side-filtering", "se EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "persistent-subscriptions", "persistent-subscriptions\persistent-subscriptions.csproj", "{A5A5EF0D-1AE4-4647-823D-FA172E8858F2}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EventStore.Client.PersistentSubscriptions", "..\src\EventStore.Client.PersistentSubscriptions\EventStore.Client.PersistentSubscriptions.csproj", "{7200BB01-A405-45D5-A6E8-A8FA8DE39DA0}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EventStore.Client.ProjectionManagement", "..\src\EventStore.Client.ProjectionManagement\EventStore.Client.ProjectionManagement.csproj", "{79992D7B-C311-4E8A-856F-896C1EA61042}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "projection-management", "projection-management\projection-management.csproj", "{9DEA2684-C38B-465C-91A6-ED2AB67A4338}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "diagnostics", "diagnostics\diagnostics.csproj", "{546496AD-E355-4C20-930C-30D0FC76D26F}" @@ -51,10 +45,6 @@ Global {521987E5-4394-4EE0-B217-E3DC9DB0D327}.Debug|Any CPU.Build.0 = Debug|Any CPU {521987E5-4394-4EE0-B217-E3DC9DB0D327}.Release|Any CPU.ActiveCfg = Release|Any CPU {521987E5-4394-4EE0-B217-E3DC9DB0D327}.Release|Any CPU.Build.0 = Release|Any CPU - {A32CE3CB-AB71-45C6-A1B2-CD94BE2D2B28}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A32CE3CB-AB71-45C6-A1B2-CD94BE2D2B28}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A32CE3CB-AB71-45C6-A1B2-CD94BE2D2B28}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A32CE3CB-AB71-45C6-A1B2-CD94BE2D2B28}.Release|Any CPU.Build.0 = Release|Any CPU {A71A13F7-8480-4E48-B88D-A2364F7C95B6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A71A13F7-8480-4E48-B88D-A2364F7C95B6}.Debug|Any CPU.Build.0 = Debug|Any CPU {A71A13F7-8480-4E48-B88D-A2364F7C95B6}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -95,14 +85,6 @@ Global {A5A5EF0D-1AE4-4647-823D-FA172E8858F2}.Debug|Any CPU.Build.0 = Debug|Any CPU {A5A5EF0D-1AE4-4647-823D-FA172E8858F2}.Release|Any CPU.ActiveCfg = Release|Any CPU {A5A5EF0D-1AE4-4647-823D-FA172E8858F2}.Release|Any CPU.Build.0 = Release|Any CPU - {7200BB01-A405-45D5-A6E8-A8FA8DE39DA0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7200BB01-A405-45D5-A6E8-A8FA8DE39DA0}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7200BB01-A405-45D5-A6E8-A8FA8DE39DA0}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7200BB01-A405-45D5-A6E8-A8FA8DE39DA0}.Release|Any CPU.Build.0 = Release|Any CPU - {79992D7B-C311-4E8A-856F-896C1EA61042}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {79992D7B-C311-4E8A-856F-896C1EA61042}.Debug|Any CPU.Build.0 = Debug|Any CPU - {79992D7B-C311-4E8A-856F-896C1EA61042}.Release|Any CPU.ActiveCfg = Release|Any CPU - {79992D7B-C311-4E8A-856F-896C1EA61042}.Release|Any CPU.Build.0 = Release|Any CPU {9DEA2684-C38B-465C-91A6-ED2AB67A4338}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {9DEA2684-C38B-465C-91A6-ED2AB67A4338}.Debug|Any CPU.Build.0 = Debug|Any CPU {9DEA2684-C38B-465C-91A6-ED2AB67A4338}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -124,10 +106,7 @@ Global HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution - {A32CE3CB-AB71-45C6-A1B2-CD94BE2D2B28} = {EBB93BBC-42A7-48E4-B1EA-0EA3953D347C} {A71A13F7-8480-4E48-B88D-A2364F7C95B6} = {EBB93BBC-42A7-48E4-B1EA-0EA3953D347C} - {7200BB01-A405-45D5-A6E8-A8FA8DE39DA0} = {EBB93BBC-42A7-48E4-B1EA-0EA3953D347C} - {79992D7B-C311-4E8A-856F-896C1EA61042} = {EBB93BBC-42A7-48E4-B1EA-0EA3953D347C} {29E3F07A-6676-45C1-805C-46BDF6CF325B} = {EBB93BBC-42A7-48E4-B1EA-0EA3953D347C} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution diff --git a/samples/Samples.sln.DotSettings b/samples/Samples.sln.DotSettings index c341e4095..06c105615 100644 --- a/samples/Samples.sln.DotSettings +++ b/samples/Samples.sln.DotSettings @@ -390,6 +390,7 @@ True True True + True True True True diff --git a/samples/appending-events/appending-events.csproj b/samples/appending-events/appending-events.csproj index fb93d800e..3dbe997bc 100644 --- a/samples/appending-events/appending-events.csproj +++ b/samples/appending-events/appending-events.csproj @@ -5,9 +5,6 @@ - - - diff --git a/samples/connecting-to-a-cluster/connecting-to-a-cluster.csproj b/samples/connecting-to-a-cluster/connecting-to-a-cluster.csproj index 4754bd0e2..af05183b9 100644 --- a/samples/connecting-to-a-cluster/connecting-to-a-cluster.csproj +++ b/samples/connecting-to-a-cluster/connecting-to-a-cluster.csproj @@ -5,9 +5,6 @@ - - - diff --git a/samples/connecting-to-a-single-node/connecting-to-a-single-node.csproj b/samples/connecting-to-a-single-node/connecting-to-a-single-node.csproj index 4ef794ee4..7700f4667 100644 --- a/samples/connecting-to-a-single-node/connecting-to-a-single-node.csproj +++ b/samples/connecting-to-a-single-node/connecting-to-a-single-node.csproj @@ -4,9 +4,6 @@ - - - diff --git a/samples/diagnostics/diagnostics.csproj b/samples/diagnostics/diagnostics.csproj index 1763a9d9e..d0ca46e87 100644 --- a/samples/diagnostics/diagnostics.csproj +++ b/samples/diagnostics/diagnostics.csproj @@ -5,17 +5,16 @@ - - - + + + - + - diff --git a/samples/persistent-subscriptions/persistent-subscriptions.csproj b/samples/persistent-subscriptions/persistent-subscriptions.csproj index e845bda8e..4204ed387 100644 --- a/samples/persistent-subscriptions/persistent-subscriptions.csproj +++ b/samples/persistent-subscriptions/persistent-subscriptions.csproj @@ -5,8 +5,6 @@ - - diff --git a/samples/projection-management/projection-management.csproj b/samples/projection-management/projection-management.csproj index 3b5b2c3c7..80fe56f78 100644 --- a/samples/projection-management/projection-management.csproj +++ b/samples/projection-management/projection-management.csproj @@ -4,7 +4,6 @@ - - + diff --git a/samples/quick-start/quick-start.csproj b/samples/quick-start/quick-start.csproj index b33948026..74eea2001 100644 --- a/samples/quick-start/quick-start.csproj +++ b/samples/quick-start/quick-start.csproj @@ -4,9 +4,6 @@ - - - diff --git a/samples/reading-events/reading-events.csproj b/samples/reading-events/reading-events.csproj index 436254ea7..d29b531fb 100644 --- a/samples/reading-events/reading-events.csproj +++ b/samples/reading-events/reading-events.csproj @@ -4,8 +4,6 @@ - - - + - \ No newline at end of file + diff --git a/samples/secure-with-tls/secure-with-tls.csproj b/samples/secure-with-tls/secure-with-tls.csproj index 7dd4b7c03..9d29b3972 100644 --- a/samples/secure-with-tls/secure-with-tls.csproj +++ b/samples/secure-with-tls/secure-with-tls.csproj @@ -14,8 +14,6 @@ - - - + diff --git a/samples/server-side-filtering/server-side-filtering.csproj b/samples/server-side-filtering/server-side-filtering.csproj index b69b90670..0c13c9497 100644 --- a/samples/server-side-filtering/server-side-filtering.csproj +++ b/samples/server-side-filtering/server-side-filtering.csproj @@ -4,9 +4,6 @@ - - - - \ No newline at end of file + diff --git a/samples/setting-up-dependency-injection/setting-up-dependency-injection.csproj b/samples/setting-up-dependency-injection/setting-up-dependency-injection.csproj index 0e2adfd05..3a57950a1 100644 --- a/samples/setting-up-dependency-injection/setting-up-dependency-injection.csproj +++ b/samples/setting-up-dependency-injection/setting-up-dependency-injection.csproj @@ -4,9 +4,6 @@ - - - - \ No newline at end of file + diff --git a/samples/subscribing-to-streams/subscribing-to-streams.csproj b/samples/subscribing-to-streams/subscribing-to-streams.csproj index 397f07197..92424b533 100644 --- a/samples/subscribing-to-streams/subscribing-to-streams.csproj +++ b/samples/subscribing-to-streams/subscribing-to-streams.csproj @@ -4,9 +4,6 @@ - - - - \ No newline at end of file + diff --git a/samples/user-certificates/user-certificates.csproj b/samples/user-certificates/user-certificates.csproj index 7eaa70127..8cb9d2294 100644 --- a/samples/user-certificates/user-certificates.csproj +++ b/samples/user-certificates/user-certificates.csproj @@ -9,8 +9,7 @@ - - + diff --git a/src/EventStore.Client.Extensions.OpenTelemetry/EventStore.Client.Extensions.OpenTelemetry.csproj b/src/EventStore.Client.Extensions.OpenTelemetry/EventStore.Client.Extensions.OpenTelemetry.csproj index ef5c6aa35..81f4d38ea 100644 --- a/src/EventStore.Client.Extensions.OpenTelemetry/EventStore.Client.Extensions.OpenTelemetry.csproj +++ b/src/EventStore.Client.Extensions.OpenTelemetry/EventStore.Client.Extensions.OpenTelemetry.csproj @@ -10,7 +10,7 @@ - + diff --git a/test/EventStore.Client.Tests.Common/EventStore.Client.Tests.Common.csproj b/test/EventStore.Client.Tests.Common/EventStore.Client.Tests.Common.csproj index 5745f4665..9826714b3 100644 --- a/test/EventStore.Client.Tests.Common/EventStore.Client.Tests.Common.csproj +++ b/test/EventStore.Client.Tests.Common/EventStore.Client.Tests.Common.csproj @@ -24,13 +24,13 @@ - - - - - - - + + + + + + + @@ -63,4 +63,4 @@ - \ No newline at end of file + diff --git a/test/EventStore.Client.Tests.Common/Fixtures/EventStoreTestNode.cs b/test/EventStore.Client.Tests.Common/Fixtures/EventStoreTestNode.cs index b4c2d4bb2..6417ad3b9 100644 --- a/test/EventStore.Client.Tests.Common/Fixtures/EventStoreTestNode.cs +++ b/test/EventStore.Client.Tests.Common/Fixtures/EventStoreTestNode.cs @@ -32,7 +32,6 @@ public static EventStoreFixtureOptions DefaultOptions() { .With(x => x.ConnectivitySettings.DiscoveryInterval = FromSeconds(1)); var defaultEnvironment = new Dictionary(GlobalEnvironment.Variables) { - ["EVENTSTORE_TELEMETRY_OPTOUT"] = "true", ["EVENTSTORE_MEM_DB"] = "true", ["EVENTSTORE_CHUNK_SIZE"] = (1024 * 1024 * 1024).ToString(), ["EVENTSTORE_CERTIFICATE_FILE"] = "/etc/eventstore/certs/node/node.crt", @@ -81,40 +80,40 @@ protected override ContainerBuilder Configure() { .WithEnvironment(env) .MountVolume(certsPath, "/etc/eventstore/certs", MountType.ReadOnly) .ExposePort(port, 2113) - //.KeepContainer().KeepRunning().ReuseIfExists() + // .KeepContainer().KeepRunning().ReuseIfExists() .WaitUntilReadyWithConstantBackoff(1_000, 60, service => { - var output = service.ExecuteCommand("curl -o - -I http://admin:changeit@localhost:2113/health/live"); + var output = service.ExecuteCommand("curl -u admin:changeit --cacert /etc/eventstore/certs/ca/ca.crt https://localhost:2113/health/live"); if (!output.Success) throw new Exception(output.Error); }); } - /// - /// max of 30 seconds (300 * 100ms) - /// - static readonly IEnumerable DefaultBackoffDelay = Backoff.ConstantBackoff(FromMilliseconds(100), 300); - - protected override async Task OnServiceStarted() { - using var http = new HttpClient( -#if NET - new SocketsHttpHandler { SslOptions = { RemoteCertificateValidationCallback = delegate { return true; } } } -#else - new WinHttpHandler { ServerCertificateValidationCallback = delegate { return true; } } -#endif - ) { - BaseAddress = Options.ClientSettings.ConnectivitySettings.Address - }; - - await Policy.Handle() - .WaitAndRetryAsync(DefaultBackoffDelay) - .ExecuteAsync( - async () => { - using var response = await http.GetAsync("/health/live", CancellationToken.None); - if (response.StatusCode >= HttpStatusCode.BadRequest) - throw new FluentDockerException($"Health check failed with status code: {response.StatusCode}."); - } - ); - } +// /// +// /// max of 30 seconds (300 * 100ms) +// /// +// static readonly IEnumerable DefaultBackoffDelay = Backoff.ConstantBackoff(FromMilliseconds(100), 300); +// +// protected override async Task OnServiceStarted() { +// using var http = new HttpClient( +// #if NET +// new SocketsHttpHandler { SslOptions = { RemoteCertificateValidationCallback = delegate { return true; } } } +// #else +// new WinHttpHandler { ServerCertificateValidationCallback = delegate { return true; } } +// #endif +// ) { +// BaseAddress = Options.ClientSettings.ConnectivitySettings.Address +// }; +// +// await Policy.Handle() +// .WaitAndRetryAsync(DefaultBackoffDelay) +// .ExecuteAsync( +// async () => { +// using var response = await http.GetAsync("/health/live", CancellationToken.None); +// if (response.StatusCode >= HttpStatusCode.BadRequest) +// throw new FluentDockerException($"Health check failed with status code: {response.StatusCode}."); +// } +// ); +// } } /// @@ -164,4 +163,4 @@ public async Task GetNextAvailablePort(TimeSpan delay = default) { } public int NextAvailablePort => GetNextAvailablePort(FromMilliseconds(100)).GetAwaiter().GetResult(); -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Tests.Common/GlobalEnvironment.cs b/test/EventStore.Client.Tests.Common/GlobalEnvironment.cs index d7d26dc67..1186d5f68 100644 --- a/test/EventStore.Client.Tests.Common/GlobalEnvironment.cs +++ b/test/EventStore.Client.Tests.Common/GlobalEnvironment.cs @@ -23,9 +23,11 @@ static void EnsureDefaults(IConfiguration configuration) { configuration.EnsureValue("ES_USE_EXTERNAL_SERVER", "false"); configuration.EnsureValue("ES_DOCKER_REGISTRY", "docker.eventstore.com/eventstore-ce/eventstoredb-ce"); - configuration.EnsureValue("ES_DOCKER_TAG", "ci"); + configuration.EnsureValue("ES_DOCKER_TAG", "previous-lts"); configuration.EnsureValue("ES_DOCKER_IMAGE", $"{configuration["ES_DOCKER_REGISTRY"]}:{configuration["ES_DOCKER_TAG"]}"); + configuration.EnsureValue("EVENTSTORE_TELEMETRY_OPTOUT", "true"); + configuration.EnsureValue("EVENTSTORE_ALLOW_UNKNOWN_OPTIONS", "true"); configuration.EnsureValue("EVENTSTORE_MEM_DB", "false"); configuration.EnsureValue("EVENTSTORE_RUN_PROJECTIONS", "None"); configuration.EnsureValue("EVENTSTORE_START_STANDARD_PROJECTIONS", "false"); From 0c2d0e3ec321310b8547fa96c29854cd0734d5df Mon Sep 17 00:00:00 2001 From: William Chong Date: Mon, 6 Jan 2025 16:03:29 +0400 Subject: [PATCH 04/15] Tests --- .github/workflows/base.yml | 18 +- .github/workflows/ci.yml | 36 +- .github/workflows/dispatch-ce.yml | 8 +- .github/workflows/dispatch-ee.yml | 58 +- .github/workflows/publish.yml | 386 ++++---- .github/workflows/pull-request-check.yml | 46 +- Directory.Build.props | 4 +- EventStore.Client.sln | 54 +- src/Directory.Build.props | 4 +- .../Core/Certificates/X509Certificates.cs | 57 +- ...entStoreClientSettings.ConnectionString.cs | 42 +- test/Directory.Build.props | 3 +- .../AssemblyInfo.cs | 1 - .../EventStore.Client.Operations.Tests.csproj | 6 - .../ResignNodeTests.cs | 20 - .../RestartPersistentSubscriptionsTests.cs | 20 - .../ShutdownNodeAuthenticationTests.cs | 12 - .../ShutdownNodeTests.cs | 12 - .../AssemblyInfo.cs | 1 - .../Bugs/Issue_1125.cs | 84 -- ...ubscriptionsTracingInstrumentationTests.cs | 155 --- ...lient.PersistentSubscriptions.Tests.csproj | 9 - ...tentSubscriptions.Tests.csproj.DotSettings | 4 - .../EventStoreClientFixture.cs | 41 - .../PersistentSubscriptionSettingsTests.cs | 11 - ...o_existing_with_max_one_client_obsolete.cs | 43 - ...t_to_existing_with_permissions_obsolete.cs | 37 - ...ting_with_start_from_beginning_obsolete.cs | 66 -- ...isting_with_start_from_not_set_obsolete.cs | 64 -- ...rom_not_set_then_event_written_obsolete.cs | 72 -- ...start_from_set_to_end_position_obsolete.cs | 63 -- ...nd_position_then_event_written_obsolete.cs | 69 -- ...set_to_invalid_middle_position_obsolete.cs | 49 - ...m_set_to_valid_middle_position_obsolete.cs | 65 -- ...o_existing_without_permissions_obsolete.cs | 32 - ...g_without_read_all_permissions_obsolete.cs | 33 - ..._non_existing_with_permissions_obsolete.cs | 32 - .../Obsolete/connect_with_retries_obsolete.cs | 59 -- ...eting_existing_with_subscriber_obsolete.cs | 66 -- ...p_to_link_to_events_manual_ack_obsolete.cs | 78 -- ...up_to_normal_events_manual_ack_obsolete.cs | 65 -- .../Obsolete/happy_case_filtered_obsolete.cs | 72 -- ...e_filtered_with_start_from_set_obsolete.cs | 86 -- ...ng_to_normal_events_manual_ack_obsolete.cs | 65 -- ...ting_with_check_point_filtered_obsolete.cs | 120 --- ...date_existing_with_subscribers_obsolete.cs | 60 -- ...iting_and_filtering_out_events_obsolete.cs | 108 --- ...g_to_normal_events_manual_nack_obsolete.cs | 68 -- ...ate_duplicate_name_on_different_streams.cs | 22 - ...connect_to_existing_with_max_one_client.cs | 31 - .../connect_to_existing_with_permissions.cs | 23 - ...t_to_existing_with_start_from_beginning.cs | 52 - ...ect_to_existing_with_start_from_not_set.cs | 44 - ...h_start_from_not_set_then_event_written.cs | 54 -- ...ing_with_start_from_set_to_end_position.cs | 46 - ..._set_to_end_position_then_event_written.cs | 56 -- ...art_from_set_to_invalid_middle_position.cs | 48 - ...start_from_set_to_valid_middle_position.cs | 51 - ...connect_to_existing_without_permissions.cs | 23 - ...o_existing_without_read_all_permissions.cs | 25 - ...onnect_to_non_existing_with_permissions.cs | 24 - .../SubscriptionToAll/connect_with_retries.cs | 49 - .../create_after_deleting_the_same.cs | 32 - .../SubscriptionToAll/create_duplicate.cs | 33 - .../SubscriptionToAll/create_filtered.cs | 29 - .../SubscriptionToAll/create_on_all_stream.cs | 21 - ...rsistent_subscription_with_dont_timeout.cs | 21 - ...position_equal_to_last_indexed_position.cs | 32 - ...ition_larger_than_last_indexed_position.cs | 41 - ...re_position_larger_than_commit_position.cs | 24 - .../create_without_permissions.cs | 25 - .../deleting_existing_with_permissions.cs | 26 - .../deleting_existing_with_subscriber.cs | 23 - .../SubscriptionToAll/deleting_filtered.cs | 24 - .../SubscriptionToAll/deleting_nonexistent.cs | 19 - .../deleting_without_permissions.cs | 19 - .../SubscriptionToAll/get_info.cs | 181 ---- ...atching_up_to_link_to_events_manual_ack.cs | 57 -- ...catching_up_to_normal_events_manual_ack.cs | 51 - .../SubscriptionToAll/happy_case_filtered.cs | 54 -- ...happy_case_filtered_with_start_from_set.cs | 62 -- ...subscribing_to_normal_events_manual_ack.cs | 57 -- .../list_with_persistent_subscriptions.cs | 82 -- .../list_without_persistent_subscriptions.cs | 26 - .../SubscriptionToAll/replay_parked.cs | 90 -- .../SubscriptionToAll/update_existing.cs | 28 - .../update_existing_filtered.cs | 30 - .../update_existing_with_check_point.cs | 108 --- ...date_existing_with_check_point_filtered.cs | 111 --- ...position_equal_to_last_indexed_position.cs | 41 - ...ition_larger_than_last_indexed_position.cs | 49 - .../update_existing_with_subscribers.cs | 49 - .../update_existing_without_permissions.cs | 32 - .../SubscriptionToAll/update_non_existent.cs | 25 - ...re_position_larger_than_commit_position.cs | 26 - .../when_writing_and_filtering_out_events.cs | 87 -- ...ubscribing_to_normal_events_manual_nack.cs | 57 -- ...o_existing_with_max_one_client_obsolete.cs | 47 - ...t_to_existing_with_permissions_obsolete.cs | 39 - ...rom_beginning_and_events_in_it_obsolete.cs | 65 -- ...t_from_beginning_and_no_stream_obsolete.cs | 65 -- ..._from_not_set_and_events_in_it_obsolete.cs | 61 -- ...vents_in_it_then_event_written_obsolete.cs | 67 -- ..._end_position_and_events_in_it_obsolete.cs | 60 -- ...vents_in_it_then_event_written_obsolete.cs | 72 -- ...h_start_from_two_and_no_stream_obsolete.cs | 65 -- ...rt_from_x_set_and_events_in_it_obsolete.cs | 65 -- ...vents_in_it_then_event_written_obsolete.cs | 68 -- ...vents_in_it_then_event_written_obsolete.cs | 72 -- ...o_existing_without_permissions_obsolete.cs | 35 - ..._non_existing_with_permissions_obsolete.cs | 34 - .../Obsolete/connect_with_retries_obsolete.cs | 70 -- ...g_to_a_persistent_subscription_obsolete.cs | 72 -- ...eting_existing_with_subscriber_obsolete.cs | 67 -- .../Obsolete/get_info_obsolete.cs | 199 ---- ...p_to_link_to_events_manual_ack_obsolete.cs | 80 -- ...up_to_normal_events_manual_ack_obsolete.cs | 67 -- ...ng_to_normal_events_manual_ack_obsolete.cs | 67 -- ...date_existing_with_check_point_obsolete.cs | 124 --- ...date_existing_with_subscribers_obsolete.cs | 61 -- ...g_to_normal_events_manual_nack_obsolete.cs | 70 -- ...ate_duplicate_name_on_different_streams.cs | 32 - ...connect_to_existing_with_max_one_client.cs | 33 - .../connect_to_existing_with_permissions.cs | 26 - ...h_start_from_beginning_and_events_in_it.cs | 51 - ...with_start_from_beginning_and_no_stream.cs | 51 - ...ith_start_from_not_set_and_events_in_it.cs | 52 - ...set_and_events_in_it_then_event_written.cs | 58 -- ...om_set_to_end_position_and_events_in_it.cs | 53 - ...ion_and_events_in_it_then_event_written.cs | 57 -- ...sting_with_start_from_two_and_no_stream.cs | 55 -- ..._with_start_from_x_set_and_events_in_it.cs | 55 -- ...set_and_events_in_it_then_event_written.cs | 54 -- ...n_x_and_events_in_it_then_event_written.cs | 58 -- ...connect_to_existing_without_permissions.cs | 29 - ...onnect_to_non_existing_with_permissions.cs | 24 - .../connect_with_retries.cs | 60 -- ...connecting_to_a_persistent_subscription.cs | 47 - .../create_after_deleting_the_same.cs | 38 - .../SubscriptionToStream/create_duplicate.cs | 37 - .../create_on_existing_stream.cs | 24 - .../create_on_non_existing_stream.cs | 23 - ...rsistent_subscription_with_dont_timeout.cs | 23 - .../create_without_permissions.cs | 27 - .../deleting_existing_with_permissions.cs | 29 - .../deleting_existing_with_subscriber.cs | 26 - .../deleting_nonexistent.cs | 24 - .../deleting_without_permissions.cs | 25 - .../SubscriptionToStream/get_info.cs | 198 ---- ...atching_up_to_link_to_events_manual_ack.cs | 59 -- ...catching_up_to_normal_events_manual_ack.cs | 53 - ...subscribing_to_normal_events_manual_ack.cs | 52 - .../list_with_persistent_subscriptions.cs | 82 -- .../list_without_persistent_subscriptions.cs | 36 - .../SubscriptionToStream/replay_parked.cs | 80 -- .../SubscriptionToStream/update_existing.cs | 33 - .../update_existing_with_check_point.cs | 95 -- .../update_existing_with_subscribers.cs | 50 - .../update_existing_without_permissions.cs | 42 - .../update_non_existent.cs | 26 - ...ubscribing_to_normal_events_manual_nack.cs | 54 -- .../restart_subsystem.cs | 74 -- .../EventStore.Client.Plugins.Tests.csproj | 6 - .../AssemblyInfo.cs | 1 - ...e.Client.ProjectionManagement.Tests.csproj | 6 - .../EventStoreClientFixture.cs | 50 - .../StandardProjections.cs | 30 - .../abort.cs | 21 - .../create.cs | 35 - .../disable.cs | 21 - .../enable.cs | 22 - .../get_result.cs | 51 - .../get_state.cs | 51 - .../get_status.cs | 21 - .../list_all_projections.cs | 20 - .../list_continuous_projections.cs | 31 - .../list_one_time_projections.cs | 23 - .../reset.cs | 22 - .../restart_subsystem.cs | 20 - .../update.cs | 30 - .../Append/append_to_stream_limits.cs | 54 -- .../Append/append_to_stream_retry.cs | 37 - .../append_to_stream_with_tls_ca_file.cs | 35 - .../appending_to_implicitly_created_stream.cs | 266 ------ .../sending_and_receiving_large_messages.cs | 29 - .../AssemblyInfo.cs | 1 - .../DependencyInjectionTests.cs | 75 -- .../StreamsTracingInstrumentationTests.cs | 221 ----- .../EventDataTests.cs | 23 - .../EventStore.Client.Streams.Tests.csproj | 9 - ...re.Client.Streams.Tests.csproj.DotSettings | 12 - .../Obsolete/SecurityFixture_obsolete.cs | 321 ------- ...ll_stream_with_no_acl_security_obsolete.cs | 37 - ...system_stream_security_for_all_obsolete.cs | 70 -- ...erriden_system_stream_security_obsolete.cs | 87 -- ...overriden_user_stream_security_obsolete.cs | 78 -- .../subscribe_to_all_security_obsolete.cs | 22 - .../subscribe_to_stream_security_obsolete.cs | 75 -- .../system_stream_security_obsolete.cs | 146 --- .../Serialization/is_json.cs | 59 -- .../Obsolete/subscribe_to_all_obsolete.cs | 592 ------------ .../Obsolete/subscribe_to_stream_obsolete.cs | 303 ------ .../SubscriptionDroppedResult.cs | 16 - .../Subscriptions/SubscriptionsFixture.cs | 18 - .../Subscriptions/reconnection.cs | 178 ---- .../Subscriptions/subscribe_to_all.cs | 480 ---------- .../AssertEx.cs | 4 +- .../EventStore.Client.Tests.Common.csproj | 118 +-- .../Extensions/ConfigurationExtensions.cs | 2 +- .../Extensions/EventStoreClientExtensions.cs | 2 +- .../EventStoreClientWarmupExtensions.cs | 2 +- .../Extensions/OperatingSystemExtensions.cs | 2 +- .../Extensions/ReadOnlyMemoryExtensions.cs | 2 +- .../Extensions}/ShouldThrowAsyncExtensions.cs | 4 +- .../Extensions}/TypeExtensions.cs | 4 +- .../Extensions/WithExtension.cs | 2 +- .../Facts/AnonymousAccess.cs | 2 +- .../Facts/Deprecation.cs | 6 +- .../Facts/Regression.cs | 6 +- .../Facts/SupportsPSToAllFact.cs | 18 - .../Fakers/TestUserFaker.cs | 2 +- .../Base/EventStoreClientFixtureBase.cs | 147 --- .../Fixtures/Base/EventStoreTestServer.cs | 143 --- .../Base/EventStoreTestServerCluster.cs | 93 -- .../Base/EventStoreTestServerExternal.cs | 8 - .../Fixtures/Base/IEventStoreTestServer.cs | 6 - .../Fixtures/BaseTestNode.cs | 162 ++++ .../Fixtures/DiagnosticsFixture.cs | 107 --- .../Fixtures/EventStoreTestCluster.cs | 52 - .../Fixtures/InsecureClientTestFixture.cs | 7 - .../Fixtures/KurrentFixtureOptions.cs | 20 + ....cs => KurrentPermanentFixture.Helpers.cs} | 24 +- ...eFixture.cs => KurrentPermanentFixture.cs} | 90 +- ...estNode.cs => KurrentPermanentTestNode.cs} | 150 +-- .../KurrentTemporaryFixture.Helpers.cs | 107 +++ .../Fixtures/KurrentTemporaryFixture.cs | 189 ++++ .../Fixtures/KurrentTemporaryTestNode.cs | 193 ++++ .../Fixtures/RunInMemoryTestFixture.cs | 4 - .../Fixtures/RunProjectionsTestFixture.cs | 4 - .../FluentDockerBuilderExtensions.cs | 2 +- .../FluentDockerServiceExtensions.cs | 2 +- .../FluentDocker/TestCompositeService.cs | 2 +- .../FluentDocker/TestContainerService.cs | 2 +- .../GlobalEnvironment.cs | 34 +- .../EventStore.Client.Tests.Common/Logging.cs | 2 +- .../PasswordGenerator.cs | 2 +- .../Shouldly/ShouldThrowAsyncExtensions.cs | 2 +- .../TestCredentials.cs | 2 +- .../Assertions/ComparableAssertion.cs | 2 +- .../Assertions/EqualityAssertion.cs | 2 +- .../Assertions/NullArgumentAssertion.cs | 2 +- .../Assertions/StringConversionAssertion.cs | 2 +- .../Assertions/ValueObjectAssertion.cs | 2 +- .../AutoScenarioDataAttribute.cs | 2 +- .../ClientCertificatesTests.cs} | 27 +- .../ConnectionStringTests.cs | 24 +- ....cs => EventStoreClientOperationsTests.cs} | 6 +- test/EventStore.Client.Tests/FromAllTests.cs | 8 +- .../FromStreamTests.cs | 14 +- .../GossipChannelSelectorTests.cs | 6 +- .../GrpcServerCapabilitiesClientTests.cs | 2 +- .../ReportLeaderInterceptorTests.cs | 224 ----- .../ListProjectionTests.cs | 43 + .../NodePreferenceComparerTests.cs | 2 +- .../NodeSelectorTests.cs | 4 +- .../Operations}/AuthenticationTests.cs | 33 +- .../Operations/MergeIndexTests.cs} | 18 +- .../Operations/ResignNodeTests.cs | 21 + .../RestartPersistentSubscriptionsTests.cs | 20 + .../Operations}/ScavengeTests.cs | 29 +- .../ShutdownNodeAuthenticationTests.cs | 13 + .../Operations/ShutdownNodeTests.cs | 13 + .../FilterTestCases.cs} | 4 +- ...ToAllConnectWithoutReadPermissionsTests.cs | 30 + .../SubscribeToAllFilterTests.cs | 129 +++ .../SubscribeToAllGetInfoTests.cs | 101 ++ ...eToAllListWithIncorrectCredentialsTests.cs | 63 ++ ...SubscribeToAllNoDefaultCredentialsTests.cs | 93 ++ .../SubscribeToAllReplayParkedTests.cs | 84 ++ ...AllResultWithNormalUserCredentialsTests.cs | 34 + .../SubscribeToAllReturnsAllSubscriptions.cs | 41 + ...AllReturnsSubscriptionsToAllStreamTests.cs | 35 + .../SubscribeToAllTests.cs | 903 ++++++++++++++++++ ...beToAllUpdateExistingWithCheckpointTest.cs | 86 ++ .../SubscribeToAllWithoutPSTests.cs | 15 + .../SubscribeToStreamGetInfoTests.cs} | 201 ++-- .../SubscribeToStreamListTests.cs | 44 + ...scribeToStreamNoDefaultCredentialsTests.cs | 77 ++ .../SubscribeToStreamReplayParkedTests.cs | 68 ++ .../SubscribeToStreamTests.cs | 743 ++++++++++++++ test/EventStore.Client.Tests/PositionTests.cs | 6 +- .../PrefixFilterExpressionTests.cs | 11 - .../ProjectionManagementTests.cs | 205 ++++ .../RegularFilterExpressionTests.cs | 2 +- .../AllStreamWithNoAclSecurityTests.cs} | 10 +- .../Security/DeleteStreamSecurityTests.cs} | 35 +- .../Security/MultipleRoleSecurityTests.cs} | 12 +- ...erridenSystemStreamSecurityForAllTests.cs} | 8 +- .../OverridenSystemStreamSecurityTests.cs} | 8 +- .../OverridenUserStreamSecurityTests.cs} | 8 +- .../Security/ReadAllSecurityTests.cs} | 9 +- .../Security/ReadStreamMetaSecurityTests.cs} | 25 +- .../Security/ReadStreamSecurityTests.cs} | 11 +- .../Security/SecurityFixture.cs | 17 +- .../StreamSecurityInheritanceTests.cs} | 14 +- .../Security/SubscribeToAllSecurityTests.cs} | 9 +- .../SubscribeToStreamSecurityTests.cs} | 25 +- .../Security/SystemStreamSecurityTests.cs} | 11 +- .../Security/WriteStreamMetaSecurityTests.cs} | 16 +- .../Security/WriteStreamSecurityTests.cs} | 8 +- .../SharingProviderTests.cs | 22 +- .../StreamPositionTests.cs | 26 +- .../StreamRevisionTests.cs | 26 +- .../StreamStateTests.cs | 18 +- .../Streams/AppendTests.cs} | 390 +++++++- .../Streams/Bugs/Obsolete/Issue104.cs} | 4 +- .../Streams/Bugs/Obsolete/Issue2544.cs} | 42 +- .../Streams/DeleteTests.cs} | 45 +- .../Streams}/Read/EventBinaryData.cs | 28 +- .../Streams}/Read/EventDataComparer.cs | 4 +- .../Read/ReadAllEventsBackwardTests.cs} | 28 +- .../Streams}/Read/ReadAllEventsFixture.cs | 12 +- .../Read/ReadAllEventsForwardTests.cs} | 31 +- .../Streams/Read/ReadStreamBackwardTests.cs} | 18 +- ...StreamEventsLinkedToDeletedStreamTests.cs} | 32 +- .../Streams/Read/ReadStreamForwardTests.cs} | 12 +- ...eamWhenHavingMaxCountSetForStreamTests.cs} | 10 +- .../Streams/SoftDeleteTests.cs} | 34 +- .../Streams/StreamMetadataTests.cs} | 17 +- .../Streams/SubscribeToStreamTests.cs} | 63 +- .../Streams}/SubscriptionFilter.cs | 9 +- test/EventStore.Client.Tests/UuidTests.cs | 14 +- .../ValueObjectTests.cs | 6 +- .../X509CertificatesTests.cs | 14 +- .../AssemblyInfo.cs | 1 - ...ntStore.Client.UserManagement.Tests.csproj | 12 - ...nt.UserManagement.Tests.csproj.DotSettings | 2 - .../InvalidCredentialsTestCases.cs | 30 - .../UserCredentialsTests.cs | 42 - .../changing_user_password.cs | 76 -- .../creating_a_user.cs | 84 -- .../deleting_a_user.cs | 68 -- .../disabling_a_user.cs | 64 -- .../enabling_a_user.cs | 62 -- .../getting_current_user.cs | 14 - .../listing_users.cs | 42 - .../resetting_user_password.cs | 80 -- 347 files changed, 5062 insertions(+), 14058 deletions(-) delete mode 100644 test/EventStore.Client.Operations.Tests/AssemblyInfo.cs delete mode 100644 test/EventStore.Client.Operations.Tests/EventStore.Client.Operations.Tests.csproj delete mode 100644 test/EventStore.Client.Operations.Tests/ResignNodeTests.cs delete mode 100644 test/EventStore.Client.Operations.Tests/RestartPersistentSubscriptionsTests.cs delete mode 100644 test/EventStore.Client.Operations.Tests/ShutdownNodeAuthenticationTests.cs delete mode 100644 test/EventStore.Client.Operations.Tests/ShutdownNodeTests.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/AssemblyInfo.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/Bugs/Issue_1125.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/Diagnostics/PersistentSubscriptionsTracingInstrumentationTests.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/EventStore.Client.PersistentSubscriptions.Tests.csproj delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/EventStore.Client.PersistentSubscriptions.Tests.csproj.DotSettings delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/EventStoreClientFixture.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/PersistentSubscriptionSettingsTests.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/connect_to_existing_with_max_one_client_obsolete.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/connect_to_existing_with_permissions_obsolete.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/connect_to_existing_with_start_from_beginning_obsolete.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/connect_to_existing_with_start_from_not_set_obsolete.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/connect_to_existing_with_start_from_not_set_then_event_written_obsolete.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/connect_to_existing_with_start_from_set_to_end_position_obsolete.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/connect_to_existing_with_start_from_set_to_end_position_then_event_written_obsolete.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/connect_to_existing_with_start_from_set_to_invalid_middle_position_obsolete.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/connect_to_existing_with_start_from_set_to_valid_middle_position_obsolete.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/connect_to_existing_without_permissions_obsolete.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/connect_to_existing_without_read_all_permissions_obsolete.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/connect_to_non_existing_with_permissions_obsolete.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/connect_with_retries_obsolete.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/deleting_existing_with_subscriber_obsolete.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/happy_case_catching_up_to_link_to_events_manual_ack_obsolete.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/happy_case_catching_up_to_normal_events_manual_ack_obsolete.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/happy_case_filtered_obsolete.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/happy_case_filtered_with_start_from_set_obsolete.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/happy_case_writing_and_subscribing_to_normal_events_manual_ack_obsolete.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/update_existing_with_check_point_filtered_obsolete.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/update_existing_with_subscribers_obsolete.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/when_writing_and_filtering_out_events_obsolete.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/when_writing_and_subscribing_to_normal_events_manual_nack_obsolete.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/can_create_duplicate_name_on_different_streams.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_max_one_client.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_permissions.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_beginning.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_not_set.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_not_set_then_event_written.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_set_to_end_position.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_set_to_end_position_then_event_written.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_set_to_invalid_middle_position.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_set_to_valid_middle_position.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_without_permissions.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_without_read_all_permissions.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_non_existing_with_permissions.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_with_retries.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/create_after_deleting_the_same.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/create_duplicate.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/create_filtered.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/create_on_all_stream.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/create_persistent_subscription_with_dont_timeout.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/create_with_commit_position_equal_to_last_indexed_position.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/create_with_commit_position_larger_than_last_indexed_position.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/create_with_prepare_position_larger_than_commit_position.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/create_without_permissions.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/deleting_existing_with_permissions.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/deleting_existing_with_subscriber.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/deleting_filtered.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/deleting_nonexistent.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/deleting_without_permissions.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/get_info.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/happy_case_catching_up_to_link_to_events_manual_ack.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/happy_case_catching_up_to_normal_events_manual_ack.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/happy_case_filtered.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/happy_case_filtered_with_start_from_set.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/happy_case_writing_and_subscribing_to_normal_events_manual_ack.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/list_with_persistent_subscriptions.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/list_without_persistent_subscriptions.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/replay_parked.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/update_existing.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/update_existing_filtered.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/update_existing_with_check_point.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/update_existing_with_check_point_filtered.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/update_existing_with_commit_position_equal_to_last_indexed_position.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/update_existing_with_commit_position_larger_than_last_indexed_position.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/update_existing_with_subscribers.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/update_existing_without_permissions.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/update_non_existent.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/update_with_prepare_position_larger_than_commit_position.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/when_writing_and_filtering_out_events.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/when_writing_and_subscribing_to_normal_events_manual_nack.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/connect_to_existing_with_max_one_client_obsolete.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/connect_to_existing_with_permissions_obsolete.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/connect_to_existing_with_start_from_beginning_and_events_in_it_obsolete.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/connect_to_existing_with_start_from_beginning_and_no_stream_obsolete.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/connect_to_existing_with_start_from_not_set_and_events_in_it_obsolete.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/connect_to_existing_with_start_from_not_set_and_events_in_it_then_event_written_obsolete.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/connect_to_existing_with_start_from_set_to_end_position_and_events_in_it_obsolete.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/connect_to_existing_with_start_from_set_to_end_position_and_events_in_it_then_event_written_obsolete.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/connect_to_existing_with_start_from_two_and_no_stream_obsolete.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/connect_to_existing_with_start_from_x_set_and_events_in_it_obsolete.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/connect_to_existing_with_start_from_x_set_and_events_in_it_then_event_written_obsolete.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/connect_to_existing_with_start_from_x_set_higher_than_x_and_events_in_it_then_event_written_obsolete.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/connect_to_existing_without_permissions_obsolete.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/connect_to_non_existing_with_permissions_obsolete.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/connect_with_retries_obsolete.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/connecting_to_a_persistent_subscription_obsolete.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/deleting_existing_with_subscriber_obsolete.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/get_info_obsolete.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/happy_case_catching_up_to_link_to_events_manual_ack_obsolete.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/happy_case_catching_up_to_normal_events_manual_ack_obsolete.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/happy_case_writing_and_subscribing_to_normal_events_manual_ack_obsolete.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/update_existing_with_check_point_obsolete.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/update_existing_with_subscribers_obsolete.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/when_writing_and_subscribing_to_normal_events_manual_nack_obsolete.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/can_create_duplicate_name_on_different_streams.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_max_one_client.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_permissions.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_beginning_and_events_in_it.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_beginning_and_no_stream.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_not_set_and_events_in_it.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_not_set_and_events_in_it_then_event_written.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_set_to_end_position_and_events_in_it.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_set_to_end_position_and_events_in_it_then_event_written.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_two_and_no_stream.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_x_set_and_events_in_it.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_x_set_and_events_in_it_then_event_written.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_x_set_higher_than_x_and_events_in_it_then_event_written.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_without_permissions.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_non_existing_with_permissions.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_with_retries.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connecting_to_a_persistent_subscription.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/create_after_deleting_the_same.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/create_duplicate.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/create_on_existing_stream.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/create_on_non_existing_stream.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/create_persistent_subscription_with_dont_timeout.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/create_without_permissions.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/deleting_existing_with_permissions.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/deleting_existing_with_subscriber.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/deleting_nonexistent.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/deleting_without_permissions.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/get_info.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/happy_case_catching_up_to_link_to_events_manual_ack.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/happy_case_catching_up_to_normal_events_manual_ack.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/happy_case_writing_and_subscribing_to_normal_events_manual_ack.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/list_with_persistent_subscriptions.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/list_without_persistent_subscriptions.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/replay_parked.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/update_existing.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/update_existing_with_check_point.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/update_existing_with_subscribers.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/update_existing_without_permissions.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/update_non_existent.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/when_writing_and_subscribing_to_normal_events_manual_nack.cs delete mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/restart_subsystem.cs delete mode 100644 test/EventStore.Client.Plugins.Tests/EventStore.Client.Plugins.Tests.csproj delete mode 100644 test/EventStore.Client.ProjectionManagement.Tests/AssemblyInfo.cs delete mode 100644 test/EventStore.Client.ProjectionManagement.Tests/EventStore.Client.ProjectionManagement.Tests.csproj delete mode 100644 test/EventStore.Client.ProjectionManagement.Tests/EventStoreClientFixture.cs delete mode 100644 test/EventStore.Client.ProjectionManagement.Tests/StandardProjections.cs delete mode 100644 test/EventStore.Client.ProjectionManagement.Tests/abort.cs delete mode 100644 test/EventStore.Client.ProjectionManagement.Tests/create.cs delete mode 100644 test/EventStore.Client.ProjectionManagement.Tests/disable.cs delete mode 100644 test/EventStore.Client.ProjectionManagement.Tests/enable.cs delete mode 100644 test/EventStore.Client.ProjectionManagement.Tests/get_result.cs delete mode 100644 test/EventStore.Client.ProjectionManagement.Tests/get_state.cs delete mode 100644 test/EventStore.Client.ProjectionManagement.Tests/get_status.cs delete mode 100644 test/EventStore.Client.ProjectionManagement.Tests/list_all_projections.cs delete mode 100644 test/EventStore.Client.ProjectionManagement.Tests/list_continuous_projections.cs delete mode 100644 test/EventStore.Client.ProjectionManagement.Tests/list_one_time_projections.cs delete mode 100644 test/EventStore.Client.ProjectionManagement.Tests/reset.cs delete mode 100644 test/EventStore.Client.ProjectionManagement.Tests/restart_subsystem.cs delete mode 100644 test/EventStore.Client.ProjectionManagement.Tests/update.cs delete mode 100644 test/EventStore.Client.Streams.Tests/Append/append_to_stream_limits.cs delete mode 100644 test/EventStore.Client.Streams.Tests/Append/append_to_stream_retry.cs delete mode 100644 test/EventStore.Client.Streams.Tests/Append/append_to_stream_with_tls_ca_file.cs delete mode 100644 test/EventStore.Client.Streams.Tests/Append/appending_to_implicitly_created_stream.cs delete mode 100644 test/EventStore.Client.Streams.Tests/Append/sending_and_receiving_large_messages.cs delete mode 100644 test/EventStore.Client.Streams.Tests/AssemblyInfo.cs delete mode 100644 test/EventStore.Client.Streams.Tests/DependencyInjectionTests.cs delete mode 100644 test/EventStore.Client.Streams.Tests/Diagnostics/StreamsTracingInstrumentationTests.cs delete mode 100644 test/EventStore.Client.Streams.Tests/EventDataTests.cs delete mode 100644 test/EventStore.Client.Streams.Tests/EventStore.Client.Streams.Tests.csproj delete mode 100644 test/EventStore.Client.Streams.Tests/EventStore.Client.Streams.Tests.csproj.DotSettings delete mode 100644 test/EventStore.Client.Streams.Tests/Security/Obsolete/SecurityFixture_obsolete.cs delete mode 100644 test/EventStore.Client.Streams.Tests/Security/Obsolete/all_stream_with_no_acl_security_obsolete.cs delete mode 100644 test/EventStore.Client.Streams.Tests/Security/Obsolete/overriden_system_stream_security_for_all_obsolete.cs delete mode 100644 test/EventStore.Client.Streams.Tests/Security/Obsolete/overriden_system_stream_security_obsolete.cs delete mode 100644 test/EventStore.Client.Streams.Tests/Security/Obsolete/overriden_user_stream_security_obsolete.cs delete mode 100644 test/EventStore.Client.Streams.Tests/Security/Obsolete/subscribe_to_all_security_obsolete.cs delete mode 100644 test/EventStore.Client.Streams.Tests/Security/Obsolete/subscribe_to_stream_security_obsolete.cs delete mode 100644 test/EventStore.Client.Streams.Tests/Security/Obsolete/system_stream_security_obsolete.cs delete mode 100644 test/EventStore.Client.Streams.Tests/Serialization/is_json.cs delete mode 100644 test/EventStore.Client.Streams.Tests/Subscriptions/Obsolete/subscribe_to_all_obsolete.cs delete mode 100644 test/EventStore.Client.Streams.Tests/Subscriptions/Obsolete/subscribe_to_stream_obsolete.cs delete mode 100644 test/EventStore.Client.Streams.Tests/Subscriptions/SubscriptionDroppedResult.cs delete mode 100644 test/EventStore.Client.Streams.Tests/Subscriptions/SubscriptionsFixture.cs delete mode 100644 test/EventStore.Client.Streams.Tests/Subscriptions/reconnection.cs delete mode 100644 test/EventStore.Client.Streams.Tests/Subscriptions/subscribe_to_all.cs rename test/{EventStore.Client.ProjectionManagement.Tests => EventStore.Client.Tests.Common}/AssertEx.cs (97%) rename test/{EventStore.Client.Streams.Tests/Append => EventStore.Client.Tests.Common/Extensions}/ShouldThrowAsyncExtensions.cs (90%) rename test/{EventStore.Client.Tests => EventStore.Client.Tests.Common/Extensions}/TypeExtensions.cs (98%) delete mode 100644 test/EventStore.Client.Tests.Common/Facts/SupportsPSToAllFact.cs delete mode 100644 test/EventStore.Client.Tests.Common/Fixtures/Base/EventStoreClientFixtureBase.cs delete mode 100644 test/EventStore.Client.Tests.Common/Fixtures/Base/EventStoreTestServer.cs delete mode 100644 test/EventStore.Client.Tests.Common/Fixtures/Base/EventStoreTestServerCluster.cs delete mode 100644 test/EventStore.Client.Tests.Common/Fixtures/Base/EventStoreTestServerExternal.cs delete mode 100644 test/EventStore.Client.Tests.Common/Fixtures/Base/IEventStoreTestServer.cs create mode 100644 test/EventStore.Client.Tests.Common/Fixtures/BaseTestNode.cs delete mode 100644 test/EventStore.Client.Tests.Common/Fixtures/DiagnosticsFixture.cs delete mode 100644 test/EventStore.Client.Tests.Common/Fixtures/EventStoreTestCluster.cs delete mode 100644 test/EventStore.Client.Tests.Common/Fixtures/InsecureClientTestFixture.cs create mode 100644 test/EventStore.Client.Tests.Common/Fixtures/KurrentFixtureOptions.cs rename test/EventStore.Client.Tests.Common/Fixtures/{EventStoreFixture.Helpers.cs => KurrentPermanentFixture.Helpers.cs} (78%) rename test/EventStore.Client.Tests.Common/Fixtures/{EventStoreFixture.cs => KurrentPermanentFixture.cs} (64%) rename test/EventStore.Client.Tests.Common/Fixtures/{EventStoreTestNode.cs => KurrentPermanentTestNode.cs} (54%) create mode 100644 test/EventStore.Client.Tests.Common/Fixtures/KurrentTemporaryFixture.Helpers.cs create mode 100644 test/EventStore.Client.Tests.Common/Fixtures/KurrentTemporaryFixture.cs create mode 100644 test/EventStore.Client.Tests.Common/Fixtures/KurrentTemporaryTestNode.cs delete mode 100644 test/EventStore.Client.Tests.Common/Fixtures/RunInMemoryTestFixture.cs delete mode 100644 test/EventStore.Client.Tests.Common/Fixtures/RunProjectionsTestFixture.cs rename test/{EventStore.Client.Plugins.Tests/ClientCertificateTests.cs => EventStore.Client.Tests/ClientCertificatesTests.cs} (76%) rename test/EventStore.Client.Tests/{EventStoreClientOperationOptionsTests.cs => EventStoreClientOperationsTests.cs} (89%) delete mode 100644 test/EventStore.Client.Tests/Interceptors/ReportLeaderInterceptorTests.cs create mode 100644 test/EventStore.Client.Tests/ListProjectionTests.cs rename test/{EventStore.Client.Operations.Tests => EventStore.Client.Tests/Operations}/AuthenticationTests.cs (64%) rename test/{EventStore.Client.Operations.Tests/MergeIndexesTests.cs => EventStore.Client.Tests/Operations/MergeIndexTests.cs} (50%) create mode 100644 test/EventStore.Client.Tests/Operations/ResignNodeTests.cs create mode 100644 test/EventStore.Client.Tests/Operations/RestartPersistentSubscriptionsTests.cs rename test/{EventStore.Client.Operations.Tests => EventStore.Client.Tests/Operations}/ScavengeTests.cs (79%) create mode 100644 test/EventStore.Client.Tests/Operations/ShutdownNodeAuthenticationTests.cs create mode 100644 test/EventStore.Client.Tests/Operations/ShutdownNodeTests.cs rename test/{EventStore.Client.PersistentSubscriptions.Tests/FilterTestCase.cs => EventStore.Client.Tests/PersistentSubscriptions/FilterTestCases.cs} (95%) create mode 100644 test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToAllConnectWithoutReadPermissionsTests.cs create mode 100644 test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToAllFilterTests.cs create mode 100644 test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToAllGetInfoTests.cs create mode 100644 test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToAllListWithIncorrectCredentialsTests.cs create mode 100644 test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToAllNoDefaultCredentialsTests.cs create mode 100644 test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToAllReplayParkedTests.cs create mode 100644 test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToAllResultWithNormalUserCredentialsTests.cs create mode 100644 test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToAllReturnsAllSubscriptions.cs create mode 100644 test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToAllReturnsSubscriptionsToAllStreamTests.cs create mode 100644 test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToAllTests.cs create mode 100644 test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToAllUpdateExistingWithCheckpointTest.cs create mode 100644 test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToAllWithoutPSTests.cs rename test/{EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/get_info_obsolete.cs => EventStore.Client.Tests/PersistentSubscriptions/SubscribeToStreamGetInfoTests.cs} (52%) create mode 100644 test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToStreamListTests.cs create mode 100644 test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToStreamNoDefaultCredentialsTests.cs create mode 100644 test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToStreamReplayParkedTests.cs create mode 100644 test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToStreamTests.cs delete mode 100644 test/EventStore.Client.Tests/PrefixFilterExpressionTests.cs create mode 100644 test/EventStore.Client.Tests/ProjectionManagementTests.cs rename test/{EventStore.Client.Streams.Tests/Security/all_stream_with_no_acl_security.cs => EventStore.Client.Tests/Security/AllStreamWithNoAclSecurityTests.cs} (90%) rename test/{EventStore.Client.Streams.Tests/Security/delete_stream_security.cs => EventStore.Client.Tests/Security/DeleteStreamSecurityTests.cs} (87%) rename test/{EventStore.Client.Streams.Tests/Security/multiple_role_security.cs => EventStore.Client.Tests/Security/MultipleRoleSecurityTests.cs} (83%) rename test/{EventStore.Client.Streams.Tests/Security/overriden_system_stream_security_for_all.cs => EventStore.Client.Tests/Security/OverridenSystemStreamSecurityForAllTests.cs} (86%) rename test/{EventStore.Client.Streams.Tests/Security/overriden_system_stream_security.cs => EventStore.Client.Tests/Security/OverridenSystemStreamSecurityTests.cs} (91%) rename test/{EventStore.Client.Streams.Tests/Security/overriden_user_stream_security.cs => EventStore.Client.Tests/Security/OverridenUserStreamSecurityTests.cs} (91%) rename test/{EventStore.Client.Streams.Tests/Security/read_all_security.cs => EventStore.Client.Tests/Security/ReadAllSecurityTests.cs} (83%) rename test/{EventStore.Client.Streams.Tests/Security/read_stream_meta_security.cs => EventStore.Client.Tests/Security/ReadStreamMetaSecurityTests.cs} (74%) rename test/{EventStore.Client.Streams.Tests/Security/read_stream_security.cs => EventStore.Client.Tests/Security/ReadStreamSecurityTests.cs} (96%) rename test/{EventStore.Client.Streams.Tests => EventStore.Client.Tests}/Security/SecurityFixture.cs (97%) rename test/{EventStore.Client.Streams.Tests/Security/stream_security_inheritance.cs => EventStore.Client.Tests/Security/StreamSecurityInheritanceTests.cs} (95%) rename test/{EventStore.Client.Streams.Tests/Security/subscribe_to_all_security.cs => EventStore.Client.Tests/Security/SubscribeToAllSecurityTests.cs} (77%) rename test/{EventStore.Client.Streams.Tests/Security/subscribe_to_stream_security.cs => EventStore.Client.Tests/Security/SubscribeToStreamSecurityTests.cs} (78%) rename test/{EventStore.Client.Streams.Tests/Security/system_stream_security.cs => EventStore.Client.Tests/Security/SystemStreamSecurityTests.cs} (94%) rename test/{EventStore.Client.Streams.Tests/Security/write_stream_meta_security.cs => EventStore.Client.Tests/Security/WriteStreamMetaSecurityTests.cs} (86%) rename test/{EventStore.Client.Streams.Tests/Security/write_stream_security.cs => EventStore.Client.Tests/Security/WriteStreamSecurityTests.cs} (93%) rename test/{EventStore.Client.Streams.Tests/Append/append_to_stream.cs => EventStore.Client.Tests/Streams/AppendTests.cs} (55%) rename test/{EventStore.Client.Streams.Tests/Bugs/Obsolete/Issue_104.cs => EventStore.Client.Tests/Streams/Bugs/Obsolete/Issue104.cs} (89%) rename test/{EventStore.Client.Streams.Tests/Bugs/Obsolete/Issue_2544.cs => EventStore.Client.Tests/Streams/Bugs/Obsolete/Issue2544.cs} (82%) rename test/{EventStore.Client.Streams.Tests/Delete/deleting_stream.cs => EventStore.Client.Tests/Streams/DeleteTests.cs} (72%) rename test/{EventStore.Client.Streams.Tests => EventStore.Client.Tests/Streams}/Read/EventBinaryData.cs (81%) rename test/{EventStore.Client.Streams.Tests => EventStore.Client.Tests/Streams}/Read/EventDataComparer.cs (91%) rename test/{EventStore.Client.Streams.Tests/Read/read_all_events_backward.cs => EventStore.Client.Tests/Streams/Read/ReadAllEventsBackwardTests.cs} (87%) rename test/{EventStore.Client.Streams.Tests => EventStore.Client.Tests/Streams}/Read/ReadAllEventsFixture.cs (81%) rename test/{EventStore.Client.Streams.Tests/Read/read_all_events_forward.cs => EventStore.Client.Tests/Streams/Read/ReadAllEventsForwardTests.cs} (91%) rename test/{EventStore.Client.Streams.Tests/Read/read_stream_backward.cs => EventStore.Client.Tests/Streams/Read/ReadStreamBackwardTests.cs} (96%) rename test/{EventStore.Client.Streams.Tests/Read/read_stream_events_linked_to_deleted_stream.cs => EventStore.Client.Tests/Streams/Read/ReadStreamEventsLinkedToDeletedStreamTests.cs} (70%) rename test/{EventStore.Client.Streams.Tests/Read/read_stream_forward.cs => EventStore.Client.Tests/Streams/Read/ReadStreamForwardTests.cs} (96%) rename test/{EventStore.Client.Streams.Tests/Read/read_stream_when_having_max_count_set_for_stream.cs => EventStore.Client.Tests/Streams/Read/ReadStreamWhenHavingMaxCountSetForStreamTests.cs} (93%) rename test/{EventStore.Client.Streams.Tests/Delete/soft_deleted_stream.cs => EventStore.Client.Tests/Streams/SoftDeleteTests.cs} (94%) rename test/{EventStore.Client.Streams.Tests/Metadata/stream_metadata.cs => EventStore.Client.Tests/Streams/StreamMetadataTests.cs} (92%) rename test/{EventStore.Client.Streams.Tests/Subscriptions/subscribe_to_stream.cs => EventStore.Client.Tests/Streams/SubscribeToStreamTests.cs} (80%) rename test/{EventStore.Client.Streams.Tests/Subscriptions => EventStore.Client.Tests/Streams}/SubscriptionFilter.cs (92%) delete mode 100644 test/EventStore.Client.UserManagement.Tests/AssemblyInfo.cs delete mode 100644 test/EventStore.Client.UserManagement.Tests/EventStore.Client.UserManagement.Tests.csproj delete mode 100644 test/EventStore.Client.UserManagement.Tests/EventStore.Client.UserManagement.Tests.csproj.DotSettings delete mode 100644 test/EventStore.Client.UserManagement.Tests/InvalidCredentialsTestCases.cs delete mode 100644 test/EventStore.Client.UserManagement.Tests/UserCredentialsTests.cs delete mode 100644 test/EventStore.Client.UserManagement.Tests/changing_user_password.cs delete mode 100644 test/EventStore.Client.UserManagement.Tests/creating_a_user.cs delete mode 100644 test/EventStore.Client.UserManagement.Tests/deleting_a_user.cs delete mode 100644 test/EventStore.Client.UserManagement.Tests/disabling_a_user.cs delete mode 100644 test/EventStore.Client.UserManagement.Tests/enabling_a_user.cs delete mode 100644 test/EventStore.Client.UserManagement.Tests/getting_current_user.cs delete mode 100644 test/EventStore.Client.UserManagement.Tests/listing_users.cs delete mode 100644 test/EventStore.Client.UserManagement.Tests/resetting_user_password.cs diff --git a/.github/workflows/base.yml b/.github/workflows/base.yml index 20cfe91d7..e121c28bf 100644 --- a/.github/workflows/base.yml +++ b/.github/workflows/base.yml @@ -15,9 +15,9 @@ on: required: false type: string default: eventstore-ce/eventstoredb-ce - test: - required: true - type: string +# test: +# required: true +# type: string jobs: test: @@ -29,7 +29,8 @@ jobs: os: [ ubuntu-latest ] configuration: [ release ] runs-on: ${{ matrix.os }} - name: ${{ inputs.test }} (${{ matrix.os }}, ${{ matrix.framework }}) +# name: ${{ inputs.test }} (${{ matrix.os }}, ${{ matrix.framework }}) + name: (${{ matrix.os }}, ${{ matrix.framework }}) env: CLOUDSMITH_CICD_USER: ${{ secrets.CLOUDSMITH_CICD_USER }} steps: @@ -63,4 +64,11 @@ jobs: dotnet test --configuration ${{ matrix.configuration }} --blame \ --logger:"GitHubActions;report-warnings=false" --logger:"console;verbosity=normal" \ --framework ${{ matrix.framework }} \ - test/EventStore.Client.${{ inputs.test }}.Tests + test/EventStore.Client.Tests + +# run: | +# sudo ./gencert.sh +# dotnet test --configuration ${{ matrix.configuration }} --blame \ +# --logger:"GitHubActions;report-warnings=false" --logger:"console;verbosity=normal" \ +# --framework ${{ matrix.framework }} \ +# test/EventStore.Client.${{ inputs.test }}.Tests diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4b8e1f9b1..c1ab57e76 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,25 +15,25 @@ jobs: fail-fast: false matrix: docker-tag: [ ci, lts, previous-lts ] - test: [ Streams, PersistentSubscriptions, Operations, UserManagement, ProjectionManagement ] +# test: [ Streams, PersistentSubscriptions, Operations, UserManagement, ProjectionManagement ] name: Test CE (${{ matrix.docker-tag }}) with: docker-tag: ${{ matrix.docker-tag }} docker-image: eventstore-ce/eventstoredb-ce - test: ${{ matrix.test }} - ee: - uses: ./.github/workflows/base.yml - if: ${{ github.repository_owner == 'EventStore' }} - strategy: - fail-fast: false - matrix: - docker-tag: [ 24.2.0-jammy ] - test: [ Plugins ] - name: Test EE (${{ matrix.docker-tag }}) - with: - docker-tag: ${{ matrix.docker-tag }} - docker-image: eventstore-ee/eventstoredb-commercial - test: ${{ matrix.test }} - secrets: - CLOUDSMITH_CICD_USER: ${{ secrets.CLOUDSMITH_CICD_USER }} - CLOUDSMITH_CICD_TOKEN: ${{ secrets.CLOUDSMITH_CICD_TOKEN }} +# test: ${{ matrix.test }} +# ee: +# uses: ./.github/workflows/base.yml +# if: ${{ github.repository_owner == 'EventStore' }} +# strategy: +# fail-fast: false +# matrix: +# docker-tag: [ 24.2.0-jammy ] +# test: [ Plugins ] +# name: Test EE (${{ matrix.docker-tag }}) +# with: +# docker-tag: ${{ matrix.docker-tag }} +# docker-image: eventstore-ee/eventstoredb-commercial +# test: ${{ matrix.test }} +# secrets: +# CLOUDSMITH_CICD_USER: ${{ secrets.CLOUDSMITH_CICD_USER }} +# CLOUDSMITH_CICD_TOKEN: ${{ secrets.CLOUDSMITH_CICD_TOKEN }} diff --git a/.github/workflows/dispatch-ce.yml b/.github/workflows/dispatch-ce.yml index 722baaea7..31e2d8966 100644 --- a/.github/workflows/dispatch-ce.yml +++ b/.github/workflows/dispatch-ce.yml @@ -15,10 +15,10 @@ on: jobs: test: uses: ./.github/workflows/base.yml - strategy: - fail-fast: false - matrix: - test: [ Streams, PersistentSubscriptions, Operations, UserManagement, ProjectionManagement ] +# strategy: +# fail-fast: false +# matrix: +# test: [ Streams, PersistentSubscriptions, Operations, UserManagement, ProjectionManagement ] name: Test CE (${{ inputs.docker-tag }}) with: docker-tag: ${{ inputs.docker-tag }} diff --git a/.github/workflows/dispatch-ee.yml b/.github/workflows/dispatch-ee.yml index 13f0b3398..75bde07de 100644 --- a/.github/workflows/dispatch-ee.yml +++ b/.github/workflows/dispatch-ee.yml @@ -1,29 +1,29 @@ -name: Dispatch EE - -on: - workflow_dispatch: - inputs: - docker-tag: - description: "Docker tag" - required: true - type: string - docker-image: - description: "Docker image" - required: true - type: string - -jobs: - test: - uses: ./.github/workflows/base.yml - strategy: - fail-fast: false - matrix: - test: [ Plugins ] - name: Test EE (${{ inputs.docker-tag }}) - with: - docker-tag: ${{ inputs.docker-tag }} - docker-image: ${{ inputs.docker-image }} - test: ${{ matrix.test }} - secrets: - CLOUDSMITH_CICD_USER: ${{ secrets.CLOUDSMITH_CICD_USER }} - CLOUDSMITH_CICD_TOKEN: ${{ secrets.CLOUDSMITH_CICD_TOKEN }} +#name: Dispatch EE +# +#on: +# workflow_dispatch: +# inputs: +# docker-tag: +# description: "Docker tag" +# required: true +# type: string +# docker-image: +# description: "Docker image" +# required: true +# type: string +# +#jobs: +# test: +# uses: ./.github/workflows/base.yml +## strategy: +## fail-fast: false +## matrix: +## test: [ Plugins ] +# name: Test EE (${{ inputs.docker-tag }}) +# with: +# docker-tag: ${{ inputs.docker-tag }} +# docker-image: ${{ inputs.docker-image }} +# test: ${{ matrix.test }} +# secrets: +# CLOUDSMITH_CICD_USER: ${{ secrets.CLOUDSMITH_CICD_USER }} +# CLOUDSMITH_CICD_TOKEN: ${{ secrets.CLOUDSMITH_CICD_TOKEN }} diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 42535a11d..ebe348e3e 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,193 +1,193 @@ -name: Publish - -on: - pull_request: - push: - branches: - - master - tags: - - v* - -jobs: - vulnerability-scan: - timeout-minutes: 10 - strategy: - fail-fast: false - matrix: - framework: [ net8.0 ] - os: [ ubuntu-latest, windows-latest ] - runs-on: ${{ matrix.os }} - name: scan-vulnerabilities/${{ matrix.os }}/${{ matrix.framework }} - steps: - - name: Checkout - uses: actions/checkout@v3 - - name: Install dotnet SDKs - uses: actions/setup-dotnet@v3 - with: - dotnet-version: | - 8.0.x - - name: Scan for Vulnerabilities - shell: bash - run: | - dotnet nuget list source - dotnet restore - dotnet list package --vulnerable --include-transitive --framework ${{ matrix.framework }} | tee vulnerabilities.txt - ! cat vulnerabilities.txt | grep -q "has the following vulnerable packages" - - build-samples: - timeout-minutes: 5 - name: build-samples/${{ matrix.framework }} - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - framework: [ net8.0 ] - services: - esdb: - image: docker.eventstore.com/eventstore-ce/eventstoredb-ce:lts - env: - EVENTSTORE_INSECURE: true - EVENTSTORE_MEM_DB: false - EVENTSTORE_RUN_PROJECTIONS: all - EVENTSTORE_START_STANDARD_PROJECTIONS: true - ports: - - 2113:2113 - options: --health-cmd "exit 0" - steps: - - name: Checkout - uses: actions/checkout@v3 - - name: Install dotnet SDKs - uses: actions/setup-dotnet@v3 - with: - dotnet-version: | - 8.0.x - - name: Compile - shell: bash - run: | - dotnet build samples - - name: Run - shell: bash - run: | - find samples/ -type f -iname "*.csproj" -print0 | xargs -0L1 dotnet run --framework ${{ matrix.framework }} --project - - generate-certificates: - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - name: Generate certificates - run: | - mkdir -p certs - docker run --rm --user root --volume "$PWD/certs:/tmp" docker.eventstore.com/eventstore-utils/es-gencert-cli:latest create-ca -out /tmp/ca - docker run --rm --user root --volume "$PWD/certs:/tmp" docker.eventstore.com/eventstore-utils/es-gencert-cli:latest create-node -ca-certificate /tmp/ca/ca.crt -ca-key /tmp/ca/ca.key -out /tmp/node -ip-addresses 127.0.0.1 -dns-names localhost - docker run --rm --user root --volume "$PWD/certs:/tmp" docker.eventstore.com/eventstore-utils/es-gencert-cli:latest create-user -username admin -ca-certificate /tmp/ca/ca.crt -ca-key /tmp/ca/ca.key -out /tmp/user-admin - docker run --rm --user root --volume "$PWD/certs:/tmp" docker.eventstore.com/eventstore-utils/es-gencert-cli:latest create-user -username invalid -ca-certificate /tmp/ca/ca.crt -ca-key /tmp/ca/ca.key -out /tmp/user-invalid - - name: Set permissions on certificates - run: | - sudo chown -R $USER:$USER certs - sudo chmod -R 755 certs - - name: Upload certificates - uses: actions/upload-artifact@v4 - with: - name: certs - path: certs - - test: - needs: generate-certificates - timeout-minutes: 20 - strategy: - fail-fast: false - matrix: - framework: [ net8.0 ] - os: [ ubuntu-latest, windows-latest ] - configuration: [ release ] - runs-on: ${{ matrix.os }} - name: test/EventStore.Client/${{ matrix.os }}/${{ matrix.framework }} - steps: - - name: Checkout - uses: actions/checkout@v3 - - shell: bash - run: | - git fetch --prune --unshallow - - name: Install dotnet SDKs - uses: actions/setup-dotnet@v3 - with: - dotnet-version: | - 8.0.x - - name: Compile - shell: bash - run: | - dotnet build --configuration ${{ matrix.configuration }} --framework ${{ matrix.framework }} src/EventStore.Client - - name: Download certificates - uses: actions/download-artifact@v4 - with: - name: certs - path: certs - - name: Run Tests (Linux) - if: runner.os == 'Linux' - shell: bash - run: | - dotnet test --configuration ${{ matrix.configuration }} --blame \ - --logger:"GitHubActions;report-warnings=false" --logger:"console;verbosity=normal" \ - --framework ${{ matrix.framework }} \ - test/EventStore.Client.Tests - - name: Run Tests (Windows) - if: runner.os == 'Windows' - shell: pwsh - run: | - dotnet test --configuration ${{ matrix.configuration }} --blame ` - --logger:"GitHubActions;report-warnings=false" --logger:"console;verbosity=normal" ` - --framework ${{ matrix.framework }} ` - test/EventStore.Client.Tests - - publish: - timeout-minutes: 5 - needs: [ vulnerability-scan, test, build-samples ] - runs-on: ubuntu-latest - name: publish - steps: - - name: Checkout - uses: actions/checkout@v3 - - name: Get Version - id: get_version - run: | - echo "branch=${GITHUB_REF:10}" >> $GITHUB_OUTPUT - dotnet nuget list source - dotnet tool restore - version=$(dotnet tool run minver -- --tag-prefix=v) - echo "version=${version}" >> $GITHUB_OUTPUT - - shell: bash - run: | - git fetch --prune --unshallow - - name: Install dotnet SDKs - uses: actions/setup-dotnet@v3 - with: - dotnet-version: | - 8.0.x - - name: Dotnet Pack - shell: bash - run: | - mkdir -p packages - dotnet pack /p:Version=${{ steps.get_version.outputs.version }} --configuration=Release \ - /p:PublishDir=./packages \ - /p:NoWarn=NU5105 \ - /p:RepositoryUrl=https://github.com/EventStore/EventStore-Client-Dotnet \ - /p:RepositoryType=git - - name: Publish Artifacts - uses: actions/upload-artifact@v4 - with: - path: packages - name: nuget-packages - - name: Dotnet Push to Github Packages - shell: bash - if: github.event_name == 'push' - run: | - dotnet tool restore - find . -name "*.nupkg" | xargs -n1 dotnet nuget push --api-key=${{ secrets.github_token }} --source https://nuget.pkg.github.com/EventStore/index.json --skip-duplicate - - name: Dotnet Push to Nuget.org - shell: bash - if: contains(steps.get_version.outputs.branch, 'v') - run: | - dotnet nuget list source - dotnet tool restore - find . -name "*.nupkg" | xargs -n1 dotnet nuget push --api-key=${{ secrets.nuget_key }} --source https://api.nuget.org/v3/index.json --skip-duplicate +#name: Publish +# +#on: +# pull_request: +# push: +# branches: +# - master +# tags: +# - v* +# +#jobs: +# vulnerability-scan: +# timeout-minutes: 10 +# strategy: +# fail-fast: false +# matrix: +# framework: [ net8.0 ] +# os: [ ubuntu-latest, windows-latest ] +# runs-on: ${{ matrix.os }} +# name: scan-vulnerabilities/${{ matrix.os }}/${{ matrix.framework }} +# steps: +# - name: Checkout +# uses: actions/checkout@v3 +# - name: Install dotnet SDKs +# uses: actions/setup-dotnet@v3 +# with: +# dotnet-version: | +# 8.0.x +# - name: Scan for Vulnerabilities +# shell: bash +# run: | +# dotnet nuget list source +# dotnet restore +# dotnet list package --vulnerable --include-transitive --framework ${{ matrix.framework }} | tee vulnerabilities.txt +# ! cat vulnerabilities.txt | grep -q "has the following vulnerable packages" +# +# build-samples: +# timeout-minutes: 5 +# name: build-samples/${{ matrix.framework }} +# runs-on: ubuntu-latest +# strategy: +# fail-fast: false +# matrix: +# framework: [ net8.0 ] +# services: +# esdb: +# image: docker.eventstore.com/eventstore-ce/eventstoredb-ce:lts +# env: +# EVENTSTORE_INSECURE: true +# EVENTSTORE_MEM_DB: false +# EVENTSTORE_RUN_PROJECTIONS: all +# EVENTSTORE_START_STANDARD_PROJECTIONS: true +# ports: +# - 2113:2113 +# options: --health-cmd "exit 0" +# steps: +# - name: Checkout +# uses: actions/checkout@v3 +# - name: Install dotnet SDKs +# uses: actions/setup-dotnet@v3 +# with: +# dotnet-version: | +# 8.0.x +# - name: Compile +# shell: bash +# run: | +# dotnet build samples +# - name: Run +# shell: bash +# run: | +# find samples/ -type f -iname "*.csproj" -print0 | xargs -0L1 dotnet run --framework ${{ matrix.framework }} --project +# +# generate-certificates: +# runs-on: ubuntu-latest +# steps: +# - name: Checkout code +# uses: actions/checkout@v4 +# - name: Generate certificates +# run: | +# mkdir -p certs +# docker run --rm --user root --volume "$PWD/certs:/tmp" docker.eventstore.com/eventstore-utils/es-gencert-cli:latest create-ca -out /tmp/ca +# docker run --rm --user root --volume "$PWD/certs:/tmp" docker.eventstore.com/eventstore-utils/es-gencert-cli:latest create-node -ca-certificate /tmp/ca/ca.crt -ca-key /tmp/ca/ca.key -out /tmp/node -ip-addresses 127.0.0.1 -dns-names localhost +# docker run --rm --user root --volume "$PWD/certs:/tmp" docker.eventstore.com/eventstore-utils/es-gencert-cli:latest create-user -username admin -ca-certificate /tmp/ca/ca.crt -ca-key /tmp/ca/ca.key -out /tmp/user-admin +# docker run --rm --user root --volume "$PWD/certs:/tmp" docker.eventstore.com/eventstore-utils/es-gencert-cli:latest create-user -username invalid -ca-certificate /tmp/ca/ca.crt -ca-key /tmp/ca/ca.key -out /tmp/user-invalid +# - name: Set permissions on certificates +# run: | +# sudo chown -R $USER:$USER certs +# sudo chmod -R 755 certs +# - name: Upload certificates +# uses: actions/upload-artifact@v4 +# with: +# name: certs +# path: certs +# +# test: +# needs: generate-certificates +# timeout-minutes: 20 +# strategy: +# fail-fast: false +# matrix: +# framework: [ net8.0 ] +# os: [ ubuntu-latest, windows-latest ] +# configuration: [ release ] +# runs-on: ${{ matrix.os }} +# name: test/EventStore.Client/${{ matrix.os }}/${{ matrix.framework }} +# steps: +# - name: Checkout +# uses: actions/checkout@v3 +# - shell: bash +# run: | +# git fetch --prune --unshallow +# - name: Install dotnet SDKs +# uses: actions/setup-dotnet@v3 +# with: +# dotnet-version: | +# 8.0.x +# - name: Compile +# shell: bash +# run: | +# dotnet build --configuration ${{ matrix.configuration }} --framework ${{ matrix.framework }} src/EventStore.Client +# - name: Download certificates +# uses: actions/download-artifact@v4 +# with: +# name: certs +# path: certs +# - name: Run Tests (Linux) +# if: runner.os == 'Linux' +# shell: bash +# run: | +# dotnet test --configuration ${{ matrix.configuration }} --blame \ +# --logger:"GitHubActions;report-warnings=false" --logger:"console;verbosity=normal" \ +# --framework ${{ matrix.framework }} \ +# test/EventStore.Client.Tests +# - name: Run Tests (Windows) +# if: runner.os == 'Windows' +# shell: pwsh +# run: | +# dotnet test --configuration ${{ matrix.configuration }} --blame ` +# --logger:"GitHubActions;report-warnings=false" --logger:"console;verbosity=normal" ` +# --framework ${{ matrix.framework }} ` +# test/EventStore.Client.Tests +# +# publish: +# timeout-minutes: 5 +# needs: [ vulnerability-scan, test, build-samples ] +# runs-on: ubuntu-latest +# name: publish +# steps: +# - name: Checkout +# uses: actions/checkout@v3 +# - name: Get Version +# id: get_version +# run: | +# echo "branch=${GITHUB_REF:10}" >> $GITHUB_OUTPUT +# dotnet nuget list source +# dotnet tool restore +# version=$(dotnet tool run minver -- --tag-prefix=v) +# echo "version=${version}" >> $GITHUB_OUTPUT +# - shell: bash +# run: | +# git fetch --prune --unshallow +# - name: Install dotnet SDKs +# uses: actions/setup-dotnet@v3 +# with: +# dotnet-version: | +# 8.0.x +# - name: Dotnet Pack +# shell: bash +# run: | +# mkdir -p packages +# dotnet pack /p:Version=${{ steps.get_version.outputs.version }} --configuration=Release \ +# /p:PublishDir=./packages \ +# /p:NoWarn=NU5105 \ +# /p:RepositoryUrl=https://github.com/EventStore/EventStore-Client-Dotnet \ +# /p:RepositoryType=git +# - name: Publish Artifacts +# uses: actions/upload-artifact@v4 +# with: +# path: packages +# name: nuget-packages +# - name: Dotnet Push to Github Packages +# shell: bash +# if: github.event_name == 'push' +# run: | +# dotnet tool restore +# find . -name "*.nupkg" | xargs -n1 dotnet nuget push --api-key=${{ secrets.github_token }} --source https://nuget.pkg.github.com/EventStore/index.json --skip-duplicate +# - name: Dotnet Push to Nuget.org +# shell: bash +# if: contains(steps.get_version.outputs.branch, 'v') +# run: | +# dotnet nuget list source +# dotnet tool restore +# find . -name "*.nupkg" | xargs -n1 dotnet nuget push --api-key=${{ secrets.nuget_key }} --source https://api.nuget.org/v3/index.json --skip-duplicate diff --git a/.github/workflows/pull-request-check.yml b/.github/workflows/pull-request-check.yml index 17d1a4561..9b327e375 100644 --- a/.github/workflows/pull-request-check.yml +++ b/.github/workflows/pull-request-check.yml @@ -1,23 +1,23 @@ -name: Pull Request check -on: - pull_request: - paths-ignore: - - "test/**" - - "generators/**" - - "samples/**" - - "**.md" - - "gencert.sh" - - ".github/**" - - ".gitignore" - - ".gitattributes" - - ".editorconfig" - types: [ opened, edited ] -jobs: - checkPullRequest: - name: Pull Request check - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v3 - - name: Check pull requests - uses: EventStore/Automations/pr-check@master +#name: Pull Request check +#on: +# pull_request: +# paths-ignore: +# - "test/**" +# - "generators/**" +# - "samples/**" +# - "**.md" +# - "gencert.sh" +# - ".github/**" +# - ".gitignore" +# - ".gitattributes" +# - ".editorconfig" +# types: [ opened, edited ] +#jobs: +# checkPullRequest: +# name: Pull Request check +# runs-on: ubuntu-latest +# steps: +# - name: Checkout +# uses: actions/checkout@v3 +# - name: Check pull requests +# uses: EventStore/Automations/pr-check@master diff --git a/Directory.Build.props b/Directory.Build.props index 52c0d73a5..a22d9632a 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,6 +1,6 @@ - net48;net8.0 + net48;net8.0;net9.0 true enable enable @@ -18,4 +18,4 @@ - \ No newline at end of file + diff --git a/EventStore.Client.sln b/EventStore.Client.sln index 373aa58a2..4b4791ec9 100644 --- a/EventStore.Client.sln +++ b/EventStore.Client.sln @@ -9,23 +9,11 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventStore.Client", "src\Ev EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{C51F2C69-45A9-4D0D-A708-4FC319D5D340}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventStore.Client.ProjectionManagement.Tests", "test\EventStore.Client.ProjectionManagement.Tests\EventStore.Client.ProjectionManagement.Tests.csproj", "{8F8548D6-694C-4BAE-9EF3-A020140E04C7}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventStore.Client.Operations.Tests", "test\EventStore.Client.Operations.Tests\EventStore.Client.Operations.Tests.csproj", "{4BA2E05E-6B45-47C3-9001-8B039244ECA9}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventStore.Client.Streams.Tests", "test\EventStore.Client.Streams.Tests\EventStore.Client.Streams.Tests.csproj", "{082C77F5-4FF5-41D4-A1F1-710F05956E1C}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventStore.Client.Tests", "test\EventStore.Client.Tests\EventStore.Client.Tests.csproj", "{FC829F1B-43AD-4C96-9002-23D04BBA3AF3}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventStore.Client.PersistentSubscriptions.Tests", "test\EventStore.Client.PersistentSubscriptions.Tests\EventStore.Client.PersistentSubscriptions.Tests.csproj", "{6CEB731F-72E1-461F-A6B3-54DBF3FD786C}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventStore.Client.UserManagement.Tests", "test\EventStore.Client.UserManagement.Tests\EventStore.Client.UserManagement.Tests.csproj", "{22634CEE-4F7B-4679-A48D-38A2A8580ECA}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventStore.Client.Tests.Common", "test\EventStore.Client.Tests.Common\EventStore.Client.Tests.Common.csproj", "{E326832D-DE52-4DE4-9E54-C800908B75F3}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventStore.Client.Extensions.OpenTelemetry", "src\EventStore.Client.Extensions.OpenTelemetry\EventStore.Client.Extensions.OpenTelemetry.csproj", "{3723933C-585A-49BE-98E8-52D3FAD904CE}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventStore.Client.Plugins.Tests", "test\EventStore.Client.Plugins.Tests\EventStore.Client.Plugins.Tests.csproj", "{7D929D45-F1D9-462B-BE49-84BEC11D5039}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventStore.Client.Extensions.OpenTelemetry", "src\EventStore.Client.Extensions.OpenTelemetry\EventStore.Client.Extensions.OpenTelemetry.csproj", "{F6A7B391-36F1-4838-AD08-E0EE0F2FE57E}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -40,53 +28,23 @@ Global {8853D875-4A8E-450B-A1BE-9CEF8BEDABC3}.Debug|x64.Build.0 = Debug|Any CPU {8853D875-4A8E-450B-A1BE-9CEF8BEDABC3}.Release|x64.ActiveCfg = Release|Any CPU {8853D875-4A8E-450B-A1BE-9CEF8BEDABC3}.Release|x64.Build.0 = Release|Any CPU - {8F8548D6-694C-4BAE-9EF3-A020140E04C7}.Debug|x64.ActiveCfg = Debug|Any CPU - {8F8548D6-694C-4BAE-9EF3-A020140E04C7}.Debug|x64.Build.0 = Debug|Any CPU - {8F8548D6-694C-4BAE-9EF3-A020140E04C7}.Release|x64.ActiveCfg = Release|Any CPU - {8F8548D6-694C-4BAE-9EF3-A020140E04C7}.Release|x64.Build.0 = Release|Any CPU - {4BA2E05E-6B45-47C3-9001-8B039244ECA9}.Debug|x64.ActiveCfg = Debug|Any CPU - {4BA2E05E-6B45-47C3-9001-8B039244ECA9}.Debug|x64.Build.0 = Debug|Any CPU - {4BA2E05E-6B45-47C3-9001-8B039244ECA9}.Release|x64.ActiveCfg = Release|Any CPU - {4BA2E05E-6B45-47C3-9001-8B039244ECA9}.Release|x64.Build.0 = Release|Any CPU - {082C77F5-4FF5-41D4-A1F1-710F05956E1C}.Debug|x64.ActiveCfg = Debug|Any CPU - {082C77F5-4FF5-41D4-A1F1-710F05956E1C}.Debug|x64.Build.0 = Debug|Any CPU - {082C77F5-4FF5-41D4-A1F1-710F05956E1C}.Release|x64.ActiveCfg = Release|Any CPU - {082C77F5-4FF5-41D4-A1F1-710F05956E1C}.Release|x64.Build.0 = Release|Any CPU {FC829F1B-43AD-4C96-9002-23D04BBA3AF3}.Debug|x64.ActiveCfg = Debug|Any CPU {FC829F1B-43AD-4C96-9002-23D04BBA3AF3}.Debug|x64.Build.0 = Debug|Any CPU {FC829F1B-43AD-4C96-9002-23D04BBA3AF3}.Release|x64.ActiveCfg = Release|Any CPU {FC829F1B-43AD-4C96-9002-23D04BBA3AF3}.Release|x64.Build.0 = Release|Any CPU - {6CEB731F-72E1-461F-A6B3-54DBF3FD786C}.Debug|x64.ActiveCfg = Debug|Any CPU - {6CEB731F-72E1-461F-A6B3-54DBF3FD786C}.Debug|x64.Build.0 = Debug|Any CPU - {6CEB731F-72E1-461F-A6B3-54DBF3FD786C}.Release|x64.ActiveCfg = Release|Any CPU - {6CEB731F-72E1-461F-A6B3-54DBF3FD786C}.Release|x64.Build.0 = Release|Any CPU - {22634CEE-4F7B-4679-A48D-38A2A8580ECA}.Debug|x64.ActiveCfg = Debug|Any CPU - {22634CEE-4F7B-4679-A48D-38A2A8580ECA}.Debug|x64.Build.0 = Debug|Any CPU - {22634CEE-4F7B-4679-A48D-38A2A8580ECA}.Release|x64.ActiveCfg = Release|Any CPU - {22634CEE-4F7B-4679-A48D-38A2A8580ECA}.Release|x64.Build.0 = Release|Any CPU {E326832D-DE52-4DE4-9E54-C800908B75F3}.Debug|x64.ActiveCfg = Debug|Any CPU {E326832D-DE52-4DE4-9E54-C800908B75F3}.Debug|x64.Build.0 = Debug|Any CPU {E326832D-DE52-4DE4-9E54-C800908B75F3}.Release|x64.ActiveCfg = Release|Any CPU {E326832D-DE52-4DE4-9E54-C800908B75F3}.Release|x64.Build.0 = Release|Any CPU - {3723933C-585A-49BE-98E8-52D3FAD904CE}.Debug|x64.ActiveCfg = Debug|Any CPU - {3723933C-585A-49BE-98E8-52D3FAD904CE}.Debug|x64.Build.0 = Debug|Any CPU - {3723933C-585A-49BE-98E8-52D3FAD904CE}.Release|x64.ActiveCfg = Release|Any CPU - {3723933C-585A-49BE-98E8-52D3FAD904CE}.Release|x64.Build.0 = Release|Any CPU - {7D929D45-F1D9-462B-BE49-84BEC11D5039}.Debug|x64.ActiveCfg = Debug|Any CPU - {7D929D45-F1D9-462B-BE49-84BEC11D5039}.Debug|x64.Build.0 = Debug|Any CPU - {7D929D45-F1D9-462B-BE49-84BEC11D5039}.Release|x64.ActiveCfg = Release|Any CPU - {7D929D45-F1D9-462B-BE49-84BEC11D5039}.Release|x64.Build.0 = Release|Any CPU + {F6A7B391-36F1-4838-AD08-E0EE0F2FE57E}.Debug|x64.ActiveCfg = Debug|Any CPU + {F6A7B391-36F1-4838-AD08-E0EE0F2FE57E}.Debug|x64.Build.0 = Debug|Any CPU + {F6A7B391-36F1-4838-AD08-E0EE0F2FE57E}.Release|x64.ActiveCfg = Release|Any CPU + {F6A7B391-36F1-4838-AD08-E0EE0F2FE57E}.Release|x64.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {8853D875-4A8E-450B-A1BE-9CEF8BEDABC3} = {EA59C1CB-16DA-4F68-AF8A-642A969B4CF8} - {8F8548D6-694C-4BAE-9EF3-A020140E04C7} = {C51F2C69-45A9-4D0D-A708-4FC319D5D340} - {4BA2E05E-6B45-47C3-9001-8B039244ECA9} = {C51F2C69-45A9-4D0D-A708-4FC319D5D340} - {082C77F5-4FF5-41D4-A1F1-710F05956E1C} = {C51F2C69-45A9-4D0D-A708-4FC319D5D340} {FC829F1B-43AD-4C96-9002-23D04BBA3AF3} = {C51F2C69-45A9-4D0D-A708-4FC319D5D340} - {6CEB731F-72E1-461F-A6B3-54DBF3FD786C} = {C51F2C69-45A9-4D0D-A708-4FC319D5D340} - {22634CEE-4F7B-4679-A48D-38A2A8580ECA} = {C51F2C69-45A9-4D0D-A708-4FC319D5D340} {E326832D-DE52-4DE4-9E54-C800908B75F3} = {C51F2C69-45A9-4D0D-A708-4FC319D5D340} - {3723933C-585A-49BE-98E8-52D3FAD904CE} = {EA59C1CB-16DA-4F68-AF8A-642A969B4CF8} - {7D929D45-F1D9-462B-BE49-84BEC11D5039} = {C51F2C69-45A9-4D0D-A708-4FC319D5D340} + {F6A7B391-36F1-4838-AD08-E0EE0F2FE57E} = {EA59C1CB-16DA-4F68-AF8A-642A969B4CF8} EndGlobalSection EndGlobal diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 15441ca5d..9f5159807 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -4,7 +4,7 @@ true EventStore.Client - Kurrent.Client + EventStore.Client @@ -50,4 +50,4 @@ - \ No newline at end of file + diff --git a/src/EventStore.Client/Core/Certificates/X509Certificates.cs b/src/EventStore.Client/Core/Certificates/X509Certificates.cs index 78efda04e..3fe1006f5 100644 --- a/src/EventStore.Client/Core/Certificates/X509Certificates.cs +++ b/src/EventStore.Client/Core/Certificates/X509Certificates.cs @@ -13,29 +13,36 @@ namespace EventStore.Client; static class X509Certificates { - // TODO SS: Use .NET 8 X509Certificate2.CreateFromPemFile(certPemFilePath, keyPemFilePath) once the Windows32Exception issue is resolved - public static X509Certificate2 CreateFromPemFile(string certPemFilePath, string keyPemFilePath) { - try { - using var publicCert = new X509Certificate2(certPemFilePath); - using var privateKey = RSA.Create().ImportPrivateKeyFromFile(keyPemFilePath); - using var certificate = publicCert.CopyWithPrivateKey(privateKey); - - return new(certificate.Export(X509ContentType.Pfx)); - } - catch (Exception ex) { - throw new CryptographicException($"Failed to load private key: {ex.Message}"); - } - - // Notes: - // using X509Certificate2.CreateFromPemFile(certPemFilePath, keyPemFilePath) would be the ideal choice here, - // but it's currently causing a Win32Exception specifically on Windows. Alternative implementation is used until the issue is resolved. - // - // Error: The SSL connection could not be established, see inner exception. AuthenticationException: Authentication failed because the platform - // does not support ephemeral keys. Win32Exception: No credentials are available in the security package - // - // public static X509Certificate2 CreateFromPemFile(string certPemFilePath, string keyPemFilePath) => - // X509Certificate2.CreateFromPemFile(certPemFilePath, keyPemFilePath); - } + // TODO SS: Use .NET 8 X509Certificate2.CreateFromPemFile(certPemFilePath, keyPemFilePath) once the Windows32Exception issue is resolved + public static X509Certificate2 CreateFromPemFile(string certPemFilePath, string keyPemFilePath) { + try { +#if NET9_0_OR_GREATER + using var publicCert = X509CertificateLoader.LoadCertificateFromFile(certPemFilePath); +#else + using var publicCert = new X509Certificate2(certPemFilePath); +#endif + using var privateKey = RSA.Create().ImportPrivateKeyFromFile(keyPemFilePath); + using var certificate = publicCert.CopyWithPrivateKey(privateKey); + +#if NET48 + return new(certificate.Export(X509ContentType.Pfx)); +#else + return X509Certificate2.CreateFromPemFile(certPemFilePath, keyPemFilePath); +#endif + } catch (Exception ex) { + throw new CryptographicException($"Failed to load private key: {ex.Message}"); + } + + // Notes: + // using X509Certificate2.CreateFromPemFile(certPemFilePath, keyPemFilePath) would be the ideal choice here, + // but it's currently causing a Win32Exception specifically on Windows. Alternative implementation is used until the issue is resolved. + // + // Error: The SSL connection could not be established, see inner exception. AuthenticationException: Authentication failed because the platform + // does not support ephemeral keys. Win32Exception: No credentials are available in the security package + // + // public static X509Certificate2 CreateFromPemFile(string certPemFilePath, string keyPemFilePath) => + // X509Certificate2.CreateFromPemFile(certPemFilePath, keyPemFilePath); + } } public static class RsaExtensions { @@ -59,7 +66,7 @@ public static RSA ImportPrivateKeyFromFile(this RSA rsa, string privateKeyPath) public static RSA ImportPrivateKeyFromFile(this RSA rsa, string privateKeyPath) { var (content, label) = LoadPemKeyFile(privateKeyPath); - var privateKey = string.Join(string.Empty, content[1..^1]); + var privateKey = string.Join(string.Empty, content[1..^1]); var privateKeyBytes = Convert.FromBase64String(privateKey); if (label == RsaPemLabels.Pkcs8PrivateKey) @@ -104,4 +111,4 @@ public static string ParseKeyLabel(string pemFileHeader) { return label; } -} \ No newline at end of file +} diff --git a/src/EventStore.Client/Core/EventStoreClientSettings.ConnectionString.cs b/src/EventStore.Client/Core/EventStoreClientSettings.ConnectionString.cs index 48eb84956..2aadebf64 100644 --- a/src/EventStore.Client/Core/EventStoreClientSettings.ConnectionString.cs +++ b/src/EventStore.Client/Core/EventStoreClientSettings.ConnectionString.cs @@ -41,7 +41,7 @@ private static class ConnectionStringParser { private const string ThrowOnAppendFailure = nameof(ThrowOnAppendFailure); private const string KeepAliveInterval = nameof(KeepAliveInterval); private const string KeepAliveTimeout = nameof(KeepAliveTimeout); - private const string UserCertFile = nameof(UserCertFile); + private const string UserCertFile = nameof(UserCertFile); private const string UserKeyFile = nameof(UserKeyFile); private const string UriSchemeDiscover = "esdb+discover"; @@ -64,8 +64,8 @@ private static class ConnectionStringParser { { ThrowOnAppendFailure, typeof(bool) }, { KeepAliveInterval, typeof(int) }, { KeepAliveTimeout, typeof(int) }, - { UserCertFile, typeof(string)}, - { UserKeyFile, typeof(string)}, + { UserCertFile, typeof(string) }, + { UserKeyFile, typeof(string) }, }; public static EventStoreClientSettings Parse(string connectionString) { @@ -77,10 +77,10 @@ public static EventStoreClientSettings Parse(string connectionString) { var scheme = ParseScheme(connectionString.Substring(0, schemeIndex)); currentIndex = schemeIndex + SchemeSeparator.Length; - var userInfoIndex = connectionString.IndexOf(UserInfoSeparator, currentIndex, StringComparison.Ordinal); - (string user, string pass)? userInfo = null; + var userInfoIndex = connectionString.IndexOf(UserInfoSeparator, currentIndex, StringComparison.Ordinal); + (string user, string pass)? userInfo = null; if (userInfoIndex != -1) { - userInfo = ParseUserInfo(connectionString.Substring(currentIndex, userInfoIndex - currentIndex)); + userInfo = ParseUserInfo(connectionString.Substring(currentIndex, userInfoIndex - currentIndex)); currentIndex = userInfoIndex + UserInfoSeparator.Length; } @@ -93,7 +93,7 @@ public static EventStoreClientSettings Parse(string connectionString) { if (questionMarkIndex == -1) questionMarkIndex = int.MaxValue; var hostSeparatorIndex = Math.Min(Math.Min(slashIndex, questionMarkIndex), endIndex); - var hosts = ParseHosts(connectionString.Substring(currentIndex, hostSeparatorIndex - currentIndex)); + var hosts = ParseHosts(connectionString.Substring(currentIndex, hostSeparatorIndex - currentIndex)); currentIndex = hostSeparatorIndex; string path = ""; @@ -163,11 +163,11 @@ private static EventStoreClientSettings CreateSettings( if (typedOptions.TryGetValue(NodePreference, out object? nodePreference)) { settings.ConnectivitySettings.NodePreference = ((string)nodePreference).ToLowerInvariant() switch { - "leader" => EventStore.Client.NodePreference.Leader, - "follower" => EventStore.Client.NodePreference.Follower, - "random" => EventStore.Client.NodePreference.Random, + "leader" => EventStore.Client.NodePreference.Leader, + "follower" => EventStore.Client.NodePreference.Follower, + "random" => EventStore.Client.NodePreference.Random, "readonlyreplica" => EventStore.Client.NodePreference.ReadOnlyReplica, - _ => throw new InvalidSettingException($"Invalid NodePreference: {nodePreference}") + _ => throw new InvalidSettingException($"Invalid NodePreference: {nodePreference}") }; } @@ -184,17 +184,17 @@ private static EventStoreClientSettings CreateSettings( if (typedOptions.TryGetValue(KeepAliveInterval, out var keepAliveIntervalMs)) { settings.ConnectivitySettings.KeepAliveInterval = keepAliveIntervalMs switch { - -1 => Timeout_.InfiniteTimeSpan, + -1 => Timeout_.InfiniteTimeSpan, int value and >= 0 => TimeSpan.FromMilliseconds(value), - _ => throw new InvalidSettingException($"Invalid KeepAliveInterval: {keepAliveIntervalMs}") + _ => throw new InvalidSettingException($"Invalid KeepAliveInterval: {keepAliveIntervalMs}") }; } if (typedOptions.TryGetValue(KeepAliveTimeout, out var keepAliveTimeoutMs)) { settings.ConnectivitySettings.KeepAliveTimeout = keepAliveTimeoutMs switch { - -1 => Timeout_.InfiniteTimeSpan, + -1 => Timeout_.InfiniteTimeSpan, int value and >= 0 => TimeSpan.FromMilliseconds(value), - _ => throw new InvalidSettingException($"Invalid KeepAliveTimeout: {keepAliveTimeoutMs}") + _ => throw new InvalidSettingException($"Invalid KeepAliveTimeout: {keepAliveTimeoutMs}") }; } @@ -221,7 +221,11 @@ private static EventStoreClientSettings CreateSettings( } try { +#if NET9_0_OR_GREATER + settings.ConnectivitySettings.TlsCaFile = X509CertificateLoader.LoadCertificateFromFile(tlsCaFilePath); +#else settings.ConnectivitySettings.TlsCaFile = new X509Certificate2(tlsCaFilePath); +#endif } catch (CryptographicException) { throw new InvalidClientCertificateException("Failed to load certificate. Invalid file format."); } @@ -239,9 +243,9 @@ HttpMessageHandler CreateDefaultHandler() { return settings.CreateHttpMessageHandler.Invoke(); var handler = new WinHttpHandler { - TcpKeepAliveEnabled = true, - TcpKeepAliveTime = settings.ConnectivitySettings.KeepAliveTimeout, - TcpKeepAliveInterval = settings.ConnectivitySettings.KeepAliveInterval, + TcpKeepAliveEnabled = true, + TcpKeepAliveTime = settings.ConnectivitySettings.KeepAliveTimeout, + TcpKeepAliveInterval = settings.ConnectivitySettings.KeepAliveInterval, EnableMultipleHttp2Connections = true }; @@ -285,7 +289,7 @@ HttpMessageHandler CreateDefaultHandler() { true when settings.ConnectivitySettings.TlsCaFile is not null => (sender, certificate, chain, errors) => { if (certificate is not X509Certificate2 peerCertificate || chain is null) return false; - chain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust; + chain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust; chain.ChainPolicy.CustomTrustStore.Add(settings.ConnectivitySettings.TlsCaFile); return chain.Build(peerCertificate); }, diff --git a/test/Directory.Build.props b/test/Directory.Build.props index 0d2e0dec9..8c770cca2 100644 --- a/test/Directory.Build.props +++ b/test/Directory.Build.props @@ -27,6 +27,7 @@ + @@ -40,4 +41,4 @@ - \ No newline at end of file + diff --git a/test/EventStore.Client.Operations.Tests/AssemblyInfo.cs b/test/EventStore.Client.Operations.Tests/AssemblyInfo.cs deleted file mode 100644 index b0b47aa73..000000000 --- a/test/EventStore.Client.Operations.Tests/AssemblyInfo.cs +++ /dev/null @@ -1 +0,0 @@ -[assembly: CollectionBehavior(DisableTestParallelization = true)] \ No newline at end of file diff --git a/test/EventStore.Client.Operations.Tests/EventStore.Client.Operations.Tests.csproj b/test/EventStore.Client.Operations.Tests/EventStore.Client.Operations.Tests.csproj deleted file mode 100644 index f80438c6b..000000000 --- a/test/EventStore.Client.Operations.Tests/EventStore.Client.Operations.Tests.csproj +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/test/EventStore.Client.Operations.Tests/ResignNodeTests.cs b/test/EventStore.Client.Operations.Tests/ResignNodeTests.cs deleted file mode 100644 index 6012ae943..000000000 --- a/test/EventStore.Client.Operations.Tests/ResignNodeTests.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace EventStore.Client.Operations.Tests; - -public class ResignNodeTests : IClassFixture { - public ResignNodeTests(ITestOutputHelper output, InsecureClientTestFixture fixture) => - Fixture = fixture.With(x => x.CaptureTestRun(output)); - - InsecureClientTestFixture Fixture { get; } - - [Fact] - public async Task resign_node_does_not_throw() => - await Fixture.Operations - .ResignNodeAsync(userCredentials: TestCredentials.Root) - .ShouldNotThrowAsync(); - - [Fact] - public async Task resign_node_without_credentials_throws() => - await Fixture.Operations - .ResignNodeAsync() - .ShouldThrowAsync(); -} \ No newline at end of file diff --git a/test/EventStore.Client.Operations.Tests/RestartPersistentSubscriptionsTests.cs b/test/EventStore.Client.Operations.Tests/RestartPersistentSubscriptionsTests.cs deleted file mode 100644 index 1d04d1ca4..000000000 --- a/test/EventStore.Client.Operations.Tests/RestartPersistentSubscriptionsTests.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace EventStore.Client.Operations.Tests; - -public class RestartPersistentSubscriptionsTests : IClassFixture { - public RestartPersistentSubscriptionsTests(ITestOutputHelper output, InsecureClientTestFixture fixture) => - Fixture = fixture.With(x => x.CaptureTestRun(output)); - - InsecureClientTestFixture Fixture { get; } - - [Fact] - public async Task restart_persistent_subscriptions_does_not_throw() => - await Fixture.Operations - .RestartPersistentSubscriptions(userCredentials: TestCredentials.Root) - .ShouldNotThrowAsync(); - - [Fact] - public async Task restart_persistent_subscriptions_without_credentials_throws() => - await Fixture.Operations - .RestartPersistentSubscriptions() - .ShouldThrowAsync(); -} \ No newline at end of file diff --git a/test/EventStore.Client.Operations.Tests/ShutdownNodeAuthenticationTests.cs b/test/EventStore.Client.Operations.Tests/ShutdownNodeAuthenticationTests.cs deleted file mode 100644 index 4219f616d..000000000 --- a/test/EventStore.Client.Operations.Tests/ShutdownNodeAuthenticationTests.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace EventStore.Client.Operations.Tests; - -public class ShutdownNodeAuthenticationTests : IClassFixture { - public ShutdownNodeAuthenticationTests(ITestOutputHelper output, InsecureClientTestFixture fixture) => - Fixture = fixture.With(x => x.CaptureTestRun(output)); - - InsecureClientTestFixture Fixture { get; } - - [Fact] - public async Task shutdown_without_credentials_throws() => - await Fixture.Operations.ShutdownAsync().ShouldThrowAsync(); -} \ No newline at end of file diff --git a/test/EventStore.Client.Operations.Tests/ShutdownNodeTests.cs b/test/EventStore.Client.Operations.Tests/ShutdownNodeTests.cs deleted file mode 100644 index 4c73f52fa..000000000 --- a/test/EventStore.Client.Operations.Tests/ShutdownNodeTests.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace EventStore.Client.Operations.Tests; - -public class ShutdownNodeTests : IClassFixture { - public ShutdownNodeTests(ITestOutputHelper output, InsecureClientTestFixture fixture) => - Fixture = fixture.With(x => x.CaptureTestRun(output)); - - InsecureClientTestFixture Fixture { get; } - - [Fact] - public async Task shutdown_does_not_throw() => - await Fixture.Operations.ShutdownAsync(userCredentials: TestCredentials.Root).ShouldNotThrowAsync(); -} \ No newline at end of file diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/AssemblyInfo.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/AssemblyInfo.cs deleted file mode 100644 index b0b47aa73..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/AssemblyInfo.cs +++ /dev/null @@ -1 +0,0 @@ -[assembly: CollectionBehavior(DisableTestParallelization = true)] \ No newline at end of file diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/Bugs/Issue_1125.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/Bugs/Issue_1125.cs deleted file mode 100644 index c94429ca3..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/Bugs/Issue_1125.cs +++ /dev/null @@ -1,84 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.Bugs; - -public class Issue_1125 : IClassFixture { - readonly Fixture _fixture; - - public Issue_1125(Fixture fixture) => _fixture = fixture; - - public static IEnumerable TestCases() => Enumerable.Range(0, 50).Select(i => new object[] { i }); - - [Theory] - [MemberData(nameof(TestCases))] - public async Task persistent_subscription_delivers_all_events(int iteration) { - const int eventCount = 250; - const int totalEvents = eventCount * 2; - - var hitCount = 0; - - var userCredentials = new UserCredentials("admin", "changeit"); - - var streamName = $"stream_{iteration}"; - var subscriptionName = $"subscription_{iteration}"; - - for (var i = 0; i < eventCount; i++) - await _fixture.StreamsClient.AppendToStreamAsync( - streamName, - StreamState.Any, - _fixture.CreateTestEvents() - ); - - await _fixture.Client.CreateToStreamAsync( - streamName, - subscriptionName, - new( - true, - StreamPosition.Start, - readBatchSize: 10, - historyBufferSize: 20 - ), - userCredentials: userCredentials - ); - - await using var subscription = - _fixture.Client.SubscribeToStream(streamName, subscriptionName, userCredentials: userCredentials); - - await Task.WhenAll(Subscribe(), Append()).WithTimeout(); - - Assert.Equal(totalEvents, hitCount); - - return; - - async Task Subscribe() { - await foreach (var message in subscription.Messages) { - if (message is not PersistentSubscriptionMessage.Event(var resolvedEvent, var retryCount)) { - continue; - } - - if (retryCount is 0 or null) { - var result = Interlocked.Increment(ref hitCount); - - await subscription.Ack(resolvedEvent); - - if (totalEvents == result) - return; - } else { - // This is a retry - await subscription.Ack(resolvedEvent); - } - } - } - - async Task Append() { - for (var i = 0; i < eventCount; i++) - await _fixture.StreamsClient.AppendToStreamAsync( - streamName, - StreamState.Any, - _fixture.CreateTestEvents()); - } - } - - public class Fixture : EventStoreClientFixture { - protected override Task Given() => Task.CompletedTask; - protected override Task When() => Task.CompletedTask; - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/Diagnostics/PersistentSubscriptionsTracingInstrumentationTests.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/Diagnostics/PersistentSubscriptionsTracingInstrumentationTests.cs deleted file mode 100644 index 904f74f70..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/Diagnostics/PersistentSubscriptionsTracingInstrumentationTests.cs +++ /dev/null @@ -1,155 +0,0 @@ -using EventStore.Diagnostics.Tracing; - -namespace EventStore.Client.PersistentSubscriptions.Tests.Diagnostics; - -[Trait("Category", "Diagnostics:Tracing")] -public class PersistentSubscriptionsTracingInstrumentationTests(ITestOutputHelper output, DiagnosticsFixture fixture) - : EventStoreTests(output, fixture) { - [Fact] - public async Task PersistentSubscriptionIsInstrumentedWithTracingAndRestoresRemoteAppendContextAsExpected() { - var stream = Fixture.GetStreamName(); - var events = Fixture.CreateTestEvents(2, metadata: Fixture.CreateTestJsonMetadata()).ToArray(); - - var groupName = $"{stream}-group"; - await Fixture.Subscriptions.CreateToStreamAsync( - stream, - groupName, - new() - ); - - await Fixture.Streams.AppendToStreamAsync( - stream, - StreamState.NoStream, - events - ); - - string? subscriptionId = null; - await Subscribe().WithTimeout(); - - var appendActivity = Fixture - .GetActivitiesForOperation(TracingConstants.Operations.Append, stream) - .SingleOrDefault() - .ShouldNotBeNull(); - - var subscribeActivities = Fixture - .GetActivitiesForOperation(TracingConstants.Operations.Subscribe, stream) - .ToArray(); - - subscriptionId.ShouldNotBeNull(); - subscribeActivities.Length.ShouldBe(events.Length); - - for (var i = 0; i < subscribeActivities.Length; i++) { - subscribeActivities[i].TraceId.ShouldBe(appendActivity.Context.TraceId); - subscribeActivities[i].ParentSpanId.ShouldBe(appendActivity.Context.SpanId); - subscribeActivities[i].HasRemoteParent.ShouldBeTrue(); - - Fixture.AssertSubscriptionActivityHasExpectedTags( - subscribeActivities[i], - stream, - events[i].EventId.ToString(), - subscriptionId - ); - } - - return; - - async Task Subscribe() { - await using var subscription = Fixture.Subscriptions.SubscribeToStream(stream, groupName); - await using var enumerator = subscription.Messages.GetAsyncEnumerator(); - - int eventsAppeared = 0; - while (await enumerator.MoveNextAsync()) { - if (enumerator.Current is PersistentSubscriptionMessage.SubscriptionConfirmation(var sid)) - subscriptionId = sid; - - if (enumerator.Current is not PersistentSubscriptionMessage.Event(_, _)) - continue; - - eventsAppeared++; - if (eventsAppeared >= events.Length) - return; - } - } - } - - [Fact] - public async Task PersistentSubscriptionDoesNotThrowWhenInstrumentedWithTracingAndReceivesNonJsonEvents() { - var stream = Fixture.GetStreamName(); - var events = Fixture.CreateTestEvents( - 2, - metadata: Fixture.CreateTestJsonMetadata(), - contentType: Constants.Metadata.ContentTypes.ApplicationOctetStream - ).ToArray(); - - var groupName = $"{stream}-group"; - await Fixture.Subscriptions.CreateToStreamAsync( - stream, - groupName, - new() - ); - - await Fixture.Streams.AppendToStreamAsync( - stream, - StreamState.NoStream, - events - ); - - await Subscribe().WithTimeout(); - - return; - - async Task Subscribe() { - await using var subscription = Fixture.Subscriptions.SubscribeToStream(stream, groupName); - await using var enumerator = subscription.Messages.GetAsyncEnumerator(); - - var eventsAppeared = 0; - while (await enumerator.MoveNextAsync()) { - if (enumerator.Current is PersistentSubscriptionMessage.Event(_, _)) - eventsAppeared++; - - if (eventsAppeared >= events.Length) - return; - } - } - } - - [Fact] - public async Task PersistentSubscriptionDoesNotThrowWhenInstrumentedWithTracingAndReceivesEventsWithInvalidJsonMetadata() { - var stream = Fixture.GetStreamName(); - var events = Fixture.CreateTestEvents( - 2, - metadata: "clearlynotavalidjsonobject"u8.ToArray() - ).ToArray(); - - var groupName = $"{stream}-group"; - await Fixture.Subscriptions.CreateToStreamAsync( - stream, - groupName, - new() - ); - - await Fixture.Streams.AppendToStreamAsync( - stream, - StreamState.NoStream, - events - ); - - await Subscribe().WithTimeout(); - - return; - - async Task Subscribe() { - await using var subscription = Fixture.Subscriptions.SubscribeToStream(stream, groupName); - await using var enumerator = subscription.Messages.GetAsyncEnumerator(); - - var eventsAppeared = 0; - while (await enumerator.MoveNextAsync()) { - if (enumerator.Current is PersistentSubscriptionMessage.Event(_, _)) - eventsAppeared++; - - if (eventsAppeared >= events.Length) - return; - } - } - } -} \ No newline at end of file diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/EventStore.Client.PersistentSubscriptions.Tests.csproj b/test/EventStore.Client.PersistentSubscriptions.Tests/EventStore.Client.PersistentSubscriptions.Tests.csproj deleted file mode 100644 index 0287e82db..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/EventStore.Client.PersistentSubscriptions.Tests.csproj +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/EventStore.Client.PersistentSubscriptions.Tests.csproj.DotSettings b/test/EventStore.Client.PersistentSubscriptions.Tests/EventStore.Client.PersistentSubscriptions.Tests.csproj.DotSettings deleted file mode 100644 index a456beab9..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/EventStore.Client.PersistentSubscriptions.Tests.csproj.DotSettings +++ /dev/null @@ -1,4 +0,0 @@ - - False - False - False \ No newline at end of file diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/EventStoreClientFixture.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/EventStoreClientFixture.cs deleted file mode 100644 index babe29328..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/EventStoreClientFixture.cs +++ /dev/null @@ -1,41 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests; - -public abstract class EventStoreClientFixture : EventStoreClientFixtureBase { - readonly bool _skipPsWarmUp; - - protected EventStoreClientFixture(EventStoreClientSettings? settings = null, bool skipPSWarmUp = false, bool noDefaultCredentials = false) - : base(settings, noDefaultCredentials: noDefaultCredentials) { - _skipPsWarmUp = skipPSWarmUp; - - Client = new(Settings); - StreamsClient = new(Settings); - UserManagementClient = new(Settings); - } - - public EventStorePersistentSubscriptionsClient Client { get; } - public EventStoreClient StreamsClient { get; } - public EventStoreUserManagementClient UserManagementClient { get; } - - protected override async Task OnServerUpAsync() { - await StreamsClient.WarmUp(); - await UserManagementClient.WarmUp(); - - if (!_skipPsWarmUp) - await Client.WarmUp(); - - await UserManagementClient.CreateUserWithRetry( - TestCredentials.TestUser1.Username!, - TestCredentials.TestUser1.Username!, - Array.Empty(), - TestCredentials.TestUser1.Password!, - TestCredentials.Root - ); - } - - public override async Task DisposeAsync() { - await UserManagementClient.DisposeAsync(); - await StreamsClient.DisposeAsync(); - await Client.DisposeAsync(); - await base.DisposeAsync(); - } -} \ No newline at end of file diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/PersistentSubscriptionSettingsTests.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/PersistentSubscriptionSettingsTests.cs deleted file mode 100644 index de32c4160..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/PersistentSubscriptionSettingsTests.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests; - -public class PersistentSubscriptionSettingsTests { - [Fact] - public void LargeCheckpointAfterThrows() => - Assert.Throws(() => new PersistentSubscriptionSettings(checkPointAfter: TimeSpan.FromDays(25 * 365))); - - [Fact] - public void LargeMessageTimeoutThrows() => - Assert.Throws(() => new PersistentSubscriptionSettings(messageTimeout: TimeSpan.FromDays(25 * 365))); -} \ No newline at end of file diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/connect_to_existing_with_max_one_client_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/connect_to_existing_with_max_one_client_obsolete.cs deleted file mode 100644 index 3c39e3598..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/connect_to_existing_with_max_one_client_obsolete.cs +++ /dev/null @@ -1,43 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll.Obsolete; - -[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class connect_to_existing_with_max_one_client_obsolete : IClassFixture { - const string Group = "maxoneclient"; - - readonly Fixture _fixture; - - public connect_to_existing_with_max_one_client_obsolete(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public async Task the_second_subscription_fails_to_connect() { - using var first = await _fixture.Client.SubscribeToAllAsync( - Group, - delegate { return Task.CompletedTask; }, - userCredentials: TestCredentials.Root - ).WithTimeout(); - - var ex = await Assert.ThrowsAsync( - async () => { - using var _ = await _fixture.Client.SubscribeToAllAsync( - Group, - delegate { return Task.CompletedTask; }, - userCredentials: TestCredentials.Root - ); - } - ).WithTimeout(); - - Assert.Equal(SystemStreams.AllStream, ex.StreamName); - Assert.Equal(Group, ex.GroupName); - } - - public class Fixture : EventStoreClientFixture { - protected override Task Given() => - Client.CreateToAllAsync( - Group, - new(maxSubscriberCount: 1), - userCredentials: TestCredentials.Root - ); - - protected override Task When() => Task.CompletedTask; - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/connect_to_existing_with_permissions_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/connect_to_existing_with_permissions_obsolete.cs deleted file mode 100644 index 890dacf49..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/connect_to_existing_with_permissions_obsolete.cs +++ /dev/null @@ -1,37 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll.Obsolete; - -[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class connect_to_existing_with_permissions_obsolete - : IClassFixture { - const string Group = "connectwithpermissions"; - - readonly Fixture _fixture; - - public connect_to_existing_with_permissions_obsolete(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public async Task the_subscription_succeeds() { - var dropped = new TaskCompletionSource<(SubscriptionDroppedReason, Exception?)>(); - using var subscription = await _fixture.Client.SubscribeToAllAsync( - Group, - delegate { return Task.CompletedTask; }, - (s, reason, ex) => dropped.TrySetResult((reason, ex)), - TestCredentials.Root - ).WithTimeout(); - - Assert.NotNull(subscription); - - await Assert.ThrowsAsync(() => dropped.Task.WithTimeout()); - } - - public class Fixture : EventStoreClientFixture { - protected override Task Given() => - Client.CreateToAllAsync( - Group, - new(), - userCredentials: TestCredentials.Root - ); - - protected override Task When() => Task.CompletedTask; - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/connect_to_existing_with_start_from_beginning_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/connect_to_existing_with_start_from_beginning_obsolete.cs deleted file mode 100644 index 10b06a965..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/connect_to_existing_with_start_from_beginning_obsolete.cs +++ /dev/null @@ -1,66 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll.Obsolete; - -[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class connect_to_existing_with_start_from_beginning_obsolete : IClassFixture { - const string Group = "startfrombeginning"; - - readonly Fixture _fixture; - - public connect_to_existing_with_start_from_beginning_obsolete(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public async Task the_subscription_gets_event_zero_as_its_first_event() { - var resolvedEvent = await _fixture.FirstEvent.WithTimeout(TimeSpan.FromSeconds(10)); - Assert.Equal(_fixture.Events![0].Event.EventId, resolvedEvent.Event.EventId); - } - - public class Fixture : EventStoreClientFixture { - readonly TaskCompletionSource _firstEventSource; - - PersistentSubscription? _subscription; - - public Fixture() => _firstEventSource = new(); - - public ResolvedEvent[]? Events { get; set; } - - public Task FirstEvent => _firstEventSource.Task; - - protected override async Task Given() { - //append 10 events to random streams to make sure we have at least 10 events in the transaction file - foreach (var @event in CreateTestEvents(10)) - await StreamsClient.AppendToStreamAsync(Guid.NewGuid().ToString(), StreamState.NoStream, new[] { @event }); - - Events = await StreamsClient.ReadAllAsync( - Direction.Forwards, - Position.Start, - 10, - userCredentials: TestCredentials.Root - ).ToArrayAsync(); - - await Client.CreateToAllAsync( - Group, - new(startFrom: Position.Start), - userCredentials: TestCredentials.Root - ); - } - - protected override async Task When() => - _subscription = await Client.SubscribeToAllAsync( - Group, - async (subscription, e, r, ct) => { - _firstEventSource.TrySetResult(e); - await subscription.Ack(e); - }, - (subscription, reason, ex) => { - if (reason != SubscriptionDroppedReason.Disposed) - _firstEventSource.TrySetException(ex!); - }, - TestCredentials.Root - ); - - public override Task DisposeAsync() { - _subscription?.Dispose(); - return base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/connect_to_existing_with_start_from_not_set_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/connect_to_existing_with_start_from_not_set_obsolete.cs deleted file mode 100644 index 5446e1eef..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/connect_to_existing_with_start_from_not_set_obsolete.cs +++ /dev/null @@ -1,64 +0,0 @@ - -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll.Obsolete; - -[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class connect_to_existing_with_start_from_not_set_obsolete : IClassFixture { - const string Group = "startfromend1"; - readonly Fixture _fixture; - - public connect_to_existing_with_start_from_not_set_obsolete(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public async Task the_subscription_gets_no_non_system_events() => - await Assert.ThrowsAsync(() => _fixture.FirstNonSystemEvent.WithTimeout()); - - public class Fixture : EventStoreClientFixture { - readonly TaskCompletionSource _firstNonSystemEventSource; - PersistentSubscription? _subscription; - - public Fixture() => _firstNonSystemEventSource = new(); - - public Task FirstNonSystemEvent => _firstNonSystemEventSource.Task; - - protected override async Task Given() { - foreach (var @event in CreateTestEvents(10)) - await StreamsClient.AppendToStreamAsync( - "non-system-stream-" + Guid.NewGuid(), - StreamState.Any, - new[] { - @event - } - ); - - await Client.CreateToAllAsync( - Group, - new(), - userCredentials: TestCredentials.Root - ); - } - - protected override async Task When() => - _subscription = await Client.SubscribeToAllAsync( - Group, - async (subscription, e, r, ct) => { - if (SystemStreams.IsSystemStream(e.OriginalStreamId)) { - await subscription.Ack(e); - return; - } - - _firstNonSystemEventSource.TrySetResult(e); - await subscription.Ack(e); - }, - (subscription, reason, ex) => { - if (reason != SubscriptionDroppedReason.Disposed) - _firstNonSystemEventSource.TrySetException(ex!); - }, - TestCredentials.Root - ); - - public override Task DisposeAsync() { - _subscription?.Dispose(); - return base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/connect_to_existing_with_start_from_not_set_then_event_written_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/connect_to_existing_with_start_from_not_set_then_event_written_obsolete.cs deleted file mode 100644 index 1ee2b6fae..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/connect_to_existing_with_start_from_not_set_then_event_written_obsolete.cs +++ /dev/null @@ -1,72 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll.Obsolete; - -[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class connect_to_existing_with_start_from_not_set_then_event_written_obsolete - : IClassFixture { - const string Group = "startfromnotset2"; - - readonly Fixture _fixture; - - public - connect_to_existing_with_start_from_not_set_then_event_written_obsolete(Fixture fixture) => - _fixture = fixture; - - [SupportsPSToAll.Fact] - public async Task the_subscription_gets_the_written_event_as_its_first_non_system_event2() { - var resolvedEvent = await _fixture.FirstNonSystemEvent.WithTimeout(); - Assert.Equal(_fixture.ExpectedEvent!.EventId, resolvedEvent.Event.EventId); - Assert.Equal(_fixture.ExpectedStreamId, resolvedEvent.Event.EventStreamId); - } - - public class Fixture : EventStoreClientFixture { - readonly TaskCompletionSource _firstNonSystemEventSource; - - public readonly EventData? ExpectedEvent; - public readonly string ExpectedStreamId; - - PersistentSubscription? _subscription; - - public Fixture() { - _firstNonSystemEventSource = new(); - ExpectedEvent = CreateTestEvents(1).First(); - ExpectedStreamId = Guid.NewGuid().ToString(); - } - - public Task FirstNonSystemEvent => _firstNonSystemEventSource.Task; - - protected override async Task Given() { - foreach (var @event in CreateTestEvents(10)) - await StreamsClient.AppendToStreamAsync( - "non-system-stream-" + Guid.NewGuid(), - StreamState.Any, - new[] { @event } - ); - - await Client.CreateToAllAsync(Group, new(), userCredentials: TestCredentials.Root); - _subscription = await Client.SubscribeToAllAsync( - Group, - async (subscription, e, r, ct) => { - if (SystemStreams.IsSystemStream(e.OriginalStreamId)) { - await subscription.Ack(e); - return; - } - - _firstNonSystemEventSource.TrySetResult(e); - await subscription.Ack(e); - }, - (subscription, reason, ex) => { - if (reason != SubscriptionDroppedReason.Disposed) - _firstNonSystemEventSource.TrySetException(ex!); - }, - TestCredentials.Root - ); - } - - protected override async Task When() => await StreamsClient.AppendToStreamAsync(ExpectedStreamId, StreamState.NoStream, new[] { ExpectedEvent! }); - - public override Task DisposeAsync() { - _subscription?.Dispose(); - return base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/connect_to_existing_with_start_from_set_to_end_position_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/connect_to_existing_with_start_from_set_to_end_position_obsolete.cs deleted file mode 100644 index 7057b1489..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/connect_to_existing_with_start_from_set_to_end_position_obsolete.cs +++ /dev/null @@ -1,63 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll.Obsolete; - -[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class connect_to_existing_with_start_from_set_to_end_position_obsolete: IClassFixture { - const string Group = "startfromend1"; - - readonly Fixture _fixture; - - public connect_to_existing_with_start_from_set_to_end_position_obsolete(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public async Task the_subscription_gets_no_non_system_events() => - await Assert.ThrowsAsync(() => _fixture.FirstNonSystemEvent.WithTimeout()); - - public class Fixture : EventStoreClientFixture { - readonly TaskCompletionSource _firstNonSystemEventSource; - - PersistentSubscription? _subscription; - - public Fixture() => _firstNonSystemEventSource = new(); - - public Task FirstNonSystemEvent => _firstNonSystemEventSource.Task; - - protected override async Task Given() { - foreach (var @event in CreateTestEvents(10)) - await StreamsClient.AppendToStreamAsync( - "non-system-stream-" + Guid.NewGuid(), - StreamState.Any, - new[] { @event } - ); - - await Client.CreateToAllAsync( - Group, - new(startFrom: Position.End), - userCredentials: TestCredentials.Root - ); - } - - protected override async Task When() => - _subscription = await Client.SubscribeToAllAsync( - Group, - async (subscription, e, r, ct) => { - if (SystemStreams.IsSystemStream(e.OriginalStreamId)) { - await subscription.Ack(e); - return; - } - - _firstNonSystemEventSource.TrySetResult(e); - await subscription.Ack(e); - }, - (subscription, reason, ex) => { - if (reason != SubscriptionDroppedReason.Disposed) - _firstNonSystemEventSource.TrySetException(ex!); - }, - TestCredentials.Root - ); - - public override Task DisposeAsync() { - _subscription?.Dispose(); - return base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/connect_to_existing_with_start_from_set_to_end_position_then_event_written_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/connect_to_existing_with_start_from_set_to_end_position_then_event_written_obsolete.cs deleted file mode 100644 index 7083cb78f..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/connect_to_existing_with_start_from_set_to_end_position_then_event_written_obsolete.cs +++ /dev/null @@ -1,69 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll.Obsolete; - -[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class - connect_to_existing_with_start_from_set_to_end_position_then_event_written_obsolete - : IClassFixture { - const string Group = "startfromnotset2"; - - readonly Fixture _fixture; - - public connect_to_existing_with_start_from_set_to_end_position_then_event_written_obsolete(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public async Task the_subscription_gets_the_written_event_as_its_first_non_system_event() { - var resolvedEvent = await _fixture.FirstNonSystemEvent.WithTimeout(); - Assert.Equal(_fixture.ExpectedEvent.EventId, resolvedEvent.Event.EventId); - Assert.Equal(_fixture.ExpectedStreamId, resolvedEvent.Event.EventStreamId); - } - - public class Fixture : EventStoreClientFixture { - readonly TaskCompletionSource _firstNonSystemEventSource; - public readonly EventData ExpectedEvent; - public readonly string ExpectedStreamId; - PersistentSubscription? _subscription; - - public Fixture() { - _firstNonSystemEventSource = new(); - ExpectedEvent = CreateTestEvents(1).First(); - ExpectedStreamId = Guid.NewGuid().ToString(); - } - - public Task FirstNonSystemEvent => _firstNonSystemEventSource.Task; - - protected override async Task Given() { - foreach (var @event in CreateTestEvents(10)) - await StreamsClient.AppendToStreamAsync( - "non-system-stream-" + Guid.NewGuid(), - StreamState.Any, - new[] { @event } - ); - - await Client.CreateToAllAsync(Group, new(startFrom: Position.End), userCredentials: TestCredentials.Root); - _subscription = await Client.SubscribeToAllAsync( - Group, - async (subscription, e, r, ct) => { - if (SystemStreams.IsSystemStream(e.OriginalStreamId)) { - await subscription.Ack(e); - return; - } - - _firstNonSystemEventSource.TrySetResult(e); - await subscription.Ack(e); - }, - (subscription, reason, ex) => { - if (reason != SubscriptionDroppedReason.Disposed) - _firstNonSystemEventSource.TrySetException(ex!); - }, - TestCredentials.Root - ); - } - - protected override async Task When() => await StreamsClient.AppendToStreamAsync(ExpectedStreamId, StreamState.NoStream, new[] { ExpectedEvent }); - - public override Task DisposeAsync() { - _subscription?.Dispose(); - return base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/connect_to_existing_with_start_from_set_to_invalid_middle_position_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/connect_to_existing_with_start_from_set_to_invalid_middle_position_obsolete.cs deleted file mode 100644 index f1d317b76..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/connect_to_existing_with_start_from_set_to_invalid_middle_position_obsolete.cs +++ /dev/null @@ -1,49 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll.Obsolete; - -[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class connect_to_existing_with_start_from_set_to_invalid_middle_position_obsolete - : IClassFixture { - const string Group = "startfrominvalid1"; - readonly Fixture _fixture; - - public connect_to_existing_with_start_from_set_to_invalid_middle_position_obsolete(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public async Task the_subscription_is_dropped() { - var (reason, exception) = await _fixture.Dropped.WithTimeout(); - Assert.Equal(SubscriptionDroppedReason.ServerError, reason); - Assert.IsType(exception); - } - - public class Fixture : EventStoreClientFixture { - readonly TaskCompletionSource<(SubscriptionDroppedReason, Exception?)> _dropped; - - PersistentSubscription? _subscription; - - public Fixture() => _dropped = new(); - - public Task<(SubscriptionDroppedReason, Exception?)> Dropped => _dropped.Task; - - protected override async Task Given() { - var invalidPosition = new Position(1L, 1L); - await Client.CreateToAllAsync( - Group, - new(startFrom: invalidPosition), - userCredentials: TestCredentials.Root - ); - } - - protected override async Task When() => - _subscription = await Client.SubscribeToAllAsync( - Group, - async (subscription, e, r, ct) => await subscription.Ack(e), - (subscription, reason, ex) => { _dropped.TrySetResult((reason, ex)); }, - TestCredentials.Root - ); - - public override Task DisposeAsync() { - _subscription?.Dispose(); - return base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/connect_to_existing_with_start_from_set_to_valid_middle_position_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/connect_to_existing_with_start_from_set_to_valid_middle_position_obsolete.cs deleted file mode 100644 index 69230d695..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/connect_to_existing_with_start_from_set_to_valid_middle_position_obsolete.cs +++ /dev/null @@ -1,65 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll.Obsolete; - -[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class connect_to_existing_with_start_from_set_to_valid_middle_position_obsolete - : IClassFixture { - const string Group = "startfromvalid"; - - readonly Fixture _fixture; - - public connect_to_existing_with_start_from_set_to_valid_middle_position_obsolete(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public async Task the_subscription_gets_the_event_at_the_specified_start_position_as_its_first_event() { - var resolvedEvent = await _fixture.FirstEvent.WithTimeout(); - Assert.Equal(_fixture.ExpectedEvent.OriginalPosition, resolvedEvent.Event.Position); - Assert.Equal(_fixture.ExpectedEvent.Event.EventId, resolvedEvent.Event.EventId); - Assert.Equal(_fixture.ExpectedEvent.Event.EventStreamId, resolvedEvent.Event.EventStreamId); - } - - public class Fixture : EventStoreClientFixture { - readonly TaskCompletionSource _firstEventSource; - PersistentSubscription? _subscription; - - public Fixture() => _firstEventSource = new(); - - public Task FirstEvent => _firstEventSource.Task; - public ResolvedEvent ExpectedEvent { get; private set; } - - protected override async Task Given() { - var events = await StreamsClient.ReadAllAsync( - Direction.Forwards, - Position.Start, - 10, - userCredentials: TestCredentials.Root - ).ToArrayAsync(); - - ExpectedEvent = events[events.Length / 2]; //just a random event in the middle of the results - - await Client.CreateToAllAsync( - Group, - new(startFrom: ExpectedEvent.OriginalPosition), - userCredentials: TestCredentials.Root - ); - } - - protected override async Task When() => - _subscription = await Client.SubscribeToAllAsync( - Group, - async (subscription, e, r, ct) => { - _firstEventSource.TrySetResult(e); - await subscription.Ack(e); - }, - (subscription, reason, ex) => { - if (reason != SubscriptionDroppedReason.Disposed) - _firstEventSource.TrySetException(ex!); - }, - TestCredentials.Root - ); - - public override Task DisposeAsync() { - _subscription?.Dispose(); - return base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/connect_to_existing_without_permissions_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/connect_to_existing_without_permissions_obsolete.cs deleted file mode 100644 index b09dbc46a..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/connect_to_existing_without_permissions_obsolete.cs +++ /dev/null @@ -1,32 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll.Obsolete; - -[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class connect_to_existing_without_permissions_obsolete - : IClassFixture { - readonly Fixture _fixture; - public connect_to_existing_without_permissions_obsolete(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public Task throws_access_denied() => - Assert.ThrowsAsync( - async () => { - using var _ = await _fixture.Client.SubscribeToAllAsync( - "agroupname55", - delegate { return Task.CompletedTask; } - ); - } - ).WithTimeout(); - - public class Fixture : EventStoreClientFixture { - public Fixture() : base(noDefaultCredentials: true) { } - - protected override Task Given() => - Client.CreateToAllAsync( - "agroupname55", - new(), - userCredentials: TestCredentials.Root - ); - - protected override Task When() => Task.CompletedTask; - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/connect_to_existing_without_read_all_permissions_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/connect_to_existing_without_read_all_permissions_obsolete.cs deleted file mode 100644 index da6f19f72..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/connect_to_existing_without_read_all_permissions_obsolete.cs +++ /dev/null @@ -1,33 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll.Obsolete; - -[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class connect_to_existing_without_read_all_permissions_obsolete - : IClassFixture { - readonly Fixture _fixture; - public connect_to_existing_without_read_all_permissions_obsolete(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public Task throws_access_denied() => - Assert.ThrowsAsync( - async () => { - using var _ = await _fixture.Client.SubscribeToAllAsync( - "agroupname55", - delegate { return Task.CompletedTask; }, - userCredentials: TestCredentials.TestUser1 - ); - } - ).WithTimeout(); - - public class Fixture : EventStoreClientFixture { - public Fixture() : base(noDefaultCredentials: true) { } - - protected override Task Given() => - Client.CreateToAllAsync( - "agroupname55", - new(), - userCredentials: TestCredentials.Root - ); - - protected override Task When() => Task.CompletedTask; - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/connect_to_non_existing_with_permissions_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/connect_to_non_existing_with_permissions_obsolete.cs deleted file mode 100644 index 5f8c885c1..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/connect_to_non_existing_with_permissions_obsolete.cs +++ /dev/null @@ -1,32 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll.Obsolete; - -[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class connect_to_non_existing_with_permissions_obsolete - : IClassFixture { - const string Group = "foo"; - - readonly Fixture _fixture; - - public connect_to_non_existing_with_permissions_obsolete(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public async Task throws_persistent_subscription_not_found() { - var ex = await Assert.ThrowsAsync( - async () => { - using var _ = await _fixture.Client.SubscribeToAllAsync( - Group, - delegate { return Task.CompletedTask; }, - userCredentials: TestCredentials.Root - ); - } - ).WithTimeout(); - - Assert.Equal(SystemStreams.AllStream, ex.StreamName); - Assert.Equal(Group, ex.GroupName); - } - - public class Fixture : EventStoreClientFixture { - protected override Task Given() => Task.CompletedTask; - protected override Task When() => Task.CompletedTask; - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/connect_with_retries_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/connect_with_retries_obsolete.cs deleted file mode 100644 index 085eecc3e..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/connect_with_retries_obsolete.cs +++ /dev/null @@ -1,59 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll.Obsolete; - -[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class connect_with_retries_obsolete - : IClassFixture { - const string Group = "retries"; - readonly Fixture _fixture; - - public connect_with_retries_obsolete(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public async Task events_are_retried_until_success() => Assert.Equal(5, await _fixture.RetryCount.WithTimeout()); - - public class Fixture : EventStoreClientFixture { - readonly TaskCompletionSource _retryCountSource; - PersistentSubscription? _subscription; - - public Fixture() => _retryCountSource = new(); - - public Task RetryCount => _retryCountSource.Task; - - protected override async Task Given() { - await Client.CreateToAllAsync( - Group, - new(startFrom: Position.Start), - userCredentials: TestCredentials.Root - ); - - _subscription = await Client.SubscribeToAllAsync( - Group, - async (subscription, e, r, ct) => { - if (r > 4) { - _retryCountSource.TrySetResult(r.Value); - await subscription.Ack(e.Event.EventId); - } - else { - await subscription.Nack( - PersistentSubscriptionNakEventAction.Retry, - "Not yet tried enough times", - e - ); - } - }, - (subscription, reason, ex) => { - if (reason != SubscriptionDroppedReason.Disposed) - _retryCountSource.TrySetException(ex!); - }, - TestCredentials.Root - ); - } - - protected override Task When() => Task.CompletedTask; - - public override Task DisposeAsync() { - _subscription?.Dispose(); - return base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/deleting_existing_with_subscriber_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/deleting_existing_with_subscriber_obsolete.cs deleted file mode 100644 index fdca510d5..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/deleting_existing_with_subscriber_obsolete.cs +++ /dev/null @@ -1,66 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll.Obsolete; - -[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class deleting_existing_with_subscriber_obsolete - : IClassFixture { - readonly Fixture _fixture; - - public deleting_existing_with_subscriber_obsolete(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public async Task the_subscription_is_dropped() { - var (reason, exception) = await _fixture.Dropped.WithTimeout(); - Assert.Equal(SubscriptionDroppedReason.ServerError, reason); - var ex = Assert.IsType(exception); - - Assert.Equal(SystemStreams.AllStream, ex.StreamName); - Assert.Equal("groupname123", ex.GroupName); - } - - [Fact(Skip = "Isn't this how it should work?")] - public async Task the_subscription_is_dropped_with_not_found() { - var (reason, exception) = await _fixture.Dropped.WithTimeout(); - Assert.Equal(SubscriptionDroppedReason.ServerError, reason); - var ex = Assert.IsType(exception); - Assert.Equal(SystemStreams.AllStream, ex.StreamName); - Assert.Equal("groupname123", ex.GroupName); - } - - public class Fixture : EventStoreClientFixture { - readonly TaskCompletionSource<(SubscriptionDroppedReason, Exception?)> _dropped; - PersistentSubscription? _subscription; - - public Fixture() => _dropped = new(); - - public Task<(SubscriptionDroppedReason, Exception?)> Dropped => _dropped.Task; - - protected override async Task Given() { - await Client.CreateToAllAsync( - "groupname123", - new(), - userCredentials: TestCredentials.Root - ); - - _subscription = await Client.SubscribeToAllAsync( - "groupname123", - async (s, e, i, ct) => await s.Ack(e), - (s, r, e) => _dropped.TrySetResult((r, e)), - TestCredentials.Root - ); - - // todo: investigate why this test is flaky without this delay - await Task.Delay(500); - } - - protected override Task When() => - Client.DeleteToAllAsync( - "groupname123", - userCredentials: TestCredentials.Root - ); - - public override Task DisposeAsync() { - _subscription?.Dispose(); - return base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/happy_case_catching_up_to_link_to_events_manual_ack_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/happy_case_catching_up_to_link_to_events_manual_ack_obsolete.cs deleted file mode 100644 index cfccd2de2..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/happy_case_catching_up_to_link_to_events_manual_ack_obsolete.cs +++ /dev/null @@ -1,78 +0,0 @@ -using System.Text; - -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll.Obsolete; - -[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class happy_case_catching_up_to_link_to_events_manual_ack_obsolete - : IClassFixture { - const string Group = nameof(Group); - const int BufferCount = 10; - const int EventWriteCount = BufferCount * 2; - - readonly Fixture _fixture; - - public happy_case_catching_up_to_link_to_events_manual_ack_obsolete(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public async Task Test() => await _fixture.EventsReceived.WithTimeout(); - - public class Fixture : EventStoreClientFixture { - readonly EventData[] _events; - readonly TaskCompletionSource _eventsReceived; - int _eventReceivedCount; - - PersistentSubscription? _subscription; - - public Fixture() { - _events = CreateTestEvents(EventWriteCount) - .Select( - (e, i) => new EventData( - e.EventId, - SystemEventTypes.LinkTo, - Encoding.UTF8.GetBytes($"{i}@test"), - contentType: Constants.Metadata.ContentTypes.ApplicationOctetStream - ) - ) - .ToArray(); - - _eventsReceived = new(); - } - - public Task EventsReceived => _eventsReceived.Task; - - protected override async Task Given() { - foreach (var e in _events) - await StreamsClient.AppendToStreamAsync("test-" + Guid.NewGuid(), StreamState.Any, new[] { e }); - - await Client.CreateToAllAsync( - Group, - new(startFrom: Position.Start, resolveLinkTos: true), - userCredentials: TestCredentials.Root - ); - - _subscription = await Client.SubscribeToAllAsync( - Group, - async (subscription, e, retryCount, ct) => { - await subscription.Ack(e); - - if (e.OriginalStreamId.StartsWith("test-") - && Interlocked.Increment(ref _eventReceivedCount) == _events.Length) - _eventsReceived.TrySetResult(true); - }, - (s, r, e) => { - if (e != null) - _eventsReceived.TrySetException(e); - }, - bufferSize: BufferCount, - userCredentials: TestCredentials.Root - ); - } - - protected override Task When() => Task.CompletedTask; - - public override Task DisposeAsync() { - _subscription?.Dispose(); - return base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/happy_case_catching_up_to_normal_events_manual_ack_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/happy_case_catching_up_to_normal_events_manual_ack_obsolete.cs deleted file mode 100644 index cd1f69ee4..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/happy_case_catching_up_to_normal_events_manual_ack_obsolete.cs +++ /dev/null @@ -1,65 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll.Obsolete; - -[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class happy_case_catching_up_to_normal_events_manual_ack_obsolete : IClassFixture { - const string Group = nameof(Group); - const int BufferCount = 10; - const int EventWriteCount = BufferCount * 2; - - readonly Fixture _fixture; - - public happy_case_catching_up_to_normal_events_manual_ack_obsolete(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public async Task Test() => await _fixture.EventsReceived.WithTimeout(); - - public class Fixture : EventStoreClientFixture { - readonly EventData[] _events; - readonly TaskCompletionSource _eventsReceived; - int _eventReceivedCount; - - PersistentSubscription? _subscription; - - public Fixture() { - _events = CreateTestEvents(EventWriteCount).ToArray(); - _eventsReceived = new(); - } - - public Task EventsReceived => _eventsReceived.Task; - - protected override async Task Given() { - foreach (var e in _events) - await StreamsClient.AppendToStreamAsync("test-" + Guid.NewGuid(), StreamState.Any, new[] { e }); - - await Client.CreateToAllAsync( - Group, - new(startFrom: Position.Start, resolveLinkTos: true), - userCredentials: TestCredentials.Root - ); - - _subscription = await Client.SubscribeToAllAsync( - Group, - async (subscription, e, retryCount, ct) => { - await subscription.Ack(e); - - if (e.OriginalStreamId.StartsWith("test-") - && Interlocked.Increment(ref _eventReceivedCount) == _events.Length) - _eventsReceived.TrySetResult(true); - }, - (s, r, e) => { - if (e != null) - _eventsReceived.TrySetException(e); - }, - bufferSize: BufferCount, - userCredentials: TestCredentials.Root - ); - } - - protected override Task When() => Task.CompletedTask; - - public override Task DisposeAsync() { - _subscription?.Dispose(); - return base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/happy_case_filtered_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/happy_case_filtered_obsolete.cs deleted file mode 100644 index f9169733b..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/happy_case_filtered_obsolete.cs +++ /dev/null @@ -1,72 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll.Obsolete; - -[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class happy_case_filtered_obsolete : IClassFixture { - readonly Fixture _fixture; - - public happy_case_filtered_obsolete(Fixture fixture) => _fixture = fixture; - - public static IEnumerable FilterCases() => Filters.All.Select(filter => new object[] { filter }); - - [SupportsPSToAll.Theory] - [MemberData(nameof(FilterCases))] - public async Task reads_all_existing_filtered_events(string filterName) { - var streamPrefix = $"{filterName}-{_fixture.GetStreamName()}"; - var (getFilter, prepareEvent) = Filters.GetFilter(filterName); - var filter = getFilter(streamPrefix); - - var appeared = new TaskCompletionSource(); - var appearedEvents = new List(); - var events = _fixture.CreateTestEvents(20).Select(e => prepareEvent(streamPrefix, e)).ToArray(); - - foreach (var e in events) - await _fixture.StreamsClient.AppendToStreamAsync( - $"{streamPrefix}_{Guid.NewGuid():n}", - StreamState.NoStream, - new[] { e } - ); - - await _fixture.Client.CreateToAllAsync( - filterName, - filter, - new(startFrom: Position.Start), - userCredentials: TestCredentials.Root - ); - - using var subscription = await _fixture.Client.SubscribeToAllAsync( - filterName, - async (s, e, r, ct) => { - appearedEvents.Add(e.Event); - if (appearedEvents.Count >= events.Length) - appeared.TrySetResult(true); - - await s.Ack(e); - }, - userCredentials: TestCredentials.Root - ) - .WithTimeout(); - - await Task.WhenAll(appeared.Task).WithTimeout(); - - Assert.Equal(events.Select(x => x.EventId), appearedEvents.Select(x => x.EventId)); - } - - public class Fixture : EventStoreClientFixture { - protected override async Task Given() { - await StreamsClient.AppendToStreamAsync( - Guid.NewGuid().ToString(), - StreamState.NoStream, - CreateTestEvents(256) - ); - - await StreamsClient.SetStreamMetadataAsync( - SystemStreams.AllStream, - StreamState.Any, - new(acl: new(SystemRoles.All)), - userCredentials: TestCredentials.Root - ); - } - - protected override Task When() => Task.CompletedTask; - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/happy_case_filtered_with_start_from_set_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/happy_case_filtered_with_start_from_set_obsolete.cs deleted file mode 100644 index 111ca4e19..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/happy_case_filtered_with_start_from_set_obsolete.cs +++ /dev/null @@ -1,86 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll.Obsolete; - -[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class happy_case_filtered_with_start_from_set_obsolete : IClassFixture { - readonly Fixture _fixture; - - public happy_case_filtered_with_start_from_set_obsolete(Fixture fixture) => _fixture = fixture; - - public static IEnumerable FilterCases() => Filters.All.Select(filter => new object[] { filter }); - - [SupportsPSToAll.Theory] - [MemberData(nameof(FilterCases))] - public async Task reads_all_existing_filtered_events_from_specified_start(string filterName) { - var streamPrefix = $"{filterName}-{_fixture.GetStreamName()}"; - var (getFilter, prepareEvent) = Filters.GetFilter(filterName); - var filter = getFilter(streamPrefix); - - var appeared = new TaskCompletionSource(); - var appearedEvents = new List(); - var events = _fixture.CreateTestEvents(20).Select(e => prepareEvent(streamPrefix, e)).ToArray(); - var eventsToSkip = events.Take(10).ToArray(); - var eventsToCapture = events.Skip(10).ToArray(); - - IWriteResult? eventToCaptureResult = null; - - foreach (var e in eventsToSkip) - await _fixture.StreamsClient.AppendToStreamAsync( - $"{streamPrefix}_{Guid.NewGuid():n}", - StreamState.NoStream, - new[] { e } - ); - - foreach (var e in eventsToCapture) { - var result = await _fixture.StreamsClient.AppendToStreamAsync( - $"{streamPrefix}_{Guid.NewGuid():n}", - StreamState.NoStream, - new[] { e } - ); - - eventToCaptureResult ??= result; - } - - await _fixture.Client.CreateToAllAsync( - filterName, - filter, - new(startFrom: eventToCaptureResult!.LogPosition), - userCredentials: TestCredentials.Root - ); - - using var subscription = await _fixture.Client.SubscribeToAllAsync( - filterName, - async (s, e, r, ct) => { - appearedEvents.Add(e.Event); - if (appearedEvents.Count >= eventsToCapture.Length) - appeared.TrySetResult(true); - - await s.Ack(e); - }, - userCredentials: TestCredentials.Root - ) - .WithTimeout(); - - await Task.WhenAll(appeared.Task).WithTimeout(); - - Assert.Equal(eventsToCapture.Select(x => x.EventId), appearedEvents.Select(x => x.EventId)); - } - - public class Fixture : EventStoreClientFixture { - protected override async Task Given() { - await StreamsClient.AppendToStreamAsync( - Guid.NewGuid().ToString(), - StreamState.NoStream, - CreateTestEvents(256) - ); - - await StreamsClient.SetStreamMetadataAsync( - SystemStreams.AllStream, - StreamState.Any, - new(acl: new(SystemRoles.All)), - userCredentials: TestCredentials.Root - ); - } - - protected override Task When() => Task.CompletedTask; - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/happy_case_writing_and_subscribing_to_normal_events_manual_ack_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/happy_case_writing_and_subscribing_to_normal_events_manual_ack_obsolete.cs deleted file mode 100644 index 2563c488d..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/happy_case_writing_and_subscribing_to_normal_events_manual_ack_obsolete.cs +++ /dev/null @@ -1,65 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll.Obsolete; - -[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class happy_case_writing_and_subscribing_to_normal_events_manual_ack_obsolete - : IClassFixture { - const string Group = nameof(Group); - const int BufferCount = 10; - const int EventWriteCount = BufferCount * 2; - - readonly Fixture _fixture; - - public happy_case_writing_and_subscribing_to_normal_events_manual_ack_obsolete(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public async Task Test() => await _fixture.EventsReceived.WithTimeout(); - - public class Fixture : EventStoreClientFixture { - readonly EventData[] _events; - readonly TaskCompletionSource _eventsReceived; - int _eventReceivedCount; - - PersistentSubscription? _subscription; - - public Fixture() { - _events = CreateTestEvents(EventWriteCount).ToArray(); - _eventsReceived = new(); - } - - public Task EventsReceived => _eventsReceived.Task; - - protected override async Task Given() { - await Client.CreateToAllAsync( - Group, - new(startFrom: Position.End, resolveLinkTos: true), - userCredentials: TestCredentials.Root - ); - - _subscription = await Client.SubscribeToAllAsync( - Group, - async (subscription, e, retryCount, ct) => { - await subscription.Ack(e); - if (e.OriginalStreamId.StartsWith("test-") - && Interlocked.Increment(ref _eventReceivedCount) == _events.Length) - _eventsReceived.TrySetResult(true); - }, - (s, r, e) => { - if (e != null) - _eventsReceived.TrySetException(e); - }, - bufferSize: BufferCount, - userCredentials: TestCredentials.Root - ); - } - - protected override async Task When() { - foreach (var e in _events) - await StreamsClient.AppendToStreamAsync("test-" + Guid.NewGuid(), StreamState.Any, new[] { e }); - } - - public override Task DisposeAsync() { - _subscription?.Dispose(); - return base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/update_existing_with_check_point_filtered_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/update_existing_with_check_point_filtered_obsolete.cs deleted file mode 100644 index 0e9e2be21..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/update_existing_with_check_point_filtered_obsolete.cs +++ /dev/null @@ -1,120 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll.Obsolete; - -[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class update_existing_with_check_point_filtered_obsolete - : IClassFixture { - const string Group = "existing-with-check-point-filtered"; - - readonly Fixture _fixture; - - public update_existing_with_check_point_filtered_obsolete(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public async Task resumes_from_check_point() { - var resumedEvent = await _fixture.Resumed.WithTimeout(TimeSpan.FromSeconds(10)); - Assert.True(resumedEvent.Event.Position > _fixture.CheckPoint); - } - - public class Fixture : EventStoreClientFixture { - readonly TaskCompletionSource _appeared; - readonly List _appearedEvents; - readonly TaskCompletionSource<(SubscriptionDroppedReason, Exception?)> _droppedSource; - readonly EventData[] _events; - readonly TaskCompletionSource _resumedSource; - - PersistentSubscription? _firstSubscription; - PersistentSubscription? _secondSubscription; - - public Fixture() { - _droppedSource = new(); - _resumedSource = new(); - _appeared = new(); - _appearedEvents = new(); - _events = CreateTestEvents(5).ToArray(); - } - - public Task Resumed => _resumedSource.Task; - - public Position CheckPoint { get; private set; } - - protected override async Task Given() { - foreach (var e in _events) - await StreamsClient.AppendToStreamAsync("test-" + Guid.NewGuid(), StreamState.NoStream, new[] { e }); - - await Client.CreateToAllAsync( - Group, - StreamFilter.Prefix("test"), - new( - checkPointLowerBound: 5, - checkPointAfter: TimeSpan.FromSeconds(1), - startFrom: Position.Start - ), - userCredentials: TestCredentials.Root - ); - - _firstSubscription = await Client.SubscribeToAllAsync( - Group, - async (s, e, r, ct) => { - _appearedEvents.Add(e); - - if (_appearedEvents.Count == _events.Length) - _appeared.TrySetResult(true); - - await s.Ack(e); - }, - (subscription, reason, ex) => _droppedSource.TrySetResult((reason, ex)), - TestCredentials.Root - ); - - await Task.WhenAll(_appeared.Task, Checkpointed()).WithTimeout(); - - return; - - async Task Checkpointed() { - await using var subscription = StreamsClient.SubscribeToStream( - $"$persistentsubscription-$all::{Group}-checkpoint", - FromStream.Start, - userCredentials: TestCredentials.Root); - await foreach (var message in subscription.Messages) { - if (message is not StreamMessage.Event(var resolvedEvent)) { - continue; - } - CheckPoint = resolvedEvent.Event.Data.ParsePosition(); - return; - } - } - } - - protected override async Task When() { - // Force restart of the subscription - await Client.UpdateToAllAsync(Group, new(), userCredentials: TestCredentials.Root); - - await _droppedSource.Task.WithTimeout(); - - _secondSubscription = await Client.SubscribeToAllAsync( - Group, - async (s, e, r, ct) => { - _resumedSource.TrySetResult(e); - await s.Ack(e); - s.Dispose(); - }, - (_, reason, ex) => { - if (ex is not null) - _resumedSource.TrySetException(ex); - else - _resumedSource.TrySetResult(default); - }, - TestCredentials.Root - ); - - foreach (var e in _events) - await StreamsClient.AppendToStreamAsync("test-" + Guid.NewGuid(), StreamState.NoStream, new[] { e }); - } - - public override Task DisposeAsync() { - _firstSubscription?.Dispose(); - _secondSubscription?.Dispose(); - return base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/update_existing_with_subscribers_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/update_existing_with_subscribers_obsolete.cs deleted file mode 100644 index 17a77063c..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/update_existing_with_subscribers_obsolete.cs +++ /dev/null @@ -1,60 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll.Obsolete; - -[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class update_existing_with_subscribers_obsolete - : IClassFixture { - const string Group = "existing"; - - readonly Fixture _fixture; - - public update_existing_with_subscribers_obsolete(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public async Task existing_subscriptions_are_dropped() { - var (reason, exception) = await _fixture.Dropped.WithTimeout(TimeSpan.FromSeconds(10)); - Assert.Equal(SubscriptionDroppedReason.ServerError, reason); - var ex = Assert.IsType(exception); - - Assert.Equal(SystemStreams.AllStream, ex.StreamName); - Assert.Equal(Group, ex.GroupName); - } - - public class Fixture : EventStoreClientFixture { - readonly TaskCompletionSource<(SubscriptionDroppedReason, Exception?)> _droppedSource; - PersistentSubscription? _subscription; - - public Fixture() => _droppedSource = new(); - - public Task<(SubscriptionDroppedReason, Exception?)> Dropped => _droppedSource.Task; - - protected override async Task Given() { - await Client.CreateToAllAsync( - Group, - new(), - userCredentials: TestCredentials.Root - ); - - _subscription = await Client.SubscribeToAllAsync( - Group, - delegate { return Task.CompletedTask; }, - (subscription, reason, ex) => _droppedSource.TrySetResult((reason, ex)), - TestCredentials.Root - ); - - // todo: investigate why this test is flaky without this delay - await Task.Delay(500); - } - - protected override Task When() => - Client.UpdateToAllAsync( - Group, - new(), - userCredentials: TestCredentials.Root - ); - - public override Task DisposeAsync() { - _subscription?.Dispose(); - return base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/when_writing_and_filtering_out_events_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/when_writing_and_filtering_out_events_obsolete.cs deleted file mode 100644 index 302ac221a..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/when_writing_and_filtering_out_events_obsolete.cs +++ /dev/null @@ -1,108 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll.Obsolete; - -[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class when_writing_and_filtering_out_events_obsolete - : IClassFixture { - const string Group = "filtering-out-events"; - - readonly Fixture _fixture; - - public when_writing_and_filtering_out_events_obsolete(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public async Task it_should_write_a_check_point() { - await Task.Yield(); - Assert.True(_fixture.SecondCheckPoint > _fixture.FirstCheckPoint); - Assert.Equal( - _fixture.Events.Select(e => e.EventId), - _fixture.AppearedEvents.Select(e => e.Event.EventId) - ); - } - - public class Fixture : EventStoreClientFixture { - readonly TaskCompletionSource _appeared; - readonly List _appearedEvents, _checkPoints; - readonly EventData[] _events; - - PersistentSubscription? _subscription; - - public Fixture() { - _appeared = new(); - _appearedEvents = new(); - _checkPoints = new(); - _events = CreateTestEvents(10).ToArray(); - } - - public Position SecondCheckPoint { get; private set; } - public Position FirstCheckPoint { get; private set; } - public EventData[] Events => _events.ToArray(); - public ResolvedEvent[] AppearedEvents => _appearedEvents.ToArray(); - - protected override async Task Given() { - foreach (var e in _events) - await StreamsClient.AppendToStreamAsync("test-" + Guid.NewGuid(), StreamState.Any, new[] { e }); - - await Client.CreateToAllAsync( - Group, - StreamFilter.Prefix("test"), - new( - checkPointLowerBound: 1, - checkPointUpperBound: 5, - checkPointAfter: TimeSpan.FromSeconds(1), - startFrom: Position.Start - ), - userCredentials: TestCredentials.Root - ); - - _subscription = await Client.SubscribeToAllAsync( - Group, - async (s, e, r, ct) => { - _appearedEvents.Add(e); - - if (_appearedEvents.Count == _events.Length) - _appeared.TrySetResult(true); - - await s.Ack(e); - }, - userCredentials: TestCredentials.Root - ); - - await Task.WhenAll(_appeared.Task, Checkpointed()).WithTimeout(); - - async Task Checkpointed() { - bool firstCheckpointSet = false; - await using var subscription = StreamsClient.SubscribeToStream( - $"$persistentsubscription-$all::{Group}-checkpoint", - FromStream.Start, - userCredentials: TestCredentials.Root); - await foreach (var message in subscription.Messages) { - if (message is not StreamMessage.Event(var resolvedEvent)) { - continue; - } - - if (!firstCheckpointSet) { - FirstCheckPoint = resolvedEvent.Event.Data.ParsePosition(); - firstCheckpointSet = true; - } else { - SecondCheckPoint = resolvedEvent.Event.Data.ParsePosition(); - return; - } - } - } - } - - protected override async Task When() { - foreach (var e in _events) - await StreamsClient.AppendToStreamAsync( - "filtered-out-stream-" + Guid.NewGuid(), - StreamState.Any, - new[] { e } - ); - } - - public override Task DisposeAsync() { - _subscription?.Dispose(); - return base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/when_writing_and_subscribing_to_normal_events_manual_nack_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/when_writing_and_subscribing_to_normal_events_manual_nack_obsolete.cs deleted file mode 100644 index f18d6ad96..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/when_writing_and_subscribing_to_normal_events_manual_nack_obsolete.cs +++ /dev/null @@ -1,68 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll.Obsolete; - -[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class when_writing_and_subscribing_to_normal_events_manual_nack_obsolete - : IClassFixture { - const string Group = nameof(Group); - const int BufferCount = 10; - const int EventWriteCount = BufferCount * 2; - - readonly Fixture _fixture; - - public when_writing_and_subscribing_to_normal_events_manual_nack_obsolete(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public async Task Test() => await _fixture.EventsReceived.WithTimeout(); - - public class Fixture : EventStoreClientFixture { - readonly EventData[] _events; - readonly TaskCompletionSource _eventsReceived; - int _eventReceivedCount; - - PersistentSubscription? _subscription; - - public Fixture() { - _events = CreateTestEvents(EventWriteCount) - .ToArray(); - - _eventsReceived = new(); - } - - public Task EventsReceived => _eventsReceived.Task; - - protected override async Task Given() { - await Client.CreateToAllAsync( - Group, - new(startFrom: Position.Start, resolveLinkTos: true), - userCredentials: TestCredentials.Root - ); - - _subscription = await Client.SubscribeToAllAsync( - Group, - async (subscription, e, retryCount, ct) => { - await subscription.Nack(PersistentSubscriptionNakEventAction.Park, "fail", e); - - if (e.OriginalStreamId.StartsWith("test-") - && Interlocked.Increment(ref _eventReceivedCount) == _events.Length) - _eventsReceived.TrySetResult(true); - }, - (s, r, e) => { - if (e != null) - _eventsReceived.TrySetException(e); - }, - bufferSize: BufferCount, - userCredentials: TestCredentials.Root - ); - } - - protected override async Task When() { - foreach (var e in _events) - await StreamsClient.AppendToStreamAsync("test-" + Guid.NewGuid(), StreamState.Any, new[] { e }); - } - - public override Task DisposeAsync() { - _subscription?.Dispose(); - return base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/can_create_duplicate_name_on_different_streams.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/can_create_duplicate_name_on_different_streams.cs deleted file mode 100644 index d23e7c911..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/can_create_duplicate_name_on_different_streams.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; - -public class can_create_duplicate_name_on_different_streams : IClassFixture { - readonly Fixture _fixture; - - public can_create_duplicate_name_on_different_streams(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public Task the_completion_succeeds() => - _fixture.Client.CreateToStreamAsync("someother", "group3211", new(), userCredentials: TestCredentials.Root); - - public class Fixture : EventStoreClientFixture { - protected override Task Given() => Task.CompletedTask; - - protected override Task When() => - Client.CreateToAllAsync( - "group3211", - new(), - userCredentials: TestCredentials.Root - ); - } -} \ No newline at end of file diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_max_one_client.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_max_one_client.cs deleted file mode 100644 index db2818abf..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_max_one_client.cs +++ /dev/null @@ -1,31 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; - -public class connect_to_existing_with_max_one_client : IClassFixture { - private const string Group = "maxoneclient"; - - private readonly Fixture _fixture; - - public connect_to_existing_with_max_one_client(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public async Task the_second_subscription_fails_to_connect2() { - var ex = await Assert.ThrowsAsync( - () => Task.WhenAll(Subscribe().WithTimeout(), Subscribe().WithTimeout())); - - Assert.Equal(SystemStreams.AllStream, ex.StreamName); - Assert.Equal(Group, ex.GroupName); - return; - - async Task Subscribe() { - await using var subscription = _fixture.Client.SubscribeToAll(Group, userCredentials: TestCredentials.Root); - await subscription.Messages.AnyAsync(); - } - } - - public class Fixture : EventStoreClientFixture { - protected override Task Given() => - Client.CreateToAllAsync(Group, new(maxSubscriberCount: 1), userCredentials: TestCredentials.Root); - - protected override Task When() => Task.CompletedTask; - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_permissions.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_permissions.cs deleted file mode 100644 index e10ed9909..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_permissions.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; - -public class connect_to_existing_with_permissions : IClassFixture { - private const string Group = "connectwithpermissions"; - private readonly Fixture _fixture; - - public connect_to_existing_with_permissions(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public async Task the_subscription_succeeds() { - await using var subscription = - _fixture.Client.SubscribeToAll(Group, userCredentials: TestCredentials.Root); - - Assert.True(await subscription.Messages - .FirstAsync().AsTask().WithTimeout() is PersistentSubscriptionMessage.SubscriptionConfirmation); - } - - public class Fixture : EventStoreClientFixture { - protected override Task Given() => Client.CreateToAllAsync(Group, new(), userCredentials: TestCredentials.Root); - - protected override Task When() => Task.CompletedTask; - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_beginning.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_beginning.cs deleted file mode 100644 index 47e36c620..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_beginning.cs +++ /dev/null @@ -1,52 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; - -public class connect_to_existing_with_start_from_beginning - : IClassFixture { - private const string Group = "startfrombeginning"; - private readonly Fixture _fixture; - - public connect_to_existing_with_start_from_beginning(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public async Task the_subscription_gets_event_zero_as_its_first_event() { - var resolvedEvent = await _fixture.Subscription!.Messages.OfType() - .Select(e => e.ResolvedEvent) - .FirstOrDefaultAsync().AsTask().WithTimeout(); - - Assert.Equal(_fixture.Events[0].Event.EventId, resolvedEvent.Event.EventId); - } - - public class Fixture : EventStoreClientFixture { - public EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult? Subscription { get; private set; } - - public ResolvedEvent[] Events { get; private set; } = Array.Empty(); - - protected override async Task Given() { - //append 10 events to random streams to make sure we have at least 10 events in the transaction file - foreach (var @event in CreateTestEvents(10)) { - await StreamsClient.AppendToStreamAsync(Guid.NewGuid().ToString(), StreamState.NoStream, - new[] { @event }); - } - - Events = await StreamsClient - .ReadAllAsync(Direction.Forwards, Position.Start, 10, userCredentials: TestCredentials.Root) - .ToArrayAsync(); - - await Client.CreateToAllAsync(Group, new(startFrom: Position.Start), userCredentials: TestCredentials.Root); - } - - protected override Task When() { - Subscription = Client.SubscribeToAll(Group, userCredentials: TestCredentials.Root); - - return Task.CompletedTask; - } - - public override async Task DisposeAsync() { - if (Subscription is not null) { - await Subscription.DisposeAsync(); - } - - await base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_not_set.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_not_set.cs deleted file mode 100644 index 17a28d1d6..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_not_set.cs +++ /dev/null @@ -1,44 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; - -public class connect_to_existing_with_start_from_not_set - : IClassFixture { - private const string Group = "startfromend1"; - private readonly Fixture _fixture; - - public connect_to_existing_with_start_from_not_set(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public async Task the_subscription_gets_no_non_system_events() { - await Assert.ThrowsAsync(() => _fixture.Subscription!.Messages - .OfType() - .Where(e => !SystemStreams.IsSystemStream(e.ResolvedEvent.OriginalStreamId)) - .AnyAsync() - .AsTask() - .WithTimeout(TimeSpan.FromMilliseconds(250))); - } - - public class Fixture : EventStoreClientFixture { - public EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult? Subscription { get; private set; } - - protected override async Task Given() { - foreach (var @event in CreateTestEvents(10)) - await StreamsClient.AppendToStreamAsync("non-system-stream-" + Guid.NewGuid(), StreamState.Any, - new[] { @event }); - - await Client.CreateToAllAsync(Group, new(), userCredentials: TestCredentials.Root); - } - - protected override Task When() { - Subscription = Client.SubscribeToAll(Group, userCredentials: TestCredentials.Root); - return Task.CompletedTask; - } - - public override async Task DisposeAsync() { - if (Subscription is not null) { - await Subscription.DisposeAsync(); - } - - await base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_not_set_then_event_written.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_not_set_then_event_written.cs deleted file mode 100644 index bbd8ef16e..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_not_set_then_event_written.cs +++ /dev/null @@ -1,54 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; - -public class connect_to_existing_with_start_from_not_set_then_event_written - : IClassFixture { - private const string Group = "startfromnotset2"; - - private readonly Fixture _fixture; - - public connect_to_existing_with_start_from_not_set_then_event_written(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public async Task the_subscription_gets_the_written_event_as_its_first_non_system_event() { - var resolvedEvent = await _fixture.Subscription!.Messages.OfType() - .Select(e => e.ResolvedEvent) - .Where(resolvedEvent => !SystemStreams.IsSystemStream(resolvedEvent.OriginalStreamId)) - .FirstOrDefaultAsync().AsTask().WithTimeout(); - - Assert.Equal(_fixture.ExpectedEvent.EventId, resolvedEvent.Event.EventId); - Assert.Equal(_fixture.ExpectedStreamId, resolvedEvent.Event.EventStreamId); - } - - public class Fixture : EventStoreClientFixture { - public readonly EventData ExpectedEvent; - public readonly string ExpectedStreamId; - - public EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult? Subscription { get; private set; } - - public Fixture() { - ExpectedEvent = CreateTestEvents(1).First(); - ExpectedStreamId = Guid.NewGuid().ToString(); - } - - protected override async Task Given() { - foreach (var @event in CreateTestEvents(10)) { - await StreamsClient.AppendToStreamAsync("non-system-stream-" + Guid.NewGuid(), StreamState.Any, - new[] { @event }); - } - - await Client.CreateToAllAsync(Group, new(), userCredentials: TestCredentials.Root); - Subscription = Client.SubscribeToAll(Group, userCredentials: TestCredentials.Root); - } - - protected override async Task When() => - await StreamsClient.AppendToStreamAsync(ExpectedStreamId, StreamState.NoStream, new[] { ExpectedEvent }); - - public override async Task DisposeAsync() { - if (Subscription is not null) { - await Subscription.DisposeAsync(); - } - - await base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_set_to_end_position.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_set_to_end_position.cs deleted file mode 100644 index e7f3c9913..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_set_to_end_position.cs +++ /dev/null @@ -1,46 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; - -public class connect_to_existing_with_start_from_set_to_end_position - : IClassFixture { - private const string Group = "startfromend1"; - - private readonly Fixture _fixture; - - public connect_to_existing_with_start_from_set_to_end_position(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public async Task the_subscription_gets_no_non_system_events() { - await Assert.ThrowsAsync(() => _fixture.Subscription!.Messages - .OfType() - .Where(e => !SystemStreams.IsSystemStream(e.ResolvedEvent.OriginalStreamId)) - .AnyAsync() - .AsTask() - .WithTimeout(TimeSpan.FromMilliseconds(250))); - } - - public class Fixture : EventStoreClientFixture { - public EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult? Subscription { get; private set; } - - protected override async Task Given() { - foreach (var @event in CreateTestEvents(10)) { - await StreamsClient.AppendToStreamAsync("non-system-stream-" + Guid.NewGuid(), StreamState.Any, - new[] { @event }); - } - - await Client.CreateToAllAsync(Group, new(startFrom: Position.End), userCredentials: TestCredentials.Root); - } - - protected override Task When() { - Subscription = Client.SubscribeToAll(Group, userCredentials: TestCredentials.Root); - return Task.CompletedTask; - } - - public override async Task DisposeAsync() { - if (Subscription is not null) { - await Subscription.DisposeAsync(); - } - - await base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_set_to_end_position_then_event_written.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_set_to_end_position_then_event_written.cs deleted file mode 100644 index 835c8538c..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_set_to_end_position_then_event_written.cs +++ /dev/null @@ -1,56 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; - -public class - connect_to_existing_with_start_from_set_to_end_position_then_event_written - : IClassFixture { - private const string Group = "startfromnotset2"; - private readonly Fixture _fixture; - - public connect_to_existing_with_start_from_set_to_end_position_then_event_written(Fixture fixture) => - _fixture = fixture; - - [SupportsPSToAll.Fact] - public async Task the_subscription_gets_the_written_event_as_its_first_non_system_event() { - var resolvedEvent = await _fixture.Subscription!.Messages - .OfType() - .Select(e => e.ResolvedEvent) - .Where(resolvedEvent => !SystemStreams.IsSystemStream(resolvedEvent.OriginalStreamId)) - .FirstAsync() - .AsTask() - .WithTimeout(); - Assert.Equal(_fixture.ExpectedEvent.EventId, resolvedEvent.Event.EventId); - Assert.Equal(_fixture.ExpectedStreamId, resolvedEvent.Event.EventStreamId); - } - - public class Fixture : EventStoreClientFixture { - public readonly EventData ExpectedEvent; - public readonly string ExpectedStreamId; - public EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult? Subscription { get; private set; } - - public Fixture() { - ExpectedEvent = CreateTestEvents(1).First(); - ExpectedStreamId = Guid.NewGuid().ToString(); - } - - protected override async Task Given() { - foreach (var @event in CreateTestEvents(10)) { - await StreamsClient.AppendToStreamAsync("non-system-stream-" + Guid.NewGuid(), StreamState.Any, - new[] { @event }); - } - - await Client.CreateToAllAsync(Group, new(startFrom: Position.End), userCredentials: TestCredentials.Root); - Subscription = Client.SubscribeToAll(Group, userCredentials: TestCredentials.Root); - } - - protected override async Task When() => - await StreamsClient.AppendToStreamAsync(ExpectedStreamId, StreamState.NoStream, new[] { ExpectedEvent }); - - public override async Task DisposeAsync() { - if (Subscription is not null) { - await Subscription.DisposeAsync(); - } - - await base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_set_to_invalid_middle_position.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_set_to_invalid_middle_position.cs deleted file mode 100644 index b45330a9e..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_set_to_invalid_middle_position.cs +++ /dev/null @@ -1,48 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; - -public class connect_to_existing_with_start_from_set_to_invalid_middle_position - : IClassFixture { - private const string Group = "startfrominvalid1"; - private readonly Fixture _fixture; - - public connect_to_existing_with_start_from_set_to_invalid_middle_position(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public async Task the_subscription_is_dropped() { - var ex = await Assert.ThrowsAsync(async () => - await _fixture.Enumerator!.MoveNextAsync()); - - Assert.Equal(SystemStreams.AllStream, ex.StreamName); - Assert.Equal(Group, ex.GroupName); - } - - public class Fixture : EventStoreClientFixture { - public EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult? Subscription { get; private set; } - public IAsyncEnumerator? Enumerator { get; private set; } - - protected override async Task Given() { - var invalidPosition = new Position(1L, 1L); - await Client.CreateToAllAsync(Group, new(startFrom: invalidPosition), - userCredentials: TestCredentials.Root); - } - - protected override async Task When() { - Subscription = Client.SubscribeToAll(Group, userCredentials: TestCredentials.Root); - Enumerator = Subscription.Messages.GetAsyncEnumerator(); - - await Enumerator.MoveNextAsync(); - } - - public override async Task DisposeAsync() { - if (Enumerator is not null) { - await Enumerator.DisposeAsync(); - } - - if (Subscription is not null) { - await Subscription.DisposeAsync(); - } - - await base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_set_to_valid_middle_position.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_set_to_valid_middle_position.cs deleted file mode 100644 index daaf838a9..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_set_to_valid_middle_position.cs +++ /dev/null @@ -1,51 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; - -public class connect_to_existing_with_start_from_set_to_valid_middle_position - : IClassFixture { - private const string Group = "startfromvalid"; - - private readonly Fixture _fixture; - - public connect_to_existing_with_start_from_set_to_valid_middle_position(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public async Task the_subscription_gets_the_event_at_the_specified_start_position_as_its_first_event() { - var resolvedEvent = await _fixture.Subscription!.Messages.OfType() - .Select(e => e.ResolvedEvent) - .FirstAsync() - .AsTask() - .WithTimeout(); - Assert.Equal(_fixture.ExpectedEvent.OriginalPosition, resolvedEvent.Event.Position); - Assert.Equal(_fixture.ExpectedEvent.Event.EventId, resolvedEvent.Event.EventId); - Assert.Equal(_fixture.ExpectedEvent.Event.EventStreamId, resolvedEvent.Event.EventStreamId); - } - - public class Fixture : EventStoreClientFixture { - public EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult? Subscription { get; private set; } - public ResolvedEvent ExpectedEvent { get; private set; } - - protected override async Task Given() { - var events = await StreamsClient - .ReadAllAsync(Direction.Forwards, Position.Start, 10, userCredentials: TestCredentials.Root) - .ToArrayAsync(); - - ExpectedEvent = events[events.Length / 2]; //just a random event in the middle of the results - - await Client.CreateToAllAsync(Group, new(startFrom: ExpectedEvent.OriginalPosition), - userCredentials: TestCredentials.Root); - } - - protected override Task When() { - Subscription = Client.SubscribeToAll(Group, userCredentials: TestCredentials.Root); - return Task.CompletedTask; - } - - public override async Task DisposeAsync() { - if (Subscription is not null) { - await Subscription.DisposeAsync(); - } - - await base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_without_permissions.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_without_permissions.cs deleted file mode 100644 index 865281ec0..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_without_permissions.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; - -public class connect_to_existing_without_permissions : IClassFixture { - private readonly Fixture _fixture; - public connect_to_existing_without_permissions(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public Task throws_access_denied() => - Assert.ThrowsAsync( - async () => { - await using var subscription = _fixture.Client.SubscribeToAll("agroupname55"); - await subscription.AnyAsync().AsTask().WithTimeout(); - }); - - public class Fixture : EventStoreClientFixture { - public Fixture() : base(noDefaultCredentials: true) { } - - protected override Task Given() => - Client.CreateToAllAsync("agroupname55", new(), userCredentials: TestCredentials.Root); - - protected override Task When() => Task.CompletedTask; - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_without_read_all_permissions.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_without_read_all_permissions.cs deleted file mode 100644 index 56eccd0fe..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_without_read_all_permissions.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; - -public class connect_to_existing_without_read_all_permissions - : IClassFixture { - private readonly Fixture _fixture; - public connect_to_existing_without_read_all_permissions(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public Task throws_access_denied() => - Assert.ThrowsAsync( - async () => { - await using var subscription = - _fixture.Client.SubscribeToAll("agroupname55", userCredentials: TestCredentials.TestUser1); - await subscription.Messages.AnyAsync().AsTask().WithTimeout(); - }); - - public class Fixture : EventStoreClientFixture { - public Fixture() : base(noDefaultCredentials: true) { } - - protected override Task Given() => - Client.CreateToAllAsync("agroupname55", new(), userCredentials: TestCredentials.Root); - - protected override Task When() => Task.CompletedTask; - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_non_existing_with_permissions.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_non_existing_with_permissions.cs deleted file mode 100644 index 40a541040..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_non_existing_with_permissions.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; - -public class connect_to_non_existing_with_permissions - : IClassFixture { - private const string Group = "foo"; - - private readonly Fixture _fixture; - - public connect_to_non_existing_with_permissions(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public async Task throws_persistent_subscription_not_found() { - await using var subscription = _fixture.Client.SubscribeToAll(Group, userCredentials: TestCredentials.Root); - - Assert.True(await subscription.Messages.OfType().AnyAsync() - .AsTask() - .WithTimeout()); - } - - public class Fixture : EventStoreClientFixture { - protected override Task Given() => Task.CompletedTask; - protected override Task When() => Task.CompletedTask; - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_with_retries.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_with_retries.cs deleted file mode 100644 index ea4236278..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_with_retries.cs +++ /dev/null @@ -1,49 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; - -public class connect_with_retries : IClassFixture { - private const string Group = "retries"; - private readonly Fixture _fixture; - - public connect_with_retries(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public async Task events_are_retried_until_success() { - var retryCount = await _fixture.Subscription!.Messages.OfType() - .SelectAwait(async e => { - if (e.RetryCount > 4) { - await _fixture.Subscription.Ack(e.ResolvedEvent); - } else { - await _fixture.Subscription.Nack(PersistentSubscriptionNakEventAction.Retry, - "Not yet tried enough times", e.ResolvedEvent); - } - - return e.RetryCount; - }) - .Where(retryCount => retryCount > 4) - .FirstOrDefaultAsync() - .AsTask() - .WithTimeout(); - - Assert.Equal(5, retryCount); - } - - public class Fixture : EventStoreClientFixture { - public EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult? Subscription { get; private set; } - - protected override async Task Given() { - await Client.CreateToAllAsync(Group, new(startFrom: Position.Start), userCredentials: TestCredentials.Root); - - Subscription = Client.SubscribeToAll(Group, userCredentials: TestCredentials.Root); - } - - protected override Task When() => Task.CompletedTask; - - public override async Task DisposeAsync() { - if (Subscription is not null) { - await Subscription.DisposeAsync(); - } - - await base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/create_after_deleting_the_same.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/create_after_deleting_the_same.cs deleted file mode 100644 index a3c673f48..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/create_after_deleting_the_same.cs +++ /dev/null @@ -1,32 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; - -public class create_after_deleting_the_same : IClassFixture { - readonly Fixture _fixture; - - public create_after_deleting_the_same(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public async Task the_completion_succeeds() => - await _fixture.Client.CreateToAllAsync( - "existing", - new(), - userCredentials: TestCredentials.Root - ); - - public class Fixture : EventStoreClientFixture { - protected override Task Given() => Task.CompletedTask; - - protected override async Task When() { - await Client.CreateToAllAsync( - "existing", - new(), - userCredentials: TestCredentials.Root - ); - - await Client.DeleteToAllAsync( - "existing", - userCredentials: TestCredentials.Root - ); - } - } -} \ No newline at end of file diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/create_duplicate.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/create_duplicate.cs deleted file mode 100644 index abe410c26..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/create_duplicate.cs +++ /dev/null @@ -1,33 +0,0 @@ -using Grpc.Core; - -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; - -public class create_duplicate : IClassFixture { - readonly Fixture _fixture; - - public create_duplicate(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public async Task the_completion_fails() { - var ex = await Assert.ThrowsAsync( - () => _fixture.Client.CreateToAllAsync( - "group32", - new(), - userCredentials: TestCredentials.Root - ) - ); - - Assert.Equal(StatusCode.AlreadyExists, ex.StatusCode); - } - - public class Fixture : EventStoreClientFixture { - protected override Task Given() => Task.CompletedTask; - - protected override Task When() => - Client.CreateToAllAsync( - "group32", - new(), - userCredentials: TestCredentials.Root - ); - } -} \ No newline at end of file diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/create_filtered.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/create_filtered.cs deleted file mode 100644 index 8eff2a425..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/create_filtered.cs +++ /dev/null @@ -1,29 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; - -public class create_filtered : IClassFixture { - readonly Fixture _fixture; - - public create_filtered(Fixture fixture) => _fixture = fixture; - - public static IEnumerable FilterCases() => Filters.All.Select(filter => new object[] { filter }); - - [SupportsPSToAll.Theory] - [MemberData(nameof(FilterCases))] - public async Task the_completion_succeeds(string filterName) { - var streamPrefix = _fixture.GetStreamName(); - var (getFilter, _) = Filters.GetFilter(filterName); - var filter = getFilter(streamPrefix); - - await _fixture.Client.CreateToAllAsync( - filterName, - filter, - new(), - userCredentials: TestCredentials.Root - ); - } - - public class Fixture : EventStoreClientFixture { - protected override Task Given() => Task.CompletedTask; - protected override Task When() => Task.CompletedTask; - } -} \ No newline at end of file diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/create_on_all_stream.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/create_on_all_stream.cs deleted file mode 100644 index 92aa86e1d..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/create_on_all_stream.cs +++ /dev/null @@ -1,21 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; - -public class create_on_all_stream : IClassFixture { - readonly Fixture _fixture; - - public create_on_all_stream(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public Task the_completion_succeeds() => _fixture.Client.CreateToAllAsync("existing", new(), userCredentials: TestCredentials.Root); - - [SupportsPSToAll.Fact] - public Task throws_argument_exception_if_wrong_start_from_type_passed() => - Assert.ThrowsAsync( - () => _fixture.Client.CreateToAllAsync("existing", new(startFrom: StreamPosition.End), userCredentials: TestCredentials.Root) - ); - - public class Fixture : EventStoreClientFixture { - protected override Task Given() => Task.CompletedTask; - protected override Task When() => Task.CompletedTask; - } -} \ No newline at end of file diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/create_persistent_subscription_with_dont_timeout.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/create_persistent_subscription_with_dont_timeout.cs deleted file mode 100644 index f44c830f0..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/create_persistent_subscription_with_dont_timeout.cs +++ /dev/null @@ -1,21 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; - -public class create_with_dont_timeout - : IClassFixture { - readonly Fixture _fixture; - - public create_with_dont_timeout(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public Task the_subscription_is_created_without_error() => - _fixture.Client.CreateToAllAsync( - "dont-timeout", - new(messageTimeout: TimeSpan.Zero), - userCredentials: TestCredentials.Root - ); - - public class Fixture : EventStoreClientFixture { - protected override Task Given() => Task.CompletedTask; - protected override Task When() => Task.CompletedTask; - } -} \ No newline at end of file diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/create_with_commit_position_equal_to_last_indexed_position.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/create_with_commit_position_equal_to_last_indexed_position.cs deleted file mode 100644 index e6db02901..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/create_with_commit_position_equal_to_last_indexed_position.cs +++ /dev/null @@ -1,32 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; - -public class create_with_commit_position_equal_to_last_indexed_position : IClassFixture { - readonly Fixture _fixture; - - public create_with_commit_position_equal_to_last_indexed_position(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public async Task the_completion_succeeds() => - await _fixture.Client.CreateToAllAsync( - "group57", - new(startFrom: new Position(_fixture.LastCommitPosition, _fixture.LastCommitPosition)), - userCredentials: TestCredentials.Root - ); - - public class Fixture : EventStoreClientFixture { - public ulong LastCommitPosition; - - protected override async Task Given() { - var lastEvent = await StreamsClient.ReadAllAsync( - Direction.Backwards, - Position.End, - 1, - userCredentials: TestCredentials.Root - ).FirstAsync(); - - LastCommitPosition = lastEvent.OriginalPosition?.CommitPosition ?? throw new(); - } - - protected override Task When() => Task.CompletedTask; - } -} \ No newline at end of file diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/create_with_commit_position_larger_than_last_indexed_position.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/create_with_commit_position_larger_than_last_indexed_position.cs deleted file mode 100644 index 7769f8b70..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/create_with_commit_position_larger_than_last_indexed_position.cs +++ /dev/null @@ -1,41 +0,0 @@ -using Grpc.Core; - -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; - -public class create_with_commit_position_larger_than_last_indexed_position - : IClassFixture { - readonly Fixture _fixture; - - public create_with_commit_position_larger_than_last_indexed_position(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public async Task fails() { - var ex = await Assert.ThrowsAsync( - () => - _fixture.Client.CreateToAllAsync( - "group57", - new(startFrom: new Position(_fixture.LastCommitPosition + 1, _fixture.LastCommitPosition)), - userCredentials: TestCredentials.Root - ) - ); - - Assert.Equal(StatusCode.Internal, ex.StatusCode); - } - - public class Fixture : EventStoreClientFixture { - public ulong LastCommitPosition; - - protected override async Task Given() { - var lastEvent = await StreamsClient.ReadAllAsync( - Direction.Backwards, - Position.End, - 1, - userCredentials: TestCredentials.Root - ).FirstAsync(); - - LastCommitPosition = lastEvent.OriginalPosition?.CommitPosition ?? throw new(); - } - - protected override Task When() => Task.CompletedTask; - } -} \ No newline at end of file diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/create_with_prepare_position_larger_than_commit_position.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/create_with_prepare_position_larger_than_commit_position.cs deleted file mode 100644 index 434e90d7a..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/create_with_prepare_position_larger_than_commit_position.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; - -public class create_with_prepare_position_larger_than_commit_position - : IClassFixture { - readonly Fixture _fixture; - - public create_with_prepare_position_larger_than_commit_position(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public Task fails_with_argument_out_of_range_exception() => - Assert.ThrowsAsync( - () => - _fixture.Client.CreateToAllAsync( - "group57", - new(startFrom: new Position(0, 1)), - userCredentials: TestCredentials.Root - ) - ); - - public class Fixture : EventStoreClientFixture { - protected override Task Given() => Task.CompletedTask; - protected override Task When() => Task.CompletedTask; - } -} \ No newline at end of file diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/create_without_permissions.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/create_without_permissions.cs deleted file mode 100644 index 71d87a128..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/create_without_permissions.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; - -public class create_without_permissions - : IClassFixture { - readonly Fixture _fixture; - - public create_without_permissions(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public Task the_completion_fails_with_access_denied() => - Assert.ThrowsAsync( - () => - _fixture.Client.CreateToAllAsync( - "group57", - new() - ) - ); - - public class Fixture : EventStoreClientFixture { - public Fixture() : base(noDefaultCredentials: true) { } - - protected override Task Given() => Task.CompletedTask; - protected override Task When() => Task.CompletedTask; - } -} \ No newline at end of file diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/deleting_existing_with_permissions.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/deleting_existing_with_permissions.cs deleted file mode 100644 index a81180092..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/deleting_existing_with_permissions.cs +++ /dev/null @@ -1,26 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; - -public class deleting_existing_with_permissions - : IClassFixture { - readonly Fixture _fixture; - - public deleting_existing_with_permissions(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public Task the_delete_of_group_succeeds() => - _fixture.Client.DeleteToAllAsync( - "groupname123", - userCredentials: TestCredentials.Root - ); - - public class Fixture : EventStoreClientFixture { - protected override Task Given() => Task.CompletedTask; - - protected override Task When() => - Client.CreateToAllAsync( - "groupname123", - new(), - userCredentials: TestCredentials.Root - ); - } -} \ No newline at end of file diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/deleting_existing_with_subscriber.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/deleting_existing_with_subscriber.cs deleted file mode 100644 index 4db83e251..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/deleting_existing_with_subscriber.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; - -public class deleting_existing_with_subscriber : IClassFixture { - public const string Group = "groupname123"; - private readonly Fixture _fixture; - - public deleting_existing_with_subscriber(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public async Task the_subscription_is_dropped_with_not_found() { - await using var subscription = _fixture.Client.SubscribeToAll(Group, userCredentials: TestCredentials.Root); - - Assert.True(await subscription.Messages.OfType().AnyAsync() - .AsTask() - .WithTimeout()); - } - - public class Fixture : EventStoreClientFixture { - protected override Task Given() => Client.CreateToAllAsync(Group, new(), userCredentials: TestCredentials.Root); - - protected override Task When() => Client.DeleteToAllAsync(Group, userCredentials: TestCredentials.Root); - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/deleting_filtered.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/deleting_filtered.cs deleted file mode 100644 index 7a461ebf9..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/deleting_filtered.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; - -public class deleting_filtered - : IClassFixture { - const string Group = "to-be-deleted"; - readonly Fixture _fixture; - - public deleting_filtered(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public async Task the_completion_succeeds() => await _fixture.Client.DeleteToAllAsync(Group, userCredentials: TestCredentials.Root); - - public class Fixture : EventStoreClientFixture { - protected override async Task Given() => - await Client.CreateToAllAsync( - Group, - EventTypeFilter.Prefix("prefix-filter-"), - new(), - userCredentials: TestCredentials.Root - ); - - protected override Task When() => Task.CompletedTask; - } -} \ No newline at end of file diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/deleting_nonexistent.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/deleting_nonexistent.cs deleted file mode 100644 index 804850e89..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/deleting_nonexistent.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; - -public class deleting_nonexistent - : IClassFixture { - readonly Fixture _fixture; - - public deleting_nonexistent(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public async Task the_delete_fails_with_argument_exception() => - await Assert.ThrowsAsync( - () => _fixture.Client.DeleteToAllAsync(Guid.NewGuid().ToString(), userCredentials: TestCredentials.Root) - ); - - public class Fixture : EventStoreClientFixture { - protected override Task Given() => Task.CompletedTask; - protected override Task When() => Task.CompletedTask; - } -} \ No newline at end of file diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/deleting_without_permissions.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/deleting_without_permissions.cs deleted file mode 100644 index 13e1339a9..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/deleting_without_permissions.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; - -public class deleting_without_permissions - : IClassFixture { - readonly Fixture _fixture; - - public deleting_without_permissions(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public async Task the_delete_fails_with_access_denied() => - await Assert.ThrowsAsync(() => _fixture.Client.DeleteToAllAsync(Guid.NewGuid().ToString())); - - public class Fixture : EventStoreClientFixture { - public Fixture() : base(noDefaultCredentials: true) { } - - protected override Task Given() => Task.CompletedTask; - protected override Task When() => Task.CompletedTask; - } -} \ No newline at end of file diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/get_info.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/get_info.cs deleted file mode 100644 index ab748a5d5..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/get_info.cs +++ /dev/null @@ -1,181 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; - -public class get_info : IClassFixture { - private const string GroupName = nameof(get_info); - - private static readonly PersistentSubscriptionSettings Settings = new( - resolveLinkTos: true, - startFrom: Position.Start, - extraStatistics: true, - messageTimeout: TimeSpan.FromSeconds(9), - maxRetryCount: 11, - liveBufferSize: 303, - readBatchSize: 30, - historyBufferSize: 909, - checkPointAfter: TimeSpan.FromSeconds(1), - checkPointLowerBound: 1, - checkPointUpperBound: 1, - maxSubscriberCount: 500, - consumerStrategyName: SystemConsumerStrategies.Pinned); - - private readonly Fixture _fixture; - - public get_info(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task throws_when_not_supported() { - if (SupportsPSToAll.No) { - await Assert.ThrowsAsync(async () => - await _fixture.Client.GetInfoToAllAsync(GroupName, userCredentials: TestCredentials.Root)); - } - } - - [SupportsPSToAll.Fact] - public async Task returns_expected_result() { - var result = await _fixture.Client.GetInfoToAllAsync(GroupName, userCredentials: TestCredentials.Root); - - Assert.Equal("$all", result.EventSource); - Assert.Equal(GroupName, result.GroupName); - Assert.Equal("Live", result.Status); - - Assert.NotNull(Settings.StartFrom); - Assert.True(result.Stats.TotalItems > 0); - Assert.True(result.Stats.OutstandingMessagesCount > 0); - Assert.True(result.Stats.AveragePerSecond >= 0); - Assert.True(result.Stats.ParkedMessageCount > 0); - Assert.True(result.Stats.AveragePerSecond >= 0); - Assert.True(result.Stats.CountSinceLastMeasurement >= 0); - Assert.True(result.Stats.TotalInFlightMessages >= 0); - Assert.NotNull(result.Stats.LastKnownEventPosition); - Assert.NotNull(result.Stats.LastCheckpointedEventPosition); - Assert.True(result.Stats.LiveBufferCount >= 0); - - Assert.NotNull(result.Connections); - Assert.NotEmpty(result.Connections); - - var connection = result.Connections.First(); - Assert.NotNull(connection.From); - Assert.Equal(TestCredentials.Root.Username, connection.Username); - Assert.NotEmpty(connection.ConnectionName); - Assert.True(connection.AverageItemsPerSecond >= 0); - Assert.True(connection.TotalItems > 0); - Assert.True(connection.CountSinceLastMeasurement >= 0); - Assert.True(connection.AvailableSlots >= 0); - Assert.True(connection.InFlightMessages >= 0); - Assert.NotNull(connection.ExtraStatistics); - Assert.NotEmpty(connection.ExtraStatistics); - - AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.Highest); - AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.Mean); - AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.Median); - AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.Fastest); - AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.Quintile1); - AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.Quintile2); - AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.Quintile3); - AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.Quintile4); - AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.Quintile5); - AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.NinetyPercent); - AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.NinetyFivePercent); - AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.NinetyNinePercent); - AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.NinetyNinePointFivePercent); - AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.NinetyNinePointNinePercent); - - Assert.NotNull(result.Settings); - Assert.Equal(Settings.StartFrom, result.Settings!.StartFrom); - Assert.Equal(Settings.ResolveLinkTos, result.Settings!.ResolveLinkTos); - Assert.Equal(Settings.ExtraStatistics, result.Settings!.ExtraStatistics); - Assert.Equal(Settings.MessageTimeout, result.Settings!.MessageTimeout); - Assert.Equal(Settings.MaxRetryCount, result.Settings!.MaxRetryCount); - Assert.Equal(Settings.LiveBufferSize, result.Settings!.LiveBufferSize); - Assert.Equal(Settings.ReadBatchSize, result.Settings!.ReadBatchSize); - Assert.Equal(Settings.HistoryBufferSize, result.Settings!.HistoryBufferSize); - Assert.Equal(Settings.CheckPointAfter, result.Settings!.CheckPointAfter); - Assert.Equal(Settings.CheckPointLowerBound, result.Settings!.CheckPointLowerBound); - Assert.Equal(Settings.CheckPointUpperBound, result.Settings!.CheckPointUpperBound); - Assert.Equal(Settings.MaxSubscriberCount, result.Settings!.MaxSubscriberCount); - Assert.Equal(Settings.ConsumerStrategyName, result.Settings!.ConsumerStrategyName); - } - - [SupportsPSToAll.Fact] - public async Task throws_with_non_existing_subscription() => - await Assert.ThrowsAsync( - async () => await _fixture.Client.GetInfoToAllAsync("NonExisting", userCredentials: TestCredentials.Root)); - - [SupportsPSToAll.Fact] - public async Task throws_with_no_credentials() => - await Assert.ThrowsAsync(async () => - await _fixture.Client.GetInfoToAllAsync("NonExisting")); - - [SupportsPSToAll.Fact] - public async Task throws_with_non_existing_user() => - await Assert.ThrowsAsync(async () => - await _fixture.Client.GetInfoToAllAsync("NonExisting", userCredentials: TestCredentials.TestBadUser)); - - [SupportsPSToAll.Fact] - public async Task returns_result_with_normal_user_credentials() { - var result = await _fixture.Client.GetInfoToAllAsync(GroupName, userCredentials: TestCredentials.TestUser1); - - Assert.Equal("$all", result.EventSource); - } - - private void AssertKeyAndValue(IDictionary items, string key) { - Assert.True(items.ContainsKey(key)); - Assert.True(items[key] > 0); - } - - public class Fixture : EventStoreClientFixture { - private EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult? _subscription; - private IAsyncEnumerator? _enumerator; - public Fixture() : base(noDefaultCredentials: true) { } - - protected override async Task Given() { - if (SupportsPSToAll.No) - return; - - await Client.CreateToAllAsync(GroupName, get_info.Settings, userCredentials: TestCredentials.Root); - - foreach (var eventData in CreateTestEvents(20)) { - await StreamsClient.AppendToStreamAsync($"test-{Guid.NewGuid():n}", StreamState.NoStream, - new[] { eventData }, userCredentials: TestCredentials.Root); - } - } - - protected override async Task When() { - if (SupportsPSToAll.No) - return; - - var counter = 0; - - _subscription = Client.SubscribeToAll(GroupName, userCredentials: TestCredentials.Root); - _enumerator = _subscription.Messages.GetAsyncEnumerator(); - - while (await _enumerator.MoveNextAsync()) { - if (_enumerator.Current is not PersistentSubscriptionMessage.Event (var resolvedEvent, _)) { - continue; - } - - counter++; - - if (counter == 1) { - await _subscription.Nack(PersistentSubscriptionNakEventAction.Park, "Test", resolvedEvent); - } - - if (counter > 10) { - return; - } - } - } - - public override async Task DisposeAsync() { - if (_enumerator is not null) { - await _enumerator.DisposeAsync(); - } - - if (_subscription is not null) { - await _subscription.DisposeAsync(); - } - - await base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/happy_case_catching_up_to_link_to_events_manual_ack.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/happy_case_catching_up_to_link_to_events_manual_ack.cs deleted file mode 100644 index dbcc44b43..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/happy_case_catching_up_to_link_to_events_manual_ack.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System.Text; - -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; - -public class happy_case_catching_up_to_link_to_events_manual_ack - : IClassFixture { - private const string Group = nameof(Group); - private const int BufferCount = 10; - private const int EventWriteCount = BufferCount * 2; - - private readonly Fixture _fixture; - - public happy_case_catching_up_to_link_to_events_manual_ack(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public async Task Test() { - await _fixture.Subscription!.Messages.OfType() - .Take(_fixture.Events.Length) - .ForEachAwaitAsync(e => _fixture.Subscription.Ack(e.ResolvedEvent)) - .WithTimeout(); - - } - - public class Fixture : EventStoreClientFixture { - public readonly EventData[] Events; - - public EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult? Subscription { get; private set; } - - public Fixture() { - Events = CreateTestEvents(EventWriteCount) - .Select((e, i) => new EventData(e.EventId, SystemEventTypes.LinkTo, Encoding.UTF8.GetBytes($"{i}@test"), - contentType: Constants.Metadata.ContentTypes.ApplicationOctetStream)) - .ToArray(); - } - - protected override async Task Given() { - foreach (var e in Events) { - await StreamsClient.AppendToStreamAsync("test-" + Guid.NewGuid(), StreamState.Any, new[] { e }); - } - - await Client.CreateToAllAsync(Group, new(startFrom: Position.Start, resolveLinkTos: true), - userCredentials: TestCredentials.Root); - - Subscription = Client.SubscribeToAll(Group, bufferSize: BufferCount, userCredentials: TestCredentials.Root); - } - - protected override Task When() => Task.CompletedTask; - - public override async Task DisposeAsync() { - if (Subscription is not null) { - await Subscription.DisposeAsync(); - } - - await base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/happy_case_catching_up_to_normal_events_manual_ack.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/happy_case_catching_up_to_normal_events_manual_ack.cs deleted file mode 100644 index 05612443d..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/happy_case_catching_up_to_normal_events_manual_ack.cs +++ /dev/null @@ -1,51 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; - -public class happy_case_catching_up_to_normal_events_manual_ack - : IClassFixture { - private const string Group = nameof(Group); - private const int BufferCount = 10; - private const int EventWriteCount = BufferCount * 2; - - private readonly Fixture _fixture; - - public happy_case_catching_up_to_normal_events_manual_ack(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public async Task Test() { - await _fixture.Subscription!.Messages.OfType() - .Take(_fixture.Events.Length) - .ForEachAwaitAsync(e => _fixture.Subscription.Ack(e.ResolvedEvent)) - .WithTimeout(); - } - - - public class Fixture : EventStoreClientFixture { - public readonly EventData[] Events; - - public EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult? Subscription { get; private set; } - - public Fixture() { - Events = CreateTestEvents(EventWriteCount).ToArray(); - } - - protected override async Task Given() { - foreach (var e in Events) - await StreamsClient.AppendToStreamAsync("test-" + Guid.NewGuid(), StreamState.Any, new[] { e }); - - await Client.CreateToAllAsync(Group, new(startFrom: Position.Start, resolveLinkTos: true), - userCredentials: TestCredentials.Root); - - Subscription = Client.SubscribeToAll(Group, bufferSize: BufferCount, userCredentials: TestCredentials.Root); - } - - protected override Task When() => Task.CompletedTask; - - public override async Task DisposeAsync() { - if (Subscription is not null) { - await Subscription.DisposeAsync(); - } - - await base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/happy_case_filtered.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/happy_case_filtered.cs deleted file mode 100644 index 9d89fddb1..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/happy_case_filtered.cs +++ /dev/null @@ -1,54 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; - -public class happy_case_filtered : IClassFixture { - private readonly Fixture _fixture; - - public happy_case_filtered(Fixture fixture) => _fixture = fixture; - - public static IEnumerable FilterCases() => Filters.All.Select(filter => new object[] { filter }); - - [SupportsPSToAll.Theory] - [MemberData(nameof(FilterCases))] - public async Task reads_all_existing_filtered_events(string filterName) { - var streamPrefix = $"{filterName}-{_fixture.GetStreamName()}"; - var (getFilter, prepareEvent) = Filters.GetFilter(filterName); - var filter = getFilter(streamPrefix); - - var appearedEvents = new List(); - var events = _fixture.CreateTestEvents(20).Select(e => prepareEvent(streamPrefix, e)).ToArray(); - - foreach (var e in events) { - await _fixture.StreamsClient.AppendToStreamAsync($"{streamPrefix}_{Guid.NewGuid():n}", StreamState.NoStream, - new[] { e }); - } - - await _fixture.Client.CreateToAllAsync(filterName, filter, new(startFrom: Position.Start), - userCredentials: TestCredentials.Root); - - await using var subscription = - _fixture.Client.SubscribeToAll(filterName, userCredentials: TestCredentials.Root); - await subscription.Messages - .OfType() - .Take(events.Length) - .ForEachAwaitAsync(async e => { - var (resolvedEvent, _) = e; - appearedEvents.Add(resolvedEvent.Event); - await subscription.Ack(resolvedEvent); - }) - .WithTimeout(); - - Assert.Equal(events.Select(x => x.EventId), appearedEvents.Select(x => x.EventId)); - } - - public class Fixture : EventStoreClientFixture { - protected override async Task Given() { - await StreamsClient.AppendToStreamAsync(Guid.NewGuid().ToString(), StreamState.NoStream, - CreateTestEvents(256)); - - await StreamsClient.SetStreamMetadataAsync(SystemStreams.AllStream, StreamState.Any, - new(acl: new(SystemRoles.All)), userCredentials: TestCredentials.Root); - } - - protected override Task When() => Task.CompletedTask; - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/happy_case_filtered_with_start_from_set.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/happy_case_filtered_with_start_from_set.cs deleted file mode 100644 index 065782783..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/happy_case_filtered_with_start_from_set.cs +++ /dev/null @@ -1,62 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; - -public class happy_case_filtered_with_start_from_set : IClassFixture { - private readonly Fixture _fixture; - - public happy_case_filtered_with_start_from_set(Fixture fixture) => _fixture = fixture; - - public static IEnumerable FilterCases() => Filters.All.Select(filter => new object[] { filter }); - - [SupportsPSToAll.Theory] - [MemberData(nameof(FilterCases))] - public async Task reads_all_existing_filtered_events_from_specified_start(string filterName) { - var streamPrefix = $"{filterName}-{_fixture.GetStreamName()}"; - var (getFilter, prepareEvent) = Filters.GetFilter(filterName); - var filter = getFilter(streamPrefix); - - var events = _fixture.CreateTestEvents(20).Select(e => prepareEvent(streamPrefix, e)).ToArray(); - var eventsToSkip = events.Take(10).ToArray(); - var eventsToCapture = events.Skip(10).ToArray(); - - IWriteResult? eventToCaptureResult = null; - - foreach (var e in eventsToSkip) { - await _fixture.StreamsClient.AppendToStreamAsync($"{streamPrefix}_{Guid.NewGuid():n}", StreamState.NoStream, - new[] { e }); - } - - foreach (var e in eventsToCapture) { - var result = await _fixture.StreamsClient.AppendToStreamAsync($"{streamPrefix}_{Guid.NewGuid():n}", - StreamState.NoStream, new[] { e }); - - eventToCaptureResult ??= result; - } - - await _fixture.Client.CreateToAllAsync(filterName, filter, new(startFrom: eventToCaptureResult!.LogPosition), - userCredentials: TestCredentials.Root); - - await using var subscription = - _fixture.Client.SubscribeToAll(filterName, userCredentials: TestCredentials.Root); - - var appearedEvents = await subscription.Messages.OfType() - .Take(10) - .Select(e => e.ResolvedEvent.Event) - .ToArrayAsync() - .AsTask() - .WithTimeout(); - - Assert.Equal(eventsToCapture.Select(x => x.EventId), appearedEvents.Select(x => x.EventId)); - } - - public class Fixture : EventStoreClientFixture { - protected override async Task Given() { - await StreamsClient.AppendToStreamAsync(Guid.NewGuid().ToString(), StreamState.NoStream, - CreateTestEvents(256)); - - await StreamsClient.SetStreamMetadataAsync(SystemStreams.AllStream, StreamState.Any, - new(acl: new(SystemRoles.All)), userCredentials: TestCredentials.Root); - } - - protected override Task When() => Task.CompletedTask; - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/happy_case_writing_and_subscribing_to_normal_events_manual_ack.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/happy_case_writing_and_subscribing_to_normal_events_manual_ack.cs deleted file mode 100644 index 100e49f27..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/happy_case_writing_and_subscribing_to_normal_events_manual_ack.cs +++ /dev/null @@ -1,57 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; - -public class happy_case_writing_and_subscribing_to_normal_events_manual_ack - : IClassFixture { - private const string Group = nameof(Group); - private const int BufferCount = 10; - private const int EventWriteCount = BufferCount * 2; - - private readonly Fixture _fixture; - - public happy_case_writing_and_subscribing_to_normal_events_manual_ack(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public async Task Test() { - await _fixture.Subscription!.Messages.OfType() - .SelectAwait(async e => { - await _fixture.Subscription.Ack(e.ResolvedEvent); - return e; - }) - .Where(e => e.ResolvedEvent.OriginalStreamId.StartsWith("test-")) - .Take(_fixture.Events.Length) - .ToArrayAsync() - .AsTask() - .WithTimeout(); - } - - public class Fixture : EventStoreClientFixture { - public readonly EventData[] Events; - - public EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult? Subscription { get; private set; } - - public Fixture() { - Events = CreateTestEvents(EventWriteCount).ToArray(); - } - - protected override async Task Given() { - await Client.CreateToAllAsync(Group, new(startFrom: Position.End, resolveLinkTos: true), - userCredentials: TestCredentials.Root); - - Subscription = Client.SubscribeToAll(Group, bufferSize: BufferCount, userCredentials: TestCredentials.Root); - } - - protected override async Task When() { - foreach (var e in Events) { - await StreamsClient.AppendToStreamAsync("test-" + Guid.NewGuid(), StreamState.Any, new[] { e }); - } - } - - public override async Task DisposeAsync() { - if (Subscription is not null) { - await Subscription.DisposeAsync(); - } - - await base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/list_with_persistent_subscriptions.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/list_with_persistent_subscriptions.cs deleted file mode 100644 index 443a9d04e..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/list_with_persistent_subscriptions.cs +++ /dev/null @@ -1,82 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; - -public class list_with_persistent_subscriptions : IClassFixture { - const int AllStreamSubscriptionCount = 3; - const int StreamSubscriptionCount = 4; - const string GroupName = nameof(list_with_persistent_subscriptions); - const string StreamName = nameof(list_with_persistent_subscriptions); - - readonly Fixture _fixture; - - public list_with_persistent_subscriptions(Fixture fixture) => _fixture = fixture; - - int TotalSubscriptionCount => - SupportsPSToAll.No - ? StreamSubscriptionCount - : StreamSubscriptionCount + AllStreamSubscriptionCount; - - [Fact] - public async Task throws_when_not_supported() { - if (SupportsPSToAll.No) - await Assert.ThrowsAsync(async () => { await _fixture.Client.ListToAllAsync(userCredentials: TestCredentials.Root); }); - } - - [SupportsPSToAll.Fact] - public async Task returns_subscriptions_to_all_stream() { - var result = (await _fixture.Client.ListToAllAsync(userCredentials: TestCredentials.Root)).ToList(); - Assert.Equal(AllStreamSubscriptionCount, result.Count); - Assert.All(result, s => Assert.Equal("$all", s.EventSource)); - } - - [Fact] - public async Task returns_all_subscriptions() { - var result = (await _fixture.Client.ListAllAsync(userCredentials: TestCredentials.Root)).ToList(); - Assert.Equal(TotalSubscriptionCount, result.Count()); - } - - [SupportsPSToAll.Fact] - public async Task throws_with_no_credentials() => - await Assert.ThrowsAsync( - async () => - await _fixture.Client.ListToAllAsync() - ); - - [SupportsPSToAll.Fact] - public async Task throws_with_non_existing_user() => - await Assert.ThrowsAsync( - async () => - await _fixture.Client.ListToAllAsync(userCredentials: TestCredentials.TestBadUser) - ); - - [SupportsPSToAll.Fact] - public async Task returns_result_with_normal_user_credentials() { - var result = await _fixture.Client.ListToAllAsync(userCredentials: TestCredentials.TestUser1); - Assert.Equal(AllStreamSubscriptionCount, result.Count()); - } - - public class Fixture : EventStoreClientFixture { - public Fixture() : base(skipPSWarmUp: true, noDefaultCredentials: true) { } - - protected override async Task Given() { - for (var i = 0; i < StreamSubscriptionCount; i++) - await Client.CreateToStreamAsync( - StreamName, - GroupName + i, - new(), - userCredentials: TestCredentials.Root - ); - - if (SupportsPSToAll.No) - return; - - for (var i = 0; i < AllStreamSubscriptionCount; i++) - await Client.CreateToAllAsync( - GroupName + i, - new(), - userCredentials: TestCredentials.Root - ); - } - - protected override Task When() => Task.CompletedTask; - } -} \ No newline at end of file diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/list_without_persistent_subscriptions.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/list_without_persistent_subscriptions.cs deleted file mode 100644 index 0a9c435be..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/list_without_persistent_subscriptions.cs +++ /dev/null @@ -1,26 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; - -public class list_without_persistent_subscriptions : IClassFixture { - readonly Fixture _fixture; - - public list_without_persistent_subscriptions(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task throws() { - if (SupportsPSToAll.No) { - await Assert.ThrowsAsync(async () => { await _fixture.Client.ListToAllAsync(userCredentials: TestCredentials.Root); }); - - return; - } - - await Assert.ThrowsAsync( - async () => - await _fixture.Client.ListToAllAsync(userCredentials: TestCredentials.Root) - ); - } - - public class Fixture : EventStoreClientFixture { - protected override Task Given() => Task.CompletedTask; - protected override Task When() => Task.CompletedTask; - } -} \ No newline at end of file diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/replay_parked.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/replay_parked.cs deleted file mode 100644 index 501500abd..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/replay_parked.cs +++ /dev/null @@ -1,90 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; - -public class replay_parked : IClassFixture { - const string GroupName = nameof(replay_parked); - - readonly Fixture _fixture; - - public replay_parked(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task throws_when_not_supported() { - if (SupportsPSToAll.No) - await Assert.ThrowsAsync( - async () => { - await _fixture.Client.ReplayParkedMessagesToAllAsync( - GroupName, - userCredentials: TestCredentials.Root - ); - } - ); - } - - [SupportsPSToAll.Fact] - public async Task does_not_throw() { - await _fixture.Client.ReplayParkedMessagesToAllAsync( - GroupName, - userCredentials: TestCredentials.Root - ); - - await _fixture.Client.ReplayParkedMessagesToAllAsync( - GroupName, - 100, - userCredentials: TestCredentials.Root - ); - } - - [SupportsPSToAll.Fact] - public async Task throws_when_given_non_existing_subscription() => - await Assert.ThrowsAsync( - () => - _fixture.Client.ReplayParkedMessagesToAllAsync( - "NonExisting", - userCredentials: TestCredentials.Root - ) - ); - - [SupportsPSToAll.Fact] - public async Task throws_with_no_credentials() => - await Assert.ThrowsAsync( - () => - _fixture.Client.ReplayParkedMessagesToAllAsync(GroupName) - ); - - [SupportsPSToAll.Fact] - public async Task throws_with_non_existing_user() => - await Assert.ThrowsAsync( - () => - _fixture.Client.ReplayParkedMessagesToAllAsync( - GroupName, - userCredentials: TestCredentials.TestBadUser - ) - ); - - [SupportsPSToAll.Fact] - public async Task throws_with_normal_user_credentials() => - await Assert.ThrowsAsync( - () => - _fixture.Client.ReplayParkedMessagesToAllAsync( - GroupName, - userCredentials: TestCredentials.TestUser1 - ) - ); - - public class Fixture : EventStoreClientFixture { - public Fixture() : base(noDefaultCredentials: true) { } - - protected override async Task Given() { - if (SupportsPSToAll.No) - return; - - await Client.CreateToAllAsync( - GroupName, - new(), - userCredentials: TestCredentials.Root - ); - } - - protected override Task When() => Task.CompletedTask; - } -} \ No newline at end of file diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/update_existing.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/update_existing.cs deleted file mode 100644 index c1fac1c1f..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/update_existing.cs +++ /dev/null @@ -1,28 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; - -public class update_existing : IClassFixture { - const string Group = "existing"; - - readonly Fixture _fixture; - - public update_existing(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public async Task the_completion_succeeds() => - await _fixture.Client.UpdateToAllAsync( - Group, - new(), - userCredentials: TestCredentials.Root - ); - - public class Fixture : EventStoreClientFixture { - protected override async Task Given() => - await Client.CreateToAllAsync( - Group, - new(), - userCredentials: TestCredentials.Root - ); - - protected override Task When() => Task.CompletedTask; - } -} \ No newline at end of file diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/update_existing_filtered.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/update_existing_filtered.cs deleted file mode 100644 index ff0f5ab5d..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/update_existing_filtered.cs +++ /dev/null @@ -1,30 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; - -public class update_existing_filtered - : IClassFixture { - const string Group = "existing-filtered"; - - readonly Fixture _fixture; - - public update_existing_filtered(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public async Task the_completion_succeeds() => - await _fixture.Client.UpdateToAllAsync( - Group, - new(true), - userCredentials: TestCredentials.Root - ); - - public class Fixture : EventStoreClientFixture { - protected override async Task Given() => - await Client.CreateToAllAsync( - Group, - EventTypeFilter.Prefix("prefix-filter-"), - new(), - userCredentials: TestCredentials.Root - ); - - protected override Task When() => Task.CompletedTask; - } -} \ No newline at end of file diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/update_existing_with_check_point.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/update_existing_with_check_point.cs deleted file mode 100644 index 05d6a70e3..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/update_existing_with_check_point.cs +++ /dev/null @@ -1,108 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; - -public class update_existing_with_check_point : IClassFixture { - private const string Group = "existing-with-check-point"; - - private readonly Fixture _fixture; - - public update_existing_with_check_point(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public void resumes_from_check_point() { - Assert.True(_fixture.Resumed.Event.Position > _fixture.CheckPoint); - } - - public class Fixture : EventStoreClientFixture { - private readonly List _appearedEvents; - private readonly EventData[] _events; - - private EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult? _subscription; - private IAsyncEnumerator? _enumerator; - - public ResolvedEvent Resumed { get; private set; } - public Position CheckPoint { get; private set; } - - public Fixture() { - _appearedEvents = new(); - _events = CreateTestEvents(5).ToArray(); - } - - protected override async Task Given() { - foreach (var e in _events) { - await StreamsClient.AppendToStreamAsync("test-" + Guid.NewGuid(), StreamState.Any, new[] { e }); - } - - await Client.CreateToAllAsync(Group, - new(checkPointLowerBound: 5, checkPointAfter: TimeSpan.FromSeconds(1), startFrom: Position.Start), - userCredentials: TestCredentials.Root); - - _subscription = Client.SubscribeToAll(Group, userCredentials: TestCredentials.Root); - _enumerator = _subscription.Messages.GetAsyncEnumerator(); - await _enumerator.MoveNextAsync(); - - await Task.WhenAll(Subscribe().WithTimeout(), WaitForCheckpoint().WithTimeout()); - - return; - - async Task Subscribe() { - while (await _enumerator.MoveNextAsync()) { - if (_enumerator.Current is not PersistentSubscriptionMessage.Event(var resolvedEvent, _)) { - continue; - } - - _appearedEvents.Add(resolvedEvent); - await _subscription.Ack(resolvedEvent); - if (_appearedEvents.Count == _events.Length) { - break; - } - } - } - - async Task WaitForCheckpoint() { - await using var subscription = StreamsClient.SubscribeToStream( - $"$persistentsubscription-$all::{Group}-checkpoint", FromStream.Start, - userCredentials: TestCredentials.Root); - await foreach (var message in subscription.Messages) { - if (message is not StreamMessage.Event(var resolvedEvent)) { - continue; - } - - CheckPoint = resolvedEvent.Event.Data.ParsePosition(); - return; - } - } } - - protected override async Task When() { - // Force restart of the subscription - await Client.UpdateToAllAsync(Group, new(), userCredentials: TestCredentials.Root); - - try { - while (await _enumerator!.MoveNextAsync()) { } - } catch (PersistentSubscriptionDroppedByServerException) { } - - foreach (var e in _events) { - await StreamsClient.AppendToStreamAsync("test-" + Guid.NewGuid(), StreamState.NoStream, new[] { e }); - } - - await using var subscription = Client.SubscribeToAll(Group, userCredentials: TestCredentials.Root); - Resumed = await subscription.Messages.OfType() - .Select(e => e.ResolvedEvent) - .Take(1) - .FirstAsync() - .AsTask() - .WithTimeout(); - } - - public override async Task DisposeAsync() { - if (_enumerator is not null) { - await _enumerator.DisposeAsync(); - } - - if (_subscription is not null) { - await _subscription.DisposeAsync(); - } - - await base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/update_existing_with_check_point_filtered.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/update_existing_with_check_point_filtered.cs deleted file mode 100644 index 829aca768..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/update_existing_with_check_point_filtered.cs +++ /dev/null @@ -1,111 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; - -public class update_existing_with_check_point_filtered - : IClassFixture { - private const string Group = "existing-with-check-point-filtered"; - - private readonly Fixture _fixture; - - public update_existing_with_check_point_filtered(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public void resumes_from_check_point() { - Assert.True(_fixture.Resumed.Event.Position > _fixture.CheckPoint); - } - - public class Fixture : EventStoreClientFixture { - private readonly List _appearedEvents; - private readonly EventData[] _events; - - private EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult? _subscription; - private IAsyncEnumerator? _enumerator; - - public ResolvedEvent Resumed { get; private set; } - public Position CheckPoint { get; private set; } - - public Fixture() { - _appearedEvents = new(); - _events = CreateTestEvents(5).ToArray(); - } - - - protected override async Task Given() { - foreach (var e in _events) { - await StreamsClient.AppendToStreamAsync("test-" + Guid.NewGuid(), StreamState.NoStream, new[] { e }); - } - - await Client.CreateToAllAsync(Group, StreamFilter.Prefix("test"), - new(checkPointLowerBound: 5, checkPointAfter: TimeSpan.FromSeconds(1), startFrom: Position.Start), - userCredentials: TestCredentials.Root); - - _subscription = Client.SubscribeToAll(Group, userCredentials: TestCredentials.Root); - _enumerator = _subscription.Messages.GetAsyncEnumerator(); - await _enumerator.MoveNextAsync(); - - await Task.WhenAll(Subscribe().WithTimeout(), WaitForCheckpoint().WithTimeout()); - - return; - - async Task Subscribe() { - while (await _enumerator.MoveNextAsync()) { - if (_enumerator.Current is not PersistentSubscriptionMessage.Event(var resolvedEvent, _)) { - continue; - } - - _appearedEvents.Add(resolvedEvent); - await _subscription.Ack(resolvedEvent); - if (_appearedEvents.Count == _events.Length) { - break; - } - } - } - - async Task WaitForCheckpoint() { - await using var subscription = StreamsClient.SubscribeToStream( - $"$persistentsubscription-$all::{Group}-checkpoint", FromStream.Start, - userCredentials: TestCredentials.Root); - await foreach (var message in subscription.Messages) { - if (message is not StreamMessage.Event(var resolvedEvent)) { - continue; - } - - CheckPoint = resolvedEvent.Event.Data.ParsePosition(); - return; - } - } - } - - protected override async Task When() { - // Force restart of the subscription - await Client.UpdateToAllAsync(Group, new(), userCredentials: TestCredentials.Root); - - try { - while (await _enumerator!.MoveNextAsync()) { } - } catch (PersistentSubscriptionDroppedByServerException) { } - - foreach (var e in _events) { - await StreamsClient.AppendToStreamAsync("test-" + Guid.NewGuid(), StreamState.NoStream, new[] { e }); - } - - await using var subscription = Client.SubscribeToAll(Group, userCredentials: TestCredentials.Root); - Resumed = await subscription.Messages.OfType() - .Select(e => e.ResolvedEvent) - .Take(1) - .FirstAsync() - .AsTask() - .WithTimeout(); - } - - public override async Task DisposeAsync() { - if (_enumerator is not null) { - await _enumerator.DisposeAsync(); - } - - if (_subscription is not null) { - await _subscription.DisposeAsync(); - } - - await base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/update_existing_with_commit_position_equal_to_last_indexed_position.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/update_existing_with_commit_position_equal_to_last_indexed_position.cs deleted file mode 100644 index 983349b85..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/update_existing_with_commit_position_equal_to_last_indexed_position.cs +++ /dev/null @@ -1,41 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; - -public class update_existing_with_commit_position_equal_to_last_indexed_position - : IClassFixture { - const string Group = "existing"; - - readonly Fixture _fixture; - - public update_existing_with_commit_position_equal_to_last_indexed_position(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public async Task the_completion_succeeds() => - await _fixture.Client.UpdateToAllAsync( - Group, - new(startFrom: new Position(_fixture.LastCommitPosition, _fixture.LastCommitPosition)), - userCredentials: TestCredentials.Root - ); - - public class Fixture : EventStoreClientFixture { - public ulong LastCommitPosition; - - protected override async Task Given() { - await Client.CreateToAllAsync( - Group, - new(), - userCredentials: TestCredentials.Root - ); - - var lastEvent = await StreamsClient.ReadAllAsync( - Direction.Backwards, - Position.End, - 1, - userCredentials: TestCredentials.Root - ).FirstAsync(); - - LastCommitPosition = lastEvent.OriginalPosition?.CommitPosition ?? throw new(); - } - - protected override Task When() => Task.CompletedTask; - } -} \ No newline at end of file diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/update_existing_with_commit_position_larger_than_last_indexed_position.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/update_existing_with_commit_position_larger_than_last_indexed_position.cs deleted file mode 100644 index 864e6a806..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/update_existing_with_commit_position_larger_than_last_indexed_position.cs +++ /dev/null @@ -1,49 +0,0 @@ -using Grpc.Core; - -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; - -public class update_existing_with_commit_position_larger_than_last_indexed_position - : IClassFixture { - const string Group = "existing"; - - readonly Fixture _fixture; - - public update_existing_with_commit_position_larger_than_last_indexed_position(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public async Task fails() { - var ex = await Assert.ThrowsAsync( - () => - _fixture.Client.UpdateToAllAsync( - Group, - new(startFrom: new Position(_fixture.LastCommitPosition + 1, _fixture.LastCommitPosition)), - userCredentials: TestCredentials.Root - ) - ); - - Assert.Equal(StatusCode.Internal, ex.StatusCode); - } - - public class Fixture : EventStoreClientFixture { - public ulong LastCommitPosition; - - protected override async Task When() { - await Client.CreateToAllAsync( - Group, - new(), - userCredentials: TestCredentials.Root - ); - - var lastEvent = await StreamsClient.ReadAllAsync( - Direction.Backwards, - Position.End, - 1, - userCredentials: TestCredentials.Root - ).FirstAsync(); - - LastCommitPosition = lastEvent.OriginalPosition?.CommitPosition ?? throw new(); - } - - protected override Task Given() => Task.CompletedTask; - } -} \ No newline at end of file diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/update_existing_with_subscribers.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/update_existing_with_subscribers.cs deleted file mode 100644 index 71e89d260..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/update_existing_with_subscribers.cs +++ /dev/null @@ -1,49 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; - -public class update_existing_with_subscribers : IClassFixture { - private const string Group = "existing"; - - private readonly Fixture _fixture; - - public update_existing_with_subscribers(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public async Task existing_subscriptions_are_dropped() { - var ex = await Assert.ThrowsAsync(async () => { - while (await _fixture.Enumerator!.MoveNextAsync()) { - } - }).WithTimeout(); - - Assert.Equal(SystemStreams.AllStream, ex.StreamName); - Assert.Equal(Group, ex.GroupName); - } - - public class Fixture : EventStoreClientFixture { - private EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult? _subscription; - public IAsyncEnumerator? Enumerator { get; private set; } - - protected override async Task Given() { - await Client.CreateToAllAsync(Group, new(startFrom: Position.Start), userCredentials: TestCredentials.Root); - - _subscription = Client.SubscribeToAll(Group, userCredentials: TestCredentials.Root); - - Enumerator = _subscription.Messages.GetAsyncEnumerator(); - - await Enumerator.MoveNextAsync(); - } - - protected override Task When() => Client.UpdateToAllAsync(Group, new(), userCredentials: TestCredentials.Root); - - public override async Task DisposeAsync() { - if (Enumerator is not null) { - await Enumerator.DisposeAsync(); - } - - if (_subscription is not null) { - await _subscription.DisposeAsync(); - } - - await base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/update_existing_without_permissions.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/update_existing_without_permissions.cs deleted file mode 100644 index fa72629ca..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/update_existing_without_permissions.cs +++ /dev/null @@ -1,32 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; - -public class update_existing_without_permissions - : IClassFixture { - const string Group = "existing"; - - readonly Fixture _fixture; - - public update_existing_without_permissions(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public async Task the_completion_fails_with_access_denied() => - await Assert.ThrowsAsync( - () => _fixture.Client.UpdateToAllAsync( - Group, - new() - ) - ); - - public class Fixture : EventStoreClientFixture { - public Fixture() : base(noDefaultCredentials: true) { } - - protected override async Task Given() => - await Client.CreateToAllAsync( - Group, - new(), - userCredentials: TestCredentials.Root - ); - - protected override Task When() => Task.CompletedTask; - } -} \ No newline at end of file diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/update_non_existent.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/update_non_existent.cs deleted file mode 100644 index 5082e86d0..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/update_non_existent.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; - -public class update_non_existent - : IClassFixture { - const string Group = "nonexistent"; - - readonly Fixture _fixture; - - public update_non_existent(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public async Task the_completion_fails_with_not_found() => - await Assert.ThrowsAsync( - () => _fixture.Client.UpdateToAllAsync( - Group, - new(), - userCredentials: TestCredentials.Root - ) - ); - - public class Fixture : EventStoreClientFixture { - protected override Task Given() => Task.CompletedTask; - protected override Task When() => Task.CompletedTask; - } -} \ No newline at end of file diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/update_with_prepare_position_larger_than_commit_position.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/update_with_prepare_position_larger_than_commit_position.cs deleted file mode 100644 index ce53d81ae..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/update_with_prepare_position_larger_than_commit_position.cs +++ /dev/null @@ -1,26 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; - -public class update_with_prepare_position_larger_than_commit_position - : IClassFixture { - const string Group = "existing"; - - readonly Fixture _fixture; - - public update_with_prepare_position_larger_than_commit_position(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public Task fails_with_argument_out_of_range_exception() => - Assert.ThrowsAsync( - () => - _fixture.Client.UpdateToAllAsync( - Group, - new(startFrom: new Position(0, 1)), - userCredentials: TestCredentials.Root - ) - ); - - public class Fixture : EventStoreClientFixture { - protected override Task Given() => Task.CompletedTask; - protected override Task When() => Task.CompletedTask; - } -} \ No newline at end of file diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/when_writing_and_filtering_out_events.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/when_writing_and_filtering_out_events.cs deleted file mode 100644 index cda94d8b3..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/when_writing_and_filtering_out_events.cs +++ /dev/null @@ -1,87 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; - -public class when_writing_and_filtering_out_events : IClassFixture { - private const string Group = "filtering-out-events"; - - private readonly Fixture _fixture; - - public when_writing_and_filtering_out_events(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public void it_should_write_a_check_point() { - Assert.True(_fixture.SecondCheckPoint > _fixture.FirstCheckPoint); - Assert.Equal(_fixture.Events.Select(e => e.EventId), _fixture.AppearedEvents.Select(e => e.Event.EventId)); - } - - public class Fixture : EventStoreClientFixture { - private readonly List _appearedEvents; - - public Fixture() { - Events = CreateTestEvents(10).ToArray(); - _appearedEvents = new(); - } - - public Position SecondCheckPoint { get; private set; } - public Position FirstCheckPoint { get; private set; } - public EventData[] Events { get; } - public ResolvedEvent[] AppearedEvents => _appearedEvents.ToArray(); - - protected override async Task Given() { - foreach (var e in Events) { - await StreamsClient.AppendToStreamAsync("test-" + Guid.NewGuid(), StreamState.Any, new[] { e }); - } - - await Client.CreateToAllAsync(Group, StreamFilter.Prefix("test"), - new(checkPointLowerBound: 1, checkPointUpperBound: 5, checkPointAfter: TimeSpan.FromSeconds(1), - startFrom: Position.Start), userCredentials: TestCredentials.Root); - - await using var subscription = Client.SubscribeToAll(Group, userCredentials: TestCredentials.Root); - await using var enumerator = subscription.Messages.GetAsyncEnumerator(); - - await Task.WhenAll(Subscribe().WithTimeout(), WaitForCheckpoints().WithTimeout()); - - return; - - async Task Subscribe() { - while (await enumerator.MoveNextAsync()) { - if (enumerator.Current is not PersistentSubscriptionMessage.Event(var resolvedEvent, _)) { - continue; - } - - _appearedEvents.Add(resolvedEvent); - await subscription.Ack(resolvedEvent); - if (_appearedEvents.Count == Events.Length) { - break; - } - } - } - - async Task WaitForCheckpoints() { - bool firstCheckpointSet = false; - await using var subscription = StreamsClient.SubscribeToStream( - $"$persistentsubscription-$all::{Group}-checkpoint", FromStream.Start, - userCredentials: TestCredentials.Root); - await foreach (var message in subscription.Messages) { - if (message is not StreamMessage.Event(var resolvedEvent)) { - continue; - } - - if (!firstCheckpointSet) { - FirstCheckPoint = resolvedEvent.Event.Data.ParsePosition(); - firstCheckpointSet = true; - } else { - SecondCheckPoint = resolvedEvent.Event.Data.ParsePosition(); - return; - } - } - } - } - - protected override async Task When() { - foreach (var e in Events) { - await StreamsClient.AppendToStreamAsync("filtered-out-stream-" + Guid.NewGuid(), StreamState.Any, - new[] { e }); - } - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/when_writing_and_subscribing_to_normal_events_manual_nack.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/when_writing_and_subscribing_to_normal_events_manual_nack.cs deleted file mode 100644 index 72e0cfd00..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/when_writing_and_subscribing_to_normal_events_manual_nack.cs +++ /dev/null @@ -1,57 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; - -public class when_writing_and_subscribing_to_normal_events_manual_nack - : IClassFixture { - private const string Group = nameof(Group); - private const int BufferCount = 10; - private const int EventWriteCount = BufferCount * 2; - - private readonly Fixture _fixture; - - public when_writing_and_subscribing_to_normal_events_manual_nack(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public async Task Test() { - await _fixture.Subscription!.Messages.OfType() - .SelectAwait(async e => { - await _fixture.Subscription.Nack(PersistentSubscriptionNakEventAction.Park, "fail", e.ResolvedEvent); - return e; - }) - .Where(e => e.ResolvedEvent.OriginalStreamId.StartsWith("test-")) - .Take(_fixture.Events.Length) - .ToArrayAsync() - .AsTask() - .WithTimeout(); - } - - public class Fixture : EventStoreClientFixture { - public readonly EventData[] Events; - - public EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult? Subscription { get; private set; } - - public Fixture() { - Events = CreateTestEvents(EventWriteCount).ToArray(); - } - - protected override async Task Given() { - await Client.CreateToAllAsync(Group, new(startFrom: Position.Start, resolveLinkTos: true), - userCredentials: TestCredentials.Root); - - Subscription = Client.SubscribeToAll(Group, bufferSize: BufferCount, userCredentials: TestCredentials.Root); - } - - protected override async Task When() { - foreach (var e in Events) { - await StreamsClient.AppendToStreamAsync("test-" + Guid.NewGuid(), StreamState.Any, new[] { e }); - } - } - - public override async Task DisposeAsync() { - if (Subscription is not null) { - await Subscription.DisposeAsync(); - } - - await base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/connect_to_existing_with_max_one_client_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/connect_to_existing_with_max_one_client_obsolete.cs deleted file mode 100644 index 8f290088c..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/connect_to_existing_with_max_one_client_obsolete.cs +++ /dev/null @@ -1,47 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream.Obsolete; - -[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class connect_to_existing_with_max_one_client_obsolete - : IClassFixture { - const string Group = "startinbeginning1"; - const string Stream = nameof(connect_to_existing_with_max_one_client_obsolete); - readonly Fixture _fixture; - - public connect_to_existing_with_max_one_client_obsolete(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task the_second_subscription_fails_to_connect() { - using var first = await _fixture.Client.SubscribeToStreamAsync( - Stream, - Group, - delegate { return Task.CompletedTask; }, - userCredentials: TestCredentials.Root - ).WithTimeout(); - - var ex = await Assert.ThrowsAsync( - async () => { - using var _ = await _fixture.Client.SubscribeToStreamAsync( - Stream, - Group, - delegate { return Task.CompletedTask; }, - userCredentials: TestCredentials.Root - ); - } - ).WithTimeout(); - - Assert.Equal(Stream, ex.StreamName); - Assert.Equal(Group, ex.GroupName); - } - - public class Fixture : EventStoreClientFixture { - protected override Task Given() => - Client.CreateToStreamAsync( - Stream, - Group, - new(maxSubscriberCount: 1), - userCredentials: TestCredentials.Root - ); - - protected override Task When() => Task.CompletedTask; - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/connect_to_existing_with_permissions_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/connect_to_existing_with_permissions_obsolete.cs deleted file mode 100644 index 70ada6f17..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/connect_to_existing_with_permissions_obsolete.cs +++ /dev/null @@ -1,39 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream.Obsolete; - -[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class connect_to_existing_with_permissions_obsolete - : IClassFixture { - const string Stream = nameof(connect_to_existing_with_permissions_obsolete); - - readonly Fixture _fixture; - - public connect_to_existing_with_permissions_obsolete(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task the_subscription_succeeds() { - var dropped = new TaskCompletionSource<(SubscriptionDroppedReason, Exception?)>(); - using var subscription = await _fixture.Client.SubscribeToStreamAsync( - Stream, - "agroupname17", - delegate { return Task.CompletedTask; }, - (s, reason, ex) => dropped.TrySetResult((reason, ex)), - TestCredentials.Root - ).WithTimeout(); - - Assert.NotNull(subscription); - - await Assert.ThrowsAsync(() => dropped.Task.WithTimeout()); - } - - public class Fixture : EventStoreClientFixture { - protected override Task Given() => - Client.CreateToStreamAsync( - Stream, - "agroupname17", - new(), - userCredentials: TestCredentials.Root - ); - - protected override Task When() => Task.CompletedTask; - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/connect_to_existing_with_start_from_beginning_and_events_in_it_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/connect_to_existing_with_start_from_beginning_and_events_in_it_obsolete.cs deleted file mode 100644 index c023db1f8..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/connect_to_existing_with_start_from_beginning_and_events_in_it_obsolete.cs +++ /dev/null @@ -1,65 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream.Obsolete; - -[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class connect_to_existing_with_start_from_beginning_and_events_in_it_obsolete - : IClassFixture { - const string Group = "startinbeginning1"; - - const string Stream = - nameof(connect_to_existing_with_start_from_beginning_and_events_in_it_obsolete); - - readonly Fixture _fixture; - - public connect_to_existing_with_start_from_beginning_and_events_in_it_obsolete(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task the_subscription_gets_event_zero_as_its_first_event() { - var resolvedEvent = await _fixture.FirstEvent.WithTimeout(TimeSpan.FromSeconds(10)); - Assert.Equal(StreamPosition.Start, resolvedEvent.Event.EventNumber); - Assert.Equal(_fixture.Events[0].EventId, resolvedEvent.Event.EventId); - } - - public class Fixture : EventStoreClientFixture { - readonly TaskCompletionSource _firstEventSource; - public readonly EventData[] Events; - PersistentSubscription? _subscription; - - public Fixture() { - _firstEventSource = new(); - Events = CreateTestEvents(10).ToArray(); - } - - public Task FirstEvent => _firstEventSource.Task; - - protected override async Task Given() { - await StreamsClient.AppendToStreamAsync(Stream, StreamState.NoStream, Events); - await Client.CreateToStreamAsync( - Stream, - Group, - new(startFrom: StreamPosition.Start), - userCredentials: TestCredentials.Root - ); - } - - protected override async Task When() => - _subscription = await Client.SubscribeToStreamAsync( - Stream, - Group, - async (subscription, e, r, ct) => { - _firstEventSource.TrySetResult(e); - await subscription.Ack(e); - }, - (subscription, reason, ex) => { - if (reason != SubscriptionDroppedReason.Disposed) - _firstEventSource.TrySetException(ex!); - }, - TestCredentials.TestUser1 - ); - - public override Task DisposeAsync() { - _subscription?.Dispose(); - return base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/connect_to_existing_with_start_from_beginning_and_no_stream_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/connect_to_existing_with_start_from_beginning_and_no_stream_obsolete.cs deleted file mode 100644 index 037ec52e5..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/connect_to_existing_with_start_from_beginning_and_no_stream_obsolete.cs +++ /dev/null @@ -1,65 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream.Obsolete; - -[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class connect_to_existing_with_start_from_beginning_and_no_stream_obsolete - : IClassFixture { - const string Group = "startinbeginning1"; - - const string Stream = - nameof(connect_to_existing_with_start_from_beginning_and_no_stream_obsolete); - - readonly Fixture _fixture; - - public connect_to_existing_with_start_from_beginning_and_no_stream_obsolete(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task the_subscription_gets_event_zero_as_its_first_event() { - var firstEvent = await _fixture.FirstEvent.WithTimeout(TimeSpan.FromSeconds(10)); - Assert.Equal(StreamPosition.Start, firstEvent.Event.EventNumber); - Assert.Equal(_fixture.EventId, firstEvent.Event.EventId); - } - - public class Fixture : EventStoreClientFixture { - readonly TaskCompletionSource _firstEventSource; - public readonly EventData[] Events; - PersistentSubscription? _subscription; - - public Fixture() { - _firstEventSource = new(); - Events = CreateTestEvents().ToArray(); - } - - public Task FirstEvent => _firstEventSource.Task; - public Uuid EventId => Events.Single().EventId; - - protected override async Task Given() { - await Client.CreateToStreamAsync( - Stream, - Group, - new(), - userCredentials: TestCredentials.Root - ); - - _subscription = await Client.SubscribeToStreamAsync( - Stream, - Group, - async (subscription, e, r, ct) => { - _firstEventSource.TrySetResult(e); - await subscription.Ack(e); - }, - (subscription, reason, ex) => { - if (reason != SubscriptionDroppedReason.Disposed) - _firstEventSource.TrySetException(ex!); - }, - TestCredentials.TestUser1 - ); - } - - protected override Task When() => StreamsClient.AppendToStreamAsync(Stream, StreamState.NoStream, Events); - - public override Task DisposeAsync() { - _subscription?.Dispose(); - return base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/connect_to_existing_with_start_from_not_set_and_events_in_it_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/connect_to_existing_with_start_from_not_set_and_events_in_it_obsolete.cs deleted file mode 100644 index f0bd49b06..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/connect_to_existing_with_start_from_not_set_and_events_in_it_obsolete.cs +++ /dev/null @@ -1,61 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream.Obsolete; - -[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class connect_to_existing_with_start_from_not_set_and_events_in_it_obsolete - : IClassFixture { - const string Group = "startinbeginning1"; - - const string Stream = - nameof(connect_to_existing_with_start_from_not_set_and_events_in_it_obsolete); - - readonly Fixture _fixture; - - public connect_to_existing_with_start_from_not_set_and_events_in_it_obsolete(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task the_subscription_gets_no_events() => await Assert.ThrowsAsync(() => _fixture.FirstEvent.WithTimeout()); - - public class Fixture : EventStoreClientFixture { - readonly TaskCompletionSource _firstEventSource; - public readonly EventData[] Events; - PersistentSubscription? _subscription; - - public Fixture() { - _firstEventSource = new(); - Events = CreateTestEvents(10).ToArray(); - } - - public Task FirstEvent => _firstEventSource.Task; - - protected override async Task Given() { - await StreamsClient.AppendToStreamAsync(Stream, StreamState.NoStream, Events); - await Client.CreateToStreamAsync( - Stream, - Group, - new(), - userCredentials: TestCredentials.Root - ); - } - - protected override async Task When() => - _subscription = await Client.SubscribeToStreamAsync( - Stream, - Group, - async (subscription, e, r, ct) => { - _firstEventSource.TrySetResult(e); - await subscription.Ack(e); - }, - (subscription, reason, ex) => { - if (reason != SubscriptionDroppedReason.Disposed) - _firstEventSource.TrySetException(ex!); - }, - TestCredentials.TestUser1 - ); - - public override Task DisposeAsync() { - _subscription?.Dispose(); - return base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/connect_to_existing_with_start_from_not_set_and_events_in_it_then_event_written_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/connect_to_existing_with_start_from_not_set_and_events_in_it_then_event_written_obsolete.cs deleted file mode 100644 index ea1987ceb..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/connect_to_existing_with_start_from_not_set_and_events_in_it_then_event_written_obsolete.cs +++ /dev/null @@ -1,67 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream.Obsolete; - -[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class connect_to_existing_with_start_from_not_set_and_events_in_it_then_event_written_obsolete - : IClassFixture { - const string Group = "startinbeginning1"; - const string Stream = nameof(connect_to_existing_with_start_from_not_set_and_events_in_it_then_event_written_obsolete); - - readonly Fixture _fixture; - - public - connect_to_existing_with_start_from_not_set_and_events_in_it_then_event_written_obsolete(Fixture fixture) => - _fixture = fixture; - - [Fact] - public async Task the_subscription_gets_the_written_event_as_its_first_event() { - var resolvedEvent = await _fixture.FirstEvent.WithTimeout(); - Assert.Equal(new(10), resolvedEvent.Event.EventNumber); - Assert.Equal(_fixture.Events.Last().EventId, resolvedEvent.Event.EventId); - } - - public class Fixture : EventStoreClientFixture { - readonly TaskCompletionSource _firstEventSource; - - public readonly EventData[] Events; - - PersistentSubscription? _subscription; - - public Fixture() { - _firstEventSource = new(); - Events = CreateTestEvents(11).ToArray(); - } - - public Task FirstEvent => _firstEventSource.Task; - - protected override async Task Given() { - await StreamsClient.AppendToStreamAsync(Stream, StreamState.NoStream, Events.Take(10)); - await Client.CreateToStreamAsync( - Stream, - Group, - new(), - userCredentials: TestCredentials.Root - ); - - _subscription = await Client.SubscribeToStreamAsync( - Stream, - Group, - async (subscription, e, r, ct) => { - _firstEventSource.TrySetResult(e); - await subscription.Ack(e); - }, - (subscription, reason, ex) => { - if (reason != SubscriptionDroppedReason.Disposed) - _firstEventSource.TrySetException(ex!); - }, - TestCredentials.TestUser1 - ); - } - - protected override Task When() => StreamsClient.AppendToStreamAsync(Stream, new StreamRevision(9), Events.Skip(10)); - - public override Task DisposeAsync() { - _subscription?.Dispose(); - return base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/connect_to_existing_with_start_from_set_to_end_position_and_events_in_it_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/connect_to_existing_with_start_from_set_to_end_position_and_events_in_it_obsolete.cs deleted file mode 100644 index b25765019..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/connect_to_existing_with_start_from_set_to_end_position_and_events_in_it_obsolete.cs +++ /dev/null @@ -1,60 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream.Obsolete; - -[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class connect_to_existing_with_start_from_set_to_end_position_and_events_in_it_obsolete - : IClassFixture { - const string Group = "startinbeginning1"; - - const string Stream = - nameof(connect_to_existing_with_start_from_set_to_end_position_and_events_in_it_obsolete); - - readonly Fixture _fixture; - - public connect_to_existing_with_start_from_set_to_end_position_and_events_in_it_obsolete(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task the_subscription_gets_no_events() => await Assert.ThrowsAsync(() => _fixture.FirstEvent.WithTimeout()); - - public class Fixture : EventStoreClientFixture { - readonly TaskCompletionSource _firstEventSource; - public readonly EventData[] Events; - PersistentSubscription? _subscription; - - public Fixture() { - _firstEventSource = new(); - Events = CreateTestEvents(10).ToArray(); - } - - public Task FirstEvent => _firstEventSource.Task; - - protected override async Task Given() { - await StreamsClient.AppendToStreamAsync(Stream, StreamState.NoStream, Events); - await Client.CreateToStreamAsync( - Stream, - Group, - new(startFrom: StreamPosition.End), - userCredentials: TestCredentials.Root - ); - } - - protected override async Task When() => - _subscription = await Client.SubscribeToStreamAsync( - Stream, - Group, - async (subscription, e, r, ct) => { - _firstEventSource.TrySetResult(e); - await subscription.Ack(e); - }, - (subscription, reason, ex) => { - if (reason != SubscriptionDroppedReason.Disposed) - _firstEventSource.TrySetException(ex!); - }, - TestCredentials.TestUser1 - ); - - public override Task DisposeAsync() { - _subscription?.Dispose(); - return base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/connect_to_existing_with_start_from_set_to_end_position_and_events_in_it_then_event_written_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/connect_to_existing_with_start_from_set_to_end_position_and_events_in_it_then_event_written_obsolete.cs deleted file mode 100644 index 9bc45141b..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/connect_to_existing_with_start_from_set_to_end_position_and_events_in_it_then_event_written_obsolete.cs +++ /dev/null @@ -1,72 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream.Obsolete; - -[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class - connect_to_existing_with_start_from_set_to_end_position_and_events_in_it_then_event_written_obsolete - : IClassFixture< - connect_to_existing_with_start_from_set_to_end_position_and_events_in_it_then_event_written_obsolete - .Fixture> { - const string Group = "startinbeginning1"; - - const string Stream = - nameof( - connect_to_existing_with_start_from_set_to_end_position_and_events_in_it_then_event_written_obsolete - ); - - readonly Fixture _fixture; - - public - connect_to_existing_with_start_from_set_to_end_position_and_events_in_it_then_event_written_obsolete(Fixture fixture) => - _fixture = fixture; - - [Fact] - public async Task the_subscription_gets_the_written_event_as_its_first_event() { - var resolvedEvent = await _fixture.FirstEvent.WithTimeout(); - Assert.Equal(new(10), resolvedEvent.Event.EventNumber); - Assert.Equal(_fixture.Events.Last().EventId, resolvedEvent.Event.EventId); - } - - public class Fixture : EventStoreClientFixture { - readonly TaskCompletionSource _firstEventSource; - public readonly EventData[] Events; - PersistentSubscription? _subscription; - - public Fixture() { - _firstEventSource = new(); - Events = CreateTestEvents(11).ToArray(); - } - - public Task FirstEvent => _firstEventSource.Task; - - protected override async Task Given() { - await StreamsClient.AppendToStreamAsync(Stream, StreamState.NoStream, Events.Take(10)); - await Client.CreateToStreamAsync( - Stream, - Group, - new(startFrom: StreamPosition.End), - userCredentials: TestCredentials.Root - ); - - _subscription = await Client.SubscribeToStreamAsync( - Stream, - Group, - async (subscription, e, r, ct) => { - _firstEventSource.TrySetResult(e); - await subscription.Ack(e); - }, - (subscription, reason, ex) => { - if (reason != SubscriptionDroppedReason.Disposed) - _firstEventSource.TrySetException(ex!); - }, - TestCredentials.TestUser1 - ); - } - - protected override Task When() => StreamsClient.AppendToStreamAsync(Stream, new StreamRevision(9), Events.Skip(10)); - - public override Task DisposeAsync() { - _subscription?.Dispose(); - return base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/connect_to_existing_with_start_from_two_and_no_stream_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/connect_to_existing_with_start_from_two_and_no_stream_obsolete.cs deleted file mode 100644 index 09d6e4f21..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/connect_to_existing_with_start_from_two_and_no_stream_obsolete.cs +++ /dev/null @@ -1,65 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream.Obsolete; - -[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class connect_to_existing_with_start_from_two_and_no_stream_obsolete - : IClassFixture { - const string Group = "startinbeginning1"; - - const string Stream = - nameof(connect_to_existing_with_start_from_two_and_no_stream_obsolete); - - readonly Fixture _fixture; - - public connect_to_existing_with_start_from_two_and_no_stream_obsolete(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task the_subscription_gets_event_two_as_its_first_event() { - var resolvedEvent = await _fixture.FirstEvent.WithTimeout(TimeSpan.FromSeconds(10)); - Assert.Equal(new(2), resolvedEvent.Event.EventNumber); - Assert.Equal(_fixture.EventId, resolvedEvent.Event.EventId); - } - - public class Fixture : EventStoreClientFixture { - readonly TaskCompletionSource _firstEventSource; - public readonly EventData[] Events; - PersistentSubscription? _subscription; - - public Fixture() { - _firstEventSource = new(); - Events = CreateTestEvents(3).ToArray(); - } - - public Task FirstEvent => _firstEventSource.Task; - public Uuid EventId => Events.Last().EventId; - - protected override async Task Given() { - await Client.CreateToStreamAsync( - Stream, - Group, - new(startFrom: new StreamPosition(2)), - userCredentials: TestCredentials.Root - ); - - _subscription = await Client.SubscribeToStreamAsync( - Stream, - Group, - async (subscription, e, r, ct) => { - _firstEventSource.TrySetResult(e); - await subscription.Ack(e); - }, - (subscription, reason, ex) => { - if (reason != SubscriptionDroppedReason.Disposed) - _firstEventSource.TrySetException(ex!); - }, - TestCredentials.TestUser1 - ); - } - - protected override Task When() => StreamsClient.AppendToStreamAsync(Stream, StreamState.NoStream, Events); - - public override Task DisposeAsync() { - _subscription?.Dispose(); - return base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/connect_to_existing_with_start_from_x_set_and_events_in_it_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/connect_to_existing_with_start_from_x_set_and_events_in_it_obsolete.cs deleted file mode 100644 index c74f8b45f..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/connect_to_existing_with_start_from_x_set_and_events_in_it_obsolete.cs +++ /dev/null @@ -1,65 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream.Obsolete; - -[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class connect_to_existing_with_start_from_x_set_and_events_in_it_obsolete - : IClassFixture { - const string Group = "startinx2"; - - const string Stream = - nameof(connect_to_existing_with_start_from_x_set_and_events_in_it_obsolete); - - readonly Fixture _fixture; - - public connect_to_existing_with_start_from_x_set_and_events_in_it_obsolete(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task the_subscription_gets_the_written_event_as_its_first_event() { - var resolvedEvent = await _fixture.FirstEvent.WithTimeout(); - Assert.Equal(new(4), resolvedEvent.Event.EventNumber); - Assert.Equal(_fixture.Events.Skip(4).First().EventId, resolvedEvent.Event.EventId); - } - - public class Fixture : EventStoreClientFixture { - readonly TaskCompletionSource _firstEventSource; - public readonly EventData[] Events; - PersistentSubscription? _subscription; - - public Fixture() { - _firstEventSource = new(); - Events = CreateTestEvents(10).ToArray(); - } - - public Task FirstEvent => _firstEventSource.Task; - - protected override async Task Given() { - await StreamsClient.AppendToStreamAsync(Stream, StreamState.NoStream, Events.Take(10)); - await Client.CreateToStreamAsync( - Stream, - Group, - new(startFrom: new StreamPosition(4)), - userCredentials: TestCredentials.Root - ); - - _subscription = await Client.SubscribeToStreamAsync( - Stream, - Group, - async (subscription, e, r, ct) => { - _firstEventSource.TrySetResult(e); - await subscription.Ack(e); - }, - (subscription, reason, ex) => { - if (reason != SubscriptionDroppedReason.Disposed) - _firstEventSource.TrySetException(ex!); - }, - TestCredentials.TestUser1 - ); - } - - protected override Task When() => StreamsClient.AppendToStreamAsync(Stream, new StreamRevision(9), Events.Skip(10)); - - public override Task DisposeAsync() { - _subscription?.Dispose(); - return base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/connect_to_existing_with_start_from_x_set_and_events_in_it_then_event_written_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/connect_to_existing_with_start_from_x_set_and_events_in_it_then_event_written_obsolete.cs deleted file mode 100644 index a3fae30a1..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/connect_to_existing_with_start_from_x_set_and_events_in_it_then_event_written_obsolete.cs +++ /dev/null @@ -1,68 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream.Obsolete; - -[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class connect_to_existing_with_start_from_x_set_and_events_in_it_then_event_written_obsolete - : IClassFixture< - connect_to_existing_with_start_from_x_set_and_events_in_it_then_event_written_obsolete. - Fixture> { - const string Group = "startinbeginning1"; - - const string Stream = - nameof(connect_to_existing_with_start_from_x_set_and_events_in_it_then_event_written_obsolete - ); - - readonly Fixture _fixture; - - public connect_to_existing_with_start_from_x_set_and_events_in_it_then_event_written_obsolete(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task the_subscription_gets_the_written_event_as_its_first_event() { - var resolvedEvent = await _fixture.FirstEvent.WithTimeout(); - Assert.Equal(new(10), resolvedEvent.Event.EventNumber); - Assert.Equal(_fixture.Events.Last().EventId, resolvedEvent.Event.EventId); - } - - public class Fixture : EventStoreClientFixture { - readonly TaskCompletionSource _firstEventSource; - public readonly EventData[] Events; - PersistentSubscription? _subscription; - - public Fixture() { - _firstEventSource = new(); - Events = CreateTestEvents(11).ToArray(); - } - - public Task FirstEvent => _firstEventSource.Task; - - protected override async Task Given() { - await StreamsClient.AppendToStreamAsync(Stream, StreamState.NoStream, Events.Take(10)); - await Client.CreateToStreamAsync( - Stream, - Group, - new(startFrom: new StreamPosition(10)), - userCredentials: TestCredentials.Root - ); - - _subscription = await Client.SubscribeToStreamAsync( - Stream, - Group, - async (subscription, e, r, ct) => { - _firstEventSource.TrySetResult(e); - await subscription.Ack(e); - }, - (subscription, reason, ex) => { - if (reason != SubscriptionDroppedReason.Disposed) - _firstEventSource.TrySetException(ex!); - }, - TestCredentials.TestUser1 - ); - } - - protected override Task When() => StreamsClient.AppendToStreamAsync(Stream, new StreamRevision(9), Events.Skip(10)); - - public override Task DisposeAsync() { - _subscription?.Dispose(); - return base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/connect_to_existing_with_start_from_x_set_higher_than_x_and_events_in_it_then_event_written_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/connect_to_existing_with_start_from_x_set_higher_than_x_and_events_in_it_then_event_written_obsolete.cs deleted file mode 100644 index 59198a660..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/connect_to_existing_with_start_from_x_set_higher_than_x_and_events_in_it_then_event_written_obsolete.cs +++ /dev/null @@ -1,72 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream.Obsolete; - -[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class - connect_to_existing_with_start_from_x_set_higher_than_x_and_events_in_it_then_event_written_obsolete - : IClassFixture< - connect_to_existing_with_start_from_x_set_higher_than_x_and_events_in_it_then_event_written_obsolete - .Fixture> { - const string Group = "startinbeginning1"; - - const string Stream = - nameof( - connect_to_existing_with_start_from_x_set_higher_than_x_and_events_in_it_then_event_written_obsolete - ); - - readonly Fixture _fixture; - - public - connect_to_existing_with_start_from_x_set_higher_than_x_and_events_in_it_then_event_written_obsolete(Fixture fixture) => - _fixture = fixture; - - [Fact] - public async Task the_subscription_gets_the_written_event_as_its_first_event() { - var resolvedEvent = await _fixture.FirstEvent.WithTimeout(); - Assert.Equal(new(11), resolvedEvent.Event.EventNumber); - Assert.Equal(_fixture.Events.Last().EventId, resolvedEvent.Event.EventId); - } - - public class Fixture : EventStoreClientFixture { - readonly TaskCompletionSource _firstEventSource; - public readonly EventData[] Events; - PersistentSubscription? _subscription; - - public Fixture() { - _firstEventSource = new(); - Events = CreateTestEvents(12).ToArray(); - } - - public Task FirstEvent => _firstEventSource.Task; - - protected override async Task Given() { - await StreamsClient.AppendToStreamAsync(Stream, StreamState.NoStream, Events.Take(11)); - await Client.CreateToStreamAsync( - Stream, - Group, - new(startFrom: new StreamPosition(11)), - userCredentials: TestCredentials.Root - ); - - _subscription = await Client.SubscribeToStreamAsync( - Stream, - Group, - async (subscription, e, r, ct) => { - _firstEventSource.TrySetResult(e); - await subscription.Ack(e); - }, - (subscription, reason, ex) => { - if (reason != SubscriptionDroppedReason.Disposed) - _firstEventSource.TrySetException(ex!); - }, - TestCredentials.TestUser1 - ); - } - - protected override Task When() => StreamsClient.AppendToStreamAsync(Stream, new StreamRevision(10), Events.Skip(11)); - - public override Task DisposeAsync() { - _subscription?.Dispose(); - return base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/connect_to_existing_without_permissions_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/connect_to_existing_without_permissions_obsolete.cs deleted file mode 100644 index 25942cd05..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/connect_to_existing_without_permissions_obsolete.cs +++ /dev/null @@ -1,35 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream.Obsolete; - -[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class connect_to_existing_without_permissions_obsolete - : IClassFixture { - const string Stream = "$" + nameof(connect_to_existing_without_permissions_obsolete); - readonly Fixture _fixture; - public connect_to_existing_without_permissions_obsolete(Fixture fixture) => _fixture = fixture; - - [Fact] - public Task throws_access_denied() => - Assert.ThrowsAsync( - async () => { - using var _ = await _fixture.Client.SubscribeToStreamAsync( - Stream, - "agroupname55", - delegate { return Task.CompletedTask; } - ); - } - ).WithTimeout(); - - public class Fixture : EventStoreClientFixture { - public Fixture() : base(noDefaultCredentials: true) { } - - protected override Task Given() => - Client.CreateToStreamAsync( - Stream, - "agroupname55", - new(), - userCredentials: TestCredentials.Root - ); - - protected override Task When() => Task.CompletedTask; - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/connect_to_non_existing_with_permissions_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/connect_to_non_existing_with_permissions_obsolete.cs deleted file mode 100644 index dd9d74900..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/connect_to_non_existing_with_permissions_obsolete.cs +++ /dev/null @@ -1,34 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream.Obsolete; - -[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class connect_to_non_existing_with_permissions_obsolete - : IClassFixture { - const string Stream = nameof(connect_to_non_existing_with_permissions_obsolete); - const string Group = "foo"; - - readonly Fixture _fixture; - - public connect_to_non_existing_with_permissions_obsolete(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task throws_persistent_subscription_not_found() { - var ex = await Assert.ThrowsAsync( - async () => { - using var _ = await _fixture.Client.SubscribeToStreamAsync( - Stream, - Group, - delegate { return Task.CompletedTask; }, - userCredentials: TestCredentials.Root - ); - } - ).WithTimeout(); - - Assert.Equal(Stream, ex.StreamName); - Assert.Equal(Group, ex.GroupName); - } - - public class Fixture : EventStoreClientFixture { - protected override Task Given() => Task.CompletedTask; - protected override Task When() => Task.CompletedTask; - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/connect_with_retries_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/connect_with_retries_obsolete.cs deleted file mode 100644 index d31fcc961..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/connect_with_retries_obsolete.cs +++ /dev/null @@ -1,70 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream.Obsolete; - -[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class connect_with_retries_obsolete : IClassFixture { - const string Group = "retries"; - const string Stream = nameof(connect_with_retries_obsolete); - - readonly Fixture _fixture; - - public connect_with_retries_obsolete(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task events_are_retried_until_success() => Assert.Equal(5, await _fixture.RetryCount.WithTimeout()); - - public class Fixture : EventStoreClientFixture { - readonly TaskCompletionSource _retryCountSource; - - public readonly EventData[] Events; - - PersistentSubscription? _subscription; - - public Fixture() { - _retryCountSource = new(); - - Events = CreateTestEvents().ToArray(); - } - - public Task RetryCount => _retryCountSource.Task; - - protected override async Task Given() { - await StreamsClient.AppendToStreamAsync(Stream, StreamState.NoStream, Events); - await Client.CreateToStreamAsync( - Stream, - Group, - new(startFrom: StreamPosition.Start), - userCredentials: TestCredentials.Root - ); - - _subscription = await Client.SubscribeToStreamAsync( - Stream, - Group, - async (subscription, e, r, ct) => { - if (r > 4) { - _retryCountSource.TrySetResult(r.Value); - await subscription.Ack(e.Event.EventId); - } - else { - await subscription.Nack( - PersistentSubscriptionNakEventAction.Retry, - "Not yet tried enough times", - e - ); - } - }, - (subscription, reason, ex) => { - if (reason != SubscriptionDroppedReason.Disposed) - _retryCountSource.TrySetException(ex!); - }, - TestCredentials.TestUser1 - ); - } - - protected override Task When() => StreamsClient.AppendToStreamAsync(Stream, StreamState.NoStream, Events); - - public override Task DisposeAsync() { - _subscription?.Dispose(); - return base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/connecting_to_a_persistent_subscription_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/connecting_to_a_persistent_subscription_obsolete.cs deleted file mode 100644 index af09f9867..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/connecting_to_a_persistent_subscription_obsolete.cs +++ /dev/null @@ -1,72 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream.Obsolete; - -[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class - connecting_to_a_persistent_subscription_obsolete - : IClassFixture< - connecting_to_a_persistent_subscription_obsolete - .Fixture> { - const string Group = "startinbeginning1"; - - const string Stream = - nameof( - connecting_to_a_persistent_subscription_obsolete - ); - - readonly Fixture _fixture; - - public - connecting_to_a_persistent_subscription_obsolete(Fixture fixture) => - _fixture = fixture; - - [Fact] - public async Task the_subscription_gets_the_written_event_as_its_first_event() { - var resolvedEvent = await _fixture.FirstEvent.WithTimeout(); - Assert.Equal(new(11), resolvedEvent.Event.EventNumber); - Assert.Equal(_fixture.Events.Last().EventId, resolvedEvent.Event.EventId); - } - - public class Fixture : EventStoreClientFixture { - readonly TaskCompletionSource _firstEventSource; - public readonly EventData[] Events; - PersistentSubscription? _subscription; - - public Fixture() { - _firstEventSource = new(); - Events = CreateTestEvents(12).ToArray(); - } - - public Task FirstEvent => _firstEventSource.Task; - - protected override async Task Given() { - await StreamsClient.AppendToStreamAsync(Stream, StreamState.NoStream, Events.Take(11)); - await Client.CreateToStreamAsync( - Stream, - Group, - new(startFrom: new StreamPosition(11)), - userCredentials: TestCredentials.Root - ); - - _subscription = await Client.SubscribeToStreamAsync( - Stream, - Group, - async (subscription, e, r, ct) => { - _firstEventSource.TrySetResult(e); - await subscription.Ack(e); - }, - (subscription, reason, ex) => { - if (reason != SubscriptionDroppedReason.Disposed) - _firstEventSource.TrySetException(ex!); - }, - TestCredentials.TestUser1 - ); - } - - protected override Task When() => StreamsClient.AppendToStreamAsync(Stream, new StreamRevision(10), Events.Skip(11)); - - public override Task DisposeAsync() { - _subscription?.Dispose(); - return base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/deleting_existing_with_subscriber_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/deleting_existing_with_subscriber_obsolete.cs deleted file mode 100644 index d684c76af..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/deleting_existing_with_subscriber_obsolete.cs +++ /dev/null @@ -1,67 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream.Obsolete; - -[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class deleting_existing_with_subscriber_obsolete - : IClassFixture { - const string Stream = nameof(deleting_existing_with_subscriber_obsolete); - readonly Fixture _fixture; - - public deleting_existing_with_subscriber_obsolete(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task the_subscription_is_dropped() { - var (reason, exception) = await _fixture.Dropped.WithTimeout(); - Assert.Equal(SubscriptionDroppedReason.ServerError, reason); - var ex = Assert.IsType(exception); - - Assert.Equal(Stream, ex.StreamName); - Assert.Equal("groupname123", ex.GroupName); - } - - [Fact(Skip = "Isn't this how it should work?")] - public async Task the_subscription_is_dropped_with_not_found() { - var (reason, exception) = await _fixture.Dropped.WithTimeout(); - Assert.Equal(SubscriptionDroppedReason.ServerError, reason); - var ex = Assert.IsType(exception); - Assert.Equal(Stream, ex.StreamName); - Assert.Equal("groupname123", ex.GroupName); - } - - public class Fixture : EventStoreClientFixture { - readonly TaskCompletionSource<(SubscriptionDroppedReason, Exception?)> _dropped; - PersistentSubscription? _subscription; - - public Fixture() => _dropped = new(); - - public Task<(SubscriptionDroppedReason, Exception?)> Dropped => _dropped.Task; - - protected override async Task Given() { - await Client.CreateToStreamAsync( - Stream, - "groupname123", - new(), - userCredentials: TestCredentials.Root - ); - - _subscription = await Client.SubscribeToStreamAsync( - Stream, - "groupname123", - (_, _, _, _) => Task.CompletedTask, - (_, r, e) => _dropped.TrySetResult((r, e)), - TestCredentials.Root - ); - } - - protected override Task When() => - Client.DeleteToStreamAsync( - Stream, - "groupname123", - userCredentials: TestCredentials.Root - ); - - public override Task DisposeAsync() { - _subscription?.Dispose(); - return base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/get_info_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/get_info_obsolete.cs deleted file mode 100644 index 34068a937..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/get_info_obsolete.cs +++ /dev/null @@ -1,199 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream.Obsolete; - -[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class get_info_obsolete : IClassFixture { - const string GroupName = nameof(get_info_obsolete); - const string StreamName = nameof(get_info_obsolete); - - static readonly PersistentSubscriptionSettings _settings = new( - true, - StreamPosition.Start, - true, - TimeSpan.FromSeconds(9), - 11, - 303, - 30, - 909, - TimeSpan.FromSeconds(1), - 1, - 1, - 500, - SystemConsumerStrategies.RoundRobin - ); - - readonly Fixture _fixture; - - public get_info_obsolete(Fixture fixture) => _fixture = fixture; - - public static IEnumerable AllowedUsers() { - yield return new object[] { TestCredentials.Root }; - yield return new object[] { TestCredentials.TestUser1 }; - } - - [Theory] - [MemberData(nameof(AllowedUsers))] - public async Task returns_expected_result(UserCredentials credentials) { - var result = await _fixture.Client.GetInfoToStreamAsync( - StreamName, - GroupName, - userCredentials: credentials - ); - - Assert.Equal(StreamName, result.EventSource); - Assert.Equal(GroupName, result.GroupName); - Assert.NotNull(_settings.StartFrom); - Assert.True(result.Stats.TotalItems > 0); - Assert.True(result.Stats.OutstandingMessagesCount > 0); - Assert.True(result.Stats.AveragePerSecond >= 0); - Assert.True(result.Stats.ParkedMessageCount >= 0); - Assert.True(result.Stats.AveragePerSecond >= 0); - Assert.True(result.Stats.ReadBufferCount >= 0); - Assert.True(result.Stats.RetryBufferCount >= 0); - Assert.True(result.Stats.CountSinceLastMeasurement >= 0); - Assert.True(result.Stats.TotalInFlightMessages >= 0); - Assert.NotNull(result.Stats.LastKnownEventPosition); - Assert.NotNull(result.Stats.LastCheckpointedEventPosition); - Assert.True(result.Stats.LiveBufferCount >= 0); - - Assert.NotNull(result.Connections); - Assert.NotEmpty(result.Connections); - var connection = result.Connections.First(); - Assert.NotNull(connection.From); - Assert.Equal(TestCredentials.Root.Username, connection.Username); - Assert.NotEmpty(connection.ConnectionName); - Assert.True(connection.AverageItemsPerSecond >= 0); - Assert.True(connection.TotalItems >= 0); - Assert.True(connection.CountSinceLastMeasurement >= 0); - Assert.True(connection.AvailableSlots >= 0); - Assert.True(connection.InFlightMessages >= 0); - Assert.NotNull(connection.ExtraStatistics); - Assert.NotEmpty(connection.ExtraStatistics); - - AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.Highest); - AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.Mean); - AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.Median); - AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.Fastest); - AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.Quintile1); - AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.Quintile2); - AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.Quintile3); - AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.Quintile4); - AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.Quintile5); - AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.NinetyPercent); - AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.NinetyFivePercent); - AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.NinetyNinePercent); - AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.NinetyNinePointFivePercent); - AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.NinetyNinePointNinePercent); - - Assert.NotNull(result.Settings); - Assert.Equal(_settings.StartFrom, result.Settings!.StartFrom); - Assert.Equal(_settings.ResolveLinkTos, result.Settings!.ResolveLinkTos); - Assert.Equal(_settings.ExtraStatistics, result.Settings!.ExtraStatistics); - Assert.Equal(_settings.MessageTimeout, result.Settings!.MessageTimeout); - Assert.Equal(_settings.MaxRetryCount, result.Settings!.MaxRetryCount); - Assert.Equal(_settings.LiveBufferSize, result.Settings!.LiveBufferSize); - Assert.Equal(_settings.ReadBatchSize, result.Settings!.ReadBatchSize); - Assert.Equal(_settings.HistoryBufferSize, result.Settings!.HistoryBufferSize); - Assert.Equal(_settings.CheckPointAfter, result.Settings!.CheckPointAfter); - Assert.Equal(_settings.CheckPointLowerBound, result.Settings!.CheckPointLowerBound); - Assert.Equal(_settings.CheckPointUpperBound, result.Settings!.CheckPointUpperBound); - Assert.Equal(_settings.MaxSubscriberCount, result.Settings!.MaxSubscriberCount); - Assert.Equal(_settings.ConsumerStrategyName, result.Settings!.ConsumerStrategyName); - } - - [Fact] - public async Task throws_when_given_non_existing_subscription() => - await Assert.ThrowsAsync( - async () => { - await _fixture.Client.GetInfoToStreamAsync( - "NonExisting", - "NonExisting", - userCredentials: TestCredentials.Root - ); - } - ); - - [Fact(Skip = "Unable to produce same behavior with HTTP fallback!")] - public async Task throws_with_non_existing_user() => - await Assert.ThrowsAsync( - async () => { - await _fixture.Client.GetInfoToStreamAsync( - "NonExisting", - "NonExisting", - userCredentials: TestCredentials.TestBadUser - ); - } - ); - - [Fact] - public async Task throws_with_no_credentials() => - await Assert.ThrowsAsync( - async () => { - await _fixture.Client.GetInfoToStreamAsync( - "NonExisting", - "NonExisting" - ); - } - ); - - [Fact] - public async Task returns_result_for_normal_user() { - var result = await _fixture.Client.GetInfoToStreamAsync( - StreamName, - GroupName, - userCredentials: TestCredentials.TestUser1 - ); - - Assert.NotNull(result); - } - - void AssertKeyAndValue(IDictionary items, string key) { - Assert.True(items.ContainsKey(key)); - Assert.True(items[key] > 0); - } - - public class Fixture : EventStoreClientFixture { - public Fixture() : base(noDefaultCredentials: true) { } - - protected override Task Given() => - Client.CreateToStreamAsync( - groupName: GroupName, - streamName: StreamName, - settings: _settings, - userCredentials: TestCredentials.Root - ); - - protected override async Task When() { - var counter = 0; - var tcs = new TaskCompletionSource(); - - await Client.SubscribeToStreamAsync( - StreamName, - GroupName, - (s, e, r, ct) => { - counter++; - - if (counter == 1) - s.Nack(PersistentSubscriptionNakEventAction.Park, "Test", e); - - if (counter > 10) - tcs.TrySetResult(); - - return Task.CompletedTask; - }, - userCredentials: TestCredentials.Root - ); - - for (var i = 0; i < 15; i++) - await StreamsClient.AppendToStreamAsync( - StreamName, - StreamState.Any, - new[] { - new EventData(Uuid.NewUuid(), "test-event", ReadOnlyMemory.Empty) - }, - userCredentials: TestCredentials.Root - ); - - await tcs.Task; - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/happy_case_catching_up_to_link_to_events_manual_ack_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/happy_case_catching_up_to_link_to_events_manual_ack_obsolete.cs deleted file mode 100644 index e69e88fb2..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/happy_case_catching_up_to_link_to_events_manual_ack_obsolete.cs +++ /dev/null @@ -1,80 +0,0 @@ -using System.Text; - -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream.Obsolete; - -[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class happy_case_catching_up_to_link_to_events_manual_ack_obsolete - : IClassFixture { - const string Stream = nameof(happy_case_catching_up_to_link_to_events_manual_ack_obsolete); - const string Group = nameof(Group); - const int BufferCount = 10; - const int EventWriteCount = BufferCount * 2; - - readonly Fixture _fixture; - - public happy_case_catching_up_to_link_to_events_manual_ack_obsolete(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task Test() => await _fixture.EventsReceived.WithTimeout(); - - public class Fixture : EventStoreClientFixture { - readonly EventData[] _events; - readonly TaskCompletionSource _eventsReceived; - int _eventReceivedCount; - - PersistentSubscription? _subscription; - - public Fixture() { - _events = CreateTestEvents(EventWriteCount) - .Select( - (e, i) => new EventData( - e.EventId, - SystemEventTypes.LinkTo, - Encoding.UTF8.GetBytes($"{i}@{Stream}"), - contentType: Constants.Metadata.ContentTypes.ApplicationOctetStream - ) - ) - .ToArray(); - - _eventsReceived = new(); - } - - public Task EventsReceived => _eventsReceived.Task; - - protected override async Task Given() { - foreach (var e in _events) - await StreamsClient.AppendToStreamAsync(Stream, StreamState.Any, new[] { e }); - - await Client.CreateToStreamAsync( - Stream, - Group, - new(startFrom: StreamPosition.Start, resolveLinkTos: true), - userCredentials: TestCredentials.Root - ); - - _subscription = await Client.SubscribeToStreamAsync( - Stream, - Group, - async (subscription, e, retryCount, ct) => { - await subscription.Ack(e); - - if (Interlocked.Increment(ref _eventReceivedCount) == _events.Length) - _eventsReceived.TrySetResult(true); - }, - (s, r, e) => { - if (e != null) - _eventsReceived.TrySetException(e); - }, - bufferSize: BufferCount, - userCredentials: TestCredentials.Root - ); - } - - protected override Task When() => Task.CompletedTask; - - public override Task DisposeAsync() { - _subscription?.Dispose(); - return base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/happy_case_catching_up_to_normal_events_manual_ack_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/happy_case_catching_up_to_normal_events_manual_ack_obsolete.cs deleted file mode 100644 index 6973087cc..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/happy_case_catching_up_to_normal_events_manual_ack_obsolete.cs +++ /dev/null @@ -1,67 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream.Obsolete; - -[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class happy_case_catching_up_to_normal_events_manual_ack_obsolete : IClassFixture { - const string Stream = nameof(happy_case_catching_up_to_normal_events_manual_ack_obsolete); - const string Group = nameof(Group); - const int BufferCount = 10; - const int EventWriteCount = BufferCount * 2; - - readonly Fixture _fixture; - - public happy_case_catching_up_to_normal_events_manual_ack_obsolete(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task Test() => await _fixture.EventsReceived.WithTimeout(); - - public class Fixture : EventStoreClientFixture { - readonly EventData[] _events; - readonly TaskCompletionSource _eventsReceived; - int _eventReceivedCount; - - PersistentSubscription? _subscription; - - public Fixture() { - _events = CreateTestEvents(EventWriteCount).ToArray(); - _eventsReceived = new(); - } - - public Task EventsReceived => _eventsReceived.Task; - - protected override async Task Given() { - foreach (var e in _events) - await StreamsClient.AppendToStreamAsync(Stream, StreamState.Any, new[] { e }); - - await Client.CreateToStreamAsync( - Stream, - Group, - new(startFrom: StreamPosition.Start, resolveLinkTos: true), - userCredentials: TestCredentials.Root - ); - - _subscription = await Client.SubscribeToStreamAsync( - Stream, - Group, - async (subscription, e, retryCount, ct) => { - await subscription.Ack(e); - - if (Interlocked.Increment(ref _eventReceivedCount) == _events.Length) - _eventsReceived.TrySetResult(true); - }, - (s, r, e) => { - if (e != null) - _eventsReceived.TrySetException(e); - }, - bufferSize: BufferCount, - userCredentials: TestCredentials.Root - ); - } - - protected override Task When() => Task.CompletedTask; - - public override Task DisposeAsync() { - _subscription?.Dispose(); - return base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/happy_case_writing_and_subscribing_to_normal_events_manual_ack_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/happy_case_writing_and_subscribing_to_normal_events_manual_ack_obsolete.cs deleted file mode 100644 index af07bcb4c..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/happy_case_writing_and_subscribing_to_normal_events_manual_ack_obsolete.cs +++ /dev/null @@ -1,67 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream.Obsolete; - -[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class happy_case_writing_and_subscribing_to_normal_events_manual_ack_obsolete - : IClassFixture { - const string Stream = nameof(happy_case_writing_and_subscribing_to_normal_events_manual_ack_obsolete); - const string Group = nameof(Group); - const int BufferCount = 10; - const int EventWriteCount = BufferCount * 2; - - readonly Fixture _fixture; - - public happy_case_writing_and_subscribing_to_normal_events_manual_ack_obsolete(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task Test() => await _fixture.EventsReceived.WithTimeout(); - - public class Fixture : EventStoreClientFixture { - readonly EventData[] _events; - readonly TaskCompletionSource _eventsReceived; - int _eventReceivedCount; - - PersistentSubscription? _subscription; - - public Fixture() { - _events = CreateTestEvents(EventWriteCount).ToArray(); - _eventsReceived = new(); - } - - public Task EventsReceived => _eventsReceived.Task; - - protected override async Task Given() { - await Client.CreateToStreamAsync( - Stream, - Group, - new(startFrom: StreamPosition.End, resolveLinkTos: true), - userCredentials: TestCredentials.Root - ); - - _subscription = await Client.SubscribeToStreamAsync( - Stream, - Group, - async (subscription, e, retryCount, ct) => { - await subscription.Ack(e); - if (Interlocked.Increment(ref _eventReceivedCount) == _events.Length) - _eventsReceived.TrySetResult(true); - }, - (s, r, e) => { - if (e != null) - _eventsReceived.TrySetException(e); - }, - bufferSize: BufferCount, - userCredentials: TestCredentials.Root - ); - } - - protected override async Task When() { - foreach (var e in _events) - await StreamsClient.AppendToStreamAsync(Stream, StreamState.Any, new[] { e }); - } - - public override Task DisposeAsync() { - _subscription?.Dispose(); - return base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/update_existing_with_check_point_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/update_existing_with_check_point_obsolete.cs deleted file mode 100644 index f5922c746..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/update_existing_with_check_point_obsolete.cs +++ /dev/null @@ -1,124 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream.Obsolete; - -[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class update_existing_with_check_point_obsolete - : IClassFixture { - const string Stream = nameof(update_existing_with_check_point_obsolete); - const string Group = "existing-with-check-point"; - readonly Fixture _fixture; - - public update_existing_with_check_point_obsolete(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task resumes_from_check_point() { - var resumedEvent = await _fixture.Resumed.WithTimeout(TimeSpan.FromSeconds(10)); - Assert.Equal(_fixture.CheckPoint.Next(), resumedEvent.Event.EventNumber); - } - - public class Fixture : EventStoreClientFixture { - readonly TaskCompletionSource _appeared; - readonly List _appearedEvents; - - readonly TaskCompletionSource<(SubscriptionDroppedReason, Exception?)> _droppedSource; - readonly EventData[] _events; - readonly TaskCompletionSource _resumedSource; - PersistentSubscription? _firstSubscription; - PersistentSubscription? _secondSubscription; - - public Fixture() { - _droppedSource = new(); - _resumedSource = new(); - _appeared = new(); - _appearedEvents = new(); - _events = CreateTestEvents(5).ToArray(); - } - - public Task Resumed => _resumedSource.Task; - public StreamPosition CheckPoint { get; private set; } - - protected override async Task Given() { - await StreamsClient.AppendToStreamAsync(Stream, StreamState.NoStream, _events); - - await Client.CreateToStreamAsync( - Stream, - Group, - new( - checkPointLowerBound: 5, - checkPointAfter: TimeSpan.FromSeconds(1), - startFrom: StreamPosition.Start - ), - userCredentials: TestCredentials.Root - ); - - var checkPointStream = $"$persistentsubscription-{Stream}::{Group}-checkpoint"; - - _firstSubscription = await Client.SubscribeToStreamAsync( - Stream, - Group, - async (s, e, _, _) => { - _appearedEvents.Add(e); - await s.Ack(e); - - if (_appearedEvents.Count == _events.Length) - _appeared.TrySetResult(true); - }, - (_, reason, ex) => _droppedSource.TrySetResult((reason, ex)), - TestCredentials.Root - ); - - await Task.WhenAll(_appeared.Task.WithTimeout(), Checkpointed()); - - return; - - async Task Checkpointed() { - await using var subscription = StreamsClient.SubscribeToStream(checkPointStream, FromStream.Start, - userCredentials: TestCredentials.Root); - await foreach (var message in subscription.Messages) { - if (message is not StreamMessage.Event(var resolvedEvent)) { - continue; - } - CheckPoint = resolvedEvent.Event.Data.ParseStreamPosition(); - return; - } - - throw new InvalidOperationException(); - } - } - - protected override async Task When() { - // Force restart of the subscription - await Client.UpdateToStreamAsync( - Stream, - Group, - new(), - userCredentials: TestCredentials.Root - ); - - await _droppedSource.Task.WithTimeout(); - - _secondSubscription = await Client.SubscribeToStreamAsync( - Stream, - Group, - async (s, e, _, _) => { - _resumedSource.TrySetResult(e); - await s.Ack(e); - }, - (_, reason, ex) => { - if (ex is not null) - _resumedSource.TrySetException(ex); - else - _resumedSource.TrySetResult(default); - }, - TestCredentials.Root - ); - - await StreamsClient.AppendToStreamAsync(Stream, StreamState.Any, CreateTestEvents(1)); - } - - public override Task DisposeAsync() { - _firstSubscription?.Dispose(); - _secondSubscription?.Dispose(); - return base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/update_existing_with_subscribers_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/update_existing_with_subscribers_obsolete.cs deleted file mode 100644 index 7ff215206..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/update_existing_with_subscribers_obsolete.cs +++ /dev/null @@ -1,61 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream.Obsolete; - -[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class update_existing_with_subscribers_obsolete - : IClassFixture { - const string Stream = nameof(update_existing_with_subscribers_obsolete); - const string Group = "existing"; - readonly Fixture _fixture; - - public update_existing_with_subscribers_obsolete(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task existing_subscriptions_are_dropped() { - var (reason, exception) = await _fixture.Dropped.WithTimeout(TimeSpan.FromSeconds(10)); - Assert.Equal(SubscriptionDroppedReason.ServerError, reason); - var ex = Assert.IsType(exception); - - Assert.Equal(Stream, ex.StreamName); - Assert.Equal(Group, ex.GroupName); - } - - public class Fixture : EventStoreClientFixture { - readonly TaskCompletionSource<(SubscriptionDroppedReason, Exception?)> _droppedSource; - PersistentSubscription? _subscription; - - public Fixture() => _droppedSource = new(); - - public Task<(SubscriptionDroppedReason, Exception?)> Dropped => _droppedSource.Task; - - protected override async Task Given() { - await StreamsClient.AppendToStreamAsync(Stream, StreamState.NoStream, CreateTestEvents()); - await Client.CreateToStreamAsync( - Stream, - Group, - new(), - userCredentials: TestCredentials.Root - ); - - _subscription = await Client.SubscribeToStreamAsync( - Stream, - Group, - delegate { return Task.CompletedTask; }, - (_, reason, ex) => _droppedSource.TrySetResult((reason, ex)), - TestCredentials.Root - ); - } - - protected override Task When() => - Client.UpdateToStreamAsync( - Stream, - Group, - new(), - userCredentials: TestCredentials.Root - ); - - public override Task DisposeAsync() { - _subscription?.Dispose(); - return base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/when_writing_and_subscribing_to_normal_events_manual_nack_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/when_writing_and_subscribing_to_normal_events_manual_nack_obsolete.cs deleted file mode 100644 index 96a2d0af0..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/when_writing_and_subscribing_to_normal_events_manual_nack_obsolete.cs +++ /dev/null @@ -1,70 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream.Obsolete; - -[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class when_writing_and_subscribing_to_normal_events_manual_nack_obsolete - : IClassFixture { - const string Stream = nameof(when_writing_and_subscribing_to_normal_events_manual_nack_obsolete); - const string Group = nameof(Group); - const int BufferCount = 10; - const int EventWriteCount = BufferCount * 2; - - readonly Fixture _fixture; - - public when_writing_and_subscribing_to_normal_events_manual_nack_obsolete(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task Test() => await _fixture.EventsReceived.WithTimeout(); - - public class Fixture : EventStoreClientFixture { - readonly EventData[] _events; - readonly TaskCompletionSource _eventsReceived; - int _eventReceivedCount; - - PersistentSubscription? _subscription; - - public Fixture() { - _events = CreateTestEvents(EventWriteCount) - .ToArray(); - - _eventsReceived = new(); - } - - public Task EventsReceived => _eventsReceived.Task; - - protected override async Task Given() { - await Client.CreateToStreamAsync( - Stream, - Group, - new(startFrom: StreamPosition.Start, resolveLinkTos: true), - userCredentials: TestCredentials.Root - ); - - _subscription = await Client.SubscribeToStreamAsync( - Stream, - Group, - async (subscription, e, retryCount, ct) => { - await subscription.Nack(PersistentSubscriptionNakEventAction.Park, "fail", e); - - if (Interlocked.Increment(ref _eventReceivedCount) == _events.Length) - _eventsReceived.TrySetResult(true); - }, - (s, r, e) => { - if (e != null) - _eventsReceived.TrySetException(e); - }, - bufferSize: BufferCount, - userCredentials: TestCredentials.Root - ); - } - - protected override async Task When() { - foreach (var e in _events) - await StreamsClient.AppendToStreamAsync(Stream, StreamState.Any, new[] { e }); - } - - public override Task DisposeAsync() { - _subscription?.Dispose(); - return base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/can_create_duplicate_name_on_different_streams.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/can_create_duplicate_name_on_different_streams.cs deleted file mode 100644 index cb9af1bd6..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/can_create_duplicate_name_on_different_streams.cs +++ /dev/null @@ -1,32 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; - -public class can_create_duplicate_name_on_different_streams - : IClassFixture { - const string Stream = - nameof(can_create_duplicate_name_on_different_streams); - - readonly Fixture _fixture; - - public can_create_duplicate_name_on_different_streams(Fixture fixture) => _fixture = fixture; - - [Fact] - public Task the_completion_succeeds() => - _fixture.Client.CreateToStreamAsync( - "someother" + Stream, - "group3211", - new(), - userCredentials: TestCredentials.Root - ); - - public class Fixture : EventStoreClientFixture { - protected override Task Given() => Task.CompletedTask; - - protected override Task When() => - Client.CreateToStreamAsync( - Stream, - "group3211", - new(), - userCredentials: TestCredentials.Root - ); - } -} \ No newline at end of file diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_max_one_client.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_max_one_client.cs deleted file mode 100644 index eb5e442ae..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_max_one_client.cs +++ /dev/null @@ -1,33 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; - -public class connect_to_existing_with_max_one_client - : IClassFixture { - private const string Group = "startinbeginning1"; - private const string Stream = nameof(connect_to_existing_with_max_one_client); - private readonly Fixture _fixture; - - public connect_to_existing_with_max_one_client(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task the_second_subscription_fails_to_connect() { - var ex = await Assert.ThrowsAsync( - () => Task.WhenAll(Subscribe().WithTimeout(), Subscribe().WithTimeout())); - - Assert.Equal(Stream, ex.StreamName); - Assert.Equal(Group, ex.GroupName); - return; - - async Task Subscribe() { - await using var subscription = - _fixture.Client.SubscribeToStream(Stream, Group, userCredentials: TestCredentials.Root); - await subscription.Messages.AnyAsync(); - } - } - - public class Fixture : EventStoreClientFixture { - protected override Task Given() => Client.CreateToStreamAsync(Stream, Group, new(maxSubscriberCount: 1), - userCredentials: TestCredentials.Root); - - protected override Task When() => Task.CompletedTask; - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_permissions.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_permissions.cs deleted file mode 100644 index 70f4b51f0..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_permissions.cs +++ /dev/null @@ -1,26 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; - -public class connect_to_existing_with_permissions - : IClassFixture { - private const string Stream = nameof(connect_to_existing_with_permissions); - - private readonly Fixture _fixture; - - public connect_to_existing_with_permissions(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task the_subscription_succeeds() { - await using var subscription = - _fixture.Client.SubscribeToStream(Stream, "agroupname17", userCredentials: TestCredentials.Root); - - Assert.True(await subscription.Messages - .FirstAsync().AsTask().WithTimeout() is PersistentSubscriptionMessage.SubscriptionConfirmation); - } - - public class Fixture : EventStoreClientFixture { - protected override Task Given() => - Client.CreateToStreamAsync(Stream, "agroupname17", new(), userCredentials: TestCredentials.Root); - - protected override Task When() => Task.CompletedTask; - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_beginning_and_events_in_it.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_beginning_and_events_in_it.cs deleted file mode 100644 index 2c0f5225e..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_beginning_and_events_in_it.cs +++ /dev/null @@ -1,51 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; - -public class connect_to_existing_with_start_from_beginning_and_events_in_it - : IClassFixture { - private const string Group = "startinbeginning1"; - private const string Stream = nameof(connect_to_existing_with_start_from_beginning_and_events_in_it); - - private readonly Fixture _fixture; - - public connect_to_existing_with_start_from_beginning_and_events_in_it(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task the_subscription_gets_event_zero_as_its_first_event() { - var resolvedEvent = await _fixture.Subscription!.Messages.OfType() - .Select(e => e.ResolvedEvent) - .FirstOrDefaultAsync().AsTask().WithTimeout(); - - Assert.Equal(StreamPosition.Start, resolvedEvent.Event.EventNumber); - Assert.Equal(_fixture.Events[0].EventId, resolvedEvent.Event.EventId); - } - - public class Fixture : EventStoreClientFixture { - public readonly EventData[] Events; - public EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult? Subscription { get; private set; } - - public Fixture() { - Events = CreateTestEvents(10).ToArray(); - } - - protected override async Task Given() { - await StreamsClient.AppendToStreamAsync(Stream, StreamState.NoStream, Events); - await Client.CreateToStreamAsync(Stream, Group, new(startFrom: StreamPosition.Start), - userCredentials: TestCredentials.Root); - } - - protected override Task When() { - Subscription = Client.SubscribeToStream(Stream, Group, userCredentials: TestCredentials.TestUser1); - - return Task.CompletedTask; - } - - public override async Task DisposeAsync() { - if (Subscription is not null) { - await Subscription.DisposeAsync(); - } - - await base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_beginning_and_no_stream.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_beginning_and_no_stream.cs deleted file mode 100644 index a6e9cb26b..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_beginning_and_no_stream.cs +++ /dev/null @@ -1,51 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; - -public class connect_to_existing_with_start_from_beginning_and_no_stream - : IClassFixture { - private const string Group = "startinbeginning1"; - private const string Stream = nameof(connect_to_existing_with_start_from_beginning_and_no_stream); - - private readonly Fixture _fixture; - - public connect_to_existing_with_start_from_beginning_and_no_stream(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task the_subscription_gets_event_zero_as_its_first_event() { - var resolvedEvent = await _fixture.Subscription!.Messages.OfType() - .Select(e => e.ResolvedEvent) - .FirstOrDefaultAsync().AsTask().WithTimeout(); - - Assert.Equal(StreamPosition.Start, resolvedEvent.Event.EventNumber); - Assert.Equal(_fixture.EventId, resolvedEvent.Event.EventId); - } - - public class Fixture : EventStoreClientFixture { - public readonly EventData[] Events; - public EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult? Subscription { get; private set; } - - public Fixture() { - Events = CreateTestEvents().ToArray(); - } - - public Uuid EventId => Events.Single().EventId; - - protected override async Task Given() { - await Client.CreateToStreamAsync( - Stream, - Group, - new(), - userCredentials: TestCredentials.Root); - - Subscription = Client.SubscribeToStream(Stream, Group, userCredentials: TestCredentials.TestUser1); - } - - protected override Task When() => StreamsClient.AppendToStreamAsync(Stream, StreamState.NoStream, Events); - - public override async Task DisposeAsync() { - if (Subscription is not null) { - await Subscription.DisposeAsync(); - } - await base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_not_set_and_events_in_it.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_not_set_and_events_in_it.cs deleted file mode 100644 index cef6a28a7..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_not_set_and_events_in_it.cs +++ /dev/null @@ -1,52 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; - -[Obsolete] -public class connect_to_existing_with_start_from_not_set_and_events_in_it - : IClassFixture { - private const string Group = "startinbeginning1"; - - private const string Stream = nameof(connect_to_existing_with_start_from_not_set_and_events_in_it); - private readonly Fixture _fixture; - - public connect_to_existing_with_start_from_not_set_and_events_in_it(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task the_subscription_gets_no_events() => - await Assert.ThrowsAsync( - () => _fixture.Subscription!.Messages.AnyAsync(message => message is PersistentSubscriptionMessage.Event) - .AsTask().WithTimeout(TimeSpan.FromMilliseconds(250))); - - public class Fixture : EventStoreClientFixture { - public readonly EventData[] Events; - public EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult? Subscription { get; private set; } - - public Fixture() { - Events = CreateTestEvents(10).ToArray(); - } - - protected override async Task Given() { - await StreamsClient.AppendToStreamAsync(Stream, StreamState.NoStream, Events); - await Client.CreateToStreamAsync( - Stream, - Group, - new(), - userCredentials: TestCredentials.Root - ); - } - - protected override Task When() { - Subscription = Client.SubscribeToStream( - Stream, - Group, - userCredentials: TestCredentials.TestUser1); - return Task.CompletedTask; - } - - public override async Task DisposeAsync() { - if (Subscription is not null) { - await Subscription.DisposeAsync(); - } - await base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_not_set_and_events_in_it_then_event_written.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_not_set_and_events_in_it_then_event_written.cs deleted file mode 100644 index 08a2172d4..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_not_set_and_events_in_it_then_event_written.cs +++ /dev/null @@ -1,58 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; - -public class connect_to_existing_with_start_from_not_set_and_events_in_it_then_event_written - : IClassFixture { - private const string Group = "startinbeginning1"; - - private const string Stream = - nameof(connect_to_existing_with_start_from_not_set_and_events_in_it_then_event_written); - - private readonly Fixture _fixture; - - public connect_to_existing_with_start_from_not_set_and_events_in_it_then_event_written(Fixture fixture) => - _fixture = fixture; - - [Fact] - public async Task the_subscription_gets_the_written_event_as_its_first_event() { - var resolvedEvent = await _fixture.Subscription!.Messages.OfType() - .Select(e => e.ResolvedEvent) - .FirstOrDefaultAsync().AsTask().WithTimeout(); - - Assert.Equal(new(10), resolvedEvent.Event.EventNumber); - Assert.Equal(_fixture.Events.Last().EventId, resolvedEvent.Event.EventId); - } - - public class Fixture : EventStoreClientFixture { - public readonly EventData[] Events; - public EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult? Subscription { get; private set; } - - public Fixture() { - Events = CreateTestEvents(11).ToArray(); - } - - protected override async Task Given() { - await StreamsClient.AppendToStreamAsync(Stream, StreamState.NoStream, Events.Take(10)); - await Client.CreateToStreamAsync( - Stream, - Group, - new(), - userCredentials: TestCredentials.Root - ); - - Subscription = Client.SubscribeToStream( - Stream, - Group, - userCredentials: TestCredentials.TestUser1); - } - - protected override Task When() => - StreamsClient.AppendToStreamAsync(Stream, new StreamRevision(9), Events.Skip(10)); - - public override async Task DisposeAsync() { - if (Subscription is not null) { - await Subscription.DisposeAsync(); - } - await base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_set_to_end_position_and_events_in_it.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_set_to_end_position_and_events_in_it.cs deleted file mode 100644 index f2bf5e807..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_set_to_end_position_and_events_in_it.cs +++ /dev/null @@ -1,53 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; - -public class connect_to_existing_with_start_from_set_to_end_position_and_events_in_it - : IClassFixture { - private const string Group = "startinbeginning1"; - private const string Stream = nameof(connect_to_existing_with_start_from_set_to_end_position_and_events_in_it); - - private readonly Fixture _fixture; - - public connect_to_existing_with_start_from_set_to_end_position_and_events_in_it(Fixture fixture) => - _fixture = fixture; - - [Fact] - public async Task the_subscription_gets_no_events() => - await Assert.ThrowsAsync( - () => _fixture.Subscription!.Messages.AnyAsync(message => message is PersistentSubscriptionMessage.Event) - .AsTask().WithTimeout(TimeSpan.FromMilliseconds(250))); - - public class Fixture : EventStoreClientFixture { - public readonly EventData[] Events; - public EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult? Subscription { get; private set; } - - public Fixture() { - Events = CreateTestEvents(10).ToArray(); - } - - protected override async Task Given() { - await StreamsClient.AppendToStreamAsync(Stream, StreamState.NoStream, Events); - await Client.CreateToStreamAsync( - Stream, - Group, - new(startFrom: StreamPosition.End), - userCredentials: TestCredentials.Root - ); - } - - protected override Task When() { - Subscription = Client.SubscribeToStream( - Stream, - Group, - userCredentials: TestCredentials.TestUser1); - return Task.CompletedTask; - } - - public override async Task DisposeAsync() { - if (Subscription is not null) { - await Subscription.DisposeAsync(); - } - - await base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_set_to_end_position_and_events_in_it_then_event_written.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_set_to_end_position_and_events_in_it_then_event_written.cs deleted file mode 100644 index 2e7b8a4c1..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_set_to_end_position_and_events_in_it_then_event_written.cs +++ /dev/null @@ -1,57 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; - -public class connect_to_existing_with_start_from_set_to_end_position_and_events_in_it_then_event_written - : IClassFixture { - private const string Group = "startinbeginning1"; - - private const string Stream = - nameof(connect_to_existing_with_start_from_set_to_end_position_and_events_in_it_then_event_written); - - private readonly Fixture _fixture; - - public - connect_to_existing_with_start_from_set_to_end_position_and_events_in_it_then_event_written(Fixture fixture) => - _fixture = fixture; - - [Fact] - public async Task the_subscription_gets_the_written_event_as_its_first_event() { - var resolvedEvent = await _fixture.Subscription!.Messages.OfType() - .Select(e => e.ResolvedEvent) - .FirstOrDefaultAsync().AsTask().WithTimeout(); - - Assert.Equal(new(10), resolvedEvent.Event.EventNumber); - Assert.Equal(_fixture.Events.Last().EventId, resolvedEvent.Event.EventId); - } - - public class Fixture : EventStoreClientFixture { - public readonly EventData[] Events; - public EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult? Subscription { get; private set; } - - public Fixture() { - Events = CreateTestEvents(11).ToArray(); - } - - protected override async Task Given() { - await StreamsClient.AppendToStreamAsync(Stream, StreamState.NoStream, Events.Take(10)); - await Client.CreateToStreamAsync( - Stream, - Group, - new(startFrom: StreamPosition.End), - userCredentials: TestCredentials.Root); - - Subscription = Client.SubscribeToStream(Stream, Group, userCredentials: TestCredentials.TestUser1); - } - - protected override Task When() => - StreamsClient.AppendToStreamAsync(Stream, new StreamRevision(9), Events.Skip(10)); - - public override async Task DisposeAsync() { - if (Subscription is not null) { - await Subscription.DisposeAsync(); - } - - await base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_two_and_no_stream.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_two_and_no_stream.cs deleted file mode 100644 index 0199eaa3b..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_two_and_no_stream.cs +++ /dev/null @@ -1,55 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; - -public class connect_to_existing_with_start_from_two_and_no_stream - : IClassFixture { - private const string Group = "startinbeginning1"; - private const string Stream = nameof(connect_to_existing_with_start_from_two_and_no_stream); - - private readonly Fixture _fixture; - - public connect_to_existing_with_start_from_two_and_no_stream(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task the_subscription_gets_event_two_as_its_first_event() { - var resolvedEvent = await _fixture.Subscription!.Messages.OfType() - .Select(e => e.ResolvedEvent) - .FirstOrDefaultAsync().AsTask().WithTimeout(); - - Assert.Equal(new(2), resolvedEvent.Event.EventNumber); - Assert.Equal(_fixture.EventId, resolvedEvent.Event.EventId); - } - - public class Fixture : EventStoreClientFixture { - public readonly EventData[] Events; - public EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult? Subscription { get; private set; } - - public Fixture() { - Events = CreateTestEvents(3).ToArray(); - } - - public Uuid EventId => Events.Last().EventId; - - protected override async Task Given() { - await Client.CreateToStreamAsync( - Stream, - Group, - new(startFrom: new StreamPosition(2)), - userCredentials: TestCredentials.Root); - - Subscription = Client.SubscribeToStream( - Stream, - Group, - userCredentials: TestCredentials.TestUser1); - } - - protected override Task When() => StreamsClient.AppendToStreamAsync(Stream, StreamState.NoStream, Events); - - public override async Task DisposeAsync() { - if (Subscription is not null) { - await Subscription.DisposeAsync(); - } - - await base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_x_set_and_events_in_it.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_x_set_and_events_in_it.cs deleted file mode 100644 index 3a7edafba..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_x_set_and_events_in_it.cs +++ /dev/null @@ -1,55 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; - -public class connect_to_existing_with_start_from_x_set_and_events_in_it - : IClassFixture { - private const string Group = "startinx2"; - private const string Stream = nameof(connect_to_existing_with_start_from_x_set_and_events_in_it); - - private readonly Fixture _fixture; - - public connect_to_existing_with_start_from_x_set_and_events_in_it(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task the_subscription_gets_the_written_event_as_its_first_event() { - var resolvedEvent = await _fixture.Subscription!.Messages - .OfType() - .Select(e => e.ResolvedEvent) - .FirstOrDefaultAsync().AsTask().WithTimeout(); - Assert.Equal(new(4), resolvedEvent.Event.EventNumber); - Assert.Equal(_fixture.Events.Skip(4).First().EventId, resolvedEvent.Event.EventId); - } - - public class Fixture : EventStoreClientFixture { - public readonly EventData[] Events; - public EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult? Subscription { get; private set; } - - public Fixture() { - Events = CreateTestEvents(10).ToArray(); - } - - protected override async Task Given() { - await StreamsClient.AppendToStreamAsync(Stream, StreamState.NoStream, Events.Take(10)); - await Client.CreateToStreamAsync( - Stream, - Group, - new(startFrom: new StreamPosition(4)), - userCredentials: TestCredentials.Root); - - Subscription = Client.SubscribeToStream( - Stream, - Group, - userCredentials: TestCredentials.TestUser1); - } - - protected override Task When() => - StreamsClient.AppendToStreamAsync(Stream, new StreamRevision(9), Events.Skip(10)); - - public override async Task DisposeAsync() { - if (Subscription is not null) { - await Subscription.DisposeAsync(); - } - - await base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_x_set_and_events_in_it_then_event_written.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_x_set_and_events_in_it_then_event_written.cs deleted file mode 100644 index 276bae9aa..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_x_set_and_events_in_it_then_event_written.cs +++ /dev/null @@ -1,54 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; - -public class connect_to_existing_with_start_from_x_set_and_events_in_it_then_event_written - : IClassFixture { - private const string Group = "startinbeginning1"; - private const string Stream = nameof(connect_to_existing_with_start_from_x_set_and_events_in_it_then_event_written); - private readonly Fixture _fixture; - - public connect_to_existing_with_start_from_x_set_and_events_in_it_then_event_written(Fixture fixture) => - _fixture = fixture; - - [Fact] - public async Task the_subscription_gets_the_written_event_as_its_first_event() { - var resolvedEvent = await _fixture.Subscription!.Messages.OfType() - .Select(e => e.ResolvedEvent) - .FirstOrDefaultAsync().AsTask().WithTimeout(); - Assert.Equal(new(10), resolvedEvent.Event.EventNumber); - Assert.Equal(_fixture.Events.Last().EventId, resolvedEvent.Event.EventId); - } - - public class Fixture : EventStoreClientFixture { - public readonly EventData[] Events; - public EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult? Subscription { get; private set; } - - public Fixture() { - Events = CreateTestEvents(11).ToArray(); - } - - protected override async Task Given() { - await StreamsClient.AppendToStreamAsync(Stream, StreamState.NoStream, Events.Take(10)); - await Client.CreateToStreamAsync( - Stream, - Group, - new(startFrom: new StreamPosition(10)), - userCredentials: TestCredentials.Root); - - Subscription = Client.SubscribeToStream( - Stream, - Group, - userCredentials: TestCredentials.TestUser1); - } - - protected override Task When() => - StreamsClient.AppendToStreamAsync(Stream, new StreamRevision(9), Events.Skip(10)); - - public override async Task DisposeAsync() { - if (Subscription is not null) { - await Subscription.DisposeAsync(); - } - - await base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_x_set_higher_than_x_and_events_in_it_then_event_written.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_x_set_higher_than_x_and_events_in_it_then_event_written.cs deleted file mode 100644 index f4b249372..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_x_set_higher_than_x_and_events_in_it_then_event_written.cs +++ /dev/null @@ -1,58 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; - -public class connect_to_existing_with_start_from_x_set_higher_than_x_and_events_in_it_then_event_written : - IClassFixture { - private const string Group = "startinbeginning1"; - private const string Stream = - nameof(connect_to_existing_with_start_from_x_set_higher_than_x_and_events_in_it_then_event_written); - - private readonly Fixture _fixture; - - public - connect_to_existing_with_start_from_x_set_higher_than_x_and_events_in_it_then_event_written(Fixture fixture) => - _fixture = fixture; - - [Fact] - public async Task the_subscription_gets_the_written_event_as_its_first_event() { - var resolvedEvent = await _fixture.Subscription!.Messages.OfType() - .Select(e => e.ResolvedEvent) - .FirstOrDefaultAsync().AsTask().WithTimeout(); - Assert.Equal(new(11), resolvedEvent.Event.EventNumber); - Assert.Equal(_fixture.Events.Last().EventId, resolvedEvent.Event.EventId); - } - - public class Fixture : EventStoreClientFixture { - public readonly EventData[] Events; - public EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult? Subscription { get; private set; } - - public Fixture() { - Events = CreateTestEvents(12).ToArray(); - } - - protected override async Task Given() { - await StreamsClient.AppendToStreamAsync(Stream, StreamState.NoStream, Events.Take(11)); - await Client.CreateToStreamAsync( - Stream, - Group, - new(startFrom: new StreamPosition(11)), - userCredentials: TestCredentials.Root); - - Subscription = Client.SubscribeToStream( - Stream, - Group, - userCredentials: TestCredentials.TestUser1); - } - - protected override Task When() => - StreamsClient.AppendToStreamAsync(Stream, new StreamRevision(10), Events.Skip(11)); - - public override async Task DisposeAsync() { - if (Subscription is not null) { - await Subscription.DisposeAsync(); - } - - await base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_without_permissions.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_without_permissions.cs deleted file mode 100644 index 756de2519..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_without_permissions.cs +++ /dev/null @@ -1,29 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; - -public class connect_to_existing_without_permissions : IClassFixture { - private const string Stream = $"${nameof(connect_to_existing_without_permissions)}"; - private readonly Fixture _fixture; - public connect_to_existing_without_permissions(Fixture fixture) => _fixture = fixture; - - [Fact] - public Task throws_access_denied() => - Assert.ThrowsAsync( - async () => { - await using var subscription = _fixture.Client.SubscribeToStream(Stream, "agroupname55"); - await subscription.Messages.AnyAsync(); - }).WithTimeout(); - - public class Fixture : EventStoreClientFixture { - public Fixture() : base(noDefaultCredentials: true) { } - - protected override Task Given() => - Client.CreateToStreamAsync( - Stream, - "agroupname55", - new(), - userCredentials: TestCredentials.Root - ); - - protected override Task When() => Task.CompletedTask; - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_non_existing_with_permissions.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_non_existing_with_permissions.cs deleted file mode 100644 index 3e13c3983..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_non_existing_with_permissions.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; - -public class connect_to_non_existing_with_permissions : IClassFixture { - private const string Stream = nameof(connect_to_non_existing_with_permissions); - private const string Group = "foo"; - - private readonly Fixture _fixture; - - public connect_to_non_existing_with_permissions(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task throws_persistent_subscription_not_found() { - await using var subscription = _fixture.Client.SubscribeToStream(Stream, Group, userCredentials: TestCredentials.Root); - Assert.True(await subscription.Messages.OfType() - .AnyAsync() - .AsTask() - .WithTimeout()); - } - - public class Fixture : EventStoreClientFixture { - protected override Task Given() => Task.CompletedTask; - protected override Task When() => Task.CompletedTask; - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_with_retries.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_with_retries.cs deleted file mode 100644 index 89995fedf..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_with_retries.cs +++ /dev/null @@ -1,60 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; - -public class connect_with_retries : IClassFixture { - private const string Group = "retries"; - private const string Stream = nameof(connect_with_retries); - - private readonly Fixture _fixture; - - public connect_with_retries(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task events_are_retried_until_success() { - var retryCount = await _fixture.Subscription!.Messages.OfType() - .SelectAwait(async e => { - if (e.RetryCount > 4) { - await _fixture.Subscription.Ack(e.ResolvedEvent); - } else { - await _fixture.Subscription.Nack(PersistentSubscriptionNakEventAction.Retry, - "Not yet tried enough times", e.ResolvedEvent); - } - - return e.RetryCount; - }) - .Where(retryCount => retryCount > 4) - .FirstOrDefaultAsync() - .AsTask() - .WithTimeout(); - - Assert.Equal(5, retryCount); - } - - public class Fixture : EventStoreClientFixture { - - public readonly EventData[] Events; - - public EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult? Subscription { get; private set; } - - public Fixture() { - Events = CreateTestEvents().ToArray(); - } - - protected override async Task Given() { - await StreamsClient.AppendToStreamAsync(Stream, StreamState.NoStream, Events); - await Client.CreateToStreamAsync(Stream, Group, new(startFrom: StreamPosition.Start), - userCredentials: TestCredentials.Root); - - Subscription = Client.SubscribeToStream(Stream, Group, userCredentials: TestCredentials.TestUser1); - } - - protected override Task When() => StreamsClient.AppendToStreamAsync(Stream, StreamState.NoStream, Events); - - public override async Task DisposeAsync() { - if (Subscription is not null) { - await Subscription.DisposeAsync(); - } - - await base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connecting_to_a_persistent_subscription.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connecting_to_a_persistent_subscription.cs deleted file mode 100644 index a9dbb4b59..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connecting_to_a_persistent_subscription.cs +++ /dev/null @@ -1,47 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; - -public class connecting_to_a_persistent_subscription : IClassFixture { - private const string Group = "startinbeginning1"; - private const string Stream = nameof(connecting_to_a_persistent_subscription); - - private readonly Fixture _fixture; - - public connecting_to_a_persistent_subscription(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task the_subscription_gets_the_written_event_as_its_first_event() { - var resolvedEvent = await _fixture.Subscription!.Messages.OfType() - .Select(e => e.ResolvedEvent) - .FirstOrDefaultAsync().AsTask().WithTimeout(); - Assert.Equal(new(11), resolvedEvent.Event.EventNumber); - Assert.Equal(_fixture.Events.Last().EventId, resolvedEvent.Event.EventId); - } - - public class Fixture : EventStoreClientFixture { - public readonly EventData[] Events; - public EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult? Subscription { get; private set; } - - public Fixture() { - Events = CreateTestEvents(12).ToArray(); - } - - protected override async Task Given() { - await StreamsClient.AppendToStreamAsync(Stream, StreamState.NoStream, Events.Take(11)); - await Client.CreateToStreamAsync(Stream, Group, new(startFrom: new StreamPosition(11)), - userCredentials: TestCredentials.Root); - - Subscription = Client.SubscribeToStream(Stream, Group, userCredentials: TestCredentials.TestUser1); - } - - protected override Task When() => - StreamsClient.AppendToStreamAsync(Stream, new StreamRevision(10), Events.Skip(11)); - - public override async Task DisposeAsync() { - if (Subscription is not null) { - await Subscription.DisposeAsync(); - } - - await base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/create_after_deleting_the_same.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/create_after_deleting_the_same.cs deleted file mode 100644 index 3e8e0a6c5..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/create_after_deleting_the_same.cs +++ /dev/null @@ -1,38 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; - -public class create_after_deleting_the_same - : IClassFixture { - const string Stream = nameof(create_after_deleting_the_same); - readonly Fixture _fixture; - - public create_after_deleting_the_same(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task the_completion_succeeds() => - await _fixture.Client.CreateToStreamAsync( - Stream, - "existing", - new(), - userCredentials: TestCredentials.Root - ); - - public class Fixture : EventStoreClientFixture { - protected override Task Given() => Task.CompletedTask; - - protected override async Task When() { - await StreamsClient.AppendToStreamAsync(Stream, StreamState.Any, CreateTestEvents()); - await Client.CreateToStreamAsync( - Stream, - "existing", - new(), - userCredentials: TestCredentials.Root - ); - - await Client.DeleteToStreamAsync( - Stream, - "existing", - userCredentials: TestCredentials.Root - ); - } - } -} \ No newline at end of file diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/create_duplicate.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/create_duplicate.cs deleted file mode 100644 index c67748a7a..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/create_duplicate.cs +++ /dev/null @@ -1,37 +0,0 @@ -using Grpc.Core; - -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; - -public class create_duplicate - : IClassFixture { - const string Stream = nameof(create_duplicate); - readonly Fixture _fixture; - - public create_duplicate(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task the_completion_fails() { - var ex = await Assert.ThrowsAsync( - () => _fixture.Client.CreateToStreamAsync( - Stream, - "group32", - new(), - userCredentials: TestCredentials.Root - ) - ); - - Assert.Equal(StatusCode.AlreadyExists, ex.StatusCode); - } - - public class Fixture : EventStoreClientFixture { - protected override Task Given() => Task.CompletedTask; - - protected override Task When() => - Client.CreateToStreamAsync( - Stream, - "group32", - new(), - userCredentials: TestCredentials.Root - ); - } -} \ No newline at end of file diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/create_on_existing_stream.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/create_on_existing_stream.cs deleted file mode 100644 index 6ad81a6db..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/create_on_existing_stream.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; - -public class create_on_existing_stream - : IClassFixture { - const string Stream = nameof(create_on_existing_stream); - readonly Fixture _fixture; - - public create_on_existing_stream(Fixture fixture) => _fixture = fixture; - - [Fact] - public Task the_completion_succeeds() => - _fixture.Client.CreateToStreamAsync( - Stream, - "existing", - new(), - userCredentials: TestCredentials.Root - ); - - public class Fixture : EventStoreClientFixture { - protected override Task Given() => Task.CompletedTask; - - protected override async Task When() => await StreamsClient.AppendToStreamAsync(Stream, StreamState.Any, CreateTestEvents()); - } -} \ No newline at end of file diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/create_on_non_existing_stream.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/create_on_non_existing_stream.cs deleted file mode 100644 index f0d7e0fba..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/create_on_non_existing_stream.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; - -public class create_on_non_existing_stream - : IClassFixture { - const string Stream = nameof(create_on_non_existing_stream); - readonly Fixture _fixture; - - public create_on_non_existing_stream(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task the_completion_succeeds() => - await _fixture.Client.CreateToStreamAsync( - Stream, - "nonexistinggroup", - new(), - userCredentials: TestCredentials.Root - ); - - public class Fixture : EventStoreClientFixture { - protected override Task Given() => Task.CompletedTask; - protected override Task When() => Task.CompletedTask; - } -} \ No newline at end of file diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/create_persistent_subscription_with_dont_timeout.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/create_persistent_subscription_with_dont_timeout.cs deleted file mode 100644 index d32c65b4c..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/create_persistent_subscription_with_dont_timeout.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; - -public class create_with_dont_timeout - : IClassFixture { - const string Stream = nameof(create_with_dont_timeout); - readonly Fixture _fixture; - - public create_with_dont_timeout(Fixture fixture) => _fixture = fixture; - - [Fact] - public Task the_subscription_is_created_without_error() => - _fixture.Client.CreateToStreamAsync( - Stream, - "dont-timeout", - new(messageTimeout: TimeSpan.Zero), - userCredentials: TestCredentials.Root - ); - - public class Fixture : EventStoreClientFixture { - protected override Task Given() => Task.CompletedTask; - protected override Task When() => Task.CompletedTask; - } -} \ No newline at end of file diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/create_without_permissions.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/create_without_permissions.cs deleted file mode 100644 index d2838cf34..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/create_without_permissions.cs +++ /dev/null @@ -1,27 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; - -public class create_without_permissions - : IClassFixture { - const string Stream = nameof(create_without_permissions); - readonly Fixture _fixture; - - public create_without_permissions(Fixture fixture) => _fixture = fixture; - - [Fact] - public Task the_completion_fails_with_access_denied() => - Assert.ThrowsAsync( - () => - _fixture.Client.CreateToStreamAsync( - Stream, - "group57", - new() - ) - ); - - public class Fixture : EventStoreClientFixture { - public Fixture() : base(noDefaultCredentials: true) { } - - protected override Task Given() => Task.CompletedTask; - protected override Task When() => Task.CompletedTask; - } -} \ No newline at end of file diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/deleting_existing_with_permissions.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/deleting_existing_with_permissions.cs deleted file mode 100644 index 4d3a0f17d..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/deleting_existing_with_permissions.cs +++ /dev/null @@ -1,29 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; - -public class deleting_existing_with_permissions - : IClassFixture { - const string Stream = nameof(deleting_existing_with_permissions); - readonly Fixture _fixture; - - public deleting_existing_with_permissions(Fixture fixture) => _fixture = fixture; - - [Fact] - public Task the_delete_of_group_succeeds() => - _fixture.Client.DeleteToStreamAsync( - Stream, - "groupname123", - userCredentials: TestCredentials.Root - ); - - public class Fixture : EventStoreClientFixture { - protected override Task Given() => Task.CompletedTask; - - protected override Task When() => - Client.CreateToStreamAsync( - Stream, - "groupname123", - new(), - userCredentials: TestCredentials.Root - ); - } -} \ No newline at end of file diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/deleting_existing_with_subscriber.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/deleting_existing_with_subscriber.cs deleted file mode 100644 index dae83cec6..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/deleting_existing_with_subscriber.cs +++ /dev/null @@ -1,26 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; - -public class deleting_existing_with_subscriber : IClassFixture { - private const string Stream = nameof(deleting_existing_with_subscriber); - private readonly Fixture _fixture; - - public deleting_existing_with_subscriber(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task the_subscription_is_dropped_with_not_found() { - await using var subscription = _fixture.Client.SubscribeToStream(Stream, "groupname123", userCredentials: TestCredentials.Root); - - Assert.True(await subscription.Messages.OfType().AnyAsync() - .AsTask() - .WithTimeout()); - } - - public class Fixture : EventStoreClientFixture { - protected override async Task Given() { - await Client.CreateToStreamAsync(Stream, "groupname123", new(), userCredentials: TestCredentials.Root); - } - - protected override Task When() => - Client.DeleteToStreamAsync(Stream, "groupname123", userCredentials: TestCredentials.Root); - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/deleting_nonexistent.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/deleting_nonexistent.cs deleted file mode 100644 index 58ad6bc13..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/deleting_nonexistent.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; - -public class deleting_nonexistent - : IClassFixture { - const string Stream = nameof(deleting_nonexistent); - readonly Fixture _fixture; - - public deleting_nonexistent(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task the_delete_fails_with_argument_exception() => - await Assert.ThrowsAsync( - () => _fixture.Client.DeleteToStreamAsync( - Stream, - Guid.NewGuid().ToString(), - userCredentials: TestCredentials.Root - ) - ); - - public class Fixture : EventStoreClientFixture { - protected override Task Given() => Task.CompletedTask; - protected override Task When() => Task.CompletedTask; - } -} \ No newline at end of file diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/deleting_without_permissions.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/deleting_without_permissions.cs deleted file mode 100644 index 31dab0ba4..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/deleting_without_permissions.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; - -public class deleting_without_permissions - : IClassFixture { - const string Stream = nameof(deleting_without_permissions); - readonly Fixture _fixture; - - public deleting_without_permissions(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task the_delete_fails_with_access_denied() => - await Assert.ThrowsAsync( - () => _fixture.Client.DeleteToStreamAsync( - Stream, - Guid.NewGuid().ToString() - ) - ); - - public class Fixture : EventStoreClientFixture { - public Fixture() : base(noDefaultCredentials: true) { } - - protected override Task Given() => Task.CompletedTask; - protected override Task When() => Task.CompletedTask; - } -} \ No newline at end of file diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/get_info.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/get_info.cs deleted file mode 100644 index bec3c4118..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/get_info.cs +++ /dev/null @@ -1,198 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; - -public class get_info : IClassFixture { - private const string GroupName = nameof(get_info); - private const string StreamName = nameof(get_info); - - private static readonly PersistentSubscriptionSettings _settings = new( - true, - StreamPosition.Start, - true, - TimeSpan.FromSeconds(9), - 11, - 303, - 30, - 909, - TimeSpan.FromSeconds(1), - 1, - 1, - 500, - SystemConsumerStrategies.RoundRobin - ); - - private readonly Fixture _fixture; - - public get_info(Fixture fixture) => _fixture = fixture; - - public static IEnumerable AllowedUsers() { - yield return new object[] { TestCredentials.Root }; - yield return new object[] { TestCredentials.TestUser1 }; - } - - [Theory] - [MemberData(nameof(AllowedUsers))] - public async Task returns_expected_result(UserCredentials credentials) { - var result = await _fixture.Client.GetInfoToStreamAsync(StreamName, GroupName, userCredentials: credentials); - - Assert.Equal(StreamName, result.EventSource); - Assert.Equal(GroupName, result.GroupName); - Assert.NotNull(_settings.StartFrom); - Assert.True(result.Stats.TotalItems > 0); - Assert.True(result.Stats.OutstandingMessagesCount > 0); - Assert.True(result.Stats.AveragePerSecond >= 0); - Assert.True(result.Stats.ParkedMessageCount >= 0); - Assert.True(result.Stats.AveragePerSecond >= 0); - Assert.True(result.Stats.ReadBufferCount >= 0); - Assert.True(result.Stats.RetryBufferCount >= 0); - Assert.True(result.Stats.CountSinceLastMeasurement >= 0); - Assert.True(result.Stats.TotalInFlightMessages >= 0); - Assert.NotNull(result.Stats.LastKnownEventPosition); - Assert.NotNull(result.Stats.LastCheckpointedEventPosition); - Assert.True(result.Stats.LiveBufferCount >= 0); - - Assert.NotNull(result.Connections); - Assert.NotEmpty(result.Connections); - var connection = result.Connections.First(); - Assert.NotNull(connection.From); - Assert.Equal(TestCredentials.Root.Username, connection.Username); - Assert.NotEmpty(connection.ConnectionName); - Assert.True(connection.AverageItemsPerSecond >= 0); - Assert.True(connection.TotalItems >= 0); - Assert.True(connection.CountSinceLastMeasurement >= 0); - Assert.True(connection.AvailableSlots >= 0); - Assert.True(connection.InFlightMessages >= 0); - Assert.NotNull(connection.ExtraStatistics); - Assert.NotEmpty(connection.ExtraStatistics); - - AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.Highest); - AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.Mean); - AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.Median); - AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.Fastest); - AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.Quintile1); - AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.Quintile2); - AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.Quintile3); - AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.Quintile4); - AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.Quintile5); - AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.NinetyPercent); - AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.NinetyFivePercent); - AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.NinetyNinePercent); - AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.NinetyNinePointFivePercent); - AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.NinetyNinePointNinePercent); - - Assert.NotNull(result.Settings); - Assert.Equal(_settings.StartFrom, result.Settings!.StartFrom); - Assert.Equal(_settings.ResolveLinkTos, result.Settings!.ResolveLinkTos); - Assert.Equal(_settings.ExtraStatistics, result.Settings!.ExtraStatistics); - Assert.Equal(_settings.MessageTimeout, result.Settings!.MessageTimeout); - Assert.Equal(_settings.MaxRetryCount, result.Settings!.MaxRetryCount); - Assert.Equal(_settings.LiveBufferSize, result.Settings!.LiveBufferSize); - Assert.Equal(_settings.ReadBatchSize, result.Settings!.ReadBatchSize); - Assert.Equal(_settings.HistoryBufferSize, result.Settings!.HistoryBufferSize); - Assert.Equal(_settings.CheckPointAfter, result.Settings!.CheckPointAfter); - Assert.Equal(_settings.CheckPointLowerBound, result.Settings!.CheckPointLowerBound); - Assert.Equal(_settings.CheckPointUpperBound, result.Settings!.CheckPointUpperBound); - Assert.Equal(_settings.MaxSubscriberCount, result.Settings!.MaxSubscriberCount); - Assert.Equal(_settings.ConsumerStrategyName, result.Settings!.ConsumerStrategyName); - } - - [Fact] - public async Task throws_when_given_non_existing_subscription() => - await Assert.ThrowsAsync( - async () => { - await _fixture.Client.GetInfoToStreamAsync( - "NonExisting", - "NonExisting", - userCredentials: TestCredentials.Root - ); - } - ); - - [Fact(Skip = "Unable to produce same behavior with HTTP fallback!")] - public async Task throws_with_non_existing_user() => - await Assert.ThrowsAsync( - async () => { - await _fixture.Client.GetInfoToStreamAsync( - "NonExisting", - "NonExisting", - userCredentials: TestCredentials.TestBadUser - ); - } - ); - - [Fact] - public async Task throws_with_no_credentials() => - await Assert.ThrowsAsync( - async () => { - await _fixture.Client.GetInfoToStreamAsync( - "NonExisting", - "NonExisting" - ); - } - ); - - [Fact] - public async Task returns_result_for_normal_user() { - var result = await _fixture.Client.GetInfoToStreamAsync( - StreamName, - GroupName, - userCredentials: TestCredentials.TestUser1 - ); - - Assert.NotNull(result); - } - - private void AssertKeyAndValue(IDictionary items, string key) { - Assert.True(items.ContainsKey(key)); - Assert.True(items[key] > 0); - } - - public class Fixture : EventStoreClientFixture { - private EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult? _subscription; - private IAsyncEnumerator? _enumerator; - public Fixture() : base(noDefaultCredentials: true) { } - - protected override Task Given() => - Client.CreateToStreamAsync(groupName: GroupName, streamName: StreamName, settings: _settings, - userCredentials: TestCredentials.Root); - - protected override async Task When() { - var counter = 0; - _subscription = Client.SubscribeToStream(StreamName, GroupName, userCredentials: TestCredentials.Root); - _enumerator = _subscription.Messages.GetAsyncEnumerator(); - - for (var i = 0; i < 15; i++) { - await StreamsClient.AppendToStreamAsync(StreamName, StreamState.Any, - new[] { new EventData(Uuid.NewUuid(), "test-event", ReadOnlyMemory.Empty) }, - userCredentials: TestCredentials.Root); - } - - while (await _enumerator.MoveNextAsync()) { - if (_enumerator.Current is not PersistentSubscriptionMessage.Event(var resolvedEvent, _)) { - continue; - } - - counter++; - - if (counter == 1) { - await _subscription.Nack(PersistentSubscriptionNakEventAction.Park, "Test", resolvedEvent); - } - - if (counter > 10) { - return; - } - } - } - - public override async Task DisposeAsync() { - if (_enumerator is not null) { - await _enumerator.DisposeAsync(); - } - - if (_subscription is not null) { - await _subscription.DisposeAsync(); - } - - await base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/happy_case_catching_up_to_link_to_events_manual_ack.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/happy_case_catching_up_to_link_to_events_manual_ack.cs deleted file mode 100644 index 864eccd8a..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/happy_case_catching_up_to_link_to_events_manual_ack.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System.Text; - -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; - -public class happy_case_catching_up_to_link_to_events_manual_ack - : IClassFixture { - private const string Stream = nameof(happy_case_catching_up_to_link_to_events_manual_ack); - private const string Group = nameof(Group); - private const int BufferCount = 10; - private const int EventWriteCount = BufferCount * 2; - - private readonly Fixture _fixture; - - public happy_case_catching_up_to_link_to_events_manual_ack(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task Test() { - await _fixture.Subscription!.Messages.OfType() - .Take(_fixture.Events.Length) - .ForEachAwaitAsync(e => _fixture.Subscription.Ack(e.ResolvedEvent)) - .WithTimeout(); - } - - public class Fixture : EventStoreClientFixture { - public readonly EventData[] Events; - public EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult? Subscription { get; private set; } - - public Fixture() { - Events = CreateTestEvents(EventWriteCount) - .Select((e, i) => new EventData( - e.EventId, - SystemEventTypes.LinkTo, - Encoding.UTF8.GetBytes($"{i}@{Stream}"), - contentType: Constants.Metadata.ContentTypes.ApplicationOctetStream - )).ToArray(); - } - - protected override async Task Given() { - foreach (var e in Events) - await StreamsClient.AppendToStreamAsync(Stream, StreamState.Any, new[] { e }); - - await Client.CreateToStreamAsync(Stream, Group, new(startFrom: StreamPosition.Start, resolveLinkTos: true), - userCredentials: TestCredentials.Root); - - Subscription = Client.SubscribeToStream(Stream, Group, bufferSize: BufferCount, - userCredentials: TestCredentials.Root); - } - - protected override Task When() => Task.CompletedTask; - - public override async Task DisposeAsync() { - if (Subscription is not null) { - await Subscription.DisposeAsync(); - } - - await base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/happy_case_catching_up_to_normal_events_manual_ack.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/happy_case_catching_up_to_normal_events_manual_ack.cs deleted file mode 100644 index 5bcf18171..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/happy_case_catching_up_to_normal_events_manual_ack.cs +++ /dev/null @@ -1,53 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; - -public class happy_case_catching_up_to_normal_events_manual_ack - : IClassFixture { - private const string Stream = nameof(happy_case_catching_up_to_normal_events_manual_ack); - private const string Group = nameof(Group); - private const int BufferCount = 10; - private const int EventWriteCount = BufferCount * 2; - - private readonly Fixture _fixture; - - public happy_case_catching_up_to_normal_events_manual_ack(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task Test() { - await _fixture.Subscription!.Messages.OfType() - .Take(_fixture.Events.Length) - .ForEachAwaitAsync(e => _fixture.Subscription.Ack(e.ResolvedEvent)) - .WithTimeout(); - } - - public class Fixture : EventStoreClientFixture { - public readonly EventData[] Events; - - public EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult? Subscription { get; private set; } - - - public Fixture() { - Events = CreateTestEvents(EventWriteCount).ToArray(); - } - - protected override async Task Given() { - foreach (var e in Events) - await StreamsClient.AppendToStreamAsync(Stream, StreamState.Any, new[] { e }); - - await Client.CreateToStreamAsync(Stream, Group, new(startFrom: StreamPosition.Start, resolveLinkTos: true), - userCredentials: TestCredentials.Root); - - Subscription = Client.SubscribeToStream(Stream, Group, bufferSize: BufferCount, - userCredentials: TestCredentials.Root); - } - - protected override Task When() => Task.CompletedTask; - - public override async Task DisposeAsync() { - if (Subscription is not null) { - await Subscription.DisposeAsync(); - } - - await base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/happy_case_writing_and_subscribing_to_normal_events_manual_ack.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/happy_case_writing_and_subscribing_to_normal_events_manual_ack.cs deleted file mode 100644 index b4f957d47..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/happy_case_writing_and_subscribing_to_normal_events_manual_ack.cs +++ /dev/null @@ -1,52 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; - -public class happy_case_writing_and_subscribing_to_normal_events_manual_ack - : IClassFixture { - private const string Stream = nameof(happy_case_writing_and_subscribing_to_normal_events_manual_ack); - private const string Group = nameof(Group); - private const int BufferCount = 10; - private const int EventWriteCount = BufferCount * 2; - - private readonly Fixture _fixture; - - public happy_case_writing_and_subscribing_to_normal_events_manual_ack(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task Test() { - await _fixture.Subscription!.Messages.OfType() - .Take(_fixture.Events.Length) - .ForEachAwaitAsync(e => _fixture.Subscription.Ack(e.ResolvedEvent)) - .WithTimeout(); - } - - public class Fixture : EventStoreClientFixture { - public readonly EventData[] Events; - - public EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult? Subscription { get; private set; } - - public Fixture() { - Events = CreateTestEvents(EventWriteCount).ToArray(); - } - - protected override async Task Given() { - await Client.CreateToStreamAsync(Stream, Group, new(startFrom: StreamPosition.End, resolveLinkTos: true), - userCredentials: TestCredentials.Root); - - Subscription = Client.SubscribeToStream(Stream, Group, bufferSize: BufferCount, - userCredentials: TestCredentials.Root); - } - - protected override async Task When() { - foreach (var e in Events) - await StreamsClient.AppendToStreamAsync(Stream, StreamState.Any, new[] { e }); - } - - public override async Task DisposeAsync() { - if (Subscription is not null) { - await Subscription.DisposeAsync(); - } - - await base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/list_with_persistent_subscriptions.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/list_with_persistent_subscriptions.cs deleted file mode 100644 index 61f93cb11..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/list_with_persistent_subscriptions.cs +++ /dev/null @@ -1,82 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; - -public class list_with_persistent_subscriptions : IClassFixture { - const int AllStreamSubscriptionCount = 4; - const int StreamSubscriptionCount = 3; - const string GroupName = nameof(list_with_persistent_subscriptions); - const string StreamName = nameof(list_with_persistent_subscriptions); - readonly Fixture _fixture; - - public list_with_persistent_subscriptions(Fixture fixture) => _fixture = fixture; - - int TotalSubscriptionCount => - SupportsPSToAll.No - ? StreamSubscriptionCount - : AllStreamSubscriptionCount + StreamSubscriptionCount; - - [Fact] - public async Task returns_subscriptions_to_stream() { - var result = (await _fixture.Client.ListToStreamAsync(StreamName, userCredentials: TestCredentials.Root)).ToList(); - Assert.Equal(StreamSubscriptionCount, result.Count); - Assert.All(result, p => Assert.Equal(StreamName, p.EventSource)); - } - - [Fact] - public async Task returns_all_subscriptions() { - var result = (await _fixture.Client.ListAllAsync(userCredentials: TestCredentials.Root)).ToList(); - Assert.Equal(TotalSubscriptionCount, result.Count); - } - - [Fact] - public async Task throws_for_non_existing() => - await Assert.ThrowsAsync( - async () => - await _fixture.Client.ListToStreamAsync("NonExistingStream", userCredentials: TestCredentials.Root) - ); - - [Fact] - public async Task throws_with_no_credentials() => - await Assert.ThrowsAsync( - async () => - await _fixture.Client.ListToStreamAsync("NonExistingStream") - ); - - [Fact] - public async Task throws_with_non_existing_user() => - await Assert.ThrowsAsync( - async () => - await _fixture.Client.ListAllAsync(userCredentials: TestCredentials.TestBadUser) - ); - - [Fact] - public async Task returns_result_with_normal_user_credentials() { - var result = await _fixture.Client.ListAllAsync(userCredentials: TestCredentials.TestUser1); - Assert.Equal(TotalSubscriptionCount, result.Count()); - } - - public class Fixture : EventStoreClientFixture { - public Fixture() : base(skipPSWarmUp: true, noDefaultCredentials: true) { } - - protected override async Task Given() { - for (var i = 0; i < StreamSubscriptionCount; i++) - await Client.CreateToStreamAsync( - StreamName, - GroupName + i, - new(), - userCredentials: TestCredentials.Root - ); - - if (SupportsPSToAll.No) - return; - - for (var i = 0; i < AllStreamSubscriptionCount; i++) - await Client.CreateToAllAsync( - GroupName + i, - new(), - userCredentials: TestCredentials.Root - ); - } - - protected override Task When() => Task.CompletedTask; - } -} \ No newline at end of file diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/list_without_persistent_subscriptions.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/list_without_persistent_subscriptions.cs deleted file mode 100644 index 7e3ec3ad6..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/list_without_persistent_subscriptions.cs +++ /dev/null @@ -1,36 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; - -public class list_without_persistent_subscriptions : IClassFixture { - readonly Fixture _fixture; - - public list_without_persistent_subscriptions(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public async Task throws() { - if (SupportsPSToAll.No) - return; - - await Assert.ThrowsAsync( - async () => - await _fixture.Client.ListToStreamAsync("stream", userCredentials: TestCredentials.Root) - ); - } - - [Fact] - public async Task returns_empty_collection() { - if (SupportsPSToAll.No) - return; - - var result = await _fixture.Client.ListAllAsync(userCredentials: TestCredentials.Root); - - Assert.Empty(result); - } - - public class Fixture : EventStoreClientFixture { - public Fixture() : base(skipPSWarmUp: true) { } - - protected override Task Given() => Task.CompletedTask; - - protected override Task When() => Task.CompletedTask; - } -} \ No newline at end of file diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/replay_parked.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/replay_parked.cs deleted file mode 100644 index 562449e52..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/replay_parked.cs +++ /dev/null @@ -1,80 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; - -public class replay_parked : IClassFixture { - const string GroupName = nameof(replay_parked); - const string StreamName = nameof(replay_parked); - - readonly Fixture _fixture; - - public replay_parked(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task does_not_throw() { - await _fixture.Client.ReplayParkedMessagesToStreamAsync( - StreamName, - GroupName, - userCredentials: TestCredentials.Root - ); - - await _fixture.Client.ReplayParkedMessagesToStreamAsync( - StreamName, - GroupName, - 100, - userCredentials: TestCredentials.Root - ); - } - - [Fact] - public async Task throws_when_given_non_existing_subscription() => - await Assert.ThrowsAsync( - () => - _fixture.Client.ReplayParkedMessagesToStreamAsync( - "NonExisting", - "NonExisting", - userCredentials: TestCredentials.Root - ) - ); - - [Fact] - public async Task throws_with_no_credentials() => - await Assert.ThrowsAsync( - () => - _fixture.Client.ReplayParkedMessagesToStreamAsync(StreamName, GroupName) - ); - - [Fact(Skip = "Unable to produce same behavior with HTTP fallback!")] - public async Task throws_with_non_existing_user() => - await Assert.ThrowsAsync( - () => - _fixture.Client.ReplayParkedMessagesToStreamAsync( - StreamName, - GroupName, - userCredentials: TestCredentials.TestBadUser - ) - ); - - [Fact] - public async Task throws_with_normal_user_credentials() => - await Assert.ThrowsAsync( - () => - _fixture.Client.ReplayParkedMessagesToStreamAsync( - StreamName, - GroupName, - userCredentials: TestCredentials.TestUser1 - ) - ); - - public class Fixture : EventStoreClientFixture { - public Fixture() : base(noDefaultCredentials: true) { } - - protected override Task Given() => - Client.CreateToStreamAsync( - StreamName, - GroupName, - new(), - userCredentials: TestCredentials.Root - ); - - protected override Task When() => Task.CompletedTask; - } -} \ No newline at end of file diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/update_existing.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/update_existing.cs deleted file mode 100644 index b76d5b189..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/update_existing.cs +++ /dev/null @@ -1,33 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; - -public class update_existing - : IClassFixture { - const string Stream = nameof(update_existing); - const string Group = "existing"; - readonly Fixture _fixture; - - public update_existing(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task the_completion_succeeds() => - await _fixture.Client.UpdateToStreamAsync( - Stream, - Group, - new(), - userCredentials: TestCredentials.Root - ); - - public class Fixture : EventStoreClientFixture { - protected override async Task Given() { - await StreamsClient.AppendToStreamAsync(Stream, StreamState.NoStream, CreateTestEvents()); - await Client.CreateToStreamAsync( - Stream, - Group, - new(), - userCredentials: TestCredentials.Root - ); - } - - protected override Task When() => Task.CompletedTask; - } -} \ No newline at end of file diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/update_existing_with_check_point.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/update_existing_with_check_point.cs deleted file mode 100644 index 09020a8df..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/update_existing_with_check_point.cs +++ /dev/null @@ -1,95 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; - -public class update_existing_with_check_point - : IClassFixture { - private const string Stream = nameof(update_existing_with_check_point); - private const string Group = "existing-with-check-point"; - private readonly Fixture _fixture; - - public update_existing_with_check_point(Fixture fixture) { - _fixture = fixture; - } - - [Fact] - public async Task resumes_from_check_point() { - await using var subscription = - _fixture.Client.SubscribeToStream(Stream, Group, userCredentials: TestCredentials.Root); - - var resolvedEvent = await subscription.Messages - .OfType() - .Select(e => e.ResolvedEvent) - .FirstAsync() - .AsTask() - .WithTimeout(); - - Assert.Equal(_fixture.CheckPoint.Next(), resolvedEvent.Event.EventNumber); - } - - public class Fixture : EventStoreClientFixture { - private readonly EventData[] _events; - - public Fixture() { - _events = CreateTestEvents(5).ToArray(); - } - - public StreamPosition CheckPoint { get; private set; } - - protected override async Task Given() { - await StreamsClient.AppendToStreamAsync(Stream, StreamState.NoStream, _events); - - await Client.CreateToStreamAsync(Stream, Group, - new(checkPointLowerBound: 5, checkPointAfter: TimeSpan.FromSeconds(1), startFrom: StreamPosition.Start), - userCredentials: TestCredentials.Root); - - await using var subscription = - Client.SubscribeToStream(Stream, Group, userCredentials: TestCredentials.Root); - - await using var enumerator = subscription.Messages.GetAsyncEnumerator(); - - await enumerator.MoveNextAsync(); - - await Task.WhenAll(Subscribe().WithTimeout(), WaitForCheckpoint().WithTimeout()); - - return; - - async Task Subscribe() { - var count = 0; - - while (await enumerator.MoveNextAsync()) { - if (enumerator.Current is not PersistentSubscriptionMessage.Event(var resolvedEvent, _)) { - continue; - } - - count++; - - await subscription.Ack(resolvedEvent); - if (count >= _events.Length) { - break; - } - } - } - - async Task WaitForCheckpoint() { - await using var subscription = StreamsClient.SubscribeToStream( - $"$persistentsubscription-{Stream}::{Group}-checkpoint", FromStream.Start, - userCredentials: TestCredentials.Root); - - await foreach (var message in subscription.Messages) { - if (message is not StreamMessage.Event (var resolvedEvent)) { - continue; - } - - CheckPoint = resolvedEvent.Event.Data.ParseStreamPosition(); - return; - } - } - } - - protected override async Task When() { - // Force restart of the subscription - await Client.UpdateToStreamAsync(Stream, Group, new(), userCredentials: TestCredentials.Root); - - await StreamsClient.AppendToStreamAsync(Stream, StreamState.Any, CreateTestEvents(1)); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/update_existing_with_subscribers.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/update_existing_with_subscribers.cs deleted file mode 100644 index e472aec9d..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/update_existing_with_subscribers.cs +++ /dev/null @@ -1,50 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; - -public class update_existing_with_subscribers : IClassFixture { - private const string Stream = nameof(update_existing_with_subscribers); - private const string Group = "existing"; - private readonly Fixture _fixture; - - public update_existing_with_subscribers(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task existing_subscriptions_are_dropped() { - var ex = await Assert.ThrowsAsync(async () => { - while (await _fixture.Enumerator!.MoveNextAsync()) { - } - }).WithTimeout(); - - Assert.Equal(Stream, ex.StreamName); - Assert.Equal(Group, ex.GroupName); - } - - public class Fixture : EventStoreClientFixture { - private EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult? _subscription; - public IAsyncEnumerator? Enumerator { get; private set; } - - protected override async Task Given() { - await StreamsClient.AppendToStreamAsync(Stream, StreamState.NoStream, CreateTestEvents()); - await Client.CreateToStreamAsync(Stream, Group, new(), userCredentials: TestCredentials.Root); - - _subscription = Client.SubscribeToStream(Stream, Group, userCredentials: TestCredentials.Root); - Enumerator = _subscription.Messages.GetAsyncEnumerator(); - - await Enumerator.MoveNextAsync(); - } - - protected override Task When() => - Client.UpdateToStreamAsync(Stream, Group, new(), userCredentials: TestCredentials.Root); - - public override async Task DisposeAsync() { - if (Enumerator is not null) { - await Enumerator.DisposeAsync(); - } - - if (_subscription is not null) { - await _subscription.DisposeAsync(); - } - - await base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/update_existing_without_permissions.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/update_existing_without_permissions.cs deleted file mode 100644 index 021aa47b9..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/update_existing_without_permissions.cs +++ /dev/null @@ -1,42 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; - -public class update_existing_without_permissions - : IClassFixture { - const string Stream = nameof(update_existing_without_permissions); - const string Group = "existing"; - readonly Fixture _fixture; - - public update_existing_without_permissions(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task the_completion_fails_with_access_denied() => - await Assert.ThrowsAsync( - () => _fixture.Client.UpdateToStreamAsync( - Stream, - Group, - new() - ) - ); - - public class Fixture : EventStoreClientFixture { - public Fixture() : base(noDefaultCredentials: true) { } - - protected override async Task Given() { - await StreamsClient.AppendToStreamAsync( - Stream, - StreamState.NoStream, - CreateTestEvents(), - userCredentials: TestCredentials.Root - ); - - await Client.CreateToStreamAsync( - Stream, - Group, - new(), - userCredentials: TestCredentials.Root - ); - } - - protected override Task When() => Task.CompletedTask; - } -} \ No newline at end of file diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/update_non_existent.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/update_non_existent.cs deleted file mode 100644 index 9e1f37e86..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/update_non_existent.cs +++ /dev/null @@ -1,26 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; - -public class update_non_existent - : IClassFixture { - const string Stream = nameof(update_non_existent); - const string Group = "nonexistent"; - readonly Fixture _fixture; - - public update_non_existent(Fixture fixture) => _fixture = fixture; - - [Regression.Fact(21, "20.x returns the wrong exception")] - public async Task the_completion_fails_with_not_found() => - await Assert.ThrowsAsync( - () => _fixture.Client.UpdateToStreamAsync( - Stream, - Group, - new(), - userCredentials: TestCredentials.Root - ) - ); - - public class Fixture : EventStoreClientFixture { - protected override Task Given() => Task.CompletedTask; - protected override Task When() => Task.CompletedTask; - } -} \ No newline at end of file diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/when_writing_and_subscribing_to_normal_events_manual_nack.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/when_writing_and_subscribing_to_normal_events_manual_nack.cs deleted file mode 100644 index 267baa684..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/when_writing_and_subscribing_to_normal_events_manual_nack.cs +++ /dev/null @@ -1,54 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; - -[Obsolete] -public class when_writing_and_subscribing_to_normal_events_manual_nack - : IClassFixture { - private const string Stream = nameof(when_writing_and_subscribing_to_normal_events_manual_nack); - private const string Group = nameof(Group); - private const int BufferCount = 10; - private const int EventWriteCount = BufferCount * 2; - - private readonly Fixture _fixture; - - public when_writing_and_subscribing_to_normal_events_manual_nack(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task Test() { - await _fixture.Subscription!.Messages.OfType() - .Take(1) - .ForEachAwaitAsync(async message => - await _fixture.Subscription.Nack(PersistentSubscriptionNakEventAction.Park, "fail", - message.ResolvedEvent)) - .WithTimeout(); - } - - public class Fixture : EventStoreClientFixture { - private readonly EventData[] _events; - public EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult? Subscription { get; private set; } - - public Fixture() { - _events = CreateTestEvents(EventWriteCount).ToArray(); - } - - protected override async Task Given() { - await Client.CreateToStreamAsync(Stream, Group, new(startFrom: StreamPosition.Start, resolveLinkTos: true), - userCredentials: TestCredentials.Root); - - Subscription = Client.SubscribeToStream(Stream, Group, bufferSize: BufferCount, userCredentials: TestCredentials.Root); - } - - protected override async Task When() { - foreach (var e in _events) { - await StreamsClient.AppendToStreamAsync(Stream, StreamState.Any, new[] { e }); - } - } - - public override async Task DisposeAsync() { - if (Subscription is not null) { - await Subscription.DisposeAsync(); - } - - await base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/restart_subsystem.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/restart_subsystem.cs deleted file mode 100644 index cc6047e00..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/restart_subsystem.cs +++ /dev/null @@ -1,74 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests; - -public class restart_subsystem : IClassFixture { - readonly Fixture _fixture; - - public restart_subsystem(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task does_not_throw() => await _fixture.Client.RestartSubsystemAsync(userCredentials: TestCredentials.Root); - - [Fact] - public async Task throws_with_no_credentials() => - await Assert.ThrowsAsync( - async () => - await _fixture.Client.RestartSubsystemAsync() - ); - - [Fact(Skip = "Unable to produce same behavior with HTTP fallback!")] - public async Task throws_with_non_existing_user() => - await Assert.ThrowsAsync( - async () => await _fixture.Client.RestartSubsystemAsync(userCredentials: TestCredentials.TestBadUser) - ); - - [Fact] - public async Task throws_with_normal_user_credentials() => - await Assert.ThrowsAsync(async () => await _fixture.Client.RestartSubsystemAsync(userCredentials: TestCredentials.TestUser1)); - - public class Fixture : EventStoreClientFixture { - public Fixture() : base(noDefaultCredentials: true) { } - - protected override Task Given() => Task.CompletedTask; - protected override Task When() => Task.CompletedTask; - } -} - -// namespace EventStore.Client.PersistentSubscriptions.Tests; -// -// public class restart_subsystem : IClassFixture { -// readonly InsecureClientTestFixture _fixture; -// -// public restart_subsystem(InsecureClientTestFixture fixture) => _fixture = fixture; -// -// [Fact] -// public async Task does_not_throw() => -// await _fixture.PersistentSubscriptions.RestartSubsystemAsync(userCredentials: TestCredentials.Root); -// -// [Fact] -// public async Task throws_with_no_credentials() => -// await Assert.ThrowsAsync( -// async () => -// await _fixture.PersistentSubscriptions.RestartSubsystemAsync() -// ); -// -// [Fact(Skip = "Unable to produce same behavior with HTTP fallback!")] -// public async Task throws_with_non_existing_user() => -// await Assert.ThrowsAsync( -// async () => await _fixture.PersistentSubscriptions.RestartSubsystemAsync(userCredentials: TestCredentials.TestBadUser) -// ); -// -// [Fact] -// public async Task throws_with_normal_user_credentials() { -// await _fixture.Users.CreateUserWithRetry( -// TestCredentials.TestUser1.Username!, -// TestCredentials.TestUser1.Username!, -// Array.Empty(), -// TestCredentials.TestUser1.Password!, -// TestCredentials.Root -// ); -// -// await Assert.ThrowsAsync( -// async () => await _fixture.PersistentSubscriptions.RestartSubsystemAsync(userCredentials: TestCredentials.TestUser1) -// ); -// } -// } \ No newline at end of file diff --git a/test/EventStore.Client.Plugins.Tests/EventStore.Client.Plugins.Tests.csproj b/test/EventStore.Client.Plugins.Tests/EventStore.Client.Plugins.Tests.csproj deleted file mode 100644 index dac52c701..000000000 --- a/test/EventStore.Client.Plugins.Tests/EventStore.Client.Plugins.Tests.csproj +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/test/EventStore.Client.ProjectionManagement.Tests/AssemblyInfo.cs b/test/EventStore.Client.ProjectionManagement.Tests/AssemblyInfo.cs deleted file mode 100644 index b0b47aa73..000000000 --- a/test/EventStore.Client.ProjectionManagement.Tests/AssemblyInfo.cs +++ /dev/null @@ -1 +0,0 @@ -[assembly: CollectionBehavior(DisableTestParallelization = true)] \ No newline at end of file diff --git a/test/EventStore.Client.ProjectionManagement.Tests/EventStore.Client.ProjectionManagement.Tests.csproj b/test/EventStore.Client.ProjectionManagement.Tests/EventStore.Client.ProjectionManagement.Tests.csproj deleted file mode 100644 index dac52c701..000000000 --- a/test/EventStore.Client.ProjectionManagement.Tests/EventStore.Client.ProjectionManagement.Tests.csproj +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/test/EventStore.Client.ProjectionManagement.Tests/EventStoreClientFixture.cs b/test/EventStore.Client.ProjectionManagement.Tests/EventStoreClientFixture.cs deleted file mode 100644 index 62d9ba053..000000000 --- a/test/EventStore.Client.ProjectionManagement.Tests/EventStoreClientFixture.cs +++ /dev/null @@ -1,50 +0,0 @@ -namespace EventStore.Client.ProjectionManagement.Tests; - -public abstract class EventStoreClientFixture : EventStoreClientFixtureBase { - protected EventStoreClientFixture(EventStoreClientSettings? settings = null, bool noDefaultCredentials = false) : - base( - settings, - new Dictionary { - ["EVENTSTORE_RUN_PROJECTIONS"] = "ALL", - ["EVENTSTORE_START_STANDARD_PROJECTIONS"] = "True" - }, - noDefaultCredentials - ) { - Client = new(Settings); - UserManagementClient = new(Settings); - StreamsClient = new(Settings); - } - - public EventStoreUserManagementClient UserManagementClient { get; } - public EventStoreClient StreamsClient { get; } - public EventStoreProjectionManagementClient Client { get; } - - protected virtual bool RunStandardProjections => true; - - protected override async Task OnServerUpAsync() { - await StreamsClient.WarmUp(); - await UserManagementClient.WarmUp(); - await Client.WarmUp(); - await UserManagementClient.CreateUserWithRetry( - TestCredentials.TestUser1.Username!, - TestCredentials.TestUser1.Username!, - Array.Empty(), - TestCredentials.TestUser1.Password!, - TestCredentials.Root - ).WithTimeout(); - - await StandardProjections.Created(Client).WithTimeout(TimeSpan.FromMinutes(2)); - - if (RunStandardProjections) - await Task - .WhenAll(StandardProjections.Names.Select(name => Client.EnableAsync(name, userCredentials: TestCredentials.Root))) - .WithTimeout(TimeSpan.FromMinutes(2)); - } - - public override async Task DisposeAsync() { - await StreamsClient.DisposeAsync(); - await UserManagementClient.DisposeAsync(); - await Client.DisposeAsync(); - await base.DisposeAsync(); - } -} \ No newline at end of file diff --git a/test/EventStore.Client.ProjectionManagement.Tests/StandardProjections.cs b/test/EventStore.Client.ProjectionManagement.Tests/StandardProjections.cs deleted file mode 100644 index 4f57a0ae2..000000000 --- a/test/EventStore.Client.ProjectionManagement.Tests/StandardProjections.cs +++ /dev/null @@ -1,30 +0,0 @@ -namespace EventStore.Client.ProjectionManagement.Tests; - -static class StandardProjections { - public static readonly string[] Names = { - "$streams", - "$stream_by_category", - "$by_category", - "$by_event_type", - "$by_correlation_id" - }; - - public static Task Created(EventStoreProjectionManagementClient client) { - var systemProjectionsReady = Names.Select( - async name => { - var ready = false; - - while (!ready) { - var result = await client.GetStatusAsync(name, userCredentials: TestCredentials.Root); - - if (result?.Status.Contains("Running") ?? false) - ready = true; - else - await Task.Delay(100); - } - } - ); - - return Task.WhenAll(systemProjectionsReady); - } -} \ No newline at end of file diff --git a/test/EventStore.Client.ProjectionManagement.Tests/abort.cs b/test/EventStore.Client.ProjectionManagement.Tests/abort.cs deleted file mode 100644 index 1983a79b6..000000000 --- a/test/EventStore.Client.ProjectionManagement.Tests/abort.cs +++ /dev/null @@ -1,21 +0,0 @@ -namespace EventStore.Client.ProjectionManagement.Tests; - -public class @abort : IClassFixture { - readonly Fixture _fixture; - - public abort(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task status_is_aborted() { - var name = StandardProjections.Names.First(); - await _fixture.Client.AbortAsync(name, userCredentials: TestCredentials.Root); - var result = await _fixture.Client.GetStatusAsync(name, userCredentials: TestCredentials.Root); - Assert.NotNull(result); - Assert.Contains(new[] { "Aborted/Stopped", "Stopped" }, x => x == result!.Status); - } - - public class Fixture : EventStoreClientFixture { - protected override Task Given() => Task.CompletedTask; - protected override Task When() => Task.CompletedTask; - } -} \ No newline at end of file diff --git a/test/EventStore.Client.ProjectionManagement.Tests/create.cs b/test/EventStore.Client.ProjectionManagement.Tests/create.cs deleted file mode 100644 index e8f9c21d2..000000000 --- a/test/EventStore.Client.ProjectionManagement.Tests/create.cs +++ /dev/null @@ -1,35 +0,0 @@ -namespace EventStore.Client.ProjectionManagement.Tests; - -public class @create : IClassFixture { - readonly Fixture _fixture; - - public create(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task one_time() => - await _fixture.Client.CreateOneTimeAsync("fromAll().when({$init: function (state, ev) {return {};}});", userCredentials: TestCredentials.Root); - - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task continuous(bool trackEmittedStreams) => - await _fixture.Client.CreateContinuousAsync( - $"{nameof(continuous)}_{trackEmittedStreams}", - "fromAll().when({$init: function (state, ev) {return {};}});", - trackEmittedStreams, - userCredentials: TestCredentials.Root - ); - - [Fact] - public async Task transient() => - await _fixture.Client.CreateTransientAsync( - nameof(transient), - "fromAll().when({$init: function (state, ev) {return {};}});", - userCredentials: TestCredentials.Root - ); - - public class Fixture : EventStoreClientFixture { - protected override Task Given() => Task.CompletedTask; - protected override Task When() => Task.CompletedTask; - } -} \ No newline at end of file diff --git a/test/EventStore.Client.ProjectionManagement.Tests/disable.cs b/test/EventStore.Client.ProjectionManagement.Tests/disable.cs deleted file mode 100644 index bd03a2847..000000000 --- a/test/EventStore.Client.ProjectionManagement.Tests/disable.cs +++ /dev/null @@ -1,21 +0,0 @@ -namespace EventStore.Client.ProjectionManagement.Tests; - -public class @disable : IClassFixture { - readonly Fixture _fixture; - - public disable(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task status_is_stopped() { - var name = StandardProjections.Names.First(); - await _fixture.Client.DisableAsync(name, userCredentials: TestCredentials.Root); - var result = await _fixture.Client.GetStatusAsync(name, userCredentials: TestCredentials.Root); - Assert.NotNull(result); - Assert.Contains(new[] { "Aborted/Stopped", "Stopped" }, x => x == result!.Status); - } - - public class Fixture : EventStoreClientFixture { - protected override Task Given() => Task.CompletedTask; - protected override Task When() => Task.CompletedTask; - } -} \ No newline at end of file diff --git a/test/EventStore.Client.ProjectionManagement.Tests/enable.cs b/test/EventStore.Client.ProjectionManagement.Tests/enable.cs deleted file mode 100644 index 0afecaaa7..000000000 --- a/test/EventStore.Client.ProjectionManagement.Tests/enable.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace EventStore.Client.ProjectionManagement.Tests; - -public class @enable : IClassFixture { - readonly Fixture _fixture; - - public enable(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task status_is_running() { - var name = StandardProjections.Names.First(); - await _fixture.Client.EnableAsync(name, userCredentials: TestCredentials.Root); - var result = await _fixture.Client.GetStatusAsync(name, userCredentials: TestCredentials.Root); - Assert.NotNull(result); - Assert.Equal("Running", result!.Status); - } - - public class Fixture : EventStoreClientFixture { - protected override bool RunStandardProjections => false; - protected override Task Given() => Task.CompletedTask; - protected override Task When() => Task.CompletedTask; - } -} \ No newline at end of file diff --git a/test/EventStore.Client.ProjectionManagement.Tests/get_result.cs b/test/EventStore.Client.ProjectionManagement.Tests/get_result.cs deleted file mode 100644 index d664862b5..000000000 --- a/test/EventStore.Client.ProjectionManagement.Tests/get_result.cs +++ /dev/null @@ -1,51 +0,0 @@ -namespace EventStore.Client.ProjectionManagement.Tests; - -public class get_result : IClassFixture { - readonly Fixture _fixture; - - public get_result(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task returns_expected_result() { - Result? result = null; - - await AssertEx.IsOrBecomesTrue( - async () => { - result = await _fixture.Client - .GetResultAsync(nameof(get_result), userCredentials: TestCredentials.TestUser1); - - return result.Count > 0; - } - ); - - Assert.NotNull(result); - Assert.Equal(1, result!.Count); - } - - class Result { - public int Count { get; set; } - } - - public class Fixture : EventStoreClientFixture { - static readonly string Projection = $@" -fromStream('{nameof(get_result)}').when({{ - ""$init"": function() {{ return {{ Count: 0 }}; }}, - ""$any"": function(s, e) {{ s.Count++; return s; }} -}}); -"; - - protected override Task Given() => - Client.CreateContinuousAsync( - nameof(get_result), - Projection, - userCredentials: TestCredentials.Root - ); - - protected override async Task When() => - await StreamsClient.AppendToStreamAsync( - nameof(get_result), - StreamState.NoStream, - CreateTestEvents() - ); - } -} \ No newline at end of file diff --git a/test/EventStore.Client.ProjectionManagement.Tests/get_state.cs b/test/EventStore.Client.ProjectionManagement.Tests/get_state.cs deleted file mode 100644 index 2295ed722..000000000 --- a/test/EventStore.Client.ProjectionManagement.Tests/get_state.cs +++ /dev/null @@ -1,51 +0,0 @@ -namespace EventStore.Client.ProjectionManagement.Tests; - -public class get_state : IClassFixture { - readonly Fixture _fixture; - - public get_state(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task returns_expected_result() { - Result? result = null; - - await AssertEx.IsOrBecomesTrue( - async () => { - result = await _fixture.Client - .GetStateAsync(nameof(get_state), userCredentials: TestCredentials.TestUser1); - - return result.Count > 0; - } - ); - - Assert.NotNull(result); - Assert.Equal(1, result!.Count); - } - - class Result { - public int Count { get; set; } - } - - public class Fixture : EventStoreClientFixture { - static readonly string Projection = $@" -fromStream('{nameof(get_state)}').when({{ - ""$init"": function() {{ return {{ Count: 0 }}; }}, - ""$any"": function(s, e) {{ s.Count++; return s; }} -}}); -"; - - protected override Task Given() => - Client.CreateContinuousAsync( - nameof(get_state), - Projection, - userCredentials: TestCredentials.Root - ); - - protected override Task When() => - StreamsClient.AppendToStreamAsync( - nameof(get_state), - StreamState.NoStream, - CreateTestEvents() - ); - } -} \ No newline at end of file diff --git a/test/EventStore.Client.ProjectionManagement.Tests/get_status.cs b/test/EventStore.Client.ProjectionManagement.Tests/get_status.cs deleted file mode 100644 index 7382a758b..000000000 --- a/test/EventStore.Client.ProjectionManagement.Tests/get_status.cs +++ /dev/null @@ -1,21 +0,0 @@ -namespace EventStore.Client.ProjectionManagement.Tests; - -public class get_status : IClassFixture { - readonly Fixture _fixture; - - public get_status(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task returns_expected_result() { - var name = StandardProjections.Names.First(); - var result = await _fixture.Client.GetStatusAsync(name, userCredentials: TestCredentials.TestUser1); - - Assert.NotNull(result); - Assert.Equal(name, result!.Name); - } - - public class Fixture : EventStoreClientFixture { - protected override Task Given() => Task.CompletedTask; - protected override Task When() => Task.CompletedTask; - } -} \ No newline at end of file diff --git a/test/EventStore.Client.ProjectionManagement.Tests/list_all_projections.cs b/test/EventStore.Client.ProjectionManagement.Tests/list_all_projections.cs deleted file mode 100644 index cac2e85da..000000000 --- a/test/EventStore.Client.ProjectionManagement.Tests/list_all_projections.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace EventStore.Client.ProjectionManagement.Tests; - -public class list_all_projections : IClassFixture { - readonly Fixture _fixture; - - public list_all_projections(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task returns_expected_result() { - var result = await _fixture.Client.ListAllAsync(userCredentials: TestCredentials.Root) - .ToArrayAsync(); - - Assert.Equal(result.Select(x => x.Name).OrderBy(x => x), StandardProjections.Names.OrderBy(x => x)); - } - - public class Fixture : EventStoreClientFixture { - protected override Task Given() => Task.CompletedTask; - protected override Task When() => Task.CompletedTask; - } -} \ No newline at end of file diff --git a/test/EventStore.Client.ProjectionManagement.Tests/list_continuous_projections.cs b/test/EventStore.Client.ProjectionManagement.Tests/list_continuous_projections.cs deleted file mode 100644 index c96ed825f..000000000 --- a/test/EventStore.Client.ProjectionManagement.Tests/list_continuous_projections.cs +++ /dev/null @@ -1,31 +0,0 @@ -namespace EventStore.Client.ProjectionManagement.Tests; - -public class list_continuous_projections : IClassFixture { - readonly Fixture _fixture; - - public list_continuous_projections(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task returns_expected_result() { - var result = await _fixture.Client.ListContinuousAsync(userCredentials: TestCredentials.Root) - .ToArrayAsync(); - - Assert.Equal( - result.Select(x => x.Name).OrderBy(x => x), - StandardProjections.Names.Concat(new[] { nameof(list_continuous_projections) }).OrderBy(x => x) - ); - - Assert.True(result.All(x => x.Mode == "Continuous")); - } - - public class Fixture : EventStoreClientFixture { - protected override Task Given() => - Client.CreateContinuousAsync( - nameof(list_continuous_projections), - "fromAll().when({$init: function (state, ev) {return {};}});", - userCredentials: TestCredentials.Root - ); - - protected override Task When() => Task.CompletedTask; - } -} \ No newline at end of file diff --git a/test/EventStore.Client.ProjectionManagement.Tests/list_one_time_projections.cs b/test/EventStore.Client.ProjectionManagement.Tests/list_one_time_projections.cs deleted file mode 100644 index d88d68b5c..000000000 --- a/test/EventStore.Client.ProjectionManagement.Tests/list_one_time_projections.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace EventStore.Client.ProjectionManagement.Tests; - -public class list_one_time_projections : IClassFixture { - readonly Fixture _fixture; - - public list_one_time_projections(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task returns_expected_result() { - var result = await _fixture.Client.ListOneTimeAsync(userCredentials: TestCredentials.Root) - .ToArrayAsync(); - - var details = Assert.Single(result); - Assert.Equal("OneTime", details.Mode); - } - - public class Fixture : EventStoreClientFixture { - protected override Task Given() => - Client.CreateOneTimeAsync("fromAll().when({$init: function (state, ev) {return {};}});", userCredentials: TestCredentials.Root); - - protected override Task When() => Task.CompletedTask; - } -} \ No newline at end of file diff --git a/test/EventStore.Client.ProjectionManagement.Tests/reset.cs b/test/EventStore.Client.ProjectionManagement.Tests/reset.cs deleted file mode 100644 index 3dbfc15a5..000000000 --- a/test/EventStore.Client.ProjectionManagement.Tests/reset.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace EventStore.Client.ProjectionManagement.Tests; - -public class @reset : IClassFixture { - readonly Fixture _fixture; - - public reset(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task status_is_running() { - var name = StandardProjections.Names.First(); - await _fixture.Client.ResetAsync(name, userCredentials: TestCredentials.Root); - var result = await _fixture.Client.GetStatusAsync(name, userCredentials: TestCredentials.Root); - - Assert.NotNull(result); - Assert.Equal("Running", result!.Status); - } - - public class Fixture : EventStoreClientFixture { - protected override Task Given() => Task.CompletedTask; - protected override Task When() => Task.CompletedTask; - } -} \ No newline at end of file diff --git a/test/EventStore.Client.ProjectionManagement.Tests/restart_subsystem.cs b/test/EventStore.Client.ProjectionManagement.Tests/restart_subsystem.cs deleted file mode 100644 index d10ba1545..000000000 --- a/test/EventStore.Client.ProjectionManagement.Tests/restart_subsystem.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace EventStore.Client.ProjectionManagement.Tests; - -public class restart_subsystem : IClassFixture { - readonly Fixture _fixture; - - public restart_subsystem(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task does_not_throw() => await _fixture.Client.RestartSubsystemAsync(userCredentials: TestCredentials.Root); - - [Fact] - public async Task throws_when_given_no_credentials() => await Assert.ThrowsAsync(() => _fixture.Client.RestartSubsystemAsync()); - - public class Fixture : EventStoreClientFixture { - public Fixture() : base(noDefaultCredentials: true) { } - - protected override Task Given() => Task.CompletedTask; - protected override Task When() => Task.CompletedTask; - } -} \ No newline at end of file diff --git a/test/EventStore.Client.ProjectionManagement.Tests/update.cs b/test/EventStore.Client.ProjectionManagement.Tests/update.cs deleted file mode 100644 index ba8b682ad..000000000 --- a/test/EventStore.Client.ProjectionManagement.Tests/update.cs +++ /dev/null @@ -1,30 +0,0 @@ -namespace EventStore.Client.ProjectionManagement.Tests; - -public class @update : IClassFixture { - readonly Fixture _fixture; - - public update(Fixture fixture) => _fixture = fixture; - - [Theory] - [InlineData(true)] - [InlineData(false)] - [InlineData(null)] - public async Task returns_expected_result(bool? emitEnabled) => - await _fixture.Client.UpdateAsync( - nameof(update), - "fromAll().when({$init: function (s, e) {return {};}});", - emitEnabled, - userCredentials: TestCredentials.Root - ); - - public class Fixture : EventStoreClientFixture { - protected override Task Given() => - Client.CreateContinuousAsync( - nameof(update), - "fromAll().when({$init: function (state, ev) {return {};}});", - userCredentials: TestCredentials.Root - ); - - protected override Task When() => Task.CompletedTask; - } -} \ No newline at end of file diff --git a/test/EventStore.Client.Streams.Tests/Append/append_to_stream_limits.cs b/test/EventStore.Client.Streams.Tests/Append/append_to_stream_limits.cs deleted file mode 100644 index 6bdd3fbb8..000000000 --- a/test/EventStore.Client.Streams.Tests/Append/append_to_stream_limits.cs +++ /dev/null @@ -1,54 +0,0 @@ -namespace EventStore.Client.Streams.Tests.Append; - -[Trait("Category", "Target:Stream")] -[Trait("Category", "Operation:Append")] -public class append_to_stream_limits(ITestOutputHelper output, StreamLimitsFixture fixture) : EventStoreTests(output, fixture) { - [Fact] - public async Task succeeds_when_size_is_less_than_max_append_size() { - var stream = Fixture.GetStreamName(); - - var (events, size) = Fixture.CreateTestEventsUpToMaxSize(StreamLimitsFixture.MaxAppendSize - 1); - - await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events); - } - - [Fact] - public async Task fails_when_size_exceeds_max_append_size() { - var stream = Fixture.GetStreamName(); - - var eventsAppendSize = StreamLimitsFixture.MaxAppendSize * 2; - - // beware of the size of the events... - var (events, size) = Fixture.CreateTestEventsUpToMaxSize(eventsAppendSize); - - size.ShouldBeGreaterThan(StreamLimitsFixture.MaxAppendSize); - - var ex = await Fixture.Streams - .AppendToStreamAsync(stream, StreamState.NoStream, events) - .ShouldThrowAsync(); - - ex.MaxAppendSize.ShouldBe(StreamLimitsFixture.MaxAppendSize); - } -} - -public class StreamLimitsFixture() : EventStoreFixture(x => x.WithMaxAppendSize(MaxAppendSize)) { - public const uint MaxAppendSize = 64; - - public (IEnumerable Events, uint size) CreateTestEventsUpToMaxSize(uint maxSize) { - var size = 0; - var events = new List(); - - foreach (var evt in CreateTestEvents(int.MaxValue)) { - size += evt.Data.Length; - - if (size >= maxSize) { - size -= evt.Data.Length; - break; - } - - events.Add(evt); - } - - return (events, (uint)size); - } -} diff --git a/test/EventStore.Client.Streams.Tests/Append/append_to_stream_retry.cs b/test/EventStore.Client.Streams.Tests/Append/append_to_stream_retry.cs deleted file mode 100644 index d67240c49..000000000 --- a/test/EventStore.Client.Streams.Tests/Append/append_to_stream_retry.cs +++ /dev/null @@ -1,37 +0,0 @@ -using Polly; -using Polly.Contrib.WaitAndRetry; - -namespace EventStore.Client.Streams.Tests.Append; - -[Trait("Category", "Target:Stream")] -[Trait("Category", "Operation:Append")] -public class append_to_stream_retry(ITestOutputHelper output, StreamRetryFixture fixture) : EventStoreTests(output, fixture) { - [Fact] - public async Task can_retry() { - var stream = Fixture.GetStreamName(); - - // can definitely write without throwing - var result = await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, Fixture.CreateTestEvents()); - - result.NextExpectedStreamRevision.ShouldBe(new(0)); - - await Fixture.Service.Restart(); - - // write can be retried - var writeResult = await Policy - .Handle() - .WaitAndRetryAsync( - Backoff.LinearBackoff(TimeSpan.FromMilliseconds(250), 10), - (ex, ts) => Fixture.Log.Debug("Error writing events to stream. Retrying. Reason: {Message}.", ex.Message) - ) - .ExecuteAsync(() => Fixture.Streams.AppendToStreamAsync(stream, result.NextExpectedStreamRevision, Fixture.CreateTestEvents())); - - Fixture.Log.Information("Successfully wrote events to stream {Stream}.", stream); - - writeResult.NextExpectedStreamRevision.ShouldBe(new(1)); - } -} - -public class StreamRetryFixture() : EventStoreFixture( - x => x.RunInMemory(false).With(o => o.ClientSettings.ConnectivitySettings.MaxDiscoverAttempts = 2) -); \ No newline at end of file diff --git a/test/EventStore.Client.Streams.Tests/Append/append_to_stream_with_tls_ca_file.cs b/test/EventStore.Client.Streams.Tests/Append/append_to_stream_with_tls_ca_file.cs deleted file mode 100644 index 2fdc983ba..000000000 --- a/test/EventStore.Client.Streams.Tests/Append/append_to_stream_with_tls_ca_file.cs +++ /dev/null @@ -1,35 +0,0 @@ -namespace EventStore.Client.Streams.Tests.Append; - -[Trait("Category", "Target:Stream")] -[Trait("Category", "Operation:Append")] -public class append_to_stream_with_tls_ca_file(ITestOutputHelper output, EventStoreFixture fixture) - : EventStoreTests(output, fixture) { - public static IEnumerable CertPaths => - new List { - new object[] { Path.Combine("certs", "ca", "ca.crt") }, - new object[] { Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "certs", "ca", "ca.crt") }, - }; - - [Theory] - [MemberData(nameof(CertPaths))] - private async Task TestAppendWithCaFile(string certificateFilePath) { - Fixture.Log.Information($"Using certificate: {certificateFilePath}"); - - var connectionString = - $"esdb://admin:changeit@localhost:2113/?tls=true&tlsVerifyCert=true&tlsCAFile={certificateFilePath}"; - - var settings = EventStoreClientSettings.Create(connectionString); - - var client = new EventStoreClient(settings); - - var appendResult = await client.AppendToStreamAsync( - "some-stream", - StreamState.Any, - new[] { new EventData(Uuid.NewUuid(), "some-event", default) } - ); - - appendResult.ShouldNotBeNull(); - - await client.DisposeAsync(); - } -} diff --git a/test/EventStore.Client.Streams.Tests/Append/appending_to_implicitly_created_stream.cs b/test/EventStore.Client.Streams.Tests/Append/appending_to_implicitly_created_stream.cs deleted file mode 100644 index 59ab89df0..000000000 --- a/test/EventStore.Client.Streams.Tests/Append/appending_to_implicitly_created_stream.cs +++ /dev/null @@ -1,266 +0,0 @@ -namespace EventStore.Client.Streams.Tests.Append; - -[Trait("Category", "Target:Stream")] -[Trait("Category", "Operation:Append")] -public class appending_to_implicitly_created_stream(ITestOutputHelper output, EventStoreFixture fixture) : EventStoreTests(output, fixture) { - [Fact] - public async Task sequence_0em1_1e0_2e1_3e2_4e3_5e4_0em1_idempotent() { - var stream = Fixture.GetStreamName(); - - var events = Fixture.CreateTestEvents(6).ToArray(); - - await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events); - await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events.Take(1)); - - var count = await Fixture.Streams - .ReadStreamAsync(Direction.Forwards, stream, StreamPosition.Start, events.Length + 1).CountAsync(); - - Assert.Equal(events.Length, count); - } - - [Fact] - public async Task sequence_0em1_1e0_2e1_3e2_4e3_4e4_0any_idempotent() { - var stream = Fixture.GetStreamName(); - - var events = Fixture.CreateTestEvents(6).ToArray(); - - await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events); - await Fixture.Streams.AppendToStreamAsync(stream, StreamState.Any, events.Take(1)); - - var count = await Fixture.Streams - .ReadStreamAsync(Direction.Forwards, stream, StreamPosition.Start, events.Length + 1).CountAsync(); - - Assert.Equal(events.Length, count); - } - - [Fact] - public async Task sequence_0em1_1e0_2e1_3e2_4e3_5e4_0e5_non_idempotent() { - var stream = Fixture.GetStreamName(); - - var events = Fixture.CreateTestEvents(6).ToArray(); - - await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events); - await Fixture.Streams.AppendToStreamAsync(stream, new StreamRevision(5), events.Take(1)); - - var count = await Fixture.Streams - .ReadStreamAsync(Direction.Forwards, stream, StreamPosition.Start, events.Length + 2).CountAsync(); - - Assert.Equal(events.Length + 1, count); - } - - [Fact] - public async Task sequence_0em1_1e0_2e1_3e2_4e3_5e4_0e6_throws_wev() { - var stream = Fixture.GetStreamName(); - - var events = Fixture.CreateTestEvents(6).ToArray(); - - await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events); - - await Assert.ThrowsAsync(() => Fixture.Streams.AppendToStreamAsync(stream, new StreamRevision(6), events.Take(1))); - } - - [Fact] - public async Task sequence_0em1_1e0_2e1_3e2_4e3_5e4_0e6_returns_wev() { - var stream = Fixture.GetStreamName(); - - var events = Fixture.CreateTestEvents(6).ToArray(); - - await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events); - - var writeResult = await Fixture.Streams.AppendToStreamAsync( - stream, - new StreamRevision(6), - events.Take(1), - options => options.ThrowOnAppendFailure = false - ); - - Assert.IsType(writeResult); - } - - [Fact] - public async Task sequence_0em1_1e0_2e1_3e2_4e3_5e4_0e4_throws_wev() { - var stream = Fixture.GetStreamName(); - - var events = Fixture.CreateTestEvents(6).ToArray(); - - await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events); - - await Assert.ThrowsAsync(() => Fixture.Streams.AppendToStreamAsync(stream, new StreamRevision(4), events.Take(1))); - } - - [Fact] - public async Task sequence_0em1_1e0_2e1_3e2_4e3_5e4_0e4_returns_wev() { - var stream = Fixture.GetStreamName(); - - var events = Fixture.CreateTestEvents(6).ToArray(); - - await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events); - - var writeResult = await Fixture.Streams.AppendToStreamAsync( - stream, - new StreamRevision(4), - events.Take(1), - options => options.ThrowOnAppendFailure = false - ); - - Assert.IsType(writeResult); - } - - [Fact] - public async Task sequence_0em1_0e0_non_idempotent() { - var stream = Fixture.GetStreamName(); - - var events = Fixture.CreateTestEvents().ToArray(); - - await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events); - await Fixture.Streams.AppendToStreamAsync(stream, new StreamRevision(0), events.Take(1)); - - var count = await Fixture.Streams - .ReadStreamAsync(Direction.Forwards, stream, StreamPosition.Start, events.Length + 2).CountAsync(); - - Assert.Equal(events.Length + 1, count); - } - - [Fact] - public async Task sequence_0em1_0any_idempotent() { - var stream = Fixture.GetStreamName(); - - var events = Fixture.CreateTestEvents().ToArray(); - - await Task.Delay(TimeSpan.FromSeconds(30)); - await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events); - await Fixture.Streams.AppendToStreamAsync(stream, StreamState.Any, events.Take(1)); - - var count = await Fixture.Streams - .ReadStreamAsync(Direction.Forwards, stream, StreamPosition.Start, events.Length + 1).CountAsync(); - - Assert.Equal(events.Length, count); - } - - [Fact] - public async Task sequence_0em1_0em1_idempotent() { - var stream = Fixture.GetStreamName(); - - var events = Fixture.CreateTestEvents().ToArray(); - - await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events); - await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events.Take(1)); - - var count = await Fixture.Streams - .ReadStreamAsync(Direction.Forwards, stream, StreamPosition.Start, events.Length + 1).CountAsync(); - - Assert.Equal(events.Length, count); - } - - [Fact] - public async Task sequence_0em1_1e0_2e1_1any_1any_idempotent() { - var stream = Fixture.GetStreamName(); - - var events = Fixture.CreateTestEvents(3).ToArray(); - - await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events); - await Fixture.Streams.AppendToStreamAsync(stream, StreamState.Any, events.Skip(1).Take(1)); - await Fixture.Streams.AppendToStreamAsync(stream, StreamState.Any, events.Skip(1).Take(1)); - - var count = await Fixture.Streams - .ReadStreamAsync(Direction.Forwards, stream, StreamPosition.Start, events.Length + 1).CountAsync(); - - Assert.Equal(events.Length, count); - } - - [Fact] - public async Task sequence_S_0em1_1em1_E_S_0em1_E_idempotent() { - var stream = Fixture.GetStreamName(); - - var events = Fixture.CreateTestEvents(2).ToArray(); - - await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events); - await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events.Take(1)); - - var count = await Fixture.Streams - .ReadStreamAsync(Direction.Forwards, stream, StreamPosition.Start, events.Length + 1).CountAsync(); - - Assert.Equal(events.Length, count); - } - - [Fact] - public async Task sequence_S_0em1_1em1_E_S_0any_E_idempotent() { - var stream = Fixture.GetStreamName(); - - var events = Fixture.CreateTestEvents(2).ToArray(); - - await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events); - await Fixture.Streams.AppendToStreamAsync(stream, StreamState.Any, events.Take(1)); - - var count = await Fixture.Streams - .ReadStreamAsync(Direction.Forwards, stream, StreamPosition.Start, events.Length + 1).CountAsync(); - - Assert.Equal(events.Length, count); - } - - [Fact] - public async Task sequence_S_0em1_1em1_E_S_1e0_E_idempotent() { - var stream = Fixture.GetStreamName(); - - var events = Fixture.CreateTestEvents(2).ToArray(); - - await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events); - - await Fixture.Streams.AppendToStreamAsync(stream, new StreamRevision(0), events.Skip(1)); - - var count = await Fixture.Streams - .ReadStreamAsync(Direction.Forwards, stream, StreamPosition.Start, events.Length + 1).CountAsync(); - - Assert.Equal(events.Length, count); - } - - [Fact] - public async Task sequence_S_0em1_1em1_E_S_1any_E_idempotent() { - var stream = Fixture.GetStreamName(); - - var events = Fixture.CreateTestEvents(2).ToArray(); - - await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events); - await Fixture.Streams.AppendToStreamAsync(stream, StreamState.Any, events.Skip(1).Take(1)); - - var count = await Fixture.Streams - .ReadStreamAsync(Direction.Forwards, stream, StreamPosition.Start, events.Length + 1).CountAsync(); - - Assert.Equal(events.Length, count); - } - - [Fact] - public async Task sequence_S_0em1_1em1_E_S_0em1_1em1_2em1_E_idempotancy_fail_throws() { - var stream = Fixture.GetStreamName(); - - var events = Fixture.CreateTestEvents(3).ToArray(); - - await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events.Take(2)); - - await Assert.ThrowsAsync( - () => Fixture.Streams.AppendToStreamAsync( - stream, - StreamState.NoStream, - events - ) - ); - } - - [Fact] - public async Task sequence_S_0em1_1em1_E_S_0em1_1em1_2em1_E_idempotancy_fail_returns() { - var stream = Fixture.GetStreamName(); - - var events = Fixture.CreateTestEvents(3).ToArray(); - - await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events.Take(2)); - - var writeResult = await Fixture.Streams.AppendToStreamAsync( - stream, - StreamState.NoStream, - events, - options => options.ThrowOnAppendFailure = false - ); - - Assert.IsType(writeResult); - } -} \ No newline at end of file diff --git a/test/EventStore.Client.Streams.Tests/Append/sending_and_receiving_large_messages.cs b/test/EventStore.Client.Streams.Tests/Append/sending_and_receiving_large_messages.cs deleted file mode 100644 index 099ece45c..000000000 --- a/test/EventStore.Client.Streams.Tests/Append/sending_and_receiving_large_messages.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Grpc.Core; - -namespace EventStore.Client.Streams.Tests.Append; - -[Trait("Category", "Target:Stream")] -[Trait("Category", "Operation:Append")] -public class sending_and_receiving_large_messages(ITestOutputHelper output, sending_and_receiving_large_messages.CustomFixture fixture) - : EventStoreTests(output, fixture) { - [Fact] - public async Task over_the_hard_limit() { - var streamName = Fixture.GetStreamName(); - var largeEvent = Fixture.CreateTestEvents() - .Select(e => new EventData(e.EventId, "-", new byte[CustomFixture.MaximumSize + 1])); - - var ex = await Assert.ThrowsAsync( - () => Fixture.Streams.AppendToStreamAsync( - streamName, - StreamState.NoStream, - largeEvent - ) - ); - - Assert.Equal(StatusCode.ResourceExhausted, ex.StatusCode); - } - - public class CustomFixture() : EventStoreFixture(x => x.WithMaxAppendSize(MaximumSize)) { - public const int MaximumSize = 16 * 1024 * 1024 - 10000; - } -} \ No newline at end of file diff --git a/test/EventStore.Client.Streams.Tests/AssemblyInfo.cs b/test/EventStore.Client.Streams.Tests/AssemblyInfo.cs deleted file mode 100644 index b0b47aa73..000000000 --- a/test/EventStore.Client.Streams.Tests/AssemblyInfo.cs +++ /dev/null @@ -1 +0,0 @@ -[assembly: CollectionBehavior(DisableTestParallelization = true)] \ No newline at end of file diff --git a/test/EventStore.Client.Streams.Tests/DependencyInjectionTests.cs b/test/EventStore.Client.Streams.Tests/DependencyInjectionTests.cs deleted file mode 100644 index 96512557b..000000000 --- a/test/EventStore.Client.Streams.Tests/DependencyInjectionTests.cs +++ /dev/null @@ -1,75 +0,0 @@ -using Grpc.Core.Interceptors; -using Microsoft.Extensions.DependencyInjection; - -namespace EventStore.Client.Streams.Tests; - -[Trait("Category", "UnitTest")] -public class DependencyInjectionTests { - [Fact] - public void Register() => - new ServiceCollection() - .AddEventStoreClient() - .BuildServiceProvider() - .GetRequiredService(); - - [Fact] - public void RegisterWithConnectionString() => - new ServiceCollection() - .AddEventStoreClient("esdb://localhost:2113?tls=false") - .BuildServiceProvider() - .GetRequiredService(); - - [Fact] - public void RegisterWithConnectionStringFactory() => - new ServiceCollection() - .AddEventStoreClient(provider => "esdb://localhost:2113?tls=false") - .BuildServiceProvider() - .GetRequiredService(); - - [Fact] - public void RegisterWithUri() => - new ServiceCollection() - .AddEventStoreClient(new Uri("https://localhost:1234")) - .BuildServiceProvider() - .GetRequiredService(); - - [Fact] - public void RegisterWithUriFactory() => - new ServiceCollection() - .AddEventStoreClient(provider => new Uri("https://localhost:1234")) - .BuildServiceProvider() - .GetRequiredService(); - - [Fact] - public void RegisterWithSettings() => - new ServiceCollection() - .AddEventStoreClient(settings => { }) - .BuildServiceProvider() - .GetRequiredService(); - - [Fact] - public void RegisterWithSettingsFactory() => - new ServiceCollection() - .AddEventStoreClient(provider => settings => { }) - .BuildServiceProvider() - .GetRequiredService(); - - [Fact] - public void RegisterInterceptors() { - var interceptorResolved = false; - new ServiceCollection() - .AddEventStoreClient() - .AddSingleton(() => interceptorResolved = true) - .AddSingleton() - .BuildServiceProvider() - .GetRequiredService(); - - Assert.True(interceptorResolved); - } - - delegate void ConstructorInvoked(); - - class TestInterceptor : Interceptor { - public TestInterceptor(ConstructorInvoked invoked) => invoked.Invoke(); - } -} \ No newline at end of file diff --git a/test/EventStore.Client.Streams.Tests/Diagnostics/StreamsTracingInstrumentationTests.cs b/test/EventStore.Client.Streams.Tests/Diagnostics/StreamsTracingInstrumentationTests.cs deleted file mode 100644 index 5a19c6b0d..000000000 --- a/test/EventStore.Client.Streams.Tests/Diagnostics/StreamsTracingInstrumentationTests.cs +++ /dev/null @@ -1,221 +0,0 @@ -// ReSharper disable ConditionalAccessQualifierIsNonNullableAccordingToAPIContract - -using EventStore.Client.Diagnostics; -using EventStore.Diagnostics.Tracing; - -namespace EventStore.Client.Streams.Tests.Diagnostics; - -[Trait("Category", "Diagnostics:Tracing")] -public class StreamsTracingInstrumentationTests(ITestOutputHelper output, DiagnosticsFixture fixture) - : EventStoreTests(output, fixture) { - [Fact] - public async Task AppendIsInstrumentedWithTracingAsExpected() { - var stream = Fixture.GetStreamName(); - - await Fixture.Streams.AppendToStreamAsync( - stream, - StreamState.NoStream, - Fixture.CreateTestEvents() - ); - - var activity = Fixture - .GetActivitiesForOperation(TracingConstants.Operations.Append, stream) - .SingleOrDefault() - .ShouldNotBeNull(); - - Fixture.AssertAppendActivityHasExpectedTags(activity, stream); - } - - [Fact] - public async Task AppendTraceIsTaggedWithErrorStatusOnException() { - var stream = Fixture.GetStreamName(); - - var actualException = await Fixture.Streams.AppendToStreamAsync( - stream, - StreamState.NoStream, - Fixture.CreateTestEventsThatThrowsException() - ).ShouldThrowAsync(); - - var activity = Fixture - .GetActivitiesForOperation(TracingConstants.Operations.Append, stream) - .SingleOrDefault() - .ShouldNotBeNull(); - - Fixture.AssertErroneousAppendActivityHasExpectedTags(activity, actualException); - } - - [Fact] - public async Task TracingContextIsInjectedWhenUserMetadataIsValidJsonObject() { - var stream = Fixture.GetStreamName(); - - await Fixture.Streams.AppendToStreamAsync( - stream, - StreamState.NoStream, - Fixture.CreateTestEvents(1, metadata: Fixture.CreateTestJsonMetadata()) - ); - - var activity = Fixture - .GetActivitiesForOperation(TracingConstants.Operations.Append, stream) - .SingleOrDefault() - .ShouldNotBeNull(); - - var readResult = await Fixture.Streams - .ReadStreamAsync(Direction.Forwards, stream, StreamPosition.Start) - .ToListAsync(); - - var tracingMetadata = readResult[0].OriginalEvent.Metadata.ExtractTracingMetadata(); - - tracingMetadata.ShouldNotBe(TracingMetadata.None); - tracingMetadata.TraceId.ShouldBe(activity.TraceId.ToString()); - tracingMetadata.SpanId.ShouldBe(activity.SpanId.ToString()); - } - - [Fact] - public async Task TracingContextIsNotInjectedWhenUserMetadataIsNotValidJsonObject() { - var stream = Fixture.GetStreamName(); - - var inputMetadata = "clearlynotavalidjsonobject"u8.ToArray(); - await Fixture.Streams.AppendToStreamAsync( - stream, - StreamState.NoStream, - Fixture.CreateTestEvents(1, metadata: inputMetadata) - ); - - var readResult = await Fixture.Streams - .ReadStreamAsync(Direction.Forwards, stream, StreamPosition.Start) - .ToListAsync(); - - var outputMetadata = readResult[0].OriginalEvent.Metadata.ToArray(); - outputMetadata.ShouldBe(inputMetadata); - } - - [Fact] - public async Task TracingContextIsInjectedWhenEventIsNotJsonButHasJsonMetadata() { - var stream = Fixture.GetStreamName(); - - var inputMetadata = Fixture.CreateTestJsonMetadata().ToArray(); - await Fixture.Streams.AppendToStreamAsync( - stream, - StreamState.NoStream, - Fixture.CreateTestEvents( - metadata: inputMetadata, - contentType: Constants.Metadata.ContentTypes.ApplicationOctetStream - ) - ); - - var readResult = await Fixture.Streams - .ReadStreamAsync(Direction.Forwards, stream, StreamPosition.Start) - .ToListAsync(); - - var outputMetadata = readResult[0].OriginalEvent.Metadata.ToArray(); - outputMetadata.ShouldNotBe(inputMetadata); - - var appendActivities = Fixture.GetActivitiesForOperation(TracingConstants.Operations.Append, stream); - - appendActivities.ShouldNotBeEmpty(); - } - - [Fact] - public async Task json_metadata_event_is_traced_and_non_json_metadata_event_is_not_traced() { - var streamName = Fixture.GetStreamName(); - - var seedEvents = new[] { - Fixture.CreateTestEvent(metadata: Fixture.CreateTestJsonMetadata()), - Fixture.CreateTestEvent(metadata: Fixture.CreateTestNonJsonMetadata()) - }; - - var availableEvents = new HashSet(seedEvents.Select(x => x.EventId)); - - await Fixture.Streams.AppendToStreamAsync(streamName, StreamState.NoStream, seedEvents); - - await using var subscription = Fixture.Streams.SubscribeToStream(streamName, FromStream.Start); - await using var enumerator = subscription.Messages.GetAsyncEnumerator(); - - var appendActivities = Fixture - .GetActivitiesForOperation(TracingConstants.Operations.Append, streamName) - .ShouldNotBeNull(); - - Assert.True(await enumerator.MoveNextAsync()); - - Assert.IsType(enumerator.Current); - - await Subscribe(enumerator).WithTimeout(); - - var subscribeActivities = Fixture - .GetActivitiesForOperation(TracingConstants.Operations.Subscribe, streamName) - .ToArray(); - - appendActivities.ShouldHaveSingleItem(); - - subscribeActivities.ShouldHaveSingleItem(); - - subscribeActivities.First().ParentId.ShouldBe(appendActivities.First().Id); - - var jsonMetadataEvent = seedEvents.First(); - - Fixture.AssertSubscriptionActivityHasExpectedTags( - subscribeActivities.First(), - streamName, - jsonMetadataEvent.EventId.ToString() - ); - - return; - - async Task Subscribe(IAsyncEnumerator internalEnumerator) { - while (await internalEnumerator.MoveNextAsync()) { - if (internalEnumerator.Current is not StreamMessage.Event(var resolvedEvent)) - continue; - - availableEvents.Remove(resolvedEvent.Event.EventId); - - if (availableEvents.Count == 0) - return; - } - } - } - - [Fact] - [Trait("Category", "Special cases")] - public async Task should_not_trace_when_event_is_null() { - var category = Guid.NewGuid().ToString("N"); - var streamName = category + "-123"; - - var seedEvents = Fixture.CreateTestEvents(type: $"{category}-{Fixture.GetStreamName()}").ToArray(); - await Fixture.Streams.AppendToStreamAsync(streamName, StreamState.NoStream, seedEvents); - - await Fixture.Streams.DeleteAsync(streamName, StreamState.StreamExists); - - await using var subscription = Fixture.Streams.SubscribeToStream("$ce-" + category, FromStream.Start, resolveLinkTos: true); - - await using var enumerator = subscription.Messages.GetAsyncEnumerator(); - - Assert.True(await enumerator.MoveNextAsync()); - - Assert.IsType(enumerator.Current); - - await Subscribe().WithTimeout(); - - var appendActivities = Fixture - .GetActivitiesForOperation(TracingConstants.Operations.Append, streamName) - .ShouldNotBeNull(); - - var subscribeActivities = Fixture - .GetActivitiesForOperation(TracingConstants.Operations.Subscribe, "$ce-" + category) - .ToArray(); - - appendActivities.ShouldHaveSingleItem(); - subscribeActivities.ShouldBeEmpty(); - - return; - - async Task Subscribe() { - while (await enumerator.MoveNextAsync()) { - if (enumerator.Current is not StreamMessage.Event(var resolvedEvent)) - continue; - - if (resolvedEvent.Event?.EventType is "$metadata") - return; - } - } - } -} diff --git a/test/EventStore.Client.Streams.Tests/EventDataTests.cs b/test/EventStore.Client.Streams.Tests/EventDataTests.cs deleted file mode 100644 index 87ed00191..000000000 --- a/test/EventStore.Client.Streams.Tests/EventDataTests.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace EventStore.Client.Streams.Tests; - -[Trait("Category", "UnitTest")] -public class EventDataTests { - [Fact] - public void EmptyEventIdThrows() { - var ex = Assert.Throws( - () => new EventData(Uuid.Empty, "-", Array.Empty()) - ); - - Assert.Equal("eventId", ex.ParamName); - } - - [Fact] - public void MalformedContentTypeThrows() => - Assert.Throws(() => new EventData(Uuid.NewUuid(), "-", Array.Empty(), contentType: "application")); - - [Fact] - public void InvalidContentTypeThrows() { - var ex = Assert.Throws(() => new EventData(Uuid.NewUuid(), "-", Array.Empty(), contentType: "application/xml")); - Assert.Equal("contentType", ex.ParamName); - } -} \ No newline at end of file diff --git a/test/EventStore.Client.Streams.Tests/EventStore.Client.Streams.Tests.csproj b/test/EventStore.Client.Streams.Tests/EventStore.Client.Streams.Tests.csproj deleted file mode 100644 index 3e9700e08..000000000 --- a/test/EventStore.Client.Streams.Tests/EventStore.Client.Streams.Tests.csproj +++ /dev/null @@ -1,9 +0,0 @@ - - - - CS0612;xUnit1031 - - - - - \ No newline at end of file diff --git a/test/EventStore.Client.Streams.Tests/EventStore.Client.Streams.Tests.csproj.DotSettings b/test/EventStore.Client.Streams.Tests/EventStore.Client.Streams.Tests.csproj.DotSettings deleted file mode 100644 index 9176e378d..000000000 --- a/test/EventStore.Client.Streams.Tests/EventStore.Client.Streams.Tests.csproj.DotSettings +++ /dev/null @@ -1,12 +0,0 @@ - - False - True - False - False - True - False - True - True - True - True - False \ No newline at end of file diff --git a/test/EventStore.Client.Streams.Tests/Security/Obsolete/SecurityFixture_obsolete.cs b/test/EventStore.Client.Streams.Tests/Security/Obsolete/SecurityFixture_obsolete.cs deleted file mode 100644 index 18b52be82..000000000 --- a/test/EventStore.Client.Streams.Tests/Security/Obsolete/SecurityFixture_obsolete.cs +++ /dev/null @@ -1,321 +0,0 @@ -using System.Runtime.CompilerServices; - -namespace EventStore.Client.Streams.Tests.Obsolete; - -[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class SecurityFixture_obsolete : EventStoreFixture { - public const string NoAclStream = nameof(NoAclStream); - public const string ReadStream = nameof(ReadStream); - public const string WriteStream = nameof(WriteStream); - public const string MetaReadStream = nameof(MetaReadStream); - public const string MetaWriteStream = nameof(MetaWriteStream); - public const string AllStream = SystemStreams.AllStream; - public const string NormalAllStream = nameof(NormalAllStream); - public const string SystemAllStream = $"${nameof(SystemAllStream)}"; - public const string SystemAdminStream = $"${nameof(SystemAdminStream)}"; - public const string SystemAclStream = $"${nameof(SystemAclStream)}"; - - const int TimeoutMs = 1000; - - public SecurityFixture_obsolete() : base(x => x.WithoutDefaultCredentials()) { - OnSetup = async () => { - await Users.CreateUserWithRetry( - TestCredentials.TestUser1.Username!, - nameof(TestCredentials.TestUser1), - Array.Empty(), - TestCredentials.TestUser1.Password!, - TestCredentials.Root - ).WithTimeout(TimeSpan.FromMilliseconds(TimeoutMs)); - - await Users.CreateUserWithRetry( - TestCredentials.TestUser2.Username!, - nameof(TestCredentials.TestUser2), - Array.Empty(), - TestCredentials.TestUser2.Password!, - TestCredentials.Root - ).WithTimeout(TimeSpan.FromMilliseconds(TimeoutMs)); - - await Users.CreateUserWithRetry( - TestCredentials.TestAdmin.Username!, - nameof(TestCredentials.TestAdmin), - new[] { SystemRoles.Admins }, - TestCredentials.TestAdmin.Password!, - TestCredentials.Root - ).WithTimeout(TimeSpan.FromMilliseconds(TimeoutMs)); - - await Given(); - await When(); - }; - } - - protected virtual async Task Given() { - await Streams.SetStreamMetadataAsync( - NoAclStream, - StreamState.NoStream, - new(), - userCredentials: TestCredentials.TestAdmin - ).WithTimeout(TimeSpan.FromMilliseconds(TimeoutMs)); - - await Streams.SetStreamMetadataAsync( - ReadStream, - StreamState.NoStream, - new(acl: new(TestCredentials.TestUser1.Username)), - userCredentials: TestCredentials.TestAdmin - ).WithTimeout(TimeSpan.FromMilliseconds(TimeoutMs)); - - await Streams.SetStreamMetadataAsync( - WriteStream, - StreamState.NoStream, - new(acl: new(writeRole: TestCredentials.TestUser1.Username)), - userCredentials: TestCredentials.TestAdmin - ).WithTimeout(TimeSpan.FromMilliseconds(TimeoutMs)); - - await Streams.SetStreamMetadataAsync( - MetaReadStream, - StreamState.NoStream, - new(acl: new(metaReadRole: TestCredentials.TestUser1.Username)), - userCredentials: TestCredentials.TestAdmin - ).WithTimeout(TimeSpan.FromMilliseconds(TimeoutMs)); - - await Streams.SetStreamMetadataAsync( - MetaWriteStream, - StreamState.NoStream, - new(acl: new(metaWriteRole: TestCredentials.TestUser1.Username)), - userCredentials: TestCredentials.TestAdmin - ).WithTimeout(TimeSpan.FromMilliseconds(TimeoutMs)); - - await Streams.SetStreamMetadataAsync( - AllStream, - StreamState.Any, - new(acl: new(TestCredentials.TestUser1.Username)), - userCredentials: TestCredentials.TestAdmin - ).WithTimeout(TimeSpan.FromMilliseconds(TimeoutMs)); - - await Streams.SetStreamMetadataAsync( - SystemAclStream, - StreamState.NoStream, - new( - acl: new( - writeRole: TestCredentials.TestUser1.Username, - readRole: TestCredentials.TestUser1.Username, - metaWriteRole: TestCredentials.TestUser1.Username, - metaReadRole: TestCredentials.TestUser1.Username - ) - ), - userCredentials: TestCredentials.TestAdmin - ).WithTimeout(TimeSpan.FromMilliseconds(TimeoutMs)); - - await Streams.SetStreamMetadataAsync( - SystemAdminStream, - StreamState.NoStream, - new( - acl: new( - writeRole: SystemRoles.Admins, - readRole: SystemRoles.Admins, - metaWriteRole: SystemRoles.Admins, - metaReadRole: SystemRoles.Admins - ) - ), - userCredentials: TestCredentials.TestAdmin - ).WithTimeout(TimeSpan.FromMilliseconds(TimeoutMs)); - - await Streams.SetStreamMetadataAsync( - NormalAllStream, - StreamState.NoStream, - new( - acl: new( - writeRole: SystemRoles.All, - readRole: SystemRoles.All, - metaWriteRole: SystemRoles.All, - metaReadRole: SystemRoles.All - ) - ), - userCredentials: TestCredentials.TestAdmin - ).WithTimeout(TimeSpan.FromMilliseconds(TimeoutMs)); - - await Streams.SetStreamMetadataAsync( - SystemAllStream, - StreamState.NoStream, - new( - acl: new( - writeRole: SystemRoles.All, - readRole: SystemRoles.All, - metaWriteRole: SystemRoles.All, - metaReadRole: SystemRoles.All - ) - ), - userCredentials: TestCredentials.TestAdmin - ).WithTimeout(TimeSpan.FromMilliseconds(TimeoutMs)); - } - - protected virtual Task When() => Task.CompletedTask; - - public Task ReadEvent(string streamId, UserCredentials? userCredentials = default) => - Streams.ReadStreamAsync( - Direction.Forwards, - streamId, - StreamPosition.Start, - 1, - false, - userCredentials: userCredentials - ) - .ToArrayAsync() - .AsTask() - .WithTimeout(TimeSpan.FromMilliseconds(TimeoutMs)); - - public Task ReadStreamForward(string streamId, UserCredentials? userCredentials = default) => - Streams.ReadStreamAsync( - Direction.Forwards, - streamId, - StreamPosition.Start, - 1, - false, - userCredentials: userCredentials - ) - .ToArrayAsync() - .AsTask() - .WithTimeout(TimeSpan.FromMilliseconds(TimeoutMs)); - - public Task ReadStreamBackward(string streamId, UserCredentials? userCredentials = default) => - Streams.ReadStreamAsync( - Direction.Backwards, - streamId, - StreamPosition.Start, - 1, - false, - userCredentials: userCredentials - ) - .ToArrayAsync() - .AsTask() - .WithTimeout(TimeSpan.FromMilliseconds(TimeoutMs)); - - public Task AppendStream(string streamId, UserCredentials? userCredentials = default) => - Streams.AppendToStreamAsync( - streamId, - StreamState.Any, - CreateTestEvents(3), - userCredentials: userCredentials - ) - .WithTimeout(TimeSpan.FromMilliseconds(TimeoutMs)); - - public Task ReadAllForward(UserCredentials? userCredentials = default) => - Streams.ReadAllAsync( - Direction.Forwards, - Position.Start, - 1, - false, - userCredentials: userCredentials - ) - .ToArrayAsync() - .AsTask() - .WithTimeout(TimeSpan.FromMilliseconds(TimeoutMs)); - - public Task ReadAllBackward(UserCredentials? userCredentials = default) => - Streams - .ReadAllAsync( - Direction.Backwards, - Position.End, - 1, - false, - userCredentials: userCredentials - ) - .ToArrayAsync() - .AsTask() - .WithTimeout(TimeSpan.FromMilliseconds(TimeoutMs)); - - public Task ReadMeta(string streamId, UserCredentials? userCredentials = default) => - Streams.GetStreamMetadataAsync(streamId, userCredentials: userCredentials) - .WithTimeout(TimeSpan.FromMilliseconds(TimeoutMs)); - - public Task WriteMeta(string streamId, UserCredentials? userCredentials = default, string? role = default) => - Streams.SetStreamMetadataAsync( - streamId, - StreamState.Any, - new( - acl: new( - writeRole: role, - readRole: role, - metaWriteRole: role, - metaReadRole: role - ) - ), - userCredentials: userCredentials - ) - .WithTimeout(TimeSpan.FromMilliseconds(TimeoutMs)); - - [Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client", false)] - public async Task SubscribeToStreamObsolete(string streamId, UserCredentials? userCredentials = default) { - var source = new TaskCompletionSource(); - using (await Streams.SubscribeToStreamAsync( - streamId, - FromStream.Start, - (_, _, _) => { - source.TrySetResult(true); - return Task.CompletedTask; - }, - subscriptionDropped: (_, _, ex) => { - if (ex == null) - source.TrySetResult(true); - else - source.TrySetException(ex); - }, - userCredentials: userCredentials - ).WithTimeout(TimeSpan.FromMilliseconds(TimeoutMs))) { - await source.Task.WithTimeout(TimeSpan.FromMilliseconds(TimeoutMs)); - } - } - - public async Task SubscribeToStream(string streamId, UserCredentials? userCredentials = default) { - await using var subscription = - Streams.SubscribeToStream(streamId, FromStream.Start, userCredentials: userCredentials); - await subscription - .Messages.OfType().AnyAsync().AsTask() - .WithTimeout(TimeSpan.FromMilliseconds(TimeoutMs)); - } - - [Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client", false)] - public async Task SubscribeToAllObsolete(UserCredentials? userCredentials = default) { - var source = new TaskCompletionSource(); - using (await Streams.SubscribeToAllAsync( - FromAll.Start, - (_, _, _) => { - source.TrySetResult(true); - return Task.CompletedTask; - }, - false, - (_, _, ex) => { - if (ex == null) - source.TrySetResult(true); - else - source.TrySetException(ex); - }, - userCredentials: userCredentials - ).WithTimeout(TimeSpan.FromMilliseconds(TimeoutMs))) { - await source.Task.WithTimeout(TimeSpan.FromMilliseconds(TimeoutMs)); - } - } - - public async Task SubscribeToAll(UserCredentials? userCredentials = default) { - await using var subscription = - Streams.SubscribeToAll(FromAll.Start, userCredentials: userCredentials); - await subscription - .Messages.OfType().AnyAsync().AsTask() - .WithTimeout(TimeSpan.FromMilliseconds(TimeoutMs)); - } - - public async Task CreateStreamWithMeta(StreamMetadata metadata, [CallerMemberName] string streamId = "") { - await Streams.SetStreamMetadataAsync( - streamId, - StreamState.NoStream, - metadata, - userCredentials: TestCredentials.TestAdmin - ) - .WithTimeout(TimeSpan.FromMilliseconds(TimeoutMs)); - - return streamId; - } - - public Task DeleteStream(string streamId, UserCredentials? userCredentials = default) => - Streams.TombstoneAsync(streamId, StreamState.Any, userCredentials: userCredentials) - .WithTimeout(TimeSpan.FromMilliseconds(TimeoutMs)); -} diff --git a/test/EventStore.Client.Streams.Tests/Security/Obsolete/all_stream_with_no_acl_security_obsolete.cs b/test/EventStore.Client.Streams.Tests/Security/Obsolete/all_stream_with_no_acl_security_obsolete.cs deleted file mode 100644 index 40cdd81ef..000000000 --- a/test/EventStore.Client.Streams.Tests/Security/Obsolete/all_stream_with_no_acl_security_obsolete.cs +++ /dev/null @@ -1,37 +0,0 @@ -namespace EventStore.Client.Streams.Tests.Obsolete; - -[Trait("Category", "Security")] -[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class all_stream_with_no_acl_security_obsolete(ITestOutputHelper output, all_stream_with_no_acl_security_obsolete.CustomFixture fixture) : EventStoreTests(output, fixture) { - [Fact] - public async Task reading_and_subscribing_is_not_allowed_when_no_credentials_are_passed() { - await Assert.ThrowsAsync(() => Fixture.ReadAllForward()); - await Assert.ThrowsAsync(() => Fixture.ReadAllBackward()); - await Assert.ThrowsAsync(() => Fixture.ReadMeta(SecurityFixture_obsolete.AllStream)); - await Assert.ThrowsAsync(() => Fixture.SubscribeToAllObsolete()); - } - - [Fact] - public async Task reading_and_subscribing_is_not_allowed_for_usual_user() { - await Assert.ThrowsAsync(() => Fixture.ReadAllForward(TestCredentials.TestUser1)); - await Assert.ThrowsAsync(() => Fixture.ReadAllBackward(TestCredentials.TestUser1)); - await Assert.ThrowsAsync(() => Fixture.ReadMeta(SecurityFixture_obsolete.AllStream, TestCredentials.TestUser1)); - await Assert.ThrowsAsync(() => Fixture.SubscribeToAllObsolete(TestCredentials.TestUser1)); - } - - [Fact] - public async Task reading_and_subscribing_is_allowed_for_admin_user() { - await Fixture.ReadAllForward(TestCredentials.TestAdmin); - await Fixture.ReadAllBackward(TestCredentials.TestAdmin); - await Fixture.ReadMeta(SecurityFixture_obsolete.AllStream, TestCredentials.TestAdmin); - await Fixture.SubscribeToAllObsolete(TestCredentials.TestAdmin); - } - - public class CustomFixture : SecurityFixture_obsolete { - protected override async Task Given() { - await base.Given(); - - await Streams.SetStreamMetadataAsync(AllStream, StreamState.Any, new(), userCredentials: TestCredentials.Root); - } - } -} \ No newline at end of file diff --git a/test/EventStore.Client.Streams.Tests/Security/Obsolete/overriden_system_stream_security_for_all_obsolete.cs b/test/EventStore.Client.Streams.Tests/Security/Obsolete/overriden_system_stream_security_for_all_obsolete.cs deleted file mode 100644 index 4593e39e0..000000000 --- a/test/EventStore.Client.Streams.Tests/Security/Obsolete/overriden_system_stream_security_for_all_obsolete.cs +++ /dev/null @@ -1,70 +0,0 @@ -namespace EventStore.Client.Streams.Tests.Obsolete; - -[Trait("Category", "Security")] -[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class overriden_system_stream_security_for_all_obsolete(ITestOutputHelper output, overriden_system_stream_security_for_all_obsolete.CustomFixture fixture) : EventStoreTests(output, fixture) { - [Fact] - public async Task operations_on_system_stream_succeeds_for_user() { - var stream = $"${Fixture.GetStreamName()}"; - await Fixture.AppendStream(stream, TestCredentials.TestUser1); - await Fixture.ReadEvent(stream, TestCredentials.TestUser1); - await Fixture.ReadStreamForward(stream, TestCredentials.TestUser1); - await Fixture.ReadStreamBackward(stream, TestCredentials.TestUser1); - - await Fixture.ReadMeta(stream, TestCredentials.TestUser1); - await Fixture.WriteMeta(stream, TestCredentials.TestUser1); - - await Fixture.SubscribeToStreamObsolete(stream, TestCredentials.TestUser1); - - await Fixture.DeleteStream(stream, TestCredentials.TestUser1); - } - - [AnonymousAccess.Fact] - public async Task operations_on_system_stream_fail_for_anonymous_user() { - var stream = $"${Fixture.GetStreamName()}"; - await Fixture.AppendStream(stream); - await Fixture.ReadEvent(stream); - await Fixture.ReadStreamForward(stream); - await Fixture.ReadStreamBackward(stream); - - await Fixture.ReadMeta(stream); - await Fixture.WriteMeta(stream); - - await Fixture.SubscribeToStreamObsolete(stream); - - await Fixture.DeleteStream(stream); - } - - [Fact] - public async Task operations_on_system_stream_succeed_for_admin() { - var stream = $"${Fixture.GetStreamName()}"; - await Fixture.AppendStream(stream, TestCredentials.TestAdmin); - - await Fixture.ReadEvent(stream, TestCredentials.TestAdmin); - await Fixture.ReadStreamForward(stream, TestCredentials.TestAdmin); - await Fixture.ReadStreamBackward(stream, TestCredentials.TestAdmin); - - await Fixture.ReadMeta(stream, TestCredentials.TestAdmin); - await Fixture.WriteMeta(stream, TestCredentials.TestAdmin); - - await Fixture.SubscribeToStreamObsolete(stream, TestCredentials.TestAdmin); - - await Fixture.DeleteStream(stream, TestCredentials.TestAdmin); - } - - public class CustomFixture : SecurityFixture_obsolete { - protected override Task When() { - var settings = new SystemSettings( - systemStreamAcl: new( - SystemRoles.All, - SystemRoles.All, - SystemRoles.All, - SystemRoles.All, - SystemRoles.All - ) - ); - - return Streams.SetSystemSettingsAsync(settings, userCredentials: TestCredentials.TestAdmin); - } - } -} diff --git a/test/EventStore.Client.Streams.Tests/Security/Obsolete/overriden_system_stream_security_obsolete.cs b/test/EventStore.Client.Streams.Tests/Security/Obsolete/overriden_system_stream_security_obsolete.cs deleted file mode 100644 index f36646be1..000000000 --- a/test/EventStore.Client.Streams.Tests/Security/Obsolete/overriden_system_stream_security_obsolete.cs +++ /dev/null @@ -1,87 +0,0 @@ -namespace EventStore.Client.Streams.Tests.Obsolete; - -[Trait("Category", "Security")] -[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class overriden_system_stream_security_obsolete(ITestOutputHelper output, overriden_system_stream_security_obsolete.CustomFixture fixture) : EventStoreTests(output, fixture) { - [Fact] - public async Task operations_on_system_stream_succeed_for_authorized_user() { - var stream = $"${Fixture.GetStreamName()}"; - await Fixture.AppendStream(stream, TestCredentials.TestUser1); - - await Fixture.ReadEvent(stream, TestCredentials.TestUser1); - await Fixture.ReadStreamForward(stream, TestCredentials.TestUser1); - await Fixture.ReadStreamBackward(stream, TestCredentials.TestUser1); - - await Fixture.ReadMeta(stream, TestCredentials.TestUser1); - await Fixture.WriteMeta(stream, TestCredentials.TestUser1); - - await Fixture.SubscribeToStreamObsolete(stream, TestCredentials.TestUser1); - - await Fixture.DeleteStream(stream, TestCredentials.TestUser1); - } - - [Fact] - public async Task operations_on_system_stream_fail_for_not_authorized_user() { - var stream = $"${Fixture.GetStreamName()}"; - await Assert.ThrowsAsync(() => Fixture.ReadEvent(stream, TestCredentials.TestUser2)); - await Assert.ThrowsAsync(() => Fixture.ReadStreamForward(stream, TestCredentials.TestUser2)); - await Assert.ThrowsAsync( - () => - Fixture.ReadStreamBackward(stream, TestCredentials.TestUser2) - ); - - await Assert.ThrowsAsync(() => Fixture.AppendStream(stream, TestCredentials.TestUser2)); - - await Assert.ThrowsAsync(() => Fixture.ReadMeta(stream, TestCredentials.TestUser2)); - await Assert.ThrowsAsync(() => Fixture.WriteMeta(stream, TestCredentials.TestUser2)); - - await Assert.ThrowsAsync(() => Fixture.SubscribeToStreamObsolete(stream, TestCredentials.TestUser2)); - - await Assert.ThrowsAsync(() => Fixture.DeleteStream(stream, TestCredentials.TestUser2)); - } - - [Fact] - public async Task operations_on_system_stream_fail_for_anonymous_user() { - var stream = $"${Fixture.GetStreamName()}"; - await Assert.ThrowsAsync(() => Fixture.ReadEvent(stream)); - await Assert.ThrowsAsync(() => Fixture.ReadStreamForward(stream)); - await Assert.ThrowsAsync(() => Fixture.ReadStreamBackward(stream)); - - await Assert.ThrowsAsync(() => Fixture.AppendStream(stream)); - - await Assert.ThrowsAsync(() => Fixture.ReadMeta(stream)); - await Assert.ThrowsAsync(() => Fixture.WriteMeta(stream)); - - await Assert.ThrowsAsync(() => Fixture.SubscribeToStreamObsolete(stream)); - - await Assert.ThrowsAsync(() => Fixture.DeleteStream(stream)); - } - - [Fact] - public async Task operations_on_system_stream_succeed_for_admin() { - var stream = $"${Fixture.GetStreamName()}"; - await Fixture.AppendStream(stream, TestCredentials.TestAdmin); - - await Fixture.ReadEvent(stream, TestCredentials.TestAdmin); - await Fixture.ReadStreamForward(stream, TestCredentials.TestAdmin); - await Fixture.ReadStreamBackward(stream, TestCredentials.TestAdmin); - - await Fixture.ReadMeta(stream, TestCredentials.TestAdmin); - await Fixture.WriteMeta(stream, TestCredentials.TestAdmin); - - await Fixture.SubscribeToStreamObsolete(stream, TestCredentials.TestAdmin); - - await Fixture.DeleteStream(stream, TestCredentials.TestAdmin); - } - - public class CustomFixture : SecurityFixture_obsolete { - protected override Task When() { - var settings = new SystemSettings( - systemStreamAcl: new("user1", "user1", "user1", "user1", "user1"), - userStreamAcl: default - ); - - return Streams.SetSystemSettingsAsync(settings, userCredentials: TestCredentials.TestAdmin); - } - } -} diff --git a/test/EventStore.Client.Streams.Tests/Security/Obsolete/overriden_user_stream_security_obsolete.cs b/test/EventStore.Client.Streams.Tests/Security/Obsolete/overriden_user_stream_security_obsolete.cs deleted file mode 100644 index dcbd88fa1..000000000 --- a/test/EventStore.Client.Streams.Tests/Security/Obsolete/overriden_user_stream_security_obsolete.cs +++ /dev/null @@ -1,78 +0,0 @@ -namespace EventStore.Client.Streams.Tests.Obsolete; - -[Trait("Category", "Security")] -[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class overriden_user_stream_security_obsolete(ITestOutputHelper output, overriden_user_stream_security_obsolete.CustomFixture fixture) : EventStoreTests(output, fixture) { - [Fact] - public async Task operations_on_user_stream_succeeds_for_authorized_user() { - var stream = Fixture.GetStreamName(); - await Fixture.AppendStream(stream, TestCredentials.TestUser1); - - await Fixture.ReadEvent(stream, TestCredentials.TestUser1); - await Fixture.ReadStreamForward(stream, TestCredentials.TestUser1); - await Fixture.ReadStreamBackward(stream, TestCredentials.TestUser1); - - await Fixture.ReadMeta(stream, TestCredentials.TestUser1); - await Fixture.WriteMeta(stream, TestCredentials.TestUser1); - - await Fixture.SubscribeToStreamObsolete(stream, TestCredentials.TestUser1); - - await Fixture.DeleteStream(stream, TestCredentials.TestUser1); - } - - [Fact] - public async Task operations_on_user_stream_fail_for_not_authorized_user() { - var stream = Fixture.GetStreamName(); - await Assert.ThrowsAsync(() => Fixture.ReadEvent(stream, TestCredentials.TestUser2)); - await Assert.ThrowsAsync(() => Fixture.ReadStreamForward(stream, TestCredentials.TestUser2)); - await Assert.ThrowsAsync(() => Fixture.ReadStreamBackward(stream, TestCredentials.TestUser2)); - - await Assert.ThrowsAsync(() => Fixture.AppendStream(stream, TestCredentials.TestUser2)); - await Assert.ThrowsAsync(() => Fixture.ReadMeta(stream, TestCredentials.TestUser2)); - await Assert.ThrowsAsync(() => Fixture.WriteMeta(stream, TestCredentials.TestUser2)); - - await Assert.ThrowsAsync(() => Fixture.SubscribeToStreamObsolete(stream, TestCredentials.TestUser2)); - - await Assert.ThrowsAsync(() => Fixture.DeleteStream(stream, TestCredentials.TestUser2)); - } - - [Fact] - public async Task operations_on_user_stream_fail_for_anonymous_user() { - var stream = Fixture.GetStreamName(); - await Assert.ThrowsAsync(() => Fixture.ReadEvent(stream)); - await Assert.ThrowsAsync(() => Fixture.ReadStreamForward(stream)); - await Assert.ThrowsAsync(() => Fixture.ReadStreamBackward(stream)); - - await Assert.ThrowsAsync(() => Fixture.AppendStream(stream)); - await Assert.ThrowsAsync(() => Fixture.ReadMeta(stream)); - await Assert.ThrowsAsync(() => Fixture.WriteMeta(stream)); - - await Assert.ThrowsAsync(() => Fixture.SubscribeToStreamObsolete(stream)); - - await Assert.ThrowsAsync(() => Fixture.DeleteStream(stream)); - } - - [Fact] - public async Task operations_on_user_stream_succeed_for_admin() { - var stream = Fixture.GetStreamName(); - await Fixture.AppendStream(stream, TestCredentials.TestAdmin); - - await Fixture.ReadEvent(stream, TestCredentials.TestAdmin); - await Fixture.ReadStreamForward(stream, TestCredentials.TestAdmin); - await Fixture.ReadStreamBackward(stream, TestCredentials.TestAdmin); - - await Fixture.ReadMeta(stream, TestCredentials.TestAdmin); - await Fixture.WriteMeta(stream, TestCredentials.TestAdmin); - - await Fixture.SubscribeToStreamObsolete(stream, TestCredentials.TestAdmin); - - await Fixture.DeleteStream(stream, TestCredentials.TestAdmin); - } - - public class CustomFixture : SecurityFixture_obsolete { - protected override Task When() { - var settings = new SystemSettings(new("user1", "user1", "user1", "user1", "user1")); - return Streams.SetSystemSettingsAsync(settings, userCredentials: TestCredentials.TestAdmin); - } - } -} diff --git a/test/EventStore.Client.Streams.Tests/Security/Obsolete/subscribe_to_all_security_obsolete.cs b/test/EventStore.Client.Streams.Tests/Security/Obsolete/subscribe_to_all_security_obsolete.cs deleted file mode 100644 index b21cf66c6..000000000 --- a/test/EventStore.Client.Streams.Tests/Security/Obsolete/subscribe_to_all_security_obsolete.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace EventStore.Client.Streams.Tests.Obsolete; - -[Trait("Category", "Security")] -[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class subscribe_to_all_security_obsolete(ITestOutputHelper output, SecurityFixture_obsolete fixture) : EventStoreTests(output, fixture) { - [Fact] - public async Task subscribing_to_all_with_not_existing_credentials_is_not_authenticated() => - await Assert.ThrowsAsync(() => Fixture.SubscribeToAllObsolete(TestCredentials.TestBadUser)); - - [Fact] - public async Task subscribing_to_all_with_no_credentials_is_denied() => await Assert.ThrowsAsync(() => Fixture.SubscribeToAllObsolete()); - - [Fact] - public async Task subscribing_to_all_with_not_authorized_user_credentials_is_denied() => - await Assert.ThrowsAsync(() => Fixture.SubscribeToAllObsolete(TestCredentials.TestUser2)); - - [Fact] - public async Task subscribing_to_all_with_authorized_user_credentials_succeeds() => await Fixture.SubscribeToAllObsolete(TestCredentials.TestUser1); - - [Fact] - public async Task subscribing_to_all_with_admin_user_credentials_succeeds() => await Fixture.SubscribeToAllObsolete(TestCredentials.TestAdmin); -} \ No newline at end of file diff --git a/test/EventStore.Client.Streams.Tests/Security/Obsolete/subscribe_to_stream_security_obsolete.cs b/test/EventStore.Client.Streams.Tests/Security/Obsolete/subscribe_to_stream_security_obsolete.cs deleted file mode 100644 index 409256650..000000000 --- a/test/EventStore.Client.Streams.Tests/Security/Obsolete/subscribe_to_stream_security_obsolete.cs +++ /dev/null @@ -1,75 +0,0 @@ -namespace EventStore.Client.Streams.Tests.Obsolete; - -[Trait("Category", "Security")] -[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class subscribe_to_stream_security_obsolete(ITestOutputHelper output, SecurityFixture_obsolete fixture) : EventStoreTests(output, fixture) { - [Fact] - public async Task subscribing_to_stream_with_not_existing_credentials_is_not_authenticated() => - await Assert.ThrowsAsync(() => Fixture.SubscribeToStreamObsolete(SecurityFixture_obsolete.ReadStream, TestCredentials.TestBadUser)); - - [Fact] - public async Task subscribing_to_stream_with_no_credentials_is_denied() => - await Assert.ThrowsAsync(() => Fixture.SubscribeToStreamObsolete(SecurityFixture_obsolete.ReadStream)); - - [Fact] - public async Task subscribing_to_stream_with_not_authorized_user_credentials_is_denied() => - await Assert.ThrowsAsync(() => Fixture.SubscribeToStreamObsolete(SecurityFixture_obsolete.ReadStream, TestCredentials.TestUser2)); - - [Fact] - public async Task reading_stream_with_authorized_user_credentials_succeeds() { - await Fixture.AppendStream(SecurityFixture_obsolete.ReadStream, TestCredentials.TestUser1); - await Fixture.SubscribeToStreamObsolete(SecurityFixture_obsolete.ReadStream, TestCredentials.TestUser1); - } - - [Fact] - public async Task reading_stream_with_admin_user_credentials_succeeds() { - await Fixture.AppendStream(SecurityFixture_obsolete.ReadStream, TestCredentials.TestAdmin); - await Fixture.SubscribeToStreamObsolete(SecurityFixture_obsolete.ReadStream, TestCredentials.TestAdmin); - } - - [AnonymousAccess.Fact] - public async Task subscribing_to_no_acl_stream_succeeds_when_no_credentials_are_passed() { - await Fixture.AppendStream(SecurityFixture_obsolete.NoAclStream); - await Fixture.SubscribeToStreamObsolete(SecurityFixture_obsolete.NoAclStream); - } - - [Fact] - public async Task subscribing_to_no_acl_stream_is_not_authenticated_when_not_existing_credentials_are_passed() => - await Assert.ThrowsAsync(() => Fixture.SubscribeToStreamObsolete(SecurityFixture_obsolete.NoAclStream, TestCredentials.TestBadUser)); - - [Fact] - public async Task subscribing_to_no_acl_stream_succeeds_when_any_existing_user_credentials_are_passed() { - await Fixture.AppendStream(SecurityFixture_obsolete.NoAclStream, TestCredentials.TestUser1); - await Fixture.SubscribeToStreamObsolete(SecurityFixture_obsolete.NoAclStream, TestCredentials.TestUser1); - await Fixture.SubscribeToStreamObsolete(SecurityFixture_obsolete.NoAclStream, TestCredentials.TestUser2); - } - - [Fact] - public async Task subscribing_to_no_acl_stream_succeeds_when_admin_user_credentials_are_passed() { - await Fixture.AppendStream(SecurityFixture_obsolete.NoAclStream, TestCredentials.TestAdmin); - await Fixture.SubscribeToStreamObsolete(SecurityFixture_obsolete.NoAclStream, TestCredentials.TestAdmin); - } - - [AnonymousAccess.Fact] - public async Task subscribing_to_all_access_normal_stream_succeeds_when_no_credentials_are_passed() { - await Fixture.AppendStream(SecurityFixture_obsolete.NormalAllStream); - await Fixture.SubscribeToStreamObsolete(SecurityFixture_obsolete.NormalAllStream); - } - - [Fact] - public async Task subscribing_to_all_access_normal_stream_is_not_authenticated_when_not_existing_credentials_are_passed() => - await Assert.ThrowsAsync(() => Fixture.SubscribeToStreamObsolete(SecurityFixture_obsolete.NormalAllStream, TestCredentials.TestBadUser)); - - [Fact] - public async Task subscribing_to_all_access_normal_stream_succeeds_when_any_existing_user_credentials_are_passed() { - await Fixture.AppendStream(SecurityFixture_obsolete.NormalAllStream, TestCredentials.TestUser1); - await Fixture.SubscribeToStreamObsolete(SecurityFixture_obsolete.NormalAllStream, TestCredentials.TestUser1); - await Fixture.SubscribeToStreamObsolete(SecurityFixture_obsolete.NormalAllStream, TestCredentials.TestUser2); - } - - [Fact] - public async Task subscribing_to_all_access_normal_streamm_succeeds_when_admin_user_credentials_are_passed() { - await Fixture.AppendStream(SecurityFixture_obsolete.NormalAllStream, TestCredentials.TestAdmin); - await Fixture.SubscribeToStreamObsolete(SecurityFixture_obsolete.NormalAllStream, TestCredentials.TestAdmin); - } -} diff --git a/test/EventStore.Client.Streams.Tests/Security/Obsolete/system_stream_security_obsolete.cs b/test/EventStore.Client.Streams.Tests/Security/Obsolete/system_stream_security_obsolete.cs deleted file mode 100644 index 462696ff2..000000000 --- a/test/EventStore.Client.Streams.Tests/Security/Obsolete/system_stream_security_obsolete.cs +++ /dev/null @@ -1,146 +0,0 @@ -namespace EventStore.Client.Streams.Tests.Obsolete; - -[Trait("Category", "Security")] -[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class system_stream_security_obsolete(ITestOutputHelper output, SecurityFixture_obsolete fixture) : EventStoreTests(output, fixture) { - [Fact] - public async Task operations_on_system_stream_with_no_acl_set_fail_for_non_admin() { - await Assert.ThrowsAsync(() => Fixture.ReadEvent("$system-no-acl", TestCredentials.TestUser1)); - await Assert.ThrowsAsync(() => Fixture.ReadStreamForward("$system-no-acl", TestCredentials.TestUser1)); - - await Assert.ThrowsAsync(() => Fixture.ReadStreamBackward("$system-no-acl", TestCredentials.TestUser1)); - - await Assert.ThrowsAsync(() => Fixture.AppendStream("$system-no-acl", TestCredentials.TestUser1)); - - await Assert.ThrowsAsync(() => Fixture.ReadMeta("$system-no-acl", TestCredentials.TestUser1)); - await Assert.ThrowsAsync(() => Fixture.WriteMeta("$system-no-acl", TestCredentials.TestUser1)); - - await Assert.ThrowsAsync(() => Fixture.SubscribeToStreamObsolete("$system-no-acl", TestCredentials.TestUser1)); - } - - [Fact] - public async Task operations_on_system_stream_with_no_acl_set_succeed_for_admin() { - await Fixture.AppendStream("$system-no-acl", TestCredentials.TestAdmin); - - await Fixture.ReadEvent("$system-no-acl", TestCredentials.TestAdmin); - await Fixture.ReadStreamForward("$system-no-acl", TestCredentials.TestAdmin); - await Fixture.ReadStreamBackward("$system-no-acl", TestCredentials.TestAdmin); - - await Fixture.ReadMeta("$system-no-acl", TestCredentials.TestAdmin); - await Fixture.WriteMeta("$system-no-acl", TestCredentials.TestAdmin); - - await Fixture.SubscribeToStreamObsolete("$system-no-acl", TestCredentials.TestAdmin); - } - - [Fact] - public async Task operations_on_system_stream_with_acl_set_to_usual_user_fail_for_not_authorized_user() { - await Assert.ThrowsAsync(() => Fixture.ReadEvent(SecurityFixture_obsolete.SystemAclStream, TestCredentials.TestUser2)); - await Assert.ThrowsAsync(() => Fixture.ReadStreamForward(SecurityFixture_obsolete.SystemAclStream, TestCredentials.TestUser2)); - await Assert.ThrowsAsync(() => Fixture.ReadStreamBackward(SecurityFixture_obsolete.SystemAclStream, TestCredentials.TestUser2)); - - await Assert.ThrowsAsync(() => Fixture.AppendStream(SecurityFixture_obsolete.SystemAclStream, TestCredentials.TestUser2)); - - await Assert.ThrowsAsync(() => Fixture.ReadMeta(SecurityFixture_obsolete.SystemAclStream, TestCredentials.TestUser2)); - await Assert.ThrowsAsync(() => Fixture.WriteMeta(SecurityFixture_obsolete.SystemAclStream, TestCredentials.TestUser2, TestCredentials.TestUser1.Username)); - - await Assert.ThrowsAsync(() => Fixture.SubscribeToStreamObsolete(SecurityFixture_obsolete.SystemAclStream, TestCredentials.TestUser2)); - } - - [Fact] - public async Task operations_on_system_stream_with_acl_set_to_usual_user_succeed_for_that_user() { - await Fixture.AppendStream(SecurityFixture_obsolete.SystemAclStream, TestCredentials.TestUser1); - await Fixture.ReadEvent(SecurityFixture_obsolete.SystemAclStream, TestCredentials.TestUser1); - await Fixture.ReadStreamForward(SecurityFixture_obsolete.SystemAclStream, TestCredentials.TestUser1); - await Fixture.ReadStreamBackward(SecurityFixture_obsolete.SystemAclStream, TestCredentials.TestUser1); - - await Fixture.ReadMeta(SecurityFixture_obsolete.SystemAclStream, TestCredentials.TestUser1); - await Fixture.WriteMeta(SecurityFixture_obsolete.SystemAclStream, TestCredentials.TestUser1, TestCredentials.TestUser1.Username); - - await Fixture.SubscribeToStreamObsolete(SecurityFixture_obsolete.SystemAclStream, TestCredentials.TestUser1); - } - - [Fact] - public async Task operations_on_system_stream_with_acl_set_to_usual_user_succeed_for_admin() { - await Fixture.AppendStream(SecurityFixture_obsolete.SystemAclStream, TestCredentials.TestAdmin); - await Fixture.ReadEvent(SecurityFixture_obsolete.SystemAclStream, TestCredentials.TestAdmin); - await Fixture.ReadStreamForward(SecurityFixture_obsolete.SystemAclStream, TestCredentials.TestAdmin); - await Fixture.ReadStreamBackward(SecurityFixture_obsolete.SystemAclStream, TestCredentials.TestAdmin); - - await Fixture.ReadMeta(SecurityFixture_obsolete.SystemAclStream, TestCredentials.TestAdmin); - await Fixture.WriteMeta(SecurityFixture_obsolete.SystemAclStream, TestCredentials.TestAdmin, TestCredentials.TestUser1.Username); - - await Fixture.SubscribeToStreamObsolete(SecurityFixture_obsolete.SystemAclStream, TestCredentials.TestAdmin); - } - - [Fact] - public async Task operations_on_system_stream_with_acl_set_to_admins_fail_for_usual_user() { - await Assert.ThrowsAsync(() => Fixture.ReadEvent(SecurityFixture_obsolete.SystemAdminStream, TestCredentials.TestUser1)); - await Assert.ThrowsAsync(() => Fixture.ReadStreamForward(SecurityFixture_obsolete.SystemAdminStream, TestCredentials.TestUser1)); - await Assert.ThrowsAsync( - () => - Fixture.ReadStreamBackward(SecurityFixture_obsolete.SystemAdminStream, TestCredentials.TestUser1) - ); - - await Assert.ThrowsAsync(() => Fixture.AppendStream(SecurityFixture_obsolete.SystemAdminStream, TestCredentials.TestUser1)); - - await Assert.ThrowsAsync(() => Fixture.ReadMeta(SecurityFixture_obsolete.SystemAdminStream, TestCredentials.TestUser1)); - await Assert.ThrowsAsync( - () => - Fixture.WriteMeta(SecurityFixture_obsolete.SystemAdminStream, TestCredentials.TestUser1, SystemRoles.Admins) - ); - - await Assert.ThrowsAsync(() => Fixture.SubscribeToStreamObsolete(SecurityFixture_obsolete.SystemAdminStream, TestCredentials.TestUser1)); - } - - [Fact] - public async Task operations_on_system_stream_with_acl_set_to_admins_succeed_for_admin() { - await Fixture.AppendStream(SecurityFixture_obsolete.SystemAdminStream, TestCredentials.TestAdmin); - await Fixture.ReadEvent(SecurityFixture_obsolete.SystemAdminStream, TestCredentials.TestAdmin); - await Fixture.ReadStreamForward(SecurityFixture_obsolete.SystemAdminStream, TestCredentials.TestAdmin); - await Fixture.ReadStreamBackward(SecurityFixture_obsolete.SystemAdminStream, TestCredentials.TestAdmin); - - await Fixture.ReadMeta(SecurityFixture_obsolete.SystemAdminStream, TestCredentials.TestAdmin); - await Fixture.WriteMeta(SecurityFixture_obsolete.SystemAdminStream, TestCredentials.TestAdmin, SystemRoles.Admins); - - await Fixture.SubscribeToStreamObsolete(SecurityFixture_obsolete.SystemAdminStream, TestCredentials.TestAdmin); - } - - [AnonymousAccess.Fact] - public async Task operations_on_system_stream_with_acl_set_to_all_succeed_for_not_authenticated_user() { - await Fixture.AppendStream(SecurityFixture_obsolete.SystemAllStream); - await Fixture.ReadEvent(SecurityFixture_obsolete.SystemAllStream); - await Fixture.ReadStreamForward(SecurityFixture_obsolete.SystemAllStream); - await Fixture.ReadStreamBackward(SecurityFixture_obsolete.SystemAllStream); - - await Fixture.ReadMeta(SecurityFixture_obsolete.SystemAllStream); - await Fixture.WriteMeta(SecurityFixture_obsolete.SystemAllStream, role: SystemRoles.All); - - await Fixture.SubscribeToStreamObsolete(SecurityFixture_obsolete.SystemAllStream); - } - - [Fact] - public async Task operations_on_system_stream_with_acl_set_to_all_succeed_for_usual_user() { - await Fixture.AppendStream(SecurityFixture_obsolete.SystemAllStream, TestCredentials.TestUser1); - await Fixture.ReadEvent(SecurityFixture_obsolete.SystemAllStream, TestCredentials.TestUser1); - await Fixture.ReadStreamForward(SecurityFixture_obsolete.SystemAllStream, TestCredentials.TestUser1); - await Fixture.ReadStreamBackward(SecurityFixture_obsolete.SystemAllStream, TestCredentials.TestUser1); - - await Fixture.ReadMeta(SecurityFixture_obsolete.SystemAllStream, TestCredentials.TestUser1); - await Fixture.WriteMeta(SecurityFixture_obsolete.SystemAllStream, TestCredentials.TestUser1, SystemRoles.All); - - await Fixture.SubscribeToStreamObsolete(SecurityFixture_obsolete.SystemAllStream, TestCredentials.TestUser1); - } - - [Fact] - public async Task operations_on_system_stream_with_acl_set_to_all_succeed_for_admin() { - await Fixture.AppendStream(SecurityFixture_obsolete.SystemAllStream, TestCredentials.TestAdmin); - await Fixture.ReadEvent(SecurityFixture_obsolete.SystemAllStream, TestCredentials.TestAdmin); - await Fixture.ReadStreamForward(SecurityFixture_obsolete.SystemAllStream, TestCredentials.TestAdmin); - await Fixture.ReadStreamBackward(SecurityFixture_obsolete.SystemAllStream, TestCredentials.TestAdmin); - - await Fixture.ReadMeta(SecurityFixture_obsolete.SystemAllStream, TestCredentials.TestAdmin); - await Fixture.WriteMeta(SecurityFixture_obsolete.SystemAllStream, TestCredentials.TestAdmin, SystemRoles.All); - - await Fixture.SubscribeToStreamObsolete(SecurityFixture_obsolete.SystemAllStream, TestCredentials.TestAdmin); - } -} diff --git a/test/EventStore.Client.Streams.Tests/Serialization/is_json.cs b/test/EventStore.Client.Streams.Tests/Serialization/is_json.cs deleted file mode 100644 index d2b44a918..000000000 --- a/test/EventStore.Client.Streams.Tests/Serialization/is_json.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System.Runtime.CompilerServices; -using System.Text; - -namespace EventStore.Client.Streams.Tests.Serialization; - -[Trait("Category", "Serialization")] -public class is_json(ITestOutputHelper output, EventStoreFixture fixture) : EventStoreTests(output, fixture) { - public static IEnumerable TestCases() { - var json = @"{""some"":""json""}"; - - yield return new object?[] { true, json, string.Empty }; - yield return new object?[] { true, string.Empty, json }; - yield return new object?[] { true, json, json }; - yield return new object?[] { false, json, string.Empty }; - yield return new object?[] { false, string.Empty, json }; - yield return new object?[] { false, json, json }; - } - - [Theory] - [MemberData(nameof(TestCases))] - public async Task is_preserved(bool isJson, string data, string metadata) { - var stream = GetStreamName(isJson, data, metadata); - var encoding = Encoding.UTF8; - var eventData = new EventData( - Uuid.NewUuid(), - "-", - encoding.GetBytes(data), - encoding.GetBytes(metadata), - isJson - ? Constants.Metadata.ContentTypes.ApplicationJson - : Constants.Metadata.ContentTypes.ApplicationOctetStream - ); - - await Fixture.Streams.AppendToStreamAsync(stream, StreamState.Any, new[] { eventData }); - - var @event = await Fixture.Streams - .ReadStreamAsync( - Direction.Forwards, - stream, - StreamPosition.Start, - 1, - true - ) - .FirstOrDefaultAsync(); - - Assert.Equal( - isJson - ? Constants.Metadata.ContentTypes.ApplicationJson - : Constants.Metadata.ContentTypes.ApplicationOctetStream, - @event.Event.ContentType - ); - - Assert.Equal(data, encoding.GetString(@event.Event.Data.ToArray())); - Assert.Equal(metadata, encoding.GetString(@event.Event.Metadata.ToArray())); - } - - string GetStreamName(bool isJson, string data, string metadata, [CallerMemberName] string? testMethod = default) => - $"{Fixture.GetStreamName(testMethod)}_{isJson}_{(data == string.Empty ? "no_data" : "data")}_{(metadata == string.Empty ? "no_metadata" : "metadata")}"; -} \ No newline at end of file diff --git a/test/EventStore.Client.Streams.Tests/Subscriptions/Obsolete/subscribe_to_all_obsolete.cs b/test/EventStore.Client.Streams.Tests/Subscriptions/Obsolete/subscribe_to_all_obsolete.cs deleted file mode 100644 index af4b5ac43..000000000 --- a/test/EventStore.Client.Streams.Tests/Subscriptions/Obsolete/subscribe_to_all_obsolete.cs +++ /dev/null @@ -1,592 +0,0 @@ -namespace EventStore.Client.Streams.Tests.Subscriptions.Obsolete; - -[Trait("Category", "Subscriptions")] -[Trait("Category", "Target:All")] -[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class subscribe_to_all_obsolete(ITestOutputHelper output, SubscriptionsFixture fixture) : EventStoreTests(output, fixture) { - [Fact] - public async Task receives_all_events_from_start() { - var receivedAllEvents = new TaskCompletionSource(); - var subscriptionDropped = new TaskCompletionSource(); - - var seedEvents = Fixture.CreateTestEvents(10).ToArray(); - var pageSize = seedEvents.Length / 2; - - var availableEvents = new HashSet(seedEvents.Select(x => x.EventId)); - - foreach (var evt in seedEvents.Take(pageSize)) - await Fixture.Streams.AppendToStreamAsync($"stream-{evt.EventId.ToGuid():N}", StreamState.NoStream, new[] { evt }); - - using var subscription = await Fixture.Streams - .SubscribeToAllAsync(FromAll.Start, OnReceived, false, OnDropped) - .WithTimeout(); - - foreach (var evt in seedEvents.Skip(pageSize)) - await Fixture.Streams.AppendToStreamAsync($"stream-{evt.EventId.ToGuid():N}", StreamState.NoStream, new[] { evt }); - - await receivedAllEvents.Task.WithTimeout(); - - // if the subscription dropped before time, raise the reason why - if (subscriptionDropped.Task.IsCompleted) - subscriptionDropped.Task.IsCompleted.ShouldBe(false, subscriptionDropped.Task.Result.ToString()); - - // stop the subscription - subscription.Dispose(); - var result = await subscriptionDropped.Task.WithTimeout(); - result.ShouldBe(SubscriptionDroppedResult.Disposed()); - - return; - - Task OnReceived(StreamSubscription sub, ResolvedEvent re, CancellationToken ct) { - availableEvents.RemoveWhere(x => x == re.OriginalEvent.EventId); - - if (availableEvents.Count == 0) { - receivedAllEvents.TrySetResult(true); - Fixture.Log.Information("Received all {TotalEventsCount} expected events", seedEvents.Length); - } - - return Task.CompletedTask; - } - - void OnDropped(StreamSubscription sub, SubscriptionDroppedReason reason, Exception? ex) => - subscriptionDropped.SetResult(new(reason, ex)); - } - - [Fact] - public async Task receives_all_events_from_end() { - var receivedAllEvents = new TaskCompletionSource(); - var subscriptionDropped = new TaskCompletionSource(); - - var seedEvents = Fixture.CreateTestEvents(10).ToArray(); - - var availableEvents = new HashSet(seedEvents.Select(x => x.EventId)); - - using var subscription = await Fixture.Streams - .SubscribeToAllAsync(FromAll.End, OnReceived, false, OnDropped) - .WithTimeout(); - - // add the events we want to receive after we start the subscription - foreach (var evt in seedEvents) - await Fixture.Streams.AppendToStreamAsync($"stream-{evt.EventId.ToGuid():N}", StreamState.NoStream, new[] { evt }); - - await receivedAllEvents.Task.WithTimeout(); - - // if the subscription dropped before time, raise the reason why - if (subscriptionDropped.Task.IsCompleted) - subscriptionDropped.Task.IsCompleted.ShouldBe(false, subscriptionDropped.Task.Result.ToString()); - - // stop the subscription - subscription.Dispose(); - var result = await subscriptionDropped.Task.WithTimeout(); - result.ShouldBe(SubscriptionDroppedResult.Disposed()); - - return; - - Task OnReceived(StreamSubscription sub, ResolvedEvent re, CancellationToken ct) { - availableEvents.RemoveWhere(x => x == re.OriginalEvent.EventId); - - if (availableEvents.Count == 0) { - receivedAllEvents.TrySetResult(true); - Fixture.Log.Information("Received all {TotalEventsCount} expected events", seedEvents.Length); - } - - return Task.CompletedTask; - } - - void OnDropped(StreamSubscription sub, SubscriptionDroppedReason reason, Exception? ex) => - subscriptionDropped.SetResult(new(reason, ex)); - } - - [Fact] - public async Task receives_all_events_from_position() { - var receivedAllEvents = new TaskCompletionSource(); - var subscriptionDropped = new TaskCompletionSource(); - - var seedEvents = Fixture.CreateTestEvents(10).ToArray(); - var pageSize = seedEvents.Length / 2; - - // only the second half of the events will be received - var availableEvents = new HashSet(seedEvents.Skip(pageSize).Select(x => x.EventId)); - - IWriteResult writeResult = new SuccessResult(); - foreach (var evt in seedEvents.Take(pageSize)) - writeResult = await Fixture.Streams.AppendToStreamAsync($"stream-{evt.EventId.ToGuid():N}", StreamState.NoStream, new[] { evt }); - - var position = FromAll.After(writeResult.LogPosition); - - using var subscription = await Fixture.Streams - .SubscribeToAllAsync(position, OnReceived, false, OnDropped) - .WithTimeout(); - - foreach (var evt in seedEvents.Skip(pageSize)) - await Fixture.Streams.AppendToStreamAsync($"stream-{evt.EventId.ToGuid():N}", StreamState.NoStream, new[] { evt }); - - await receivedAllEvents.Task.WithTimeout(); - - // if the subscription dropped before time, raise the reason why - if (subscriptionDropped.Task.IsCompleted) - subscriptionDropped.Task.IsCompleted.ShouldBe(false, subscriptionDropped.Task.Result.ToString()); - - // stop the subscription - subscription.Dispose(); - var result = await subscriptionDropped.Task.WithTimeout(); - result.ShouldBe(SubscriptionDroppedResult.Disposed()); - - return; - - Task OnReceived(StreamSubscription sub, ResolvedEvent re, CancellationToken ct) { - availableEvents.RemoveWhere(x => x == re.OriginalEvent.EventId); - - if (availableEvents.Count == 0) { - receivedAllEvents.TrySetResult(true); - Fixture.Log.Information("Received all {TotalEventsCount} expected events", pageSize); - } - - return Task.CompletedTask; - } - - void OnDropped(StreamSubscription sub, SubscriptionDroppedReason reason, Exception? ex) => - subscriptionDropped.SetResult(new(reason, ex)); - } - - [Fact] - public async Task receives_all_events_with_resolved_links() { - var streamName = Fixture.GetStreamName(); - - var receivedAllEvents = new TaskCompletionSource(); - var subscriptionDropped = new TaskCompletionSource(); - - var seedEvents = Fixture.CreateTestEvents(3).ToArray(); - var availableEvents = new HashSet(seedEvents.Select(x => x.EventId)); - - await Fixture.Streams.AppendToStreamAsync(streamName, StreamState.NoStream, seedEvents); - - using var subscription = await Fixture.Streams - .SubscribeToAllAsync(FromAll.Start, OnReceived, true, OnDropped) - .WithTimeout(); - - await receivedAllEvents.Task.WithTimeout(); - - // if the subscription dropped before time, raise the reason why - if (subscriptionDropped.Task.IsCompleted) - subscriptionDropped.Task.IsCompleted.ShouldBe(false, subscriptionDropped.Task.Result.ToString()); - - // stop the subscription - subscription.Dispose(); - var result = await subscriptionDropped.Task.WithTimeout(); - result.ShouldBe(SubscriptionDroppedResult.Disposed()); - - return; - - Task OnReceived(StreamSubscription sub, ResolvedEvent re, CancellationToken ct) { - var hasResolvedLink = re.OriginalEvent.EventStreamId.StartsWith($"$et-{EventStoreFixture.TestEventType}"); - if (availableEvents.RemoveWhere(x => x == re.Event.EventId && hasResolvedLink) == 0) { - Fixture.Log.Debug("Received unexpected event {EventId} from stream {StreamId}", re.Event.EventId, re.OriginalEvent.EventStreamId); - return Task.CompletedTask; - } - - if (availableEvents.Count == 0) { - receivedAllEvents.TrySetResult(true); - Fixture.Log.Information("Received all {TotalEventsCount} expected events", seedEvents.Length); - } - - return Task.CompletedTask; - } - - void OnDropped(StreamSubscription sub, SubscriptionDroppedReason reason, Exception? ex) => - subscriptionDropped.SetResult(new(reason, ex)); - } - - [Theory] - [MemberData(nameof(SubscriptionFilter.TestCases), MemberType= typeof(SubscriptionFilter))] - public async Task receives_all_filtered_events_from_start(SubscriptionFilter filter) { - var streamPrefix = $"{nameof(receives_all_filtered_events_from_start)}-{filter.Name}-{Guid.NewGuid():N}"; - - Fixture.Log.Information("Using filter {FilterName} with prefix {StreamPrefix}", filter.Name, streamPrefix); - - var receivedAllEvents = new TaskCompletionSource(); - var subscriptionDropped = new TaskCompletionSource(); - var checkpointReached = new TaskCompletionSource(); - - var seedEvents = Fixture.CreateTestEvents(64) - .Select(evt => filter.PrepareEvent(streamPrefix, evt)) - .ToArray(); - - var pageSize = seedEvents.Length / 2; - - var availableEvents = new HashSet(seedEvents.Select(x => x.EventId)); - - // add noise - await Fixture.Streams.AppendToStreamAsync(Fixture.GetStreamName(), StreamState.NoStream, Fixture.CreateTestEvents(3)); - - var existingEventsCount = await Fixture.Streams.ReadAllAsync(Direction.Forwards, Position.Start).CountAsync(); - Fixture.Log.Debug("Existing events count: {ExistingEventsCount}", existingEventsCount); - - // Debugging: - // await foreach (var evt in Fixture.Streams.ReadAllAsync(Direction.Forwards, Position.Start)) - // Fixture.Log.Debug("Read event {EventId} from {StreamId}.", evt.OriginalEvent.EventId, evt.OriginalEvent.EventStreamId); - - // add some of the events we want to see before we start the subscription - foreach (var evt in seedEvents.Take(pageSize)) - await Fixture.Streams.AppendToStreamAsync($"{streamPrefix}-{evt.EventId.ToGuid():N}", StreamState.NoStream, new[] { evt }); - - var filterOptions = new SubscriptionFilterOptions(filter.Create(streamPrefix), 1, CheckpointReached); - - using var subscription = await Fixture.Streams - .SubscribeToAllAsync(FromAll.Start, OnReceived, false, OnDropped, filterOptions) - .WithTimeout(); - - // add some of the events we want to see after we start the subscription - foreach (var evt in seedEvents.Skip(pageSize)) - await Fixture.Streams.AppendToStreamAsync($"{streamPrefix}-{evt.EventId.ToGuid():N}", StreamState.NoStream, new[] { evt }); - - // wait until all events were received and at least one checkpoint was reached? - await receivedAllEvents.Task.WithTimeout(); - await checkpointReached.Task.WithTimeout(); - - // await Task.WhenAll(receivedAllEvents.Task, checkpointReached.Task).WithTimeout(); - - // if the subscription dropped before time, raise the reason why - if (subscriptionDropped.Task.IsCompleted) - subscriptionDropped.Task.IsCompleted.ShouldBe(false, subscriptionDropped.Task.Result.ToString()); - - // stop the subscription - subscription.Dispose(); - var result = await subscriptionDropped.Task.WithTimeout(); - result.ShouldBe(SubscriptionDroppedResult.Disposed()); - - Task OnReceived(StreamSubscription sub, ResolvedEvent re, CancellationToken ct) { - if (availableEvents.RemoveWhere(x => x == re.OriginalEvent.EventId) == 0) { - Fixture.Log.Error( - "Received unexpected event {EventId} from {StreamId}", - re.OriginalEvent.EventId, - re.OriginalEvent.EventStreamId - ); - - receivedAllEvents.TrySetException( - new InvalidOperationException($"Received unexpected event {re.OriginalEvent.EventId} from stream {re.OriginalEvent.EventStreamId}") - ); - } - else { - Fixture.Log.Verbose("Received expected event {EventId} from {StreamId}.", re.OriginalEvent.EventId, re.OriginalEvent.EventStreamId); - } - - if (availableEvents.Count == 0) { - receivedAllEvents.TrySetResult(true); - Fixture.Log.Information("Received all {TotalEventsCount} expected events.", seedEvents.Length); - } - - return Task.CompletedTask; - } - - void OnDropped(StreamSubscription sub, SubscriptionDroppedReason reason, Exception? ex) { - subscriptionDropped.SetResult(new(reason, ex)); - if (reason != SubscriptionDroppedReason.Disposed) { - receivedAllEvents.TrySetException(ex!); - checkpointReached.TrySetException(ex!); - } - } - - Task CheckpointReached(StreamSubscription sub, Position position, CancellationToken ct) { - Fixture.Log.Verbose( - "Checkpoint reached {Position}. Received {ReceivedEventsCount}/{TotalEventsCount} events", - position, seedEvents.Length - availableEvents.Count, seedEvents.Length - ); - checkpointReached.TrySetResult(true); - return Task.CompletedTask; - } - } - - [Theory] - [MemberData(nameof(SubscriptionFilter.TestCases), MemberType= typeof(SubscriptionFilter))] - public async Task receives_all_filtered_events_from_end(SubscriptionFilter filter) { - var streamPrefix = $"{nameof(receives_all_filtered_events_from_end)}-{filter.Name}-{Guid.NewGuid():N}"; - - Fixture.Log.Information("Using filter {FilterName} with prefix {StreamPrefix}", filter.Name, streamPrefix); - - var receivedAllEvents = new TaskCompletionSource(); - var subscriptionDropped = new TaskCompletionSource(); - var checkpointReached = new TaskCompletionSource(); - - var seedEvents = Fixture.CreateTestEvents(64) - .Select(evt => filter.PrepareEvent(streamPrefix, evt)) - .ToArray(); - - var pageSize = seedEvents.Length / 2; - - // only the second half of the events will be received - var availableEvents = new HashSet(seedEvents.Skip(pageSize).Select(x => x.EventId)); - - // add noise - await Fixture.Streams.AppendToStreamAsync(Fixture.GetStreamName(), StreamState.NoStream, Fixture.CreateTestEvents(3)); - - var existingEventsCount = await Fixture.Streams.ReadAllAsync(Direction.Forwards, Position.Start).CountAsync(); - Fixture.Log.Debug("Existing events count: {ExistingEventsCount}", existingEventsCount); - - // add some of the events that are a match to the filter but will not be received - foreach (var evt in seedEvents.Take(pageSize)) - await Fixture.Streams.AppendToStreamAsync($"{streamPrefix}-{evt.EventId.ToGuid():N}", StreamState.NoStream, new[] { evt }); - - var filterOptions = new SubscriptionFilterOptions(filter.Create(streamPrefix), 1, CheckpointReached); - - using var subscription = await Fixture.Streams - .SubscribeToAllAsync(FromAll.End, OnReceived, false, OnDropped, filterOptions) - .WithTimeout(); - - // add the events we want to receive after we start the subscription - foreach (var evt in seedEvents.Skip(pageSize)) - await Fixture.Streams.AppendToStreamAsync($"{streamPrefix}-{evt.EventId.ToGuid():N}", StreamState.NoStream, new[] { evt }); - - // wait until all events were received and at least one checkpoint was reached? - await receivedAllEvents.Task.WithTimeout(); - await checkpointReached.Task.WithTimeout(); - - // if the subscription dropped before time, raise the reason why - if (subscriptionDropped.Task.IsCompleted) - subscriptionDropped.Task.IsCompleted.ShouldBe(false, subscriptionDropped.Task.Result.ToString()); - - // stop the subscription - subscription.Dispose(); - var result = await subscriptionDropped.Task.WithTimeout(); - result.ShouldBe(SubscriptionDroppedResult.Disposed()); - - return; - - Task OnReceived(StreamSubscription sub, ResolvedEvent re, CancellationToken ct) { - if (availableEvents.RemoveWhere(x => x == re.OriginalEvent.EventId) == 0) { - Fixture.Log.Error( - "Received unexpected event {EventId} from {StreamId}", - re.OriginalEvent.EventId, - re.OriginalEvent.EventStreamId - ); - - receivedAllEvents.TrySetException( - new InvalidOperationException($"Received unexpected event {re.OriginalEvent.EventId} from stream {re.OriginalEvent.EventStreamId}") - ); - } - else { - Fixture.Log.Verbose("Received expected event {EventId} from {StreamId}", re.OriginalEvent.EventId, re.OriginalEvent.EventStreamId); - } - - if (availableEvents.Count == 0) { - receivedAllEvents.TrySetResult(true); - Fixture.Log.Information("Received all {TotalEventsCount} expected events", pageSize); - } - - return Task.CompletedTask; - } - - void OnDropped(StreamSubscription sub, SubscriptionDroppedReason reason, Exception? ex) { - subscriptionDropped.SetResult(new(reason, ex)); - if (reason != SubscriptionDroppedReason.Disposed) { - receivedAllEvents.TrySetException(ex!); - checkpointReached.TrySetException(ex!); - } - } - - Task CheckpointReached(StreamSubscription sub, Position position, CancellationToken ct) { - Fixture.Log.Verbose( - "Checkpoint reached {Position}. Received {ReceivedEventsCount}/{TotalEventsCount} events", - position, pageSize - availableEvents.Count, pageSize - ); - checkpointReached.TrySetResult(true); - return Task.CompletedTask; - } - } - - [Theory] - [MemberData(nameof(SubscriptionFilter.TestCases), MemberType= typeof(SubscriptionFilter))] - public async Task receives_all_filtered_events_from_position(SubscriptionFilter filter) { - var streamPrefix = $"{nameof(receives_all_filtered_events_from_position)}-{filter.Name}-{Guid.NewGuid():N}"; - - Fixture.Log.Information("Using filter {FilterName} with prefix {StreamPrefix}", filter.Name, streamPrefix); - - var receivedAllEvents = new TaskCompletionSource(); - var subscriptionDropped = new TaskCompletionSource(); - var checkpointReached = new TaskCompletionSource(); - - var seedEvents = Fixture.CreateTestEvents(64) - .Select(evt => filter.PrepareEvent(streamPrefix, evt)) - .ToArray(); - - var pageSize = seedEvents.Length / 2; - - // only the second half of the events will be received - var availableEvents = new HashSet(seedEvents.Skip(pageSize).Select(x => x.EventId)); - - // add noise - await Fixture.Streams.AppendToStreamAsync(Fixture.GetStreamName(), StreamState.NoStream, Fixture.CreateTestEvents(3)); - - var existingEventsCount = await Fixture.Streams.ReadAllAsync(Direction.Forwards, Position.Start).CountAsync(); - Fixture.Log.Debug("Existing events count: {ExistingEventsCount}", existingEventsCount); - - // add some of the events that are a match to the filter but will not be received - IWriteResult writeResult = new SuccessResult(); - foreach (var evt in seedEvents.Take(pageSize)) - writeResult = await Fixture.Streams.AppendToStreamAsync($"{streamPrefix}-{evt.EventId.ToGuid():N}", StreamState.NoStream, new[] { evt }); - - var position = FromAll.After(writeResult.LogPosition); - - var filterOptions = new SubscriptionFilterOptions(filter.Create(streamPrefix), 1, CheckpointReached); - - using var subscription = await Fixture.Streams - .SubscribeToAllAsync(position, OnReceived, false, OnDropped, filterOptions) - .WithTimeout(); - - // add the events we want to receive after we start the subscription - foreach (var evt in seedEvents.Skip(pageSize)) - await Fixture.Streams.AppendToStreamAsync($"{streamPrefix}-{evt.EventId.ToGuid():N}", StreamState.NoStream, new[] { evt }); - - // wait until all events were received and at least one checkpoint was reached? - await receivedAllEvents.Task.WithTimeout(); - await checkpointReached.Task.WithTimeout(); - - // if the subscription dropped before time, raise the reason why - if (subscriptionDropped.Task.IsCompleted) - subscriptionDropped.Task.IsCompleted.ShouldBe(false, subscriptionDropped.Task.Result.ToString()); - - // stop the subscription - subscription.Dispose(); - var result = await subscriptionDropped.Task.WithTimeout(); - result.ShouldBe(SubscriptionDroppedResult.Disposed()); - - return; - - Task OnReceived(StreamSubscription sub, ResolvedEvent re, CancellationToken ct) { - if (availableEvents.RemoveWhere(x => x == re.OriginalEvent.EventId) == 0) { - Fixture.Log.Error( - "Received unexpected event {EventId} from {StreamId}", - re.OriginalEvent.EventId, - re.OriginalEvent.EventStreamId - ); - - receivedAllEvents.TrySetException( - new InvalidOperationException($"Received unexpected event {re.OriginalEvent.EventId} from stream {re.OriginalEvent.EventStreamId}") - ); - } - else { - Fixture.Log.Verbose("Received expected event {EventId} from {StreamId}", re.OriginalEvent.EventId, re.OriginalEvent.EventStreamId); - } - - if (availableEvents.Count == 0) { - receivedAllEvents.TrySetResult(true); - Fixture.Log.Information("Received all {TotalEventsCount} expected events", pageSize); - } - - return Task.CompletedTask; - } - - void OnDropped(StreamSubscription sub, SubscriptionDroppedReason reason, Exception? ex) { - subscriptionDropped.SetResult(new(reason, ex)); - if (reason != SubscriptionDroppedReason.Disposed) { - receivedAllEvents.TrySetException(ex!); - checkpointReached.TrySetException(ex!); - } - } - - Task CheckpointReached(StreamSubscription sub, Position position, CancellationToken ct) { - Fixture.Log.Verbose( - "Checkpoint reached {Position}. Received {ReceivedEventsCount}/{TotalEventsCount} events", - position, pageSize - availableEvents.Count, pageSize - ); - checkpointReached.TrySetResult(true); - return Task.CompletedTask; - } - } - - [Fact] - public async Task receives_all_filtered_events_with_resolved_links() { - var streamName = Fixture.GetStreamName(); - - var receivedAllEvents = new TaskCompletionSource(); - var subscriptionDropped = new TaskCompletionSource(); - - var seedEvents = Fixture.CreateTestEvents(3).ToArray(); - var availableEvents = new HashSet(seedEvents.Select(x => x.EventId)); - - await Fixture.Streams.AppendToStreamAsync(streamName, StreamState.NoStream, seedEvents); - - var options = new SubscriptionFilterOptions( - StreamFilter.Prefix($"$et-{EventStoreFixture.TestEventType}") - ); - - using var subscription = await Fixture.Streams - .SubscribeToAllAsync(FromAll.Start, OnReceived, true, OnDropped, options) - .WithTimeout(); - - await receivedAllEvents.Task.WithTimeout(); - - // if the subscription dropped before time, raise the reason why - if (subscriptionDropped.Task.IsCompleted) - subscriptionDropped.Task.IsCompleted.ShouldBe(false, subscriptionDropped.Task.Result.ToString()); - - // stop the subscription - subscription.Dispose(); - var result = await subscriptionDropped.Task.WithTimeout(); - result.ShouldBe(SubscriptionDroppedResult.Disposed()); - - return; - - Task OnReceived(StreamSubscription sub, ResolvedEvent re, CancellationToken ct) { - var hasResolvedLink = re.OriginalEvent.EventStreamId.StartsWith($"$et-{EventStoreFixture.TestEventType}"); - if (availableEvents.RemoveWhere(x => x == re.Event.EventId && hasResolvedLink) == 0) { - Fixture.Log.Debug("Received unexpected event {EventId} from stream {StreamId}", re.Event.EventId, re.OriginalEvent.EventStreamId); - return Task.CompletedTask; - } - - if (availableEvents.Count == 0) { - receivedAllEvents.TrySetResult(true); - Fixture.Log.Information("Received all {TotalEventsCount} expected events", seedEvents.Length); - } - - return Task.CompletedTask; - } - - void OnDropped(StreamSubscription sub, SubscriptionDroppedReason reason, Exception? ex) => - subscriptionDropped.SetResult(new(reason, ex)); - } - - [Fact] - public async Task drops_when_disposed() { - var subscriptionDropped = new TaskCompletionSource(); - - using var subscription = await Fixture.Streams - .SubscribeToAllAsync( - FromAll.Start, - (sub, re, ct) => Task.CompletedTask, - false, - (sub, reason, ex) => subscriptionDropped.SetResult(new(reason, ex)) - ) - .WithTimeout(); - - // if the subscription dropped before time, raise the reason why - if (subscriptionDropped.Task.IsCompleted) - subscriptionDropped.Task.IsCompleted.ShouldBe(false, subscriptionDropped.Task.Result.ToString()); - - // stop the subscription - subscription.Dispose(); - var result = await subscriptionDropped.Task.WithTimeout(); - result.ShouldBe(SubscriptionDroppedResult.Disposed()); - } - - [Fact] - public async Task drops_when_subscriber_error() { - var expectedResult = SubscriptionDroppedResult.SubscriberError(); - - var subscriptionDropped = new TaskCompletionSource(); - - using var subscription = await Fixture.Streams - .SubscribeToAllAsync( - FromAll.Start, - (sub, re, ct) => expectedResult.Throw(), - false, - (sub, reason, ex) => subscriptionDropped.SetResult(new(reason, ex)) - ) - .WithTimeout(); - - await Fixture.Streams.AppendToStreamAsync(Fixture.GetStreamName(), StreamState.NoStream, Fixture.CreateTestEvents()); - - var result = await subscriptionDropped.Task.WithTimeout(); - result.ShouldBe(expectedResult); - } -} diff --git a/test/EventStore.Client.Streams.Tests/Subscriptions/Obsolete/subscribe_to_stream_obsolete.cs b/test/EventStore.Client.Streams.Tests/Subscriptions/Obsolete/subscribe_to_stream_obsolete.cs deleted file mode 100644 index 4e05b293b..000000000 --- a/test/EventStore.Client.Streams.Tests/Subscriptions/Obsolete/subscribe_to_stream_obsolete.cs +++ /dev/null @@ -1,303 +0,0 @@ -namespace EventStore.Client.Streams.Tests.Subscriptions.Obsolete; - -[Trait("Category", "Subscriptions")] -[Trait("Category", "Target:Stream")] -[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class subscribe_to_stream_obsolete(ITestOutputHelper output, SubscriptionsFixture fixture) : EventStoreTests(output, fixture) { - [Fact] - public async Task receives_all_events_from_start() { - var streamName = Fixture.GetStreamName(); - - var receivedAllEvents = new TaskCompletionSource(); - var subscriptionDropped = new TaskCompletionSource(); - - var seedEvents = Fixture.CreateTestEvents(10).ToArray(); - var pageSize = seedEvents.Length / 2; - - var availableEvents = new HashSet(seedEvents.Select(x => x.EventId)); - - await Fixture.Streams.AppendToStreamAsync(streamName, StreamState.NoStream, seedEvents.Take(pageSize)); - - using var subscription = await Fixture.Streams - .SubscribeToStreamAsync(streamName, FromStream.Start, OnReceived, false, OnDropped) - .WithTimeout(); - - await Fixture.Streams.AppendToStreamAsync(streamName, StreamState.StreamExists, seedEvents.Skip(pageSize)); - - await receivedAllEvents.Task.WithTimeout(); - - // if the subscription dropped before time, raise the reason why - if (subscriptionDropped.Task.IsCompleted) - subscriptionDropped.Task.IsCompleted.ShouldBe(false, subscriptionDropped.Task.Result.ToString()); - - // stop the subscription - subscription.Dispose(); - var result = await subscriptionDropped.Task.WithTimeout(); - result.ShouldBe(SubscriptionDroppedResult.Disposed()); - - return; - - Task OnReceived(StreamSubscription sub, ResolvedEvent re, CancellationToken ct) { - availableEvents.RemoveWhere(x => x == re.OriginalEvent.EventId); - - if (availableEvents.Count == 0) { - receivedAllEvents.TrySetResult(true); - Fixture.Log.Information("Received all {TotalEventsCount} expected events", seedEvents.Length); - } - - return Task.CompletedTask; - } - - void OnDropped(StreamSubscription sub, SubscriptionDroppedReason reason, Exception? ex) => - subscriptionDropped.SetResult(new(reason, ex)); - } - - [Fact] - public async Task receives_all_events_from_position() { - var streamName = Fixture.GetStreamName(); - - var receivedAllEvents = new TaskCompletionSource(); - var subscriptionDropped = new TaskCompletionSource(); - - var seedEvents = Fixture.CreateTestEvents(10).ToArray(); - var pageSize = seedEvents.Length / 2; - - // only the second half of the events will be received - var availableEvents = new HashSet(seedEvents.Skip(pageSize).Select(x => x.EventId)); - - var writeResult = await Fixture.Streams.AppendToStreamAsync(streamName, StreamState.NoStream, seedEvents.Take(pageSize)); - var streamPosition = StreamPosition.FromStreamRevision(writeResult.NextExpectedStreamRevision); - var checkpoint = FromStream.After(streamPosition); - - using var subscription = await Fixture.Streams - .SubscribeToStreamAsync(streamName, checkpoint, OnReceived, false, OnDropped) - .WithTimeout(); - - await Fixture.Streams.AppendToStreamAsync(streamName, writeResult.NextExpectedStreamRevision, seedEvents.Skip(pageSize)); - - await receivedAllEvents.Task.WithTimeout(); - - // if the subscription dropped before time, raise the reason why - if (subscriptionDropped.Task.IsCompleted) - subscriptionDropped.Task.IsCompleted.ShouldBe(false, subscriptionDropped.Task.Result.ToString()); - - // stop the subscription - subscription.Dispose(); - var result = await subscriptionDropped.Task.WithTimeout(); - result.ShouldBe(SubscriptionDroppedResult.Disposed()); - - return; - - Task OnReceived(StreamSubscription sub, ResolvedEvent re, CancellationToken ct) { - availableEvents.RemoveWhere(x => x == re.OriginalEvent.EventId); - - if (availableEvents.Count == 0) { - receivedAllEvents.TrySetResult(true); - Fixture.Log.Information("Received all {TotalEventsCount} expected events", pageSize); - } - - return Task.CompletedTask; - } - - void OnDropped(StreamSubscription sub, SubscriptionDroppedReason reason, Exception? ex) => - subscriptionDropped.SetResult(new(reason, ex)); - } - - [Fact] - public async Task receives_all_events_from_non_existing_stream() { - var streamName = Fixture.GetStreamName(); - - var receivedAllEvents = new TaskCompletionSource(); - var subscriptionDropped = new TaskCompletionSource(); - - var seedEvents = Fixture.CreateTestEvents(10).ToArray(); - - var availableEvents = new HashSet(seedEvents.Select(x => x.EventId)); - - using var subscription = await Fixture.Streams - .SubscribeToStreamAsync(streamName, FromStream.Start, OnReceived, false, OnDropped) - .WithTimeout(); - - await Fixture.Streams.AppendToStreamAsync(streamName, StreamState.NoStream, seedEvents); - - await receivedAllEvents.Task.WithTimeout(); - - // if the subscription dropped before time, raise the reason why - if (subscriptionDropped.Task.IsCompleted) - subscriptionDropped.Task.IsCompleted.ShouldBe(false, subscriptionDropped.Task.Result.ToString()); - - // stop the subscription - subscription.Dispose(); - var result = await subscriptionDropped.Task.WithTimeout(); - result.ShouldBe(SubscriptionDroppedResult.Disposed()); - - return; - - Task OnReceived(StreamSubscription sub, ResolvedEvent re, CancellationToken ct) { - availableEvents.RemoveWhere(x => x == re.OriginalEvent.EventId); - - if (availableEvents.Count == 0) { - receivedAllEvents.TrySetResult(true); - Fixture.Log.Information("Received all {TotalEventsCount} expected events", seedEvents.Length); - } - - return Task.CompletedTask; - } - - void OnDropped(StreamSubscription sub, SubscriptionDroppedReason reason, Exception? ex) => - subscriptionDropped.SetResult(new(reason, ex)); - } - - [Fact] - public async Task allow_multiple_subscriptions_to_same_stream() { - var streamName = Fixture.GetStreamName(); - - var receivedAllEvents = new TaskCompletionSource(); - - var seedEvents = Fixture.CreateTestEvents(5).ToArray(); - - var targetEventsCount = seedEvents.Length * 2; - - await Fixture.Streams.AppendToStreamAsync(streamName, StreamState.NoStream, seedEvents); - - using var subscription1 = await Fixture.Streams - .SubscribeToStreamAsync(streamName, FromStream.Start, OnReceived) - .WithTimeout(); - - using var subscription2 = await Fixture.Streams - .SubscribeToStreamAsync(streamName, FromStream.Start, OnReceived) - .WithTimeout(); - - await receivedAllEvents.Task.WithTimeout(); - - Task OnReceived(StreamSubscription sub, ResolvedEvent re, CancellationToken ct) { - if (--targetEventsCount == 0) { - receivedAllEvents.TrySetResult(true); - Fixture.Log.Information("Received all {TotalEventsCount} expected events", seedEvents.Length); - } - - return Task.CompletedTask; - } - } - - [Fact] - public async Task drops_when_disposed() { - var streamName = Fixture.GetStreamName(); - - var subscriptionDropped = new TaskCompletionSource(); - - using var subscription = await Fixture.Streams - .SubscribeToStreamAsync( - streamName, - FromStream.Start, - (sub, re, ct) => Task.CompletedTask, - false, - (sub, reason, ex) => subscriptionDropped.SetResult(new(reason, ex)) - ) - .WithTimeout(); - - // if the subscription dropped before time, raise the reason why - if (subscriptionDropped.Task.IsCompleted) - subscriptionDropped.Task.IsCompleted.ShouldBe(false, subscriptionDropped.Task.Result.ToString()); - - // stop the subscription - subscription.Dispose(); - var result = await subscriptionDropped.Task.WithTimeout(); - result.ShouldBe(SubscriptionDroppedResult.Disposed()); - } - - [Fact] - public async Task drops_when_subscriber_error() { - var streamName = Fixture.GetStreamName(); - - var expectedResult = SubscriptionDroppedResult.SubscriberError(); - - var subscriptionDropped = new TaskCompletionSource(); - - using var subscription = await Fixture.Streams - .SubscribeToStreamAsync( - streamName, - FromStream.Start, - (sub, re, ct) => expectedResult.Throw(), - false, - (sub, reason, ex) => subscriptionDropped.SetResult(new(reason, ex)) - ) - .WithTimeout(); - - await Fixture.Streams.AppendToStreamAsync(streamName, StreamState.NoStream, Fixture.CreateTestEvents()); - - var result = await subscriptionDropped.Task.WithTimeout(); - result.ShouldBe(expectedResult); - } - - [Fact] - public async Task drops_when_stream_tombstoned() { - var streamName = Fixture.GetStreamName(); - - var subscriptionDropped = new TaskCompletionSource(); - - using var subscription = await Fixture.Streams - .SubscribeToStreamAsync( - streamName, - FromStream.Start, - (sub, re, ct) => Task.CompletedTask, - false, - (sub, reason, ex) => { subscriptionDropped.SetResult(new(reason, ex)); } - ) - .WithTimeout(); - - // rest in peace - await Fixture.Streams.TombstoneAsync(streamName, StreamState.NoStream); - - var result = await subscriptionDropped.Task.WithTimeout(); - result.Error.ShouldBeOfType().Stream.ShouldBe(streamName); - } - - [Fact] - public async Task receives_all_events_with_resolved_links() { - var streamName = Fixture.GetStreamName(); - - var receivedAllEvents = new TaskCompletionSource(); - var subscriptionDropped = new TaskCompletionSource(); - - var seedEvents = Fixture.CreateTestEvents(3).ToArray(); - var availableEvents = new HashSet(seedEvents.Select(x => x.EventId)); - - await Fixture.Streams.AppendToStreamAsync(streamName, StreamState.NoStream, seedEvents); - - using var subscription = await Fixture.Streams - .SubscribeToStreamAsync($"$et-{EventStoreFixture.TestEventType}", FromStream.Start, OnReceived, true, OnDropped) - .WithTimeout(); - - await receivedAllEvents.Task.WithTimeout(); - - // if the subscription dropped before time, raise the reason why - if (subscriptionDropped.Task.IsCompleted) - subscriptionDropped.Task.IsCompleted.ShouldBe(false, subscriptionDropped.Task.Result.ToString()); - - // stop the subscription - subscription.Dispose(); - var result = await subscriptionDropped.Task.WithTimeout(); - result.ShouldBe(SubscriptionDroppedResult.Disposed()); - - return; - - Task OnReceived(StreamSubscription sub, ResolvedEvent re, CancellationToken ct) { - var hasResolvedLink = re.OriginalEvent.EventStreamId.StartsWith($"$et-{EventStoreFixture.TestEventType}"); - if (availableEvents.RemoveWhere(x => x == re.Event.EventId && hasResolvedLink) == 0) { - Fixture.Log.Debug("Received unexpected event {EventId} from stream {StreamId}", re.Event.EventId, re.OriginalEvent.EventStreamId); - return Task.CompletedTask; - } - - if (availableEvents.Count == 0) { - receivedAllEvents.TrySetResult(true); - Fixture.Log.Information("Received all {TotalEventsCount} expected events", seedEvents.Length); - } - - return Task.CompletedTask; - } - - void OnDropped(StreamSubscription sub, SubscriptionDroppedReason reason, Exception? ex) => - subscriptionDropped.SetResult(new(reason, ex)); - } -} diff --git a/test/EventStore.Client.Streams.Tests/Subscriptions/SubscriptionDroppedResult.cs b/test/EventStore.Client.Streams.Tests/Subscriptions/SubscriptionDroppedResult.cs deleted file mode 100644 index 40df3eb52..000000000 --- a/test/EventStore.Client.Streams.Tests/Subscriptions/SubscriptionDroppedResult.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace EventStore.Client.Streams.Tests.Subscriptions; - -public record SubscriptionDroppedResult(SubscriptionDroppedReason Reason, Exception? Error) { - public Task Throw() => Task.FromException(Error!); - - public static SubscriptionDroppedResult ServerError(Exception? error = null) => - new(SubscriptionDroppedReason.ServerError, error ?? new Exception("Server error")); - - public static SubscriptionDroppedResult SubscriberError(Exception? error = null) => - new(SubscriptionDroppedReason.SubscriberError, error ?? new Exception("Subscriber error")); - - public static SubscriptionDroppedResult Disposed(Exception? error = null) => - new(SubscriptionDroppedReason.Disposed, error); - - public override string ToString() => $"{Reason} {Error?.Message ?? string.Empty}".Trim(); -} \ No newline at end of file diff --git a/test/EventStore.Client.Streams.Tests/Subscriptions/SubscriptionsFixture.cs b/test/EventStore.Client.Streams.Tests/Subscriptions/SubscriptionsFixture.cs deleted file mode 100644 index 582344f98..000000000 --- a/test/EventStore.Client.Streams.Tests/Subscriptions/SubscriptionsFixture.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace EventStore.Client.Streams.Tests.Subscriptions; - - -[Trait("Category", "Subscriptions")] -public class SubscriptionsFixture : EventStoreFixture { - public SubscriptionsFixture(): base(x => x.RunProjections()) { - OnSetup = async () => { - await Streams.SetStreamMetadataAsync( - SystemStreams.AllStream, - StreamState.NoStream, - new(acl: new(SystemRoles.All)), - userCredentials: TestCredentials.Root - ); - - await Streams.AppendToStreamAsync($"SubscriptionsFixture-Noise-{Guid.NewGuid():N}", StreamState.NoStream, CreateTestEvents(10)); - }; - } -} \ No newline at end of file diff --git a/test/EventStore.Client.Streams.Tests/Subscriptions/reconnection.cs b/test/EventStore.Client.Streams.Tests/Subscriptions/reconnection.cs deleted file mode 100644 index 2dc9912e5..000000000 --- a/test/EventStore.Client.Streams.Tests/Subscriptions/reconnection.cs +++ /dev/null @@ -1,178 +0,0 @@ -using Grpc.Core; -using static System.TimeSpan; - -namespace EventStore.Client.Streams.Tests.Subscriptions; - -[Trait("Category", "Subscriptions")] -[Obsolete] -public class @reconnection(ITestOutputHelper output, ReconnectionFixture fixture) : EventStoreTests(output, fixture) { - [Theory] - [InlineData(4, 5000, 0, 30000)] - public async Task when_the_connection_is_lost(int expectedNumberOfEvents, int reconnectDelayMs, int serviceRestartDelayMs, int testTimeoutMs) { - using var cancellator = new CancellationTokenSource().With(x => x.CancelAfter(testTimeoutMs)); - - var streamName = Fixture.GetStreamName(); - - // create backpressure by producing half of the events - await Fixture.ProduceEvents(streamName, expectedNumberOfEvents / 2, cancellationToken: cancellator.Token); - - // create subscription that will actually receive the first event and - // then wait for the service to be restarted - // but we are evil and will force the drop of the subscription muah ah ah - var consumeEvents = Fixture.ConsumeEvents( - streamName, - expectedNumberOfEvents, - FromMilliseconds(reconnectDelayMs), - cancellator.Token - ); - - // create chaos by pausing the service - await Fixture.RestartService(FromMilliseconds(serviceRestartDelayMs)); - - // produce the rest of the events to make it more interesting - await Fixture.ProduceEvents(streamName, expectedNumberOfEvents / 2, cancellationToken: cancellator.Token); - - // wait for the subscription to receive all events or timeout - await consumeEvents.ShouldNotThrowAsync(); - } -} - -public class ReconnectionFixture() - : EventStoreFixture( - x => x.RunInMemory(false) - .With(o => o.ClientSettings.ConnectivitySettings.DiscoveryInterval = FromMilliseconds(100)) - .With(o => o.ClientSettings.ConnectivitySettings.GossipTimeout = FromMilliseconds(100)) - ) -{ - public async Task ProduceEvents(string streamName, int numberOfEvents, StreamState? streamState = null, CancellationToken cancellationToken = default) { - while (!cancellationToken.IsCancellationRequested) { - try { - var result = await Streams.AppendToStreamAsync( - streamName, - streamState.GetValueOrDefault(StreamState.Any), - CreateTestEvents(numberOfEvents), - cancellationToken: cancellationToken - ); - - if (result is SuccessResult success) { - Log.Information( - "{NumberOfEvents} events produced to {StreamName}.", numberOfEvents, streamName - ); - - return; - } - - Log.Error( - "Failed to produce {NumberOfEvents} events to {StreamName}.", numberOfEvents, streamName - ); - - await Task.Delay(250); - } - catch (Exception ex) when ( ex is not OperationCanceledException) { - Log.Error( - ex, "Failed to produce {NumberOfEvents} events to {StreamName}.", numberOfEvents, streamName - ); - - await Task.Delay(250); - } - } - } - - public Task ConsumeEvents( - string streamName, - int expectedNumberOfEvents, - TimeSpan reconnectDelay, - CancellationToken cancellationToken - ) { - var receivedAllEvents = new TaskCompletionSource(); - - var receivedEventsCount = 0; - - _ = SubscribeToStream( - streamName, - checkpoint: null, - OnReceive(), - OnDrop(), - cancellationToken - ); - - return receivedAllEvents.Task; - - Func OnReceive() { - return re => { - receivedEventsCount++; - Log.Debug("{ReceivedEventsCount}/{ExpectedNumberOfEvents} events received.", receivedEventsCount, expectedNumberOfEvents); - - if (receivedEventsCount == expectedNumberOfEvents) { - Log.Information("Test complete. {ReceivedEventsCount}/{ExpectedNumberOfEvents} events received.", receivedEventsCount, expectedNumberOfEvents); - receivedAllEvents.TrySetResult(); - } - - return Task.CompletedTask; - }; - } - - Func> OnDrop() { - return async (reason, ex) => { - if (ex is RpcException { StatusCode: StatusCode.Unavailable or StatusCode.DeadlineExceeded }) { - Log.Warning("Transitive exception detected. Retrying connection in {reconnectDelayMs}ms.", reconnectDelay.TotalMilliseconds); - await Task.Delay(reconnectDelay); - return true; - } - - if (reason == SubscriptionDroppedReason.Disposed || ex is OperationCanceledException || ex is TaskCanceledException || ex is null) { - if (receivedEventsCount != expectedNumberOfEvents) - receivedAllEvents.TrySetException(new TimeoutException($"Test timeout detected. {receivedEventsCount}/{expectedNumberOfEvents} events received.", ex)); - else { - Log.Information("Test cancellation requested. {ReceivedEventsCount}/{ExpectedNumberOfEvents} events received.", receivedEventsCount, expectedNumberOfEvents); - receivedAllEvents.TrySetCanceled(cancellationToken); - } - - return false; - } - - Log.Fatal(ex, "Fatal exception detected. This is the end..."); - receivedAllEvents.SetException(ex); - - return false; - }; - } - } - - [Obsolete] - async Task SubscribeToStream( - string stream, - StreamPosition? checkpoint, - Func onReceive, - Func> onDrop, - CancellationToken cancellationToken - ) { - var start = checkpoint == null ? FromStream.Start : FromStream.After(checkpoint.Value); - - Log.Verbose("Attempting to start from checkpoint: {Checkpoint}.", checkpoint); - - try { - var sub = await Streams.SubscribeToStreamAsync( - streamName: stream, - start: start, - eventAppeared: async (s, re, ct) => { - await onReceive(re); - checkpoint = re.OriginalEventNumber; - Log.Verbose("Checkpoint Set: {Checkpoint}.", checkpoint); - }, - subscriptionDropped: async (s, reason, ex) => { - var resubscribe = await onDrop(reason, ex); - if (resubscribe) _ = SubscribeToStream(stream, checkpoint, onReceive, onDrop, cancellationToken); - }, - cancellationToken: cancellationToken - ); - } catch (Exception ex) { - var reason = ex is OperationCanceledException or TaskCanceledException - ? SubscriptionDroppedReason.Disposed - : SubscriptionDroppedReason.SubscriberError; - - var resubscribe = await onDrop(reason, ex); - if (resubscribe) _ = SubscribeToStream(stream, checkpoint, onReceive, onDrop, cancellationToken); - } - } -} diff --git a/test/EventStore.Client.Streams.Tests/Subscriptions/subscribe_to_all.cs b/test/EventStore.Client.Streams.Tests/Subscriptions/subscribe_to_all.cs deleted file mode 100644 index 2e9879a4a..000000000 --- a/test/EventStore.Client.Streams.Tests/Subscriptions/subscribe_to_all.cs +++ /dev/null @@ -1,480 +0,0 @@ -namespace EventStore.Client.Streams.Tests.Subscriptions; - -[Trait("Category", "Subscriptions")] -[Trait("Category", "Target:All")] -public class subscribe_to_all(ITestOutputHelper output, SubscriptionsFixture fixture) - : EventStoreTests(output, fixture) { - [Fact] - public async Task receives_all_events_from_start() { - var seedEvents = Fixture.CreateTestEvents(10).ToArray(); - var pageSize = seedEvents.Length / 2; - - var availableEvents = new HashSet(seedEvents.Select(x => x.EventId)); - - foreach (var evt in seedEvents.Take(pageSize)) - await Fixture.Streams.AppendToStreamAsync( - $"stream-{evt.EventId.ToGuid():N}", - StreamState.NoStream, - new[] { evt } - ); - - await using var subscription = Fixture.Streams.SubscribeToAll(FromAll.Start); - await using var enumerator = subscription.Messages.GetAsyncEnumerator(); - - Assert.True(await enumerator.MoveNextAsync()); - - Assert.IsType(enumerator.Current); - - foreach (var evt in seedEvents.Skip(pageSize)) - await Fixture.Streams.AppendToStreamAsync( - $"stream-{evt.EventId.ToGuid():N}", - StreamState.NoStream, - new[] { evt } - ); - - await Subscribe().WithTimeout(); - - return; - - async Task Subscribe() { - while (await enumerator.MoveNextAsync()) { - if (enumerator.Current is not StreamMessage.Event(var resolvedEvent)) { - continue; - } - - availableEvents.Remove(resolvedEvent.Event.EventId); - - if (availableEvents.Count == 0) { - return; - } - } - } - } - - [Fact] - public async Task receives_all_events_from_end() { - var seedEvents = Fixture.CreateTestEvents(10).ToArray(); - - var availableEvents = new HashSet(seedEvents.Select(x => x.EventId)); - - await using var subscription = Fixture.Streams.SubscribeToAll(FromAll.End); - await using var enumerator = subscription.Messages.GetAsyncEnumerator(); - - Assert.True(await enumerator.MoveNextAsync()); - - Assert.IsType(enumerator.Current); - - // add the events we want to receive after we start the subscription - foreach (var evt in seedEvents) - await Fixture.Streams.AppendToStreamAsync( - $"stream-{evt.EventId.ToGuid():N}", - StreamState.NoStream, - new[] { evt } - ); - - await Subscribe().WithTimeout(); - - return; - - async Task Subscribe() { - while (await enumerator.MoveNextAsync()) { - if (enumerator.Current is not StreamMessage.Event(var resolvedEvent)) { - continue; - } - - availableEvents.Remove(resolvedEvent.OriginalEvent.EventId); - - if (availableEvents.Count == 0) { - return; - } - } - } - } - - [Fact] - public async Task receives_all_events_from_position() { - var seedEvents = Fixture.CreateTestEvents(10).ToArray(); - var pageSize = seedEvents.Length / 2; - - // only the second half of the events will be received - var availableEvents = new HashSet(seedEvents.Skip(pageSize).Select(x => x.EventId)); - - IWriteResult writeResult = new SuccessResult(); - foreach (var evt in seedEvents.Take(pageSize)) - writeResult = await Fixture.Streams.AppendToStreamAsync( - $"stream-{evt.EventId.ToGuid():N}", - StreamState.NoStream, - new[] { evt } - ); - - var position = FromAll.After(writeResult.LogPosition); - - await using var subscription = Fixture.Streams.SubscribeToAll(position); - await using var enumerator = subscription.Messages.GetAsyncEnumerator(); - - Assert.True(await enumerator.MoveNextAsync()); - - Assert.IsType(enumerator.Current); - - foreach (var evt in seedEvents.Skip(pageSize)) - await Fixture.Streams.AppendToStreamAsync( - $"stream-{evt.EventId.ToGuid():N}", - StreamState.NoStream, - new[] { evt } - ); - - await Subscribe().WithTimeout(); - - return; - - async Task Subscribe() { - while (await enumerator.MoveNextAsync()) { - if (enumerator.Current is not StreamMessage.Event(var resolvedEvent)) { - continue; - } - - availableEvents.Remove(resolvedEvent.Event.EventId); - - if (availableEvents.Count == 0) { - return; - } - } - } - } - - [Fact] - public async Task receives_all_events_with_resolved_links() { - var streamName = Fixture.GetStreamName(); - - var seedEvents = Fixture.CreateTestEvents(3).ToArray(); - var availableEvents = new HashSet(seedEvents.Select(x => x.EventId)); - - await Fixture.Streams.AppendToStreamAsync(streamName, StreamState.NoStream, seedEvents); - - await using var subscription = Fixture.Streams.SubscribeToAll(FromAll.Start, true); - await using var enumerator = subscription.Messages.GetAsyncEnumerator(); - - Assert.True(await enumerator.MoveNextAsync()); - - Assert.IsType(enumerator.Current); - - await Subscribe().WithTimeout(); - - return; - - async Task Subscribe() { - while (await enumerator.MoveNextAsync()) { - if (enumerator.Current is not StreamMessage.Event(var resolvedEvent)) { - continue; - } - - availableEvents.Remove(resolvedEvent.Event.EventId); - - if (availableEvents.Count == 0) { - return; - } - } - } - } - - [Theory] - [MemberData(nameof(SubscriptionFilter.TestCases), MemberType = typeof(SubscriptionFilter))] - public async Task receives_all_filtered_events_from_start(SubscriptionFilter filter) { - var streamPrefix = $"{nameof(receives_all_filtered_events_from_start)}-{filter.Name}-{Guid.NewGuid():N}"; - - Fixture.Log.Information("Using filter {FilterName} with prefix {StreamPrefix}", filter.Name, streamPrefix); - - var seedEvents = Fixture.CreateTestEvents(64) - .Select(evt => filter.PrepareEvent(streamPrefix, evt)) - .ToArray(); - - var pageSize = seedEvents.Length / 2; - - var availableEvents = new HashSet(seedEvents.Select(x => x.EventId)); - - // add noise - await Fixture.Streams.AppendToStreamAsync( - Fixture.GetStreamName(), - StreamState.NoStream, - Fixture.CreateTestEvents(3) - ); - - var existingEventsCount = await Fixture.Streams.ReadAllAsync(Direction.Forwards, Position.Start) - .Messages.CountAsync(); - - Fixture.Log.Debug("Existing events count: {ExistingEventsCount}", existingEventsCount); - - // Debugging: - // await foreach (var evt in Fixture.Streams.ReadAllAsync(Direction.Forwards, Position.Start)) - // Fixture.Log.Debug("Read event {EventId} from {StreamId}.", evt.OriginalEvent.EventId, evt.OriginalEvent.EventStreamId); - - // add some of the events we want to see before we start the subscription - foreach (var evt in seedEvents.Take(pageSize)) - await Fixture.Streams.AppendToStreamAsync( - $"{streamPrefix}-{evt.EventId.ToGuid():N}", - StreamState.NoStream, - new[] { evt } - ); - - var filterOptions = new SubscriptionFilterOptions(filter.Create(streamPrefix), 1); - - await using var subscription = Fixture.Streams.SubscribeToAll(FromAll.Start, filterOptions: filterOptions); - await using var enumerator = subscription.Messages.GetAsyncEnumerator(); - - Assert.True(await enumerator.MoveNextAsync()); - - Assert.IsType(enumerator.Current); - - // add some of the events we want to see after we start the subscription - foreach (var evt in seedEvents.Skip(pageSize)) - await Fixture.Streams.AppendToStreamAsync( - $"{streamPrefix}-{evt.EventId.ToGuid():N}", - StreamState.NoStream, - new[] { evt } - ); - - bool checkpointReached = false; - - await Subscribe().WithTimeout(); - - Assert.True(checkpointReached); - - return; - - async Task Subscribe() { - while (await enumerator.MoveNextAsync()) { - switch (enumerator.Current) { - case StreamMessage.AllStreamCheckpointReached: - checkpointReached = true; - - break; - - case StreamMessage.Event(var resolvedEvent): { - availableEvents.Remove(resolvedEvent.Event.EventId); - - if (availableEvents.Count == 0) { - return; - } - - break; - } - } - } - } - } - - [Theory] - [MemberData(nameof(SubscriptionFilter.TestCases), MemberType = typeof(SubscriptionFilter))] - public async Task receives_all_filtered_events_from_end(SubscriptionFilter filter) { - var streamPrefix = $"{nameof(receives_all_filtered_events_from_end)}-{filter.Name}-{Guid.NewGuid():N}"; - - Fixture.Log.Information("Using filter {FilterName} with prefix {StreamPrefix}", filter.Name, streamPrefix); - - var seedEvents = Fixture.CreateTestEvents(64) - .Select(evt => filter.PrepareEvent(streamPrefix, evt)) - .ToArray(); - - var pageSize = seedEvents.Length / 2; - - // only the second half of the events will be received - var availableEvents = new HashSet(seedEvents.Skip(pageSize).Select(x => x.EventId)); - - // add noise - await Fixture.Streams.AppendToStreamAsync( - Fixture.GetStreamName(), - StreamState.NoStream, - Fixture.CreateTestEvents(3) - ); - - var existingEventsCount = await Fixture.Streams.ReadAllAsync(Direction.Forwards, Position.Start) - .Messages.CountAsync(); - - Fixture.Log.Debug("Existing events count: {ExistingEventsCount}", existingEventsCount); - - // Debugging: - // await foreach (var evt in Fixture.Streams.ReadAllAsync(Direction.Forwards, Position.Start)) - // Fixture.Log.Debug("Read event {EventId} from {StreamId}.", evt.OriginalEvent.EventId, evt.OriginalEvent.EventStreamId); - - // add some of the events we want to see before we start the subscription - foreach (var evt in seedEvents.Take(pageSize)) - await Fixture.Streams.AppendToStreamAsync( - $"{streamPrefix}-{evt.EventId.ToGuid():N}", - StreamState.NoStream, - new[] { evt } - ); - - var filterOptions = new SubscriptionFilterOptions(filter.Create(streamPrefix), 1); - - await using var subscription = Fixture.Streams.SubscribeToAll(FromAll.End, filterOptions: filterOptions); - await using var enumerator = subscription.Messages.GetAsyncEnumerator(); - - Assert.True(await enumerator.MoveNextAsync()); - - Assert.IsType(enumerator.Current); - - // add some of the events we want to see after we start the subscription - foreach (var evt in seedEvents.Skip(pageSize)) - await Fixture.Streams.AppendToStreamAsync( - $"{streamPrefix}-{evt.EventId.ToGuid():N}", - StreamState.NoStream, - new[] { evt } - ); - - bool checkpointReached = false; - - await Subscribe().WithTimeout(); - - Assert.True(checkpointReached); - - return; - - async Task Subscribe() { - while (await enumerator.MoveNextAsync()) { - switch (enumerator.Current) { - case StreamMessage.AllStreamCheckpointReached: - checkpointReached = true; - - break; - - case StreamMessage.Event(var resolvedEvent): { - availableEvents.Remove(resolvedEvent.Event.EventId); - - if (availableEvents.Count == 0) { - return; - } - - break; - } - } - } - } - } - - [Theory] - [MemberData(nameof(SubscriptionFilter.TestCases), MemberType = typeof(SubscriptionFilter))] - public async Task receives_all_filtered_events_from_position(SubscriptionFilter filter) { - var streamPrefix = $"{nameof(receives_all_filtered_events_from_position)}-{filter.Name}-{Guid.NewGuid():N}"; - - Fixture.Log.Information("Using filter {FilterName} with prefix {StreamPrefix}", filter.Name, streamPrefix); - - var seedEvents = Fixture.CreateTestEvents(64) - .Select(evt => filter.PrepareEvent(streamPrefix, evt)) - .ToArray(); - - var pageSize = seedEvents.Length / 2; - - // only the second half of the events will be received - var availableEvents = new HashSet(seedEvents.Skip(pageSize).Select(x => x.EventId)); - - // add noise - await Fixture.Streams.AppendToStreamAsync( - Fixture.GetStreamName(), - StreamState.NoStream, - Fixture.CreateTestEvents(3) - ); - - var existingEventsCount = await Fixture.Streams.ReadAllAsync(Direction.Forwards, Position.Start) - .Messages.CountAsync(); - - Fixture.Log.Debug("Existing events count: {ExistingEventsCount}", existingEventsCount); - - // add some of the events that are a match to the filter but will not be received - IWriteResult writeResult = new SuccessResult(); - foreach (var evt in seedEvents.Take(pageSize)) - writeResult = await Fixture.Streams.AppendToStreamAsync( - $"{streamPrefix}-{evt.EventId.ToGuid():N}", - StreamState.NoStream, - new[] { evt } - ); - - var position = FromAll.After(writeResult.LogPosition); - - var filterOptions = new SubscriptionFilterOptions(filter.Create(streamPrefix), 1); - - await using var subscription = Fixture.Streams.SubscribeToAll(position, filterOptions: filterOptions); - await using var enumerator = subscription.Messages.GetAsyncEnumerator(); - - Assert.True(await enumerator.MoveNextAsync()); - - Assert.IsType(enumerator.Current); - - // add the events we want to receive after we start the subscription - foreach (var evt in seedEvents.Skip(pageSize)) - await Fixture.Streams.AppendToStreamAsync( - $"{streamPrefix}-{evt.EventId.ToGuid():N}", - StreamState.NoStream, - new[] { evt } - ); - - bool checkpointReached = false; - - await Subscribe().WithTimeout(); - - Assert.True(checkpointReached); - - return; - - async Task Subscribe() { - while (await enumerator.MoveNextAsync()) { - switch (enumerator.Current) { - case StreamMessage.AllStreamCheckpointReached: - checkpointReached = true; - - break; - - case StreamMessage.Event(var resolvedEvent): { - availableEvents.Remove(resolvedEvent.Event.EventId); - - if (availableEvents.Count == 0) { - return; - } - - break; - } - } - } - } - } - - [Fact] - public async Task receives_all_filtered_events_with_resolved_links() { - var streamName = Fixture.GetStreamName(); - - var seedEvents = Fixture.CreateTestEvents(3).ToArray(); - var availableEvents = new HashSet(seedEvents.Select(x => x.EventId)); - - await Fixture.Streams.AppendToStreamAsync(streamName, StreamState.NoStream, seedEvents); - - var filterOptions = new SubscriptionFilterOptions( - StreamFilter.Prefix($"$et-{EventStoreFixture.TestEventType}") - ); - - await using var subscription = - Fixture.Streams.SubscribeToAll(FromAll.Start, true, filterOptions: filterOptions); - - await using var enumerator = subscription.Messages.GetAsyncEnumerator(); - - Assert.True(await enumerator.MoveNextAsync()); - - Assert.IsType(enumerator.Current); - - await Subscribe().WithTimeout(); - - return; - - async Task Subscribe() { - while (await enumerator.MoveNextAsync()) { - if (enumerator.Current is not StreamMessage.Event(var resolvedEvent) || - !resolvedEvent.OriginalEvent.EventStreamId.StartsWith($"$et-{EventStoreFixture.TestEventType}")) { - continue; - } - - availableEvents.Remove(resolvedEvent.Event.EventId); - - if (availableEvents.Count == 0) { - return; - } - } - } - } -} diff --git a/test/EventStore.Client.ProjectionManagement.Tests/AssertEx.cs b/test/EventStore.Client.Tests.Common/AssertEx.cs similarity index 97% rename from test/EventStore.Client.ProjectionManagement.Tests/AssertEx.cs rename to test/EventStore.Client.Tests.Common/AssertEx.cs index f7d846294..db2386374 100644 --- a/test/EventStore.Client.ProjectionManagement.Tests/AssertEx.cs +++ b/test/EventStore.Client.Tests.Common/AssertEx.cs @@ -1,7 +1,7 @@ using System.Runtime.CompilerServices; using Xunit.Sdk; -namespace EventStore.Client.ProjectionManagement.Tests; +namespace EventStore.Client.Tests; public static class AssertEx { /// @@ -53,4 +53,4 @@ static async Task IsOrBecomesTrueImpl(Func> func, TimeSpan? tim return false; } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Tests.Common/EventStore.Client.Tests.Common.csproj b/test/EventStore.Client.Tests.Common/EventStore.Client.Tests.Common.csproj index 9826714b3..6ebee89cb 100644 --- a/test/EventStore.Client.Tests.Common/EventStore.Client.Tests.Common.csproj +++ b/test/EventStore.Client.Tests.Common/EventStore.Client.Tests.Common.csproj @@ -1,66 +1,68 @@ - - EventStore.Client.Tests - + + EventStore.Client.Tests + - - - + + + - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + - - - - - - - + + + + + + + - - - certs\%(RecursiveDir)/%(FileName)%(Extension) - Always - - + + + certs\%(RecursiveDir)/%(FileName)%(Extension) + Always + + - - - Always - - - Always - - - Always - - - PreserveNewest - - - Always - - - Always - - + + + Always + + + Always + + + Always + + + PreserveNewest + + + Always + + + Always + + - - - + + + diff --git a/test/EventStore.Client.Tests.Common/Extensions/ConfigurationExtensions.cs b/test/EventStore.Client.Tests.Common/Extensions/ConfigurationExtensions.cs index 3e975fc23..621758f36 100644 --- a/test/EventStore.Client.Tests.Common/Extensions/ConfigurationExtensions.cs +++ b/test/EventStore.Client.Tests.Common/Extensions/ConfigurationExtensions.cs @@ -9,4 +9,4 @@ public static void EnsureValue(this IConfiguration configuration, string key, st if (string.IsNullOrEmpty(value)) configuration[key] = defaultValue; } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Tests.Common/Extensions/EventStoreClientExtensions.cs b/test/EventStore.Client.Tests.Common/Extensions/EventStoreClientExtensions.cs index e1475bc16..cd7b808bd 100644 --- a/test/EventStore.Client.Tests.Common/Extensions/EventStoreClientExtensions.cs +++ b/test/EventStore.Client.Tests.Common/Extensions/EventStoreClientExtensions.cs @@ -21,4 +21,4 @@ public static Task CreateUserWithRetry( ), cancellationToken ); -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Tests.Common/Extensions/EventStoreClientWarmupExtensions.cs b/test/EventStore.Client.Tests.Common/Extensions/EventStoreClientWarmupExtensions.cs index dea2f4ef9..aff15195c 100644 --- a/test/EventStore.Client.Tests.Common/Extensions/EventStoreClientWarmupExtensions.cs +++ b/test/EventStore.Client.Tests.Common/Extensions/EventStoreClientWarmupExtensions.cs @@ -129,4 +129,4 @@ public static Task WarmUp(this EventStoreUserMan }, cancellationToken ); -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Tests.Common/Extensions/OperatingSystemExtensions.cs b/test/EventStore.Client.Tests.Common/Extensions/OperatingSystemExtensions.cs index 1889b084d..5899f625c 100644 --- a/test/EventStore.Client.Tests.Common/Extensions/OperatingSystemExtensions.cs +++ b/test/EventStore.Client.Tests.Common/Extensions/OperatingSystemExtensions.cs @@ -4,4 +4,4 @@ public static class OperatingSystemExtensions { public static bool IsWindows(this OperatingSystem operatingSystem) => operatingSystem.Platform != PlatformID.Unix && operatingSystem.Platform != PlatformID.MacOSX; -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Tests.Common/Extensions/ReadOnlyMemoryExtensions.cs b/test/EventStore.Client.Tests.Common/Extensions/ReadOnlyMemoryExtensions.cs index 49ffd1cd9..63b6694a3 100644 --- a/test/EventStore.Client.Tests.Common/Extensions/ReadOnlyMemoryExtensions.cs +++ b/test/EventStore.Client.Tests.Common/Extensions/ReadOnlyMemoryExtensions.cs @@ -25,4 +25,4 @@ public static StreamPosition ParseStreamPosition(this ReadOnlyMemory json) return StreamPosition.FromInt64(int.Parse(checkPoint)); } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Streams.Tests/Append/ShouldThrowAsyncExtensions.cs b/test/EventStore.Client.Tests.Common/Extensions/ShouldThrowAsyncExtensions.cs similarity index 90% rename from test/EventStore.Client.Streams.Tests/Append/ShouldThrowAsyncExtensions.cs rename to test/EventStore.Client.Tests.Common/Extensions/ShouldThrowAsyncExtensions.cs index 80f983ce0..edba53a25 100644 --- a/test/EventStore.Client.Streams.Tests/Append/ShouldThrowAsyncExtensions.cs +++ b/test/EventStore.Client.Tests.Common/Extensions/ShouldThrowAsyncExtensions.cs @@ -1,4 +1,4 @@ -namespace EventStore.Client.Streams.Tests.Append; +namespace EventStore.Client.Tests; public static class ShouldThrowAsyncExtensions { public static Task ShouldThrowAsync(this EventStoreClient.ReadStreamResult source) where TException : Exception => @@ -11,4 +11,4 @@ public static async Task ShouldThrowAsync(this EventStoreClient.Read var ex = await source.ShouldThrowAsync(); handler(ex); } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Tests/TypeExtensions.cs b/test/EventStore.Client.Tests.Common/Extensions/TypeExtensions.cs similarity index 98% rename from test/EventStore.Client.Tests/TypeExtensions.cs rename to test/EventStore.Client.Tests.Common/Extensions/TypeExtensions.cs index 268109da8..465b04f92 100644 --- a/test/EventStore.Client.Tests/TypeExtensions.cs +++ b/test/EventStore.Client.Tests.Common/Extensions/TypeExtensions.cs @@ -2,7 +2,7 @@ namespace EventStore.Client.Tests; -static class TypeExtensions { +public static class TypeExtensions { public static bool InvokeEqualityOperator(this Type type, object? left, object? right) => type.InvokeOperator("Equality", left, right); public static bool InvokeInequalityOperator(this Type type, object? left, object? right) => type.InvokeOperator("Inequality", left, right); @@ -36,4 +36,4 @@ static bool InvokeOperator(this Type type, string name, object? left, object? ri return (bool)op.Invoke(null, new[] { left, right })!; } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Tests.Common/Extensions/WithExtension.cs b/test/EventStore.Client.Tests.Common/Extensions/WithExtension.cs index b2ea3dbac..35f731de3 100644 --- a/test/EventStore.Client.Tests.Common/Extensions/WithExtension.cs +++ b/test/EventStore.Client.Tests.Common/Extensions/WithExtension.cs @@ -59,4 +59,4 @@ public static T With(this T instance, Func action, Func when) { return when() ? action(instance) : instance; } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Tests.Common/Facts/AnonymousAccess.cs b/test/EventStore.Client.Tests.Common/Facts/AnonymousAccess.cs index 575093085..0e8a3c248 100644 --- a/test/EventStore.Client.Tests.Common/Facts/AnonymousAccess.cs +++ b/test/EventStore.Client.Tests.Common/Facts/AnonymousAccess.cs @@ -8,4 +8,4 @@ public class AnonymousAccess { public class FactAttribute() : Deprecation.FactAttribute(LegacySince, SkipMessage); public class TheoryAttribute() : Deprecation.TheoryAttribute(LegacySince, SkipMessage); -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Tests.Common/Facts/Deprecation.cs b/test/EventStore.Client.Tests.Common/Facts/Deprecation.cs index 01adee242..98e9fe671 100644 --- a/test/EventStore.Client.Tests.Common/Facts/Deprecation.cs +++ b/test/EventStore.Client.Tests.Common/Facts/Deprecation.cs @@ -4,7 +4,7 @@ public class Deprecation { public class FactAttribute(Version since, string skipMessage) : Xunit.FactAttribute { public override string? Skip { - get => EventStoreTestServer.Version >= since ? skipMessage : null; + get => KurrentPermanentTestNode.Version >= since ? skipMessage : null; set => throw new NotSupportedException(); } } @@ -19,8 +19,8 @@ public TheoryAttribute(Version since, string skipMessage) { } public override string? Skip { - get => EventStoreTestServer.Version >= _legacySince ? _skipMessage : null; + get => KurrentPermanentTestNode.Version >= _legacySince ? _skipMessage : null; set => throw new NotSupportedException(); } } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Tests.Common/Facts/Regression.cs b/test/EventStore.Client.Tests.Common/Facts/Regression.cs index 3c6a92f0c..5abdfbff6 100644 --- a/test/EventStore.Client.Tests.Common/Facts/Regression.cs +++ b/test/EventStore.Client.Tests.Common/Facts/Regression.cs @@ -4,15 +4,15 @@ namespace EventStore.Client.Tests; public class Regression { public class FactAttribute(int major, string skipMessage) : Xunit.FactAttribute { public override string? Skip { - get => (EventStoreTestServer.Version?.Major ?? int.MaxValue) < major ? skipMessage : null; + get => (KurrentPermanentTestNode.Version?.Major ?? int.MaxValue) < major ? skipMessage : null; set => throw new NotSupportedException(); } } public class TheoryAttribute(int major, string skipMessage) : Xunit.TheoryAttribute { public override string? Skip { - get => (EventStoreTestServer.Version?.Major ?? int.MaxValue) < major ? skipMessage : null; + get => (KurrentPermanentTestNode.Version?.Major ?? int.MaxValue) < major ? skipMessage : null; set => throw new NotSupportedException(); } } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Tests.Common/Facts/SupportsPSToAllFact.cs b/test/EventStore.Client.Tests.Common/Facts/SupportsPSToAllFact.cs deleted file mode 100644 index 3f5d993fe..000000000 --- a/test/EventStore.Client.Tests.Common/Facts/SupportsPSToAllFact.cs +++ /dev/null @@ -1,18 +0,0 @@ -// ReSharper disable InconsistentNaming - -namespace EventStore.Client.Tests; - -[PublicAPI] -public class SupportsPSToAll { - const int SupportedFromMajorVersion = 21; - - static readonly string SkipMessage = $"Persistent Subscriptions to $all are not supported on" - + $" {EventStoreTestServer.Version?.ToString(3) ?? "unknown"}"; - - public static bool No => !Yes; - public static bool Yes => (EventStoreTestServer.Version?.Major ?? int.MaxValue) >= SupportedFromMajorVersion; - - public class FactAttribute() : Regression.FactAttribute(SupportedFromMajorVersion, SkipMessage); - - public class TheoryAttribute() : Regression.TheoryAttribute(SupportedFromMajorVersion, SkipMessage); -} \ No newline at end of file diff --git a/test/EventStore.Client.Tests.Common/Fakers/TestUserFaker.cs b/test/EventStore.Client.Tests.Common/Fakers/TestUserFaker.cs index 1aca4aac1..9cb3c3fbb 100644 --- a/test/EventStore.Client.Tests.Common/Fakers/TestUserFaker.cs +++ b/test/EventStore.Client.Tests.Common/Fakers/TestUserFaker.cs @@ -50,4 +50,4 @@ public TestUser WithNonAsciiPassword() => public static partial class Fakers { public static TestUserFaker Users => TestUserFaker.Instance; -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Tests.Common/Fixtures/Base/EventStoreClientFixtureBase.cs b/test/EventStore.Client.Tests.Common/Fixtures/Base/EventStoreClientFixtureBase.cs deleted file mode 100644 index 141a2a8da..000000000 --- a/test/EventStore.Client.Tests.Common/Fixtures/Base/EventStoreClientFixtureBase.cs +++ /dev/null @@ -1,147 +0,0 @@ -using System.Diagnostics; -using System.Net; -using System.Reactive.Linq; -using System.Reactive.Subjects; -using System.Runtime.CompilerServices; -using System.Text; -using Serilog; -using Serilog.Events; -using Serilog.Extensions.Logging; -using Serilog.Formatting.Display; - -namespace EventStore.Client; - -public abstract class EventStoreClientFixtureBase : IAsyncLifetime { - public const string TestEventType = "-"; - - const string ConnectionStringSingle = "esdb://admin:changeit@localhost:2113/?tls=true&tlsVerifyCert=false"; - const string ConnectionStringCluster = "esdb://admin:changeit@localhost:2113,localhost:2112,localhost:2111?tls=true&tlsVerifyCert=false"; - - static readonly Subject LogEventSubject = new(); - - readonly IList _disposables; - - static EventStoreClientFixtureBase() => ConfigureLogging(); - - protected EventStoreClientFixtureBase( - EventStoreClientSettings? clientSettings, - IDictionary? env = null, bool noDefaultCredentials = false - ) { - _disposables = new List(); - - ServicePointManager.ServerCertificateValidationCallback = delegate { return true; }; - - var connectionString = GlobalEnvironment.UseCluster ? ConnectionStringCluster : ConnectionStringSingle; - Settings = clientSettings ?? EventStoreClientSettings.Create(connectionString); - - if (noDefaultCredentials) - Settings.DefaultCredentials = null; - - Settings.DefaultDeadline = Debugger.IsAttached - ? new TimeSpan?() - : TimeSpan.FromSeconds(30); - - var hostCertificatePath = Path.Combine( - Environment.CurrentDirectory, - GlobalEnvironment.UseCluster ? "certs-cluster" : "certs" - ); - - Settings.LoggerFactory ??= new SerilogLoggerFactory(); - - Settings.ConnectivitySettings.MaxDiscoverAttempts = 20; - Settings.ConnectivitySettings.DiscoveryInterval = TimeSpan.FromSeconds(1); - - if (GlobalEnvironment.UseExternalServer) - TestServer = new EventStoreTestServerExternal(); - else - TestServer = GlobalEnvironment.UseCluster - ? new EventStoreTestServerCluster(hostCertificatePath, Settings.ConnectivitySettings.ResolvedAddressOrDefault, env) - : new EventStoreTestServer(hostCertificatePath, Settings.ConnectivitySettings.ResolvedAddressOrDefault, env); - } - - public IEventStoreTestServer TestServer { get; } - protected EventStoreClientSettings Settings { get; } - - public Faker Faker { get; } = new(); - - public virtual async Task InitializeAsync() { - await TestServer.StartAsync().WithTimeout(TimeSpan.FromMinutes(5)); - await OnServerUpAsync().WithTimeout(TimeSpan.FromMinutes(5)); - await Given().WithTimeout(TimeSpan.FromMinutes(5)); - await When().WithTimeout(TimeSpan.FromMinutes(5)); - } - - public virtual Task DisposeAsync() { - foreach (var disposable in _disposables) - disposable.Dispose(); - - return TestServer.DisposeAsync().AsTask().WithTimeout(TimeSpan.FromMinutes(5)); - } - - static void ConfigureLogging() { - var loggerConfiguration = new LoggerConfiguration() - .Enrich.FromLogContext() - .MinimumLevel.Is(LogEventLevel.Verbose) - .MinimumLevel.Override("Microsoft", LogEventLevel.Warning) - .MinimumLevel.Override("Grpc", LogEventLevel.Verbose) - .WriteTo.Observers(observable => observable.Subscribe(LogEventSubject.OnNext)) - .WriteTo.Seq("http://localhost:5341/", period: TimeSpan.FromMilliseconds(1)); - - Log.Logger = loggerConfiguration.CreateLogger(); - AppDomain.CurrentDomain.DomainUnload += (_, e) => Log.CloseAndFlush(); - } - - protected abstract Task OnServerUpAsync(); - protected abstract Task Given(); - protected abstract Task When(); - - public IEnumerable CreateTestEvents(int count = 1, string? type = null, int metadataSize = 1) => - Enumerable.Range(0, count).Select(index => CreateTestEvent(index, type ?? TestEventType, metadataSize)); - - protected static EventData CreateTestEvent(int index) => CreateTestEvent(index, TestEventType, 1); - - protected static EventData CreateTestEvent(int index, string type, int metadataSize) => - new( - Uuid.NewUuid(), - type, - Encoding.UTF8.GetBytes($@"{{""x"":{index}}}"), - Encoding.UTF8.GetBytes("\"" + new string('$', metadataSize) + "\"") - ); - - public string GetStreamName([CallerMemberName] string? testMethod = null) { - var type = GetType(); - - return $"{type.DeclaringType?.Name}.{testMethod ?? "unknown"}"; - } - - public void CaptureLogs(ITestOutputHelper testOutputHelper) { - const string captureCorrelationId = nameof(captureCorrelationId); - - var captureId = Guid.NewGuid(); - - var callContextData = new AsyncLocal<(string, Guid)> { - Value = (captureCorrelationId, captureId) - }; - - bool Filter(LogEvent logEvent) => callContextData.Value.Item2.Equals(captureId); - - var formatter = new MessageTemplateTextFormatter("{Timestamp:yyyy-MM-dd HH:mm:ss.fff} [{Level:u3}] [{SourceContext}] {Message}"); - - var formatterWithException = - new MessageTemplateTextFormatter("{Timestamp:yyyy-MM-dd HH:mm:ss.fff} [{Level:u3}] [{SourceContext}] {Message}{NewLine}{Exception}"); - - var subscription = LogEventSubject.Where(Filter).Subscribe( - logEvent => { - using var writer = new StringWriter(); - if (logEvent.Exception != null) - formatterWithException.Format(logEvent, writer); - else - formatter.Format(logEvent, writer); - - testOutputHelper.WriteLine(writer.ToString()); - } - ); - - _disposables.Add(subscription); - } -} diff --git a/test/EventStore.Client.Tests.Common/Fixtures/Base/EventStoreTestServer.cs b/test/EventStore.Client.Tests.Common/Fixtures/Base/EventStoreTestServer.cs deleted file mode 100644 index 20c8c0c93..000000000 --- a/test/EventStore.Client.Tests.Common/Fixtures/Base/EventStoreTestServer.cs +++ /dev/null @@ -1,143 +0,0 @@ -using System.Net; -using System.Net.Http; -using Ductus.FluentDocker.Builders; -using Ductus.FluentDocker.Extensions; -using Ductus.FluentDocker.Model.Builders; -using Ductus.FluentDocker.Services; -using Ductus.FluentDocker.Services.Extensions; -using Polly; - -namespace EventStore.Client.Tests; - -public class EventStoreTestServer : IEventStoreTestServer { - static readonly string ContainerName = "es-client-dotnet-test"; - - static Version? _version; - readonly IContainerService _eventStore; - readonly string _hostCertificatePath; - readonly HttpClient _httpClient; - - public EventStoreTestServer( - string hostCertificatePath, - Uri address, - IDictionary? envOverrides - ) { - _hostCertificatePath = hostCertificatePath; - VerifyCertificatesExist(); - -#if NET - _httpClient = new HttpClient(new SocketsHttpHandler { - SslOptions = {RemoteCertificateValidationCallback = delegate { return true; }} - }) { - BaseAddress = address, - }; -#else - _httpClient = new HttpClient(new WinHttpHandler { - ServerCertificateValidationCallback = delegate { return true; } - }) { - BaseAddress = address, - }; -#endif - - var env = new Dictionary { - ["EVENTSTORE_DB_LOG_FORMAT"] = "V2", - ["EVENTSTORE_MEM_DB"] = "true", - ["EVENTSTORE_CHUNK_SIZE"] = (1024 * 1024 * 1024).ToString(), - ["EVENTSTORE_CERTIFICATE_FILE"] = "/etc/eventstore/certs/node/node.crt", - ["EVENTSTORE_CERTIFICATE_PRIVATE_KEY_FILE"] = "/etc/eventstore/certs/node/node.key", - ["EVENTSTORE_TRUSTED_ROOT_CERTIFICATES_PATH"] = "/etc/eventstore/certs/ca", - ["EVENTSTORE_LOG_LEVEL"] = "Verbose", - ["EVENTSTORE_STREAM_EXISTENCE_FILTER_SIZE"] = "10000", - ["EVENTSTORE_STREAM_INFO_CACHE_CAPACITY"] = "10000", - ["EVENTSTORE_ENABLE_ATOM_PUB_OVER_HTTP"] = "false", - ["EVENTSTORE_DISABLE_LOG_FILE"] = "true" - }; - - foreach (var val in envOverrides ?? Enumerable.Empty>()) - env[val.Key] = val.Value; - - _eventStore = new Builder() - .UseContainer() - .UseImage(GlobalEnvironment.DockerImage) - .WithEnvironment(env.Select(pair => $"{pair.Key}={pair.Value}").ToArray()) - .WithName(ContainerName) - .MountVolume(_hostCertificatePath, "/etc/eventstore/certs", MountType.ReadOnly) - .ExposePort(2113, 2113) - //.WaitForHealthy(TimeSpan.FromSeconds(120)) - //.KeepContainer() - //.KeepRunning() - .Build(); - } - - public static Version Version => _version ??= GetVersion(); - - public async Task StartAsync(CancellationToken cancellationToken = default) { - _eventStore.Start(); - try { - await Policy.Handle() - .WaitAndRetryAsync(200, retryCount => TimeSpan.FromMilliseconds(100)) - .ExecuteAsync( - async () => { - using var response = await _httpClient.GetAsync("/health/live", cancellationToken); - if (response.StatusCode >= HttpStatusCode.BadRequest) - throw new($"Health check failed with status code: {response.StatusCode}."); - } - ); - } - catch (Exception) { - _eventStore.Dispose(); - throw; - } - } - - public void Stop() => _eventStore.Stop(); - - public ValueTask DisposeAsync() { - _httpClient?.Dispose(); - _eventStore?.Dispose(); - - return new ValueTask(Task.CompletedTask); - } - - static Version GetVersion() { - const string versionPrefix = "EventStoreDB version"; - - using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30)); - using var eventstore = new Builder().UseContainer() - .UseImage(GlobalEnvironment.DockerImage) - .Command("--version") - .Build() - .Start(); - - using var log = eventstore.Logs(true, cts.Token); - foreach (var line in log.ReadToEnd()) { - if (line.StartsWith(versionPrefix) && - Version.TryParse(new string(ReadVersion(line[(versionPrefix.Length + 1)..]).ToArray()), out var version)) { - return version; - } - } - - throw new InvalidOperationException("Could not determine server version."); - - IEnumerable ReadVersion(string s) { - foreach (var c in s.TakeWhile(c => c == '.' || char.IsDigit(c))) { - yield return c; - } - } - } - - void VerifyCertificatesExist() { - var certificateFiles = new[] { - Path.Combine("ca", "ca.crt"), - Path.Combine("ca", "ca.key"), - Path.Combine("node", "node.crt"), - Path.Combine("node", "node.key") - }.Select(path => Path.Combine(_hostCertificatePath, path)); - - foreach (var file in certificateFiles) - if (!File.Exists(file)) - throw new InvalidOperationException( - $"Could not locate the certificates file {file} needed to run EventStoreDB. Please run the 'gencert' tool at the root of the repository." - ); - } -} diff --git a/test/EventStore.Client.Tests.Common/Fixtures/Base/EventStoreTestServerCluster.cs b/test/EventStore.Client.Tests.Common/Fixtures/Base/EventStoreTestServerCluster.cs deleted file mode 100644 index ceb263e15..000000000 --- a/test/EventStore.Client.Tests.Common/Fixtures/Base/EventStoreTestServerCluster.cs +++ /dev/null @@ -1,93 +0,0 @@ -using System.Net; -using System.Net.Http; -using Ductus.FluentDocker.Builders; -using Ductus.FluentDocker.Common; -using Ductus.FluentDocker.Services; -using Polly; - -namespace EventStore.Client; - -// [Obsolete("Use EventStoreTestCluster instead.", false)] -public class EventStoreTestServerCluster : IEventStoreTestServer { - readonly ICompositeService _eventStoreCluster; - readonly HttpClient _httpClient; - - public EventStoreTestServerCluster( - string hostCertificatePath, - Uri address, - IDictionary? envOverrides - ) { - envOverrides ??= new Dictionary(); - envOverrides["ES_CERTS_CLUSTER"] = hostCertificatePath; - - _eventStoreCluster = BuildCluster(envOverrides); - -#if NET - _httpClient = new HttpClient(new SocketsHttpHandler { - SslOptions = {RemoteCertificateValidationCallback = delegate { return true; }} - }) { - BaseAddress = address, - }; -#else - _httpClient = new HttpClient(new WinHttpHandler { - ServerCertificateValidationCallback = delegate { return true; } - }) { - BaseAddress = address, - }; -#endif - } - - public async Task StartAsync(CancellationToken cancellationToken = default) { - try { - // don't know why, sometimes the default network (e.g. net50_default) remains - // from previous cluster and prevents docker-compose up from executing successfully - Policy.Handle() - .WaitAndRetry( - 10, - retryCount => TimeSpan.FromSeconds(2), - (ex, _) => { - BuildCluster().Dispose(); - _eventStoreCluster.Start(); - } - ) - .Execute(() => { _eventStoreCluster.Start(); }); - - await Policy.Handle() - .WaitAndRetryAsync(200, retryCount => TimeSpan.FromMilliseconds(100)) - .ExecuteAsync( - async () => { - using var response = await _httpClient.GetAsync("/health/live", cancellationToken); - if (response.StatusCode >= HttpStatusCode.BadRequest) - throw new($"Health check failed with status code: {response.StatusCode}."); - } - ); - } - catch (Exception) { - _eventStoreCluster.Dispose(); - throw; - } - } - - public void Stop() => _eventStoreCluster.Stop(); - - public ValueTask DisposeAsync() { - _eventStoreCluster.Dispose(); - return new(Task.CompletedTask); - } - - ICompositeService BuildCluster(IDictionary? envOverrides = null) { - var env = GlobalEnvironment - .GetEnvironmentVariables(envOverrides) - .Select(pair => $"{pair.Key}={pair.Value}") - .ToArray(); - - return new Builder() - .UseContainer() - .UseCompose() - .WithEnvironment(env) - .FromFile("docker-compose.yml") - .ForceRecreate() - .RemoveOrphans() - .Build(); - } -} diff --git a/test/EventStore.Client.Tests.Common/Fixtures/Base/EventStoreTestServerExternal.cs b/test/EventStore.Client.Tests.Common/Fixtures/Base/EventStoreTestServerExternal.cs deleted file mode 100644 index 19b866a63..000000000 --- a/test/EventStore.Client.Tests.Common/Fixtures/Base/EventStoreTestServerExternal.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace EventStore.Client; - -public class EventStoreTestServerExternal : IEventStoreTestServer { - public Task StartAsync(CancellationToken cancellationToken = default) => Task.CompletedTask; - public void Stop() { } - - public ValueTask DisposeAsync() => new ValueTask(Task.CompletedTask); -} diff --git a/test/EventStore.Client.Tests.Common/Fixtures/Base/IEventStoreTestServer.cs b/test/EventStore.Client.Tests.Common/Fixtures/Base/IEventStoreTestServer.cs deleted file mode 100644 index 2d467835d..000000000 --- a/test/EventStore.Client.Tests.Common/Fixtures/Base/IEventStoreTestServer.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace EventStore.Client; - -public interface IEventStoreTestServer : IAsyncDisposable { - Task StartAsync(CancellationToken cancellationToken = default); - void Stop(); -} \ No newline at end of file diff --git a/test/EventStore.Client.Tests.Common/Fixtures/BaseTestNode.cs b/test/EventStore.Client.Tests.Common/Fixtures/BaseTestNode.cs new file mode 100644 index 000000000..789384e9d --- /dev/null +++ b/test/EventStore.Client.Tests.Common/Fixtures/BaseTestNode.cs @@ -0,0 +1,162 @@ +// // ReSharper disable InconsistentNaming +// +// using System.Globalization; +// using System.Net; +// using System.Net.Sockets; +// using Ductus.FluentDocker.Builders; +// using Ductus.FluentDocker.Extensions; +// using Ductus.FluentDocker.Services.Extensions; +// using EventStore.Client.Tests.FluentDocker; +// using Humanizer; +// using Serilog; +// using Serilog.Extensions.Logging; +// using static System.TimeSpan; +// +// namespace EventStore.Client.Tests; +// +// public abstract class BaseTestNode(EventStoreFixtureOptions? options = null) : TestContainerService { +// static readonly NetworkPortProvider NetworkPortProvider = new(NetworkPortProvider.DefaultEsdbPort); +// +// public EventStoreFixtureOptions Options { get; } = options ?? DefaultOptions(); +// +// static Version? _version; +// +// public static Version Version => _version ??= GetVersion(); +// +// public static EventStoreFixtureOptions DefaultOptions() { +// const string connString = "esdb://admin:changeit@localhost:{port}/?tlsVerifyCert=false"; +// +// var port = NetworkPortProvider.NextAvailablePort; +// +// var defaultSettings = EventStoreClientSettings +// .Create(connString.Replace("{port}", $"{port}")) +// .With(x => x.LoggerFactory = new SerilogLoggerFactory(Log.Logger)) +// .With(x => x.DefaultDeadline = Application.DebuggerIsAttached ? new TimeSpan?() : FromSeconds(30)) +// .With(x => x.ConnectivitySettings.MaxDiscoverAttempts = 20) +// .With(x => x.ConnectivitySettings.DiscoveryInterval = FromSeconds(1)); +// +// var defaultEnvironment = new Dictionary(GlobalEnvironment.Variables) { +// // ["EVENTSTORE_MEM_DB"] = "true", +// // ["EVENTSTORE_CERTIFICATE_FILE"] = "/etc/eventstore/certs/node/node.crt", +// // ["EVENTSTORE_CERTIFICATE_PRIVATE_KEY_FILE"] = "/etc/eventstore/certs/node/node.key", +// // ["EVENTSTORE_STREAM_EXISTENCE_FILTER_SIZE"] = "10000", +// // ["EVENTSTORE_STREAM_INFO_CACHE_CAPACITY"] = "10000", +// // ["EVENTSTORE_ENABLE_ATOM_PUB_OVER_HTTP"] = "true", +// // ["EVENTSTORE_LOG_LEVEL"] = "Default", // required to use serilog settings +// // ["EVENTSTORE_DISABLE_LOG_FILE"] = "true", +// // ["EVENTSTORE_START_STANDARD_PROJECTIONS"] = "true", +// // ["EVENTSTORE_RUN_PROJECTIONS"] = "All", +// // ["EVENTSTORE_CHUNK_SIZE"] = (1024 * 1024 * 1024).ToString(), +// // ["EVENTSTORE_MAX_APPEND_SIZE"] = 100.Kilobytes().Bytes.ToString(CultureInfo.InvariantCulture), +// // ["EVENTSTORE_ADVERTISE_HTTP_PORT_TO_CLIENT_AS"] = $"{NetworkPortProvider.DefaultEsdbPort}" +// +// ["EVENTSTORE_MEM_DB"] = "true", +// ["EVENTSTORE_CERTIFICATE_FILE"] = "/etc/eventstore/certs/node/node.crt", +// ["EVENTSTORE_CERTIFICATE_PRIVATE_KEY_FILE"] = "/etc/eventstore/certs/node/node.key", +// ["EVENTSTORE_STREAM_EXISTENCE_FILTER_SIZE"] = "10000", +// ["EVENTSTORE_STREAM_INFO_CACHE_CAPACITY"] = "10000", +// ["EVENTSTORE_ENABLE_ATOM_PUB_OVER_HTTP"] = "true", +// ["EVENTSTORE_LOG_LEVEL"] = "Default", // required to use serilog settings +// ["EVENTSTORE_DISABLE_LOG_FILE"] = "true", +// ["EVENTSTORE_CHUNK_SIZE"] = (1024 * 1024 * 1024).ToString(), +// ["EVENTSTORE_MAX_APPEND_SIZE"] = 100.Kilobytes().Bytes.ToString(CultureInfo.InvariantCulture), +// ["EVENTSTORE_ADVERTISE_HTTP_PORT_TO_CLIENT_AS"] = $"{NetworkPortProvider.DefaultEsdbPort}" +// }; +// +// if (port != NetworkPortProvider.DefaultEsdbPort) { +// if (GlobalEnvironment.Variables.TryGetValue("ES_DOCKER_TAG", out var tag) && tag == "ci") +// defaultEnvironment["EVENTSTORE_ADVERTISE_NODE_PORT_TO_CLIENT_AS"] = $"{port}"; +// else +// defaultEnvironment["EVENTSTORE_ADVERTISE_HTTP_PORT_TO_CLIENT_AS"] = $"{port}"; +// } +// +// return new(defaultSettings, defaultEnvironment); +// } +// +// static Version GetVersion() { +// const string versionPrefix = "EventStoreDB version"; +// +// using var cts = new CancellationTokenSource(FromSeconds(30)); +// using var eventstore = new Builder().UseContainer() +// .UseImage(GlobalEnvironment.DockerImage) +// .Command("--version") +// .Build() +// .Start(); +// +// using var log = eventstore.Logs(true, cts.Token); +// foreach (var line in log.ReadToEnd()) { +// if (line.StartsWith(versionPrefix) && +// Version.TryParse(new string(ReadVersion(line[(versionPrefix.Length + 1)..]).ToArray()), out var version)) { +// return version; +// } +// } +// +// throw new InvalidOperationException("Could not determine server version."); +// +// IEnumerable ReadVersion(string s) { +// foreach (var c in s.TakeWhile(c => c == '.' || char.IsDigit(c))) { +// yield return c; +// } +// } +// } +// +// string[] GetEnvironmentVariables() => +// Options.Environment.Select(pair => $"{pair.Key}={pair.Value}").ToArray(); +// +// protected abstract ContainerBuilder ConfigureContainer(ContainerBuilder builder); +// +// protected override ContainerBuilder Configure() { +// var certsPath = Path.Combine(Environment.CurrentDirectory, "certs"); +// +// CertificatesManager.VerifyCertificatesExist(certsPath); +// +// var builder = new Builder().UseContainer().WithEnvironment(GetEnvironmentVariables()); +// +// return ConfigureContainer(builder); +// } +// } +// +// /// +// /// Using the default 2113 port assumes that the test is running sequentially. +// /// +// /// +// class NetworkPortProvider(int port = 2114) { +// public const int DefaultEsdbPort = 2113; +// +// static readonly SemaphoreSlim Semaphore = new(1, 1); +// +// async Task GetNextAvailablePort(TimeSpan delay = default) { +// if (port == DefaultEsdbPort) +// return port; +// +// await Semaphore.WaitAsync(); +// +// try { +// using var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); +// +// while (true) { +// var nexPort = Interlocked.Increment(ref port); +// +// try { +// await socket.ConnectAsync(IPAddress.Any, nexPort); +// } catch (SocketException ex) { +// if (ex.SocketErrorCode is SocketError.ConnectionRefused or not SocketError.IsConnected) { +// return nexPort; +// } +// +// await Task.Delay(delay); +// } finally { +// #if NET +// if (socket.Connected) await socket.DisconnectAsync(true); +// #else +// if (socket.Connected) socket.Disconnect(true); +// #endif +// } +// } +// } finally { +// Semaphore.Release(); +// } +// } +// +// public int NextAvailablePort => GetNextAvailablePort(FromMilliseconds(100)).GetAwaiter().GetResult(); +// } diff --git a/test/EventStore.Client.Tests.Common/Fixtures/DiagnosticsFixture.cs b/test/EventStore.Client.Tests.Common/Fixtures/DiagnosticsFixture.cs deleted file mode 100644 index 549093654..000000000 --- a/test/EventStore.Client.Tests.Common/Fixtures/DiagnosticsFixture.cs +++ /dev/null @@ -1,107 +0,0 @@ -using System.Collections.Concurrent; -using System.Diagnostics; -using EventStore.Client.Diagnostics; -using EventStore.Diagnostics; -using EventStore.Diagnostics.Telemetry; -using EventStore.Diagnostics.Tracing; - -namespace EventStore.Client.Tests; - -[PublicAPI] -public class DiagnosticsFixture : EventStoreFixture { - readonly ConcurrentDictionary<(string Operation, string Stream), List> _activities = []; - - public DiagnosticsFixture() : base(x => x.RunProjections()) { - var diagnosticActivityListener = new ActivityListener { - ShouldListenTo = source => source.Name == EventStoreClientDiagnostics.InstrumentationName, - Sample = (ref ActivityCreationOptions _) => ActivitySamplingResult.AllDataAndRecorded, - ActivityStopped = activity => { - var operation = (string?)activity.GetTagItem(TelemetryTags.Database.Operation); - var stream = (string?)activity.GetTagItem(TelemetryTags.EventStore.Stream); - - if (operation is null || stream is null) - return; - - _activities.AddOrUpdate( - (operation, stream), - _ => [activity], - (_, activities) => { - activities.Add(activity); - return activities; - } - ); - } - }; - - OnSetup = () => { - ActivitySource.AddActivityListener(diagnosticActivityListener); - return Task.CompletedTask; - }; - - OnTearDown = () => { - diagnosticActivityListener.Dispose(); - return Task.CompletedTask; - }; - } - - public List GetActivitiesForOperation(string operation, string stream) => - _activities.TryGetValue((operation, stream), out var activities) ? activities : []; - - public void AssertAppendActivityHasExpectedTags(Activity activity, string stream) { - var expectedTags = new Dictionary { - { TelemetryTags.Database.System, EventStoreClientDiagnostics.InstrumentationName }, - { TelemetryTags.Database.Operation, TracingConstants.Operations.Append }, - { TelemetryTags.EventStore.Stream, stream }, - { TelemetryTags.Database.User, TestCredentials.Root.Username }, - { TelemetryTags.Otel.StatusCode, ActivityStatusCodeHelper.OkStatusCodeTagValue } - }; - - foreach (var tag in expectedTags) - activity.Tags.ShouldContain(tag); - } - - public void AssertErroneousAppendActivityHasExpectedTags(Activity activity, Exception actualException) { - var expectedTags = new Dictionary { - { TelemetryTags.Otel.StatusCode, ActivityStatusCodeHelper.ErrorStatusCodeTagValue } - }; - - foreach (var tag in expectedTags) - activity.Tags.ShouldContain(tag); - - var actualEvent = activity.Events.ShouldHaveSingleItem(); - - actualEvent.Name.ShouldBe(TelemetryTags.Exception.EventName); - actualEvent.Tags.ShouldContain( - new KeyValuePair(TelemetryTags.Exception.Type, actualException.GetType().FullName) - ); - - actualEvent.Tags.ShouldContain( - new KeyValuePair(TelemetryTags.Exception.Message, actualException.Message) - ); - - actualEvent.Tags.Any(x => x.Key == TelemetryTags.Exception.Stacktrace).ShouldBeTrue(); - } - - public void AssertSubscriptionActivityHasExpectedTags( - Activity activity, - string stream, - string eventId, - string? subscriptionId = null - ) { - var expectedTags = new Dictionary { - { TelemetryTags.Database.System, EventStoreClientDiagnostics.InstrumentationName }, - { TelemetryTags.Database.Operation, TracingConstants.Operations.Subscribe }, - { TelemetryTags.EventStore.Stream, stream }, - { TelemetryTags.EventStore.EventId, eventId }, - { TelemetryTags.EventStore.EventType, TestEventType }, - { TelemetryTags.Database.User, TestCredentials.Root.Username } - }; - - if (subscriptionId != null) - expectedTags[TelemetryTags.EventStore.SubscriptionId] = subscriptionId; - - foreach (var tag in expectedTags) { - activity.Tags.ShouldContain(tag); - } - } -} diff --git a/test/EventStore.Client.Tests.Common/Fixtures/EventStoreTestCluster.cs b/test/EventStore.Client.Tests.Common/Fixtures/EventStoreTestCluster.cs deleted file mode 100644 index ad8246843..000000000 --- a/test/EventStore.Client.Tests.Common/Fixtures/EventStoreTestCluster.cs +++ /dev/null @@ -1,52 +0,0 @@ -using Ductus.FluentDocker.Builders; -using EventStore.Client.Tests.FluentDocker; -using Serilog; -using Serilog.Extensions.Logging; - -namespace EventStore.Client.Tests; - -public class EventStoreTestCluster(EventStoreFixtureOptions options) : TestCompositeService { - EventStoreFixtureOptions Options { get; } = options; - - public static EventStoreFixtureOptions DefaultOptions() { - const string connString = "esdb://localhost:2113,localhost:2112,localhost:2111?tls=true&tlsVerifyCert=false"; - - var defaultSettings = EventStoreClientSettings - .Create(connString) - .With(x => x.LoggerFactory = new SerilogLoggerFactory(Log.Logger)) - .With(x => x.DefaultDeadline = Application.DebuggerIsAttached ? new TimeSpan?() : TimeSpan.FromSeconds(30)) - .With(x => x.ConnectivitySettings.MaxDiscoverAttempts = 30) - .With(x => x.ConnectivitySettings.DiscoveryInterval = TimeSpan.FromSeconds(1)); - - var defaultEnvironment = new Dictionary(GlobalEnvironment.Variables) { - ["ES_CERTS_CLUSTER"] = Path.Combine(Environment.CurrentDirectory, "certs-cluster"), - ["EVENTSTORE_CLUSTER_SIZE"] = "3", - ["EVENTSTORE_INT_TCP_PORT"] = "1112", - ["EVENTSTORE_HTTP_PORT"] = "2113", - ["EVENTSTORE_DISCOVER_VIA_DNS"] = "false", - ["EVENTSTORE_STREAM_EXISTENCE_FILTER_SIZE"] = "10000", - ["EVENTSTORE_STREAM_INFO_CACHE_CAPACITY"] = "10000" - }; - - return new(defaultSettings, defaultEnvironment); - } - - protected override CompositeBuilder Configure() { - var env = Options.Environment.Select(pair => $"{pair.Key}={pair.Value}").ToArray(); - - var builder = new Builder() - .UseContainer() - .FromComposeFile("docker-compose.yml") - .ServiceName("esdb-test-cluster") - .WithEnvironment(env) - .RemoveOrphans() - .NoRecreate() - .KeepRunning(); - - return builder; - } - - protected override async Task OnServiceStarted() { - await Service.WaitUntilNodesAreHealthy("esdb-node", TimeSpan.FromSeconds(60)); - } -} diff --git a/test/EventStore.Client.Tests.Common/Fixtures/InsecureClientTestFixture.cs b/test/EventStore.Client.Tests.Common/Fixtures/InsecureClientTestFixture.cs deleted file mode 100644 index 33407d177..000000000 --- a/test/EventStore.Client.Tests.Common/Fixtures/InsecureClientTestFixture.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace EventStore.Client.Tests; - -/// -/// The clients dont have default credentials set. -/// -[PublicAPI] -public class InsecureClientTestFixture() : EventStoreFixture(x => x.WithoutDefaultCredentials()); \ No newline at end of file diff --git a/test/EventStore.Client.Tests.Common/Fixtures/KurrentFixtureOptions.cs b/test/EventStore.Client.Tests.Common/Fixtures/KurrentFixtureOptions.cs new file mode 100644 index 000000000..9e8496d27 --- /dev/null +++ b/test/EventStore.Client.Tests.Common/Fixtures/KurrentFixtureOptions.cs @@ -0,0 +1,20 @@ +namespace EventStore.Client.Tests; + +public record KurrentFixtureOptions( + EventStoreClientSettings ClientSettings, + IDictionary Environment +) { + public KurrentFixtureOptions WithoutDefaultCredentials() => this with { ClientSettings = ClientSettings.With(x => x.DefaultCredentials = null) }; + + public KurrentFixtureOptions RunProjections(bool runProjections = true) => + this with { + Environment = Environment.With( + x => { + x["EVENTSTORE_START_STANDARD_PROJECTIONS"] = runProjections.ToString(); + x["EVENTSTORE_RUN_PROJECTIONS"] = runProjections ? "All" : "None"; + } + ) + }; +} + +public delegate KurrentFixtureOptions ConfigureFixture(KurrentFixtureOptions options); diff --git a/test/EventStore.Client.Tests.Common/Fixtures/EventStoreFixture.Helpers.cs b/test/EventStore.Client.Tests.Common/Fixtures/KurrentPermanentFixture.Helpers.cs similarity index 78% rename from test/EventStore.Client.Tests.Common/Fixtures/EventStoreFixture.Helpers.cs rename to test/EventStore.Client.Tests.Common/Fixtures/KurrentPermanentFixture.Helpers.cs index 1e8bb3d83..41457d3b4 100644 --- a/test/EventStore.Client.Tests.Common/Fixtures/EventStoreFixture.Helpers.cs +++ b/test/EventStore.Client.Tests.Common/Fixtures/KurrentPermanentFixture.Helpers.cs @@ -3,7 +3,7 @@ namespace EventStore.Client.Tests; -public partial class EventStoreFixture { +public partial class KurrentPermanentFixture { public const string TestEventType = "test-event-type"; public const string AnotherTestEventTypePrefix = "another"; public const string AnotherTestEventType = $"{AnotherTestEventTypePrefix}-test-event-type"; @@ -12,7 +12,17 @@ public partial class EventStoreFixture { (T)Activator.CreateInstance(typeof(T), [ClientSettings.With(configure)])!; public string GetStreamName([CallerMemberName] string? testMethod = null) => - $"{testMethod}-{Guid.NewGuid():N}"; + $"stream-{testMethod}-{Guid.NewGuid():N}"; + + public string GetGroupName([CallerMemberName] string? testMethod = null) => + $"group-{testMethod}-{Guid.NewGuid():N}"; + + public UserCredentials GetUserCredentials([CallerMemberName] string? testMethod = null) => new UserCredentials( + $"user-{testMethod}-{Guid.NewGuid():N}", "pa$$word" + ); + + public string GetProjectionName([CallerMemberName] string? testMethod = null) => + $"projection-{testMethod}-{Guid.NewGuid():N}"; public ReadOnlyMemory CreateMetadataOfSize(int metadataSize) => Encoding.UTF8.GetBytes($"\"{new string('$', metadataSize)}\""); @@ -21,6 +31,16 @@ public ReadOnlyMemory CreateMetadataOfSize(int metadataSize) => public ReadOnlyMemory CreateTestNonJsonMetadata() => "non-json-metadata"u8.ToArray(); + public (IEnumerable Events, uint size) CreateTestEventsUpToMaxSize(uint maxSize) { + var size = 0; + + var events = CreateTestEvents(int.MaxValue) + .TakeWhile(evt => (size += evt.Data.Length) < maxSize) + .ToList(); + + return (events, (uint)size); + } + public IEnumerable CreateTestEvents( int count = 1, string? type = null, ReadOnlyMemory? metadata = null, string? contentType = null ) => diff --git a/test/EventStore.Client.Tests.Common/Fixtures/EventStoreFixture.cs b/test/EventStore.Client.Tests.Common/Fixtures/KurrentPermanentFixture.cs similarity index 64% rename from test/EventStore.Client.Tests.Common/Fixtures/EventStoreFixture.cs rename to test/EventStore.Client.Tests.Common/Fixtures/KurrentPermanentFixture.cs index 50faebf7a..e3c8e426f 100644 --- a/test/EventStore.Client.Tests.Common/Fixtures/EventStoreFixture.cs +++ b/test/EventStore.Client.Tests.Common/Fixtures/KurrentPermanentFixture.cs @@ -8,59 +8,27 @@ namespace EventStore.Client.Tests; -public record EventStoreFixtureOptions( - EventStoreClientSettings ClientSettings, - IDictionary Environment -) { - public EventStoreFixtureOptions RunInMemory(bool runInMemory = true) => - this with { Environment = Environment.With(x => x["EVENTSTORE_MEM_DB"] = runInMemory.ToString()) }; - - public EventStoreFixtureOptions RunProjections(bool runProjections = true) => - this with { - Environment = Environment.With( - x => { - x["EVENTSTORE_START_STANDARD_PROJECTIONS"] = runProjections.ToString(); - x["EVENTSTORE_RUN_PROJECTIONS"] = runProjections ? "All" : "None"; - } - ) - }; - - public EventStoreFixtureOptions WithoutDefaultCredentials() => - this with { ClientSettings = ClientSettings.With(x => x.DefaultCredentials = null) }; - - public EventStoreFixtureOptions WithMaxAppendSize(uint maxAppendSize) => - this with { Environment = Environment.With(x => x["EVENTSTORE_MAX_APPEND_SIZE"] = $"{maxAppendSize}") }; -} - -public delegate EventStoreFixtureOptions ConfigureFixture(EventStoreFixtureOptions options); - [PublicAPI] -public partial class EventStoreFixture : IAsyncLifetime, IAsyncDisposable { +public partial class KurrentPermanentFixture : IAsyncLifetime, IAsyncDisposable { static readonly ILogger Logger; - static EventStoreFixture() { + static KurrentPermanentFixture() { Logging.Initialize(); - Logger = Serilog.Log.ForContext(); + Logger = Serilog.Log.ForContext(); +#if NET9_0_OR_GREATER + var httpClientHandler = new HttpClientHandler(); + httpClientHandler.ServerCertificateCustomValidationCallback = delegate { return true; }; +#else ServicePointManager.ServerCertificateValidationCallback = delegate { return true; }; +#endif } - public EventStoreFixture() : this(options => options) { } - - protected EventStoreFixture(ConfigureFixture configure) { - // TODO SS: should I verify the certificates exist here? - if (GlobalEnvironment.UseExternalServer) { - Options = new(new(), new Dictionary()); - Service = new TestBypassService(); - } + public KurrentPermanentFixture() : this(options => options) { } - if (GlobalEnvironment.UseCluster) { - Options = configure(EventStoreTestCluster.DefaultOptions()); - Service = new EventStoreTestCluster(Options); - } else { - Options = configure(EventStoreTestNode.DefaultOptions()); - Service = new EventStoreTestNode(Options); - } + protected KurrentPermanentFixture(ConfigureFixture configure) { + Options = configure(KurrentPermanentTestNode.DefaultOptions()); + Service = new KurrentPermanentTestNode(Options); } List TestRuns { get; } = new(); @@ -68,7 +36,7 @@ protected EventStoreFixture(ConfigureFixture configure) { public ILogger Log => Logger; public ITestService Service { get; } - public EventStoreFixtureOptions Options { get; } + public KurrentFixtureOptions Options { get; } public Faker Faker { get; } = new Faker(); public Version EventStoreVersion { get; private set; } = null!; @@ -80,6 +48,8 @@ protected EventStoreFixture(ConfigureFixture configure) { public EventStorePersistentSubscriptionsClient Subscriptions { get; private set; } = null!; public EventStoreOperationsClient Operations { get; private set; } = null!; + public bool SkipPsWarmUp { get; set; } + public Func OnSetup { get; init; } = () => Task.CompletedTask; public Func OnTearDown { get; init; } = () => Task.CompletedTask; @@ -102,6 +72,8 @@ protected EventStoreFixture(ConfigureFixture configure) { InterlockedBoolean WarmUpCompleted { get; } = new InterlockedBoolean(); SemaphoreSlim WarmUpGatekeeper { get; } = new(1, 1); + static readonly SemaphoreSlim ContainerSemaphore = new(1, 1); + public void CaptureTestRun(ITestOutputHelper outputHelper) { var testRunId = Logging.CaptureLogs(outputHelper); TestRuns.Add(testRunId); @@ -110,7 +82,12 @@ public void CaptureTestRun(ITestOutputHelper outputHelper) { } public async Task InitializeAsync() { - await Service.Start(); + await ContainerSemaphore.WaitAsync(); + try { + await Service.Start(); + } finally { + ContainerSemaphore.Release(); + } EventStoreVersion = GetEventStoreVersion(); EventStoreHasLastStreamPosition = (EventStoreVersion?.Major ?? int.MaxValue) >= 21; @@ -128,7 +105,7 @@ await Task.WhenAll( async x => Projections = await x.WarmUp(), Options.Environment["EVENTSTORE_RUN_PROJECTIONS"] != "None" ), - InitClient(async x => Subscriptions = await x.WarmUp()), + InitClient(async x => Subscriptions = SkipPsWarmUp ? x : await x.WarmUp()), InitClient(async x => Operations = await x.WarmUp()) ); @@ -149,7 +126,7 @@ await Task.WhenAll( async Task InitClient(Func action, bool execute = true) where T : EventStoreClientBase { if (!execute) return default(T)!; - var client = (Activator.CreateInstance(typeof(T), new object?[] { ClientSettings }) as T)!; + var client = (Activator.CreateInstance(typeof(T), ClientSettings) as T)!; await action(client); return client; } @@ -202,21 +179,8 @@ public async Task DisposeAsync() { async ValueTask IAsyncDisposable.DisposeAsync() => await DisposeAsync(); } -[CollectionDefinition(nameof(EventStoreSharedDatabaseFixture))] -public class EventStoreSharedDatabaseFixture : ICollectionFixture { - // This class has no code, and is never created. Its purpose is simply - // to be the place to apply [CollectionDefinition] and all the - // ICollectionFixture<> interfaces. -} +public abstract class EventStorePermanentTests : IClassFixture where TFixture : KurrentPermanentFixture { + protected EventStorePermanentTests(ITestOutputHelper output, TFixture fixture) => Fixture = fixture.With(x => x.CaptureTestRun(output)); -public abstract class EventStoreTests : IClassFixture where TFixture : EventStoreFixture { - protected EventStoreTests(ITestOutputHelper output, TFixture fixture) => - Fixture = fixture.With(x => x.CaptureTestRun(output)); - protected TFixture Fixture { get; } } - -[Collection(nameof(EventStoreSharedDatabaseFixture))] -public abstract class EventStoreSharedDatabaseTests(ITestOutputHelper output, TFixture fixture) - : EventStoreTests(output, fixture) - where TFixture : EventStoreFixture; diff --git a/test/EventStore.Client.Tests.Common/Fixtures/EventStoreTestNode.cs b/test/EventStore.Client.Tests.Common/Fixtures/KurrentPermanentTestNode.cs similarity index 54% rename from test/EventStore.Client.Tests.Common/Fixtures/EventStoreTestNode.cs rename to test/EventStore.Client.Tests.Common/Fixtures/KurrentPermanentTestNode.cs index 6417ad3b9..d2c4549ab 100644 --- a/test/EventStore.Client.Tests.Common/Fixtures/EventStoreTestNode.cs +++ b/test/EventStore.Client.Tests.Common/Fixtures/KurrentPermanentTestNode.cs @@ -1,25 +1,61 @@ +// ReSharper disable InconsistentNaming + +// using Ductus.FluentDocker.Builders; +// using Ductus.FluentDocker.Model.Builders; +// using EventStore.Client.Tests.FluentDocker; +// +// namespace EventStore.Client.Tests; +// +// public class EventStorePermanentTestNode(EventStoreFixtureOptions? options = null) : BaseTestNode(options) { +// protected override ContainerBuilder ConfigureContainer(ContainerBuilder builder) { +// var port = Options.ClientSettings.ConnectivitySettings.ResolvedAddressOrDefault.Port; +// var certsPath = Path.Combine(Environment.CurrentDirectory, "certs"); +// +// var containerName = "es-client-dotnet-test"; +// +// return builder +// .UseImage(Options.Environment["ES_DOCKER_IMAGE"]) +// .WithName(containerName) +// .WithPublicEndpointResolver() +// .MountVolume(certsPath, "/etc/eventstore/certs", MountType.ReadOnly) +// .ExposePort(port, 2113) +// .KeepContainer().KeepRunning().ReuseIfExists() +// .WaitUntilReadyWithConstantBackoff( +// 1_000, +// 60, +// service => { +// var output = service.ExecuteCommand("curl -u admin:changeit --cacert /etc/eventstore/certs/ca/ca.crt https://localhost:2113/health/live"); +// if (!output.Success) +// throw new Exception(output.Error); +// } +// ); +// } +// } + +using System.Globalization; using System.Net; -using System.Net.Http; using System.Net.Sockets; using Ductus.FluentDocker.Builders; -using Ductus.FluentDocker.Common; +using Ductus.FluentDocker.Extensions; using Ductus.FluentDocker.Model.Builders; +using Ductus.FluentDocker.Services.Extensions; +using EventStore.Client; using EventStore.Client.Tests.FluentDocker; -using Polly; -using Polly.Contrib.WaitAndRetry; +using Humanizer; using Serilog; using Serilog.Extensions.Logging; using static System.TimeSpan; -namespace EventStore.Client.Tests; +public class KurrentPermanentTestNode(KurrentFixtureOptions? options = null) : TestContainerService { + static readonly NetworkPortProvider NetworkPortProvider = new(NetworkPortProvider.DefaultEsdbPort); -public class EventStoreTestNode(EventStoreFixtureOptions? options = null) : TestContainerService { + KurrentFixtureOptions Options { get; } = options ?? DefaultOptions(); - static readonly NetworkPortProvider NetworkPortProvider = new(NetworkPortProvider.DefaultEsdbPort); + static Version? _version; - EventStoreFixtureOptions Options { get; } = options ?? DefaultOptions(); + public static Version Version => _version ??= GetVersion(); - public static EventStoreFixtureOptions DefaultOptions() { + public static KurrentFixtureOptions DefaultOptions() { const string connString = "esdb://admin:changeit@localhost:{port}/?tlsVerifyCert=false"; var port = NetworkPortProvider.NextAvailablePort; @@ -33,23 +69,20 @@ public static EventStoreFixtureOptions DefaultOptions() { var defaultEnvironment = new Dictionary(GlobalEnvironment.Variables) { ["EVENTSTORE_MEM_DB"] = "true", - ["EVENTSTORE_CHUNK_SIZE"] = (1024 * 1024 * 1024).ToString(), ["EVENTSTORE_CERTIFICATE_FILE"] = "/etc/eventstore/certs/node/node.crt", ["EVENTSTORE_CERTIFICATE_PRIVATE_KEY_FILE"] = "/etc/eventstore/certs/node/node.key", ["EVENTSTORE_STREAM_EXISTENCE_FILTER_SIZE"] = "10000", ["EVENTSTORE_STREAM_INFO_CACHE_CAPACITY"] = "10000", ["EVENTSTORE_ENABLE_ATOM_PUB_OVER_HTTP"] = "true", - ["EVENTSTORE_LOG_LEVEL"] = "Default", // required to use serilog settings + ["EVENTSTORE_LOG_LEVEL"] = "Default", // required to use serilog settings ["EVENTSTORE_DISABLE_LOG_FILE"] = "true", + ["EVENTSTORE_START_STANDARD_PROJECTIONS"] = "true", + ["EVENTSTORE_RUN_PROJECTIONS"] = "All", + ["EVENTSTORE_CHUNK_SIZE"] = (1024 * 1024 * 1024).ToString(), + ["EVENTSTORE_MAX_APPEND_SIZE"] = 100.Kilobytes().Bytes.ToString(CultureInfo.InvariantCulture), ["EVENTSTORE_ADVERTISE_HTTP_PORT_TO_CLIENT_AS"] = $"{NetworkPortProvider.DefaultEsdbPort}" }; - if (GlobalEnvironment.DockerImage.Contains("commercial")) { - defaultEnvironment["EVENTSTORE_TRUSTED_ROOT_CERTIFICATES_PATH"] = "/etc/eventstore/certs/ca"; - defaultEnvironment["EventStore__Plugins__UserCertificates__Enabled"] = "true"; - } - - // TODO SS: must find a way to enable parallel tests on CI. It works locally. if (port != NetworkPortProvider.DefaultEsdbPort) { if (GlobalEnvironment.Variables.TryGetValue("ES_DOCKER_TAG", out var tag) && tag == "ci") defaultEnvironment["EVENTSTORE_ADVERTISE_NODE_PORT_TO_CLIENT_AS"] = $"{port}"; @@ -60,6 +93,33 @@ public static EventStoreFixtureOptions DefaultOptions() { return new(defaultSettings, defaultEnvironment); } + static Version GetVersion() { + const string versionPrefix = "EventStoreDB version"; + + using var cts = new CancellationTokenSource(FromSeconds(30)); + using var eventstore = new Builder().UseContainer() + .UseImage(GlobalEnvironment.DockerImage) + .Command("--version") + .Build() + .Start(); + + using var log = eventstore.Logs(true, cts.Token); + foreach (var line in log.ReadToEnd()) { + if (line.StartsWith(versionPrefix) && + Version.TryParse(new string(ReadVersion(line[(versionPrefix.Length + 1)..]).ToArray()), out var version)) { + return version; + } + } + + throw new InvalidOperationException("Could not determine server version."); + + IEnumerable ReadVersion(string s) { + foreach (var c in s.TakeWhile(c => c == '.' || char.IsDigit(c))) { + yield return c; + } + } + } + protected override ContainerBuilder Configure() { var env = Options.Environment.Select(pair => $"{pair.Key}={pair.Value}").ToArray(); @@ -76,44 +136,21 @@ protected override ContainerBuilder Configure() { .UseContainer() .UseImage(Options.Environment["ES_DOCKER_IMAGE"]) .WithName(containerName) - .WithPublicEndpointResolver() + .WithPublicEndpointResolver() .WithEnvironment(env) .MountVolume(certsPath, "/etc/eventstore/certs", MountType.ReadOnly) .ExposePort(port, 2113) - // .KeepContainer().KeepRunning().ReuseIfExists() - .WaitUntilReadyWithConstantBackoff(1_000, 60, service => { - var output = service.ExecuteCommand("curl -u admin:changeit --cacert /etc/eventstore/certs/ca/ca.crt https://localhost:2113/health/live"); - if (!output.Success) - throw new Exception(output.Error); - }); + .KeepContainer().KeepRunning().ReuseIfExists() + .WaitUntilReadyWithConstantBackoff( + 1_000, + 60, + service => { + var output = service.ExecuteCommand("curl -u admin:changeit --cacert /etc/eventstore/certs/ca/ca.crt https://localhost:2113/health/live"); + if (!output.Success) + throw new Exception(output.Error); + } + ); } - -// /// -// /// max of 30 seconds (300 * 100ms) -// /// -// static readonly IEnumerable DefaultBackoffDelay = Backoff.ConstantBackoff(FromMilliseconds(100), 300); -// -// protected override async Task OnServiceStarted() { -// using var http = new HttpClient( -// #if NET -// new SocketsHttpHandler { SslOptions = { RemoteCertificateValidationCallback = delegate { return true; } } } -// #else -// new WinHttpHandler { ServerCertificateValidationCallback = delegate { return true; } } -// #endif -// ) { -// BaseAddress = Options.ClientSettings.ConnectivitySettings.Address -// }; -// -// await Policy.Handle() -// .WaitAndRetryAsync(DefaultBackoffDelay) -// .ExecuteAsync( -// async () => { -// using var response = await http.GetAsync("/health/live", CancellationToken.None); -// if (response.StatusCode >= HttpStatusCode.BadRequest) -// throw new FluentDockerException($"Health check failed with status code: {response.StatusCode}."); -// } -// ); -// } } /// @@ -125,7 +162,7 @@ class NetworkPortProvider(int port = 2114) { static readonly SemaphoreSlim Semaphore = new(1, 1); - public async Task GetNextAvailablePort(TimeSpan delay = default) { + async Task GetNextAvailablePort(TimeSpan delay = default) { // TODO SS: find a way to enable parallel tests on CI if (port == DefaultEsdbPort) return port; @@ -140,15 +177,13 @@ public async Task GetNextAvailablePort(TimeSpan delay = default) { try { await socket.ConnectAsync(IPAddress.Any, nexPort); - } - catch (SocketException ex) { + } catch (SocketException ex) { if (ex.SocketErrorCode is SocketError.ConnectionRefused or not SocketError.IsConnected) { return nexPort; } await Task.Delay(delay); - } - finally { + } finally { #if NET if (socket.Connected) await socket.DisconnectAsync(true); #else @@ -156,8 +191,7 @@ public async Task GetNextAvailablePort(TimeSpan delay = default) { #endif } } - } - finally { + } finally { Semaphore.Release(); } } diff --git a/test/EventStore.Client.Tests.Common/Fixtures/KurrentTemporaryFixture.Helpers.cs b/test/EventStore.Client.Tests.Common/Fixtures/KurrentTemporaryFixture.Helpers.cs new file mode 100644 index 000000000..3090742b9 --- /dev/null +++ b/test/EventStore.Client.Tests.Common/Fixtures/KurrentTemporaryFixture.Helpers.cs @@ -0,0 +1,107 @@ +using System.Runtime.CompilerServices; +using System.Text; + +namespace EventStore.Client.Tests.TestNode; + +public partial class KurrentTemporaryFixture { + public const string TestEventType = "test-event-type"; + public const string AnotherTestEventTypePrefix = "another"; + public const string AnotherTestEventType = $"{AnotherTestEventTypePrefix}-test-event-type"; + + public T NewClient(Action configure) where T : EventStoreClientBase, new() => + (T)Activator.CreateInstance(typeof(T), [ClientSettings.With(configure)])!; + + public string GetStreamName([CallerMemberName] string? testMethod = null) => + $"stream-{testMethod}-{Guid.NewGuid():N}"; + + public string GetGroupName([CallerMemberName] string? testMethod = null) => + $"group-{testMethod}-{Guid.NewGuid():N}"; + + public UserCredentials GetUserCredentials([CallerMemberName] string? testMethod = null) => new UserCredentials( + $"user-{testMethod}-{Guid.NewGuid():N}", + "pa$$word" + ); + + public string GetProjectionName([CallerMemberName] string? testMethod = null) => + $"projection-{testMethod}-{Guid.NewGuid():N}"; + + public ReadOnlyMemory CreateMetadataOfSize(int metadataSize) => + Encoding.UTF8.GetBytes($"\"{new string('$', metadataSize)}\""); + + public ReadOnlyMemory CreateTestJsonMetadata() => "{\"Foo\": \"Bar\"}"u8.ToArray(); + + public ReadOnlyMemory CreateTestNonJsonMetadata() => "non-json-metadata"u8.ToArray(); + + public (IEnumerable Events, uint size) CreateTestEventsUpToMaxSize(uint maxSize) { + var size = 0; + + var events = CreateTestEvents(int.MaxValue) + .TakeWhile(evt => (size += evt.Data.Length) < maxSize) + .ToList(); + + return (events, (uint)size); + } + + public IEnumerable CreateTestEvents( + int count = 1, string? type = null, ReadOnlyMemory? metadata = null, string? contentType = null + ) => + Enumerable.Range(0, count) + .Select(index => CreateTestEvent(index, type ?? TestEventType, metadata, contentType)); + + public EventData CreateTestEvent( + string? type = null, ReadOnlyMemory? metadata = null, string? contentType = null + ) => + CreateTestEvent(0, type ?? TestEventType, metadata, contentType); + + public IEnumerable CreateTestEventsThatThrowsException() { + // Ensure initial IEnumerator.Current does not throw + yield return CreateTestEvent(1); + + // Throw after enumerator advances + throw new Exception(); + } + + protected static EventData CreateTestEvent(int index) => CreateTestEvent(index, TestEventType); + + protected static EventData CreateTestEvent( + int index, string type, ReadOnlyMemory? metadata = null, string? contentType = null + ) => + new( + Uuid.NewUuid(), + type, + Encoding.UTF8.GetBytes($$"""{"x":{{index}}}"""), + metadata, + contentType ?? "application/json" + ); + + public async Task CreateTestUser(bool withoutGroups = true, bool useUserCredentials = false) { + var result = await CreateTestUsers(1, withoutGroups, useUserCredentials); + return result.First(); + } + + public Task CreateTestUsers( + int count = 3, bool withoutGroups = true, bool useUserCredentials = false + ) => + Fakers.Users + .RuleFor(x => x.Groups, f => withoutGroups ? Array.Empty() : f.Lorem.Words()) + .Generate(count) + .Select( + async user => { + await Users.CreateUserAsync( + user.LoginName, + user.FullName, + user.Groups, + user.Password, + userCredentials: useUserCredentials ? user.Credentials : TestCredentials.Root + ); + + return user; + } + ).WhenAll(); + + public async Task RestartService(TimeSpan delay) { + await Service.Restart(delay); + await Streams.WarmUp(); + Log.Information("Service restarted."); + } +} diff --git a/test/EventStore.Client.Tests.Common/Fixtures/KurrentTemporaryFixture.cs b/test/EventStore.Client.Tests.Common/Fixtures/KurrentTemporaryFixture.cs new file mode 100644 index 000000000..e7e6bb889 --- /dev/null +++ b/test/EventStore.Client.Tests.Common/Fixtures/KurrentTemporaryFixture.cs @@ -0,0 +1,189 @@ +// ReSharper disable InconsistentNaming + +using System.Net; +using Ductus.FluentDocker.Builders; +using Ductus.FluentDocker.Extensions; +using Ductus.FluentDocker.Services.Extensions; +using EventStore.Client.Tests.FluentDocker; +using Serilog; +using static System.TimeSpan; + +namespace EventStore.Client.Tests.TestNode; + +[PublicAPI] +public partial class KurrentTemporaryFixture : IAsyncLifetime, IAsyncDisposable { + static readonly ILogger Logger; + + static KurrentTemporaryFixture() { + Logging.Initialize(); + Logger = Serilog.Log.ForContext(); + +#if NET9_0_OR_GREATER + var httpClientHandler = new HttpClientHandler(); + httpClientHandler.ServerCertificateCustomValidationCallback = delegate { return true; }; +#else + ServicePointManager.ServerCertificateValidationCallback = delegate { return true; }; +#endif + } + + public KurrentTemporaryFixture() : this(options => options) { } + + protected KurrentTemporaryFixture(ConfigureFixture configure) { + // Options = configure(EventStoreTemporaryTestNode.DefaultOptions()); + Options = configure(KurrentTemporaryTestNode.DefaultOptions()); + Service = new KurrentTemporaryTestNode(Options); + } + + List TestRuns { get; } = new(); + + public ILogger Log => Logger; + + public ITestService Service { get; } + public KurrentFixtureOptions Options { get; } + public Faker Faker { get; } = new Faker(); + + public Version EventStoreVersion { get; private set; } = null!; + public bool EventStoreHasLastStreamPosition { get; private set; } + + public EventStoreClient Streams { get; private set; } = null!; + public EventStoreUserManagementClient Users { get; private set; } = null!; + public EventStoreProjectionManagementClient Projections { get; private set; } = null!; + public EventStorePersistentSubscriptionsClient Subscriptions { get; private set; } = null!; + public EventStoreOperationsClient Operations { get; private set; } = null!; + + public bool SkipPsWarmUp { get; set; } + + public Func OnSetup { get; init; } = () => Task.CompletedTask; + public Func OnTearDown { get; init; } = () => Task.CompletedTask; + + /// + /// must test this + /// + public EventStoreClientSettings ClientSettings => + new() { + Interceptors = Options.ClientSettings.Interceptors, + ConnectionName = Options.ClientSettings.ConnectionName, + CreateHttpMessageHandler = Options.ClientSettings.CreateHttpMessageHandler, + LoggerFactory = Options.ClientSettings.LoggerFactory, + ChannelCredentials = Options.ClientSettings.ChannelCredentials, + OperationOptions = Options.ClientSettings.OperationOptions, + ConnectivitySettings = Options.ClientSettings.ConnectivitySettings, + DefaultCredentials = Options.ClientSettings.DefaultCredentials, + DefaultDeadline = Options.ClientSettings.DefaultDeadline + }; + + InterlockedBoolean WarmUpCompleted { get; } = new InterlockedBoolean(); + SemaphoreSlim WarmUpGatekeeper { get; } = new(1, 1); + static readonly SemaphoreSlim ContainerSemaphore = new(1, 1); + + public void CaptureTestRun(ITestOutputHelper outputHelper) { + var testRunId = Logging.CaptureLogs(outputHelper); + TestRuns.Add(testRunId); + Logger.Information(">>> Test Run {TestRunId} {Operation} <<<", testRunId, "starting"); + Service.ReportStatus(); + } + + public async Task InitializeAsync() { + await ContainerSemaphore.WaitAsync(); + try { + await Service.Start(); + } finally { + ContainerSemaphore.Release(); + } + + EventStoreVersion = GetEventStoreVersion(); + EventStoreHasLastStreamPosition = (EventStoreVersion?.Major ?? int.MaxValue) >= 21; + + await WarmUpGatekeeper.WaitAsync(); + + try { + if (!WarmUpCompleted.CurrentValue) { + Logger.Warning("*** Warmup started ***"); + + await Task.WhenAll( + InitClient(async x => Users = await x.WarmUp()), + InitClient(async x => Streams = await x.WarmUp()), + InitClient( + async x => Projections = await x.WarmUp(), + Options.Environment["EVENTSTORE_RUN_PROJECTIONS"] != "None" + ), + InitClient(async x => Subscriptions = SkipPsWarmUp ? x : await x.WarmUp()), + InitClient(async x => Operations = await x.WarmUp()) + ); + + WarmUpCompleted.EnsureCalledOnce(); + + Logger.Warning("*** Warmup completed ***"); + } else { + Logger.Information("*** Warmup skipped ***"); + } + } finally { + WarmUpGatekeeper.Release(); + } + + await OnSetup(); + + return; + + async Task InitClient(Func action, bool execute = true) where T : EventStoreClientBase { + if (!execute) return default(T)!; + + var client = (Activator.CreateInstance(typeof(T), ClientSettings) as T)!; + await action(client); + return client; + } + + static Version GetEventStoreVersion() { + const string versionPrefix = "EventStoreDB version"; + + using var cancellator = new CancellationTokenSource(FromSeconds(30)); + using var eventstore = new Builder() + .UseContainer() + .UseImage(GlobalEnvironment.DockerImage) + .Command("--version") + .Build() + .Start(); + + using var log = eventstore.Logs(true, cancellator.Token); + foreach (var line in log.ReadToEnd()) { + if (line.StartsWith(versionPrefix) && + Version.TryParse( + new string(ReadVersion(line[(versionPrefix.Length + 1)..]).ToArray()), + out var version + )) { + return version; + } + } + + throw new InvalidOperationException("Could not determine server version."); + + IEnumerable ReadVersion(string s) { + foreach (var c in s.TakeWhile(c => c == '.' || char.IsDigit(c))) { + yield return c; + } + } + } + } + + public async Task DisposeAsync() { + try { + await OnTearDown(); + } catch { + // ignored + } + + await Service.DisposeAsync().AsTask().WithTimeout(FromMinutes(5)); + + foreach (var testRunId in TestRuns) + Logging.ReleaseLogs(testRunId); + } + + async ValueTask IAsyncDisposable.DisposeAsync() => await DisposeAsync(); +} + +public abstract class KurrentTemporaryTests : IClassFixture where TFixture : KurrentTemporaryFixture { + protected KurrentTemporaryTests(ITestOutputHelper output, TFixture fixture) => + Fixture = fixture.With(x => x.CaptureTestRun(output)); + + protected TFixture Fixture { get; } +} diff --git a/test/EventStore.Client.Tests.Common/Fixtures/KurrentTemporaryTestNode.cs b/test/EventStore.Client.Tests.Common/Fixtures/KurrentTemporaryTestNode.cs new file mode 100644 index 000000000..ca7ef8cc0 --- /dev/null +++ b/test/EventStore.Client.Tests.Common/Fixtures/KurrentTemporaryTestNode.cs @@ -0,0 +1,193 @@ +// ReSharper disable InconsistentNaming + +// using Ductus.FluentDocker.Builders; +// using Ductus.FluentDocker.Model.Builders; +// using EventStore.Client.Tests.FluentDocker; +// +// namespace EventStore.Client.Tests.TestNode; +// +// public class EventStoreTemporaryTestNode(EventStoreFixtureOptions? options = null) : BaseTestNode(options) { +// protected override ContainerBuilder ConfigureContainer(ContainerBuilder builder) { +// var port = Options.ClientSettings.ConnectivitySettings.ResolvedAddressOrDefault.Port; +// var certsPath = Path.Combine(Environment.CurrentDirectory, "certs"); +// +// var containerName = $"es-client-dotnet-test-{port}-{Guid.NewGuid().ToString()[30..]}"; +// +// return builder +// .UseImage(Options.Environment["ES_DOCKER_IMAGE"]) +// .WithName(containerName) +// .WithPublicEndpointResolver() +// .MountVolume(certsPath, "/etc/eventstore/certs", MountType.ReadOnly) +// .ExposePort(port, 2113) +// .WaitUntilReadyWithConstantBackoff( +// 1_000, +// 60, +// service => { +// var output = service.ExecuteCommand("curl -u admin:changeit --cacert /etc/eventstore/certs/ca/ca.crt https://localhost:2113/health/live"); +// if (!output.Success) +// throw new Exception(output.Error); +// } +// ); +// } +// } + +using System.Globalization; +using System.Net; +using System.Net.Sockets; +using Ductus.FluentDocker.Builders; +using Ductus.FluentDocker.Extensions; +using Ductus.FluentDocker.Model.Builders; +using Ductus.FluentDocker.Services.Extensions; +using EventStore.Client.Tests.FluentDocker; +using Humanizer; +using Serilog; +using Serilog.Extensions.Logging; +using static System.TimeSpan; + +namespace EventStore.Client.Tests.TestNode; + +public class KurrentTemporaryTestNode(KurrentFixtureOptions? options = null) : TestContainerService { + static readonly NetworkPortProvider NetworkPortProvider = new(NetworkPortProvider.DefaultEsdbPort); + + KurrentFixtureOptions Options { get; } = options ?? DefaultOptions(); + + static Version? _version; + + public static Version Version => _version ??= GetVersion(); + + public static KurrentFixtureOptions DefaultOptions() { + const string connString = "esdb://admin:changeit@localhost:{port}/?tlsVerifyCert=false"; + + var port = NetworkPortProvider.NextAvailablePort; + + var defaultSettings = EventStoreClientSettings + .Create(connString.Replace("{port}", $"{port}")) + .With(x => x.LoggerFactory = new SerilogLoggerFactory(Log.Logger)) + .With(x => x.DefaultDeadline = Application.DebuggerIsAttached ? new TimeSpan?() : FromSeconds(30)) + .With(x => x.ConnectivitySettings.MaxDiscoverAttempts = 20) + .With(x => x.ConnectivitySettings.DiscoveryInterval = FromSeconds(1)); + + var defaultEnvironment = new Dictionary(GlobalEnvironment.Variables) { + ["EVENTSTORE_MEM_DB"] = "true", + ["EVENTSTORE_CERTIFICATE_FILE"] = "/etc/eventstore/certs/node/node.crt", + ["EVENTSTORE_CERTIFICATE_PRIVATE_KEY_FILE"] = "/etc/eventstore/certs/node/node.key", + ["EVENTSTORE_STREAM_EXISTENCE_FILTER_SIZE"] = "10000", + ["EVENTSTORE_STREAM_INFO_CACHE_CAPACITY"] = "10000", + ["EVENTSTORE_ENABLE_ATOM_PUB_OVER_HTTP"] = "true", + ["EVENTSTORE_LOG_LEVEL"] = "Default", // required to use serilog settings + ["EVENTSTORE_DISABLE_LOG_FILE"] = "true", + ["EVENTSTORE_CHUNK_SIZE"] = (1024 * 1024 * 1024).ToString(), + ["EVENTSTORE_MAX_APPEND_SIZE"] = 100.Kilobytes().Bytes.ToString(CultureInfo.InvariantCulture), + ["EVENTSTORE_ADVERTISE_HTTP_PORT_TO_CLIENT_AS"] = $"{NetworkPortProvider.DefaultEsdbPort}" + }; + + if (port != NetworkPortProvider.DefaultEsdbPort) { + if (GlobalEnvironment.Variables.TryGetValue("ES_DOCKER_TAG", out var tag) && tag == "ci") + defaultEnvironment["EVENTSTORE_ADVERTISE_NODE_PORT_TO_CLIENT_AS"] = $"{port}"; + else + defaultEnvironment["EVENTSTORE_ADVERTISE_HTTP_PORT_TO_CLIENT_AS"] = $"{port}"; + } + + return new(defaultSettings, defaultEnvironment); + } + + static Version GetVersion() { + const string versionPrefix = "EventStoreDB version"; + + using var cts = new CancellationTokenSource(FromSeconds(30)); + using var eventstore = new Builder().UseContainer() + .UseImage(GlobalEnvironment.DockerImage) + .Command("--version") + .Build() + .Start(); + + using var log = eventstore.Logs(true, cts.Token); + foreach (var line in log.ReadToEnd()) { + if (line.StartsWith(versionPrefix) && + Version.TryParse(new string(ReadVersion(line[(versionPrefix.Length + 1)..]).ToArray()), out var version)) { + return version; + } + } + + throw new InvalidOperationException("Could not determine server version."); + + IEnumerable ReadVersion(string s) { + foreach (var c in s.TakeWhile(c => c == '.' || char.IsDigit(c))) { + yield return c; + } + } + } + + protected override ContainerBuilder Configure() { + var env = Options.Environment.Select(pair => $"{pair.Key}={pair.Value}").ToArray(); + + var port = Options.ClientSettings.ConnectivitySettings.ResolvedAddressOrDefault.Port; + var certsPath = Path.Combine(Environment.CurrentDirectory, "certs"); + + var containerName = $"es-client-dotnet-test-{port}-{Guid.NewGuid().ToString()[30..]}"; + + CertificatesManager.VerifyCertificatesExist(certsPath); + + var builder = new Builder() + .UseContainer() + .UseImage(Options.Environment["ES_DOCKER_IMAGE"]) + .WithName(containerName) + .WithPublicEndpointResolver() + .WithEnvironment(env) + .MountVolume(certsPath, "/etc/eventstore/certs", MountType.ReadOnly) + .ExposePort(port, 2113) + .WaitUntilReadyWithConstantBackoff( + 1_000, + 60, + service => { + var output = service.ExecuteCommand("curl -u admin:changeit --cacert /etc/eventstore/certs/ca/ca.crt https://localhost:2113/health/live"); + if (!output.Success) + throw new Exception(output.Error); + } + ); + + return builder; + } +} + +/// +/// Using the default 2113 port assumes that the test is running sequentially. +/// +/// +class NetworkPortProvider(int port = 2114) { + public const int DefaultEsdbPort = 2113; + + static readonly SemaphoreSlim Semaphore = new(1, 1); + + async Task GetNextAvailablePort(TimeSpan delay = default) { + await Semaphore.WaitAsync(); + + try { + using var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + + while (true) { + var nexPort = Interlocked.Increment(ref port); + + try { + await socket.ConnectAsync(IPAddress.Any, nexPort); + } catch (SocketException ex) { + if (ex.SocketErrorCode is SocketError.ConnectionRefused or not SocketError.IsConnected) { + return nexPort; + } + + await Task.Delay(delay); + } finally { +#if NET + if (socket.Connected) await socket.DisconnectAsync(true); +#else + if (socket.Connected) socket.Disconnect(true); +#endif + } + } + } finally { + Semaphore.Release(); + } + } + + public int NextAvailablePort => GetNextAvailablePort(FromMilliseconds(100)).GetAwaiter().GetResult(); +} diff --git a/test/EventStore.Client.Tests.Common/Fixtures/RunInMemoryTestFixture.cs b/test/EventStore.Client.Tests.Common/Fixtures/RunInMemoryTestFixture.cs deleted file mode 100644 index 61cfbc77c..000000000 --- a/test/EventStore.Client.Tests.Common/Fixtures/RunInMemoryTestFixture.cs +++ /dev/null @@ -1,4 +0,0 @@ -namespace EventStore.Client.Tests; - -[PublicAPI] -public class RunInMemoryTestFixture() : EventStoreFixture(x => x.RunInMemory()); \ No newline at end of file diff --git a/test/EventStore.Client.Tests.Common/Fixtures/RunProjectionsTestFixture.cs b/test/EventStore.Client.Tests.Common/Fixtures/RunProjectionsTestFixture.cs deleted file mode 100644 index cb42cc1db..000000000 --- a/test/EventStore.Client.Tests.Common/Fixtures/RunProjectionsTestFixture.cs +++ /dev/null @@ -1,4 +0,0 @@ -namespace EventStore.Client.Tests; - -[PublicAPI] -public class RunProjectionsTestFixture() : EventStoreFixture(x => x.RunProjections()); \ No newline at end of file diff --git a/test/EventStore.Client.Tests.Common/FluentDocker/FluentDockerBuilderExtensions.cs b/test/EventStore.Client.Tests.Common/FluentDocker/FluentDockerBuilderExtensions.cs index b41c031c9..3dd450676 100644 --- a/test/EventStore.Client.Tests.Common/FluentDocker/FluentDockerBuilderExtensions.cs +++ b/test/EventStore.Client.Tests.Common/FluentDocker/FluentDockerBuilderExtensions.cs @@ -85,4 +85,4 @@ public static ContainerBuilder WaitUntilReadyWithConstantBackoffAsync( public static ContainerBuilder WaitUntilReadyWithExponentialBackoffAsync( this ContainerBuilder builder, int delayMs, int retryCount, Func action ) => builder.WaitUntilReadyWithExponentialBackoffAsync(FromMilliseconds(delayMs), retryCount, action); -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Tests.Common/FluentDocker/FluentDockerServiceExtensions.cs b/test/EventStore.Client.Tests.Common/FluentDocker/FluentDockerServiceExtensions.cs index 3fab79a6e..0546c4e04 100644 --- a/test/EventStore.Client.Tests.Common/FluentDocker/FluentDockerServiceExtensions.cs +++ b/test/EventStore.Client.Tests.Common/FluentDocker/FluentDockerServiceExtensions.cs @@ -85,4 +85,4 @@ public static CommandResponse> ExecuteCommand(this IContainerServi var config = service.GetConfiguration(); return service.DockerHost.Execute(config.Id, command, service.Certificates); } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Tests.Common/FluentDocker/TestCompositeService.cs b/test/EventStore.Client.Tests.Common/FluentDocker/TestCompositeService.cs index 104827f30..262133e9f 100644 --- a/test/EventStore.Client.Tests.Common/FluentDocker/TestCompositeService.cs +++ b/test/EventStore.Client.Tests.Common/FluentDocker/TestCompositeService.cs @@ -3,4 +3,4 @@ namespace EventStore.Client.Tests.FluentDocker; -public abstract class TestCompositeService : TestService; \ No newline at end of file +public abstract class TestCompositeService : TestService; diff --git a/test/EventStore.Client.Tests.Common/FluentDocker/TestContainerService.cs b/test/EventStore.Client.Tests.Common/FluentDocker/TestContainerService.cs index 40ed937fc..ae37c353d 100644 --- a/test/EventStore.Client.Tests.Common/FluentDocker/TestContainerService.cs +++ b/test/EventStore.Client.Tests.Common/FluentDocker/TestContainerService.cs @@ -3,4 +3,4 @@ namespace EventStore.Client.Tests.FluentDocker; -public abstract class TestContainerService : TestService; \ No newline at end of file +public abstract class TestContainerService : TestService; diff --git a/test/EventStore.Client.Tests.Common/GlobalEnvironment.cs b/test/EventStore.Client.Tests.Common/GlobalEnvironment.cs index 1186d5f68..1fd1ee020 100644 --- a/test/EventStore.Client.Tests.Common/GlobalEnvironment.cs +++ b/test/EventStore.Client.Tests.Common/GlobalEnvironment.cs @@ -23,7 +23,7 @@ static void EnsureDefaults(IConfiguration configuration) { configuration.EnsureValue("ES_USE_EXTERNAL_SERVER", "false"); configuration.EnsureValue("ES_DOCKER_REGISTRY", "docker.eventstore.com/eventstore-ce/eventstoredb-ce"); - configuration.EnsureValue("ES_DOCKER_TAG", "previous-lts"); + configuration.EnsureValue("ES_DOCKER_TAG", "ci"); configuration.EnsureValue("ES_DOCKER_IMAGE", $"{configuration["ES_DOCKER_REGISTRY"]}:{configuration["ES_DOCKER_TAG"]}"); configuration.EnsureValue("EVENTSTORE_TELEMETRY_OPTOUT", "true"); @@ -35,7 +35,6 @@ static void EnsureDefaults(IConfiguration configuration) { configuration.EnsureValue("EVENTSTORE_DISABLE_LOG_FILE", "true"); configuration.EnsureValue("EVENTSTORE_TRUSTED_ROOT_CERTIFICATES_PATH", "/etc/eventstore/certs/ca"); configuration.EnsureValue("EVENTSTORE_ENABLE_ATOM_PUB_OVER_HTTP", "true"); - } } @@ -44,35 +43,4 @@ static void EnsureDefaults(IConfiguration configuration) { public static bool UseCluster { get; } public static bool UseExternalServer { get; } public static string DockerImage { get; } - - #region . Obsolete . - - //[Obsolete("Use the EventStoreFixture instead so you don't have to use this method.", false)] - public static IDictionary GetEnvironmentVariables(IDictionary? overrides = null) { - var env = new Dictionary { - ["ES_DOCKER_TAG"] = "ci", - ["EVENTSTORE_DB_LOG_FORMAT"] = "V2", - }; - - foreach (var @override in overrides ?? Enumerable.Empty>()) { - if (@override.Key.StartsWith("EVENTSTORE") && !SharedEnv.Contains(@override.Key)) - throw new Exception($"Add {@override.Key} to shared.env and _sharedEnv to pass it to the cluster containers"); - - env[@override.Key] = @override.Value; - } - - return env; - } - - // matches with the pass-through vars in shared.env... better way? - static readonly HashSet SharedEnv = new() { - "EVENTSTORE_DB_LOG_FORMAT", - "EVENTSTORE_LOG_LEVEL", - "EVENTSTORE_MAX_APPEND_SIZE", - "EVENTSTORE_MEM_DB", - "EVENTSTORE_RUN_PROJECTIONS", - "EVENTSTORE_START_STANDARD_PROJECTIONS", - }; - - #endregion } diff --git a/test/EventStore.Client.Tests.Common/Logging.cs b/test/EventStore.Client.Tests.Common/Logging.cs index a9df9709a..5742e1134 100644 --- a/test/EventStore.Client.Tests.Common/Logging.cs +++ b/test/EventStore.Client.Tests.Common/Logging.cs @@ -78,4 +78,4 @@ public static void ReleaseLogs(Guid captureId) { // ignored } } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Tests.Common/PasswordGenerator.cs b/test/EventStore.Client.Tests.Common/PasswordGenerator.cs index 298600529..f8990b18d 100644 --- a/test/EventStore.Client.Tests.Common/PasswordGenerator.cs +++ b/test/EventStore.Client.Tests.Common/PasswordGenerator.cs @@ -63,4 +63,4 @@ public static string GenerateSimplePassword(int length = 8) { return new(password); } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Tests.Common/Shouldly/ShouldThrowAsyncExtensions.cs b/test/EventStore.Client.Tests.Common/Shouldly/ShouldThrowAsyncExtensions.cs index b75778b47..f5ec36636 100644 --- a/test/EventStore.Client.Tests.Common/Shouldly/ShouldThrowAsyncExtensions.cs +++ b/test/EventStore.Client.Tests.Common/Shouldly/ShouldThrowAsyncExtensions.cs @@ -9,4 +9,4 @@ namespace Shouldly; public static class ShouldThrowAsyncExtensions { public static Task ShouldThrowAsync(this EventStoreClient.ReadStreamResult source) where TException : Exception => source.ToArrayAsync().AsTask().ShouldThrowAsync(); -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Tests.Common/TestCredentials.cs b/test/EventStore.Client.Tests.Common/TestCredentials.cs index a489cd13d..b3d075ba4 100644 --- a/test/EventStore.Client.Tests.Common/TestCredentials.cs +++ b/test/EventStore.Client.Tests.Common/TestCredentials.cs @@ -6,4 +6,4 @@ public static class TestCredentials { public static readonly UserCredentials TestUser2 = new("user2", "pa$$2"); public static readonly UserCredentials TestAdmin = new("adm", "admpa$$"); public static readonly UserCredentials TestBadUser = new("badlogin", "badpass"); -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Tests/Assertions/ComparableAssertion.cs b/test/EventStore.Client.Tests/Assertions/ComparableAssertion.cs index 0d626cf61..a41bd9fa9 100644 --- a/test/EventStore.Client.Tests/Assertions/ComparableAssertion.cs +++ b/test/EventStore.Client.Tests/Assertions/ComparableAssertion.cs @@ -142,4 +142,4 @@ public override void Verify(Type type) { } } } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Tests/Assertions/EqualityAssertion.cs b/test/EventStore.Client.Tests/Assertions/EqualityAssertion.cs index 055cef40b..69ab7aed6 100644 --- a/test/EventStore.Client.Tests/Assertions/EqualityAssertion.cs +++ b/test/EventStore.Client.Tests/Assertions/EqualityAssertion.cs @@ -67,4 +67,4 @@ public override void Verify(Type type) { throw new($"The type '{type}' did not implement the inequality (!=) operator correctly."); } } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Tests/Assertions/NullArgumentAssertion.cs b/test/EventStore.Client.Tests/Assertions/NullArgumentAssertion.cs index 770591afc..866ce8a47 100644 --- a/test/EventStore.Client.Tests/Assertions/NullArgumentAssertion.cs +++ b/test/EventStore.Client.Tests/Assertions/NullArgumentAssertion.cs @@ -50,4 +50,4 @@ public override void Verify(Type type) { } ); } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Tests/Assertions/StringConversionAssertion.cs b/test/EventStore.Client.Tests/Assertions/StringConversionAssertion.cs index 8b97e33f8..302803c51 100644 --- a/test/EventStore.Client.Tests/Assertions/StringConversionAssertion.cs +++ b/test/EventStore.Client.Tests/Assertions/StringConversionAssertion.cs @@ -43,4 +43,4 @@ public override void Verify(Type type) { if (toString is not null) Assert.Equal(value, toString.Invoke(instance, null)); } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Tests/Assertions/ValueObjectAssertion.cs b/test/EventStore.Client.Tests/Assertions/ValueObjectAssertion.cs index dbdbb5ca2..3ff92b7c9 100644 --- a/test/EventStore.Client.Tests/Assertions/ValueObjectAssertion.cs +++ b/test/EventStore.Client.Tests/Assertions/ValueObjectAssertion.cs @@ -13,4 +13,4 @@ static IEnumerable CreateChildrenAssertions(ISpecimenBuilde yield return new StringConversionAssertion(builder); yield return new NullArgumentAssertion(builder); } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Tests/AutoScenarioDataAttribute.cs b/test/EventStore.Client.Tests/AutoScenarioDataAttribute.cs index d5840a41b..1bad5bce5 100644 --- a/test/EventStore.Client.Tests/AutoScenarioDataAttribute.cs +++ b/test/EventStore.Client.Tests/AutoScenarioDataAttribute.cs @@ -25,4 +25,4 @@ public override IEnumerable GetData(MethodInfo testMethod) { class CustomAutoData : AutoDataAttribute { public CustomAutoData(Type fixtureType) : base(() => (IFixture)Activator.CreateInstance(fixtureType)!) { } } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Plugins.Tests/ClientCertificateTests.cs b/test/EventStore.Client.Tests/ClientCertificatesTests.cs similarity index 76% rename from test/EventStore.Client.Plugins.Tests/ClientCertificateTests.cs rename to test/EventStore.Client.Tests/ClientCertificatesTests.cs index e50fde0a5..e2e1a248c 100644 --- a/test/EventStore.Client.Plugins.Tests/ClientCertificateTests.cs +++ b/test/EventStore.Client.Tests/ClientCertificatesTests.cs @@ -1,9 +1,12 @@ -namespace EventStore.Client.Plugins.Tests; +using Humanizer; + +namespace EventStore.Client.Tests; [Trait("Category", "Target:Plugins")] [Trait("Category", "Type:UserCertificate")] -public class ClientCertificateTests(ITestOutputHelper output, EventStoreFixture fixture) : EventStoreTests(output, fixture) { - [Theory, BadClientCertificatesTestCases] +public class ClientCertificateTests(ITestOutputHelper output, KurrentPermanentFixture fixture) + : EventStorePermanentTests(output, fixture) { + [SupportsPlugins.Theory(EventStoreRepository.Commercial, "This server version does not support plugins"), BadClientCertificatesTestCases] async Task bad_certificates_combinations_should_return_authentication_error(string userCertFile, string userKeyFile, string tlsCaFile) { var stream = Fixture.GetStreamName(); var seedEvents = Fixture.CreateTestEvents(); @@ -17,7 +20,7 @@ async Task bad_certificates_combinations_should_return_authentication_error(stri await client.AppendToStreamAsync(stream, StreamState.NoStream, seedEvents).ShouldThrowAsync(); } - [Theory, ValidClientCertificatesTestCases] + [SupportsPlugins.Theory(EventStoreRepository.Commercial, "This server version does not support plugins"), ValidClientCertificatesTestCases] async Task valid_certificates_combinations_should_write_to_stream(string userCertFile, string userKeyFile, string tlsCaFile) { var stream = Fixture.GetStreamName(); var seedEvents = Fixture.CreateTestEvents(); @@ -32,7 +35,7 @@ async Task valid_certificates_combinations_should_write_to_stream(string userCer result.ShouldNotBeNull(); } - [Theory, BadClientCertificatesTestCases] + [SupportsPlugins.Theory(EventStoreRepository.Commercial, "This server version does not support plugins"), BadClientCertificatesTestCases] async Task basic_authentication_should_take_precedence(string userCertFile, string userKeyFile, string tlsCaFile) { var stream = Fixture.GetStreamName(); var seedEvents = Fixture.CreateTestEvents(); @@ -65,3 +68,17 @@ protected override IEnumerable Data() { } } } + +public enum EventStoreRepository { + Commercial = 1 +} + +[PublicAPI] +public class SupportsPlugins { + public class TheoryAttribute(EventStoreRepository repository, string skipMessage) : Xunit.TheoryAttribute { + public override string? Skip { + get => !GlobalEnvironment.DockerImage.Contains(repository.Humanize().ToLower()) ? skipMessage : null; + set => throw new NotSupportedException(); + } + } +} diff --git a/test/EventStore.Client.Tests/ConnectionStringTests.cs b/test/EventStore.Client.Tests/ConnectionStringTests.cs index e258c682d..15b86703c 100644 --- a/test/EventStore.Client.Tests/ConnectionStringTests.cs +++ b/test/EventStore.Client.Tests/ConnectionStringTests.cs @@ -1,8 +1,11 @@ using System.Net; -using System.Net.Http; -using AutoFixture; using System.Reflection; using System.Security.Cryptography.X509Certificates; +using AutoFixture; + +#if NET48 +using System.Net.Http; +#endif namespace EventStore.Client.Tests; @@ -130,8 +133,7 @@ public void tls_verify_cert(bool tlsVerifyCert) { default ) ); - } - else { + } else { Assert.Null(socketsHandler.SslOptions.RemoteCertificateValidationCallback); } #else @@ -156,16 +158,14 @@ public void tls_verify_cert(bool tlsVerifyCert) { [Theory] [MemberData(nameof(InvalidTlsCertificates))] public void connection_string_with_invalid_tls_certificate_should_throw(string clientCertificatePath) { - Assert.Throws( - () => EventStoreClientSettings.Create( - $"esdb://admin:changeit@localhost:2113/?tls=true&tlsVerifyCert=true&tlsCAFile={clientCertificatePath}" - ) + Assert.Throws( + () => EventStoreClientSettings.Create($"esdb://admin:changeit@localhost:2113/?tls=true&tlsVerifyCert=true&tlsCAFile={clientCertificatePath}") ); } public static IEnumerable InvalidClientCertificates() { var invalidPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "path", "not", "found"); - var validPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "certs", "ca", "ca.crt"); + var validPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "certs", "ca", "ca.crt"); yield return [invalidPath, invalidPath]; yield return [validPath, invalidPath]; } @@ -180,7 +180,7 @@ public void connection_string_with_invalid_client_certificate_should_throw(strin ); } - [Fact] + [RetryFact] public void infinite_grpc_timeouts() { var result = EventStoreClientSettings.Create("esdb://localhost:2113?keepAliveInterval=-1&keepAliveTimeout=-1"); @@ -200,7 +200,7 @@ public void infinite_grpc_timeouts() { #endif } - [Fact] + [RetryFact] public void connection_string_with_no_schema() => Assert.Throws(() => EventStoreClientSettings.Create(":so/mething/random")); [Theory] @@ -271,7 +271,7 @@ public void connection_string_with_duplicate_key_should_throw(string connectionS public void connection_string_with_invalid_settings_should_throw(string connectionString) => Assert.Throws(() => EventStoreClientSettings.Create(connectionString)); - [Fact] + [RetryFact] public void with_default_settings() { var settings = EventStoreClientSettings.Create("esdb://hostname:4321/"); diff --git a/test/EventStore.Client.Tests/EventStoreClientOperationOptionsTests.cs b/test/EventStore.Client.Tests/EventStoreClientOperationsTests.cs similarity index 89% rename from test/EventStore.Client.Tests/EventStoreClientOperationOptionsTests.cs rename to test/EventStore.Client.Tests/EventStoreClientOperationsTests.cs index baf9bf5b3..031a4bfd6 100644 --- a/test/EventStore.Client.Tests/EventStoreClientOperationOptionsTests.cs +++ b/test/EventStore.Client.Tests/EventStoreClientOperationsTests.cs @@ -1,7 +1,7 @@ -namespace EventStore.Client.Tests; +namespace EventStore.Client.Tests; public class EventStoreClientOperationOptionsTests { - [Fact] + [RetryFact] public void setting_options_on_clone_should_not_modify_original() { var options = EventStoreClientOperationOptions.Default; @@ -11,4 +11,4 @@ public void setting_options_on_clone_should_not_modify_original() { Assert.Equal(options.BatchAppendSize, EventStoreClientOperationOptions.Default.BatchAppendSize); Assert.Equal(int.MaxValue, clonedOptions.BatchAppendSize); } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Tests/FromAllTests.cs b/test/EventStore.Client.Tests/FromAllTests.cs index 82b8338ec..ae00af5dc 100644 --- a/test/EventStore.Client.Tests/FromAllTests.cs +++ b/test/EventStore.Client.Tests/FromAllTests.cs @@ -5,7 +5,7 @@ namespace EventStore.Client.Tests; public class FromAllTests : ValueObjectTests { public FromAllTests() : base(new ScenarioFixture()) { } - [Fact] + [RetryFact] public void IsComparable() => Assert.IsAssignableFrom>(_fixture.Create()); [Theory] @@ -28,10 +28,10 @@ public FromAllTests() : base(new ScenarioFixture()) { } [MemberData(nameof(ToStringCases))] public void ToStringReturnsExpectedResult(FromAll sut, string expected) => Assert.Equal(expected, sut.ToString()); - [Fact] + [RetryFact] public void AfterLiveThrows() => Assert.Throws(() => FromAll.After(Position.End)); - [Fact] + [RetryFact] public void ToUInt64ReturnsExpectedResults() { var position = _fixture.Create(); Assert.Equal( @@ -46,4 +46,4 @@ public ScenarioFixture() { Customize(composter => composter.FromFactory(FromAll.After)); } } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Tests/FromStreamTests.cs b/test/EventStore.Client.Tests/FromStreamTests.cs index a293aa44d..e6e1c08f8 100644 --- a/test/EventStore.Client.Tests/FromStreamTests.cs +++ b/test/EventStore.Client.Tests/FromStreamTests.cs @@ -5,7 +5,7 @@ namespace EventStore.Client.Tests; public class FromStreamTests : ValueObjectTests { public FromStreamTests() : base(new ScenarioFixture()) { } - [Fact] + [RetryFact] public void IsComparable() => Assert.IsAssignableFrom>(_fixture.Create()); [Theory] @@ -19,19 +19,19 @@ public FromStreamTests() : base(new ScenarioFixture()) { } public static IEnumerable ToStringCases() { var fixture = new ScenarioFixture(); var position = fixture.Create(); - yield return new object?[] { FromStream.After(position), position.ToString() }; - yield return new object?[] { FromStream.Start, "Start" }; - yield return new object?[] { FromStream.End, "Live" }; + yield return [FromStream.After(position), position.ToString()]; + yield return [FromStream.Start, "Start"]; + yield return [FromStream.End, "Live"]; } [Theory] [MemberData(nameof(ToStringCases))] public void ToStringReturnsExpectedResult(FromStream sut, string expected) => Assert.Equal(expected, sut.ToString()); - [Fact] + [RetryFact] public void AfterLiveThrows() => Assert.Throws(() => FromStream.After(StreamPosition.End)); - [Fact] + [RetryFact] public void ToUInt64ReturnsExpectedResults() { var position = _fixture.Create(); Assert.Equal(position.ToUInt64(), FromStream.After(position).ToUInt64()); @@ -43,4 +43,4 @@ public ScenarioFixture() { Customize(composter => composter.FromFactory(FromStream.After)); } } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Tests/GossipChannelSelectorTests.cs b/test/EventStore.Client.Tests/GossipChannelSelectorTests.cs index 5967f7384..00cdc0e0f 100644 --- a/test/EventStore.Client.Tests/GossipChannelSelectorTests.cs +++ b/test/EventStore.Client.Tests/GossipChannelSelectorTests.cs @@ -4,7 +4,7 @@ namespace EventStore.Client.Tests; public class GossipChannelSelectorTests { - [Fact] + [RetryFact] public async Task ExplicitlySettingEndPointChangesChannels() { var firstId = Uuid.NewUuid(); var secondId = Uuid.NewUuid(); @@ -53,7 +53,7 @@ public async Task ExplicitlySettingEndPointChangesChannels() { Assert.Equal($"{secondSelection.Host}:{secondSelection.Port}", channel.Target); } - [Fact] + [RetryFact] public async Task ThrowsWhenDiscoveryFails() { var settings = new EventStoreClientSettings { ConnectivitySettings = { @@ -93,4 +93,4 @@ CancellationToken cancellationToken ) => throw new NotSupportedException(); } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Tests/GrpcServerCapabilitiesClientTests.cs b/test/EventStore.Client.Tests/GrpcServerCapabilitiesClientTests.cs index 258f51cd6..b1b5014df 100644 --- a/test/EventStore.Client.Tests/GrpcServerCapabilitiesClientTests.cs +++ b/test/EventStore.Client.Tests/GrpcServerCapabilitiesClientTests.cs @@ -99,4 +99,4 @@ // public override Task GetSupportedMethods(Empty request, ServerCallContext context) => Task.FromResult(_supportedMethods); // } // } -// #endif \ No newline at end of file +// #endif diff --git a/test/EventStore.Client.Tests/Interceptors/ReportLeaderInterceptorTests.cs b/test/EventStore.Client.Tests/Interceptors/ReportLeaderInterceptorTests.cs deleted file mode 100644 index 5703c3bd7..000000000 --- a/test/EventStore.Client.Tests/Interceptors/ReportLeaderInterceptorTests.cs +++ /dev/null @@ -1,224 +0,0 @@ -using EventStore.Client.Interceptors; -using Grpc.Core; -using Grpc.Core.Interceptors; - -namespace EventStore.Client.Tests.Interceptors; - -public class ReportLeaderInterceptorTests { - public delegate Task GrpcCall(Interceptor interceptor, Task? response = null); - - static readonly StatusCode[] ForcesRediscoveryStatusCodes = { - //StatusCode.Unknown, TODO: use RPC exceptions on server - StatusCode.Unavailable - }; - - static readonly Marshaller Marshaller = new(_ => Array.Empty(), _ => new()); - - static IEnumerable GrpcCalls() { - yield return MakeUnaryCall; - yield return MakeClientStreamingCall; - yield return MakeDuplexStreamingCall; - yield return MakeServerStreamingCall; - yield return MakeClientStreamingCallForWriting; - yield return MakeDuplexStreamingCallForWriting; - } - - public static IEnumerable ReportsNewLeaderCases() => GrpcCalls().Select(call => new object[] { call }); - - [Theory] - [MemberData(nameof(ReportsNewLeaderCases))] - public async Task ReportsNewLeader(GrpcCall call) { - ReconnectionRequired? actual = default; - - var sut = new ReportLeaderInterceptor(result => actual = result); - - var result = await Assert.ThrowsAsync(() => call(sut, Task.FromException(new NotLeaderException("a.host", 2112)))); - - Assert.Equal(new ReconnectionRequired.NewLeader(result.LeaderEndpoint), actual); - } - - public static IEnumerable ForcesRediscoveryCases() => - from call in GrpcCalls() - from statusCode in ForcesRediscoveryStatusCodes - select new object[] { call, statusCode }; - - [Theory] - [MemberData(nameof(ForcesRediscoveryCases))] - public async Task ForcesRediscovery(GrpcCall call, StatusCode statusCode) { - ReconnectionRequired? actual = default; - - var sut = new ReportLeaderInterceptor(result => actual = result); - - await Assert.ThrowsAsync(() => call(sut, Task.FromException(new RpcException(new(statusCode, "oops"))))); - - Assert.Equal(ReconnectionRequired.Rediscover.Instance, actual); - } - - public static IEnumerable DoesNotForceRediscoveryCases() => - from call in GrpcCalls() - from statusCode in Enum.GetValues(typeof(StatusCode)) - .OfType() - .Except(ForcesRediscoveryStatusCodes) - select new object[] { call, statusCode }; - - [Theory] - [MemberData(nameof(DoesNotForceRediscoveryCases))] - public async Task DoesNotForceRediscovery(GrpcCall call, StatusCode statusCode) { - ReconnectionRequired actual = ReconnectionRequired.None.Instance; - - var sut = new ReportLeaderInterceptor(result => actual = result); - - await Assert.ThrowsAsync(() => call(sut, Task.FromException(new RpcException(new(statusCode, "oops"))))); - - Assert.Equal(ReconnectionRequired.None.Instance, actual); - } - - static async Task MakeUnaryCall(Interceptor interceptor, Task? response = null) { - using var call = interceptor.AsyncUnaryCall( - new(), - CreateClientInterceptorContext(MethodType.Unary), - (_, context) => new( - response ?? Task.FromResult(new object()), - Task.FromResult(context.Options.Headers!), - GetSuccess, - GetTrailers, - OnDispose - ) - ); - - await call.ResponseAsync; - } - - static async Task MakeClientStreamingCall(Interceptor interceptor, Task? response = null) { - using var call = interceptor.AsyncClientStreamingCall( - CreateClientInterceptorContext(MethodType.ClientStreaming), - context => new( - null!, - response ?? Task.FromResult(new object()), - Task.FromResult(context.Options.Headers!), - GetSuccess, - GetTrailers, - OnDispose - ) - ); - - await call.ResponseAsync; - } - - static async Task MakeServerStreamingCall(Interceptor interceptor, Task? response = null) { - using var call = interceptor.AsyncServerStreamingCall( - new(), - CreateClientInterceptorContext(MethodType.ServerStreaming), - (_, context) => new( - new TestAsyncStreamReader(response), - Task.FromResult(context.Options.Headers!), - GetSuccess, - GetTrailers, - OnDispose - ) - ); - - await call.ResponseStream.ReadAllAsync().ToArrayAsync(); - } - - static async Task MakeDuplexStreamingCall(Interceptor interceptor, Task? response = null) { - using var call = interceptor.AsyncDuplexStreamingCall( - CreateClientInterceptorContext(MethodType.ServerStreaming), - context => new( - null!, - new TestAsyncStreamReader(response), - Task.FromResult(context.Options.Headers!), - GetSuccess, - GetTrailers, - OnDispose - ) - ); - - await call.ResponseStream.ReadAllAsync().ToArrayAsync(); - } - - // we might write to the server before listening to its response. if that write fails because - // the server is down then we will never listen to its response, so the failed write should - // trigger rediscovery itself - static async Task MakeClientStreamingCallForWriting(Interceptor interceptor, Task? response = null) { - using var call = interceptor.AsyncClientStreamingCall( - CreateClientInterceptorContext(MethodType.ClientStreaming), - context => new( - new TestAsyncStreamWriter(response), - Task.FromResult(new object()), - Task.FromResult(context.Options.Headers!), - GetSuccess, - GetTrailers, - OnDispose - ) - ); - - await call.RequestStream.WriteAsync(new()); - } - - static async Task MakeDuplexStreamingCallForWriting(Interceptor interceptor, Task? response = null) { - using var call = interceptor.AsyncDuplexStreamingCall( - CreateClientInterceptorContext(MethodType.ServerStreaming), - _ => new( - new TestAsyncStreamWriter(response), - null!, - null!, - GetSuccess, - GetTrailers, - OnDispose - ) - ); - - await call.RequestStream.WriteAsync(new()); - } - - static Status GetSuccess() => Status.DefaultSuccess; - - static Metadata GetTrailers() => Metadata.Empty; - - static void OnDispose() { } - - static ClientInterceptorContext CreateClientInterceptorContext(MethodType methodType) => - new( - new( - methodType, - string.Empty, - string.Empty, - Marshaller, - Marshaller - ), - null, - new(new()) - ); - - class TestAsyncStreamReader : IAsyncStreamReader { - readonly Task _response; - - public TestAsyncStreamReader(Task? response = null) => _response = response ?? Task.FromResult(new object()); - - public Task MoveNext(CancellationToken cancellationToken) => - _response.IsFaulted - ? Task.FromException(_response.Exception!.GetBaseException()) - : Task.FromResult(false); - - public object Current => _response.Result; - } - - class TestAsyncStreamWriter : IClientStreamWriter { - readonly Task _response; - - public TestAsyncStreamWriter(Task? response = null) => _response = response ?? Task.FromResult(new object()); - - public WriteOptions? WriteOptions { - get => throw new NotImplementedException(); - set => throw new NotImplementedException(); - } - - public Task CompleteAsync() => throw new NotImplementedException(); - - public Task WriteAsync(object message) => - _response.IsFaulted - ? Task.FromException(_response.Exception!.GetBaseException()) - : Task.FromResult(false); - } -} \ No newline at end of file diff --git a/test/EventStore.Client.Tests/ListProjectionTests.cs b/test/EventStore.Client.Tests/ListProjectionTests.cs new file mode 100644 index 000000000..d5d673657 --- /dev/null +++ b/test/EventStore.Client.Tests/ListProjectionTests.cs @@ -0,0 +1,43 @@ +// ReSharper disable InconsistentNaming + +using EventStore.Client.Tests.TestNode; + +namespace EventStore.Client.Tests; + +public class ListProjectionTests(ITestOutputHelper output, ListProjectionTests.CustomFixture fixture) + : KurrentTemporaryTests(output, fixture) { + [Fact] + public async Task list_all_projections() { + var result = await Fixture.Projections.ListAllAsync(userCredentials: TestCredentials.Root) + .ToArrayAsync(); + + Assert.Equal(result.Select(x => x.Name).OrderBy(x => x), Names.OrderBy(x => x)); + } + + [Fact] + public async Task list_continuous_projections() { + var name = Fixture.GetProjectionName(); + + await Fixture.Projections.CreateContinuousAsync( + name, + "fromAll().when({$init: function (state, ev) {return {};}});", + userCredentials: TestCredentials.Root + ); + + var result = await Fixture.Projections.ListContinuousAsync(userCredentials: TestCredentials.Root) + .ToArrayAsync(); + + Assert.Equal( + result.Select(x => x.Name).OrderBy(x => x), + Names.Concat([name]).OrderBy(x => x) + ); + + Assert.True(result.All(x => x.Mode == "Continuous")); + } + + static readonly string[] Names = ["$streams", "$stream_by_category", "$by_category", "$by_event_type", "$by_correlation_id"]; + + public class CustomFixture : KurrentTemporaryFixture { + public CustomFixture() : base(x => x.RunProjections()) { } + } +} diff --git a/test/EventStore.Client.Tests/NodePreferenceComparerTests.cs b/test/EventStore.Client.Tests/NodePreferenceComparerTests.cs index 1866e3444..abbec81a1 100644 --- a/test/EventStore.Client.Tests/NodePreferenceComparerTests.cs +++ b/test/EventStore.Client.Tests/NodePreferenceComparerTests.cs @@ -56,4 +56,4 @@ internal void ReadOnlyReplicaTests(ClusterMessages.VNodeState expected, params C Assert.Equal(expected, actual); } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Tests/NodeSelectorTests.cs b/test/EventStore.Client.Tests/NodeSelectorTests.cs index 9815305cd..19855b362 100644 --- a/test/EventStore.Client.Tests/NodeSelectorTests.cs +++ b/test/EventStore.Client.Tests/NodeSelectorTests.cs @@ -60,7 +60,7 @@ DnsEndPoint allowedNode Assert.Equal(allowedNode.Port, selectedNode.Port); } - [Fact] + [RetryFact] public void DeadNodesAreNotConsidered() { var allowedNodeId = Uuid.NewUuid(); var allowedNode = new DnsEndPoint(allowedNodeId.ToString(), 2113); @@ -119,4 +119,4 @@ public void CanPrefer(NodePreference nodePreference, string expectedHost) { Assert.Equal(expectedHost, selectedNode.Host); } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Operations.Tests/AuthenticationTests.cs b/test/EventStore.Client.Tests/Operations/AuthenticationTests.cs similarity index 64% rename from test/EventStore.Client.Operations.Tests/AuthenticationTests.cs rename to test/EventStore.Client.Tests/Operations/AuthenticationTests.cs index 084a92989..b05bd46ba 100644 --- a/test/EventStore.Client.Operations.Tests/AuthenticationTests.cs +++ b/test/EventStore.Client.Tests/Operations/AuthenticationTests.cs @@ -1,18 +1,15 @@ -namespace EventStore.Client.Operations.Tests; +namespace EventStore.Client.Tests; -public class AuthenticationTests : IClassFixture { - public AuthenticationTests(ITestOutputHelper output, InsecureClientTestFixture fixture) => Fixture = fixture.With(x => x.CaptureTestRun(output)); - - InsecureClientTestFixture Fixture { get; } - +public class AuthenticationTests(ITestOutputHelper output, AuthenticationTests.CustomFixture fixture) + : EventStorePermanentTests(output, fixture) { public enum CredentialsCase { None, TestUser, RootUser } public static IEnumerable InvalidAuthenticationCases() { - yield return new object?[] { 2, CredentialsCase.TestUser, CredentialsCase.None }; - yield return new object?[] { 3, CredentialsCase.None, CredentialsCase.None }; - yield return new object?[] { 4, CredentialsCase.RootUser, CredentialsCase.TestUser }; - yield return new object?[] { 5, CredentialsCase.TestUser, CredentialsCase.TestUser }; - yield return new object?[] { 6, CredentialsCase.None, CredentialsCase.TestUser }; + yield return [2, CredentialsCase.TestUser, CredentialsCase.None]; + yield return [3, CredentialsCase.None, CredentialsCase.None]; + yield return [4, CredentialsCase.RootUser, CredentialsCase.TestUser]; + yield return [5, CredentialsCase.TestUser, CredentialsCase.TestUser]; + yield return [6, CredentialsCase.None, CredentialsCase.TestUser]; } [Theory] @@ -21,10 +18,10 @@ public async Task system_call_with_invalid_credentials(int caseNr, CredentialsCa await ExecuteTest(caseNr, defaultCredentials, actualCredentials, true); public static IEnumerable ValidAuthenticationCases() { - yield return new object?[] { 1, CredentialsCase.RootUser, CredentialsCase.None }; - yield return new object?[] { 7, CredentialsCase.RootUser, CredentialsCase.RootUser }; - yield return new object?[] { 8, CredentialsCase.TestUser, CredentialsCase.RootUser }; - yield return new object?[] { 9, CredentialsCase.None, CredentialsCase.RootUser }; + yield return [1, CredentialsCase.RootUser, CredentialsCase.None]; + yield return [7, CredentialsCase.RootUser, CredentialsCase.RootUser]; + yield return [8, CredentialsCase.TestUser, CredentialsCase.RootUser]; + yield return [9, CredentialsCase.None, CredentialsCase.RootUser]; } [Theory] @@ -34,7 +31,7 @@ public async Task system_call_with_valid_credentials(int caseNr, CredentialsCase async Task ExecuteTest(int caseNr, CredentialsCase defaultCredentials, CredentialsCase actualCredentials, bool shouldThrow) { var testUser = await Fixture.CreateTestUser(); - + var defaultUserCredentials = GetCredentials(defaultCredentials); var actualUserCredentials = GetCredentials(actualCredentials); @@ -64,4 +61,6 @@ await operations _ => throw new ArgumentOutOfRangeException(nameof(credentialsCase), credentialsCase, null) }; } -} \ No newline at end of file + + public class CustomFixture() : KurrentPermanentFixture(x => x.WithoutDefaultCredentials()); +} diff --git a/test/EventStore.Client.Operations.Tests/MergeIndexesTests.cs b/test/EventStore.Client.Tests/Operations/MergeIndexTests.cs similarity index 50% rename from test/EventStore.Client.Operations.Tests/MergeIndexesTests.cs rename to test/EventStore.Client.Tests/Operations/MergeIndexTests.cs index c4cf7bfb0..9da4fc434 100644 --- a/test/EventStore.Client.Operations.Tests/MergeIndexesTests.cs +++ b/test/EventStore.Client.Tests/Operations/MergeIndexTests.cs @@ -1,20 +1,18 @@ -namespace EventStore.Client.Operations.Tests; +namespace EventStore.Client.Tests; -public class MergeIndexesTests : IClassFixture { - public MergeIndexesTests(ITestOutputHelper output, InsecureClientTestFixture fixture) => - Fixture = fixture.With(x => x.CaptureTestRun(output)); - - InsecureClientTestFixture Fixture { get; } - - [Fact] +public class MergeIndexTests(ITestOutputHelper output, MergeIndexTests.CustomFixture fixture) + : EventStorePermanentTests(output, fixture) { + [RetryFact] public async Task merge_indexes_does_not_throw() => await Fixture.Operations .MergeIndexesAsync(userCredentials: TestCredentials.Root) .ShouldNotThrowAsync(); - [Fact] + [RetryFact] public async Task merge_indexes_without_credentials_throws() => await Fixture.Operations .MergeIndexesAsync() .ShouldThrowAsync(); -} \ No newline at end of file + + public class CustomFixture() : KurrentPermanentFixture(x => x.WithoutDefaultCredentials()); +} diff --git a/test/EventStore.Client.Tests/Operations/ResignNodeTests.cs b/test/EventStore.Client.Tests/Operations/ResignNodeTests.cs new file mode 100644 index 000000000..b707a41b5 --- /dev/null +++ b/test/EventStore.Client.Tests/Operations/ResignNodeTests.cs @@ -0,0 +1,21 @@ +using EventStore.Client.Tests.TestNode; +using EventStore.Client.Tests; + +namespace EventStore.Client.Tests.Operations; + +public class ResignNodeTests(ITestOutputHelper output, ResignNodeTests.CustomFixture fixture) + : KurrentTemporaryTests(output, fixture) { + [RetryFact] + public async Task resign_node_does_not_throw() => + await Fixture.Operations + .ResignNodeAsync(userCredentials: TestCredentials.Root) + .ShouldNotThrowAsync(); + + [RetryFact] + public async Task resign_node_without_credentials_throws() => + await Fixture.Operations + .ResignNodeAsync() + .ShouldThrowAsync(); + + public class CustomFixture() : KurrentTemporaryFixture(x => x.WithoutDefaultCredentials()); +} diff --git a/test/EventStore.Client.Tests/Operations/RestartPersistentSubscriptionsTests.cs b/test/EventStore.Client.Tests/Operations/RestartPersistentSubscriptionsTests.cs new file mode 100644 index 000000000..b14242f80 --- /dev/null +++ b/test/EventStore.Client.Tests/Operations/RestartPersistentSubscriptionsTests.cs @@ -0,0 +1,20 @@ +using EventStore.Client.Tests.TestNode; + +namespace EventStore.Client.Tests.Operations; + +public class RestartPersistentSubscriptionsTests(ITestOutputHelper output, RestartPersistentSubscriptionsTests.NoDefaultCredentialsFixture fixture) + : KurrentTemporaryTests(output, fixture) { + [RetryFact] + public async Task restart_persistent_subscriptions_does_not_throw() => + await Fixture.Operations + .RestartPersistentSubscriptions(userCredentials: TestCredentials.Root) + .ShouldNotThrowAsync(); + + [RetryFact] + public async Task restart_persistent_subscriptions_without_credentials_throws() => + await Fixture.Operations + .RestartPersistentSubscriptions() + .ShouldThrowAsync(); + + public class NoDefaultCredentialsFixture() : KurrentTemporaryFixture(x => x.WithoutDefaultCredentials()); +} diff --git a/test/EventStore.Client.Operations.Tests/ScavengeTests.cs b/test/EventStore.Client.Tests/Operations/ScavengeTests.cs similarity index 79% rename from test/EventStore.Client.Operations.Tests/ScavengeTests.cs rename to test/EventStore.Client.Tests/Operations/ScavengeTests.cs index 35c1b3a1d..39090938b 100644 --- a/test/EventStore.Client.Operations.Tests/ScavengeTests.cs +++ b/test/EventStore.Client.Tests/Operations/ScavengeTests.cs @@ -1,21 +1,18 @@ -namespace EventStore.Client.Operations.Tests; +using EventStore.Client.Tests.TestNode; +using EventStore.Client.Tests; -public class ScavengeTests : IClassFixture { - public class TestFixture() : EventStoreFixture(x => x.WithoutDefaultCredentials().RunInMemory(false)); - - public ScavengeTests(ITestOutputHelper output, TestFixture fixture) => - Fixture = fixture.With(x => x.CaptureTestRun(output)); +namespace EventStore.Client.Tests; - TestFixture Fixture { get; } - - [Fact] +public class ScavengeTests(ITestOutputHelper output, ScavengeTests.CustomFixture fixture) + : KurrentTemporaryTests(output, fixture) { + [RetryFact] public async Task start() { var result = await Fixture.Operations.StartScavengeAsync(userCredentials: TestCredentials.Root); - result.ShouldBe(DatabaseScavengeResult.Started(result.ScavengeId)); + result.ScavengeId.ShouldNotBeNullOrEmpty(); } - [Fact] + [RetryFact] public async Task start_without_credentials_throws() => await Fixture.Operations .StartScavengeAsync() @@ -56,10 +53,10 @@ public async Task stop() { stopResult.ShouldBe(DatabaseScavengeResult.Stopped(startResult.ScavengeId)); } - [Fact] + [RetryFact] public async Task stop_when_no_scavenge_is_running() { var scavengeId = Guid.NewGuid().ToString(); - + var ex = await Fixture.Operations .StopScavengeAsync(scavengeId, userCredentials: TestCredentials.Root) .ShouldThrowAsync(); @@ -67,9 +64,11 @@ public async Task stop_when_no_scavenge_is_running() { // ex.ScavengeId.ShouldBeNull(); // it is blowing up because of this } - [Fact] + [RetryFact] public async Task stop_without_credentials_throws() => await Fixture.Operations .StopScavengeAsync(Guid.NewGuid().ToString()) .ShouldThrowAsync(); -} \ No newline at end of file + + public class CustomFixture() : KurrentTemporaryFixture(x => x.WithoutDefaultCredentials()); +} diff --git a/test/EventStore.Client.Tests/Operations/ShutdownNodeAuthenticationTests.cs b/test/EventStore.Client.Tests/Operations/ShutdownNodeAuthenticationTests.cs new file mode 100644 index 000000000..da575c581 --- /dev/null +++ b/test/EventStore.Client.Tests/Operations/ShutdownNodeAuthenticationTests.cs @@ -0,0 +1,13 @@ +using EventStore.Client.Tests.TestNode; +using EventStore.Client.Tests; + +namespace EventStore.Client.Tests; + +public class ShutdownNodeAuthenticationTests(ITestOutputHelper output, ShutdownNodeAuthenticationTests.CustomFixture fixture) + : KurrentTemporaryTests(output, fixture) { + [RetryFact] + public async Task shutdown_without_credentials_throws() => + await Fixture.Operations.ShutdownAsync().ShouldThrowAsync(); + + public class CustomFixture() : KurrentTemporaryFixture(x => x.WithoutDefaultCredentials()); +} diff --git a/test/EventStore.Client.Tests/Operations/ShutdownNodeTests.cs b/test/EventStore.Client.Tests/Operations/ShutdownNodeTests.cs new file mode 100644 index 000000000..8680865d2 --- /dev/null +++ b/test/EventStore.Client.Tests/Operations/ShutdownNodeTests.cs @@ -0,0 +1,13 @@ +using EventStore.Client.Tests.TestNode; +using EventStore.Client.Tests; + +namespace EventStore.Client.Tests.Operations; + +public class ShutdownNodeTests(ITestOutputHelper output, ShutdownNodeTests.NoDefaultCredentialsFixture fixture) + : KurrentTemporaryTests(output, fixture) { + [RetryFact] + public async Task shutdown_does_not_throw() => + await Fixture.Operations.ShutdownAsync(userCredentials: TestCredentials.Root).ShouldNotThrowAsync(); + + public class NoDefaultCredentialsFixture() : KurrentTemporaryFixture(x => x.WithoutDefaultCredentials()); +} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/FilterTestCase.cs b/test/EventStore.Client.Tests/PersistentSubscriptions/FilterTestCases.cs similarity index 95% rename from test/EventStore.Client.PersistentSubscriptions.Tests/FilterTestCase.cs rename to test/EventStore.Client.Tests/PersistentSubscriptions/FilterTestCases.cs index 4787105a4..e7a2355e8 100644 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/FilterTestCase.cs +++ b/test/EventStore.Client.Tests/PersistentSubscriptions/FilterTestCases.cs @@ -1,6 +1,6 @@ using System.Reflection; -namespace EventStore.Client.PersistentSubscriptions.Tests; +namespace EventStore.Client.Tests.PersistentSubscriptions; public static class Filters { const string StreamNamePrefix = nameof(StreamNamePrefix); @@ -37,4 +37,4 @@ public static class Filters { public static (Func getFilter, Func prepareEvent) GetFilter(string name) => s_filters[name]; -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToAllConnectWithoutReadPermissionsTests.cs b/test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToAllConnectWithoutReadPermissionsTests.cs new file mode 100644 index 000000000..d31a6d3f3 --- /dev/null +++ b/test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToAllConnectWithoutReadPermissionsTests.cs @@ -0,0 +1,30 @@ +using EventStore.Client.Tests.TestNode; +using EventStore.Client.Tests; + +namespace EventStore.Client.Tests.PersistentSubscriptions; + +public class SubscribeToAllConnectWithoutReadPermissionsTests(ITestOutputHelper output, KurrentTemporaryFixture fixture) + : KurrentTemporaryTests(output, fixture) { + [RetryFact] + public async Task connect_to_existing_without_read_all_permissions() { + var group = Fixture.GetGroupName(); + var user = Fixture.GetUserCredentials(); + + await Fixture.Subscriptions.CreateToAllAsync(group, new(), userCredentials: TestCredentials.Root); + + await Fixture.Users.CreateUserWithRetry( + user.Username!, + user.Username!, + [], + user.Password!, + TestCredentials.Root + ); + + await Assert.ThrowsAsync( + async () => { + await using var subscription = Fixture.Subscriptions.SubscribeToAll(group, userCredentials: user); + await subscription.Messages.AnyAsync().AsTask().WithTimeout(); + } + ); + } +} diff --git a/test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToAllFilterTests.cs b/test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToAllFilterTests.cs new file mode 100644 index 000000000..b4607e03f --- /dev/null +++ b/test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToAllFilterTests.cs @@ -0,0 +1,129 @@ +using EventStore.Client.Tests.TestNode; +using EventStore.Client.Tests; + +namespace EventStore.Client.Tests.PersistentSubscriptions; + +public class SubscribeToAllFilterTests(ITestOutputHelper output, KurrentTemporaryFixture fixture) + : KurrentTemporaryTests(output, fixture) { + [RetryTheory] + [MemberData(nameof(FilterCases))] + public async Task happy_case_filtered_reads_all_existing_filtered_events(string filterName) { + var streamPrefix = $"{filterName}-{Fixture.GetStreamName()}"; + var group = Fixture.GetGroupName(); + var (getFilter, prepareEvent) = Filters.GetFilter(filterName); + var filter = getFilter(streamPrefix); + + await Fixture.Streams.AppendToStreamAsync( + Guid.NewGuid().ToString(), + StreamState.NoStream, + Fixture.CreateTestEvents(256) + ); + + await Fixture.Streams.SetStreamMetadataAsync( + SystemStreams.AllStream, + StreamState.Any, + new(acl: new(SystemRoles.All)), + userCredentials: TestCredentials.Root + ); + + var appearedEvents = new List(); + var events = Fixture.CreateTestEvents(20).Select(e => prepareEvent(streamPrefix, e)).ToArray(); + + foreach (var e in events) { + await Fixture.Streams.AppendToStreamAsync( + $"{streamPrefix}_{Guid.NewGuid():n}", + StreamState.NoStream, + [e] + ); + } + + await Fixture.Subscriptions.CreateToAllAsync( + group, + filter, + new(startFrom: Position.Start), + userCredentials: TestCredentials.Root + ); + + await using var subscription = Fixture.Subscriptions.SubscribeToAll(group, userCredentials: TestCredentials.Root); + + await subscription.Messages + .OfType() + .Take(events.Length) + .ForEachAwaitAsync( + async e => { + var (resolvedEvent, _) = e; + appearedEvents.Add(resolvedEvent.Event); + await subscription.Ack(resolvedEvent); + } + ) + .WithTimeout(); + + Assert.Equal(events.Select(x => x.EventId), appearedEvents.Select(x => x.EventId)); + } + + [RetryTheory] + [MemberData(nameof(FilterCases))] + public async Task happy_case_filtered_with_start_from_set(string filterName) { + var group = Fixture.GetGroupName(); + var streamPrefix = $"{filterName}-{Fixture.GetStreamName()}"; + var (getFilter, prepareEvent) = Filters.GetFilter(filterName); + var filter = getFilter(streamPrefix); + + var events = Fixture.CreateTestEvents(20).Select(e => prepareEvent(streamPrefix, e)).ToArray(); + var eventsToSkip = events.Take(10).ToArray(); + var eventsToCapture = events.Skip(10).ToArray(); + + IWriteResult? eventToCaptureResult = null; + + await Fixture.Streams.AppendToStreamAsync( + Guid.NewGuid().ToString(), + StreamState.NoStream, + Fixture.CreateTestEvents(256) + ); + + await Fixture.Streams.SetStreamMetadataAsync( + SystemStreams.AllStream, + StreamState.Any, + new(acl: new(SystemRoles.All)), + userCredentials: TestCredentials.Root + ); + + foreach (var e in eventsToSkip) { + await Fixture.Streams.AppendToStreamAsync( + $"{streamPrefix}_{Guid.NewGuid():n}", + StreamState.NoStream, + new[] { e } + ); + } + + foreach (var e in eventsToCapture) { + var result = await Fixture.Streams.AppendToStreamAsync( + $"{streamPrefix}_{Guid.NewGuid():n}", + StreamState.NoStream, + new[] { e } + ); + + eventToCaptureResult ??= result; + } + + await Fixture.Subscriptions.CreateToAllAsync( + group, + filter, + new(startFrom: eventToCaptureResult!.LogPosition), + userCredentials: TestCredentials.Root + ); + + await using var subscription = Fixture.Subscriptions.SubscribeToAll(group, userCredentials: TestCredentials.Root); + + var appearedEvents = await subscription.Messages.OfType() + .Take(10) + .Select(e => e.ResolvedEvent.Event) + .ToArrayAsync() + .AsTask() + .WithTimeout(); + + Assert.Equal(eventsToCapture.Select(x => x.EventId), appearedEvents.Select(x => x.EventId)); + } + + public static IEnumerable FilterCases() => Filters.All.Select(filter => new object[] { filter }); +} diff --git a/test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToAllGetInfoTests.cs b/test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToAllGetInfoTests.cs new file mode 100644 index 000000000..c9f11f171 --- /dev/null +++ b/test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToAllGetInfoTests.cs @@ -0,0 +1,101 @@ +// ReSharper disable InconsistentNaming + +using EventStore.Client.Tests.TestNode; +using EventStore.Client.Tests; + +namespace EventStore.Client.Tests.PersistentSubscriptions; + +public class SubscribeToAllGetInfoTests(SubscribeToAllGetInfoTests.CustomFixture fixture) + : IClassFixture { + static readonly PersistentSubscriptionSettings Settings = new( + resolveLinkTos: true, + startFrom: Position.Start, + extraStatistics: true, + messageTimeout: TimeSpan.FromSeconds(9), + maxRetryCount: 11, + liveBufferSize: 303, + readBatchSize: 30, + historyBufferSize: 909, + checkPointAfter: TimeSpan.FromSeconds(1), + checkPointLowerBound: 1, + checkPointUpperBound: 1, + maxSubscriberCount: 500, + consumerStrategyName: SystemConsumerStrategies.Pinned + ); + + [RetryFact] + public async Task throws_with_non_existing_subscription() { + var group = $"NonExisting-{fixture.GetGroupName()}"; + + await Assert.ThrowsAsync( + async () => await fixture.Subscriptions.GetInfoToAllAsync(group, userCredentials: TestCredentials.Root) + ); + } + + [RetryFact] + public async Task throws_with_no_credentials() { + var group = $"NonExisting-{fixture.GetGroupName()}"; + + await Assert.ThrowsAsync( + async () => + await fixture.Subscriptions.GetInfoToAllAsync(group) + ); + } + + [RetryFact] + public async Task throws_with_non_existing_user() { + var group = $"NonExisting-{fixture.GetGroupName()}"; + + await Assert.ThrowsAsync( + async () => + await fixture.Subscriptions.GetInfoToAllAsync(group, userCredentials: TestCredentials.TestBadUser) + ); + } + + [RetryFact] + public async Task returns_result_with_normal_user_credentials() { + var result = await fixture.Subscriptions.GetInfoToAllAsync(fixture.Group, userCredentials: TestCredentials.Root); + + Assert.Equal("$all", result.EventSource); + } + + public class CustomFixture : KurrentTemporaryFixture { + public string Group { get; } + + public CustomFixture() : base(x => x.WithoutDefaultCredentials()) { + Group = GetGroupName(); + + OnSetup += async () => { + await Subscriptions.CreateToAllAsync(Group, Settings, userCredentials: TestCredentials.Root); + + foreach (var eventData in CreateTestEvents(20)) { + await Streams.AppendToStreamAsync( + $"test-{Guid.NewGuid():n}", + StreamState.NoStream, + [eventData], + userCredentials: TestCredentials.Root + ); + } + + var counter = 0; + + await using var subscription = Subscriptions.SubscribeToAll(Group, userCredentials: TestCredentials.Root); + + var enumerator = subscription.Messages.GetAsyncEnumerator(); + + while (await enumerator.MoveNextAsync()) { + if (enumerator.Current is not PersistentSubscriptionMessage.Event (var resolvedEvent, _)) + continue; + + counter++; + + if (counter == 1) + await subscription.Nack(PersistentSubscriptionNakEventAction.Park, "Test", resolvedEvent); + + if (counter > 10) + return; + } + }; + } + }; +} diff --git a/test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToAllListWithIncorrectCredentialsTests.cs b/test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToAllListWithIncorrectCredentialsTests.cs new file mode 100644 index 000000000..6df8b9d7b --- /dev/null +++ b/test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToAllListWithIncorrectCredentialsTests.cs @@ -0,0 +1,63 @@ +using EventStore.Client.Tests.TestNode; +using EventStore.Client.Tests; + +namespace EventStore.Client.Tests.PersistentSubscriptions; + +public class SubscribeToAllListWithIncorrectCredentialsTests(ITestOutputHelper output, SubscribeToAllListWithIncorrectCredentialsTests.CustomFixture fixture) + : KurrentTemporaryTests(output, fixture) { + [RetryFact] + public async Task throws_with_no_credentials() { + var group = Fixture.GetGroupName(); + var stream = Fixture.GetStreamName(); + + const int streamSubscriptionCount = 4; + const int allStreamSubscriptionCount = 3; + + for (var i = 0; i < streamSubscriptionCount; i++) + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group + i, + new(), + userCredentials: TestCredentials.Root + ); + + for (var i = 0; i < allStreamSubscriptionCount; i++) + await Fixture.Subscriptions.CreateToAllAsync( + group + i, + new(), + userCredentials: TestCredentials.Root + ); + + await Assert.ThrowsAsync(async () => await Fixture.Subscriptions.ListToAllAsync()); + } + + [RetryFact] + public async Task throws_with_non_existing_user() { + var group = Fixture.GetGroupName(); + var stream = Fixture.GetStreamName(); + + const int streamSubscriptionCount = 4; + const int allStreamSubscriptionCount = 3; + + for (var i = 0; i < streamSubscriptionCount; i++) + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group + i, + new(), + userCredentials: TestCredentials.Root + ); + + for (var i = 0; i < allStreamSubscriptionCount; i++) + await Fixture.Subscriptions.CreateToAllAsync( + group + i, + new(), + userCredentials: TestCredentials.Root + ); + + await Assert.ThrowsAsync( + async () => await Fixture.Subscriptions.ListToAllAsync(userCredentials: TestCredentials.TestBadUser) + ); + } + + public class CustomFixture() : KurrentTemporaryFixture(x => x.WithoutDefaultCredentials()); +} diff --git a/test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToAllNoDefaultCredentialsTests.cs b/test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToAllNoDefaultCredentialsTests.cs new file mode 100644 index 000000000..d7143d1d2 --- /dev/null +++ b/test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToAllNoDefaultCredentialsTests.cs @@ -0,0 +1,93 @@ +namespace EventStore.Client.Tests.PersistentSubscriptions; + +public class SubscribeToAllNoDefaultCredentialsTests(ITestOutputHelper output, SubscribeToAllNoDefaultCredentialsTests.CustomFixture fixture) + : EventStorePermanentTests(output, fixture) { + [RetryFact] + public async Task connect_to_existing_without_permissions() { + var group = Fixture.GetGroupName(); + + await Fixture.Subscriptions.CreateToAllAsync(group, new(), userCredentials: TestCredentials.Root); + + await Assert.ThrowsAsync( + async () => { + await using var subscription = Fixture.Subscriptions.SubscribeToAll(group); + await subscription.AnyAsync().AsTask().WithTimeout(); + } + ); + } + + [RetryFact] + public async Task throws_persistent_subscription_not_found() { + var group = Fixture.GetGroupName(); + + await using var subscription = Fixture.Subscriptions.SubscribeToAll(group, userCredentials: TestCredentials.Root); + + Assert.True( + await subscription.Messages.OfType().AnyAsync() + .AsTask() + .WithTimeout() + ); + } + + [RetryFact] + public async Task deleting_without_permissions() { + await Assert.ThrowsAsync(() => Fixture.Subscriptions.DeleteToAllAsync(Guid.NewGuid().ToString())); + } + + [RetryFact] + public async Task create_without_permissions() { + var group = Fixture.GetGroupName(); + + await Assert.ThrowsAsync( + () => + Fixture.Subscriptions.CreateToAllAsync( + group, + new() + ) + ); + } + + [RetryFact] + public async Task update_existing_without_permissions() { + var group = Fixture.GetGroupName(); + await Fixture.Subscriptions.CreateToAllAsync( + group, + new(), + userCredentials: TestCredentials.Root + ); + + await Assert.ThrowsAsync( + () => Fixture.Subscriptions.UpdateToAllAsync( + group, + new() + ) + ); + } + + [RetryFact] + public async Task update_non_existent() { + var group = Fixture.GetGroupName(); + await Assert.ThrowsAsync( + () => Fixture.Subscriptions.UpdateToAllAsync( + group, + new(), + userCredentials: TestCredentials.Root + ) + ); + } + + [RetryFact] + public Task update_with_prepare_position_larger_than_commit_position() { + var group = Fixture.GetGroupName(); + return Assert.ThrowsAsync( + () => + Fixture.Subscriptions.UpdateToAllAsync( + group, + new(startFrom: new Position(0, 1)), + userCredentials: TestCredentials.Root + ) + ); + } + + public class CustomFixture() : KurrentPermanentFixture(x => x.WithoutDefaultCredentials()); +} diff --git a/test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToAllReplayParkedTests.cs b/test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToAllReplayParkedTests.cs new file mode 100644 index 000000000..ca608d092 --- /dev/null +++ b/test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToAllReplayParkedTests.cs @@ -0,0 +1,84 @@ +namespace EventStore.Client.Tests.PersistentSubscriptions; + +public class SubscribeToAllReplayParkedTests(ITestOutputHelper output, SubscribeToAllReplayParkedTests.CustomFixture fixture) + : EventStorePermanentTests(output, fixture) { + [RetryFact] + public async Task does_not_throw() { + var group = Fixture.GetGroupName(); + + await Fixture.Subscriptions.CreateToAllAsync(group, new(), userCredentials: TestCredentials.Root); + + await Fixture.Subscriptions.ReplayParkedMessagesToAllAsync( + group, + userCredentials: TestCredentials.Root + ); + + await Fixture.Subscriptions.ReplayParkedMessagesToAllAsync( + group, + 100, + userCredentials: TestCredentials.Root + ); + } + + [RetryFact] + public async Task throws_when_given_non_existing_subscription() { + var group = Fixture.GetGroupName(); + + await Fixture.Subscriptions.CreateToAllAsync(group, new(), userCredentials: TestCredentials.Root); + + var nonExistingGroup = Fixture.GetGroupName(); + await Assert.ThrowsAsync( + () => + Fixture.Subscriptions.ReplayParkedMessagesToAllAsync( + nonExistingGroup, + userCredentials: TestCredentials.Root + ) + ); + } + + [RetryFact] + public async Task throws_with_no_credentials() { + var group = Fixture.GetGroupName(); + + await Fixture.Subscriptions.CreateToAllAsync(group, new(), userCredentials: TestCredentials.Root); + + await Assert.ThrowsAsync( + () => + Fixture.Subscriptions.ReplayParkedMessagesToAllAsync(group) + ); + } + + [RetryFact] + public async Task throws_with_non_existing_user() { + var group = Fixture.GetGroupName(); + + await Fixture.Subscriptions.CreateToAllAsync(group, new(), userCredentials: TestCredentials.Root); + + await Assert.ThrowsAsync( + () => + Fixture.Subscriptions.ReplayParkedMessagesToAllAsync( + group, + userCredentials: TestCredentials.TestBadUser + ) + ); + } + + [RetryFact] + public async Task throws_with_normal_user_credentials() { + var user = Fixture.GetUserCredentials(); + + await Fixture.Users + .CreateUserWithRetry(user.Username!, user.Username!, [], user.Password!, TestCredentials.Root) + .WithTimeout(); + + await Assert.ThrowsAsync( + () => + Fixture.Subscriptions.ReplayParkedMessagesToAllAsync( + Fixture.GetGroupName(), + userCredentials: user + ) + ); + } + + public class CustomFixture() : KurrentPermanentFixture(x => x.WithoutDefaultCredentials()); +} diff --git a/test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToAllResultWithNormalUserCredentialsTests.cs b/test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToAllResultWithNormalUserCredentialsTests.cs new file mode 100644 index 000000000..906bbde8f --- /dev/null +++ b/test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToAllResultWithNormalUserCredentialsTests.cs @@ -0,0 +1,34 @@ +using EventStore.Client.Tests.TestNode; +using EventStore.Client.Tests; + +namespace EventStore.Client.Tests.PersistentSubscriptions; + +public class SubscribeToAllResultWithNormalUserCredentialsTests(ITestOutputHelper output, KurrentTemporaryFixture fixture) + : KurrentTemporaryTests(output, fixture) { + [RetryFact] + public async Task returns_result_with_normal_user_credentials() { + var group = Fixture.GetGroupName(); + var stream = Fixture.GetStreamName(); + + const int streamSubscriptionCount = 4; + const int allStreamSubscriptionCount = 3; + + for (var i = 0; i < streamSubscriptionCount; i++) + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group + i, + new(), + userCredentials: TestCredentials.Root + ); + + for (var i = 0; i < allStreamSubscriptionCount; i++) + await Fixture.Subscriptions.CreateToAllAsync( + group + i, + new(), + userCredentials: TestCredentials.Root + ); + + var result = await Fixture.Subscriptions.ListToAllAsync(userCredentials: TestCredentials.Root); + Assert.Equal(allStreamSubscriptionCount, result.Count()); + } +} diff --git a/test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToAllReturnsAllSubscriptions.cs b/test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToAllReturnsAllSubscriptions.cs new file mode 100644 index 000000000..e7b542da9 --- /dev/null +++ b/test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToAllReturnsAllSubscriptions.cs @@ -0,0 +1,41 @@ +using EventStore.Client.Tests.TestNode; +using EventStore.Client.Tests; + +namespace EventStore.Client.Tests.PersistentSubscriptions; + +public class SubscribeToAllReturnsAllSubscriptions(ITestOutputHelper output, SubscribeToAllReturnsAllSubscriptions.CustomFixture fixture) + : KurrentTemporaryTests(output, fixture) { + [RetryFact] + public async Task returns_all_subscriptions() { + var group = Fixture.GetGroupName(); + var stream = Fixture.GetStreamName(); + + const int streamSubscriptionCount = 4; + const int allStreamSubscriptionCount = 3; + const int totalSubscriptionCount = streamSubscriptionCount + allStreamSubscriptionCount; + + for (var i = 0; i < streamSubscriptionCount; i++) + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group + i, + new(), + userCredentials: TestCredentials.Root + ); + + for (var i = 0; i < allStreamSubscriptionCount; i++) + await Fixture.Subscriptions.CreateToAllAsync( + group + i, + new(), + userCredentials: TestCredentials.Root + ); + + var result = (await Fixture.Subscriptions.ListAllAsync(userCredentials: TestCredentials.Root)).ToList(); + Assert.Equal(totalSubscriptionCount, result.Count); + } + + public class CustomFixture : KurrentTemporaryFixture { + public CustomFixture() { + SkipPsWarmUp = true; + } + } +} diff --git a/test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToAllReturnsSubscriptionsToAllStreamTests.cs b/test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToAllReturnsSubscriptionsToAllStreamTests.cs new file mode 100644 index 000000000..9a5e9c2a1 --- /dev/null +++ b/test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToAllReturnsSubscriptionsToAllStreamTests.cs @@ -0,0 +1,35 @@ +using EventStore.Client.Tests.TestNode; +using EventStore.Client.Tests; + +namespace EventStore.Client.Tests.PersistentSubscriptions; + +public class SubscribeToAllReturnsSubscriptionsToAllStreamTests(ITestOutputHelper output, KurrentTemporaryFixture fixture) + : KurrentTemporaryTests(output, fixture) { + [RetryFact] + public async Task returns_subscriptions_to_all_stream() { + var group = Fixture.GetGroupName(); + var stream = Fixture.GetStreamName(); + + const int streamSubscriptionCount = 4; + const int allStreamSubscriptionCount = 3; + + for (var i = 0; i < streamSubscriptionCount; i++) + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group + i, + new(), + userCredentials: TestCredentials.Root + ); + + for (var i = 0; i < allStreamSubscriptionCount; i++) + await Fixture.Subscriptions.CreateToAllAsync( + group + i, + new(), + userCredentials: TestCredentials.Root + ); + + var result = (await Fixture.Subscriptions.ListToAllAsync(userCredentials: TestCredentials.Root)).ToList(); + Assert.Equal(allStreamSubscriptionCount, result.Count); + Assert.All(result, s => Assert.Equal("$all", s.EventSource)); + } +} diff --git a/test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToAllTests.cs b/test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToAllTests.cs new file mode 100644 index 000000000..1b6e55834 --- /dev/null +++ b/test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToAllTests.cs @@ -0,0 +1,903 @@ +using System.Text; +using Grpc.Core; + +namespace EventStore.Client.Tests.PersistentSubscriptions; + +public class SubscribeToAllTests(ITestOutputHelper output, KurrentPermanentFixture fixture) + : EventStorePermanentTests(output, fixture) { + [RetryFact] + public async Task can_create_duplicate_name_on_different_streams() { + // Arrange + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + + // Act + await Fixture.Subscriptions.CreateToAllAsync( + group, + new(), + userCredentials: TestCredentials.Root + ); + + await Fixture.Subscriptions.CreateToStreamAsync(stream, group, new(), userCredentials: TestCredentials.Root); + } + + [RetryFact] + public async Task connect_to_existing_with_max_one_client() { + // Arrange + var group = Fixture.GetGroupName(); + + await Fixture.Subscriptions.CreateToAllAsync(group, new(maxSubscriberCount: 1), userCredentials: TestCredentials.Root); + + var ex = await Assert.ThrowsAsync(() => Task.WhenAll(Subscribe().WithTimeout(), Subscribe().WithTimeout())); + + Assert.Equal(SystemStreams.AllStream, ex.StreamName); + Assert.Equal(group, ex.GroupName); + return; + + async Task Subscribe() { + await using var subscription = Fixture.Subscriptions.SubscribeToAll(group, userCredentials: TestCredentials.Root); + await subscription.Messages.AnyAsync(); + } + } + + [RetryFact] + public async Task connect_to_existing_with_permissions() { + // Arrange + var group = Fixture.GetGroupName(); + await Fixture.Subscriptions.CreateToAllAsync(group, new(), userCredentials: TestCredentials.Root); + + // Act + await using var subscription = Fixture.Subscriptions.SubscribeToAll(group, userCredentials: TestCredentials.Root); + + // Assert + subscription.Messages + .FirstAsync().AsTask().WithTimeout() + .GetAwaiter().GetResult() + .ShouldBeOfType(); + } + + [RetryFact] + public async Task connect_to_existing_with_start_from_beginning() { + var group = Fixture.GetGroupName(); + + // append 10 events to random streams to make sure we have at least 10 events in the transaction file + foreach (var @event in Fixture.CreateTestEvents(10)) { + await Fixture.Streams.AppendToStreamAsync( + Guid.NewGuid().ToString(), + StreamState.NoStream, + [@event] + ); + } + + var events = await Fixture.Streams + .ReadAllAsync(Direction.Forwards, Position.Start, 10, userCredentials: TestCredentials.Root) + .ToArrayAsync(); + + await Fixture.Subscriptions.CreateToAllAsync(group, new(startFrom: Position.Start), userCredentials: TestCredentials.Root); + + var subscription = Fixture.Subscriptions.SubscribeToAll(group, userCredentials: TestCredentials.Root); + + var resolvedEvent = await subscription.Messages.OfType() + .Select(e => e.ResolvedEvent) + .FirstOrDefaultAsync().AsTask().WithTimeout(); + + Assert.Equal(events[0].Event.EventId, resolvedEvent.Event.EventId); + } + + [RetryFact] + public async Task connect_to_existing_with_start_from_not_set() { + var group = Fixture.GetGroupName(); + var stream = Fixture.GetStreamName(); + + foreach (var @event in Fixture.CreateTestEvents(10)) + await Fixture.Streams.AppendToStreamAsync( + stream, + StreamState.Any, + [@event] + ); + + await Fixture.Subscriptions.CreateToAllAsync(group, new(), userCredentials: TestCredentials.Root); + var subscription = Fixture.Subscriptions.SubscribeToAll(group, userCredentials: TestCredentials.Root); + + await Assert.ThrowsAsync( + () => subscription!.Messages + .OfType() + .Where(e => !SystemStreams.IsSystemStream(e.ResolvedEvent.OriginalStreamId)) + .AnyAsync() + .AsTask() + .WithTimeout(TimeSpan.FromMilliseconds(250)) + ); + } + + [RetryFact] + public async Task connect_to_existing_with_start_from_not_set_then_event_written() { + var group = Fixture.GetGroupName(); + var stream = Fixture.GetStreamName(); + + var expectedStreamId = Guid.NewGuid().ToString(); + var expectedEvent = Fixture.CreateTestEvents(1).First(); + + foreach (var @event in Fixture.CreateTestEvents(10)) { + await Fixture.Streams.AppendToStreamAsync( + stream, + StreamState.Any, + [@event] + ); + } + + await Fixture.Subscriptions.CreateToAllAsync(group, new(), userCredentials: TestCredentials.Root); + var subscription = Fixture.Subscriptions.SubscribeToAll(group, userCredentials: TestCredentials.Root); + + await Fixture.Streams.AppendToStreamAsync(expectedStreamId, StreamState.NoStream, [expectedEvent]); + + var resolvedEvent = await subscription!.Messages.OfType() + .Select(e => e.ResolvedEvent) + .Where(resolvedEvent => !SystemStreams.IsSystemStream(resolvedEvent.OriginalStreamId)) + .FirstOrDefaultAsync().AsTask().WithTimeout(); + + Assert.Equal(expectedEvent.EventId, resolvedEvent.Event.EventId); + Assert.Equal(expectedStreamId, resolvedEvent.Event.EventStreamId); + } + + [RetryFact] + public async Task connect_to_existing_with_start_from_set_to_end_position() { + var group = Fixture.GetGroupName(); + var stream = Fixture.GetStreamName(); + + foreach (var @event in Fixture.CreateTestEvents(10)) { + await Fixture.Streams.AppendToStreamAsync( + stream, + StreamState.Any, + [@event] + ); + } + + await Fixture.Subscriptions.CreateToAllAsync(group, new(startFrom: Position.End), userCredentials: TestCredentials.Root); + + var subscription = Fixture.Subscriptions.SubscribeToAll(group, userCredentials: TestCredentials.Root); + + await Assert.ThrowsAsync( + () => subscription.Messages + .OfType() + .Where(e => !SystemStreams.IsSystemStream(e.ResolvedEvent.OriginalStreamId)) + .AnyAsync() + .AsTask() + .WithTimeout(TimeSpan.FromMilliseconds(250)) + ); + } + + [RetryFact] + public async Task connect_to_existing_with_start_from_set_to_end_position_then_event_written() { + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + var expectedStreamId = Fixture.GetStreamName(); + var expectedEvent = Fixture.CreateTestEvents().First(); + + foreach (var @event in Fixture.CreateTestEvents(10)) { + await Fixture.Streams.AppendToStreamAsync( + stream, + StreamState.Any, + [@event] + ); + } + + await Fixture.Subscriptions.CreateToAllAsync(group, new(startFrom: Position.End), userCredentials: TestCredentials.Root); + var subscription = Fixture.Subscriptions.SubscribeToAll(group, userCredentials: TestCredentials.Root); + + await Fixture.Streams.AppendToStreamAsync(expectedStreamId, StreamState.NoStream, [expectedEvent]); + + var resolvedEvent = await subscription!.Messages + .OfType() + .Select(e => e.ResolvedEvent) + .Where(resolvedEvent => !SystemStreams.IsSystemStream(resolvedEvent.OriginalStreamId)) + .FirstAsync() + .AsTask() + .WithTimeout(); + + Assert.Equal(expectedEvent.EventId, resolvedEvent.Event.EventId); + Assert.Equal(expectedStreamId, resolvedEvent.Event.EventStreamId); + } + + [RetryFact] + public async Task connect_to_existing_with_start_from_set_to_invalid_middle_position() { + var group = Fixture.GetGroupName(); + + var invalidPosition = new Position(1L, 1L); + await Fixture.Subscriptions.CreateToAllAsync( + group, + new(startFrom: invalidPosition), + userCredentials: TestCredentials.Root + ); + + var subscription = Fixture.Subscriptions.SubscribeToAll(group, userCredentials: TestCredentials.Root); + var enumerator = subscription.Messages.GetAsyncEnumerator(); + + await enumerator.MoveNextAsync(); + var ex = await Assert.ThrowsAsync( + async () => + await enumerator!.MoveNextAsync() + ); + + Assert.Equal(SystemStreams.AllStream, ex.StreamName); + Assert.Equal(group, ex.GroupName); + } + + [RetryFact] + public async Task connect_to_existing_with_start_from_set_to_valid_middle_position() { + var group = Fixture.GetGroupName(); + + var events = await Fixture.Streams + .ReadAllAsync(Direction.Forwards, Position.Start, 10, userCredentials: TestCredentials.Root) + .ToArrayAsync(); + + var expectedEvent = events[events.Length / 2]; //just a random event in the middle of the results + + await Fixture.Subscriptions.CreateToAllAsync( + group, + new(startFrom: expectedEvent.OriginalPosition), + userCredentials: TestCredentials.Root + ); + + var subscription = Fixture.Subscriptions.SubscribeToAll(group, userCredentials: TestCredentials.Root); + + var resolvedEvent = await subscription!.Messages.OfType() + .Select(e => e.ResolvedEvent) + .FirstAsync() + .AsTask() + .WithTimeout(); + + Assert.Equal(expectedEvent.OriginalPosition, resolvedEvent.Event.Position); + Assert.Equal(expectedEvent.Event.EventId, resolvedEvent.Event.EventId); + Assert.Equal(expectedEvent.Event.EventStreamId, resolvedEvent.Event.EventStreamId); + } + + [RetryFact] + public async Task connect_with_retries() { + // Arrange + var group = Fixture.GetGroupName(); + await Fixture.Subscriptions.CreateToAllAsync(group, new(startFrom: Position.Start), userCredentials: TestCredentials.Root); + + // Act + var subscription = Fixture.Subscriptions.SubscribeToAll(group, userCredentials: TestCredentials.Root); + var retryCount = await subscription.Messages.OfType() + .SelectAwait( + async e => { + if (e.RetryCount > 4) { + await subscription.Ack(e.ResolvedEvent); + } else { + await subscription.Nack( + PersistentSubscriptionNakEventAction.Retry, + "Not yet tried enough times", + e.ResolvedEvent + ); + } + + return e.RetryCount; + } + ) + .Where(retryCount => retryCount > 4) + .FirstOrDefaultAsync() + .AsTask() + .WithTimeout(); + + // Assert + retryCount.ShouldBe(5); + } + + [RetryFact] + public async Task create_after_deleting_the_same() { + var group = Fixture.GetGroupName(); + + await Fixture.Subscriptions.CreateToAllAsync(group, new(), userCredentials: TestCredentials.Root); + await Fixture.Subscriptions.DeleteToAllAsync(group, userCredentials: TestCredentials.Root); + await Fixture.Subscriptions.CreateToAllAsync(group, new(), userCredentials: TestCredentials.Root); + } + + [RetryFact] + public async Task create_duplicate() { + var group = Fixture.GetGroupName(); + + await Fixture.Subscriptions.CreateToAllAsync( + group, + new(), + userCredentials: TestCredentials.Root + ); + + var ex = await Assert.ThrowsAsync( + () => Fixture.Subscriptions.CreateToAllAsync( + group, + new(), + userCredentials: TestCredentials.Root + ) + ); + + ex.StatusCode.ShouldBe(StatusCode.AlreadyExists); + } + + [RetryFact] + public async Task create_with_commit_position_equal_to_last_indexed_position() { + // Arrange + var group = Fixture.GetGroupName(); + + // Act + var lastEvent = await Fixture.Streams.ReadAllAsync( + Direction.Backwards, + Position.End, + 1, + userCredentials: TestCredentials.Root + ).FirstAsync(); + + var lastCommitPosition = lastEvent.OriginalPosition?.CommitPosition ?? throw new(); + await Fixture.Subscriptions.CreateToAllAsync( + group, + new(startFrom: new Position(lastCommitPosition, lastCommitPosition)), + userCredentials: TestCredentials.Root + ); + } + + [RetryFact] + public Task create_with_prepare_position_larger_than_commit_position() { + var group = Fixture.GetGroupName(); + + return Assert.ThrowsAsync( + () => + Fixture.Subscriptions.CreateToAllAsync( + group, + new(startFrom: new Position(0, 1)), + userCredentials: TestCredentials.Root + ) + ); + } + + [RetryFact] + public async Task deleting_existing_with_subscriber() { + var group = Fixture.GetGroupName(); + + await Fixture.Subscriptions.CreateToAllAsync(group, new(), userCredentials: TestCredentials.Root); + await Fixture.Subscriptions.DeleteToAllAsync(group, userCredentials: TestCredentials.Root); + + await using var subscription = Fixture.Subscriptions.SubscribeToAll(group, userCredentials: TestCredentials.Root); + + Assert.True( + await subscription.Messages.OfType().AnyAsync() + .AsTask() + .WithTimeout() + ); + } + + [RetryFact] + public async Task deleting_existing_with_permissions() { + var group = Fixture.GetGroupName(); + + await Fixture.Subscriptions.CreateToAllAsync( + group, + new(), + userCredentials: TestCredentials.Root + ); + + await Fixture.Subscriptions.DeleteToAllAsync( + group, + userCredentials: TestCredentials.Root + ); + } + + [RetryFact] + public async Task deleting_filtered() { + var group = Fixture.GetGroupName(); + + await Fixture.Subscriptions.CreateToAllAsync( + group, + EventTypeFilter.Prefix("prefix-filter-"), + new(), + userCredentials: TestCredentials.Root + ); + + await Fixture.Subscriptions.DeleteToAllAsync(group, userCredentials: TestCredentials.Root); + } + + [RetryFact] + public async Task deleting_nonexistent() { + await Assert.ThrowsAsync( + () => Fixture.Subscriptions.DeleteToAllAsync(Guid.NewGuid().ToString(), userCredentials: TestCredentials.Root) + ); + } + + [RetryFact] + public async Task happy_case_catching_up_to_link_to_events_manual_ack() { + var group = Fixture.GetGroupName(); + var bufferCount = 10; + var eventWriteCount = bufferCount * 2; + + var events = Fixture.CreateTestEvents(eventWriteCount) + .Select( + (e, i) => new EventData( + e.EventId, + SystemEventTypes.LinkTo, + Encoding.UTF8.GetBytes($"{i}@test"), + contentType: Constants.Metadata.ContentTypes.ApplicationOctetStream + ) + ) + .ToArray(); + + foreach (var e in events) { + await Fixture.Streams.AppendToStreamAsync("test-" + Guid.NewGuid(), StreamState.Any, new[] { e }); + } + + await Fixture.Subscriptions.CreateToAllAsync( + group, + new(startFrom: Position.Start, resolveLinkTos: true), + userCredentials: TestCredentials.Root + ); + + var subscription = Fixture.Subscriptions.SubscribeToAll(group, bufferSize: bufferCount, userCredentials: TestCredentials.Root); + await subscription.Messages.OfType() + .Take(events.Length) + .ForEachAwaitAsync(e => subscription.Ack(e.ResolvedEvent)) + .WithTimeout(); + } + + [RetryFact] + public async Task happy_case_catching_up_to_normal_events_manual_ack() { + var group = Fixture.GetGroupName(); + var stream = Fixture.GetStreamName(); + var bufferCount = 10; + var eventWriteCount = bufferCount * 2; + + var events = Fixture.CreateTestEvents(eventWriteCount).ToArray(); + + foreach (var e in events) + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.Any, [e]); + + await Fixture.Subscriptions.CreateToAllAsync( + group, + new(startFrom: Position.Start, resolveLinkTos: true), + userCredentials: TestCredentials.Root + ); + + var subscription = Fixture.Subscriptions.SubscribeToAll(group, bufferSize: bufferCount, userCredentials: TestCredentials.Root); + await subscription!.Messages.OfType() + .Take(events.Length) + .ForEachAwaitAsync(e => subscription.Ack(e.ResolvedEvent)) + .WithTimeout(); + } + + [RetryFact] + public async Task happy_case_writing_and_subscribing_to_normal_events_manual_ack() { + var group = Fixture.GetGroupName(); + var bufferCount = 10; + var eventWriteCount = bufferCount * 2; + + var events = Fixture.CreateTestEvents(eventWriteCount).ToArray(); + + await Fixture.Subscriptions.CreateToAllAsync( + group, + new(startFrom: Position.End, resolveLinkTos: true), + userCredentials: TestCredentials.Root + ); + + var subscription = Fixture.Subscriptions.SubscribeToAll(group, bufferSize: bufferCount, userCredentials: TestCredentials.Root); + foreach (var e in events) { + await Fixture.Streams.AppendToStreamAsync("test-" + Guid.NewGuid(), StreamState.Any, new[] { e }); + } + + await subscription!.Messages.OfType() + .SelectAwait( + async e => { + await subscription.Ack(e.ResolvedEvent); + return e; + } + ) + .Where(e => e.ResolvedEvent.OriginalStreamId.StartsWith("test-")) + .Take(events.Length) + .ToArrayAsync() + .AsTask() + .WithTimeout(); + } + + [RetryFact] + public async Task update_existing() { + var group = Fixture.GetGroupName(); + + await Fixture.Subscriptions.CreateToAllAsync( + group, + new(), + userCredentials: TestCredentials.Root + ); + + await Fixture.Subscriptions.UpdateToAllAsync( + group, + new(), + userCredentials: TestCredentials.Root + ); + } + + [RetryFact] + public async Task update_existing_filtered() { + var group = Fixture.GetGroupName(); + + await Fixture.Subscriptions.CreateToAllAsync( + group, + EventTypeFilter.Prefix("prefix-filter-"), + new(), + userCredentials: TestCredentials.Root + ); + + await Fixture.Subscriptions.UpdateToAllAsync( + group, + new(true), + userCredentials: TestCredentials.Root + ); + } + + [RetryFact] + public async Task update_existing_with_check_point() { + var group = Fixture.GetGroupName(); + var events = Fixture.CreateTestEvents(5).ToArray(); + var appearedEvents = new List(); + Position checkPoint = default; + + foreach (var e in events) + await Fixture.Streams.AppendToStreamAsync("test-" + Guid.NewGuid(), StreamState.Any, [e]); + + await Fixture.Subscriptions.CreateToAllAsync( + group, + new(checkPointLowerBound: 5, checkPointAfter: TimeSpan.FromSeconds(1), startFrom: Position.Start), + userCredentials: TestCredentials.Root + ); + + var subscription = Fixture.Subscriptions.SubscribeToAll(group, userCredentials: TestCredentials.Root); + var enumerator = subscription.Messages.GetAsyncEnumerator(); + await enumerator.MoveNextAsync(); + + await Task.WhenAll(Subscribe().WithTimeout(), WaitForCheckpoint().WithTimeout()); + + // Force restart of the subscription + await Fixture.Subscriptions.UpdateToAllAsync(group, new(), userCredentials: TestCredentials.Root); + + try { + while (await enumerator!.MoveNextAsync()) { } + } catch (PersistentSubscriptionDroppedByServerException) { } + + foreach (var e in events) { + await Fixture.Streams.AppendToStreamAsync("test-" + Guid.NewGuid(), StreamState.NoStream, new[] { e }); + } + + await using var sub = Fixture.Subscriptions.SubscribeToAll(group, userCredentials: TestCredentials.Root); + var resumed = await sub.Messages.OfType() + .Select(e => e.ResolvedEvent) + .Take(1) + .FirstAsync() + .AsTask() + .WithTimeout(); + + Assert.True(resumed.Event.Position > checkPoint); + + return; + + async Task Subscribe() { + while (await enumerator.MoveNextAsync()) { + if (enumerator.Current is not PersistentSubscriptionMessage.Event(var resolvedEvent, _)) { + continue; + } + + appearedEvents.Add(resolvedEvent); + await subscription.Ack(resolvedEvent); + if (appearedEvents.Count == events.Length) { + break; + } + } + } + + async Task WaitForCheckpoint() { + await using var subscription = Fixture.Streams.SubscribeToStream( + $"$persistentsubscription-$all::{group}-checkpoint", + FromStream.Start, + userCredentials: TestCredentials.Root + ); + + await foreach (var message in subscription.Messages) { + if (message is not StreamMessage.Event(var resolvedEvent)) { + continue; + } + + checkPoint = resolvedEvent.Event.Data.ParsePosition(); + return; + } + } + } + + [RetryFact] + public async Task update_existing_with_check_point_filtered() { + List appearedEvents = []; + var events = Fixture.CreateTestEvents(5).ToArray(); + var group = Fixture.GetGroupName(); + Position checkPoint = default; + + foreach (var e in events) { + await Fixture.Streams.AppendToStreamAsync("test-" + Guid.NewGuid(), StreamState.NoStream, [e]); + } + + await Fixture.Subscriptions.CreateToAllAsync( + group, + StreamFilter.Prefix("test"), + new(checkPointLowerBound: 5, checkPointAfter: TimeSpan.FromSeconds(1), startFrom: Position.Start), + userCredentials: TestCredentials.Root + ); + + var subscription = Fixture.Subscriptions.SubscribeToAll(group, userCredentials: TestCredentials.Root); + var enumerator = subscription.Messages.GetAsyncEnumerator(); + await enumerator.MoveNextAsync(); + + await Task.WhenAll(Subscribe().WithTimeout(), WaitForCheckpoint().WithTimeout()); + + // Force restart of the subscription + await Fixture.Subscriptions.UpdateToAllAsync(group, new(), userCredentials: TestCredentials.Root); + + try { + while (await enumerator.MoveNextAsync()) { } + } catch (PersistentSubscriptionDroppedByServerException) { } + + foreach (var e in events) { + await Fixture.Streams.AppendToStreamAsync("test-" + Guid.NewGuid(), StreamState.NoStream, [e]); + } + + await using var sub = Fixture.Subscriptions.SubscribeToAll(group, userCredentials: TestCredentials.Root); + var resumed = await sub.Messages.OfType() + .Select(e => e.ResolvedEvent) + .Take(1) + .FirstAsync() + .AsTask() + .WithTimeout(); + + Assert.True(resumed.Event.Position > checkPoint); + + return; + + async Task Subscribe() { + while (await enumerator.MoveNextAsync()) { + if (enumerator.Current is not PersistentSubscriptionMessage.Event(var resolvedEvent, _)) { + continue; + } + + appearedEvents.Add(resolvedEvent); + await subscription.Ack(resolvedEvent); + if (appearedEvents.Count == events.Length) { + break; + } + } + } + + async Task WaitForCheckpoint() { + await using var subscription = Fixture.Streams.SubscribeToStream( + $"$persistentsubscription-$all::{group}-checkpoint", + FromStream.Start, + userCredentials: TestCredentials.Root + ); + + await foreach (var message in subscription.Messages) { + if (message is not StreamMessage.Event(var resolvedEvent)) { + continue; + } + + checkPoint = resolvedEvent.Event.Data.ParsePosition(); + return; + } + } + } + + [RetryFact] + public async Task update_existing_with_commit_position_equal_to_last_indexed_position() { + var group = Fixture.GetGroupName(); + + await Fixture.Subscriptions.CreateToAllAsync( + group, + new(), + userCredentials: TestCredentials.Root + ); + + var lastEvent = await Fixture.Streams.ReadAllAsync( + Direction.Backwards, + Position.End, + 1, + userCredentials: TestCredentials.Root + ).FirstAsync(); + + var lastCommitPosition = lastEvent.OriginalPosition?.CommitPosition ?? throw new(); + await Fixture.Subscriptions.UpdateToAllAsync( + group, + new(startFrom: new Position(lastCommitPosition, lastCommitPosition)), + userCredentials: TestCredentials.Root + ); + } + + [RetryFact] + public async Task update_existing_with_subscribers() { + var group = Fixture.GetGroupName(); + + await Fixture.Subscriptions.CreateToAllAsync(group, new(startFrom: Position.Start), userCredentials: TestCredentials.Root); + + var subscription = Fixture.Subscriptions.SubscribeToAll(group, userCredentials: TestCredentials.Root); + + var enumerator = subscription.Messages.GetAsyncEnumerator(); + + await enumerator.MoveNextAsync(); + await Fixture.Subscriptions.UpdateToAllAsync(group, new(), userCredentials: TestCredentials.Root); + var ex = await Assert.ThrowsAsync( + async () => { + while (await enumerator.MoveNextAsync()) { } + } + ).WithTimeout(); + + Assert.Equal(SystemStreams.AllStream, ex.StreamName); + Assert.Equal(group, ex.GroupName); + } + + [RetryFact] + public async Task when_writing_and_filtering_out_events() { + var events = Fixture.CreateTestEvents(10).ToArray(); + var group = Fixture.GetGroupName(); + var prefix = Guid.NewGuid().ToString("N"); + + Position firstCheckPoint = default; + Position secondCheckPoint = default; + List appearedEvents = []; + + foreach (var e in events) + await Fixture.Streams.AppendToStreamAsync(prefix + Guid.NewGuid(), StreamState.Any, [e]); + + await Fixture.Subscriptions.CreateToAllAsync( + group, + StreamFilter.Prefix(prefix), + new( + checkPointLowerBound: 1, + checkPointUpperBound: 5, + checkPointAfter: TimeSpan.FromSeconds(1), + startFrom: Position.Start + ), + userCredentials: TestCredentials.Root + ); + + await using var subscription = Fixture.Subscriptions.SubscribeToAll(group, userCredentials: TestCredentials.Root); + await using var enumerator = subscription.Messages.GetAsyncEnumerator(); + + await Task.WhenAll(Subscribe().WithTimeout(), WaitForCheckpoints().WithTimeout()); + + foreach (var e in events) { + await Fixture.Streams.AppendToStreamAsync( + "filtered-out-stream-" + Guid.NewGuid(), + StreamState.Any, + [e] + ); + } + + Assert.True(secondCheckPoint > firstCheckPoint); + Assert.Equal(events.Select(e => e.EventId), appearedEvents.Select(e => e.Event.EventId)); + + return; + + async Task Subscribe() { + while (await enumerator.MoveNextAsync()) { + if (enumerator.Current is not PersistentSubscriptionMessage.Event(var resolvedEvent, _)) { + continue; + } + + appearedEvents.Add(resolvedEvent); + await subscription.Ack(resolvedEvent); + if (appearedEvents.Count == events.Length) { + break; + } + } + } + + async Task WaitForCheckpoints() { + bool firstCheckpointSet = false; + await using var subscription = Fixture.Streams.SubscribeToStream( + $"$persistentsubscription-$all::{group}-checkpoint", + FromStream.Start, + userCredentials: TestCredentials.Root + ); + + await foreach (var message in subscription.Messages) { + if (message is not StreamMessage.Event(var resolvedEvent)) { + continue; + } + + if (!firstCheckpointSet) { + firstCheckPoint = resolvedEvent.Event.Data.ParsePosition(); + firstCheckpointSet = true; + } else { + secondCheckPoint = resolvedEvent.Event.Data.ParsePosition(); + return; + } + } + } + } + + // [RetryFact] + // public async Task when_writing_and_subscribing_to_normal_events_manual_nack() { + // var group = Fixture.GetGroupName(); + // var bufferCount = 10; + // var eventWriteCount = bufferCount * 2; + // var prefix = $"{Guid.NewGuid():N}-"; + // + // var events = Fixture.CreateTestEvents(eventWriteCount).ToArray(); + // + // await Fixture.Subscriptions.CreateToAllAsync( + // group, + // new(startFrom: Position.Start, resolveLinkTos: true), + // userCredentials: TestCredentials.Root + // ); + // + // var subscription = Fixture.Subscriptions.SubscribeToAll(group, bufferSize: bufferCount, userCredentials: TestCredentials.Root); + // + // foreach (var e in events) + // await Fixture.Streams.AppendToStreamAsync(prefix + Guid.NewGuid(), StreamState.Any, [e]); + // + // await subscription.Messages.OfType() + // .SelectAwait( + // async e => { + // await subscription.Nack(PersistentSubscriptionNakEventAction.Park, "fail", e.ResolvedEvent); + // return e; + // } + // ) + // .Where(e => e.ResolvedEvent.OriginalStreamId.StartsWith(prefix)) + // .Take(events.Length) + // .ToArrayAsync() + // .AsTask() + // .WithTimeout(); + // } + + // [RetryFact] + // public async Task update_existing_with_commit_position_larger_than_last_indexed_position() { + // var group = Fixture.GetGroupName(); + // + // await Fixture.Subscriptions.CreateToAllAsync( + // group, + // new(), + // userCredentials: TestCredentials.Root + // ); + // + // var lastEvent = await Fixture.Streams.ReadAllAsync( + // Direction.Backwards, + // Position.End, + // 1, + // userCredentials: TestCredentials.Root + // ).FirstAsync(); + // + // var lastCommitPosition = lastEvent.OriginalPosition?.CommitPosition ?? throw new(); + // var ex = await Assert.ThrowsAsync( + // () => + // Fixture.Subscriptions.UpdateToAllAsync( + // group, + // new(startFrom: new Position(lastCommitPosition + 1, lastCommitPosition)), + // userCredentials: TestCredentials.Root + // ) + // ); + // + // Assert.Equal(StatusCode.Internal, ex.StatusCode); + // } + + [RetryFact] + public async Task create_with_commit_position_larger_than_last_indexed_position() { + var group = Fixture.GetGroupName(); + + var lastEvent = await Fixture.Streams.ReadAllAsync( + Direction.Backwards, + Position.End, + 1, + userCredentials: TestCredentials.Root + ).FirstAsync(); + + var lastCommitPosition = lastEvent.OriginalPosition?.CommitPosition ?? throw new(); + var ex = await Assert.ThrowsAsync( + () => + Fixture.Subscriptions.CreateToAllAsync( + group, + new(startFrom: new Position(lastCommitPosition + 1, lastCommitPosition)), + userCredentials: TestCredentials.Root + ) + ); + + Assert.Equal(StatusCode.Internal, ex.StatusCode); + } +} diff --git a/test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToAllUpdateExistingWithCheckpointTest.cs b/test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToAllUpdateExistingWithCheckpointTest.cs new file mode 100644 index 000000000..091d55583 --- /dev/null +++ b/test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToAllUpdateExistingWithCheckpointTest.cs @@ -0,0 +1,86 @@ +using EventStore.Client.Tests.TestNode; +using EventStore.Client.Tests; + +namespace EventStore.Client.Tests.PersistentSubscriptions; + +public class SubscribeToAllUpdateExistingWithCheckpointTest(ITestOutputHelper output, KurrentTemporaryFixture fixture) + : KurrentTemporaryTests(output, fixture) { + [RetryFact] + public async Task update_existing_with_check_point_should_resumes_from_check_point() { + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + + StreamPosition checkPoint = default; + + var events = Fixture.CreateTestEvents(5).ToArray(); + + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events); + + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group, + new(checkPointLowerBound: 5, checkPointAfter: TimeSpan.FromSeconds(1), startFrom: StreamPosition.Start), + userCredentials: TestCredentials.Root + ); + + await using var subscription = Fixture.Subscriptions.SubscribeToStream(stream, group, userCredentials: TestCredentials.Root); + + await using var enumerator = subscription.Messages.GetAsyncEnumerator(); + + await enumerator.MoveNextAsync(); + + await Task.WhenAll(Subscribe().WithTimeout(), WaitForCheckpoint().WithTimeout()); + + // Force restart of the subscription + await Fixture.Subscriptions.UpdateToStreamAsync(stream, group, new(), userCredentials: TestCredentials.Root); + + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.Any, Fixture.CreateTestEvents(1)); + + await using var sub = Fixture.Subscriptions.SubscribeToStream(stream, group, userCredentials: TestCredentials.Root); + + var resolvedEvent = await sub.Messages + .OfType() + .Select(e => e.ResolvedEvent) + .FirstAsync() + .AsTask() + .WithTimeout(); + + Assert.Equal(checkPoint.Next(), resolvedEvent.Event.EventNumber); + + return; + + async Task Subscribe() { + var count = 0; + + while (await enumerator.MoveNextAsync()) { + if (enumerator.Current is not PersistentSubscriptionMessage.Event(var resolvedEvent, _)) { + continue; + } + + count++; + + await subscription.Ack(resolvedEvent); + if (count >= events.Length) { + break; + } + } + } + + async Task WaitForCheckpoint() { + await using var subscription = Fixture.Streams.SubscribeToStream( + $"$persistentsubscription-{stream}::{group}-checkpoint", + FromStream.Start, + userCredentials: TestCredentials.Root + ); + + await foreach (var message in subscription.Messages) { + if (message is not StreamMessage.Event (var resolvedEvent)) { + continue; + } + + checkPoint = resolvedEvent.Event.Data.ParseStreamPosition(); + return; + } + } + } +} diff --git a/test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToAllWithoutPSTests.cs b/test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToAllWithoutPSTests.cs new file mode 100644 index 000000000..4afb8744e --- /dev/null +++ b/test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToAllWithoutPSTests.cs @@ -0,0 +1,15 @@ +using EventStore.Client.Tests.TestNode; +using EventStore.Client.Tests; + +namespace EventStore.Client.Tests.PersistentSubscriptions; + +public class SubscribeToAllWithoutPsTests(ITestOutputHelper output, KurrentTemporaryFixture fixture) + : KurrentTemporaryTests(output, fixture) { + [RetryFact] + public async Task list_without_persistent_subscriptions() { + await Assert.ThrowsAsync( + async () => + await Fixture.Subscriptions.ListToAllAsync(userCredentials: TestCredentials.Root) + ); + } +} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/get_info_obsolete.cs b/test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToStreamGetInfoTests.cs similarity index 52% rename from test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/get_info_obsolete.cs rename to test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToStreamGetInfoTests.cs index 6f42531fa..941ae6a9a 100644 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/get_info_obsolete.cs +++ b/test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToStreamGetInfoTests.cs @@ -1,51 +1,47 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll.Obsolete; +// ReSharper disable InconsistentNaming -[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class get_info_obsolete : IClassFixture { - const string GroupName = nameof(get_info_obsolete); +using EventStore.Client.Tests.TestNode; +using EventStore.Client.Tests; +namespace EventStore.Client.Tests.PersistentSubscriptions; + +public class SubscribeToStreamGetInfoTests(SubscribeToStreamGetInfoTests.NoDefaultCredentialsFixture fixture) + : IClassFixture { static readonly PersistentSubscriptionSettings Settings = new( - resolveLinkTos: true, - startFrom: Position.Start, - extraStatistics: true, - messageTimeout: TimeSpan.FromSeconds(9), - maxRetryCount: 11, - liveBufferSize: 303, - readBatchSize: 30, - historyBufferSize: 909, - checkPointAfter: TimeSpan.FromSeconds(1), - checkPointLowerBound: 1, - checkPointUpperBound: 1, - maxSubscriberCount: 500, - consumerStrategyName: SystemConsumerStrategies.Pinned + true, + StreamPosition.Start, + true, + TimeSpan.FromSeconds(9), + 11, + 303, + 30, + 909, + TimeSpan.FromSeconds(1), + 1, + 1, + 500, + SystemConsumerStrategies.RoundRobin ); - readonly Fixture _fixture; - - public get_info_obsolete(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task throws_when_not_supported() { - if (SupportsPSToAll.No) - await Assert.ThrowsAsync( - async () => { await _fixture.Client.GetInfoToAllAsync(GroupName, userCredentials: TestCredentials.Root); } - ); + public static IEnumerable AllowedUsers() { + yield return new object[] { TestCredentials.Root }; } - [SupportsPSToAll.Fact] - public async Task returns_expected_result() { - var result = await _fixture.Client.GetInfoToAllAsync(GroupName, userCredentials: TestCredentials.Root); - - Assert.Equal("$all", result.EventSource); - Assert.Equal(GroupName, result.GroupName); - Assert.Equal("Live", result.Status); + [Theory] + [MemberData(nameof(AllowedUsers))] + public async Task returns_expected_result(UserCredentials credentials) { + var result = await fixture.Subscriptions.GetInfoToStreamAsync(fixture.Stream, fixture.Group, userCredentials: credentials); + Assert.Equal(fixture.Stream, result.EventSource); + Assert.Equal(fixture.Group, result.GroupName); Assert.NotNull(Settings.StartFrom); Assert.True(result.Stats.TotalItems > 0); Assert.True(result.Stats.OutstandingMessagesCount > 0); Assert.True(result.Stats.AveragePerSecond >= 0); - Assert.True(result.Stats.ParkedMessageCount > 0); + Assert.True(result.Stats.ParkedMessageCount >= 0); Assert.True(result.Stats.AveragePerSecond >= 0); + Assert.True(result.Stats.ReadBufferCount >= 0); + Assert.True(result.Stats.RetryBufferCount >= 0); Assert.True(result.Stats.CountSinceLastMeasurement >= 0); Assert.True(result.Stats.TotalInFlightMessages >= 0); Assert.NotNull(result.Stats.LastKnownEventPosition); @@ -54,13 +50,12 @@ public async Task returns_expected_result() { Assert.NotNull(result.Connections); Assert.NotEmpty(result.Connections); - var connection = result.Connections.First(); Assert.NotNull(connection.From); Assert.Equal(TestCredentials.Root.Username, connection.Username); Assert.NotEmpty(connection.ConnectionName); Assert.True(connection.AverageItemsPerSecond >= 0); - Assert.True(connection.TotalItems > 0); + Assert.True(connection.TotalItems >= 0); Assert.True(connection.CountSinceLastMeasurement >= 0); Assert.True(connection.AvailableSlots >= 0); Assert.True(connection.InFlightMessages >= 0); @@ -98,86 +93,118 @@ public async Task returns_expected_result() { Assert.Equal(Settings.ConsumerStrategyName, result.Settings!.ConsumerStrategyName); } - [SupportsPSToAll.Fact] - public async Task throws_with_non_existing_subscription() => + [RetryFact] + public async Task throws_when_given_non_existing_subscription() => await Assert.ThrowsAsync( async () => { - await _fixture.Client.GetInfoToAllAsync( + await fixture.Subscriptions.GetInfoToStreamAsync( + "NonExisting", "NonExisting", userCredentials: TestCredentials.Root ); } ); - [SupportsPSToAll.Fact] - public async Task throws_with_no_credentials() => - await Assert.ThrowsAsync(async () => { await _fixture.Client.GetInfoToAllAsync("NonExisting"); }); + [Fact(Skip = "Unable to produce same behavior with HTTP fallback!")] + public async Task throws_with_non_existing_user() { + var group = $"NonExisting-{fixture.GetGroupName()}"; + var stream = $"NonExisting-{fixture.GetStreamName()}"; - [SupportsPSToAll.Fact] - public async Task throws_with_non_existing_user() => await Assert.ThrowsAsync( async () => { - await _fixture.Client.GetInfoToAllAsync( - "NonExisting", + await fixture.Subscriptions.GetInfoToStreamAsync( + stream, + group, userCredentials: TestCredentials.TestBadUser ); } ); + } - [SupportsPSToAll.Fact] - public async Task returns_result_with_normal_user_credentials() { - var result = await _fixture.Client.GetInfoToAllAsync( - GroupName, - userCredentials: TestCredentials.TestUser1 - ); + [RetryFact] + public async Task throws_with_no_credentials() { + var group = $"NonExisting-{fixture.GetGroupName()}"; + var stream = $"NonExisting-{fixture.GetStreamName()}"; - Assert.Equal("$all", result.EventSource); + await Assert.ThrowsAsync( + async () => { + await fixture.Subscriptions.GetInfoToStreamAsync( + stream, + group + ); + } + ); } - void AssertKeyAndValue(IDictionary items, string key) { - Assert.True(items.ContainsKey(key)); - Assert.True(items[key] > 0); + [RetryFact] + public async Task returns_result_for_normal_user() { + var result = await fixture.Subscriptions.GetInfoToStreamAsync( + fixture.Stream, + fixture.Group, + userCredentials: TestCredentials.Root + ); + + Assert.NotNull(result); } - public class Fixture : EventStoreClientFixture { - public Fixture() : base(noDefaultCredentials: true) { } + public class NoDefaultCredentialsFixture : KurrentTemporaryFixture { + public string Group { get; set; } + public string Stream { get; set; } - protected override async Task Given() { - if (SupportsPSToAll.No) - return; + EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult? Subscription; + IAsyncEnumerator? Enumerator; - await Client.CreateToAllAsync( - GroupName, - get_info_obsolete.Settings, - userCredentials: TestCredentials.Root - ); - } + public NoDefaultCredentialsFixture() : base(x => x.WithoutDefaultCredentials()) { + Group = GetGroupName(); + Stream = GetStreamName(); - protected override async Task When() { - if (SupportsPSToAll.No) - return; + OnSetup += async () => { + await Subscriptions.CreateToStreamAsync( + groupName: Group, + streamName: Stream, + settings: Settings, + userCredentials: TestCredentials.Root + ); - var counter = 0; - var tcs = new TaskCompletionSource(); + var counter = 0; + Subscription = Subscriptions.SubscribeToStream(Stream, Group, userCredentials: TestCredentials.Root); + Enumerator = Subscription.Messages.GetAsyncEnumerator(); + + for (var i = 0; i < 15; i++) { + await Streams.AppendToStreamAsync( + Stream, + StreamState.Any, + [new EventData(Uuid.NewUuid(), "test-event", ReadOnlyMemory.Empty)], + userCredentials: TestCredentials.Root + ); + } + + while (await Enumerator.MoveNextAsync()) { + if (Enumerator.Current is not PersistentSubscriptionMessage.Event(var resolvedEvent, _)) { + continue; + } - await Client.SubscribeToAllAsync( - GroupName, - (s, e, r, ct) => { counter++; - switch (counter) { - case 1: s.Nack(PersistentSubscriptionNakEventAction.Park, "Test", e); - break; - case > 10: - tcs.TrySetResult(); - break; + if (counter == 1) { + await Subscription.Nack(PersistentSubscriptionNakEventAction.Park, "Test", resolvedEvent); + } + + if (counter > 10) { + return; } - return Task.CompletedTask; - }, - userCredentials: TestCredentials.Root - ); + } + }; - await tcs.Task; + OnTearDown += async () => { + if (Enumerator is not null) await Enumerator.DisposeAsync(); + if (Subscription is not null) await Subscription.DisposeAsync(); + }; } + }; + + void AssertKeyAndValue(IDictionary items, string key) { + Assert.True(items.ContainsKey(key)); + Assert.True(items[key] > 0); } } diff --git a/test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToStreamListTests.cs b/test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToStreamListTests.cs new file mode 100644 index 000000000..bea57c9ab --- /dev/null +++ b/test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToStreamListTests.cs @@ -0,0 +1,44 @@ +namespace EventStore.Client.Tests.PersistentSubscriptions; + +public class SubscribeToStreamListTests(ITestOutputHelper output, SubscribeToStreamListTests.CustomFixture fixture) + : EventStorePermanentTests(output, fixture) { + [RetryFact] + public async Task throws_with_no_credentials() { + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + + const int streamSubscriptionCount = 4; + + for (var i = 0; i < streamSubscriptionCount; i++) + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group + i, + new(), + userCredentials: TestCredentials.Root + ); + + await Assert.ThrowsAsync(async () => await Fixture.Subscriptions.ListToStreamAsync(stream)); + } + + [RetryFact] + public async Task throws_with_non_existing_user() { + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + + const int streamSubscriptionCount = 4; + + for (var i = 0; i < streamSubscriptionCount; i++) + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group + i, + new(), + userCredentials: TestCredentials.Root + ); + + await Assert.ThrowsAsync( + async () => await Fixture.Subscriptions.ListToStreamAsync(stream, userCredentials: TestCredentials.TestBadUser) + ); + } + + public class CustomFixture() : KurrentPermanentFixture(x => x.WithoutDefaultCredentials()); +} diff --git a/test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToStreamNoDefaultCredentialsTests.cs b/test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToStreamNoDefaultCredentialsTests.cs new file mode 100644 index 000000000..3a473b58e --- /dev/null +++ b/test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToStreamNoDefaultCredentialsTests.cs @@ -0,0 +1,77 @@ +namespace EventStore.Client.Tests.PersistentSubscriptions; + +public class SubscribeToStreamNoDefaultCredentialsTests(ITestOutputHelper output, SubscribeToStreamNoDefaultCredentialsTests.CustomFixture fixture) + : EventStorePermanentTests(output, fixture) { + [RetryFact] + public async Task connect_to_existing_without_permissions() { + var group = Fixture.GetGroupName(); + var stream = Fixture.GetStreamName(); + + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group, + new(), + userCredentials: TestCredentials.Root + ); + + await Assert.ThrowsAsync( + async () => { + await using var subscription = Fixture.Subscriptions.SubscribeToStream(stream, group); + await subscription.Messages.AnyAsync(); + } + ).WithTimeout(); + } + + [RetryFact] + public async Task create_without_permissions() { + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + + await Assert.ThrowsAsync( + () => + Fixture.Subscriptions.CreateToStreamAsync( + stream, + group, + new() + ) + ); + } + + [RetryFact] + public async Task deleting_without_permissions() { + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + + await Assert.ThrowsAsync(() => Fixture.Subscriptions.DeleteToStreamAsync(stream, group)); + } + + [RetryFact] + public async Task update_existing_without_permissions() { + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + + await Fixture.Streams.AppendToStreamAsync( + stream, + StreamState.NoStream, + Fixture.CreateTestEvents(), + userCredentials: TestCredentials.Root + ); + + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group, + new(), + userCredentials: TestCredentials.Root + ); + + await Assert.ThrowsAsync( + () => Fixture.Subscriptions.UpdateToStreamAsync( + stream, + group, + new() + ) + ); + } + + public class CustomFixture() : KurrentPermanentFixture(x => x.WithoutDefaultCredentials()); +} diff --git a/test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToStreamReplayParkedTests.cs b/test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToStreamReplayParkedTests.cs new file mode 100644 index 000000000..384cc8dfb --- /dev/null +++ b/test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToStreamReplayParkedTests.cs @@ -0,0 +1,68 @@ +namespace EventStore.Client.Tests.PersistentSubscriptions; + +public class SubscribeToStreamReplayParkedTests(ITestOutputHelper output, SubscribeToStreamReplayParkedTests.CustomFixture fixture) + : EventStorePermanentTests(output, fixture) { + [RetryFact] + public async Task does_not_throw() { + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + + await Fixture.Subscriptions.CreateToStreamAsync(stream, group, new(), userCredentials: TestCredentials.Root); + + await Fixture.Subscriptions.ReplayParkedMessagesToStreamAsync( + stream, + group, + userCredentials: TestCredentials.Root + ); + + await Fixture.Subscriptions.ReplayParkedMessagesToStreamAsync( + stream, + group, + 100, + userCredentials: TestCredentials.Root + ); + } + + [RetryFact] + public async Task throws_with_no_credentials() { + var group = Fixture.GetGroupName(); + var stream = Fixture.GetStreamName(); + + await Fixture.Subscriptions.CreateToStreamAsync(stream, group, new(), userCredentials: TestCredentials.Root); + + await Assert.ThrowsAsync( + () => + Fixture.Subscriptions.ReplayParkedMessagesToStreamAsync(stream, group) + ); + } + + [Fact(Skip = "Unable to produce same behavior with HTTP fallback!")] + public async Task throws_with_non_existing_user() { + var group = Fixture.GetGroupName(); + var stream = Fixture.GetStreamName(); + + await Fixture.Subscriptions.CreateToStreamAsync(stream, group, new(), userCredentials: TestCredentials.Root); + + await Assert.ThrowsAsync( + () => Fixture.Subscriptions.ReplayParkedMessagesToStreamAsync(stream, group, userCredentials: TestCredentials.TestBadUser) + ); + } + + [RetryFact] + public async Task throws_with_normal_user_credentials() { + var group = Fixture.GetGroupName(); + var stream = Fixture.GetStreamName(); + var user = Fixture.GetUserCredentials(); + + await Fixture.Users + .CreateUserWithRetry(user.Username!, user.Username!, [], user.Password!, TestCredentials.Root) + .WithTimeout(); + + await Assert.ThrowsAsync( + () => + Fixture.Subscriptions.ReplayParkedMessagesToStreamAsync(stream, group, userCredentials: user) + ); + } + + public class CustomFixture() : KurrentPermanentFixture(x => x.WithoutDefaultCredentials()); +} diff --git a/test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToStreamTests.cs b/test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToStreamTests.cs new file mode 100644 index 000000000..f9cdb68d4 --- /dev/null +++ b/test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToStreamTests.cs @@ -0,0 +1,743 @@ +using System.Text; +using Grpc.Core; + +namespace EventStore.Client.Tests.PersistentSubscriptions; + +public class SubscribeToStreamTests(ITestOutputHelper output, KurrentPermanentFixture fixture) + : EventStorePermanentTests(output, fixture) { + [RetryFact] + public async Task can_create_duplicate_name_on_different_streams() { + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group, + new(), + userCredentials: TestCredentials.Root + ); + + await Fixture.Subscriptions.CreateToStreamAsync( + "someother" + stream, + group, + new(), + userCredentials: TestCredentials.Root + ); + } + + [RetryFact] + public async Task connect_to_existing_with_max_one_client() { + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group, + new(maxSubscriberCount: 1), + userCredentials: TestCredentials.Root + ); + + var ex = await Assert.ThrowsAsync(() => Task.WhenAll(Subscribe().WithTimeout(), Subscribe().WithTimeout())); + + Assert.Equal(stream, ex.StreamName); + Assert.Equal(group, ex.GroupName); + return; + + async Task Subscribe() { + await using var subscription = Fixture.Subscriptions.SubscribeToStream(stream, group, userCredentials: TestCredentials.Root); + await subscription.Messages.AnyAsync(); + } + } + + [RetryFact] + public async Task connect_to_existing_with_permissions() { + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + + await Fixture.Subscriptions.CreateToStreamAsync(stream, group, new(), userCredentials: TestCredentials.Root); + + await using var subscription = Fixture.Subscriptions.SubscribeToStream(stream, group, userCredentials: TestCredentials.Root); + + Assert.True(await subscription.Messages.FirstAsync().AsTask().WithTimeout() is PersistentSubscriptionMessage.SubscriptionConfirmation); + } + + [RetryFact] + public async Task connect_to_existing_with_start_from_beginning_and_events_in_it() { + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + var events = Fixture.CreateTestEvents(10).ToArray(); + + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events); + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group, + new(startFrom: StreamPosition.Start), + userCredentials: TestCredentials.Root + ); + + await using var subscription = Fixture.Subscriptions.SubscribeToStream(stream, group, userCredentials: TestCredentials.Root); + var resolvedEvent = await subscription.Messages.OfType() + .Select(e => e.ResolvedEvent) + .FirstOrDefaultAsync().AsTask().WithTimeout(); + + Assert.Equal(StreamPosition.Start, resolvedEvent.Event.EventNumber); + Assert.Equal(events[0].EventId, resolvedEvent.Event.EventId); + } + + [RetryFact] + public async Task connect_to_existing_with_start_from_beginning_and_no_stream() { + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + var events = Fixture.CreateTestEvents().ToArray(); + var eventId = events.Single().EventId; + + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group, + new(), + userCredentials: TestCredentials.Root + ); + + await using var subscription = Fixture.Subscriptions.SubscribeToStream(stream, group, userCredentials: TestCredentials.Root); + + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events); + + var resolvedEvent = await subscription!.Messages.OfType() + .Select(e => e.ResolvedEvent) + .FirstOrDefaultAsync().AsTask().WithTimeout(); + + Assert.Equal(StreamPosition.Start, resolvedEvent.Event.EventNumber); + Assert.Equal(eventId, resolvedEvent.Event.EventId); + } + + [RetryFact] + public async Task connect_to_existing_with_start_from_beginning_and_no_streamconnect_to_existing_with_start_from_not_set_and_events_in_it() { + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + var events = Fixture.CreateTestEvents(10).ToArray(); + + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events); + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group, + new(), + userCredentials: TestCredentials.Root + ); + + await using var subscription = Fixture.Subscriptions.SubscribeToStream( + stream, + group, + userCredentials: TestCredentials.Root + ); + + await Assert.ThrowsAsync( + () => subscription.Messages.AnyAsync(message => message is PersistentSubscriptionMessage.Event).AsTask().WithTimeout(TimeSpan.FromMilliseconds(250)) + ); + } + + [RetryFact] + public async Task connect_to_existing_with_start_from_not_set_and_events_in_it_then_event_written() { + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + var events = Fixture.CreateTestEvents(11).ToArray(); + + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events.Take(10)); + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group, + new(), + userCredentials: TestCredentials.Root + ); + + await using var subscription = Fixture.Subscriptions.SubscribeToStream( + stream, + group, + userCredentials: TestCredentials.Root + ); + + await Fixture.Streams.AppendToStreamAsync(stream, new StreamRevision(9), events.Skip(10)); + + var resolvedEvent = await subscription.Messages.OfType() + .Select(e => e.ResolvedEvent) + .FirstOrDefaultAsync().AsTask().WithTimeout(); + + Assert.Equal(new(10), resolvedEvent.Event.EventNumber); + Assert.Equal(events.Last().EventId, resolvedEvent.Event.EventId); + } + + [RetryFact] + public async Task connect_to_existing_with_start_from_set_to_end_position_and_events_in_it() { + var events = Fixture.CreateTestEvents(10).ToArray(); + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events); + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group, + new(startFrom: StreamPosition.End), + userCredentials: TestCredentials.Root + ); + + await using var subscription = Fixture.Subscriptions.SubscribeToStream( + stream, + group, + userCredentials: TestCredentials.Root + ); + + await Assert.ThrowsAsync( + () => subscription.Messages.AnyAsync(message => message is PersistentSubscriptionMessage.Event).AsTask().WithTimeout(TimeSpan.FromMilliseconds(250)) + ); + } + + [RetryFact] + public async Task connect_to_existing_with_start_from_set_to_end_position_and_events_in_it_then_event_written() { + var events = Fixture.CreateTestEvents(11).ToArray(); + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events.Take(10)); + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group, + new(startFrom: StreamPosition.End), + userCredentials: TestCredentials.Root + ); + + await using var subscription = Fixture.Subscriptions.SubscribeToStream(stream, group, userCredentials: TestCredentials.TestUser1); + + await Fixture.Streams.AppendToStreamAsync(stream, new StreamRevision(9), events.Skip(10)); + } + + [RetryFact] + public async Task connect_to_existing_with_start_from_two_and_no_stream() { + var events = Fixture.CreateTestEvents(3).ToArray(); + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + + var eventId = events.Last().EventId; + + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group, + new(startFrom: new StreamPosition(2)), + userCredentials: TestCredentials.Root + ); + + await using var subscription = Fixture.Subscriptions.SubscribeToStream( + stream, + group, + userCredentials: TestCredentials.Root + ); + + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events); + + var resolvedEvent = await subscription.Messages.OfType() + .Select(e => e.ResolvedEvent) + .FirstOrDefaultAsync().AsTask().WithTimeout(); + + Assert.Equal(new(2), resolvedEvent.Event.EventNumber); + Assert.Equal(eventId, resolvedEvent.Event.EventId); + } + + [RetryFact] + public async Task connect_to_existing_with_start_from_x_set_and_events_in_it() { + var events = Fixture.CreateTestEvents(10).ToArray(); + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events.Take(10)); + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group, + new(startFrom: new StreamPosition(4)), + userCredentials: TestCredentials.Root + ); + + await using var subscription = Fixture.Subscriptions.SubscribeToStream( + stream, + group, + userCredentials: TestCredentials.Root + ); + + await Fixture.Streams.AppendToStreamAsync(stream, new StreamRevision(9), events.Skip(10)); + + var resolvedEvent = await subscription.Messages + .OfType() + .Select(e => e.ResolvedEvent) + .FirstOrDefaultAsync().AsTask().WithTimeout(); + + Assert.Equal(new(4), resolvedEvent.Event.EventNumber); + Assert.Equal(events.Skip(4).First().EventId, resolvedEvent.Event.EventId); + } + + [RetryFact] + public async Task connect_to_existing_with_start_from_x_set_and_events_in_it_then_event_written() { + var events = Fixture.CreateTestEvents(11).ToArray(); + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events.Take(10)); + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group, + new(startFrom: new StreamPosition(10)), + userCredentials: TestCredentials.Root + ); + + await using var subscription = Fixture.Subscriptions.SubscribeToStream( + stream, + group, + userCredentials: TestCredentials.Root + ); + + await Fixture.Streams.AppendToStreamAsync(stream, new StreamRevision(9), events.Skip(10)); + + var resolvedEvent = await subscription.Messages.OfType() + .Select(e => e.ResolvedEvent) + .FirstOrDefaultAsync().AsTask().WithTimeout(); + + Assert.Equal(new(10), resolvedEvent.Event.EventNumber); + Assert.Equal(events.Last().EventId, resolvedEvent.Event.EventId); + } + + [RetryFact] + public async Task connect_to_existing_with_start_from_x_set_higher_than_x_and_events_in_it_then_event_written() { + var events = Fixture.CreateTestEvents(12).ToArray(); + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events.Take(11)); + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group, + new(startFrom: new StreamPosition(11)), + userCredentials: TestCredentials.Root + ); + + await using var subscription = Fixture.Subscriptions.SubscribeToStream( + stream, + group, + userCredentials: TestCredentials.Root + ); + + await Fixture.Streams.AppendToStreamAsync(stream, new StreamRevision(10), events.Skip(11)); + + var resolvedEvent = await subscription.Messages.OfType() + .Select(e => e.ResolvedEvent) + .FirstOrDefaultAsync().AsTask().WithTimeout(); + + Assert.Equal(new(11), resolvedEvent.Event.EventNumber); + Assert.Equal(events.Last().EventId, resolvedEvent.Event.EventId); + } + + [RetryFact] + public async Task connect_to_non_existing_with_permissions() { + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + + await using var subscription = Fixture.Subscriptions.SubscribeToStream(stream, group, userCredentials: TestCredentials.Root); + Assert.True( + await subscription.Messages.OfType() + .AnyAsync() + .AsTask() + .WithTimeout() + ); + } + + [RetryFact] + public async Task connect_with_retries() { + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + var events = Fixture.CreateTestEvents().ToArray(); + + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events); + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group, + new(startFrom: StreamPosition.Start), + userCredentials: TestCredentials.Root + ); + + await using var subscription = Fixture.Subscriptions.SubscribeToStream(stream, group, userCredentials: TestCredentials.Root); + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events); + var retryCount = await subscription.Messages.OfType() + .SelectAwait( + async e => { + if (e.RetryCount > 4) { + await subscription.Ack(e.ResolvedEvent); + } else { + await subscription.Nack( + PersistentSubscriptionNakEventAction.Retry, + "Not yet tried enough times", + e.ResolvedEvent + ); + } + + return e.RetryCount; + } + ) + .Where(retryCount => retryCount > 4) + .FirstOrDefaultAsync() + .AsTask() + .WithTimeout(); + + Assert.Equal(5, retryCount); + } + + [RetryFact] + public async Task connecting_to_a_persistent_subscription() { + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + var events = Fixture.CreateTestEvents(12).ToArray(); + + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events.Take(11)); + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group, + new(startFrom: new StreamPosition(11)), + userCredentials: TestCredentials.Root + ); + + await using var subscription = Fixture.Subscriptions.SubscribeToStream(stream, group, userCredentials: TestCredentials.Root); + + await Fixture.Streams.AppendToStreamAsync(stream, new StreamRevision(10), events.Skip(11)); + + var resolvedEvent = await subscription.Messages.OfType() + .Select(e => e.ResolvedEvent) + .FirstOrDefaultAsync().AsTask().WithTimeout(); + + Assert.Equal(new(11), resolvedEvent.Event.EventNumber); + Assert.Equal(events.Last().EventId, resolvedEvent.Event.EventId); + } + + [RetryFact] + public async Task create_after_deleting_the_same() { + var stream = Fixture.GetStreamName(); + var group = $"existing-{Fixture.GetGroupName()}"; + + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.Any, Fixture.CreateTestEvents()); + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group, + new(), + userCredentials: TestCredentials.Root + ); + + await Fixture.Subscriptions.DeleteToStreamAsync( + stream, + group, + userCredentials: TestCredentials.Root + ); + } + + [RetryFact] + public async Task create_duplicate() { + var stream = Fixture.GetStreamName(); + var group = $"duplicate-{Fixture.GetGroupName()}"; + + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group, + new(), + userCredentials: TestCredentials.Root + ); + + var ex = await Assert.ThrowsAsync( + () => Fixture.Subscriptions.CreateToStreamAsync( + stream, + group, + new(), + userCredentials: TestCredentials.Root + ) + ); + + Assert.Equal(StatusCode.AlreadyExists, ex.StatusCode); + } + + [RetryFact] + public async Task create_on_existing_stream() { + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.Any, Fixture.CreateTestEvents()); + + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group, + new(), + userCredentials: TestCredentials.Root + ); + } + + [RetryFact] + public async Task create_on_non_existing_stream() { + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group, + new(), + userCredentials: TestCredentials.Root + ); + } + + [RetryFact] + public async Task create_with_dont_timeout() { + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group, + new(messageTimeout: TimeSpan.Zero), + userCredentials: TestCredentials.Root + ); + } + + [RetryFact] + public async Task deleting_existing_with_permissions() { + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + + await Fixture.Subscriptions.CreateToStreamAsync(stream, group, new(), userCredentials: TestCredentials.Root); + await Fixture.Subscriptions.DeleteToStreamAsync(stream, group, userCredentials: TestCredentials.Root); + } + + [RetryFact] + public async Task deleting_existing_with_subscriber() { + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + + await Fixture.Subscriptions.CreateToStreamAsync(stream, group, new(), userCredentials: TestCredentials.Root); + + await Fixture.Subscriptions.DeleteToStreamAsync(stream, group, userCredentials: TestCredentials.Root); + + await using var subscription = Fixture.Subscriptions.SubscribeToStream(stream, group, userCredentials: TestCredentials.Root); + + Assert.True( + await subscription.Messages.OfType().AnyAsync() + .AsTask() + .WithTimeout() + ); + } + + [RetryFact] + public async Task deleting_nonexistent() { + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + + await Assert.ThrowsAsync( + () => Fixture.Subscriptions.DeleteToStreamAsync(stream, group, userCredentials: TestCredentials.Root) + ); + } + + [RetryFact] + public async Task happy_case_catching_up_to_link_to_events_manual_ack() { + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + + const int bufferCount = 10; + const int eventWriteCount = bufferCount * 2; + + var events = Fixture.CreateTestEvents(eventWriteCount) + .Select( + (e, i) => new EventData( + e.EventId, + SystemEventTypes.LinkTo, + Encoding.UTF8.GetBytes($"{i}@{stream}"), + contentType: Constants.Metadata.ContentTypes.ApplicationOctetStream + ) + ).ToArray(); + + foreach (var e in events) + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.Any, [e]); + + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group, + new(startFrom: StreamPosition.Start, resolveLinkTos: true), + userCredentials: TestCredentials.Root + ); + + await using var subscription = Fixture.Subscriptions.SubscribeToStream( + stream, + group, + bufferSize: bufferCount, + userCredentials: TestCredentials.Root + ); + + await subscription!.Messages.OfType() + .Take(events.Length) + .ForEachAwaitAsync(e => subscription.Ack(e.ResolvedEvent)) + .WithTimeout(); + } + + [RetryFact] + public async Task happy_case_catching_up_to_normal_events_manual_ack() { + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + + const int bufferCount = 10; + const int eventWriteCount = bufferCount * 2; + + var events = Fixture.CreateTestEvents(eventWriteCount).ToArray(); + + foreach (var e in events) + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.Any, new[] { e }); + + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group, + new(startFrom: StreamPosition.Start, resolveLinkTos: true), + userCredentials: TestCredentials.Root + ); + + await using var subscription = Fixture.Subscriptions.SubscribeToStream( + stream, + group, + bufferSize: bufferCount, + userCredentials: TestCredentials.Root + ); + + await subscription!.Messages.OfType() + .Take(events.Length) + .ForEachAwaitAsync(e => subscription.Ack(e.ResolvedEvent)) + .WithTimeout(); + } + + [RetryFact] + public async Task happy_case_writing_and_subscribing_to_normal_events_manual_ack() { + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + + const int bufferCount = 10; + const int eventWriteCount = bufferCount * 2; + + var events = Fixture.CreateTestEvents(eventWriteCount).ToArray(); + + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group, + new(startFrom: StreamPosition.End, resolveLinkTos: true), + userCredentials: TestCredentials.Root + ); + + await using var subscription = Fixture.Subscriptions.SubscribeToStream( + stream, + group, + bufferSize: bufferCount, + userCredentials: TestCredentials.Root + ); + + foreach (var e in events) + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.Any, [e]); + + await subscription.Messages.OfType() + .Take(events.Length) + .ForEachAwaitAsync(e => subscription.Ack(e.ResolvedEvent)) + .WithTimeout(); + } + + [RetryFact] + public async Task list_without_persistent_subscriptions_should_throw() { + var stream = Fixture.GetStreamName(); + + await Assert.ThrowsAsync( + async () => + await Fixture.Subscriptions.ListToStreamAsync(stream, userCredentials: TestCredentials.Root) + ); + } + + [RetryFact] + public async Task update_existing() { + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, Fixture.CreateTestEvents()); + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group, + new(), + userCredentials: TestCredentials.Root + ); + + await Fixture.Subscriptions.UpdateToStreamAsync(stream, group, new(), userCredentials: TestCredentials.Root); + } + + [RetryFact] + public async Task update_existing_with_subscribers() { + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, Fixture.CreateTestEvents()); + await Fixture.Subscriptions.CreateToStreamAsync(stream, group, new(), userCredentials: TestCredentials.Root); + + await using var subscription = Fixture.Subscriptions.SubscribeToStream(stream, group, userCredentials: TestCredentials.Root); + await using var enumerator = subscription.Messages.GetAsyncEnumerator(); + + await enumerator.MoveNextAsync(); + + await Fixture.Subscriptions.UpdateToStreamAsync(stream, group, new(), userCredentials: TestCredentials.Root); + + var ex = await Assert.ThrowsAsync( + async () => { + while (await enumerator.MoveNextAsync()) { } + } + ).WithTimeout(); + + Assert.Equal(stream, ex.StreamName); + Assert.Equal(group, ex.GroupName); + } + + [Regression.Fact(21, "20.x returns the wrong exception")] + public async Task update_non_existent() { + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + + await Assert.ThrowsAsync( + () => Fixture.Subscriptions.UpdateToStreamAsync( + stream, + group, + new(), + userCredentials: TestCredentials.Root + ) + ); + } + + [RetryFact] + public async Task when_writing_and_subscribing_to_normal_events_manual_nack() { + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + + const int bufferCount = 10; + const int eventWriteCount = bufferCount * 2; + + var events = Fixture.CreateTestEvents(eventWriteCount).ToArray(); + + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group, + new(startFrom: StreamPosition.Start, resolveLinkTos: true), + userCredentials: TestCredentials.Root + ); + + await using var subscription = Fixture.Subscriptions.SubscribeToStream(stream, group, bufferSize: bufferCount, userCredentials: TestCredentials.Root); + + foreach (var e in events) + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.Any, [e]); + + await subscription.Messages.OfType() + .Take(1) + .ForEachAwaitAsync( + async message => + await subscription.Nack( + PersistentSubscriptionNakEventAction.Park, + "fail", + message.ResolvedEvent + ) + ) + .WithTimeout(); + } +} diff --git a/test/EventStore.Client.Tests/PositionTests.cs b/test/EventStore.Client.Tests/PositionTests.cs index b68748af8..b9657a2c5 100644 --- a/test/EventStore.Client.Tests/PositionTests.cs +++ b/test/EventStore.Client.Tests/PositionTests.cs @@ -5,7 +5,7 @@ namespace EventStore.Client.Tests; public class PositionTests : ValueObjectTests { public PositionTests() : base(new ScenarioFixture()) { } - [Fact] + [RetryFact] public void IsComparable() => Assert.IsAssignableFrom>(_fixture.Create()); [Theory] @@ -16,7 +16,7 @@ public PositionTests() : base(new ScenarioFixture()) { } [AutoScenarioData(typeof(ScenarioFixture))] public void LiveIsGreaterThanAll(Position other) => Assert.True(Position.End > other); - [Fact] + [RetryFact] public void ToStringReturnsExpectedResult() { var sut = _fixture.Create(); Assert.Equal($"C:{sut.CommitPosition}/P:{sut.PreparePosition}", sut.ToString()); @@ -60,4 +60,4 @@ public void TryParse(string s, bool success, Position? expected) { class ScenarioFixture : Fixture { public ScenarioFixture() => Customize(composer => composer.FromFactory(value => new(value, value))); } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Tests/PrefixFilterExpressionTests.cs b/test/EventStore.Client.Tests/PrefixFilterExpressionTests.cs deleted file mode 100644 index ae2cbdcd6..000000000 --- a/test/EventStore.Client.Tests/PrefixFilterExpressionTests.cs +++ /dev/null @@ -1,11 +0,0 @@ -using AutoFixture; - -namespace EventStore.Client.Tests; - -public class PrefixFilterExpressionTests : ValueObjectTests { - public PrefixFilterExpressionTests() : base(new ScenarioFixture()) { } - - class ScenarioFixture : Fixture { - public ScenarioFixture() => Customize(composer => composer.FromFactory(value => new(value))); - } -} \ No newline at end of file diff --git a/test/EventStore.Client.Tests/ProjectionManagementTests.cs b/test/EventStore.Client.Tests/ProjectionManagementTests.cs new file mode 100644 index 000000000..e119ff3cd --- /dev/null +++ b/test/EventStore.Client.Tests/ProjectionManagementTests.cs @@ -0,0 +1,205 @@ +// ReSharper disable InconsistentNaming +// ReSharper disable ClassNeverInstantiated.Local + +using EventStore.Client.Tests.TestNode; +using EventStore.Client.Tests; + +namespace EventStore.Client.Tests; + +public class ProjectionManagementTests(ITestOutputHelper output, ProjectionManagementTests.CustomFixture fixture) + : KurrentTemporaryTests(output, fixture) { + [Fact] + public async Task status_is_aborted() { + var name = Names.First(); + await Fixture.Projections.AbortAsync(name, userCredentials: TestCredentials.Root); + var result = await Fixture.Projections.GetStatusAsync(name, userCredentials: TestCredentials.Root); + Assert.NotNull(result); + Assert.Contains(["Aborted/Stopped", "Stopped"], x => x == result.Status); + } + + [Fact] + public async Task one_time() => + await Fixture.Projections.CreateOneTimeAsync("fromAll().when({$init: function (state, ev) {return {};}});", userCredentials: TestCredentials.Root); + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task continuous(bool trackEmittedStreams) { + var name = Fixture.GetProjectionName(); + + await Fixture.Projections.CreateContinuousAsync( + name, + "fromAll().when({$init: function (state, ev) {return {};}});", + trackEmittedStreams, + userCredentials: TestCredentials.Root + ); + } + + [Fact] + public async Task transient() { + var name = Fixture.GetProjectionName(); + + await Fixture.Projections.CreateTransientAsync( + name, + "fromAll().when({$init: function (state, ev) {return {};}});", + userCredentials: TestCredentials.Root + ); + } + + [Fact] + public async Task disable_projection() { + var name = Names.First(); + await Fixture.Projections.DisableAsync(name, userCredentials: TestCredentials.Root); + var result = await Fixture.Projections.GetStatusAsync(name, userCredentials: TestCredentials.Root); + Assert.NotNull(result); + Assert.Contains(["Aborted/Stopped", "Stopped"], x => x == result!.Status); + } + + [Fact] + public async Task enable_projection() { + var name = Names.First(); + await Fixture.Projections.EnableAsync(name, userCredentials: TestCredentials.Root); + var result = await Fixture.Projections.GetStatusAsync(name, userCredentials: TestCredentials.Root); + Assert.NotNull(result); + Assert.Equal("Running", result.Status); + } + + [Fact] + public async Task get_result() { + var name = Fixture.GetProjectionName(); + Result? result = null; + + var projection = $$""" + fromStream('{{name}}').when({ + "$init": function() { return { Count: 0 }; }, + "$any": function(s, e) { s.Count++; return s; } + }); + """; + + await Fixture.Projections.CreateContinuousAsync( + name, + projection, + userCredentials: TestCredentials.Root + ); + + await Fixture.Streams.AppendToStreamAsync( + name, + StreamState.NoStream, + Fixture.CreateTestEvents() + ); + + await AssertEx.IsOrBecomesTrue( + async () => { + result = await Fixture.Projections.GetResultAsync(name, userCredentials: TestCredentials.Root); + return result.Count > 0; + } + ); + + Assert.NotNull(result); + Assert.Equal(1, result!.Count); + } + + [Fact] + public async Task get_state() { + var name = Fixture.GetProjectionName(); + + var projection = $$""" + fromStream('{{name}}').when({ + "$init": function() { return { Count: 0 }; }, + "$any": function(s, e) { s.Count++; return s; } + }); + """; + + Result? result = null; + + await Fixture.Projections.CreateContinuousAsync( + name, + projection, + userCredentials: TestCredentials.Root + ); + + await Fixture.Streams.AppendToStreamAsync( + name, + StreamState.NoStream, + Fixture.CreateTestEvents() + ); + + await AssertEx.IsOrBecomesTrue( + async () => { + result = await Fixture.Projections.GetStateAsync(name, userCredentials: TestCredentials.Root); + return result.Count > 0; + } + ); + + Assert.NotNull(result); + Assert.Equal(1, result!.Count); + } + + [Fact] + public async Task get_status() { + var name = Names.First(); + var result = await Fixture.Projections.GetStatusAsync(name, userCredentials: TestCredentials.Root); + + Assert.NotNull(result); + Assert.Equal(name, result.Name); + } + + [Fact] + public async Task restart_subsystem_does_not_throw() => + await Fixture.Projections.RestartSubsystemAsync(userCredentials: TestCredentials.Root); + + [Fact] + public async Task restart_subsystem_throws_when_given_no_credentials() => + await Assert.ThrowsAsync(() => Fixture.Projections.RestartSubsystemAsync(userCredentials: TestCredentials.TestUser1)); + + [Theory] + [InlineData(true)] + [InlineData(false)] + [InlineData(null)] + public async Task update_projection(bool? emitEnabled) { + var name = Fixture.GetProjectionName(); + await Fixture.Projections.CreateContinuousAsync( + name, + "fromAll().when({$init: function (state, ev) {return {};}});", + userCredentials: TestCredentials.Root + ); + + await Fixture.Projections.UpdateAsync( + name, + "fromAll().when({$init: function (s, e) {return {};}});", + emitEnabled, + userCredentials: TestCredentials.Root + ); + } + + [Fact] + public async Task list_one_time_projections() { + await Fixture.Projections.CreateOneTimeAsync("fromAll().when({$init: function (state, ev) {return {};}});", userCredentials: TestCredentials.Root); + + var result = await Fixture.Projections.ListOneTimeAsync(userCredentials: TestCredentials.Root) + .ToArrayAsync(); + + var details = Assert.Single(result); + Assert.Equal("OneTime", details.Mode); + } + + [Fact] + public async Task reset_projection() { + var name = Names.First(); + await Fixture.Projections.ResetAsync(name, userCredentials: TestCredentials.Root); + var result = await Fixture.Projections.GetStatusAsync(name, userCredentials: TestCredentials.Root); + + Assert.NotNull(result); + Assert.Equal("Running", result.Status); + } + + static readonly string[] Names = ["$streams", "$stream_by_category", "$by_category", "$by_event_type", "$by_correlation_id"]; + + record Result { + public int Count { get; set; } + } + + public class CustomFixture : KurrentTemporaryFixture { + public CustomFixture() : base(x => x.RunProjections()) { } + } +} diff --git a/test/EventStore.Client.Tests/RegularFilterExpressionTests.cs b/test/EventStore.Client.Tests/RegularFilterExpressionTests.cs index 481e5cb46..67bd40039 100644 --- a/test/EventStore.Client.Tests/RegularFilterExpressionTests.cs +++ b/test/EventStore.Client.Tests/RegularFilterExpressionTests.cs @@ -9,4 +9,4 @@ public RegularFilterExpressionTests() : base(new ScenarioFixture()) { } class ScenarioFixture : Fixture { public ScenarioFixture() => Customize(composer => composer.FromFactory(value => new(value))); } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Streams.Tests/Security/all_stream_with_no_acl_security.cs b/test/EventStore.Client.Tests/Security/AllStreamWithNoAclSecurityTests.cs similarity index 90% rename from test/EventStore.Client.Streams.Tests/Security/all_stream_with_no_acl_security.cs rename to test/EventStore.Client.Tests/Security/AllStreamWithNoAclSecurityTests.cs index 215e0f05d..b404c7357 100644 --- a/test/EventStore.Client.Streams.Tests/Security/all_stream_with_no_acl_security.cs +++ b/test/EventStore.Client.Tests/Security/AllStreamWithNoAclSecurityTests.cs @@ -1,7 +1,11 @@ -namespace EventStore.Client.Streams.Tests; +using EventStore.Client.Tests.TestNode; +using EventStore.Client.Tests; + +namespace EventStore.Client.Tests; [Trait("Category", "Security")] -public class all_stream_with_no_acl_security(ITestOutputHelper output, all_stream_with_no_acl_security.CustomFixture fixture) : EventStoreTests(output, fixture) { +public class AllStreamWithNoAclSecurityTests(ITestOutputHelper output, AllStreamWithNoAclSecurityTests.CustomFixture fixture) + : KurrentTemporaryTests(output, fixture) { [Fact] public async Task write_to_all_is_never_allowed() { await Assert.ThrowsAsync(() => Fixture.AppendStream(SecurityFixture.AllStream)); @@ -59,4 +63,4 @@ protected override async Task Given() { await Streams.SetStreamMetadataAsync(AllStream, StreamState.Any, new(), userCredentials: TestCredentials.Root); } } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Streams.Tests/Security/delete_stream_security.cs b/test/EventStore.Client.Tests/Security/DeleteStreamSecurityTests.cs similarity index 87% rename from test/EventStore.Client.Streams.Tests/Security/delete_stream_security.cs rename to test/EventStore.Client.Tests/Security/DeleteStreamSecurityTests.cs index 61e6253ce..7e661bf9f 100644 --- a/test/EventStore.Client.Streams.Tests/Security/delete_stream_security.cs +++ b/test/EventStore.Client.Tests/Security/DeleteStreamSecurityTests.cs @@ -1,7 +1,10 @@ -namespace EventStore.Client.Streams.Tests; +using EventStore.Client.Tests.TestNode; +using EventStore.Client.Tests; + +namespace EventStore.Client.Tests; [Trait("Category", "Security")] -public class delete_stream_security(ITestOutputHelper output, SecurityFixture fixture) : EventStoreTests(output, fixture) { +public class DeleteStreamSecurityTests(ITestOutputHelper output, SecurityFixture fixture) : KurrentTemporaryTests(output, fixture) { [Fact] public async Task delete_of_all_is_never_allowed() { await Assert.ThrowsAsync(() => Fixture.DeleteStream(SecurityFixture.AllStream)); @@ -113,7 +116,7 @@ public async Task deleting_normal_all_stream_with_admin_user_is_allowed() { public async Task deleting_system_no_acl_stream_with_no_user_is_not_allowed() { var streamId = await Fixture.CreateStreamWithMeta( streamId: $"${Fixture.GetStreamName()}", - metadata: new() + metadataPermanent: new() ); await Assert.ThrowsAsync(() => Fixture.DeleteStream(streamId)); @@ -123,7 +126,7 @@ public async Task deleting_system_no_acl_stream_with_no_user_is_not_allowed() { public async Task deleting_system_no_acl_stream_with_existing_user_is_not_allowed() { var streamId = await Fixture.CreateStreamWithMeta( streamId: $"${Fixture.GetStreamName()}", - metadata: new() + metadataPermanent: new() ); await Assert.ThrowsAsync(() => Fixture.DeleteStream(streamId, TestCredentials.TestUser1)); @@ -133,7 +136,7 @@ public async Task deleting_system_no_acl_stream_with_existing_user_is_not_allowe public async Task deleting_system_no_acl_stream_with_admin_user_is_allowed() { var streamId = await Fixture.CreateStreamWithMeta( streamId: $"${Fixture.GetStreamName()}", - metadata: new() + metadataPermanent: new() ); await Fixture.DeleteStream(streamId, TestCredentials.TestAdmin); @@ -143,7 +146,7 @@ public async Task deleting_system_no_acl_stream_with_admin_user_is_allowed() { public async Task deleting_system_user_stream_with_no_user_is_not_allowed() { var streamId = await Fixture.CreateStreamWithMeta( streamId: $"${Fixture.GetStreamName()}", - metadata: new(acl: new(deleteRole: TestCredentials.TestUser1.Username)) + metadataPermanent: new(acl: new(deleteRole: TestCredentials.TestUser1.Username)) ); await Assert.ThrowsAsync(() => Fixture.DeleteStream(streamId)); @@ -153,7 +156,7 @@ public async Task deleting_system_user_stream_with_no_user_is_not_allowed() { public async Task deleting_system_user_stream_with_not_authorized_user_is_not_allowed() { var streamId = await Fixture.CreateStreamWithMeta( streamId: $"${Fixture.GetStreamName()}", - metadata: new(acl: new(deleteRole: TestCredentials.TestUser1.Username)) + metadataPermanent: new(acl: new(deleteRole: TestCredentials.TestUser1.Username)) ); await Assert.ThrowsAsync(() => Fixture.DeleteStream(streamId, TestCredentials.TestUser2)); @@ -163,7 +166,7 @@ public async Task deleting_system_user_stream_with_not_authorized_user_is_not_al public async Task deleting_system_user_stream_with_authorized_user_is_allowed() { var streamId = await Fixture.CreateStreamWithMeta( streamId: $"${Fixture.GetStreamName()}", - metadata: new(acl: new(deleteRole: TestCredentials.TestUser1.Username)) + metadataPermanent: new(acl: new(deleteRole: TestCredentials.TestUser1.Username)) ); await Fixture.DeleteStream(streamId, TestCredentials.TestUser1); @@ -173,7 +176,7 @@ public async Task deleting_system_user_stream_with_authorized_user_is_allowed() public async Task deleting_system_user_stream_with_admin_user_is_allowed() { var streamId = await Fixture.CreateStreamWithMeta( streamId: $"${Fixture.GetStreamName()}", - metadata: new(acl: new(deleteRole: TestCredentials.TestUser1.Username)) + metadataPermanent: new(acl: new(deleteRole: TestCredentials.TestUser1.Username)) ); await Fixture.DeleteStream(streamId, TestCredentials.TestAdmin); @@ -183,7 +186,7 @@ public async Task deleting_system_user_stream_with_admin_user_is_allowed() { public async Task deleting_system_admin_stream_with_no_user_is_not_allowed() { var streamId = await Fixture.CreateStreamWithMeta( streamId: $"${Fixture.GetStreamName()}", - metadata: new(acl: new(deleteRole: SystemRoles.Admins)) + metadataPermanent: new(acl: new(deleteRole: SystemRoles.Admins)) ); await Assert.ThrowsAsync(() => Fixture.DeleteStream(streamId)); @@ -193,7 +196,7 @@ public async Task deleting_system_admin_stream_with_no_user_is_not_allowed() { public async Task deleting_system_admin_stream_with_existing_user_is_not_allowed() { var streamId = await Fixture.CreateStreamWithMeta( streamId: $"${Fixture.GetStreamName()}", - metadata: new(acl: new(deleteRole: SystemRoles.Admins)) + metadataPermanent: new(acl: new(deleteRole: SystemRoles.Admins)) ); await Assert.ThrowsAsync(() => Fixture.DeleteStream(streamId, TestCredentials.TestUser1)); @@ -203,7 +206,7 @@ public async Task deleting_system_admin_stream_with_existing_user_is_not_allowed public async Task deleting_system_admin_stream_with_admin_user_is_allowed() { var streamId = await Fixture.CreateStreamWithMeta( streamId: $"${Fixture.GetStreamName()}", - metadata: new(acl: new(deleteRole: SystemRoles.Admins)) + metadataPermanent: new(acl: new(deleteRole: SystemRoles.Admins)) ); await Fixture.DeleteStream(streamId, TestCredentials.TestAdmin); @@ -213,7 +216,7 @@ public async Task deleting_system_admin_stream_with_admin_user_is_allowed() { public async Task deleting_system_all_stream_with_no_user_is_allowed() { var streamId = await Fixture.CreateStreamWithMeta( streamId: $"${Fixture.GetStreamName()}", - metadata: new(acl: new(deleteRole: SystemRoles.All)) + metadataPermanent: new(acl: new(deleteRole: SystemRoles.All)) ); await Fixture.DeleteStream(streamId); @@ -223,7 +226,7 @@ public async Task deleting_system_all_stream_with_no_user_is_allowed() { public async Task deleting_system_all_stream_with_existing_user_is_allowed() { var streamId = await Fixture.CreateStreamWithMeta( streamId: $"${Fixture.GetStreamName()}", - metadata: new(acl: new(deleteRole: SystemRoles.All)) + metadataPermanent: new(acl: new(deleteRole: SystemRoles.All)) ); await Fixture.DeleteStream(streamId, TestCredentials.TestUser1); @@ -233,9 +236,9 @@ public async Task deleting_system_all_stream_with_existing_user_is_allowed() { public async Task deleting_system_all_stream_with_admin_user_is_allowed() { var streamId = await Fixture.CreateStreamWithMeta( streamId: $"${Fixture.GetStreamName()}", - metadata: new(acl: new(deleteRole: SystemRoles.All)) + metadataPermanent: new(acl: new(deleteRole: SystemRoles.All)) ); await Fixture.DeleteStream(streamId, TestCredentials.TestAdmin); } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Streams.Tests/Security/multiple_role_security.cs b/test/EventStore.Client.Tests/Security/MultipleRoleSecurityTests.cs similarity index 83% rename from test/EventStore.Client.Streams.Tests/Security/multiple_role_security.cs rename to test/EventStore.Client.Tests/Security/MultipleRoleSecurityTests.cs index 3195c22df..3a5a0c78a 100644 --- a/test/EventStore.Client.Streams.Tests/Security/multiple_role_security.cs +++ b/test/EventStore.Client.Tests/Security/MultipleRoleSecurityTests.cs @@ -1,7 +1,11 @@ -namespace EventStore.Client.Streams.Tests; +using EventStore.Client.Tests.TestNode; +using EventStore.Client.Tests; + +namespace EventStore.Client.Tests; [Trait("Category", "Security")] -public class multiple_role_security(ITestOutputHelper output, multiple_role_security.CustomFixture fixture) : EventStoreTests(output, fixture) { +public class MultipleRoleSecurityTests(ITestOutputHelper output, MultipleRoleSecurityTests.CustomFixture fixture) + : KurrentTemporaryTests(output, fixture) { [Fact] public async Task multiple_roles_are_handled_correctly() { await Assert.ThrowsAsync(() => Fixture.ReadEvent("usr-stream")); @@ -20,7 +24,7 @@ public async Task multiple_roles_are_handled_correctly() { } [AnonymousAccess.Fact] - public async Task multiple_roles_are_handled_correctly_without_authentication() => + public async Task multiple_roles_are_handled_correctly_without_authentication() => await Fixture.DeleteStream("usr-stream1"); public class CustomFixture : SecurityFixture { @@ -36,4 +40,4 @@ protected override async Task When() { await Streams.SetSystemSettingsAsync(settings, userCredentials: TestCredentials.TestAdmin); } } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Streams.Tests/Security/overriden_system_stream_security_for_all.cs b/test/EventStore.Client.Tests/Security/OverridenSystemStreamSecurityForAllTests.cs similarity index 86% rename from test/EventStore.Client.Streams.Tests/Security/overriden_system_stream_security_for_all.cs rename to test/EventStore.Client.Tests/Security/OverridenSystemStreamSecurityForAllTests.cs index 2364717bb..dd4057df0 100644 --- a/test/EventStore.Client.Streams.Tests/Security/overriden_system_stream_security_for_all.cs +++ b/test/EventStore.Client.Tests/Security/OverridenSystemStreamSecurityForAllTests.cs @@ -1,7 +1,11 @@ -namespace EventStore.Client.Streams.Tests; +using EventStore.Client.Tests.TestNode; +using EventStore.Client.Tests; + +namespace EventStore.Client.Tests; [Trait("Category", "Security")] -public class overriden_system_stream_security_for_all(ITestOutputHelper output, overriden_system_stream_security_for_all.CustomFixture fixture) : EventStoreTests(output, fixture) { +public class OverridenSystemStreamSecurityForAllTests(ITestOutputHelper output, OverridenSystemStreamSecurityForAllTests.CustomFixture fixture) + : KurrentTemporaryTests(output, fixture) { [Fact] public async Task operations_on_system_stream_succeeds_for_user() { var stream = $"${Fixture.GetStreamName()}"; diff --git a/test/EventStore.Client.Streams.Tests/Security/overriden_system_stream_security.cs b/test/EventStore.Client.Tests/Security/OverridenSystemStreamSecurityTests.cs similarity index 91% rename from test/EventStore.Client.Streams.Tests/Security/overriden_system_stream_security.cs rename to test/EventStore.Client.Tests/Security/OverridenSystemStreamSecurityTests.cs index fcdaec541..deedbb33d 100644 --- a/test/EventStore.Client.Streams.Tests/Security/overriden_system_stream_security.cs +++ b/test/EventStore.Client.Tests/Security/OverridenSystemStreamSecurityTests.cs @@ -1,7 +1,11 @@ -namespace EventStore.Client.Streams.Tests; +using EventStore.Client.Tests.TestNode; +using EventStore.Client.Tests; + +namespace EventStore.Client.Tests; [Trait("Category", "Security")] -public class overriden_system_stream_security(ITestOutputHelper output, overriden_system_stream_security.CustomFixture fixture) : EventStoreTests(output, fixture) { +public class OverridenSystemStreamSecurityTests(ITestOutputHelper output, OverridenSystemStreamSecurityTests.CustomFixture fixture) + : KurrentTemporaryTests(output, fixture) { [Fact] public async Task operations_on_system_stream_succeed_for_authorized_user() { var stream = $"${Fixture.GetStreamName()}"; diff --git a/test/EventStore.Client.Streams.Tests/Security/overriden_user_stream_security.cs b/test/EventStore.Client.Tests/Security/OverridenUserStreamSecurityTests.cs similarity index 91% rename from test/EventStore.Client.Streams.Tests/Security/overriden_user_stream_security.cs rename to test/EventStore.Client.Tests/Security/OverridenUserStreamSecurityTests.cs index 287d5a64f..f5da17028 100644 --- a/test/EventStore.Client.Streams.Tests/Security/overriden_user_stream_security.cs +++ b/test/EventStore.Client.Tests/Security/OverridenUserStreamSecurityTests.cs @@ -1,7 +1,11 @@ -namespace EventStore.Client.Streams.Tests; +using EventStore.Client.Tests.TestNode; +using EventStore.Client.Tests; + +namespace EventStore.Client.Tests; [Trait("Category", "Security")] -public class overriden_user_stream_security(ITestOutputHelper output, overriden_user_stream_security.CustomFixture fixture) : EventStoreTests(output, fixture) { +public class OverridenUserStreamSecurityTests(ITestOutputHelper output, OverridenUserStreamSecurityTests.CustomFixture fixture) + : KurrentTemporaryTests(output, fixture) { [Fact] public async Task operations_on_user_stream_succeeds_for_authorized_user() { var stream = Fixture.GetStreamName(); diff --git a/test/EventStore.Client.Streams.Tests/Security/read_all_security.cs b/test/EventStore.Client.Tests/Security/ReadAllSecurityTests.cs similarity index 83% rename from test/EventStore.Client.Streams.Tests/Security/read_all_security.cs rename to test/EventStore.Client.Tests/Security/ReadAllSecurityTests.cs index 01089db2e..b1a88b7a4 100644 --- a/test/EventStore.Client.Streams.Tests/Security/read_all_security.cs +++ b/test/EventStore.Client.Tests/Security/ReadAllSecurityTests.cs @@ -1,7 +1,10 @@ -namespace EventStore.Client.Streams.Tests; +using EventStore.Client.Tests.TestNode; +using EventStore.Client.Tests; + +namespace EventStore.Client.Tests; [Trait("Category", "Security")] -public class read_all_security(ITestOutputHelper output, SecurityFixture fixture) : EventStoreTests(output, fixture) { +public class ReadAllSecurityTests(ITestOutputHelper output, SecurityFixture fixture) : KurrentTemporaryTests(output, fixture) { [Fact] public async Task reading_all_with_not_existing_credentials_is_not_authenticated() { await Assert.ThrowsAsync(() => Fixture.ReadAllForward(TestCredentials.TestBadUser)); @@ -31,4 +34,4 @@ public async Task reading_all_with_admin_credentials_succeeds() { await Fixture.ReadAllForward(TestCredentials.TestAdmin); await Fixture.ReadAllBackward(TestCredentials.TestAdmin); } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Streams.Tests/Security/read_stream_meta_security.cs b/test/EventStore.Client.Tests/Security/ReadStreamMetaSecurityTests.cs similarity index 74% rename from test/EventStore.Client.Streams.Tests/Security/read_stream_meta_security.cs rename to test/EventStore.Client.Tests/Security/ReadStreamMetaSecurityTests.cs index b3a7e7afa..b3c966977 100644 --- a/test/EventStore.Client.Streams.Tests/Security/read_stream_meta_security.cs +++ b/test/EventStore.Client.Tests/Security/ReadStreamMetaSecurityTests.cs @@ -1,12 +1,13 @@ -namespace EventStore.Client.Streams.Tests; +using EventStore.Client.Tests.TestNode; +using EventStore.Client.Tests; + +namespace EventStore.Client.Tests; [Trait("Category", "Security")] -public class read_stream_meta_security(ITestOutputHelper output, SecurityFixture fixture) : EventStoreTests(output, fixture) { +public class ReadStreamMetaSecurityTests(ITestOutputHelper output, SecurityFixture fixture) : KurrentTemporaryTests(output, fixture) { [Fact] public async Task reading_stream_meta_with_not_existing_credentials_is_not_authenticated() => - await Assert.ThrowsAsync( - () => Fixture.ReadMeta(SecurityFixture.MetaReadStream, TestCredentials.TestBadUser) - ); + await Assert.ThrowsAsync(() => Fixture.ReadMeta(SecurityFixture.MetaReadStream, TestCredentials.TestBadUser)); [Fact] public async Task reading_stream_meta_with_no_credentials_is_denied() => @@ -14,9 +15,7 @@ public async Task reading_stream_meta_with_no_credentials_is_denied() => [Fact] public async Task reading_stream_meta_with_not_authorized_user_credentials_is_denied() => - await Assert.ThrowsAsync( - () => Fixture.ReadMeta(SecurityFixture.MetaReadStream, TestCredentials.TestUser2) - ); + await Assert.ThrowsAsync(() => Fixture.ReadMeta(SecurityFixture.MetaReadStream, TestCredentials.TestUser2)); [Fact] public async Task reading_stream_meta_with_authorized_user_credentials_succeeds() => @@ -31,9 +30,7 @@ public async Task reading_stream_meta_with_admin_user_credentials_succeeds() => [Fact] public async Task reading_no_acl_stream_meta_is_not_authenticated_when_not_existing_credentials_are_passed() => - await Assert.ThrowsAsync( - () => Fixture.ReadMeta(SecurityFixture.NoAclStream, TestCredentials.TestBadUser) - ); + await Assert.ThrowsAsync(() => Fixture.ReadMeta(SecurityFixture.NoAclStream, TestCredentials.TestBadUser)); [Fact] public async Task reading_no_acl_stream_meta_succeeds_when_any_existing_user_credentials_are_passed() { @@ -52,9 +49,7 @@ public async Task reading_all_access_normal_stream_meta_succeeds_when_no_credent [Fact] public async Task reading_all_access_normal_stream_meta_is_not_authenticated_when_not_existing_credentials_are_passed() => - await Assert.ThrowsAsync( - () => Fixture.ReadMeta(SecurityFixture.NormalAllStream, TestCredentials.TestBadUser) - ); + await Assert.ThrowsAsync(() => Fixture.ReadMeta(SecurityFixture.NormalAllStream, TestCredentials.TestBadUser)); [Fact] public async Task @@ -66,4 +61,4 @@ public async Task [Fact] public async Task reading_all_access_normal_stream_meta_succeeds_when_admin_user_credentials_are_passed() => await Fixture.ReadMeta(SecurityFixture.NormalAllStream, TestCredentials.TestAdmin); -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Streams.Tests/Security/read_stream_security.cs b/test/EventStore.Client.Tests/Security/ReadStreamSecurityTests.cs similarity index 96% rename from test/EventStore.Client.Streams.Tests/Security/read_stream_security.cs rename to test/EventStore.Client.Tests/Security/ReadStreamSecurityTests.cs index 206627128..a26d7e797 100644 --- a/test/EventStore.Client.Streams.Tests/Security/read_stream_security.cs +++ b/test/EventStore.Client.Tests/Security/ReadStreamSecurityTests.cs @@ -1,7 +1,10 @@ -namespace EventStore.Client.Streams.Tests; +using EventStore.Client.Tests.TestNode; +using EventStore.Client.Tests; + +namespace EventStore.Client.Tests; [Trait("Category", "Security")] -public class read_stream_security(ITestOutputHelper output, SecurityFixture fixture) : EventStoreTests(output, fixture) { +public class ReadStreamSecurityTests(ITestOutputHelper output, SecurityFixture fixture) : KurrentTemporaryTests(output, fixture) { [Fact] public async Task reading_stream_with_not_existing_credentials_is_not_authenticated() { await Assert.ThrowsAsync(() => Fixture.ReadEvent(SecurityFixture.ReadStream, TestCredentials.TestBadUser)); @@ -26,7 +29,7 @@ public async Task reading_stream_with_not_authorized_user_credentials_is_denied( [Fact] public async Task reading_stream_with_authorized_user_credentials_succeeds() { await Fixture.AppendStream(SecurityFixture.ReadStream, TestCredentials.TestUser1); - + await Fixture.ReadEvent(SecurityFixture.ReadStream, TestCredentials.TestUser1); await Fixture.ReadStreamForward(SecurityFixture.ReadStream, TestCredentials.TestUser1); await Fixture.ReadStreamBackward(SecurityFixture.ReadStream, TestCredentials.TestUser1); @@ -110,4 +113,4 @@ public async Task reading_all_access_normal_stream_succeeds_when_admin_user_cred await Fixture.ReadStreamForward(SecurityFixture.NormalAllStream, TestCredentials.TestAdmin); await Fixture.ReadStreamBackward(SecurityFixture.NormalAllStream, TestCredentials.TestAdmin); } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Streams.Tests/Security/SecurityFixture.cs b/test/EventStore.Client.Tests/Security/SecurityFixture.cs similarity index 97% rename from test/EventStore.Client.Streams.Tests/Security/SecurityFixture.cs rename to test/EventStore.Client.Tests/Security/SecurityFixture.cs index 2ffc0b73c..43edb97b6 100644 --- a/test/EventStore.Client.Streams.Tests/Security/SecurityFixture.cs +++ b/test/EventStore.Client.Tests/Security/SecurityFixture.cs @@ -1,8 +1,9 @@ using System.Runtime.CompilerServices; +using EventStore.Client; +using EventStore.Client.Tests.TestNode; +using EventStore.Client.Tests; -namespace EventStore.Client.Streams.Tests; - -public class SecurityFixture : EventStoreFixture { +public class SecurityFixture : KurrentTemporaryFixture { public const string NoAclStream = nameof(NoAclStream); public const string ReadStream = nameof(ReadStream); public const string WriteStream = nameof(WriteStream); @@ -41,7 +42,7 @@ await Users.CreateUserWithRetry( TestCredentials.TestAdmin.Password!, TestCredentials.Root ).WithTimeout(TimeSpan.FromMilliseconds(TimeoutMs)); - + await Given(); await When(); }; @@ -245,6 +246,7 @@ public Task WriteMeta(string streamId, UserCredentials? userCreden public async Task SubscribeToStream(string streamId, UserCredentials? userCredentials = default) { await using var subscription = Streams.SubscribeToStream(streamId, FromStream.Start, userCredentials: userCredentials); + await subscription .Messages.OfType().AnyAsync().AsTask() .WithTimeout(TimeSpan.FromMilliseconds(TimeoutMs)); @@ -253,16 +255,17 @@ await subscription public async Task SubscribeToAll(UserCredentials? userCredentials = default) { await using var subscription = Streams.SubscribeToAll(FromAll.Start, userCredentials: userCredentials); + await subscription .Messages.OfType().AnyAsync().AsTask() .WithTimeout(TimeSpan.FromMilliseconds(TimeoutMs)); } - - public async Task CreateStreamWithMeta(StreamMetadata metadata, [CallerMemberName] string streamId = "") { + + public async Task CreateStreamWithMeta(StreamMetadata metadataPermanent, [CallerMemberName] string streamId = "") { await Streams.SetStreamMetadataAsync( streamId, StreamState.NoStream, - metadata, + metadataPermanent, userCredentials: TestCredentials.TestAdmin ) .WithTimeout(TimeSpan.FromMilliseconds(TimeoutMs)); diff --git a/test/EventStore.Client.Streams.Tests/Security/stream_security_inheritance.cs b/test/EventStore.Client.Tests/Security/StreamSecurityInheritanceTests.cs similarity index 95% rename from test/EventStore.Client.Streams.Tests/Security/stream_security_inheritance.cs rename to test/EventStore.Client.Tests/Security/StreamSecurityInheritanceTests.cs index 1e1221e16..247da1a3e 100644 --- a/test/EventStore.Client.Streams.Tests/Security/stream_security_inheritance.cs +++ b/test/EventStore.Client.Tests/Security/StreamSecurityInheritanceTests.cs @@ -1,8 +1,12 @@ -namespace EventStore.Client.Streams.Tests; +using EventStore.Client.Tests.TestNode; +using EventStore.Client.Tests; + +namespace EventStore.Client.Tests; [Trait("Category", "Security")] -public class stream_security_inheritance(ITestOutputHelper output, stream_security_inheritance.CustomFixture fixture) : EventStoreTests(output, fixture) { - [Fact] +public class StreamSecurityInheritanceTests(ITestOutputHelper output, StreamSecurityInheritanceTests.CustomFixture fixture) + : KurrentTemporaryTests(output, fixture) { + [RetryFact] public async Task acl_inheritance_is_working_properly_on_user_streams() { await Assert.ThrowsAsync(() => Fixture.AppendStream("user-no-acl")); await Fixture.AppendStream("user-no-acl", TestCredentials.TestUser1); @@ -54,7 +58,7 @@ public async Task acl_inheritance_is_working_properly_on_user_streams_when_not_a await Fixture.ReadEvent("user-no-acl"); } - [Fact] + [RetryFact] public async Task acl_inheritance_is_working_properly_on_system_streams() { await Assert.ThrowsAsync(() => Fixture.AppendStream("$sys-no-acl")); await Fixture.AppendStream("$sys-no-acl", TestCredentials.TestUser1); @@ -181,4 +185,4 @@ await Streams.SetStreamMetadataAsync( ); } } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Streams.Tests/Security/subscribe_to_all_security.cs b/test/EventStore.Client.Tests/Security/SubscribeToAllSecurityTests.cs similarity index 77% rename from test/EventStore.Client.Streams.Tests/Security/subscribe_to_all_security.cs rename to test/EventStore.Client.Tests/Security/SubscribeToAllSecurityTests.cs index 48b62a52d..c42e552f0 100644 --- a/test/EventStore.Client.Streams.Tests/Security/subscribe_to_all_security.cs +++ b/test/EventStore.Client.Tests/Security/SubscribeToAllSecurityTests.cs @@ -1,7 +1,10 @@ -namespace EventStore.Client.Streams.Tests; +using EventStore.Client.Tests.TestNode; +using EventStore.Client.Tests; + +namespace EventStore.Client.Tests; [Trait("Category", "Security")] -public class subscribe_to_all_security(ITestOutputHelper output, SecurityFixture fixture) : EventStoreTests(output, fixture) { +public class SubscribeToAllSecurityTests(ITestOutputHelper output, SecurityFixture fixture) : KurrentTemporaryTests(output, fixture) { [Fact] public async Task subscribing_to_all_with_not_existing_credentials_is_not_authenticated() => await Assert.ThrowsAsync(() => Fixture.SubscribeToAll(TestCredentials.TestBadUser)); @@ -18,4 +21,4 @@ public async Task subscribing_to_all_with_not_authorized_user_credentials_is_den [Fact] public async Task subscribing_to_all_with_admin_user_credentials_succeeds() => await Fixture.SubscribeToAll(TestCredentials.TestAdmin); -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Streams.Tests/Security/subscribe_to_stream_security.cs b/test/EventStore.Client.Tests/Security/SubscribeToStreamSecurityTests.cs similarity index 78% rename from test/EventStore.Client.Streams.Tests/Security/subscribe_to_stream_security.cs rename to test/EventStore.Client.Tests/Security/SubscribeToStreamSecurityTests.cs index 0af3f8da9..6e34a6144 100644 --- a/test/EventStore.Client.Streams.Tests/Security/subscribe_to_stream_security.cs +++ b/test/EventStore.Client.Tests/Security/SubscribeToStreamSecurityTests.cs @@ -1,13 +1,14 @@ -namespace EventStore.Client.Streams.Tests; +using EventStore.Client.Tests.TestNode; +using EventStore.Client.Tests; + +namespace EventStore.Client.Tests; [Trait("Category", "Security")] -public class subscribe_to_stream_security(ITestOutputHelper output, SecurityFixture fixture) - : EventStoreTests(output, fixture) { +public class SubscribeToStreamSecurityTests(ITestOutputHelper output, SecurityFixture fixture) + : KurrentTemporaryTests(output, fixture) { [Fact] public async Task subscribing_to_stream_with_not_existing_credentials_is_not_authenticated() => - await Assert.ThrowsAsync( - () => Fixture.SubscribeToStream(SecurityFixture.ReadStream, TestCredentials.TestBadUser) - ); + await Assert.ThrowsAsync(() => Fixture.SubscribeToStream(SecurityFixture.ReadStream, TestCredentials.TestBadUser)); [Fact] public async Task subscribing_to_stream_with_no_credentials_is_denied() => @@ -15,9 +16,7 @@ public async Task subscribing_to_stream_with_no_credentials_is_denied() => [Fact] public async Task subscribing_to_stream_with_not_authorized_user_credentials_is_denied() => - await Assert.ThrowsAsync( - () => Fixture.SubscribeToStream(SecurityFixture.ReadStream, TestCredentials.TestUser2) - ); + await Assert.ThrowsAsync(() => Fixture.SubscribeToStream(SecurityFixture.ReadStream, TestCredentials.TestUser2)); [Fact] public async Task reading_stream_with_authorized_user_credentials_succeeds() { @@ -39,9 +38,7 @@ public async Task subscribing_to_no_acl_stream_succeeds_when_no_credentials_are_ [Fact] public async Task subscribing_to_no_acl_stream_is_not_authenticated_when_not_existing_credentials_are_passed() => - await Assert.ThrowsAsync( - () => Fixture.SubscribeToStream(SecurityFixture.NoAclStream, TestCredentials.TestBadUser) - ); + await Assert.ThrowsAsync(() => Fixture.SubscribeToStream(SecurityFixture.NoAclStream, TestCredentials.TestBadUser)); [Fact] public async Task subscribing_to_no_acl_stream_succeeds_when_any_existing_user_credentials_are_passed() { @@ -65,9 +62,7 @@ public async Task subscribing_to_all_access_normal_stream_succeeds_when_no_crede [Fact] public async Task subscribing_to_all_access_normal_stream_is_not_authenticated_when_not_existing_credentials_are_passed() => - await Assert.ThrowsAsync( - () => Fixture.SubscribeToStream(SecurityFixture.NormalAllStream, TestCredentials.TestBadUser) - ); + await Assert.ThrowsAsync(() => Fixture.SubscribeToStream(SecurityFixture.NormalAllStream, TestCredentials.TestBadUser)); [Fact] public async Task subscribing_to_all_access_normal_stream_succeeds_when_any_existing_user_credentials_are_passed() { diff --git a/test/EventStore.Client.Streams.Tests/Security/system_stream_security.cs b/test/EventStore.Client.Tests/Security/SystemStreamSecurityTests.cs similarity index 94% rename from test/EventStore.Client.Streams.Tests/Security/system_stream_security.cs rename to test/EventStore.Client.Tests/Security/SystemStreamSecurityTests.cs index c4e001b4a..f5ed68732 100644 --- a/test/EventStore.Client.Streams.Tests/Security/system_stream_security.cs +++ b/test/EventStore.Client.Tests/Security/SystemStreamSecurityTests.cs @@ -1,7 +1,10 @@ -namespace EventStore.Client.Streams.Tests; +using EventStore.Client.Tests.TestNode; +using EventStore.Client.Tests; + +namespace EventStore.Client.Tests; [Trait("Category", "Security")] -public class system_stream_security(ITestOutputHelper output, SecurityFixture fixture) : EventStoreTests(output, fixture) { +public class SystemStreamSecurityTests(ITestOutputHelper output, SecurityFixture fixture) : KurrentTemporaryTests(output, fixture) { [Fact] public async Task operations_on_system_stream_with_no_acl_set_fail_for_non_admin() { await Assert.ThrowsAsync(() => Fixture.ReadEvent("$system-no-acl", TestCredentials.TestUser1)); @@ -40,7 +43,9 @@ public async Task operations_on_system_stream_with_acl_set_to_usual_user_fail_fo await Assert.ThrowsAsync(() => Fixture.AppendStream(SecurityFixture.SystemAclStream, TestCredentials.TestUser2)); await Assert.ThrowsAsync(() => Fixture.ReadMeta(SecurityFixture.SystemAclStream, TestCredentials.TestUser2)); - await Assert.ThrowsAsync(() => Fixture.WriteMeta(SecurityFixture.SystemAclStream, TestCredentials.TestUser2, TestCredentials.TestUser1.Username)); + await Assert.ThrowsAsync( + () => Fixture.WriteMeta(SecurityFixture.SystemAclStream, TestCredentials.TestUser2, TestCredentials.TestUser1.Username) + ); await Assert.ThrowsAsync(() => Fixture.SubscribeToStream(SecurityFixture.SystemAclStream, TestCredentials.TestUser2)); } diff --git a/test/EventStore.Client.Streams.Tests/Security/write_stream_meta_security.cs b/test/EventStore.Client.Tests/Security/WriteStreamMetaSecurityTests.cs similarity index 86% rename from test/EventStore.Client.Streams.Tests/Security/write_stream_meta_security.cs rename to test/EventStore.Client.Tests/Security/WriteStreamMetaSecurityTests.cs index 9b003795c..d2ace8af4 100644 --- a/test/EventStore.Client.Streams.Tests/Security/write_stream_meta_security.cs +++ b/test/EventStore.Client.Tests/Security/WriteStreamMetaSecurityTests.cs @@ -1,7 +1,10 @@ -namespace EventStore.Client.Streams.Tests; +using EventStore.Client.Tests.TestNode; +using EventStore.Client.Tests; + +namespace EventStore.Client.Tests; [Trait("Category", "Security")] -public class write_stream_meta_security(ITestOutputHelper output, SecurityFixture fixture) : EventStoreTests(output, fixture) { +public class WriteStreamMetaSecurityTests(ITestOutputHelper output, SecurityFixture fixture) : KurrentTemporaryTests(output, fixture) { [Fact] public async Task writing_meta_with_not_existing_credentials_is_not_authenticated() => await Assert.ThrowsAsync( @@ -19,7 +22,8 @@ public async Task writing_meta_to_stream_with_no_credentials_is_denied() => [Fact] public async Task writing_meta_to_stream_with_not_authorized_user_credentials_is_denied() => - await Assert.ThrowsAsync(() => + await Assert.ThrowsAsync( + () => Fixture.WriteMeta( SecurityFixture.MetaWriteStream, TestCredentials.TestUser2, @@ -66,7 +70,9 @@ public async Task writing_meta_to_all_access_normal_stream_succeeds_when_no_cred [Fact] public async Task writing_meta_to_all_access_normal_stream_is_not_authenticated_when_not_existing_credentials_are_passed() => - await Assert.ThrowsAsync(() => Fixture.WriteMeta(SecurityFixture.NormalAllStream, TestCredentials.TestBadUser, SystemRoles.All)); + await Assert.ThrowsAsync( + () => Fixture.WriteMeta(SecurityFixture.NormalAllStream, TestCredentials.TestBadUser, SystemRoles.All) + ); [Fact] public async Task @@ -78,4 +84,4 @@ public async Task [Fact] public async Task writing_meta_to_all_access_normal_stream_succeeds_when_admin_user_credentials_are_passed() => await Fixture.WriteMeta(SecurityFixture.NormalAllStream, TestCredentials.TestAdmin, SystemRoles.All); -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Streams.Tests/Security/write_stream_security.cs b/test/EventStore.Client.Tests/Security/WriteStreamSecurityTests.cs similarity index 93% rename from test/EventStore.Client.Streams.Tests/Security/write_stream_security.cs rename to test/EventStore.Client.Tests/Security/WriteStreamSecurityTests.cs index d002d37c9..aec66866a 100644 --- a/test/EventStore.Client.Streams.Tests/Security/write_stream_security.cs +++ b/test/EventStore.Client.Tests/Security/WriteStreamSecurityTests.cs @@ -1,8 +1,8 @@ -namespace EventStore.Client.Streams.Tests; +namespace EventStore.Client.Tests; [Trait("Category", "Security")] -public class write_stream_security : IClassFixture { - public write_stream_security(ITestOutputHelper output, SecurityFixture fixture) => Fixture = fixture.With(x => x.CaptureTestRun(output)); +public class WriteStreamSecurityTests : IClassFixture { + public WriteStreamSecurityTests(ITestOutputHelper output, SecurityFixture fixture) => Fixture = fixture.With(x => x.CaptureTestRun(output)); SecurityFixture Fixture { get; } @@ -67,4 +67,4 @@ public async Task writing_to_all_access_normal_stream_succeeds_when_any_existing [Fact] public async Task writing_to_all_access_normal_stream_succeeds_when_any_admin_user_credentials_are_passed() => await Fixture.AppendStream(SecurityFixture.NormalAllStream, TestCredentials.TestAdmin); -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Tests/SharingProviderTests.cs b/test/EventStore.Client.Tests/SharingProviderTests.cs index ddb4c9d5f..efebe5b09 100644 --- a/test/EventStore.Client.Tests/SharingProviderTests.cs +++ b/test/EventStore.Client.Tests/SharingProviderTests.cs @@ -3,7 +3,7 @@ namespace EventStore.Client.Tests; public class SharingProviderTests { - [Fact] + [RetryFact] public async Task CanGetCurrent() { using var sut = new SharingProvider( async (x, _) => x + 1, @@ -14,7 +14,7 @@ public async Task CanGetCurrent() { Assert.Equal(6, await sut.CurrentAsync); } - [Fact] + [RetryFact] public async Task CanReset() { var count = 0; using var sut = new SharingProvider( @@ -28,7 +28,7 @@ public async Task CanReset() { Assert.Equal(1, await sut.CurrentAsync); } - [Fact] + [RetryFact] public async Task CanReturnBroken() { Action? onBroken = null; var count = 0; @@ -50,7 +50,7 @@ public async Task CanReturnBroken() { Assert.Equal(2, await sut.CurrentAsync); } - [Fact] + [RetryFact] public async Task CanReturnSameBoxTwice() { Action? onBroken = null; var count = 0; @@ -74,7 +74,7 @@ public async Task CanReturnSameBoxTwice() { Assert.Equal(1, await sut.CurrentAsync); } - [Fact] + [RetryFact] public async Task CanReturnPendingBox() { var trigger = new SemaphoreSlim(0); Action? onBroken = null; @@ -113,7 +113,7 @@ public async Task CanReturnPendingBox() { Assert.Equal(1, count); } - [Fact] + [RetryFact] public async Task FactoryCanThrow() { using var sut = new SharingProvider( (x, _) => throw new($"input {x}"), @@ -130,7 +130,7 @@ public async Task FactoryCanThrow() { // safe to call onBroken before the factory has returned, but it doesn't // do anything because the box is not populated yet. // the factory has to indicate failure by throwing. - [Fact] + [RetryFact] public async Task FactoryCanCallOnBrokenSynchronously() { using var sut = new SharingProvider( async (x, onBroken) => { @@ -147,7 +147,7 @@ public async Task FactoryCanCallOnBrokenSynchronously() { Assert.Equal(0, await sut.CurrentAsync); } - [Fact] + [RetryFact] public async Task FactoryCanCallOnBrokenSynchronouslyAndThrow() { using var sut = new SharingProvider( async (x, onBroken) => { @@ -167,7 +167,7 @@ public async Task FactoryCanCallOnBrokenSynchronouslyAndThrow() { Assert.Equal("input 0", ex.Message); } - [Fact] + [RetryFact] public async Task StopsAfterBeingDisposed() { Action? onBroken = null; var count = 0; @@ -193,7 +193,7 @@ public async Task StopsAfterBeingDisposed() { Assert.Equal(1, count); } - [Fact] + [RetryFact] public async Task ExampleUsage() { // factory waits to be signalled by completeConstruction being released // sometimes the factory succeeds, sometimes it throws. @@ -263,4 +263,4 @@ async Task Factory(int input, Action onBroken) { await constructionCompleted.WaitAsync(); Assert.Equal(0, await sut.CurrentAsync); } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Tests/StreamPositionTests.cs b/test/EventStore.Client.Tests/StreamPositionTests.cs index 22578b548..d70475dd0 100644 --- a/test/EventStore.Client.Tests/StreamPositionTests.cs +++ b/test/EventStore.Client.Tests/StreamPositionTests.cs @@ -5,17 +5,17 @@ namespace EventStore.Client.Tests; public class StreamPositionTests : ValueObjectTests { public StreamPositionTests() : base(new ScenarioFixture()) { } - [Fact] + [RetryFact] public void IsComparable() => Assert.IsAssignableFrom>(_fixture.Create()); - [Fact] + [RetryFact] public void AdditionOperator() { var sut = StreamPosition.Start; Assert.Equal(new(1), sut + 1); Assert.Equal(new(1), 1 + sut); } - [Fact] + [RetryFact] public void NextReturnsExpectedResult() { var sut = StreamPosition.Start; Assert.Equal(sut + 1, sut.Next()); @@ -33,7 +33,7 @@ public void AdditionOutOfBoundsThrows(StreamPosition StreamPosition, ulong opera Assert.Throws(() => operand + StreamPosition); } - [Fact] + [RetryFact] public void SubtractionOperator() { var sut = new StreamPosition(1); Assert.Equal(new(0), sut - 1); @@ -64,17 +64,17 @@ public void ArgumentOutOfRange(ulong value) { Assert.Equal(nameof(value), ex.ParamName); } - [Fact] + [RetryFact] public void FromStreamPositionEndThrows() => Assert.Throws(() => StreamRevision.FromStreamPosition(StreamPosition.End)); - [Fact] + [RetryFact] public void FromStreamPositionReturnsExpectedResult() { var result = StreamPosition.FromStreamRevision(new(0)); Assert.Equal(new(0), result); } - [Fact] + [RetryFact] public void ExplicitConversionToUInt64ReturnsExpectedResult() { const ulong value = 0UL; @@ -82,7 +82,7 @@ public void ExplicitConversionToUInt64ReturnsExpectedResult() { Assert.Equal(value, actual); } - [Fact] + [RetryFact] public void ImplicitConversionToUInt64ReturnsExpectedResult() { const ulong value = 0UL; @@ -90,7 +90,7 @@ public void ImplicitConversionToUInt64ReturnsExpectedResult() { Assert.Equal(value, actual); } - [Fact] + [RetryFact] public void ExplicitConversionToStreamPositionReturnsExpectedResult() { const ulong value = 0UL; @@ -99,7 +99,7 @@ public void ExplicitConversionToStreamPositionReturnsExpectedResult() { Assert.Equal(expected, actual); } - [Fact] + [RetryFact] public void ImplicitConversionToStreamPositionReturnsExpectedResult() { const ulong value = 0UL; @@ -109,14 +109,14 @@ public void ImplicitConversionToStreamPositionReturnsExpectedResult() { Assert.Equal(expected, actual); } - [Fact] + [RetryFact] public void ToStringExpectedResult() { var expected = 0UL.ToString(); Assert.Equal(expected, new StreamPosition(0UL).ToString()); } - [Fact] + [RetryFact] public void ToUInt64ExpectedResult() { var expected = 0UL; @@ -126,4 +126,4 @@ public void ToUInt64ExpectedResult() { class ScenarioFixture : Fixture { public ScenarioFixture() => Customize(composer => composer.FromFactory(value => new(value))); } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Tests/StreamRevisionTests.cs b/test/EventStore.Client.Tests/StreamRevisionTests.cs index e42039946..ddc479251 100644 --- a/test/EventStore.Client.Tests/StreamRevisionTests.cs +++ b/test/EventStore.Client.Tests/StreamRevisionTests.cs @@ -5,17 +5,17 @@ namespace EventStore.Client.Tests; public class StreamRevisionTests : ValueObjectTests { public StreamRevisionTests() : base(new ScenarioFixture()) { } - [Fact] + [RetryFact] public void IsComparable() => Assert.IsAssignableFrom>(_fixture.Create()); - [Fact] + [RetryFact] public void AdditionOperator() { var sut = new StreamRevision(0); Assert.Equal(new(1), sut + 1); Assert.Equal(new(1), 1 + sut); } - [Fact] + [RetryFact] public void NextReturnsExpectedResult() { var sut = new StreamRevision(0); Assert.Equal(sut + 1, sut.Next()); @@ -32,7 +32,7 @@ public void AdditionOutOfBoundsThrows(StreamRevision streamRevision, ulong opera Assert.Throws(() => operand + streamRevision); } - [Fact] + [RetryFact] public void SubtractionOperator() { var sut = new StreamRevision(1); Assert.Equal(new(0), sut - 1); @@ -63,17 +63,17 @@ public void ArgumentOutOfRange(ulong value) { Assert.Equal(nameof(value), ex.ParamName); } - [Fact] + [RetryFact] public void FromStreamPositionEndThrows() => Assert.Throws(() => StreamRevision.FromStreamPosition(StreamPosition.End)); - [Fact] + [RetryFact] public void FromStreamPositionReturnsExpectedResult() { var result = StreamRevision.FromStreamPosition(StreamPosition.Start); Assert.Equal(new(0), result); } - [Fact] + [RetryFact] public void ExplicitConversionToUInt64ReturnsExpectedResult() { const ulong value = 0UL; @@ -82,7 +82,7 @@ public void ExplicitConversionToUInt64ReturnsExpectedResult() { Assert.Equal(value, actual); } - [Fact] + [RetryFact] public void ImplicitConversionToUInt64ReturnsExpectedResult() { const ulong value = 0UL; @@ -91,7 +91,7 @@ public void ImplicitConversionToUInt64ReturnsExpectedResult() { Assert.Equal(value, actual); } - [Fact] + [RetryFact] public void ExplicitConversionToStreamRevisionReturnsExpectedResult() { const ulong value = 0UL; @@ -101,7 +101,7 @@ public void ExplicitConversionToStreamRevisionReturnsExpectedResult() { Assert.Equal(expected, actual); } - [Fact] + [RetryFact] public void ImplicitConversionToStreamRevisionReturnsExpectedResult() { const ulong value = 0UL; @@ -111,14 +111,14 @@ public void ImplicitConversionToStreamRevisionReturnsExpectedResult() { Assert.Equal(expected, actual); } - [Fact] + [RetryFact] public void ToStringExpectedResult() { var expected = 0UL.ToString(); Assert.Equal(expected, new StreamRevision(0UL).ToString()); } - [Fact] + [RetryFact] public void ToUInt64ExpectedResult() { var expected = 0UL; @@ -128,4 +128,4 @@ public void ToUInt64ExpectedResult() { class ScenarioFixture : Fixture { public ScenarioFixture() => Customize(composer => composer.FromFactory(value => new(value))); } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Tests/StreamStateTests.cs b/test/EventStore.Client.Tests/StreamStateTests.cs index b4d0424c6..7bd086099 100644 --- a/test/EventStore.Client.Tests/StreamStateTests.cs +++ b/test/EventStore.Client.Tests/StreamStateTests.cs @@ -7,9 +7,9 @@ public class StreamStateTests : ValueObjectTests { public StreamStateTests() : base(new ScenarioFixture()) { } public static IEnumerable ArgumentOutOfRangeTestCases() { - yield return new object?[] { 0 }; - yield return new object?[] { int.MaxValue }; - yield return new object?[] { -3 }; + yield return [0]; + yield return [int.MaxValue]; + yield return [-3]; } [Theory] @@ -19,23 +19,23 @@ public void ArgumentOutOfRange(int value) { Assert.Equal(nameof(value), ex.ParamName); } - [Fact] + [RetryFact] public void ExplicitConversionExpectedResult() { const int expected = 1; var actual = (int)new StreamState(expected); Assert.Equal(expected, actual); } - [Fact] + [RetryFact] public void ImplicitConversionExpectedResult() { const int expected = 1; Assert.Equal(expected, new StreamState(expected)); } public static IEnumerable ToStringTestCases() { - yield return new object?[] { StreamState.Any, nameof(StreamState.Any) }; - yield return new object?[] { StreamState.NoStream, nameof(StreamState.NoStream) }; - yield return new object?[] { StreamState.StreamExists, nameof(StreamState.StreamExists) }; + yield return [StreamState.Any, nameof(StreamState.Any)]; + yield return [StreamState.NoStream, nameof(StreamState.NoStream)]; + yield return [StreamState.StreamExists, nameof(StreamState.StreamExists)]; } [Theory] @@ -54,4 +54,4 @@ class ScenarioFixture : Fixture { public ScenarioFixture() => Customize(composer => composer.FromFactory(() => Instances[Interlocked.Increment(ref RefCount) % Instances.Length])); } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Streams.Tests/Append/append_to_stream.cs b/test/EventStore.Client.Tests/Streams/AppendTests.cs similarity index 55% rename from test/EventStore.Client.Streams.Tests/Append/append_to_stream.cs rename to test/EventStore.Client.Tests/Streams/AppendTests.cs index 6cd5b813d..3b2792b8f 100644 --- a/test/EventStore.Client.Streams.Tests/Append/append_to_stream.cs +++ b/test/EventStore.Client.Tests/Streams/AppendTests.cs @@ -1,19 +1,12 @@ -using System.Text; using Grpc.Core; +using Humanizer; -namespace EventStore.Client.Streams.Tests.Append; +namespace EventStore.Client.Tests.Streams; [Trait("Category", "Target:Stream")] [Trait("Category", "Operation:Append")] -public class append_to_stream(ITestOutputHelper output, EventStoreFixture fixture) - : EventStoreTests(output, fixture) { - public static IEnumerable ExpectedVersionCreateStreamTestCases() { - yield return new object?[] { StreamState.Any }; - yield return new object?[] { StreamState.NoStream }; - } - - [Theory] - [MemberData(nameof(ExpectedVersionCreateStreamTestCases))] +public class AppendTests(ITestOutputHelper output, KurrentPermanentFixture fixture) : EventStorePermanentTests(output, fixture) { + [Theory, ExpectedVersionCreateStreamTestCases] public async Task appending_zero_events(StreamState expectedStreamState) { var stream = $"{Fixture.GetStreamName()}_{expectedStreamState}"; @@ -33,8 +26,7 @@ await Fixture.Streams .ShouldThrowAsync(ex => ex.Stream.ShouldBe(stream)); } - [Theory] - [MemberData(nameof(ExpectedVersionCreateStreamTestCases))] + [Theory, ExpectedVersionCreateStreamTestCases] public async Task appending_zero_events_again(StreamState expectedStreamState) { var stream = $"{Fixture.GetStreamName()}_{expectedStreamState}"; @@ -54,8 +46,7 @@ await Fixture.Streams .ShouldThrowAsync(ex => ex.Stream.ShouldBe(stream)); } - [Theory] - [MemberData(nameof(ExpectedVersionCreateStreamTestCases))] + [Theory, ExpectedVersionCreateStreamTestCases] public async Task create_stream_expected_version_on_first_write_if_does_not_exist(StreamState expectedStreamState) { var stream = $"{Fixture.GetStreamName()}_{expectedStreamState}"; @@ -73,7 +64,7 @@ public async Task create_stream_expected_version_on_first_write_if_does_not_exis Assert.Equal(1, count); } - [Fact] + [RetryFact] public async Task multiple_idempotent_writes() { var stream = Fixture.GetStreamName(); var events = Fixture.CreateTestEvents(4).ToArray(); @@ -85,7 +76,7 @@ public async Task multiple_idempotent_writes() { Assert.Equal(new(3), writeResult.NextExpectedStreamRevision); } - [Fact] + [RetryFact] public async Task multiple_idempotent_writes_with_same_id_bug_case() { var stream = Fixture.GetStreamName(); @@ -97,7 +88,7 @@ public async Task multiple_idempotent_writes_with_same_id_bug_case() { Assert.Equal(new(5), writeResult.NextExpectedStreamRevision); } - [Fact] + [RetryFact] public async Task in_case_where_multiple_writes_of_multiple_events_with_the_same_ids_using_expected_version_any_then_next_expected_version_is_unreliable() { var stream = Fixture.GetStreamName(); @@ -114,7 +105,7 @@ public async Task Assert.Equal(new(0), writeResult.NextExpectedStreamRevision); } - [Fact] + [RetryFact] public async Task in_case_where_multiple_writes_of_multiple_events_with_the_same_ids_using_expected_version_nostream_then_next_expected_version_is_correct() { var stream = Fixture.GetStreamName(); @@ -132,7 +123,7 @@ public async Task Assert.Equal(streamRevision, writeResult.NextExpectedStreamRevision); } - [Fact] + [RetryFact] public async Task writing_with_correct_expected_version_to_deleted_stream_throws_stream_deleted() { var stream = Fixture.GetStreamName(); @@ -143,7 +134,7 @@ await Fixture.Streams .ShouldThrowAsync(); } - [Fact] + [RetryFact] public async Task returns_log_position_when_writing() { var stream = Fixture.GetStreamName(); @@ -157,7 +148,7 @@ public async Task returns_log_position_when_writing() { Assert.True(0 < result.LogPosition.CommitPosition); } - [Fact] + [RetryFact] public async Task writing_with_any_expected_version_to_deleted_stream_throws_stream_deleted() { var stream = Fixture.GetStreamName(); await Fixture.Streams.TombstoneAsync(stream, StreamState.NoStream); @@ -167,7 +158,7 @@ await Fixture.Streams .ShouldThrowAsync(); } - [Fact] + [RetryFact] public async Task writing_with_invalid_expected_version_to_deleted_stream_throws_stream_deleted() { var stream = Fixture.GetStreamName(); @@ -178,7 +169,7 @@ await Fixture.Streams .ShouldThrowAsync(); } - [Fact] + [RetryFact] public async Task append_with_correct_expected_version_to_existing_stream() { var stream = Fixture.GetStreamName(); @@ -197,7 +188,7 @@ public async Task append_with_correct_expected_version_to_existing_stream() { Assert.Equal(new(1), writeResult.NextExpectedStreamRevision); } - [Fact] + [RetryFact] public async Task append_with_any_expected_version_to_existing_stream() { var stream = Fixture.GetStreamName(); @@ -218,7 +209,7 @@ public async Task append_with_any_expected_version_to_existing_stream() { Assert.Equal(new(1), writeResult.NextExpectedStreamRevision); } - [Fact] + [RetryFact] public async Task appending_with_wrong_expected_version_to_existing_stream_throws_wrong_expected_version() { var stream = Fixture.GetStreamName(); @@ -232,7 +223,7 @@ public async Task appending_with_wrong_expected_version_to_existing_stream_throw ex.ExpectedStreamRevision.ShouldBe(new(999)); } - [Fact] + [RetryFact] public async Task appending_with_wrong_expected_version_to_existing_stream_returns_wrong_expected_version() { var stream = Fixture.GetStreamName(); @@ -248,7 +239,7 @@ public async Task appending_with_wrong_expected_version_to_existing_stream_retur Assert.Equal(new(1), wrongExpectedVersionResult.NextExpectedStreamRevision); } - [Fact] + [RetryFact] public async Task append_with_stream_exists_expected_version_to_existing_stream() { var stream = Fixture.GetStreamName(); @@ -261,7 +252,7 @@ await Fixture.Streams.AppendToStreamAsync( ); } - [Fact] + [RetryFact] public async Task append_with_stream_exists_expected_version_to_stream_with_multiple_events() { var stream = Fixture.GetStreamName(); @@ -275,7 +266,7 @@ await Fixture.Streams.AppendToStreamAsync( ); } - [Fact] + [RetryFact] public async Task append_with_stream_exists_expected_version_if_metadata_stream_exists() { var stream = Fixture.GetStreamName(); @@ -292,7 +283,7 @@ await Fixture.Streams.AppendToStreamAsync( ); } - [Fact] + [RetryFact] public async Task appending_with_stream_exists_expected_version_and_stream_does_not_exist_throws_wrong_expected_version() { var stream = Fixture.GetStreamName(); @@ -304,7 +295,7 @@ public async Task ex.ActualStreamRevision.ShouldBe(StreamRevision.None); } - [Fact] + [RetryFact] public async Task appending_with_stream_exists_expected_version_and_stream_does_not_exist_returns_wrong_expected_version() { var stream = Fixture.GetStreamName(); @@ -321,7 +312,7 @@ public async Task Assert.Equal(StreamRevision.None, wrongExpectedVersionResult.NextExpectedStreamRevision); } - [Fact] + [RetryFact] public async Task appending_with_stream_exists_expected_version_to_hard_deleted_stream_throws_stream_deleted() { var stream = Fixture.GetStreamName(); @@ -332,7 +323,7 @@ await Fixture.Streams .ShouldThrowAsync(); } - [Fact] + [RetryFact] public async Task appending_with_stream_exists_expected_version_to_deleted_stream_throws_stream_deleted() { var stream = Fixture.GetStreamName(); @@ -345,7 +336,7 @@ await Fixture.Streams .ShouldThrowAsync(); } - [Fact] + [RetryFact] public async Task can_append_multiple_events_at_once() { var stream = Fixture.GetStreamName(); @@ -358,7 +349,7 @@ public async Task can_append_multiple_events_at_once() { Assert.Equal(new(99), writeResult.NextExpectedStreamRevision); } - [Fact] + [RetryFact] public async Task returns_failure_status_when_conditionally_appending_with_version_mismatch() { var stream = Fixture.GetStreamName(); @@ -374,7 +365,7 @@ public async Task returns_failure_status_when_conditionally_appending_with_versi ); } - [Fact] + [RetryFact] public async Task returns_success_status_when_conditionally_appending_with_matching_version() { var stream = Fixture.GetStreamName(); @@ -390,7 +381,7 @@ public async Task returns_success_status_when_conditionally_appending_with_match ); } - [Fact] + [RetryFact] public async Task returns_failure_status_when_conditionally_appending_to_a_deleted_stream() { var stream = Fixture.GetStreamName(); @@ -407,7 +398,7 @@ public async Task returns_failure_status_when_conditionally_appending_to_a_delet Assert.Equal(ConditionalWriteResult.StreamDeleted, result); } - [Fact] + [RetryFact] public async Task expected_version_no_stream() { var result = await Fixture.Streams.AppendToStreamAsync( Fixture.GetStreamName(), @@ -418,7 +409,7 @@ public async Task expected_version_no_stream() { Assert.Equal(new(0), result!.NextExpectedStreamRevision); } - [Fact] + [RetryFact] public async Task expected_version_no_stream_returns_position() { var result = await Fixture.Streams.AppendToStreamAsync( Fixture.GetStreamName(), @@ -429,7 +420,7 @@ public async Task expected_version_no_stream_returns_position() { Assert.True(result.LogPosition > Position.Start); } - [Fact] + [RetryFact] public async Task with_timeout_any_stream_revision_fails_when_operation_expired() { var stream = Fixture.GetStreamName(); @@ -443,7 +434,7 @@ public async Task with_timeout_any_stream_revision_fails_when_operation_expired( ex.StatusCode.ShouldBe(StatusCode.DeadlineExceeded); } - [Fact] + [RetryFact] public async Task with_timeout_stream_revision_fails_when_operation_expired() { var stream = Fixture.GetStreamName(); @@ -459,7 +450,7 @@ public async Task with_timeout_stream_revision_fails_when_operation_expired() { ex.StatusCode.ShouldBe(StatusCode.DeadlineExceeded); } - [Fact] + [RetryFact] public async Task when_events_enumerator_throws_the_write_does_not_succeed() { var streamName = Fixture.GetStreamName(); @@ -477,4 +468,317 @@ await Fixture.Streams state.ShouldBe(ReadState.StreamNotFound); } + + [RetryFact] + public async Task succeeds_when_size_is_less_than_max_append_size() { + // Arrange + var maxAppendSize = (uint)100.Kilobytes().Bytes; + var stream = Fixture.GetStreamName(); + + // Act + var (events, size) = Fixture.CreateTestEventsUpToMaxSize(maxAppendSize - 1); + + // Assert + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events); + } + + [RetryFact] + public async Task fails_when_size_exceeds_max_append_size() { + // Arrange + var maxAppendSize = (uint)100.Kilobytes().Bytes; + var stream = Fixture.GetStreamName(); + var eventsAppendSize = maxAppendSize * 2; + + // Act + var (events, size) = Fixture.CreateTestEventsUpToMaxSize(eventsAppendSize); + + // Assert + size.ShouldBeGreaterThan(maxAppendSize); + + var ex = await Fixture.Streams + .AppendToStreamAsync(stream, StreamState.NoStream, events) + .ShouldThrowAsync(); + + ex.MaxAppendSize.ShouldBe(maxAppendSize); + } + + [RetryFact] + public async Task sequence_0em1_1e0_2e1_3e2_4e3_5e4_0em1_idempotent() { + var stream = Fixture.GetStreamName(); + + var events = Fixture.CreateTestEvents(6).ToArray(); + + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events); + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events.Take(1)); + + var count = await Fixture.Streams + .ReadStreamAsync(Direction.Forwards, stream, StreamPosition.Start, events.Length + 1).CountAsync(); + + Assert.Equal(events.Length, count); + } + + [RetryFact] + public async Task sequence_0em1_1e0_2e1_3e2_4e3_4e4_0any_idempotent() { + var stream = Fixture.GetStreamName(); + + var events = Fixture.CreateTestEvents(6).ToArray(); + + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events); + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.Any, events.Take(1)); + + var count = await Fixture.Streams + .ReadStreamAsync(Direction.Forwards, stream, StreamPosition.Start, events.Length + 1).CountAsync(); + + Assert.Equal(events.Length, count); + } + + [RetryFact] + public async Task sequence_0em1_1e0_2e1_3e2_4e3_5e4_0e5_non_idempotent() { + var stream = Fixture.GetStreamName(); + + var events = Fixture.CreateTestEvents(6).ToArray(); + + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events); + await Fixture.Streams.AppendToStreamAsync(stream, new StreamRevision(5), events.Take(1)); + + var count = await Fixture.Streams + .ReadStreamAsync(Direction.Forwards, stream, StreamPosition.Start, events.Length + 2).CountAsync(); + + Assert.Equal(events.Length + 1, count); + } + + [RetryFact] + public async Task sequence_0em1_1e0_2e1_3e2_4e3_5e4_0e6_throws_wev() { + var stream = Fixture.GetStreamName(); + + var events = Fixture.CreateTestEvents(6).ToArray(); + + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events); + + await Assert.ThrowsAsync(() => Fixture.Streams.AppendToStreamAsync(stream, new StreamRevision(6), events.Take(1))); + } + + [RetryFact] + public async Task sequence_0em1_1e0_2e1_3e2_4e3_5e4_0e6_returns_wev() { + var stream = Fixture.GetStreamName(); + + var events = Fixture.CreateTestEvents(6).ToArray(); + + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events); + + var writeResult = await Fixture.Streams.AppendToStreamAsync( + stream, + new StreamRevision(6), + events.Take(1), + options => options.ThrowOnAppendFailure = false + ); + + Assert.IsType(writeResult); + } + + [RetryFact] + public async Task sequence_0em1_1e0_2e1_3e2_4e3_5e4_0e4_throws_wev() { + var stream = Fixture.GetStreamName(); + + var events = Fixture.CreateTestEvents(6).ToArray(); + + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events); + + await Assert.ThrowsAsync(() => Fixture.Streams.AppendToStreamAsync(stream, new StreamRevision(4), events.Take(1))); + } + + [RetryFact] + public async Task sequence_0em1_1e0_2e1_3e2_4e3_5e4_0e4_returns_wev() { + var stream = Fixture.GetStreamName(); + + var events = Fixture.CreateTestEvents(6).ToArray(); + + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events); + + var writeResult = await Fixture.Streams.AppendToStreamAsync( + stream, + new StreamRevision(4), + events.Take(1), + options => options.ThrowOnAppendFailure = false + ); + + Assert.IsType(writeResult); + } + + [RetryFact] + public async Task sequence_0em1_0e0_non_idempotent() { + var stream = Fixture.GetStreamName(); + + var events = Fixture.CreateTestEvents().ToArray(); + + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events); + await Fixture.Streams.AppendToStreamAsync(stream, new StreamRevision(0), events.Take(1)); + + var count = await Fixture.Streams + .ReadStreamAsync(Direction.Forwards, stream, StreamPosition.Start, events.Length + 2).CountAsync(); + + Assert.Equal(events.Length + 1, count); + } + + [RetryFact] + public async Task sequence_0em1_0any_idempotent() { + var stream = Fixture.GetStreamName(); + + var events = Fixture.CreateTestEvents().ToArray(); + + await Task.Delay(TimeSpan.FromSeconds(30)); + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events); + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.Any, events.Take(1)); + + var count = await Fixture.Streams + .ReadStreamAsync(Direction.Forwards, stream, StreamPosition.Start, events.Length + 1).CountAsync(); + + Assert.Equal(events.Length, count); + } + + [RetryFact] + public async Task sequence_0em1_0em1_idempotent() { + var stream = Fixture.GetStreamName(); + + var events = Fixture.CreateTestEvents().ToArray(); + + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events); + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events.Take(1)); + + var count = await Fixture.Streams + .ReadStreamAsync(Direction.Forwards, stream, StreamPosition.Start, events.Length + 1).CountAsync(); + + Assert.Equal(events.Length, count); + } + + [RetryFact] + public async Task sequence_0em1_1e0_2e1_1any_1any_idempotent() { + var stream = Fixture.GetStreamName(); + + var events = Fixture.CreateTestEvents(3).ToArray(); + + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events); + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.Any, events.Skip(1).Take(1)); + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.Any, events.Skip(1).Take(1)); + + var count = await Fixture.Streams + .ReadStreamAsync(Direction.Forwards, stream, StreamPosition.Start, events.Length + 1).CountAsync(); + + Assert.Equal(events.Length, count); + } + + [RetryFact] + public async Task sequence_S_0em1_1em1_E_S_0em1_E_idempotent() { + var stream = Fixture.GetStreamName(); + + var events = Fixture.CreateTestEvents(2).ToArray(); + + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events); + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events.Take(1)); + + var count = await Fixture.Streams + .ReadStreamAsync(Direction.Forwards, stream, StreamPosition.Start, events.Length + 1).CountAsync(); + + Assert.Equal(events.Length, count); + } + + [RetryFact] + public async Task sequence_S_0em1_1em1_E_S_0any_E_idempotent() { + var stream = Fixture.GetStreamName(); + + var events = Fixture.CreateTestEvents(2).ToArray(); + + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events); + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.Any, events.Take(1)); + + var count = await Fixture.Streams + .ReadStreamAsync(Direction.Forwards, stream, StreamPosition.Start, events.Length + 1).CountAsync(); + + Assert.Equal(events.Length, count); + } + + [RetryFact] + public async Task sequence_S_0em1_1em1_E_S_1e0_E_idempotent() { + var stream = Fixture.GetStreamName(); + + var events = Fixture.CreateTestEvents(2).ToArray(); + + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events); + + await Fixture.Streams.AppendToStreamAsync(stream, new StreamRevision(0), events.Skip(1)); + + var count = await Fixture.Streams + .ReadStreamAsync(Direction.Forwards, stream, StreamPosition.Start, events.Length + 1).CountAsync(); + + Assert.Equal(events.Length, count); + } + + [RetryFact] + public async Task sequence_S_0em1_1em1_E_S_1any_E_idempotent() { + var stream = Fixture.GetStreamName(); + + var events = Fixture.CreateTestEvents(2).ToArray(); + + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events); + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.Any, events.Skip(1).Take(1)); + + var count = await Fixture.Streams + .ReadStreamAsync(Direction.Forwards, stream, StreamPosition.Start, events.Length + 1).CountAsync(); + + Assert.Equal(events.Length, count); + } + + [RetryFact] + public async Task sequence_S_0em1_1em1_E_S_0em1_1em1_2em1_E_idempotancy_fail_throws() { + var stream = Fixture.GetStreamName(); + + var events = Fixture.CreateTestEvents(3).ToArray(); + + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events.Take(2)); + + await Assert.ThrowsAsync( + () => Fixture.Streams.AppendToStreamAsync( + stream, + StreamState.NoStream, + events + ) + ); + } + + [RetryFact] + public async Task sequence_S_0em1_1em1_E_S_0em1_1em1_2em1_E_idempotancy_fail_returns() { + var stream = Fixture.GetStreamName(); + + var events = Fixture.CreateTestEvents(3).ToArray(); + + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events.Take(2)); + + var writeResult = await Fixture.Streams.AppendToStreamAsync( + stream, + StreamState.NoStream, + events, + options => options.ThrowOnAppendFailure = false + ); + + Assert.IsType(writeResult); + } + + // [Fact] + // public async Task sending_and_receiving_large_messages_over_the_hard_limit() { + // uint maxAppendSize = 16 * 1024 * 1024 - 10000; + // var streamName = Fixture.GetStreamName(); + // var largeEvent = Fixture.CreateTestEvents() + // .Select(e => new EventData(e.EventId, "-", new byte[maxAppendSize + 1])); + // + // var ex = await Assert.ThrowsAsync(() => Fixture.Streams.AppendToStreamAsync(streamName, StreamState.NoStream, largeEvent)); + // + // Assert.Equal(StatusCode.ResourceExhausted, ex.StatusCode); + // } + + class ExpectedVersionCreateStreamTestCases : TestCaseGenerator { + protected override IEnumerable Data() { + yield return [StreamState.Any]; + yield return [StreamState.NoStream]; + } + } } diff --git a/test/EventStore.Client.Streams.Tests/Bugs/Obsolete/Issue_104.cs b/test/EventStore.Client.Tests/Streams/Bugs/Obsolete/Issue104.cs similarity index 89% rename from test/EventStore.Client.Streams.Tests/Bugs/Obsolete/Issue_104.cs rename to test/EventStore.Client.Tests/Streams/Bugs/Obsolete/Issue104.cs index 922414820..47386e105 100644 --- a/test/EventStore.Client.Streams.Tests/Bugs/Obsolete/Issue_104.cs +++ b/test/EventStore.Client.Tests/Streams/Bugs/Obsolete/Issue104.cs @@ -1,8 +1,8 @@ -namespace EventStore.Client.Streams.Tests.Bugs.Obsolete; +namespace EventStore.Client.Tests.Bugs.Obsolete; [Trait("Category", "Bug")] [Obsolete("Tests will be removed in future release when older subscriptions APIs are removed from the client")] -public class Issue_104(ITestOutputHelper output, EventStoreFixture fixture) : EventStoreTests(output, fixture) { +public class Issue104(ITestOutputHelper output, KurrentPermanentFixture fixture) : EventStorePermanentTests(output, fixture) { [Fact] public async Task subscription_does_not_send_checkpoint_reached_after_disposal() { var streamName = Fixture.GetStreamName(); diff --git a/test/EventStore.Client.Streams.Tests/Bugs/Obsolete/Issue_2544.cs b/test/EventStore.Client.Tests/Streams/Bugs/Obsolete/Issue2544.cs similarity index 82% rename from test/EventStore.Client.Streams.Tests/Bugs/Obsolete/Issue_2544.cs rename to test/EventStore.Client.Tests/Streams/Bugs/Obsolete/Issue2544.cs index 3d65b01ec..e2353090e 100644 --- a/test/EventStore.Client.Streams.Tests/Bugs/Obsolete/Issue_2544.cs +++ b/test/EventStore.Client.Tests/Streams/Bugs/Obsolete/Issue2544.cs @@ -1,30 +1,29 @@ -#pragma warning disable 1998 +// ReSharper disable InconsistentNaming -namespace EventStore.Client.Streams.Tests.Bugs.Obsolete; +namespace EventStore.Client.Tests.Bugs.Obsolete; [Trait("Category", "Bug")] [Obsolete("Tests will be removed in future release when older subscriptions APIs are removed from the client")] -public class Issue_2544 : IClassFixture { - public Issue_2544(ITestOutputHelper output, EventStoreFixture fixture) { +public class Issue2544 : IClassFixture { + public Issue2544(ITestOutputHelper output, KurrentPermanentFixture fixture) { Fixture = fixture.With(x => x.CaptureTestRun(output)); - - _seen = Enumerable.Range(0, 1 + Batches * BatchSize) + + Seen = Enumerable.Range(0, 1 + Batches * BatchSize) .Select(i => new StreamPosition((ulong)i)) .ToDictionary(r => r, _ => false); - _completed = new(); + Completed = new(); } - EventStoreFixture Fixture { get; } + KurrentPermanentFixture Fixture { get; } const int BatchSize = 18; const int Batches = 4; - - readonly TaskCompletionSource _completed; - readonly Dictionary _seen; - public static IEnumerable TestCases() => - Enumerable.Range(0, 5).Select(i => new object[] { i }); + readonly TaskCompletionSource Completed; + readonly Dictionary Seen; + + public static IEnumerable TestCases() => Enumerable.Range(0, 5).Select(i => new object[] { i }); [Theory] [MemberData(nameof(TestCases))] @@ -46,7 +45,7 @@ await Fixture.Streams await AppendEvents(streamName); - await _completed.Task.WithTimeout(); + await Completed.Task.WithTimeout(); } [Theory] @@ -68,7 +67,7 @@ await Fixture.Streams await AppendEvents(streamName); - await _completed.Task.WithTimeout(); + await Completed.Task.WithTimeout(); } [Theory] @@ -91,7 +90,7 @@ await Fixture.Streams await AppendEvents(streamName); - await _completed.Task.WithTimeout(); + await Completed.Task.WithTimeout(); } async Task AppendEvents(string streamName) { @@ -108,8 +107,7 @@ async Task AppendEvents(string streamName) { ); expectedRevision = result.NextExpectedStreamRevision; - } - else { + } else { var result = await Fixture.Streams.AppendToStreamAsync( streamName, expectedRevision, @@ -143,7 +141,7 @@ Func> resubscribe return; } - _completed.TrySetException(ex); + Completed.TrySetException(ex); } Task EventAppeared(ResolvedEvent e, string streamName, out FromStream startFrom) { @@ -160,12 +158,12 @@ Task EventAppeared(ResolvedEvent e, string streamName) { if (e.OriginalStreamId != streamName) return Task.CompletedTask; - if (_seen[e.Event.EventNumber]) + if (Seen[e.Event.EventNumber]) throw new($"Event {e.Event.EventNumber} was already seen"); - _seen[e.Event.EventNumber] = true; + Seen[e.Event.EventNumber] = true; if (e.Event.EventType == "completed") - _completed.TrySetResult(true); + Completed.TrySetResult(true); return Task.CompletedTask; } diff --git a/test/EventStore.Client.Streams.Tests/Delete/deleting_stream.cs b/test/EventStore.Client.Tests/Streams/DeleteTests.cs similarity index 72% rename from test/EventStore.Client.Streams.Tests/Delete/deleting_stream.cs rename to test/EventStore.Client.Tests/Streams/DeleteTests.cs index 552a8a4b9..69bca6b56 100644 --- a/test/EventStore.Client.Streams.Tests/Delete/deleting_stream.cs +++ b/test/EventStore.Client.Tests/Streams/DeleteTests.cs @@ -1,16 +1,11 @@ using Grpc.Core; -namespace EventStore.Client.Streams.Tests.Delete; +namespace EventStore.Client.Tests.Streams; +[Trait("Category", "Target:Stream")] [Trait("Category", "Operation:Delete")] -public class deleting_stream(ITestOutputHelper output, EventStoreFixture fixture) : EventStoreTests(output, fixture) { - public static IEnumerable ExpectedStreamStateCases() { - yield return new object?[] { StreamState.Any, nameof(StreamState.Any) }; - yield return new object?[] { StreamState.NoStream, nameof(StreamState.NoStream) }; - } - - [Theory] - [MemberData(nameof(ExpectedStreamStateCases))] +public class DeleteTests(ITestOutputHelper output, KurrentPermanentFixture fixture) : EventStorePermanentTests(output, fixture) { + [Theory, ExpectedStreamStateCases] public async Task hard_deleting_a_stream_that_does_not_exist_with_expected_version_does_not_throw(StreamState expectedVersion, string name) { var stream = $"{Fixture.GetStreamName()}_{name}"; @@ -78,14 +73,11 @@ public async Task hard_deleting_a_deleted_stream_should_throw() { await Assert.ThrowsAsync(() => Fixture.Streams.TombstoneAsync(stream, StreamState.NoStream)); } - - + [Fact] public async Task with_timeout_any_stream_revision_delete_fails_when_operation_expired() { - var stream = Fixture.GetStreamName(); - var rpcException = await Assert.ThrowsAsync( - () => Fixture.Streams.DeleteAsync(stream, StreamState.Any, TimeSpan.Zero) - ); + var stream = Fixture.GetStreamName(); + var rpcException = await Assert.ThrowsAsync(() => Fixture.Streams.DeleteAsync(stream, StreamState.Any, TimeSpan.Zero)); Assert.Equal(StatusCode.DeadlineExceeded, rpcException.StatusCode); } @@ -94,19 +86,15 @@ public async Task with_timeout_any_stream_revision_delete_fails_when_operation_e public async Task with_timeout_stream_revision_delete_fails_when_operation_expired() { var stream = Fixture.GetStreamName(); - var rpcException = await Assert.ThrowsAsync( - () => Fixture.Streams.DeleteAsync(stream, new StreamRevision(0), TimeSpan.Zero) - ); + var rpcException = await Assert.ThrowsAsync(() => Fixture.Streams.DeleteAsync(stream, new StreamRevision(0), TimeSpan.Zero)); Assert.Equal(StatusCode.DeadlineExceeded, rpcException.StatusCode); } [Fact] public async Task with_timeout_any_stream_revision_tombstoning_fails_when_operation_expired() { - var stream = Fixture.GetStreamName(); - var rpcException = await Assert.ThrowsAsync( - () => Fixture.Streams.TombstoneAsync(stream, StreamState.Any, TimeSpan.Zero) - ); + var stream = Fixture.GetStreamName(); + var rpcException = await Assert.ThrowsAsync(() => Fixture.Streams.TombstoneAsync(stream, StreamState.Any, TimeSpan.Zero)); Assert.Equal(StatusCode.DeadlineExceeded, rpcException.StatusCode); } @@ -115,10 +103,15 @@ public async Task with_timeout_any_stream_revision_tombstoning_fails_when_operat public async Task with_timeout_stream_revision_tombstoning_fails_when_operation_expired() { var stream = Fixture.GetStreamName(); - var rpcException = await Assert.ThrowsAsync( - () => Fixture.Streams.TombstoneAsync(stream, new StreamRevision(0), TimeSpan.Zero) - ); + var rpcException = await Assert.ThrowsAsync(() => Fixture.Streams.TombstoneAsync(stream, new StreamRevision(0), TimeSpan.Zero)); Assert.Equal(StatusCode.DeadlineExceeded, rpcException.StatusCode); } -} \ No newline at end of file + + class ExpectedStreamStateCases : TestCaseGenerator { + protected override IEnumerable Data() { + yield return [StreamState.Any, nameof(StreamState.Any)]; + yield return [StreamState.NoStream, nameof(StreamState.NoStream)]; + } + } +} diff --git a/test/EventStore.Client.Streams.Tests/Read/EventBinaryData.cs b/test/EventStore.Client.Tests/Streams/Read/EventBinaryData.cs similarity index 81% rename from test/EventStore.Client.Streams.Tests/Read/EventBinaryData.cs rename to test/EventStore.Client.Tests/Streams/Read/EventBinaryData.cs index 923a40cd1..53e221b5d 100644 --- a/test/EventStore.Client.Streams.Tests/Read/EventBinaryData.cs +++ b/test/EventStore.Client.Tests/Streams/Read/EventBinaryData.cs @@ -1,33 +1,33 @@ -namespace EventStore.Client.Streams.Tests.Read; +namespace EventStore.Client.Tests; public readonly record struct EventBinaryData(Uuid Id, byte[] Data, byte[] Metadata) { - public bool Equals(EventBinaryData other) => - Id.Equals(other.Id) - && Data.SequenceEqual(other.Data) + public bool Equals(EventBinaryData other) => + Id.Equals(other.Id) + && Data.SequenceEqual(other.Data) && Metadata.SequenceEqual(other.Metadata); public override int GetHashCode() => System.HashCode.Combine(Id, Data, Metadata); } public static class EventBinaryDataConverters { - public static EventBinaryData ToBinaryData(this EventData source) => + public static EventBinaryData ToBinaryData(this EventData source) => new(source.EventId, source.Data.ToArray(), source.Metadata.ToArray()); - public static EventBinaryData ToBinaryData(this EventRecord source) => + public static EventBinaryData ToBinaryData(this EventRecord source) => new(source.EventId, source.Data.ToArray(), source.Metadata.ToArray()); - public static EventBinaryData ToBinaryData(this ResolvedEvent source) => + public static EventBinaryData ToBinaryData(this ResolvedEvent source) => source.Event.ToBinaryData(); - - public static EventBinaryData[] ToBinaryData(this IEnumerable source) => + + public static EventBinaryData[] ToBinaryData(this IEnumerable source) => source.Select(x => x.ToBinaryData()).ToArray(); - - public static EventBinaryData[] ToBinaryData(this IEnumerable source) => + + public static EventBinaryData[] ToBinaryData(this IEnumerable source) => source.Select(x => x.ToBinaryData()).ToArray(); - - public static EventBinaryData[] ToBinaryData(this IEnumerable source) => + + public static EventBinaryData[] ToBinaryData(this IEnumerable source) => source.Select(x => x.ToBinaryData()).ToArray(); public static ValueTask ToBinaryData(this IAsyncEnumerable source) => source.DefaultIfEmpty().Select(x => x.ToBinaryData()).ToArrayAsync(); -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Streams.Tests/Read/EventDataComparer.cs b/test/EventStore.Client.Tests/Streams/Read/EventDataComparer.cs similarity index 91% rename from test/EventStore.Client.Streams.Tests/Read/EventDataComparer.cs rename to test/EventStore.Client.Tests/Streams/Read/EventDataComparer.cs index cd9c6f41a..9ffb2c2bc 100644 --- a/test/EventStore.Client.Streams.Tests/Read/EventDataComparer.cs +++ b/test/EventStore.Client.Tests/Streams/Read/EventDataComparer.cs @@ -1,4 +1,4 @@ -namespace EventStore.Client.Streams.Tests.Read; +namespace EventStore.Client.Tests; static class EventDataComparer { public static bool Equal(EventData expected, EventRecord actual) { @@ -18,4 +18,4 @@ public static bool Equal(EventData[] expected, EventRecord[] actual) { return !expected.Where((t, i) => !Equal(t, actual[i])).Any(); } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Streams.Tests/Read/read_all_events_backward.cs b/test/EventStore.Client.Tests/Streams/Read/ReadAllEventsBackwardTests.cs similarity index 87% rename from test/EventStore.Client.Streams.Tests/Read/read_all_events_backward.cs rename to test/EventStore.Client.Tests/Streams/Read/ReadAllEventsBackwardTests.cs index b07f199ef..1620842f9 100644 --- a/test/EventStore.Client.Streams.Tests/Read/read_all_events_backward.cs +++ b/test/EventStore.Client.Tests/Streams/Read/ReadAllEventsBackwardTests.cs @@ -1,12 +1,14 @@ +using EventStore.Client.Tests.TestNode; using Grpc.Core; -namespace EventStore.Client.Streams.Tests.Read; +namespace EventStore.Client.Tests; [Trait("Category", "Target:All")] [Trait("Category", "Operation:Read")] [Trait("Category", "Operation:Read:Backwards")] [Trait("Category", "Database:Dedicated")] -public class read_all_events_backward(ITestOutputHelper output, ReadAllEventsFixture fixture) : EventStoreTests(output, fixture) { +public class ReadAllEventsBackwardTests(ITestOutputHelper output, ReadAllEventsFixture fixture) + : KurrentTemporaryTests(output, fixture) { [Fact] public async Task return_empty_if_reading_from_start() { var result = await Fixture.Streams.ReadAllAsync(Direction.Backwards, Position.Start, 1).CountAsync(); @@ -28,7 +30,7 @@ public async Task return_events_in_reversed_order_compared_to_written() { .ReadAllAsync(Direction.Backwards, Position.End) .Where(x => x.OriginalStreamId == Fixture.ExpectedStreamName) .ToBinaryData(); - + result.ShouldBe(Fixture.ExpectedEventsReversed); } @@ -37,7 +39,7 @@ public async Task return_single_event() { var result = await Fixture.Streams .ReadAllAsync(Direction.Backwards, Position.End, 1) .ToArrayAsync(); - + result.ShouldHaveSingleItem(); } @@ -51,36 +53,36 @@ public async Task max_count_is_respected() { result.Length.ShouldBe(maxCount); } - + [Fact] public async Task stream_found() { var count = await Fixture.Streams .ReadAllAsync(Direction.Backwards, Position.End) .Where(x => x.OriginalStreamId == Fixture.ExpectedStreamName) .CountAsync(); - + count.ShouldBe(Fixture.ExpectedEvents.Length); } - + [Fact] public async Task with_timeout_fails_when_operation_expired() { var ex = await Fixture.Streams .ReadAllAsync(Direction.Backwards, Position.Start, 1, false, TimeSpan.Zero) .ToArrayAsync() .AsTask().ShouldThrowAsync(); - + ex.StatusCode.ShouldBe(StatusCode.DeadlineExceeded); } - + [Fact] public async Task filter_events_by_type() { var result = await Fixture.Streams - .ReadAllAsync(Direction.Backwards, Position.End, EventTypeFilter.Prefix(EventStoreFixture.AnotherTestEventTypePrefix)) + .ReadAllAsync(Direction.Backwards, Position.End, EventTypeFilter.Prefix(KurrentTemporaryFixture.AnotherTestEventTypePrefix)) .ToListAsync(); - - result.ForEach(x => x.Event.EventType.ShouldStartWith(EventStoreFixture.AnotherTestEventTypePrefix)); + + result.ForEach(x => x.Event.EventType.ShouldStartWith(KurrentTemporaryFixture.AnotherTestEventTypePrefix)); } - + [Fact(Skip = "Not Implemented")] public Task be_able_to_read_all_one_by_one_until_end_of_stream() => throw new NotImplementedException(); diff --git a/test/EventStore.Client.Streams.Tests/Read/ReadAllEventsFixture.cs b/test/EventStore.Client.Tests/Streams/Read/ReadAllEventsFixture.cs similarity index 81% rename from test/EventStore.Client.Streams.Tests/Read/ReadAllEventsFixture.cs rename to test/EventStore.Client.Tests/Streams/Read/ReadAllEventsFixture.cs index 197c8a481..979bcccc0 100644 --- a/test/EventStore.Client.Streams.Tests/Read/ReadAllEventsFixture.cs +++ b/test/EventStore.Client.Tests/Streams/Read/ReadAllEventsFixture.cs @@ -1,6 +1,12 @@ -namespace EventStore.Client.Streams.Tests.Read; +using EventStore.Client.Tests.TestNode; +using EventStore.Client.Tests; -public class ReadAllEventsFixture : EventStoreFixture { +namespace EventStore.Client.Tests; + +[Trait("Category", "Target:All")] +[Trait("Category", "Operation:Read")] +[Trait("Category", "Database:Dedicated")] +public class ReadAllEventsFixture : KurrentTemporaryFixture { public ReadAllEventsFixture() { OnSetup = async () => { _ = await Streams.SetStreamMetadataAsync( @@ -36,4 +42,4 @@ public ReadAllEventsFixture() { public EventBinaryData ExpectedFirstEvent { get; private set; } public EventBinaryData ExpectedLastEvent { get; private set; } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Streams.Tests/Read/read_all_events_forward.cs b/test/EventStore.Client.Tests/Streams/Read/ReadAllEventsForwardTests.cs similarity index 91% rename from test/EventStore.Client.Streams.Tests/Read/read_all_events_forward.cs rename to test/EventStore.Client.Tests/Streams/Read/ReadAllEventsForwardTests.cs index fed9629ad..fc927ef11 100644 --- a/test/EventStore.Client.Streams.Tests/Read/read_all_events_forward.cs +++ b/test/EventStore.Client.Tests/Streams/Read/ReadAllEventsForwardTests.cs @@ -1,12 +1,13 @@ using System.Text; +using EventStore.Client.Tests.TestNode; -namespace EventStore.Client.Streams.Tests.Read; +namespace EventStore.Client.Tests; [Trait("Category", "Target:All")] [Trait("Category", "Operation:Read")] [Trait("Category", "Operation:Read:Forwards")] [Trait("Category", "Database:Dedicated")] -public class read_all_events_forward(ITestOutputHelper output, ReadAllEventsFixture fixture) : EventStoreTests(output, fixture) { +public class ReadAllEventsForwardTests(ITestOutputHelper output, ReadAllEventsFixture fixture) : KurrentTemporaryTests(output, fixture) { [Fact] public async Task return_empty_if_reading_from_end() { var result = await Fixture.Streams.ReadAllAsync(Direction.Forwards, Position.End, 1).CountAsync(); @@ -27,7 +28,7 @@ public async Task return_events_in_correct_order_compared_to_written() { .ReadAllAsync(Direction.Forwards, Position.Start) .Where(x => x.OriginalStreamId == Fixture.ExpectedStreamName) .ToBinaryData(); - + result.ShouldBe(Fixture.ExpectedEvents); } @@ -36,7 +37,7 @@ public async Task return_single_event() { var result = await Fixture.Streams .ReadAllAsync(Direction.Forwards, Position.Start, 1) .ToArrayAsync(); - + result.ShouldHaveSingleItem(); } @@ -59,22 +60,22 @@ public async Task reads_all_events_by_default() { Assert.True(count >= Fixture.ExpectedEvents.Length); } - + [Fact] public async Task stream_found() { var count = await Fixture.Streams .ReadAllAsync(Direction.Forwards, Position.Start) .Where(x => x.OriginalStreamId == Fixture.ExpectedStreamName) .CountAsync(); - + count.ShouldBe(Fixture.ExpectedEvents.Length); } - + [Fact] public async Task with_linkto_passed_max_count_one_event_is_read() { const string deletedStream = nameof(deletedStream); const string linkedStream = nameof(linkedStream); - + await Fixture.Streams.AppendToStreamAsync(deletedStream, StreamState.Any, Fixture.CreateTestEvents()); await Fixture.Streams.SetStreamMetadataAsync( deletedStream, @@ -105,10 +106,10 @@ await Fixture.Streams.AppendToStreamAsync( resolveLinkTos: true ) .ToArrayAsync(); - + Assert.Single(events); } - + [Fact] public async Task enumeration_all_referencing_messages_twice_does_not_throw() { var result = Fixture.Streams.ReadAllAsync( @@ -138,16 +139,16 @@ await Assert.ThrowsAsync( await result.Messages.ToArrayAsync() ); } - + [Fact] public async Task filter_events_by_type() { var result = await Fixture.Streams - .ReadAllAsync(Direction.Forwards, Position.Start, EventTypeFilter.Prefix(EventStoreFixture.AnotherTestEventTypePrefix)) + .ReadAllAsync(Direction.Forwards, Position.Start, EventTypeFilter.Prefix(KurrentTemporaryFixture.AnotherTestEventTypePrefix)) .ToListAsync(); - - result.ForEach(x => x.Event.EventType.ShouldStartWith(EventStoreFixture.AnotherTestEventTypePrefix)); + + result.ForEach(x => x.Event.EventType.ShouldStartWith(KurrentTemporaryFixture.AnotherTestEventTypePrefix)); } - + [Fact(Skip = "Not Implemented")] public Task be_able_to_read_all_one_by_one_until_end_of_stream() => throw new NotImplementedException(); diff --git a/test/EventStore.Client.Streams.Tests/Read/read_stream_backward.cs b/test/EventStore.Client.Tests/Streams/Read/ReadStreamBackwardTests.cs similarity index 96% rename from test/EventStore.Client.Streams.Tests/Read/read_stream_backward.cs rename to test/EventStore.Client.Tests/Streams/Read/ReadStreamBackwardTests.cs index ec32888e8..01be08eb7 100644 --- a/test/EventStore.Client.Streams.Tests/Read/read_stream_backward.cs +++ b/test/EventStore.Client.Tests/Streams/Read/ReadStreamBackwardTests.cs @@ -1,11 +1,13 @@ +using EventStore.Client.Tests.TestNode; using Grpc.Core; -namespace EventStore.Client.Streams.Tests.Read; +namespace EventStore.Client.Tests; [Trait("Category", "Target:Stream")] [Trait("Category", "Operation:Read")] [Trait("Category", "Operation:Read:Backwards")] -public class read_stream_backward(ITestOutputHelper output, EventStoreFixture fixture) : EventStoreTests(output, fixture) { +public class ReadStreamBackwardTests(ITestOutputHelper output, KurrentTemporaryFixture fixture) + : KurrentTemporaryTests(output, fixture) { [Theory] [InlineData(0)] public async Task count_le_equal_zero_throws(long maxCount) { @@ -165,7 +167,7 @@ await Fixture.Streams.AppendToStreamAsync( [Fact] public async Task populates_log_position_of_event() { - if (EventStoreTestServer.Version.Major < 22) + if (KurrentTemporaryTestNode.Version.Major < 22) return; var stream = Fixture.GetStreamName(); @@ -181,7 +183,7 @@ public async Task populates_log_position_of_event() { Assert.Equal(writeResult.LogPosition.PreparePosition, writeResult.LogPosition.CommitPosition); Assert.Equal(writeResult.LogPosition, actual.First().Position); } - + [Fact] public async Task with_timeout_fails_when_operation_expired() { var stream = Fixture.GetStreamName(); @@ -203,7 +205,7 @@ public async Task with_timeout_fails_when_operation_expired() { Assert.Equal(StatusCode.DeadlineExceeded, rpcException.StatusCode); } - + [Fact] public async Task enumeration_referencing_messages_twice_does_not_throw() { var result = Fixture.Streams.ReadStreamAsync( @@ -235,7 +237,7 @@ await Assert.ThrowsAsync( await result.Messages.ToArrayAsync() ); } - + [Fact] public async Task stream_not_found() { var result = await Fixture.Streams.ReadStreamAsync( @@ -250,7 +252,7 @@ public async Task stream_not_found() { [Fact] public async Task stream_found() { const int eventCount = 32; - + var events = Fixture.CreateTestEvents(eventCount).ToArray(); var streamName = Fixture.GetStreamName(); @@ -274,4 +276,4 @@ public async Task stream_found() { if (Fixture.EventStoreHasLastStreamPosition) Assert.Equal(new StreamMessage.LastStreamPosition(new(31)), result[^1]); } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Streams.Tests/Read/read_stream_events_linked_to_deleted_stream.cs b/test/EventStore.Client.Tests/Streams/Read/ReadStreamEventsLinkedToDeletedStreamTests.cs similarity index 70% rename from test/EventStore.Client.Streams.Tests/Read/read_stream_events_linked_to_deleted_stream.cs rename to test/EventStore.Client.Tests/Streams/Read/ReadStreamEventsLinkedToDeletedStreamTests.cs index ba11b75f0..a96e82ab5 100644 --- a/test/EventStore.Client.Streams.Tests/Read/read_stream_events_linked_to_deleted_stream.cs +++ b/test/EventStore.Client.Tests/Streams/Read/ReadStreamEventsLinkedToDeletedStreamTests.cs @@ -1,13 +1,17 @@ using System.Text; +using EventStore.Client.Tests.TestNode; +using EventStore.Client.Tests; -namespace EventStore.Client.Streams.Tests.Read; +// ReSharper disable ClassNeverInstantiated.Global + +namespace EventStore.Client.Tests; [Trait("Category", "Target:Stream")] -public abstract class read_stream_events_linked_to_deleted_stream(ReadEventsLinkedToDeletedStreamFixture fixture) { +public abstract class ReadStreamEventsLinkedToDeletedStreamTests(ReadEventsLinkedToDeletedStreamFixture fixture) { ReadEventsLinkedToDeletedStreamFixture Fixture { get; } = fixture; [Fact] - public void one_event_is_read() => Assert.Single(Fixture.Events ?? Array.Empty()); + public void one_event_is_read() => Assert.Single(Fixture.Events ?? []); [Fact] public void the_linked_event_is_not_resolved() => Assert.Null(Fixture.Events![0].Event); @@ -17,32 +21,30 @@ public abstract class read_stream_events_linked_to_deleted_stream(ReadEventsLink [Fact] public void the_event_is_not_resolved() => Assert.False(Fixture.Events![0].IsResolved); - + + [UsedImplicitly] [Trait("Category", "Operation:Read")] [Trait("Category", "Operation:Read:Forwards")] - public class @forwards(forwards.CustomFixture fixture) - : read_stream_events_linked_to_deleted_stream(fixture), IClassFixture { - + public class Forwards(Forwards.CustomFixture fixture) : ReadStreamEventsLinkedToDeletedStreamTests(fixture), IClassFixture { public class CustomFixture() : ReadEventsLinkedToDeletedStreamFixture(Direction.Forwards); } - + + [UsedImplicitly] [Trait("Category", "Operation:Read")] [Trait("Category", "Operation:Read:Backwards")] - public class @backwards(backwards.CustomFixture fixture) - : read_stream_events_linked_to_deleted_stream(fixture), IClassFixture { - + public class Backwards(Backwards.CustomFixture fixture) : ReadStreamEventsLinkedToDeletedStreamTests(fixture), IClassFixture { public class CustomFixture() : ReadEventsLinkedToDeletedStreamFixture(Direction.Backwards); } } -public abstract class ReadEventsLinkedToDeletedStreamFixture : EventStoreFixture { +public abstract class ReadEventsLinkedToDeletedStreamFixture : KurrentTemporaryFixture { const string DeletedStream = nameof(DeletedStream); const string LinkedStream = nameof(LinkedStream); protected ReadEventsLinkedToDeletedStreamFixture(Direction direction) { OnSetup = async () => { await Streams.AppendToStreamAsync(DeletedStream, StreamState.Any, CreateTestEvents()); - + await Streams.AppendToStreamAsync( LinkedStream, StreamState.Any, @@ -58,7 +60,7 @@ await Streams.AppendToStreamAsync( ); await Streams.DeleteAsync(DeletedStream, StreamState.Any); - + Events = await Streams.ReadStreamAsync( direction, LinkedStream, @@ -70,4 +72,4 @@ await Streams.AppendToStreamAsync( } public ResolvedEvent[]? Events { get; private set; } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Streams.Tests/Read/read_stream_forward.cs b/test/EventStore.Client.Tests/Streams/Read/ReadStreamForwardTests.cs similarity index 96% rename from test/EventStore.Client.Streams.Tests/Read/read_stream_forward.cs rename to test/EventStore.Client.Tests/Streams/Read/ReadStreamForwardTests.cs index 14c08a79c..4100cf010 100644 --- a/test/EventStore.Client.Streams.Tests/Read/read_stream_forward.cs +++ b/test/EventStore.Client.Tests/Streams/Read/ReadStreamForwardTests.cs @@ -1,9 +1,9 @@ -namespace EventStore.Client.Streams.Tests.Read; +namespace EventStore.Client.Tests; [Trait("Category", "Target:Stream")] [Trait("Category", "Operation:Read")] [Trait("Category", "Operation:Read:Forwards")] -public class read_stream_forward(ITestOutputHelper output, EventStoreFixture fixture) : EventStoreTests(output, fixture) { +public class ReadStreamForwardTests(ITestOutputHelper output, KurrentPermanentFixture fixture) : EventStorePermanentTests(output, fixture) { [Theory] [InlineData(0)] public async Task count_le_equal_zero_throws(long maxCount) { @@ -159,7 +159,7 @@ await Fixture.Streams.AppendToStreamAsync( [Fact] public async Task populates_log_position_of_event() { - if (EventStoreTestServer.Version.Major < 22) + if (KurrentPermanentTestNode.Version.Major < 22) return; var stream = Fixture.GetStreamName(); @@ -191,7 +191,7 @@ public async Task stream_not_found() { [Fact] public async Task stream_found() { const int eventCount = 64; - + var events = Fixture.CreateTestEvents(eventCount).ToArray(); var streamName = Fixture.GetStreamName(); @@ -226,7 +226,7 @@ public async Task stream_found() { [Fact] public async Task stream_found_truncated() { const int eventCount = 64; - + var events = Fixture.CreateTestEvents(eventCount).ToArray(); var streamName = Fixture.GetStreamName(); @@ -263,4 +263,4 @@ await Fixture.Streams.SetStreamMetadataAsync( result[^1] ); } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Streams.Tests/Read/read_stream_when_having_max_count_set_for_stream.cs b/test/EventStore.Client.Tests/Streams/Read/ReadStreamWhenHavingMaxCountSetForStreamTests.cs similarity index 93% rename from test/EventStore.Client.Streams.Tests/Read/read_stream_when_having_max_count_set_for_stream.cs rename to test/EventStore.Client.Tests/Streams/Read/ReadStreamWhenHavingMaxCountSetForStreamTests.cs index 0142ebd9a..fbd2696f1 100644 --- a/test/EventStore.Client.Streams.Tests/Read/read_stream_when_having_max_count_set_for_stream.cs +++ b/test/EventStore.Client.Tests/Streams/Read/ReadStreamWhenHavingMaxCountSetForStreamTests.cs @@ -1,8 +1,12 @@ -namespace EventStore.Client.Streams.Tests.Read; +using EventStore.Client.Tests.TestNode; +using EventStore.Client.Tests; + +namespace EventStore.Client.Tests; [Trait("Category", "Target:Stream")] [Trait("Category", "Operation:Read")] -public class read_stream_when_having_max_count_set_for_stream (ITestOutputHelper output, EventStoreFixture fixture) : EventStoreTests(output, fixture) { +public class ReadStreamWhenHavingMaxCountSetForStreamTests(ITestOutputHelper output, KurrentTemporaryFixture fixture) + : KurrentTemporaryTests(output, fixture) { [Fact] public async Task read_stream_forwards_respects_max_count() { var stream = Fixture.GetStreamName(); @@ -118,4 +122,4 @@ public async Task after_setting_more_strict_max_count_read_stream_backwards_read Assert.Equal(2, actual.Length); Assert.True(EventDataComparer.Equal(expected.Skip(3).Reverse().ToArray(), actual)); } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Streams.Tests/Delete/soft_deleted_stream.cs b/test/EventStore.Client.Tests/Streams/SoftDeleteTests.cs similarity index 94% rename from test/EventStore.Client.Streams.Tests/Delete/soft_deleted_stream.cs rename to test/EventStore.Client.Tests/Streams/SoftDeleteTests.cs index 25877d2ee..7560167b8 100644 --- a/test/EventStore.Client.Streams.Tests/Delete/soft_deleted_stream.cs +++ b/test/EventStore.Client.Tests/Streams/SoftDeleteTests.cs @@ -1,12 +1,13 @@ using System.Text.Json; -namespace EventStore.Client.Streams.Tests.Delete; +namespace EventStore.Client.Tests.Streams; +[Trait("Category", "Target:Stream")] [Trait("Category", "Operation:Delete")] -public class deleted_stream(ITestOutputHelper output, EventStoreFixture fixture) : EventStoreTests(output, fixture) { +public class SoftDeleteTests(ITestOutputHelper output, KurrentPermanentFixture fixture) : EventStorePermanentTests(output, fixture) { static JsonDocument CustomMetadata { get; } - static deleted_stream() { + static SoftDeleteTests() { var customMetadata = new Dictionary { ["key1"] = true, ["key2"] = 17, @@ -15,7 +16,7 @@ static deleted_stream() { CustomMetadata = JsonDocument.Parse(JsonSerializer.Serialize(customMetadata)); } - + [Fact] public async Task reading_throws() { var stream = Fixture.GetStreamName(); @@ -36,13 +37,7 @@ await Assert.ThrowsAsync( ); } - public static IEnumerable RecreatingTestCases() { - yield return new object?[] { StreamState.Any, nameof(StreamState.Any) }; - yield return new object?[] { StreamState.NoStream, nameof(StreamState.NoStream) }; - } - - [Theory] - [MemberData(nameof(RecreatingTestCases))] + [Theory, RecreatingTestCases] public async Task recreated_with_any_expected_version(StreamState expectedState, string name) { var stream = $"{Fixture.GetStreamName()}_{name}"; @@ -124,7 +119,7 @@ public async Task recreated_with_expected_version() { [Fact] public async Task recreated_preserves_metadata_except_truncate_before() { - const int count = 2; + const int count = 2; var stream = Fixture.GetStreamName(); @@ -213,9 +208,7 @@ await Fixture.Streams.AppendToStreamAsync( Assert.Equal(SystemStreams.MetastreamOf(stream), ex.Stream); - await Assert.ThrowsAsync( - () => Fixture.Streams.AppendToStreamAsync(stream, StreamState.Any, Fixture.CreateTestEvents()) - ); + await Assert.ThrowsAsync(() => Fixture.Streams.AppendToStreamAsync(stream, StreamState.Any, Fixture.CreateTestEvents())); } [Fact] @@ -366,7 +359,7 @@ await Assert.ThrowsAsync( [Fact] public async Task recreated_on_non_empty_when_metadata_set() { - const int count = 2; + const int count = 2; var stream = Fixture.GetStreamName(); @@ -420,4 +413,11 @@ public async Task recreated_on_non_empty_when_metadata_set() { Assert.Equal(expected, metadataResult.Metadata); } -} \ No newline at end of file + + class RecreatingTestCases : TestCaseGenerator { + protected override IEnumerable Data() { + yield return [StreamState.Any, nameof(StreamState.Any)]; + yield return [StreamState.NoStream, nameof(StreamState.NoStream)]; + } + } +} diff --git a/test/EventStore.Client.Streams.Tests/Metadata/stream_metadata.cs b/test/EventStore.Client.Tests/Streams/StreamMetadataTests.cs similarity index 92% rename from test/EventStore.Client.Streams.Tests/Metadata/stream_metadata.cs rename to test/EventStore.Client.Tests/Streams/StreamMetadataTests.cs index 706d5e2b6..5ba987732 100644 --- a/test/EventStore.Client.Streams.Tests/Metadata/stream_metadata.cs +++ b/test/EventStore.Client.Tests/Streams/StreamMetadataTests.cs @@ -1,10 +1,11 @@ using System.Text.Json; using Grpc.Core; -namespace EventStore.Client.Streams.Tests.Metadata; +namespace EventStore.Client.Tests.Streams; -[Trait("Category", "Metadata")] -public class stream_metadata(ITestOutputHelper output, EventStoreFixture fixture) : EventStoreTests(output, fixture) { +[Trait("Category", "Target:Stream")] +[Trait("Category", "Operation:Metadata")] +public class StreamMetadataTests(ITestOutputHelper output, KurrentPermanentFixture fixture) : EventStorePermanentTests(output, fixture) { [Fact] public async Task getting_for_an_existing_stream_and_no_metadata_exists() { var stream = Fixture.GetStreamName(); @@ -150,7 +151,7 @@ public async Task latest_metadata_returned_stream_revision_any() { Assert.Equal(expected.CacheControl, actual.Metadata.CacheControl); Assert.Equal(expected.Acl, actual.Metadata.Acl); } - + [Fact] public async Task with_timeout_set_with_any_stream_revision_fails_when_operation_expired() { var stream = Fixture.GetStreamName(); @@ -186,9 +187,7 @@ public async Task with_timeout_set_with_stream_revision_fails_when_operation_exp [Fact] public async Task with_timeout_get_fails_when_operation_expired() { - var stream = Fixture.GetStreamName(); - var rpcException = await Assert.ThrowsAsync( - () => Fixture.Streams.GetStreamMetadataAsync(stream, TimeSpan.Zero) - ); + var stream = Fixture.GetStreamName(); + var rpcException = await Assert.ThrowsAsync(() => Fixture.Streams.GetStreamMetadataAsync(stream, TimeSpan.Zero)); } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Streams.Tests/Subscriptions/subscribe_to_stream.cs b/test/EventStore.Client.Tests/Streams/SubscribeToStreamTests.cs similarity index 80% rename from test/EventStore.Client.Streams.Tests/Subscriptions/subscribe_to_stream.cs rename to test/EventStore.Client.Tests/Streams/SubscribeToStreamTests.cs index d38391e8f..00d38e1e5 100644 --- a/test/EventStore.Client.Streams.Tests/Subscriptions/subscribe_to_stream.cs +++ b/test/EventStore.Client.Tests/Streams/SubscribeToStreamTests.cs @@ -1,10 +1,10 @@ -namespace EventStore.Client.Streams.Tests.Subscriptions; +namespace EventStore.Client.Tests.Streams; [Trait("Category", "Subscriptions")] [Trait("Category", "Target:Stream")] -public class subscribe_to_stream(ITestOutputHelper output, SubscriptionsFixture fixture) - : EventStoreTests(output, fixture) { - [Fact] +public class SubscribeToStreamTests(ITestOutputHelper output, SubscribeToStreamTests.CustomFixture fixture) + : EventStorePermanentTests(output, fixture) { + [RetryFact] public async Task receives_all_events_from_start() { var streamName = Fixture.GetStreamName(); @@ -43,7 +43,7 @@ async Task Subscribe() { } } - [Fact] + [RetryFact] public async Task receives_all_events_from_position() { var streamName = Fixture.GetStreamName(); @@ -91,7 +91,7 @@ async Task Subscribe() { } } - [Fact] + [RetryFact] public async Task receives_all_events_from_non_existing_stream() { var streamName = Fixture.GetStreamName(); @@ -127,7 +127,7 @@ async Task Subscribe() { } } - [Fact] + [RetryFact] public async Task allow_multiple_subscriptions_to_same_stream() { var streamName = Fixture.GetStreamName(); @@ -170,7 +170,7 @@ async Task Subscribe(IAsyncEnumerator subscription) { } } - [Fact] + [RetryFact] public async Task drops_when_stream_tombstoned() { var streamName = Fixture.GetStreamName(); @@ -193,41 +193,18 @@ public async Task drops_when_stream_tombstoned() { ex.ShouldBeOfType().Stream.ShouldBe(streamName); } - [Fact] - public async Task receives_all_events_with_resolved_links() { - var streamName = Fixture.GetStreamName(); - - var seedEvents = Fixture.CreateTestEvents(3).ToArray(); - var availableEvents = new HashSet(seedEvents.Select(x => x.EventId)); - - await Fixture.Streams.AppendToStreamAsync(streamName, StreamState.NoStream, seedEvents); - - await using var subscription = - Fixture.Streams.SubscribeToStream($"$et-{EventStoreFixture.TestEventType}", FromStream.Start, true); - - await using var enumerator = subscription.Messages.GetAsyncEnumerator(); - - Assert.True(await enumerator.MoveNextAsync()); - - Assert.IsType(enumerator.Current); - - await Subscribe().WithTimeout(); - - return; - - async Task Subscribe() { - while (await enumerator.MoveNextAsync()) { - if (enumerator.Current is not StreamMessage.Event(var resolvedEvent) || - !resolvedEvent.OriginalEvent.EventStreamId.StartsWith($"$et-{EventStoreFixture.TestEventType}")) { - continue; - } - - availableEvents.Remove(resolvedEvent.Event.EventId); - - if (availableEvents.Count == 0) { - return; - } - } + public class CustomFixture : KurrentPermanentFixture { + public CustomFixture() { + OnSetup = async () => { + await Streams.SetStreamMetadataAsync( + SystemStreams.AllStream, + StreamState.Any, + new(acl: new(SystemRoles.All)), + userCredentials: TestCredentials.Root + ); + + await Streams.AppendToStreamAsync($"SubscriptionsFixture-Noise-{Guid.NewGuid():N}", StreamState.NoStream, CreateTestEvents(10)); + }; } } } diff --git a/test/EventStore.Client.Streams.Tests/Subscriptions/SubscriptionFilter.cs b/test/EventStore.Client.Tests/Streams/SubscriptionFilter.cs similarity index 92% rename from test/EventStore.Client.Streams.Tests/Subscriptions/SubscriptionFilter.cs rename to test/EventStore.Client.Tests/Streams/SubscriptionFilter.cs index e670b69c9..937f7d1a2 100644 --- a/test/EventStore.Client.Streams.Tests/Subscriptions/SubscriptionFilter.cs +++ b/test/EventStore.Client.Tests/Streams/SubscriptionFilter.cs @@ -1,8 +1,9 @@ -namespace EventStore.Client.Streams.Tests.Subscriptions; +// ReSharper disable InconsistentNaming +namespace EventStore.Client.Tests.Streams; public record SubscriptionFilter(string Name, Func Create, Func PrepareEvent) { public override string ToString() => Name; - + static readonly SubscriptionFilter StreamNamePrefix = new(nameof(StreamNamePrefix), StreamFilter.Prefix, (_, evt) => evt); static readonly SubscriptionFilter StreamNameRegex = new(nameof(StreamNameRegex), f => StreamFilter.RegularExpression(f), (_, evt) => evt); static readonly SubscriptionFilter EventTypePrefix = new(nameof(EventTypePrefix), EventTypeFilter.Prefix, (term, evt) => new(evt.EventId, term, evt.Data, evt.Metadata, evt.ContentType)); @@ -15,10 +16,10 @@ static SubscriptionFilter() { EventTypePrefix, EventTypeRegex }; - + TestCases = All.Select(x => new object[] { x }); } public static SubscriptionFilter[] All { get; } public static IEnumerable TestCases { get; } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Tests/UuidTests.cs b/test/EventStore.Client.Tests/UuidTests.cs index 35d833096..4533e5416 100644 --- a/test/EventStore.Client.Tests/UuidTests.cs +++ b/test/EventStore.Client.Tests/UuidTests.cs @@ -5,7 +5,7 @@ namespace EventStore.Client.Tests; public class UuidTests : ValueObjectTests { public UuidTests() : base(new ScenarioFixture()) { } - [Fact] + [RetryFact] public void ToGuidReturnsExpectedResult() { var guid = Guid.NewGuid(); var sut = Uuid.FromGuid(guid); @@ -13,21 +13,21 @@ public void ToGuidReturnsExpectedResult() { Assert.Equal(sut.ToGuid(), guid); } - [Fact] + [RetryFact] public void ToStringProducesExpectedResult() { var sut = Uuid.NewUuid(); Assert.Equal(sut.ToGuid().ToString(), sut.ToString()); } - [Fact] + [RetryFact] public void ToFormattedStringProducesExpectedResult() { var sut = Uuid.NewUuid(); Assert.Equal(sut.ToGuid().ToString("n"), sut.ToString("n")); } - [Fact] + [RetryFact] public void ToDtoReturnsExpectedResult() { var msb = GetRandomInt64(); var lsb = GetRandomInt64(); @@ -41,7 +41,7 @@ public void ToDtoReturnsExpectedResult() { Assert.Equal(msb, result.Structured.MostSignificantBits); } - [Fact] + [RetryFact] public void ParseReturnsExpectedResult() { var guid = Guid.NewGuid(); @@ -50,7 +50,7 @@ public void ParseReturnsExpectedResult() { Assert.Equal(Uuid.FromGuid(guid), sut); } - [Fact] + [RetryFact] public void FromInt64ReturnsExpectedResult() { var guid = Guid.Parse("65678f9b-d139-4786-8305-b9166922b378"); var sut = Uuid.FromInt64(7306966819824813958L, -9005588373953137800L); @@ -70,4 +70,4 @@ static long GetRandomInt64() { class ScenarioFixture : Fixture { public ScenarioFixture() => Customize(composer => composer.FromFactory(Uuid.FromGuid)); } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Tests/ValueObjectTests.cs b/test/EventStore.Client.Tests/ValueObjectTests.cs index cce2526f7..c70b5f7d6 100644 --- a/test/EventStore.Client.Tests/ValueObjectTests.cs +++ b/test/EventStore.Client.Tests/ValueObjectTests.cs @@ -7,9 +7,9 @@ public abstract class ValueObjectTests { protected ValueObjectTests(Fixture fixture) => _fixture = fixture; - [Fact] + [RetryFact] public void ValueObjectIsWellBehaved() => _fixture.Create().Verify(typeof(T)); - [Fact] + [RetryFact] public void ValueObjectIsEquatable() => Assert.IsAssignableFrom>(_fixture.Create()); -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Tests/X509CertificatesTests.cs b/test/EventStore.Client.Tests/X509CertificatesTests.cs index dd32aabb0..5d70fdb89 100644 --- a/test/EventStore.Client.Tests/X509CertificatesTests.cs +++ b/test/EventStore.Client.Tests/X509CertificatesTests.cs @@ -3,30 +3,38 @@ namespace EventStore.Client.Tests; public class X509CertificatesTests { - [Fact] + [RetryFact] public void create_from_pem_file() { const string certPemFilePath = "certs/ca/ca.crt"; const string keyPemFilePath = "certs/ca/ca.key"; var rsa = X509Certificates.CreateFromPemFile(certPemFilePath, keyPemFilePath); +#if NET9_0_OR_GREATER + var cert = X509CertificateLoader.LoadCertificateFromFile(certPemFilePath); +#else var cert = new X509Certificate2(certPemFilePath); +#endif rsa.Issuer.ShouldBe(cert.Issuer); rsa.SerialNumber.ShouldBe(cert.SerialNumber); } - [Fact] + [RetryFact] public void create_from_pem_file_() { const string certPemFilePath = "certs/user-admin/user-admin.crt"; const string keyPemFilePath = "certs/user-admin/user-admin.key"; var rsa = X509Certificates.CreateFromPemFile(certPemFilePath, keyPemFilePath); +#if NET9_0_OR_GREATER + var cert = X509CertificateLoader.LoadCertificateFromFile(certPemFilePath); +#else var cert = new X509Certificate2(certPemFilePath); +#endif rsa.Issuer.ShouldBe(cert.Issuer); rsa.Subject.ShouldBe(cert.Subject); rsa.SerialNumber.ShouldBe(cert.SerialNumber); } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.UserManagement.Tests/AssemblyInfo.cs b/test/EventStore.Client.UserManagement.Tests/AssemblyInfo.cs deleted file mode 100644 index b0b47aa73..000000000 --- a/test/EventStore.Client.UserManagement.Tests/AssemblyInfo.cs +++ /dev/null @@ -1 +0,0 @@ -[assembly: CollectionBehavior(DisableTestParallelization = true)] \ No newline at end of file diff --git a/test/EventStore.Client.UserManagement.Tests/EventStore.Client.UserManagement.Tests.csproj b/test/EventStore.Client.UserManagement.Tests/EventStore.Client.UserManagement.Tests.csproj deleted file mode 100644 index d4b52e67f..000000000 --- a/test/EventStore.Client.UserManagement.Tests/EventStore.Client.UserManagement.Tests.csproj +++ /dev/null @@ -1,12 +0,0 @@ - - - - EventStore.Client.Tests - - - - - - - - \ No newline at end of file diff --git a/test/EventStore.Client.UserManagement.Tests/EventStore.Client.UserManagement.Tests.csproj.DotSettings b/test/EventStore.Client.UserManagement.Tests/EventStore.Client.UserManagement.Tests.csproj.DotSettings deleted file mode 100644 index 1183b3a73..000000000 --- a/test/EventStore.Client.UserManagement.Tests/EventStore.Client.UserManagement.Tests.csproj.DotSettings +++ /dev/null @@ -1,2 +0,0 @@ - - True \ No newline at end of file diff --git a/test/EventStore.Client.UserManagement.Tests/InvalidCredentialsTestCases.cs b/test/EventStore.Client.UserManagement.Tests/InvalidCredentialsTestCases.cs deleted file mode 100644 index e32a340bd..000000000 --- a/test/EventStore.Client.UserManagement.Tests/InvalidCredentialsTestCases.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System.Collections; - -namespace EventStore.Client.Tests; - -public abstract record InvalidCredentialsTestCase(TestUser User, Type ExpectedException); - -public class InvalidCredentialsTestCases : IEnumerable { - public IEnumerator GetEnumerator() { - yield return new object?[] { new MissingCredentials() }; - yield return new object?[] { new WrongUsername() }; - yield return new object?[] { new WrongPassword() }; - } - - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - - public record MissingCredentials() - : InvalidCredentialsTestCase(Fakers.Users.WithNoCredentials(), typeof(AccessDeniedException)) { - public override string ToString() => nameof(MissingCredentials); - } - - public record WrongUsername() - : InvalidCredentialsTestCase(Fakers.Users.WithInvalidCredentials(false), typeof(NotAuthenticatedException)) { - public override string ToString() => nameof(WrongUsername); - } - - public record WrongPassword() - : InvalidCredentialsTestCase(Fakers.Users.WithInvalidCredentials(wrongPassword: false), typeof(NotAuthenticatedException)) { - public override string ToString() => nameof(WrongPassword); - } -} \ No newline at end of file diff --git a/test/EventStore.Client.UserManagement.Tests/UserCredentialsTests.cs b/test/EventStore.Client.UserManagement.Tests/UserCredentialsTests.cs deleted file mode 100644 index c8eb0a570..000000000 --- a/test/EventStore.Client.UserManagement.Tests/UserCredentialsTests.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System.Net.Http.Headers; -using System.Text; -using static System.Convert; - -namespace EventStore.Client.Tests; - -public class UserCredentialsTests { - const string JwtToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." - + "eyJzdWIiOiI5OSIsIm5hbWUiOiJKb2huIFdpY2siLCJpYXQiOjE1MTYyMzkwMjJ9." - + "MEdv44JIdlLh-GgqxOTZD7DHq28xJowhQFmDnT3NDIE"; - - static readonly UTF8Encoding Utf8NoBom = new(false); - - static string EncodeCredentials(string username, string password) => ToBase64String(Utf8NoBom.GetBytes($"{username}:{password}")); - - [Fact] - public void from_username_and_password() { - var user = Fakers.Users.WithNonAsciiPassword(); - - var value = new AuthenticationHeaderValue( - Constants.Headers.BasicScheme, - EncodeCredentials(user.LoginName, user.Password) - ); - - var basicAuthInfo = value.ToString(); - - var credentials = new UserCredentials(user.LoginName, user.Password); - - credentials.Username.ShouldBe(user.LoginName); - credentials.Password.ShouldBe(user.Password); - credentials.ToString().ShouldBe(basicAuthInfo); - } - - [Fact] - public void from_bearer_token() { - var credentials = new UserCredentials(JwtToken); - - credentials.Username.ShouldBeNull(); - credentials.Password.ShouldBeNull(); - credentials.ToString().ShouldBe($"Bearer {JwtToken}"); - } -} \ No newline at end of file diff --git a/test/EventStore.Client.UserManagement.Tests/changing_user_password.cs b/test/EventStore.Client.UserManagement.Tests/changing_user_password.cs deleted file mode 100644 index d3931c1ea..000000000 --- a/test/EventStore.Client.UserManagement.Tests/changing_user_password.cs +++ /dev/null @@ -1,76 +0,0 @@ -namespace EventStore.Client.Tests; - -public class changing_user_password : IClassFixture { - public changing_user_password(ITestOutputHelper output, EventStoreFixture fixture) => Fixture = fixture.With(x => x.CaptureTestRun(output)); - - EventStoreFixture Fixture { get; } - - public static IEnumerable NullInputCases() { - yield return Fakers.Users.Generate().WithResult(x => new object?[] { null, x.Password, x.Password, "loginName" }); - yield return Fakers.Users.Generate().WithResult(x => new object?[] { x.LoginName, null, x.Password, "currentPassword" }); - yield return Fakers.Users.Generate().WithResult(x => new object?[] { x.LoginName, x.Password, null, "newPassword" }); - } - - [Theory] - [MemberData(nameof(NullInputCases))] - public async Task with_null_input_throws(string loginName, string currentPassword, string newPassword, string paramName) { - var ex = await Fixture.Users - .ChangePasswordAsync(loginName, currentPassword, newPassword, userCredentials: TestCredentials.Root) - .ShouldThrowAsync(); - - ex.ParamName.ShouldBe(paramName); - } - - public static IEnumerable EmptyInputCases() { - yield return Fakers.Users.Generate().WithResult(x => new object?[] { string.Empty, x.Password, x.Password, "loginName" }); - yield return Fakers.Users.Generate().WithResult(x => new object?[] { x.LoginName, string.Empty, x.Password, "currentPassword" }); - yield return Fakers.Users.Generate().WithResult(x => new object?[] { x.LoginName, x.Password, string.Empty, "newPassword" }); - } - - [Theory] - [MemberData(nameof(EmptyInputCases))] - public async Task with_empty_input_throws(string loginName, string currentPassword, string newPassword, string paramName) { - var ex = await Fixture.Users - .ChangePasswordAsync(loginName, currentPassword, newPassword, userCredentials: TestCredentials.Root) - .ShouldThrowAsync(); - - ex.ParamName.ShouldBe(paramName); - } - - [Theory(Skip = "This can't be right")] - [ClassData(typeof(InvalidCredentialsTestCases))] - public async Task with_user_with_insufficient_credentials_throws(string loginName, UserCredentials userCredentials) { - await Fixture.Users.CreateUserAsync(loginName, "Full Name", Array.Empty(), "password", userCredentials: TestCredentials.Root); - - await Fixture.Users - .ChangePasswordAsync(loginName, "password", "newPassword", userCredentials: userCredentials) - .ShouldThrowAsync(); - } - - [Fact] - public async Task when_the_current_password_is_wrong_throws() { - var user = await Fixture.CreateTestUser(); - - await Fixture.Users - .ChangePasswordAsync(user.LoginName, "wrong-password", "new-password", userCredentials: TestCredentials.Root) - .ShouldThrowAsync(); - } - - [Fact] - public async Task with_correct_credentials() { - var user = await Fixture.CreateTestUser(); - - await Fixture.Users - .ChangePasswordAsync(user.LoginName, user.Password, "new-password", userCredentials: TestCredentials.Root) - .ShouldNotThrowAsync(); - } - - [Fact] - public async Task with_own_credentials() { - var user = await Fixture.CreateTestUser(); - - await Fixture.Users - .ChangePasswordAsync(user.LoginName, user.Password, "new-password", userCredentials: user.Credentials) - .ShouldNotThrowAsync(); - } -} \ No newline at end of file diff --git a/test/EventStore.Client.UserManagement.Tests/creating_a_user.cs b/test/EventStore.Client.UserManagement.Tests/creating_a_user.cs deleted file mode 100644 index abd37bab1..000000000 --- a/test/EventStore.Client.UserManagement.Tests/creating_a_user.cs +++ /dev/null @@ -1,84 +0,0 @@ -namespace EventStore.Client.Tests; - -public class creating_a_user : IClassFixture { - public creating_a_user(ITestOutputHelper outputHelper, InsecureClientTestFixture fixture) => - Fixture = fixture.With(x => x.CaptureTestRun(outputHelper)); - - InsecureClientTestFixture Fixture { get; } - - public static IEnumerable NullInputCases() { - yield return Fakers.Users.Generate().WithResult(x => new object?[] { null, x.FullName, x.Groups, x.Password, "loginName" }); - yield return Fakers.Users.Generate().WithResult(x => new object?[] { x.LoginName, null, x.Groups, x.Password, "fullName" }); - yield return Fakers.Users.Generate().WithResult(x => new object?[] { x.LoginName, x.FullName, null, x.Password, "groups" }); - yield return Fakers.Users.Generate().WithResult(x => new object?[] { x.LoginName, x.FullName, x.Groups, null, "password" }); - } - - [Theory] - [MemberData(nameof(NullInputCases))] - public async Task with_null_input_throws(string loginName, string fullName, string[] groups, string password, string paramName) { - var ex = await Fixture.Users - .CreateUserAsync(loginName, fullName, groups, password, userCredentials: TestCredentials.Root) - .ShouldThrowAsync(); - - ex.ParamName.ShouldBe(paramName); - } - - public static IEnumerable EmptyInputCases() { - yield return Fakers.Users.Generate().WithResult(x => new object?[] { string.Empty, x.FullName, x.Groups, x.Password, "loginName" }); - yield return Fakers.Users.Generate().WithResult(x => new object?[] { x.LoginName, string.Empty, x.Groups, x.Password, "fullName" }); - yield return Fakers.Users.Generate().WithResult(x => new object?[] { x.LoginName, x.FullName, x.Groups, string.Empty, "password" }); - } - - [Theory] - [MemberData(nameof(EmptyInputCases))] - public async Task with_empty_input_throws(string loginName, string fullName, string[] groups, string password, string paramName) { - var ex = await Fixture.Users - .CreateUserAsync(loginName, fullName, groups, password, userCredentials: TestCredentials.Root) - .ShouldThrowAsync(); - - ex.ParamName.ShouldBe(paramName); - } - - [Fact] - public async Task with_password_containing_ascii_chars() { - var user = Fakers.Users.Generate(); - - await Fixture.Users - .CreateUserAsync(user.LoginName, user.FullName, user.Groups, user.Password, userCredentials: TestCredentials.Root) - .ShouldNotThrowAsync(); - } - - [Theory] - [ClassData(typeof(InvalidCredentialsTestCases))] - public async Task with_user_with_insufficient_credentials_throws(InvalidCredentialsTestCase testCase) => - await Fixture.Users - .CreateUserAsync(testCase.User.LoginName, testCase.User.FullName, testCase.User.Groups, testCase.User.Password, userCredentials: testCase.User.Credentials) - .ShouldThrowAsync(testCase.ExpectedException); - - [Fact] - public async Task can_be_read() { - var user = Fakers.Users.Generate(); - - await Fixture.Users - .CreateUserAsync( - user.LoginName, - user.FullName, - user.Groups, - user.Password, - userCredentials: TestCredentials.Root - ) - .ShouldNotThrowAsync(); - - var actual = await Fixture.Users.GetUserAsync(user.LoginName, userCredentials: TestCredentials.Root); - - var expected = new UserDetails( - user.Details.LoginName, - user.Details.FullName, - user.Details.Groups, - user.Details.Disabled, - actual.DateLastUpdated - ); - - actual.ShouldBeEquivalentTo(expected); - } -} \ No newline at end of file diff --git a/test/EventStore.Client.UserManagement.Tests/deleting_a_user.cs b/test/EventStore.Client.UserManagement.Tests/deleting_a_user.cs deleted file mode 100644 index 9f5948fb6..000000000 --- a/test/EventStore.Client.UserManagement.Tests/deleting_a_user.cs +++ /dev/null @@ -1,68 +0,0 @@ -namespace EventStore.Client.Tests; - -public class deleting_a_user : IClassFixture { - public deleting_a_user(ITestOutputHelper output, InsecureClientTestFixture fixture) => - Fixture = fixture.With(x => x.CaptureTestRun(output)); - - InsecureClientTestFixture Fixture { get; } - - [Fact] - public async Task with_null_input_throws() { - var ex = await Fixture.Users - .DeleteUserAsync(null!, userCredentials: TestCredentials.Root) - .ShouldThrowAsync(); - - ex.ParamName.ShouldBe("loginName"); - } - - [Fact] - public async Task with_empty_input_throws() { - var ex = await Fixture.Users - .DeleteUserAsync(string.Empty, userCredentials: TestCredentials.Root) - .ShouldThrowAsync(); - - ex.ParamName.ShouldBe("loginName"); - } - - [Theory] - [ClassData(typeof(InvalidCredentialsTestCases))] - public async Task with_user_with_insufficient_credentials_throws(InvalidCredentialsTestCase testCase) { - await Fixture.Users.CreateUserAsync( - testCase.User.LoginName, - testCase.User.FullName, - testCase.User.Groups, - testCase.User.Password, - userCredentials: TestCredentials.Root - ); - - await Fixture.Users - .DeleteUserAsync(testCase.User.LoginName, userCredentials: testCase.User.Credentials) - .ShouldThrowAsync(testCase.ExpectedException); - } - - [Fact] - public async Task cannot_be_read() { - var user = await Fixture.CreateTestUser(); - - await Fixture.Users.DeleteUserAsync(user.LoginName, userCredentials: TestCredentials.Root); - - var ex = await Fixture.Users - .GetUserAsync(user.LoginName, userCredentials: TestCredentials.Root) - .ShouldThrowAsync(); - - ex.LoginName.ShouldBe(user.LoginName); - } - - [Fact] - public async Task a_second_time_throws() { - var user = await Fixture.CreateTestUser(); - - await Fixture.Users.DeleteUserAsync(user.LoginName, userCredentials: TestCredentials.Root); - - var ex = await Fixture.Users - .DeleteUserAsync(user.LoginName, userCredentials: TestCredentials.Root) - .ShouldThrowAsync(); - - ex.LoginName.ShouldBe(user.LoginName); - } -} \ No newline at end of file diff --git a/test/EventStore.Client.UserManagement.Tests/disabling_a_user.cs b/test/EventStore.Client.UserManagement.Tests/disabling_a_user.cs deleted file mode 100644 index c72ab0295..000000000 --- a/test/EventStore.Client.UserManagement.Tests/disabling_a_user.cs +++ /dev/null @@ -1,64 +0,0 @@ -namespace EventStore.Client.Tests; - -public class disabling_a_user : IClassFixture { - public disabling_a_user(ITestOutputHelper output, InsecureClientTestFixture fixture) => Fixture = fixture.With(x => x.CaptureTestRun(output)); - - InsecureClientTestFixture Fixture { get; } - - [Fact] - public async Task with_null_input_throws() { - var ex = await Fixture.Users - .DisableUserAsync(null!, userCredentials: TestCredentials.Root) - .ShouldThrowAsync(); - - // must fix since it is returning value instead of param name - //ex.ParamName.ShouldBe("loginName"); - } - - [Fact] - public async Task with_empty_input_throws() { - var ex = await Fixture.Users - .DisableUserAsync(string.Empty, userCredentials: TestCredentials.Root) - .ShouldThrowAsync(); - - ex.ParamName.ShouldBe("loginName"); - } - - [Theory] - [ClassData(typeof(InvalidCredentialsTestCases))] - public async Task with_user_with_insufficient_credentials_throws(InvalidCredentialsTestCase testCase) { - await Fixture.Users.CreateUserAsync( - testCase.User.LoginName, - testCase.User.FullName, - testCase.User.Groups, - testCase.User.Password, - userCredentials: TestCredentials.Root - ); - - await Fixture.Users - .DisableUserAsync(testCase.User.LoginName, userCredentials: testCase.User.Credentials) - .ShouldThrowAsync(testCase.ExpectedException); - } - - [Fact] - public async Task that_was_disabled() { - var user = await Fixture.CreateTestUser(); - - await Fixture.Users - .DisableUserAsync(user.LoginName, userCredentials: TestCredentials.Root) - .ShouldNotThrowAsync(); - - await Fixture.Users - .DisableUserAsync(user.LoginName, userCredentials: TestCredentials.Root) - .ShouldNotThrowAsync(); - } - - [Fact] - public async Task that_is_enabled() { - var user = await Fixture.CreateTestUser(); - - await Fixture.Users - .DisableUserAsync(user.LoginName, userCredentials: TestCredentials.Root) - .ShouldNotThrowAsync(); - } -} \ No newline at end of file diff --git a/test/EventStore.Client.UserManagement.Tests/enabling_a_user.cs b/test/EventStore.Client.UserManagement.Tests/enabling_a_user.cs deleted file mode 100644 index 79755e891..000000000 --- a/test/EventStore.Client.UserManagement.Tests/enabling_a_user.cs +++ /dev/null @@ -1,62 +0,0 @@ -namespace EventStore.Client.Tests; - -public class enabling_a_user : IClassFixture { - public enabling_a_user(ITestOutputHelper output, InsecureClientTestFixture fixture) => - Fixture = fixture.With(x => x.CaptureTestRun(output)); - - InsecureClientTestFixture Fixture { get; } - - [Fact] - public async Task with_null_input_throws() { - var ex = await Fixture.Users - .EnableUserAsync(null!, userCredentials: TestCredentials.Root) - .ShouldThrowAsync(); - - ex.ParamName.ShouldBe("loginName"); - } - - [Fact] - public async Task with_empty_input_throws() { - var ex = await Fixture.Users - .EnableUserAsync(string.Empty, userCredentials: TestCredentials.Root) - .ShouldThrowAsync(); - - ex.ParamName.ShouldBe("loginName"); - } - - [Theory] - [ClassData(typeof(InvalidCredentialsTestCases))] - public async Task with_user_with_insufficient_credentials_throws(InvalidCredentialsTestCase testCase) { - await Fixture.Users.CreateUserAsync( - testCase.User.LoginName, - testCase.User.FullName, - testCase.User.Groups, - testCase.User.Password, - userCredentials: TestCredentials.Root - ); - - await Fixture.Users - .EnableUserAsync(testCase.User.LoginName, userCredentials: testCase.User.Credentials) - .ShouldThrowAsync(testCase.ExpectedException); - } - - [Fact] - public async Task that_was_disabled() { - var user = await Fixture.CreateTestUser(); - - await Fixture.Users.DisableUserAsync(user.LoginName, userCredentials: TestCredentials.Root); - - await Fixture.Users - .EnableUserAsync(user.LoginName, userCredentials: TestCredentials.Root) - .ShouldNotThrowAsync(); - } - - [Fact] - public async Task that_is_enabled() { - var user = await Fixture.CreateTestUser(); - - await Fixture.Users - .EnableUserAsync(user.LoginName, userCredentials: TestCredentials.Root) - .ShouldNotThrowAsync(); - } -} \ No newline at end of file diff --git a/test/EventStore.Client.UserManagement.Tests/getting_current_user.cs b/test/EventStore.Client.UserManagement.Tests/getting_current_user.cs deleted file mode 100644 index b0cb3af2d..000000000 --- a/test/EventStore.Client.UserManagement.Tests/getting_current_user.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace EventStore.Client.Tests; - -public class getting_current_user : IClassFixture { - public getting_current_user(ITestOutputHelper output, EventStoreFixture fixture) => - Fixture = fixture.With(x => x.CaptureTestRun(output)); - - EventStoreFixture Fixture { get; } - - [Fact] - public async Task returns_the_current_user() { - var user = await Fixture.Users.GetCurrentUserAsync(TestCredentials.Root); - user.LoginName.ShouldBe(TestCredentials.Root.Username); - } -} \ No newline at end of file diff --git a/test/EventStore.Client.UserManagement.Tests/listing_users.cs b/test/EventStore.Client.UserManagement.Tests/listing_users.cs deleted file mode 100644 index 4760e7898..000000000 --- a/test/EventStore.Client.UserManagement.Tests/listing_users.cs +++ /dev/null @@ -1,42 +0,0 @@ -namespace EventStore.Client.Tests; - -public class listing_users : IClassFixture { - public listing_users(ITestOutputHelper output, EventStoreFixture fixture) => - Fixture = fixture.With(x => x.CaptureTestRun(output)); - - EventStoreFixture Fixture { get; } - - [Fact] - public async Task returns_all_created_users() { - var seed = await Fixture.CreateTestUsers(); - - var admin = new UserDetails("admin", "Event Store Administrator", new[] { "$admins" }, false, default); - var ops = new UserDetails("ops", "Event Store Operations", new[] { "$ops" }, false, default); - - var expected = new[] { admin, ops } - .Concat(seed.Select(user => user.Details)) - .ToArray(); - - var actual = await Fixture.Users - .ListAllAsync(userCredentials: TestCredentials.Root) - .Select(user => new UserDetails(user.LoginName, user.FullName, user.Groups, user.Disabled, default)) - .ToArrayAsync(); - - expected.ShouldBeSubsetOf(actual); - } - - [Fact] - public async Task returns_all_system_users() { - var admin = new UserDetails("admin", "Event Store Administrator", new[] { "$admins" }, false, default); - var ops = new UserDetails("ops", "Event Store Operations", new[] { "$ops" }, false, default); - - var expected = new[] { admin, ops }; - - var actual = await Fixture.Users - .ListAllAsync(userCredentials: TestCredentials.Root) - .Select(user => new UserDetails(user.LoginName, user.FullName, user.Groups, user.Disabled, default)) - .ToArrayAsync(); - - expected.ShouldBeSubsetOf(actual); - } -} \ No newline at end of file diff --git a/test/EventStore.Client.UserManagement.Tests/resetting_user_password.cs b/test/EventStore.Client.UserManagement.Tests/resetting_user_password.cs deleted file mode 100644 index 549e7119b..000000000 --- a/test/EventStore.Client.UserManagement.Tests/resetting_user_password.cs +++ /dev/null @@ -1,80 +0,0 @@ -namespace EventStore.Client.Tests; - -public class resetting_user_password : IClassFixture { - public resetting_user_password(ITestOutputHelper output, InsecureClientTestFixture fixture) => - Fixture = fixture.With(x => x.CaptureTestRun(output)); - - InsecureClientTestFixture Fixture { get; } - - public static IEnumerable NullInputCases() { - yield return Fakers.Users.Generate().WithResult(x => new object?[] { null, x.Password, "loginName" }); - yield return Fakers.Users.Generate().WithResult(x => new object?[] { x.LoginName, null, "newPassword" }); - } - - [Theory] - [MemberData(nameof(NullInputCases))] - public async Task with_null_input_throws(string loginName, string newPassword, string paramName) { - var ex = await Fixture.Users - .ResetPasswordAsync(loginName, newPassword, userCredentials: TestCredentials.Root) - .ShouldThrowAsync(); - - ex.ParamName.ShouldBe(paramName); - } - - public static IEnumerable EmptyInputCases() { - yield return Fakers.Users.Generate().WithResult(x => new object?[] { string.Empty, x.Password, "loginName" }); - yield return Fakers.Users.Generate().WithResult(x => new object?[] { x.LoginName, string.Empty, "newPassword" }); - } - - [Theory] - [MemberData(nameof(EmptyInputCases))] - public async Task with_empty_input_throws(string loginName, string newPassword, string paramName) { - var ex = await Fixture.Users - .ResetPasswordAsync(loginName, newPassword, userCredentials: TestCredentials.Root) - .ShouldThrowAsync(); - - ex.ParamName.ShouldBe(paramName); - } - - [Theory] - [ClassData(typeof(InvalidCredentialsTestCases))] - public async Task with_user_with_insufficient_credentials_throws(InvalidCredentialsTestCase testCase) { - await Fixture.Users.CreateUserAsync( - testCase.User.LoginName, - testCase.User.FullName, - testCase.User.Groups, - testCase.User.Password, - userCredentials: TestCredentials.Root - ); - - await Fixture.Users - .ResetPasswordAsync(testCase.User.LoginName, "newPassword", userCredentials: testCase.User.Credentials) - .ShouldThrowAsync(testCase.ExpectedException); - } - - [Fact] - public async Task with_correct_credentials() { - var user = Fakers.Users.Generate(); - - await Fixture.Users.CreateUserAsync( - user.LoginName, - user.FullName, - user.Groups, - user.Password, - userCredentials: TestCredentials.Root - ); - - await Fixture.Users - .ResetPasswordAsync(user.LoginName, "new-password", userCredentials: TestCredentials.Root) - .ShouldNotThrowAsync(); - } - - [Fact] - public async Task with_own_credentials_throws() { - var user = await Fixture.CreateTestUser(); - - await Fixture.Users - .ResetPasswordAsync(user.LoginName, "new-password", userCredentials: user.Credentials) - .ShouldThrowAsync(); - } -} \ No newline at end of file From ebc6e60fb09ec4d83e8808f0cd471164b2e13890 Mon Sep 17 00:00:00 2001 From: William Chong Date: Tue, 7 Jan 2025 12:16:40 +0400 Subject: [PATCH 05/15] Rename test symbols --- .github/workflows/base.yml | 4 +- .github/workflows/ci.yml | 3 +- EventStore.Client.sln => Kurrent.Client.sln | 23 +- ...Settings => Kurrent.Client.sln.DotSettings | 0 src/Directory.Build.props | 8 +- .../Core/Certificates/X509Certificates.cs | 114 +++++ .../Core/ChannelBaseExtensions.cs | 10 + src/Kurrent.Client/Core/ChannelCache.cs | 146 ++++++ src/Kurrent.Client/Core/ChannelFactory.cs | 106 ++++ src/Kurrent.Client/Core/ChannelInfo.cs | 9 + src/Kurrent.Client/Core/ChannelSelector.cs | 24 + src/Kurrent.Client/Core/ClusterMessage.cs | 28 + .../Common/AsyncStreamReaderExtensions.cs | 29 ++ src/Kurrent.Client/Core/Common/Constants.cs | 62 +++ .../Diagnostics/ActivitySourceExtensions.cs | 85 ++++ .../ActivityTagsCollectionExtensions.cs | 32 ++ .../Diagnostics/Core/ActivityExtensions.cs | 52 ++ .../Common/Diagnostics/Core/ActivityStatus.cs | 13 + .../Core/ActivityStatusCodeHelper.cs | 24 + .../Core/ActivityTagsCollectionExtensions.cs | 25 + .../Diagnostics/Core/ExceptionExtensions.cs | 25 + .../Core/Telemetry/TelemetryTags.cs | 35 ++ .../Core/Tracing/TracingConstants.cs | 10 + .../Core/Tracing/TracingMetadata.cs | 32 ++ .../Diagnostics/EventMetadataExtensions.cs | 84 +++ .../Diagnostics/KurrentClientDiagnostics.cs | 8 + .../Diagnostics/Telemetry/TelemetryTags.cs | 12 + .../Diagnostics/Tracing/TracingConstants.cs | 10 + .../Core/Common/EnumerableTaskExtensions.cs | 11 + .../Core/Common/EpochExtensions.cs | 25 + .../Core/Common/KurrentCallOptions.cs | 74 +++ .../Core/Common/MetadataExtensions.cs | 29 ++ src/Kurrent.Client/Core/Common/Shims/Index.cs | 110 ++++ .../Core/Common/Shims/IsExternalInit.cs | 10 + src/Kurrent.Client/Core/Common/Shims/Range.cs | 75 +++ .../Core/Common/Shims/TaskCompletionSource.cs | 11 + .../Core/DefaultRequestVersionHandler.cs | 14 + src/Kurrent.Client/Core/EndPointExtensions.cs | 41 ++ src/Kurrent.Client/Core/EventData.cs | 65 +++ src/Kurrent.Client/Core/EventRecord.cs | 80 +++ src/Kurrent.Client/Core/EventTypeFilter.cs | 145 ++++++ .../Core/Exceptions/AccessDeniedException.cs | 22 + .../ConnectionStringParseException.cs | 15 + .../ConnectionString/DuplicateKeyException.cs | 13 + .../InvalidClientCertificateException.cs | 14 + .../ConnectionString/InvalidHostException.cs | 13 + .../InvalidKeyValuePairException.cs | 13 + .../InvalidSchemeException.cs | 14 + .../InvalidSettingException.cs | 12 + .../InvalidUserCredentialsException.cs | 13 + .../ConnectionString/NoSchemeException.cs | 12 + .../Core/Exceptions/DiscoveryException.cs | 33 ++ .../Exceptions/NotAuthenticatedException.cs | 16 + .../Core/Exceptions/NotLeaderException.cs | 26 + ...equiredMetadataPropertyMissingException.cs | 18 + .../Exceptions/ScavengeNotFoundException.cs | 23 + .../Core/Exceptions/StreamDeletedException.cs | 24 + .../Exceptions/StreamNotFoundException.cs | 23 + .../Core/Exceptions/UserNotFoundException.cs | 23 + .../WrongExpectedVersionException.cs | 66 +++ src/Kurrent.Client/Core/FromAll.cs | 100 ++++ src/Kurrent.Client/Core/FromStream.cs | 100 ++++ .../Core/GossipChannelSelector.cs | 99 ++++ src/Kurrent.Client/Core/GrpcGossipClient.cs | 30 ++ .../Core/GrpcServerCapabilitiesClient.cs | 75 +++ src/Kurrent.Client/Core/HashCode.cs | 37 ++ src/Kurrent.Client/Core/HttpFallback.cs | 143 ++++++ src/Kurrent.Client/Core/IChannelSelector.cs | 14 + src/Kurrent.Client/Core/IEventFilter.cs | 21 + src/Kurrent.Client/Core/IGossipClient.cs | 10 + src/Kurrent.Client/Core/IPosition.cs | 7 + .../Core/IServerCapabilitiesClient.cs | 9 + .../Interceptors/ConnectionNameInterceptor.cs | 45 ++ .../Interceptors/ReportLeaderInterceptor.cs | 126 +++++ .../Interceptors/TypedExceptionInterceptor.cs | 165 ++++++ src/Kurrent.Client/Core/KurrentClientBase.cs | 151 ++++++ .../Core/KurrentClientConnectivitySettings.cs | 128 +++++ .../Core/KurrentClientOperationOptions.cs | 46 ++ .../KurrentClientSettings.ConnectionString.cs | 405 +++++++++++++++ .../Core/KurrentClientSettings.cs | 61 +++ src/Kurrent.Client/Core/NodePreference.cs | 26 + .../Core/NodePreferenceComparers.cs | 49 ++ src/Kurrent.Client/Core/NodeSelector.cs | 63 +++ src/Kurrent.Client/Core/Position.cs | 200 ++++++++ .../Core/PrefixFilterExpression.cs | 64 +++ .../Core/ReconnectionRequired.cs | 15 + .../Core/RegularFilterExpression.cs | 86 ++++ src/Kurrent.Client/Core/ResolvedEvent.cs | 60 +++ src/Kurrent.Client/Core/ServerCapabilities.cs | 10 + src/Kurrent.Client/Core/SharingProvider.cs | 111 ++++ .../Core/SingleNodeChannelSelector.cs | 36 ++ .../Core/SingleNodeHttpHandler.cs | 22 + src/Kurrent.Client/Core/StreamFilter.cs | 134 +++++ src/Kurrent.Client/Core/StreamIdentifier.cs | 28 + src/Kurrent.Client/Core/StreamPosition.cs | 201 ++++++++ src/Kurrent.Client/Core/StreamRevision.cs | 196 +++++++ src/Kurrent.Client/Core/StreamState.cs | 88 ++++ .../Core/SubscriptionDroppedReason.cs | 19 + src/Kurrent.Client/Core/SystemRoles.cs | 21 + src/Kurrent.Client/Core/SystemStreams.cs | 54 ++ src/Kurrent.Client/Core/TaskExtensions.cs | 22 + src/Kurrent.Client/Core/UserCredentials.cs | 54 ++ src/Kurrent.Client/Core/Uuid.cs | 203 ++++++++ src/Kurrent.Client/Core/protos/code.proto | 186 +++++++ src/Kurrent.Client/Core/protos/gossip.proto | 44 ++ .../Core/protos/operations.proto | 45 ++ .../Core/protos/persistentsubscriptions.proto | 370 ++++++++++++++ .../Core/protos/projectionmanagement.proto | 174 +++++++ .../Core/protos/serverfeatures.proto | 19 + src/Kurrent.Client/Core/protos/shared.proto | 61 +++ src/Kurrent.Client/Core/protos/status.proto | 48 ++ src/Kurrent.Client/Core/protos/streams.proto | 316 ++++++++++++ .../Core/protos/usermanagement.proto | 119 +++++ src/Kurrent.Client/Kurrent.Client.csproj | 112 ++++ .../TracerProviderBuilderExtensions.cs | 19 + .../Operations/DatabaseScavengeResult.cs | 81 +++ .../KurrentOperationsClient.Admin.cs | 103 ++++ .../KurrentOperationsClient.Scavenge.cs | 82 +++ .../Operations/KurrentOperationsClient.cs | 33 ++ ...ationsClientServiceCollectionExtensions.cs | 73 +++ .../Operations/ScavengeResult.cs | 25 + ...entPersistentSubscriptionsClient.Create.cs | 252 +++++++++ ...entPersistentSubscriptionsClient.Delete.cs | 55 ++ ...rrentPersistentSubscriptionsClient.Info.cs | 77 +++ ...rrentPersistentSubscriptionsClient.List.cs | 106 ++++ ...rrentPersistentSubscriptionsClient.Read.cs | 478 ++++++++++++++++++ ...sistentSubscriptionsClient.ReplayParked.cs | 94 ++++ ...entSubscriptionsClient.RestartSubsystem.cs | 32 ++ ...entPersistentSubscriptionsClient.Update.cs | 161 ++++++ .../KurrentPersistentSubscriptionsClient.cs | 46 ++ ...SubscriptionsClientCollectionExtensions.cs | 61 +++ .../MaximumSubscribersReachedException.cs | 30 ++ .../PersistentSubscription.cs | 205 ++++++++ ...entSubscriptionDroppedByServerException.cs | 31 ++ .../PersistentSubscriptionExtraStatistic.cs | 23 + .../PersistentSubscriptionInfo.cs | 224 ++++++++ .../PersistentSubscriptionMessage.cs | 33 ++ .../PersistentSubscriptionNakEventAction.cs | 31 ++ ...PersistentSubscriptionNotFoundException.cs | 29 ++ .../PersistentSubscriptionSettings.cs | 112 ++++ .../SystemConsumerStrategies.cs | 22 + ...rrentProjectionManagementClient.Control.cs | 102 ++++ ...urrentProjectionManagementClient.Create.cs | 80 +++ ...KurrentProjectionManagementClient.State.cs | 205 ++++++++ ...ntProjectionManagementClient.Statistics.cs | 104 ++++ ...urrentProjectionManagementClient.Update.cs | 40 ++ .../KurrentProjectionManagementClient.cs | 32 ++ ...ionManagementClientCollectionExtensions.cs | 74 +++ .../ProjectionManagement/ProjectionDetails.cs | 164 ++++++ .../Streams/ConditionalWriteResult.cs | 87 ++++ .../Streams/ConditionalWriteStatus.cs | 21 + src/Kurrent.Client/Streams/DeadLine.cs | 12 + src/Kurrent.Client/Streams/DeleteResult.cs | 46 ++ src/Kurrent.Client/Streams/Direction.cs | 16 + src/Kurrent.Client/Streams/IWriteResult.cs | 23 + .../Streams/InvalidTransactionException.cs | 31 ++ .../Streams/KurrentClient.Append.cs | 430 ++++++++++++++++ .../Streams/KurrentClient.Delete.cs | 65 +++ .../Streams/KurrentClient.Metadata.cs | 106 ++++ .../Streams/KurrentClient.Read.cs | 468 +++++++++++++++++ .../Streams/KurrentClient.Subscriptions.cs | 302 +++++++++++ .../Streams/KurrentClient.Tombstone.cs | 63 +++ src/Kurrent.Client/Streams/KurrentClient.cs | 166 ++++++ .../Streams/KurrentClientExtensions.cs | 109 ++++ ...urrentClientServiceCollectionExtensions.cs | 153 ++++++ .../MaximumAppendSizeExceededException.cs | 33 ++ src/Kurrent.Client/Streams/ReadState.cs | 15 + src/Kurrent.Client/Streams/StreamAcl.cs | 107 ++++ .../Streams/StreamAclJsonConverter.cs | 111 ++++ src/Kurrent.Client/Streams/StreamMessage.cs | 83 +++ src/Kurrent.Client/Streams/StreamMetadata.cs | 111 ++++ .../Streams/StreamMetadataJsonConverter.cs | 145 ++++++ .../Streams/StreamMetadataResult.cs | 83 +++ .../Streams/StreamSubscription.cs | 165 ++++++ .../Streams/Streams/AppendReq.cs | 15 + .../Streams/Streams/BatchAppendReq.cs | 34 ++ .../Streams/Streams/BatchAppendResp.cs | 49 ++ .../Streams/Streams/DeleteReq.cs | 15 + src/Kurrent.Client/Streams/Streams/ReadReq.cs | 86 ++++ .../Streams/Streams/TombstoneReq.cs | 15 + .../Streams/SubscriptionFilterOptions.cs | 54 ++ src/Kurrent.Client/Streams/SuccessResult.cs | 57 +++ .../Streams/SystemEventTypes.cs | 31 ++ src/Kurrent.Client/Streams/SystemMetadata.cs | 71 +++ src/Kurrent.Client/Streams/SystemSettings.cs | 53 ++ .../Streams/SystemSettingsJsonConverter.cs | 62 +++ .../Streams/WriteResultExtensions.cs | 12 + .../Streams/WrongExpectedVersionResult.cs | 58 +++ .../KurrentUserManagementClient.cs | 279 ++++++++++ ...serManagementClientCollectionExtensions.cs | 72 +++ .../KurrentUserManagerClientExtensions.cs | 23 + .../UserManagement/UserDetails.cs | 97 ++++ test/Directory.Build.props | 4 +- ...ore.Client.Tests.Common.csproj.DotSettings | 5 - .../Extensions/ShouldThrowAsyncExtensions.cs | 14 - .../EventStoreClientOperationsTests.cs | 14 - .../.env | 0 .../ApplicationInfo.cs | 2 +- .../AssertEx.cs | 2 +- .../Certificates.cs | 2 +- .../Extensions/ConfigurationExtensions.cs | 2 +- .../Extensions/KurrentClientExtensions.cs} | 7 +- .../KurrentClientWarmupExtensions.cs} | 21 +- .../Extensions/OperatingSystemExtensions.cs | 2 +- .../Extensions/ReadOnlyMemoryExtensions.cs | 3 +- .../Extensions/ShouldThrowAsyncExtensions.cs | 16 + .../Extensions/TaskExtensions.cs | 2 +- .../Extensions/TypeExtensions.cs | 2 +- .../Extensions/WithExtension.cs | 2 +- .../Facts/AnonymousAccess.cs | 2 +- .../Facts/Deprecation.cs | 2 +- .../Facts/Regression.cs | 2 +- .../Fakers/TestUserFaker.cs | 4 +- .../Fixtures/BaseTestNode.cs | 6 +- .../Fixtures/CertificatesManager.cs | 2 +- .../Fixtures/KurrentFixtureOptions.cs | 6 +- .../KurrentPermanentFixture.Helpers.cs | 5 +- .../Fixtures/KurrentPermanentFixture.cs | 34 +- .../Fixtures/KurrentPermanentTestNode.cs | 10 +- .../KurrentTemporaryFixture.Helpers.cs | 5 +- .../Fixtures/KurrentTemporaryFixture.cs | 30 +- .../Fixtures/KurrentTemporaryTestNode.cs | 11 +- .../FluentDockerBuilderExtensions.cs | 2 +- .../FluentDockerServiceExtensions.cs | 2 +- .../FluentDocker/TestBypassService.cs | 2 +- .../FluentDocker/TestCompositeService.cs | 2 +- .../FluentDocker/TestContainerService.cs | 2 +- .../FluentDocker/TestService.cs | 2 +- .../GlobalEnvironment.cs | 2 +- .../InterlockedBoolean.cs | 0 .../Kurrent.Client.Tests.Common.csproj} | 23 +- .../Logging.cs | 2 +- .../PasswordGenerator.cs | 2 +- .../Shouldly/ShouldThrowAsyncExtensions.cs | 2 +- .../TestCaseGenerator.cs | 2 +- .../TestCredentials.cs | 4 +- .../appsettings.Development.json | 0 .../appsettings.json | 0 .../docker-compose.certs.yml | 0 .../docker-compose.cluster.yml | 0 .../docker-compose.node.yml | 0 .../docker-compose.yml | 0 .../shared.env | 0 .../Assertions/ComparableAssertion.cs | 2 +- .../Assertions/EqualityAssertion.cs | 2 +- .../Assertions/NullArgumentAssertion.cs | 2 +- .../Assertions/StringConversionAssertion.cs | 2 +- .../Assertions/ValueObjectAssertion.cs | 2 +- .../AutoScenarioDataAttribute.cs | 2 +- .../ClientCertificatesTests.cs | 17 +- .../ConnectionStringTests.cs | 145 +++--- .../FromAllTests.cs | 3 +- .../FromStreamTests.cs | 3 +- .../GossipChannelSelectorTests.cs | 7 +- .../GrpcServerCapabilitiesClientTests.cs | 4 +- .../Kurrent.Client.Tests.csproj} | 7 +- .../KurrentClientOperationsTests.cs | 16 + .../NodePreferenceComparerTests.cs | 3 +- .../NodeSelectorTests.cs | 11 +- .../Operations/AuthenticationTests.cs | 8 +- .../Operations/MergeIndexTests.cs | 6 +- .../Operations/ResignNodeTests.cs | 7 +- .../RestartPersistentSubscriptionsTests.cs | 11 +- .../Operations/ScavengeTests.cs | 7 +- .../ShutdownNodeAuthenticationTests.cs | 7 +- .../Operations/ShutdownNodeTests.cs | 6 +- .../FilterTestCases.cs | 3 +- ...ToAllConnectWithoutReadPermissionsTests.cs | 6 +- .../SubscribeToAllFilterTests.cs | 7 +- .../SubscribeToAllGetInfoTests.cs | 6 +- ...eToAllListWithIncorrectCredentialsTests.cs | 6 +- ...SubscribeToAllNoDefaultCredentialsTests.cs | 6 +- .../SubscribeToAllReplayParkedTests.cs | 6 +- ...AllResultWithNormalUserCredentialsTests.cs | 6 +- .../SubscribeToAllReturnsAllSubscriptions.cs | 6 +- ...AllReturnsSubscriptionsToAllStreamTests.cs | 6 +- .../SubscribeToAllTests.cs | 7 +- ...beToAllUpdateExistingWithCheckpointTest.cs | 7 +- .../SubscribeToAllWithoutPSTests.cs | 7 +- .../SubscribeToStreamGetInfoTests.cs | 8 +- .../SubscribeToStreamListTests.cs | 9 +- ...scribeToStreamNoDefaultCredentialsTests.cs | 6 +- .../SubscribeToStreamReplayParkedTests.cs | 6 +- .../SubscribeToStreamTests.cs | 5 +- .../PositionTests.cs | 3 +- .../Projections/ListAllProjectionsTests.cs} | 16 +- .../ListContinuousProjectionsTests.cs | 35 ++ .../ListOneTimeProjectionsTests.cs | 21 + .../Projections}/ProjectionManagementTests.cs | 27 +- .../Projections/ResetProjectionTests.cs | 22 + .../RegularFilterExpressionTests.cs | 3 +- .../AllStreamWithNoAclSecurityTests.cs | 7 +- .../Security/DeleteStreamSecurityTests.cs | 7 +- .../Security/MultipleRoleSecurityTests.cs | 7 +- ...verridenSystemStreamSecurityForAllTests.cs | 7 +- .../OverridenSystemStreamSecurityTests.cs | 6 +- .../OverridenUserStreamSecurityTests.cs | 6 +- .../Security/ReadAllSecurityTests.cs | 7 +- .../Security/ReadStreamMetaSecurityTests.cs | 7 +- .../Security/ReadStreamSecurityTests.cs | 7 +- .../Security/SecurityFixture.cs | 4 +- .../StreamSecurityInheritanceTests.cs | 7 +- .../Security/SubscribeToAllSecurityTests.cs | 7 +- .../SubscribeToStreamSecurityTests.cs | 7 +- .../Security/SystemStreamSecurityTests.cs | 7 +- .../Security/WriteStreamMetaSecurityTests.cs | 7 +- .../Security/WriteStreamSecurityTests.cs | 4 +- .../SharingProviderTests.cs | 6 +- .../StreamPositionTests.cs | 3 +- .../StreamRevisionTests.cs | 3 +- .../StreamStateTests.cs | 3 +- .../Streams/AppendTests.cs | 5 +- .../Streams/Bugs/Obsolete/Issue104.cs | 6 +- .../Streams/Bugs/Obsolete/Issue2544.cs | 4 +- .../Streams/DeleteTests.cs | 5 +- .../Streams/Read/EventBinaryData.cs | 4 +- .../Streams/Read/EventDataComparer.cs | 4 +- .../Read/ReadAllEventsBackwardTests.cs | 5 +- .../Streams/Read/ReadAllEventsFixture.cs | 6 +- .../Streams/Read/ReadAllEventsForwardTests.cs | 5 +- .../Streams/Read/ReadStreamBackwardTests.cs | 5 +- ...dStreamEventsLinkedToDeletedStreamTests.cs | 7 +- .../Streams/Read/ReadStreamForwardTests.cs | 6 +- ...reamWhenHavingMaxCountSetForStreamTests.cs | 7 +- .../Streams/SoftDeleteTests.cs | 5 +- .../Streams/StreamMetadataTests.cs | 5 +- .../Streams/SubscribeToStreamTests.cs | 6 +- .../Streams/SubscriptionFilter.cs | 5 +- .../UuidTests.cs | 3 +- .../ValueObjectTests.cs | 2 +- .../X509CertificatesTests.cs | 3 +- 331 files changed, 15042 insertions(+), 440 deletions(-) rename EventStore.Client.sln => Kurrent.Client.sln (66%) rename EventStore.Client.sln.DotSettings => Kurrent.Client.sln.DotSettings (100%) create mode 100644 src/Kurrent.Client/Core/Certificates/X509Certificates.cs create mode 100644 src/Kurrent.Client/Core/ChannelBaseExtensions.cs create mode 100644 src/Kurrent.Client/Core/ChannelCache.cs create mode 100644 src/Kurrent.Client/Core/ChannelFactory.cs create mode 100644 src/Kurrent.Client/Core/ChannelInfo.cs create mode 100644 src/Kurrent.Client/Core/ChannelSelector.cs create mode 100644 src/Kurrent.Client/Core/ClusterMessage.cs create mode 100644 src/Kurrent.Client/Core/Common/AsyncStreamReaderExtensions.cs create mode 100644 src/Kurrent.Client/Core/Common/Constants.cs create mode 100644 src/Kurrent.Client/Core/Common/Diagnostics/ActivitySourceExtensions.cs create mode 100644 src/Kurrent.Client/Core/Common/Diagnostics/ActivityTagsCollectionExtensions.cs create mode 100644 src/Kurrent.Client/Core/Common/Diagnostics/Core/ActivityExtensions.cs create mode 100644 src/Kurrent.Client/Core/Common/Diagnostics/Core/ActivityStatus.cs create mode 100644 src/Kurrent.Client/Core/Common/Diagnostics/Core/ActivityStatusCodeHelper.cs create mode 100644 src/Kurrent.Client/Core/Common/Diagnostics/Core/ActivityTagsCollectionExtensions.cs create mode 100644 src/Kurrent.Client/Core/Common/Diagnostics/Core/ExceptionExtensions.cs create mode 100644 src/Kurrent.Client/Core/Common/Diagnostics/Core/Telemetry/TelemetryTags.cs create mode 100644 src/Kurrent.Client/Core/Common/Diagnostics/Core/Tracing/TracingConstants.cs create mode 100644 src/Kurrent.Client/Core/Common/Diagnostics/Core/Tracing/TracingMetadata.cs create mode 100644 src/Kurrent.Client/Core/Common/Diagnostics/EventMetadataExtensions.cs create mode 100644 src/Kurrent.Client/Core/Common/Diagnostics/KurrentClientDiagnostics.cs create mode 100644 src/Kurrent.Client/Core/Common/Diagnostics/Telemetry/TelemetryTags.cs create mode 100644 src/Kurrent.Client/Core/Common/Diagnostics/Tracing/TracingConstants.cs create mode 100644 src/Kurrent.Client/Core/Common/EnumerableTaskExtensions.cs create mode 100644 src/Kurrent.Client/Core/Common/EpochExtensions.cs create mode 100644 src/Kurrent.Client/Core/Common/KurrentCallOptions.cs create mode 100644 src/Kurrent.Client/Core/Common/MetadataExtensions.cs create mode 100644 src/Kurrent.Client/Core/Common/Shims/Index.cs create mode 100644 src/Kurrent.Client/Core/Common/Shims/IsExternalInit.cs create mode 100644 src/Kurrent.Client/Core/Common/Shims/Range.cs create mode 100644 src/Kurrent.Client/Core/Common/Shims/TaskCompletionSource.cs create mode 100644 src/Kurrent.Client/Core/DefaultRequestVersionHandler.cs create mode 100644 src/Kurrent.Client/Core/EndPointExtensions.cs create mode 100644 src/Kurrent.Client/Core/EventData.cs create mode 100644 src/Kurrent.Client/Core/EventRecord.cs create mode 100644 src/Kurrent.Client/Core/EventTypeFilter.cs create mode 100644 src/Kurrent.Client/Core/Exceptions/AccessDeniedException.cs create mode 100644 src/Kurrent.Client/Core/Exceptions/ConnectionString/ConnectionStringParseException.cs create mode 100644 src/Kurrent.Client/Core/Exceptions/ConnectionString/DuplicateKeyException.cs create mode 100644 src/Kurrent.Client/Core/Exceptions/ConnectionString/InvalidClientCertificateException.cs create mode 100644 src/Kurrent.Client/Core/Exceptions/ConnectionString/InvalidHostException.cs create mode 100644 src/Kurrent.Client/Core/Exceptions/ConnectionString/InvalidKeyValuePairException.cs create mode 100644 src/Kurrent.Client/Core/Exceptions/ConnectionString/InvalidSchemeException.cs create mode 100644 src/Kurrent.Client/Core/Exceptions/ConnectionString/InvalidSettingException.cs create mode 100644 src/Kurrent.Client/Core/Exceptions/ConnectionString/InvalidUserCredentialsException.cs create mode 100644 src/Kurrent.Client/Core/Exceptions/ConnectionString/NoSchemeException.cs create mode 100644 src/Kurrent.Client/Core/Exceptions/DiscoveryException.cs create mode 100644 src/Kurrent.Client/Core/Exceptions/NotAuthenticatedException.cs create mode 100644 src/Kurrent.Client/Core/Exceptions/NotLeaderException.cs create mode 100644 src/Kurrent.Client/Core/Exceptions/RequiredMetadataPropertyMissingException.cs create mode 100644 src/Kurrent.Client/Core/Exceptions/ScavengeNotFoundException.cs create mode 100644 src/Kurrent.Client/Core/Exceptions/StreamDeletedException.cs create mode 100644 src/Kurrent.Client/Core/Exceptions/StreamNotFoundException.cs create mode 100644 src/Kurrent.Client/Core/Exceptions/UserNotFoundException.cs create mode 100644 src/Kurrent.Client/Core/Exceptions/WrongExpectedVersionException.cs create mode 100644 src/Kurrent.Client/Core/FromAll.cs create mode 100644 src/Kurrent.Client/Core/FromStream.cs create mode 100644 src/Kurrent.Client/Core/GossipChannelSelector.cs create mode 100644 src/Kurrent.Client/Core/GrpcGossipClient.cs create mode 100644 src/Kurrent.Client/Core/GrpcServerCapabilitiesClient.cs create mode 100644 src/Kurrent.Client/Core/HashCode.cs create mode 100644 src/Kurrent.Client/Core/HttpFallback.cs create mode 100644 src/Kurrent.Client/Core/IChannelSelector.cs create mode 100644 src/Kurrent.Client/Core/IEventFilter.cs create mode 100644 src/Kurrent.Client/Core/IGossipClient.cs create mode 100644 src/Kurrent.Client/Core/IPosition.cs create mode 100644 src/Kurrent.Client/Core/IServerCapabilitiesClient.cs create mode 100644 src/Kurrent.Client/Core/Interceptors/ConnectionNameInterceptor.cs create mode 100644 src/Kurrent.Client/Core/Interceptors/ReportLeaderInterceptor.cs create mode 100644 src/Kurrent.Client/Core/Interceptors/TypedExceptionInterceptor.cs create mode 100644 src/Kurrent.Client/Core/KurrentClientBase.cs create mode 100644 src/Kurrent.Client/Core/KurrentClientConnectivitySettings.cs create mode 100644 src/Kurrent.Client/Core/KurrentClientOperationOptions.cs create mode 100644 src/Kurrent.Client/Core/KurrentClientSettings.ConnectionString.cs create mode 100644 src/Kurrent.Client/Core/KurrentClientSettings.cs create mode 100644 src/Kurrent.Client/Core/NodePreference.cs create mode 100644 src/Kurrent.Client/Core/NodePreferenceComparers.cs create mode 100644 src/Kurrent.Client/Core/NodeSelector.cs create mode 100644 src/Kurrent.Client/Core/Position.cs create mode 100644 src/Kurrent.Client/Core/PrefixFilterExpression.cs create mode 100644 src/Kurrent.Client/Core/ReconnectionRequired.cs create mode 100644 src/Kurrent.Client/Core/RegularFilterExpression.cs create mode 100644 src/Kurrent.Client/Core/ResolvedEvent.cs create mode 100644 src/Kurrent.Client/Core/ServerCapabilities.cs create mode 100644 src/Kurrent.Client/Core/SharingProvider.cs create mode 100644 src/Kurrent.Client/Core/SingleNodeChannelSelector.cs create mode 100644 src/Kurrent.Client/Core/SingleNodeHttpHandler.cs create mode 100644 src/Kurrent.Client/Core/StreamFilter.cs create mode 100644 src/Kurrent.Client/Core/StreamIdentifier.cs create mode 100644 src/Kurrent.Client/Core/StreamPosition.cs create mode 100644 src/Kurrent.Client/Core/StreamRevision.cs create mode 100644 src/Kurrent.Client/Core/StreamState.cs create mode 100644 src/Kurrent.Client/Core/SubscriptionDroppedReason.cs create mode 100644 src/Kurrent.Client/Core/SystemRoles.cs create mode 100644 src/Kurrent.Client/Core/SystemStreams.cs create mode 100644 src/Kurrent.Client/Core/TaskExtensions.cs create mode 100644 src/Kurrent.Client/Core/UserCredentials.cs create mode 100644 src/Kurrent.Client/Core/Uuid.cs create mode 100644 src/Kurrent.Client/Core/protos/code.proto create mode 100644 src/Kurrent.Client/Core/protos/gossip.proto create mode 100644 src/Kurrent.Client/Core/protos/operations.proto create mode 100644 src/Kurrent.Client/Core/protos/persistentsubscriptions.proto create mode 100644 src/Kurrent.Client/Core/protos/projectionmanagement.proto create mode 100644 src/Kurrent.Client/Core/protos/serverfeatures.proto create mode 100644 src/Kurrent.Client/Core/protos/shared.proto create mode 100644 src/Kurrent.Client/Core/protos/status.proto create mode 100644 src/Kurrent.Client/Core/protos/streams.proto create mode 100644 src/Kurrent.Client/Core/protos/usermanagement.proto create mode 100644 src/Kurrent.Client/Kurrent.Client.csproj create mode 100644 src/Kurrent.Client/OpenTelemetry/TracerProviderBuilderExtensions.cs create mode 100644 src/Kurrent.Client/Operations/DatabaseScavengeResult.cs create mode 100644 src/Kurrent.Client/Operations/KurrentOperationsClient.Admin.cs create mode 100644 src/Kurrent.Client/Operations/KurrentOperationsClient.Scavenge.cs create mode 100644 src/Kurrent.Client/Operations/KurrentOperationsClient.cs create mode 100644 src/Kurrent.Client/Operations/KurrentOperationsClientServiceCollectionExtensions.cs create mode 100644 src/Kurrent.Client/Operations/ScavengeResult.cs create mode 100644 src/Kurrent.Client/PersistentSubscriptions/KurrentPersistentSubscriptionsClient.Create.cs create mode 100644 src/Kurrent.Client/PersistentSubscriptions/KurrentPersistentSubscriptionsClient.Delete.cs create mode 100644 src/Kurrent.Client/PersistentSubscriptions/KurrentPersistentSubscriptionsClient.Info.cs create mode 100644 src/Kurrent.Client/PersistentSubscriptions/KurrentPersistentSubscriptionsClient.List.cs create mode 100644 src/Kurrent.Client/PersistentSubscriptions/KurrentPersistentSubscriptionsClient.Read.cs create mode 100644 src/Kurrent.Client/PersistentSubscriptions/KurrentPersistentSubscriptionsClient.ReplayParked.cs create mode 100644 src/Kurrent.Client/PersistentSubscriptions/KurrentPersistentSubscriptionsClient.RestartSubsystem.cs create mode 100644 src/Kurrent.Client/PersistentSubscriptions/KurrentPersistentSubscriptionsClient.Update.cs create mode 100644 src/Kurrent.Client/PersistentSubscriptions/KurrentPersistentSubscriptionsClient.cs create mode 100644 src/Kurrent.Client/PersistentSubscriptions/KurrentPersistentSubscriptionsClientCollectionExtensions.cs create mode 100644 src/Kurrent.Client/PersistentSubscriptions/MaximumSubscribersReachedException.cs create mode 100644 src/Kurrent.Client/PersistentSubscriptions/PersistentSubscription.cs create mode 100644 src/Kurrent.Client/PersistentSubscriptions/PersistentSubscriptionDroppedByServerException.cs create mode 100644 src/Kurrent.Client/PersistentSubscriptions/PersistentSubscriptionExtraStatistic.cs create mode 100644 src/Kurrent.Client/PersistentSubscriptions/PersistentSubscriptionInfo.cs create mode 100644 src/Kurrent.Client/PersistentSubscriptions/PersistentSubscriptionMessage.cs create mode 100644 src/Kurrent.Client/PersistentSubscriptions/PersistentSubscriptionNakEventAction.cs create mode 100644 src/Kurrent.Client/PersistentSubscriptions/PersistentSubscriptionNotFoundException.cs create mode 100644 src/Kurrent.Client/PersistentSubscriptions/PersistentSubscriptionSettings.cs create mode 100644 src/Kurrent.Client/PersistentSubscriptions/SystemConsumerStrategies.cs create mode 100644 src/Kurrent.Client/ProjectionManagement/KurrentProjectionManagementClient.Control.cs create mode 100644 src/Kurrent.Client/ProjectionManagement/KurrentProjectionManagementClient.Create.cs create mode 100644 src/Kurrent.Client/ProjectionManagement/KurrentProjectionManagementClient.State.cs create mode 100644 src/Kurrent.Client/ProjectionManagement/KurrentProjectionManagementClient.Statistics.cs create mode 100644 src/Kurrent.Client/ProjectionManagement/KurrentProjectionManagementClient.Update.cs create mode 100644 src/Kurrent.Client/ProjectionManagement/KurrentProjectionManagementClient.cs create mode 100644 src/Kurrent.Client/ProjectionManagement/KurrentProjectionManagementClientCollectionExtensions.cs create mode 100644 src/Kurrent.Client/ProjectionManagement/ProjectionDetails.cs create mode 100644 src/Kurrent.Client/Streams/ConditionalWriteResult.cs create mode 100644 src/Kurrent.Client/Streams/ConditionalWriteStatus.cs create mode 100644 src/Kurrent.Client/Streams/DeadLine.cs create mode 100644 src/Kurrent.Client/Streams/DeleteResult.cs create mode 100644 src/Kurrent.Client/Streams/Direction.cs create mode 100644 src/Kurrent.Client/Streams/IWriteResult.cs create mode 100644 src/Kurrent.Client/Streams/InvalidTransactionException.cs create mode 100644 src/Kurrent.Client/Streams/KurrentClient.Append.cs create mode 100644 src/Kurrent.Client/Streams/KurrentClient.Delete.cs create mode 100644 src/Kurrent.Client/Streams/KurrentClient.Metadata.cs create mode 100644 src/Kurrent.Client/Streams/KurrentClient.Read.cs create mode 100644 src/Kurrent.Client/Streams/KurrentClient.Subscriptions.cs create mode 100644 src/Kurrent.Client/Streams/KurrentClient.Tombstone.cs create mode 100644 src/Kurrent.Client/Streams/KurrentClient.cs create mode 100644 src/Kurrent.Client/Streams/KurrentClientExtensions.cs create mode 100644 src/Kurrent.Client/Streams/KurrentClientServiceCollectionExtensions.cs create mode 100644 src/Kurrent.Client/Streams/MaximumAppendSizeExceededException.cs create mode 100644 src/Kurrent.Client/Streams/ReadState.cs create mode 100644 src/Kurrent.Client/Streams/StreamAcl.cs create mode 100644 src/Kurrent.Client/Streams/StreamAclJsonConverter.cs create mode 100644 src/Kurrent.Client/Streams/StreamMessage.cs create mode 100644 src/Kurrent.Client/Streams/StreamMetadata.cs create mode 100644 src/Kurrent.Client/Streams/StreamMetadataJsonConverter.cs create mode 100644 src/Kurrent.Client/Streams/StreamMetadataResult.cs create mode 100644 src/Kurrent.Client/Streams/StreamSubscription.cs create mode 100644 src/Kurrent.Client/Streams/Streams/AppendReq.cs create mode 100644 src/Kurrent.Client/Streams/Streams/BatchAppendReq.cs create mode 100644 src/Kurrent.Client/Streams/Streams/BatchAppendResp.cs create mode 100644 src/Kurrent.Client/Streams/Streams/DeleteReq.cs create mode 100644 src/Kurrent.Client/Streams/Streams/ReadReq.cs create mode 100644 src/Kurrent.Client/Streams/Streams/TombstoneReq.cs create mode 100644 src/Kurrent.Client/Streams/SubscriptionFilterOptions.cs create mode 100644 src/Kurrent.Client/Streams/SuccessResult.cs create mode 100644 src/Kurrent.Client/Streams/SystemEventTypes.cs create mode 100644 src/Kurrent.Client/Streams/SystemMetadata.cs create mode 100644 src/Kurrent.Client/Streams/SystemSettings.cs create mode 100644 src/Kurrent.Client/Streams/SystemSettingsJsonConverter.cs create mode 100644 src/Kurrent.Client/Streams/WriteResultExtensions.cs create mode 100644 src/Kurrent.Client/Streams/WrongExpectedVersionResult.cs create mode 100644 src/Kurrent.Client/UserManagement/KurrentUserManagementClient.cs create mode 100644 src/Kurrent.Client/UserManagement/KurrentUserManagementClientCollectionExtensions.cs create mode 100644 src/Kurrent.Client/UserManagement/KurrentUserManagerClientExtensions.cs create mode 100644 src/Kurrent.Client/UserManagement/UserDetails.cs delete mode 100644 test/EventStore.Client.Tests.Common/EventStore.Client.Tests.Common.csproj.DotSettings delete mode 100644 test/EventStore.Client.Tests.Common/Extensions/ShouldThrowAsyncExtensions.cs delete mode 100644 test/EventStore.Client.Tests/EventStoreClientOperationsTests.cs rename test/{EventStore.Client.Tests.Common => Kurrent.Client.Tests.Common}/.env (100%) rename test/{EventStore.Client.Tests.Common => Kurrent.Client.Tests.Common}/ApplicationInfo.cs (98%) rename test/{EventStore.Client.Tests.Common => Kurrent.Client.Tests.Common}/AssertEx.cs (98%) rename test/{EventStore.Client.Tests.Common => Kurrent.Client.Tests.Common}/Certificates.cs (97%) rename test/{EventStore.Client.Tests.Common => Kurrent.Client.Tests.Common}/Extensions/ConfigurationExtensions.cs (90%) rename test/{EventStore.Client.Tests.Common/Extensions/EventStoreClientExtensions.cs => Kurrent.Client.Tests.Common/Extensions/KurrentClientExtensions.cs} (69%) rename test/{EventStore.Client.Tests.Common/Extensions/EventStoreClientWarmupExtensions.cs => Kurrent.Client.Tests.Common/Extensions/KurrentClientWarmupExtensions.cs} (79%) rename test/{EventStore.Client.Tests.Common => Kurrent.Client.Tests.Common}/Extensions/OperatingSystemExtensions.cs (88%) rename test/{EventStore.Client.Tests.Common => Kurrent.Client.Tests.Common}/Extensions/ReadOnlyMemoryExtensions.cs (94%) create mode 100644 test/Kurrent.Client.Tests.Common/Extensions/ShouldThrowAsyncExtensions.cs rename test/{EventStore.Client.Tests.Common => Kurrent.Client.Tests.Common}/Extensions/TaskExtensions.cs (97%) rename test/{EventStore.Client.Tests.Common => Kurrent.Client.Tests.Common}/Extensions/TypeExtensions.cs (98%) rename test/{EventStore.Client.Tests.Common => Kurrent.Client.Tests.Common}/Extensions/WithExtension.cs (97%) rename test/{EventStore.Client.Tests.Common => Kurrent.Client.Tests.Common}/Facts/AnonymousAccess.cs (90%) rename test/{EventStore.Client.Tests.Common => Kurrent.Client.Tests.Common}/Facts/Deprecation.cs (94%) rename test/{EventStore.Client.Tests.Common => Kurrent.Client.Tests.Common}/Facts/Regression.cs (94%) rename test/{EventStore.Client.Tests.Common => Kurrent.Client.Tests.Common}/Fakers/TestUserFaker.cs (96%) rename test/{EventStore.Client.Tests.Common => Kurrent.Client.Tests.Common}/Fixtures/BaseTestNode.cs (97%) rename test/{EventStore.Client.Tests.Common => Kurrent.Client.Tests.Common}/Fixtures/CertificatesManager.cs (98%) rename test/{EventStore.Client.Tests.Common => Kurrent.Client.Tests.Common}/Fixtures/KurrentFixtureOptions.cs (86%) rename test/{EventStore.Client.Tests.Common => Kurrent.Client.Tests.Common}/Fixtures/KurrentPermanentFixture.Helpers.cs (95%) rename test/{EventStore.Client.Tests.Common => Kurrent.Client.Tests.Common}/Fixtures/KurrentPermanentFixture.cs (78%) rename test/{EventStore.Client.Tests.Common => Kurrent.Client.Tests.Common}/Fixtures/KurrentPermanentTestNode.cs (97%) rename test/{EventStore.Client.Tests.Common => Kurrent.Client.Tests.Common}/Fixtures/KurrentTemporaryFixture.Helpers.cs (95%) rename test/{EventStore.Client.Tests.Common => Kurrent.Client.Tests.Common}/Fixtures/KurrentTemporaryFixture.cs (82%) rename test/{EventStore.Client.Tests.Common => Kurrent.Client.Tests.Common}/Fixtures/KurrentTemporaryTestNode.cs (96%) rename test/{EventStore.Client.Tests.Common => Kurrent.Client.Tests.Common}/FluentDocker/FluentDockerBuilderExtensions.cs (98%) rename test/{EventStore.Client.Tests.Common => Kurrent.Client.Tests.Common}/FluentDocker/FluentDockerServiceExtensions.cs (98%) rename test/{EventStore.Client.Tests.Common => Kurrent.Client.Tests.Common}/FluentDocker/TestBypassService.cs (97%) rename test/{EventStore.Client.Tests.Common => Kurrent.Client.Tests.Common}/FluentDocker/TestCompositeService.cs (77%) rename test/{EventStore.Client.Tests.Common => Kurrent.Client.Tests.Common}/FluentDocker/TestContainerService.cs (77%) rename test/{EventStore.Client.Tests.Common => Kurrent.Client.Tests.Common}/FluentDocker/TestService.cs (98%) rename test/{EventStore.Client.Tests.Common => Kurrent.Client.Tests.Common}/GlobalEnvironment.cs (98%) rename test/{EventStore.Client.Tests.Common => Kurrent.Client.Tests.Common}/InterlockedBoolean.cs (100%) rename test/{EventStore.Client.Tests.Common/EventStore.Client.Tests.Common.csproj => Kurrent.Client.Tests.Common/Kurrent.Client.Tests.Common.csproj} (77%) rename test/{EventStore.Client.Tests.Common => Kurrent.Client.Tests.Common}/Logging.cs (98%) rename test/{EventStore.Client.Tests.Common => Kurrent.Client.Tests.Common}/PasswordGenerator.cs (98%) rename test/{EventStore.Client.Tests.Common => Kurrent.Client.Tests.Common}/Shouldly/ShouldThrowAsyncExtensions.cs (80%) rename test/{EventStore.Client.Tests.Common => Kurrent.Client.Tests.Common}/TestCaseGenerator.cs (95%) rename test/{EventStore.Client.Tests.Common => Kurrent.Client.Tests.Common}/TestCredentials.cs (88%) rename test/{EventStore.Client.Tests.Common => Kurrent.Client.Tests.Common}/appsettings.Development.json (100%) rename test/{EventStore.Client.Tests.Common => Kurrent.Client.Tests.Common}/appsettings.json (100%) rename test/{EventStore.Client.Tests.Common => Kurrent.Client.Tests.Common}/docker-compose.certs.yml (100%) rename test/{EventStore.Client.Tests.Common => Kurrent.Client.Tests.Common}/docker-compose.cluster.yml (100%) rename test/{EventStore.Client.Tests.Common => Kurrent.Client.Tests.Common}/docker-compose.node.yml (100%) rename test/{EventStore.Client.Tests.Common => Kurrent.Client.Tests.Common}/docker-compose.yml (100%) rename test/{EventStore.Client.Tests.Common => Kurrent.Client.Tests.Common}/shared.env (100%) rename test/{EventStore.Client.Tests => Kurrent.Client.Tests}/Assertions/ComparableAssertion.cs (99%) rename test/{EventStore.Client.Tests => Kurrent.Client.Tests}/Assertions/EqualityAssertion.cs (98%) rename test/{EventStore.Client.Tests => Kurrent.Client.Tests}/Assertions/NullArgumentAssertion.cs (97%) rename test/{EventStore.Client.Tests => Kurrent.Client.Tests}/Assertions/StringConversionAssertion.cs (97%) rename test/{EventStore.Client.Tests => Kurrent.Client.Tests}/Assertions/ValueObjectAssertion.cs (95%) rename test/{EventStore.Client.Tests => Kurrent.Client.Tests}/AutoScenarioDataAttribute.cs (95%) rename test/{EventStore.Client.Tests => Kurrent.Client.Tests}/ClientCertificatesTests.cs (88%) rename test/{EventStore.Client.Tests => Kurrent.Client.Tests}/ConnectionStringTests.cs (75%) rename test/{EventStore.Client.Tests => Kurrent.Client.Tests}/FromAllTests.cs (96%) rename test/{EventStore.Client.Tests => Kurrent.Client.Tests}/FromStreamTests.cs (96%) rename test/{EventStore.Client.Tests => Kurrent.Client.Tests}/GossipChannelSelectorTests.cs (94%) rename test/{EventStore.Client.Tests => Kurrent.Client.Tests}/GrpcServerCapabilitiesClientTests.cs (97%) rename test/{EventStore.Client.Tests/EventStore.Client.Tests.csproj => Kurrent.Client.Tests/Kurrent.Client.Tests.csproj} (70%) create mode 100644 test/Kurrent.Client.Tests/KurrentClientOperationsTests.cs rename test/{EventStore.Client.Tests => Kurrent.Client.Tests}/NodePreferenceComparerTests.cs (97%) rename test/{EventStore.Client.Tests => Kurrent.Client.Tests}/NodeSelectorTests.cs (94%) rename test/{EventStore.Client.Tests => Kurrent.Client.Tests}/Operations/AuthenticationTests.cs (92%) rename test/{EventStore.Client.Tests => Kurrent.Client.Tests}/Operations/MergeIndexTests.cs (80%) rename test/{EventStore.Client.Tests => Kurrent.Client.Tests}/Operations/ResignNodeTests.cs (82%) rename test/{EventStore.Client.Tests => Kurrent.Client.Tests}/Operations/RestartPersistentSubscriptionsTests.cs (55%) rename test/{EventStore.Client.Tests => Kurrent.Client.Tests}/Operations/ScavengeTests.cs (94%) rename test/{EventStore.Client.Tests => Kurrent.Client.Tests}/Operations/ShutdownNodeAuthenticationTests.cs (79%) rename test/{EventStore.Client.Tests => Kurrent.Client.Tests}/Operations/ShutdownNodeTests.cs (80%) rename test/{EventStore.Client.Tests => Kurrent.Client.Tests}/PersistentSubscriptions/FilterTestCases.cs (94%) rename test/{EventStore.Client.Tests => Kurrent.Client.Tests}/PersistentSubscriptions/SubscribeToAllConnectWithoutReadPermissionsTests.cs (86%) rename test/{EventStore.Client.Tests => Kurrent.Client.Tests}/PersistentSubscriptions/SubscribeToAllFilterTests.cs (96%) rename test/{EventStore.Client.Tests => Kurrent.Client.Tests}/PersistentSubscriptions/SubscribeToAllGetInfoTests.cs (95%) rename test/{EventStore.Client.Tests => Kurrent.Client.Tests}/PersistentSubscriptions/SubscribeToAllListWithIncorrectCredentialsTests.cs (93%) rename test/{EventStore.Client.Tests => Kurrent.Client.Tests}/PersistentSubscriptions/SubscribeToAllNoDefaultCredentialsTests.cs (93%) rename test/{EventStore.Client.Tests => Kurrent.Client.Tests}/PersistentSubscriptions/SubscribeToAllReplayParkedTests.cs (93%) rename test/{EventStore.Client.Tests => Kurrent.Client.Tests}/PersistentSubscriptions/SubscribeToAllResultWithNormalUserCredentialsTests.cs (88%) rename test/{EventStore.Client.Tests => Kurrent.Client.Tests}/PersistentSubscriptions/SubscribeToAllReturnsAllSubscriptions.cs (90%) rename test/{EventStore.Client.Tests => Kurrent.Client.Tests}/PersistentSubscriptions/SubscribeToAllReturnsSubscriptionsToAllStreamTests.cs (88%) rename test/{EventStore.Client.Tests => Kurrent.Client.Tests}/PersistentSubscriptions/SubscribeToAllTests.cs (99%) rename test/{EventStore.Client.Tests => Kurrent.Client.Tests}/PersistentSubscriptions/SubscribeToAllUpdateExistingWithCheckpointTest.cs (94%) rename test/{EventStore.Client.Tests => Kurrent.Client.Tests}/PersistentSubscriptions/SubscribeToAllWithoutPSTests.cs (74%) rename test/{EventStore.Client.Tests => Kurrent.Client.Tests}/PersistentSubscriptions/SubscribeToStreamGetInfoTests.cs (97%) rename test/{EventStore.Client.Tests => Kurrent.Client.Tests}/PersistentSubscriptions/SubscribeToStreamListTests.cs (81%) rename test/{EventStore.Client.Tests => Kurrent.Client.Tests}/PersistentSubscriptions/SubscribeToStreamNoDefaultCredentialsTests.cs (91%) rename test/{EventStore.Client.Tests => Kurrent.Client.Tests}/PersistentSubscriptions/SubscribeToStreamReplayParkedTests.cs (92%) rename test/{EventStore.Client.Tests => Kurrent.Client.Tests}/PersistentSubscriptions/SubscribeToStreamTests.cs (99%) rename test/{EventStore.Client.Tests => Kurrent.Client.Tests}/PositionTests.cs (97%) rename test/{EventStore.Client.Tests/ListProjectionTests.cs => Kurrent.Client.Tests/Projections/ListAllProjectionsTests.cs} (62%) create mode 100644 test/Kurrent.Client.Tests/Projections/ListContinuousProjectionsTests.cs create mode 100644 test/Kurrent.Client.Tests/Projections/ListOneTimeProjectionsTests.cs rename test/{EventStore.Client.Tests => Kurrent.Client.Tests/Projections}/ProjectionManagementTests.cs (86%) create mode 100644 test/Kurrent.Client.Tests/Projections/ResetProjectionTests.cs rename test/{EventStore.Client.Tests => Kurrent.Client.Tests}/RegularFilterExpressionTests.cs (87%) rename test/{EventStore.Client.Tests => Kurrent.Client.Tests}/Security/AllStreamWithNoAclSecurityTests.cs (96%) rename test/{EventStore.Client.Tests => Kurrent.Client.Tests}/Security/DeleteStreamSecurityTests.cs (98%) rename test/{EventStore.Client.Tests => Kurrent.Client.Tests}/Security/MultipleRoleSecurityTests.cs (93%) rename test/{EventStore.Client.Tests => Kurrent.Client.Tests}/Security/OverridenSystemStreamSecurityForAllTests.cs (95%) rename test/{EventStore.Client.Tests => Kurrent.Client.Tests}/Security/OverridenSystemStreamSecurityTests.cs (97%) rename test/{EventStore.Client.Tests => Kurrent.Client.Tests}/Security/OverridenUserStreamSecurityTests.cs (97%) rename test/{EventStore.Client.Tests => Kurrent.Client.Tests}/Security/ReadAllSecurityTests.cs (92%) rename test/{EventStore.Client.Tests => Kurrent.Client.Tests}/Security/ReadStreamMetaSecurityTests.cs (96%) rename test/{EventStore.Client.Tests => Kurrent.Client.Tests}/Security/ReadStreamSecurityTests.cs (98%) rename test/{EventStore.Client.Tests => Kurrent.Client.Tests}/Security/SecurityFixture.cs (99%) rename test/{EventStore.Client.Tests => Kurrent.Client.Tests}/Security/StreamSecurityInheritanceTests.cs (98%) rename test/{EventStore.Client.Tests => Kurrent.Client.Tests}/Security/SubscribeToAllSecurityTests.cs (89%) rename test/{EventStore.Client.Tests => Kurrent.Client.Tests}/Security/SubscribeToStreamSecurityTests.cs (96%) rename test/{EventStore.Client.Tests => Kurrent.Client.Tests}/Security/SystemStreamSecurityTests.cs (98%) rename test/{EventStore.Client.Tests => Kurrent.Client.Tests}/Security/WriteStreamMetaSecurityTests.cs (96%) rename test/{EventStore.Client.Tests => Kurrent.Client.Tests}/Security/WriteStreamSecurityTests.cs (98%) rename test/{EventStore.Client.Tests => Kurrent.Client.Tests}/SharingProviderTests.cs (97%) rename test/{EventStore.Client.Tests => Kurrent.Client.Tests}/StreamPositionTests.cs (98%) rename test/{EventStore.Client.Tests => Kurrent.Client.Tests}/StreamRevisionTests.cs (98%) rename test/{EventStore.Client.Tests => Kurrent.Client.Tests}/StreamStateTests.cs (96%) rename test/{EventStore.Client.Tests => Kurrent.Client.Tests}/Streams/AppendTests.cs (99%) rename test/{EventStore.Client.Tests => Kurrent.Client.Tests}/Streams/Bugs/Obsolete/Issue104.cs (91%) rename test/{EventStore.Client.Tests => Kurrent.Client.Tests}/Streams/Bugs/Obsolete/Issue2544.cs (98%) rename test/{EventStore.Client.Tests => Kurrent.Client.Tests}/Streams/DeleteTests.cs (96%) rename test/{EventStore.Client.Tests => Kurrent.Client.Tests}/Streams/Read/EventBinaryData.cs (95%) rename test/{EventStore.Client.Tests => Kurrent.Client.Tests}/Streams/Read/EventDataComparer.cs (90%) rename test/{EventStore.Client.Tests => Kurrent.Client.Tests}/Streams/Read/ReadAllEventsBackwardTests.cs (97%) rename test/{EventStore.Client.Tests => Kurrent.Client.Tests}/Streams/Read/ReadAllEventsFixture.cs (93%) rename test/{EventStore.Client.Tests => Kurrent.Client.Tests}/Streams/Read/ReadAllEventsForwardTests.cs (98%) rename test/{EventStore.Client.Tests => Kurrent.Client.Tests}/Streams/Read/ReadStreamBackwardTests.cs (98%) rename test/{EventStore.Client.Tests => Kurrent.Client.Tests}/Streams/Read/ReadStreamEventsLinkedToDeletedStreamTests.cs (95%) rename test/{EventStore.Client.Tests => Kurrent.Client.Tests}/Streams/Read/ReadStreamForwardTests.cs (98%) rename test/{EventStore.Client.Tests => Kurrent.Client.Tests}/Streams/Read/ReadStreamWhenHavingMaxCountSetForStreamTests.cs (97%) rename test/{EventStore.Client.Tests => Kurrent.Client.Tests}/Streams/SoftDeleteTests.cs (98%) rename test/{EventStore.Client.Tests => Kurrent.Client.Tests}/Streams/StreamMetadataTests.cs (97%) rename test/{EventStore.Client.Tests => Kurrent.Client.Tests}/Streams/SubscribeToStreamTests.cs (97%) rename test/{EventStore.Client.Tests => Kurrent.Client.Tests}/Streams/SubscriptionFilter.cs (94%) rename test/{EventStore.Client.Tests => Kurrent.Client.Tests}/UuidTests.cs (96%) rename test/{EventStore.Client.Tests => Kurrent.Client.Tests}/ValueObjectTests.cs (92%) rename test/{EventStore.Client.Tests => Kurrent.Client.Tests}/X509CertificatesTests.cs (95%) diff --git a/.github/workflows/base.yml b/.github/workflows/base.yml index e121c28bf..7b52c4e78 100644 --- a/.github/workflows/base.yml +++ b/.github/workflows/base.yml @@ -53,7 +53,7 @@ jobs: uses: actions/setup-dotnet@v3 with: dotnet-version: | - 8.0.x + 9.0.x - name: Run Tests shell: bash env: @@ -64,7 +64,7 @@ jobs: dotnet test --configuration ${{ matrix.configuration }} --blame \ --logger:"GitHubActions;report-warnings=false" --logger:"console;verbosity=normal" \ --framework ${{ matrix.framework }} \ - test/EventStore.Client.Tests + test/Kurrent.Client.Tests # run: | # sudo ./gencert.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c1ab57e76..1f408ae3c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,7 +14,8 @@ jobs: strategy: fail-fast: false matrix: - docker-tag: [ ci, lts, previous-lts ] +# docker-tag: [ ci, lts, previous-lts ] + docker-tag: [ ci ] # test: [ Streams, PersistentSubscriptions, Operations, UserManagement, ProjectionManagement ] name: Test CE (${{ matrix.docker-tag }}) with: diff --git a/EventStore.Client.sln b/Kurrent.Client.sln similarity index 66% rename from EventStore.Client.sln rename to Kurrent.Client.sln index 4b4791ec9..63cdbc278 100644 --- a/EventStore.Client.sln +++ b/Kurrent.Client.sln @@ -9,12 +9,14 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventStore.Client", "src\Ev EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{C51F2C69-45A9-4D0D-A708-4FC319D5D340}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventStore.Client.Tests", "test\EventStore.Client.Tests\EventStore.Client.Tests.csproj", "{FC829F1B-43AD-4C96-9002-23D04BBA3AF3}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventStore.Client.Tests.Common", "test\EventStore.Client.Tests.Common\EventStore.Client.Tests.Common.csproj", "{E326832D-DE52-4DE4-9E54-C800908B75F3}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kurrent.Client.Tests", "test\Kurrent.Client.Tests\Kurrent.Client.Tests.csproj", "{FC829F1B-43AD-4C96-9002-23D04BBA3AF3}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventStore.Client.Extensions.OpenTelemetry", "src\EventStore.Client.Extensions.OpenTelemetry\EventStore.Client.Extensions.OpenTelemetry.csproj", "{F6A7B391-36F1-4838-AD08-E0EE0F2FE57E}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kurrent.Client", "src\Kurrent.Client\Kurrent.Client.csproj", "{762EECAA-122E-4B0C-BC50-5AA4F72CA4E0}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kurrent.Client.Tests.Common", "test\Kurrent.Client.Tests.Common\Kurrent.Client.Tests.Common.csproj", "{47BF715B-A0BF-4044-B335-717E56422550}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|x64 = Debug|x64 @@ -32,19 +34,24 @@ Global {FC829F1B-43AD-4C96-9002-23D04BBA3AF3}.Debug|x64.Build.0 = Debug|Any CPU {FC829F1B-43AD-4C96-9002-23D04BBA3AF3}.Release|x64.ActiveCfg = Release|Any CPU {FC829F1B-43AD-4C96-9002-23D04BBA3AF3}.Release|x64.Build.0 = Release|Any CPU - {E326832D-DE52-4DE4-9E54-C800908B75F3}.Debug|x64.ActiveCfg = Debug|Any CPU - {E326832D-DE52-4DE4-9E54-C800908B75F3}.Debug|x64.Build.0 = Debug|Any CPU - {E326832D-DE52-4DE4-9E54-C800908B75F3}.Release|x64.ActiveCfg = Release|Any CPU - {E326832D-DE52-4DE4-9E54-C800908B75F3}.Release|x64.Build.0 = Release|Any CPU {F6A7B391-36F1-4838-AD08-E0EE0F2FE57E}.Debug|x64.ActiveCfg = Debug|Any CPU {F6A7B391-36F1-4838-AD08-E0EE0F2FE57E}.Debug|x64.Build.0 = Debug|Any CPU {F6A7B391-36F1-4838-AD08-E0EE0F2FE57E}.Release|x64.ActiveCfg = Release|Any CPU {F6A7B391-36F1-4838-AD08-E0EE0F2FE57E}.Release|x64.Build.0 = Release|Any CPU + {762EECAA-122E-4B0C-BC50-5AA4F72CA4E0}.Debug|x64.ActiveCfg = Debug|Any CPU + {762EECAA-122E-4B0C-BC50-5AA4F72CA4E0}.Debug|x64.Build.0 = Debug|Any CPU + {762EECAA-122E-4B0C-BC50-5AA4F72CA4E0}.Release|x64.ActiveCfg = Release|Any CPU + {762EECAA-122E-4B0C-BC50-5AA4F72CA4E0}.Release|x64.Build.0 = Release|Any CPU + {47BF715B-A0BF-4044-B335-717E56422550}.Debug|x64.ActiveCfg = Debug|Any CPU + {47BF715B-A0BF-4044-B335-717E56422550}.Debug|x64.Build.0 = Debug|Any CPU + {47BF715B-A0BF-4044-B335-717E56422550}.Release|x64.ActiveCfg = Release|Any CPU + {47BF715B-A0BF-4044-B335-717E56422550}.Release|x64.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {8853D875-4A8E-450B-A1BE-9CEF8BEDABC3} = {EA59C1CB-16DA-4F68-AF8A-642A969B4CF8} {FC829F1B-43AD-4C96-9002-23D04BBA3AF3} = {C51F2C69-45A9-4D0D-A708-4FC319D5D340} - {E326832D-DE52-4DE4-9E54-C800908B75F3} = {C51F2C69-45A9-4D0D-A708-4FC319D5D340} {F6A7B391-36F1-4838-AD08-E0EE0F2FE57E} = {EA59C1CB-16DA-4F68-AF8A-642A969B4CF8} + {762EECAA-122E-4B0C-BC50-5AA4F72CA4E0} = {EA59C1CB-16DA-4F68-AF8A-642A969B4CF8} + {47BF715B-A0BF-4044-B335-717E56422550} = {C51F2C69-45A9-4D0D-A708-4FC319D5D340} EndGlobalSection EndGlobal diff --git a/EventStore.Client.sln.DotSettings b/Kurrent.Client.sln.DotSettings similarity index 100% rename from EventStore.Client.sln.DotSettings rename to Kurrent.Client.sln.DotSettings diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 9f5159807..38e546298 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -3,8 +3,8 @@ true - EventStore.Client - EventStore.Client + Kurrent.Client + Kurrent.Client @@ -12,8 +12,8 @@ LICENSE.md https://kurrent.io false - https://eventstore.com/blog/ - kurrent eventstore client grpc + https://kurrent.io/blog/ + kurrent client grpc Kurrent Ltd Copyright 2012-$([System.DateTime]::Today.Year.ToString()) Kurrent Ltd v diff --git a/src/Kurrent.Client/Core/Certificates/X509Certificates.cs b/src/Kurrent.Client/Core/Certificates/X509Certificates.cs new file mode 100644 index 000000000..3fe1006f5 --- /dev/null +++ b/src/Kurrent.Client/Core/Certificates/X509Certificates.cs @@ -0,0 +1,114 @@ +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; + +#if NET48 +using Org.BouncyCastle.Crypto; +using Org.BouncyCastle.Crypto.Parameters; +using Org.BouncyCastle.OpenSsl; +using Org.BouncyCastle.Security; +#endif + +namespace EventStore.Client; + +static class X509Certificates { + // TODO SS: Use .NET 8 X509Certificate2.CreateFromPemFile(certPemFilePath, keyPemFilePath) once the Windows32Exception issue is resolved + public static X509Certificate2 CreateFromPemFile(string certPemFilePath, string keyPemFilePath) { + try { +#if NET9_0_OR_GREATER + using var publicCert = X509CertificateLoader.LoadCertificateFromFile(certPemFilePath); +#else + using var publicCert = new X509Certificate2(certPemFilePath); +#endif + using var privateKey = RSA.Create().ImportPrivateKeyFromFile(keyPemFilePath); + using var certificate = publicCert.CopyWithPrivateKey(privateKey); + +#if NET48 + return new(certificate.Export(X509ContentType.Pfx)); +#else + return X509Certificate2.CreateFromPemFile(certPemFilePath, keyPemFilePath); +#endif + } catch (Exception ex) { + throw new CryptographicException($"Failed to load private key: {ex.Message}"); + } + + // Notes: + // using X509Certificate2.CreateFromPemFile(certPemFilePath, keyPemFilePath) would be the ideal choice here, + // but it's currently causing a Win32Exception specifically on Windows. Alternative implementation is used until the issue is resolved. + // + // Error: The SSL connection could not be established, see inner exception. AuthenticationException: Authentication failed because the platform + // does not support ephemeral keys. Win32Exception: No credentials are available in the security package + // + // public static X509Certificate2 CreateFromPemFile(string certPemFilePath, string keyPemFilePath) => + // X509Certificate2.CreateFromPemFile(certPemFilePath, keyPemFilePath); + } +} + +public static class RsaExtensions { +#if NET48 + public static RSA ImportPrivateKeyFromFile(this RSA rsa, string privateKeyPath) { + var (content, label) = LoadPemKeyFile(privateKeyPath); + + using var reader = new PemReader(new StringReader(string.Join(Environment.NewLine, content))); + + var keyParameters = reader.ReadObject() switch { + RsaPrivateCrtKeyParameters parameters => parameters, + AsymmetricCipherKeyPair keyPair => keyPair.Private as RsaPrivateCrtKeyParameters, + _ => throw new NotSupportedException($"Invalid private key format: {label}") + }; + + rsa.ImportParameters(DotNetUtilities.ToRSAParameters(keyParameters)); + + return rsa; + } +#else + public static RSA ImportPrivateKeyFromFile(this RSA rsa, string privateKeyPath) { + var (content, label) = LoadPemKeyFile(privateKeyPath); + + var privateKey = string.Join(string.Empty, content[1..^1]); + var privateKeyBytes = Convert.FromBase64String(privateKey); + + if (label == RsaPemLabels.Pkcs8PrivateKey) + rsa.ImportPkcs8PrivateKey(privateKeyBytes, out _); + else if (label == RsaPemLabels.RSAPrivateKey) + rsa.ImportRSAPrivateKey(privateKeyBytes, out _); + + return rsa; + } +#endif + + static (string[] Content, string Label) LoadPemKeyFile(string privateKeyPath) { + var content = File.ReadAllLines(privateKeyPath); + var label = RsaPemLabels.ParseKeyLabel(content[0]); + + if (RsaPemLabels.IsEncryptedPrivateKey(label)) + throw new NotSupportedException("Encrypted private keys are not supported"); + + return (content, label); + } +} + +static class RsaPemLabels { + public const string RSAPrivateKey = "RSA PRIVATE KEY"; + public const string Pkcs8PrivateKey = "PRIVATE KEY"; + public const string EncryptedPkcs8PrivateKey = "ENCRYPTED PRIVATE KEY"; + + public static readonly string[] PrivateKeyLabels = [RSAPrivateKey, Pkcs8PrivateKey, EncryptedPkcs8PrivateKey]; + + public static bool IsPrivateKey(string label) => Array.IndexOf(PrivateKeyLabels, label) != -1; + + public static bool IsEncryptedPrivateKey(string label) => label == EncryptedPkcs8PrivateKey; + + const string LabelPrefix = "-----BEGIN "; + const string LabelSuffix = "-----"; + + public static string ParseKeyLabel(string pemFileHeader) { + var label = pemFileHeader.Replace(LabelPrefix, string.Empty).Replace(LabelSuffix, string.Empty); + + if (!IsPrivateKey(label)) + throw new CryptographicException($"Unknown private key label: {label}"); + + return label; + } +} diff --git a/src/Kurrent.Client/Core/ChannelBaseExtensions.cs b/src/Kurrent.Client/Core/ChannelBaseExtensions.cs new file mode 100644 index 000000000..9c44addef --- /dev/null +++ b/src/Kurrent.Client/Core/ChannelBaseExtensions.cs @@ -0,0 +1,10 @@ +using Grpc.Core; + +namespace EventStore.Client; + +static class ChannelBaseExtensions { + public static async ValueTask DisposeAsync(this ChannelBase channel) { + await channel.ShutdownAsync().ConfigureAwait(false); + (channel as IDisposable)?.Dispose(); + } +} diff --git a/src/Kurrent.Client/Core/ChannelCache.cs b/src/Kurrent.Client/Core/ChannelCache.cs new file mode 100644 index 000000000..09f7c2b86 --- /dev/null +++ b/src/Kurrent.Client/Core/ChannelCache.cs @@ -0,0 +1,146 @@ +using System.Net; +using TChannel = Grpc.Net.Client.GrpcChannel; + +namespace EventStore.Client { + // Maintains Channels keyed by DnsEndPoint so the channels can be reused. + // Deals with the disposal difference between grpc.net and grpc.core + // Thread safe. + internal class ChannelCache : + IAsyncDisposable { + + private readonly KurrentClientSettings _settings; + private readonly Random _random; + private readonly Dictionary _channels; + private readonly object _lock = new(); + private bool _disposed; + + public ChannelCache(KurrentClientSettings settings) { + _settings = settings; + _random = new Random(0); + _channels = new Dictionary( + DnsEndPointEqualityComparer.Instance); + } + + public TChannel GetChannelInfo(DnsEndPoint endPoint) { + lock (_lock) { + ThrowIfDisposed(); + + if (!_channels.TryGetValue(endPoint, out var channel)) { + channel = ChannelFactory.CreateChannel( + settings: _settings, + endPoint: endPoint); + _channels[endPoint] = channel; + } + + return channel; + } + } + + public KeyValuePair[] GetRandomOrderSnapshot() { + lock (_lock) { + ThrowIfDisposed(); + + return _channels + .OrderBy(_ => _random.Next()) + .ToArray(); + } + } + + // Update the cache to contain channels for exactly these endpoints + public void UpdateCache(IEnumerable endPoints) { + lock (_lock) { + ThrowIfDisposed(); + + // remove + var endPointsToDiscard = _channels.Keys + .Except(endPoints, DnsEndPointEqualityComparer.Instance) + .ToArray(); + + var channelsToDispose = new List(endPointsToDiscard.Length); + + foreach (var endPoint in endPointsToDiscard) { + if (!_channels.TryGetValue(endPoint, out var channel)) + continue; + + _channels.Remove(endPoint); + channelsToDispose.Add(channel); + } + + _ = DisposeChannelsAsync(channelsToDispose); + + // add + foreach (var endPoint in endPoints) { + GetChannelInfo(endPoint); + } + } + } + + public void Dispose() { + lock (_lock) { + if (_disposed) + return; + + _disposed = true; + + foreach (var channel in _channels.Values) { + channel.Dispose(); + } + + _channels.Clear(); + } + } + + public async ValueTask DisposeAsync() { + var channelsToDispose = Array.Empty(); + + lock (_lock) { + if (_disposed) + return; + _disposed = true; + + channelsToDispose = _channels.Values.ToArray(); + _channels.Clear(); + } + + await DisposeChannelsAsync(channelsToDispose).ConfigureAwait(false); + } + + private void ThrowIfDisposed() { + lock (_lock) { + if (_disposed) { + throw new ObjectDisposedException(GetType().ToString()); + } + } + } + + private static async Task DisposeChannelsAsync(IEnumerable channels) { + foreach (var channel in channels) + await channel.DisposeAsync().ConfigureAwait(false); + } + + private class DnsEndPointEqualityComparer : IEqualityComparer { + public static readonly DnsEndPointEqualityComparer Instance = new(); + + public bool Equals(DnsEndPoint? x, DnsEndPoint? y) { + if (ReferenceEquals(x, y)) + return true; + if (x is null) + return false; + if (y is null) + return false; + if (x.GetType() != y.GetType()) + return false; + return + string.Equals(x.Host, y.Host, StringComparison.OrdinalIgnoreCase) && + x.Port == y.Port; + } + + public int GetHashCode(DnsEndPoint obj) { + unchecked { + return (StringComparer.OrdinalIgnoreCase.GetHashCode(obj.Host) * 397) ^ + obj.Port; + } + } + } + } +} diff --git a/src/Kurrent.Client/Core/ChannelFactory.cs b/src/Kurrent.Client/Core/ChannelFactory.cs new file mode 100644 index 000000000..c63605bb4 --- /dev/null +++ b/src/Kurrent.Client/Core/ChannelFactory.cs @@ -0,0 +1,106 @@ +using System.Net.Http; +using System.Security.Cryptography.X509Certificates; +using Grpc.Net.Client; +using EndPoint = System.Net.EndPoint; +using TChannel = Grpc.Net.Client.GrpcChannel; + +namespace EventStore.Client { + + internal static class ChannelFactory { + private const int MaxReceiveMessageLength = 17 * 1024 * 1024; + + public static TChannel CreateChannel(KurrentClientSettings settings, EndPoint endPoint) { + var address = endPoint.ToUri(!settings.ConnectivitySettings.Insecure); + + if (settings.ConnectivitySettings.Insecure) { + //this must be switched on before creation of the HttpMessageHandler + AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true); + } + + return TChannel.ForAddress( + address, + new GrpcChannelOptions { +#if NET48 + HttpHandler = CreateHandler(settings), +#else + HttpClient = new HttpClient(CreateHandler(settings), true) { + Timeout = System.Threading.Timeout.InfiniteTimeSpan, + DefaultRequestVersion = new Version(2, 0) + }, +#endif + LoggerFactory = settings.LoggerFactory, + Credentials = settings.ChannelCredentials, + DisposeHttpClient = true, + MaxReceiveMessageSize = MaxReceiveMessageLength + } + ); + + +#if NET48 + static HttpMessageHandler CreateHandler(KurrentClientSettings settings) { + if (settings.CreateHttpMessageHandler is not null) + return settings.CreateHttpMessageHandler.Invoke(); + + var handler = new WinHttpHandler { + TcpKeepAliveEnabled = true, + TcpKeepAliveTime = settings.ConnectivitySettings.KeepAliveTimeout, + TcpKeepAliveInterval = settings.ConnectivitySettings.KeepAliveInterval, + EnableMultipleHttp2Connections = true + }; + + if (settings.ConnectivitySettings.Insecure) return handler; + + if (settings.ConnectivitySettings.ClientCertificate is not null) + handler.ClientCertificates.Add(settings.ConnectivitySettings.ClientCertificate); + + handler.ServerCertificateValidationCallback = settings.ConnectivitySettings.TlsVerifyCert switch { + false => delegate { return true; }, + true when settings.ConnectivitySettings.TlsCaFile is not null => (sender, certificate, chain, errors) => { + if (chain is null) return false; + + chain.ChainPolicy.ExtraStore.Add(settings.ConnectivitySettings.TlsCaFile); + return chain.Build(certificate); + }, + _ => null + }; + + return handler; + } +#else + static HttpMessageHandler CreateHandler(KurrentClientSettings settings) { + if (settings.CreateHttpMessageHandler is not null) + return settings.CreateHttpMessageHandler.Invoke(); + + var handler = new SocketsHttpHandler { + KeepAlivePingDelay = settings.ConnectivitySettings.KeepAliveInterval, + KeepAlivePingTimeout = settings.ConnectivitySettings.KeepAliveTimeout, + EnableMultipleHttp2Connections = true + }; + + if (settings.ConnectivitySettings.Insecure) + return handler; + + if (settings.ConnectivitySettings.ClientCertificate is not null) { + handler.SslOptions.ClientCertificates = new X509CertificateCollection { + settings.ConnectivitySettings.ClientCertificate + }; + } + + handler.SslOptions.RemoteCertificateValidationCallback = settings.ConnectivitySettings.TlsVerifyCert switch { + false => delegate { return true; }, + true when settings.ConnectivitySettings.TlsCaFile is not null => (sender, certificate, chain, errors) => { + if (certificate is not X509Certificate2 peerCertificate || chain is null) return false; + + chain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust; + chain.ChainPolicy.CustomTrustStore.Add(settings.ConnectivitySettings.TlsCaFile); + return chain.Build(peerCertificate); + }, + _ => null + }; + + return handler; + } +#endif + } + } +} diff --git a/src/Kurrent.Client/Core/ChannelInfo.cs b/src/Kurrent.Client/Core/ChannelInfo.cs new file mode 100644 index 000000000..87c80b644 --- /dev/null +++ b/src/Kurrent.Client/Core/ChannelInfo.cs @@ -0,0 +1,9 @@ +using Grpc.Core; + +namespace EventStore.Client { +#pragma warning disable 1591 + public record ChannelInfo( + ChannelBase Channel, + ServerCapabilities ServerCapabilities, + CallInvoker CallInvoker); +} diff --git a/src/Kurrent.Client/Core/ChannelSelector.cs b/src/Kurrent.Client/Core/ChannelSelector.cs new file mode 100644 index 000000000..af0fa3031 --- /dev/null +++ b/src/Kurrent.Client/Core/ChannelSelector.cs @@ -0,0 +1,24 @@ +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using Grpc.Core; + +namespace EventStore.Client { + internal class ChannelSelector : IChannelSelector { + private readonly IChannelSelector _inner; + + public ChannelSelector( + KurrentClientSettings settings, + ChannelCache channelCache) { + _inner = settings.ConnectivitySettings.IsSingleNode + ? new SingleNodeChannelSelector(settings, channelCache) + : new GossipChannelSelector(settings, channelCache, new GrpcGossipClient(settings)); + } + + public Task SelectChannelAsync(CancellationToken cancellationToken) => + _inner.SelectChannelAsync(cancellationToken); + + public ChannelBase SelectChannel(DnsEndPoint endPoint) => + _inner.SelectChannel(endPoint); + } +} diff --git a/src/Kurrent.Client/Core/ClusterMessage.cs b/src/Kurrent.Client/Core/ClusterMessage.cs new file mode 100644 index 000000000..64701781e --- /dev/null +++ b/src/Kurrent.Client/Core/ClusterMessage.cs @@ -0,0 +1,28 @@ +using System.Net; + +namespace EventStore.Client { + internal static class ClusterMessages { + public record ClusterInfo(MemberInfo[] Members); + + public record MemberInfo(Uuid InstanceId, VNodeState State, bool IsAlive, DnsEndPoint EndPoint); + + public enum VNodeState { + Initializing = 0, + DiscoverLeader = 1, + Unknown = 2, + PreReplica = 3, + CatchingUp = 4, + Clone = 5, + Follower = 6, + PreLeader = 7, + Leader = 8, + Manager = 9, + ShuttingDown = 10, + Shutdown = 11, + ReadOnlyLeaderless = 12, + PreReadOnlyReplica = 13, + ReadOnlyReplica = 14, + ResigningLeader = 15 + } + } +} diff --git a/src/Kurrent.Client/Core/Common/AsyncStreamReaderExtensions.cs b/src/Kurrent.Client/Core/Common/AsyncStreamReaderExtensions.cs new file mode 100644 index 000000000..98f9de54d --- /dev/null +++ b/src/Kurrent.Client/Core/Common/AsyncStreamReaderExtensions.cs @@ -0,0 +1,29 @@ +using System.Threading.Channels; +using System.Runtime.CompilerServices; +using Grpc.Core; + +namespace EventStore.Client; + +static class AsyncStreamReaderExtensions { + public static async IAsyncEnumerable ReadAllAsync( + this IAsyncStreamReader reader, + [EnumeratorCancellation] + CancellationToken cancellationToken = default + ) { + while (await reader.MoveNext(cancellationToken).ConfigureAwait(false)) + yield return reader.Current; + } + + public static async IAsyncEnumerable ReadAllAsync(this ChannelReader reader, [EnumeratorCancellation] CancellationToken cancellationToken = default) { +#if NET + await foreach (var item in reader.ReadAllAsync(cancellationToken)) + yield return item; +#else + while (await reader.WaitToReadAsync(cancellationToken).ConfigureAwait(false)) { + while (reader.TryRead(out T? item)) { + yield return item; + } + } +#endif + } +} diff --git a/src/Kurrent.Client/Core/Common/Constants.cs b/src/Kurrent.Client/Core/Common/Constants.cs new file mode 100644 index 000000000..2ed9d7c82 --- /dev/null +++ b/src/Kurrent.Client/Core/Common/Constants.cs @@ -0,0 +1,62 @@ +namespace EventStore.Client; + +static class Constants { + public static class Exceptions { + public const string ExceptionKey = "exception"; + + public const string AccessDenied = "access-denied"; + public const string InvalidTransaction = "invalid-transaction"; + public const string StreamDeleted = "stream-deleted"; + public const string WrongExpectedVersion = "wrong-expected-version"; + public const string StreamNotFound = "stream-not-found"; + public const string MaximumAppendSizeExceeded = "maximum-append-size-exceeded"; + public const string MissingRequiredMetadataProperty = "missing-required-metadata-property"; + public const string NotLeader = "not-leader"; + + public const string PersistentSubscriptionFailed = "persistent-subscription-failed"; + public const string PersistentSubscriptionDoesNotExist = "persistent-subscription-does-not-exist"; + public const string PersistentSubscriptionExists = "persistent-subscription-exists"; + public const string MaximumSubscribersReached = "maximum-subscribers-reached"; + public const string PersistentSubscriptionDropped = "persistent-subscription-dropped"; + + public const string UserNotFound = "user-not-found"; + public const string UserConflict = "user-conflict"; + + public const string ScavengeNotFound = "scavenge-not-found"; + + public const string ExpectedVersion = "expected-version"; + public const string ActualVersion = "actual-version"; + public const string StreamName = "stream-name"; + public const string GroupName = "group-name"; + public const string Reason = "reason"; + public const string MaximumAppendSize = "maximum-append-size"; + public const string RequiredMetadataProperties = "required-metadata-properties"; + public const string ScavengeId = "scavenge-id"; + public const string LeaderEndpointHost = "leader-endpoint-host"; + public const string LeaderEndpointPort = "leader-endpoint-port"; + + public const string LoginName = "login-name"; + } + + public static class Metadata { + public const string Type = "type"; + public const string Created = "created"; + public const string ContentType = "content-type"; + + public static readonly string[] RequiredMetadata = [Type, ContentType]; + + public static class ContentTypes { + public const string ApplicationJson = "application/json"; + public const string ApplicationOctetStream = "application/octet-stream"; + } + } + + public static class Headers { + public const string Authorization = "authorization"; + public const string BasicScheme = "Basic"; + public const string BearerScheme = "Bearer"; + + public const string ConnectionName = "connection-name"; + public const string RequiresLeader = "requires-leader"; + } +} diff --git a/src/Kurrent.Client/Core/Common/Diagnostics/ActivitySourceExtensions.cs b/src/Kurrent.Client/Core/Common/Diagnostics/ActivitySourceExtensions.cs new file mode 100644 index 000000000..ab914d8c9 --- /dev/null +++ b/src/Kurrent.Client/Core/Common/Diagnostics/ActivitySourceExtensions.cs @@ -0,0 +1,85 @@ +// ReSharper disable ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract + +using System.Diagnostics; +using Kurrent.Diagnostics; +using Kurrent.Diagnostics.Telemetry; +using Kurrent.Diagnostics.Tracing; + +namespace EventStore.Client.Diagnostics; + +static class ActivitySourceExtensions { + public static async ValueTask TraceClientOperation( + this ActivitySource source, + Func> tracedOperation, + string operationName, + ActivityTagsCollection? tags = null + ) { + using var activity = StartActivity(source, operationName, ActivityKind.Client, tags, Activity.Current?.Context); + + try { + var res = await tracedOperation().ConfigureAwait(false); + activity?.StatusOk(); + return res; + } catch (Exception ex) { + activity?.StatusError(ex); + throw; + } + } + + public static void TraceSubscriptionEvent( + this ActivitySource source, + string? subscriptionId, + ResolvedEvent resolvedEvent, + ChannelInfo channelInfo, + KurrentClientSettings settings, + UserCredentials? userCredentials + ) { + if (source.HasNoActiveListeners() || resolvedEvent.Event is null) + return; + + var parentContext = resolvedEvent.Event.Metadata.ExtractPropagationContext(); + + if (parentContext == default(ActivityContext)) return; + + var tags = new ActivityTagsCollection() + .WithRequiredTag(TelemetryTags.Kurrent.Stream, resolvedEvent.OriginalEvent.EventStreamId) + .WithOptionalTag(TelemetryTags.Kurrent.SubscriptionId, subscriptionId) + .WithRequiredTag(TelemetryTags.Kurrent.EventId, resolvedEvent.OriginalEvent.EventId.ToString()) + .WithRequiredTag(TelemetryTags.Kurrent.EventType, resolvedEvent.OriginalEvent.EventType) + // Ensure consistent server.address attribute when connecting to cluster via dns discovery + .WithGrpcChannelServerTags(channelInfo) + .WithClientSettingsServerTags(settings) + .WithOptionalTag( + TelemetryTags.Database.User, + userCredentials?.Username ?? settings.DefaultCredentials?.Username + ); + + StartActivity(source, TracingConstants.Operations.Subscribe, ActivityKind.Consumer, tags, parentContext) + ?.Dispose(); + } + + static Activity? StartActivity( + this ActivitySource source, + string operationName, ActivityKind activityKind, ActivityTagsCollection? tags = null, + ActivityContext? parentContext = null + ) { + if (source.HasNoActiveListeners()) + return null; + + (tags ??= new ActivityTagsCollection()) + .WithRequiredTag(TelemetryTags.Database.System, "kurrent") + .WithRequiredTag(TelemetryTags.Database.Operation, operationName); + + return source + .CreateActivity( + operationName, + activityKind, + parentContext ?? default, + tags, + idFormat: ActivityIdFormat.W3C + ) + ?.Start(); + } + + static bool HasNoActiveListeners(this ActivitySource source) => !source.HasListeners(); +} diff --git a/src/Kurrent.Client/Core/Common/Diagnostics/ActivityTagsCollectionExtensions.cs b/src/Kurrent.Client/Core/Common/Diagnostics/ActivityTagsCollectionExtensions.cs new file mode 100644 index 000000000..fd6ad661a --- /dev/null +++ b/src/Kurrent.Client/Core/Common/Diagnostics/ActivityTagsCollectionExtensions.cs @@ -0,0 +1,32 @@ +using System.Diagnostics; +using System.Runtime.CompilerServices; +using Kurrent.Diagnostics; +using Kurrent.Diagnostics.Telemetry; + +namespace EventStore.Client.Diagnostics; + +static class ActivityTagsCollectionExtensions { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ActivityTagsCollection WithGrpcChannelServerTags(this ActivityTagsCollection tags, ChannelInfo? channelInfo) { + if (channelInfo is null) + return tags; + + var authorityParts = channelInfo.Channel.Target.Split(':'); + + return tags + .WithRequiredTag(TelemetryTags.Server.Address, authorityParts[0]) + .WithRequiredTag(TelemetryTags.Server.Port, int.Parse(authorityParts[1])); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ActivityTagsCollection WithClientSettingsServerTags(this ActivityTagsCollection source, KurrentClientSettings settings) { + if (settings.ConnectivitySettings.DnsGossipSeeds?.Length != 1) + return source; + + var gossipSeed = settings.ConnectivitySettings.DnsGossipSeeds[0]; + + return source + .WithRequiredTag(TelemetryTags.Server.Address, gossipSeed.Host) + .WithRequiredTag(TelemetryTags.Server.Port, gossipSeed.Port); + } +} diff --git a/src/Kurrent.Client/Core/Common/Diagnostics/Core/ActivityExtensions.cs b/src/Kurrent.Client/Core/Common/Diagnostics/Core/ActivityExtensions.cs new file mode 100644 index 000000000..4b6404f60 --- /dev/null +++ b/src/Kurrent.Client/Core/Common/Diagnostics/Core/ActivityExtensions.cs @@ -0,0 +1,52 @@ +// ReSharper disable CheckNamespace + +using System.Diagnostics; +using System.Runtime.CompilerServices; +using Kurrent.Diagnostics.Telemetry; +using Kurrent.Diagnostics.Tracing; + +namespace Kurrent.Diagnostics; + +static class ActivityExtensions { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TracingMetadata GetTracingMetadata(this Activity activity) => + new(activity.TraceId.ToString(), activity.SpanId.ToString()); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Activity StatusOk(this Activity activity, string? description = null) => + activity.SetActivityStatus(ActivityStatus.Ok(description)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Activity StatusError(this Activity activity, Exception exception) => + activity.SetActivityStatus(ActivityStatus.Error(exception)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static Activity RecordException(this Activity activity, Exception? exception) { + if (exception is null) return activity; + + var ex = exception is AggregateException aex ? aex.Flatten() : exception; + + var tags = new ActivityTagsCollection { + { TelemetryTags.Exception.Type, ex.GetType().FullName }, + { TelemetryTags.Exception.Stacktrace, ex.ToInvariantString() } + }; + + if (!string.IsNullOrWhiteSpace(exception.Message)) + tags.Add(TelemetryTags.Exception.Message, ex.Message); + + activity.AddEvent(new ActivityEvent(TelemetryTags.Exception.EventName, default, tags)); + + return activity; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static Activity SetActivityStatus(this Activity activity, ActivityStatus status) { + var statusCode = ActivityStatusCodeHelper.GetTagValueForStatusCode(status.StatusCode); + + activity.SetStatus(status.StatusCode, status.Description); + activity.SetTag(TelemetryTags.Otel.StatusCode, statusCode); + activity.SetTag(TelemetryTags.Otel.StatusDescription, status.Description); + + return activity.IsAllDataRequested ? activity.RecordException(status.Exception) : activity; + } +} diff --git a/src/Kurrent.Client/Core/Common/Diagnostics/Core/ActivityStatus.cs b/src/Kurrent.Client/Core/Common/Diagnostics/Core/ActivityStatus.cs new file mode 100644 index 000000000..bab790b6c --- /dev/null +++ b/src/Kurrent.Client/Core/Common/Diagnostics/Core/ActivityStatus.cs @@ -0,0 +1,13 @@ +// ReSharper disable CheckNamespace + +using System.Diagnostics; + +namespace Kurrent.Diagnostics; + +record ActivityStatus(ActivityStatusCode StatusCode, string? Description, Exception? Exception) { + public static ActivityStatus Ok(string? description = null) => + new(ActivityStatusCode.Ok, description, null); + + public static ActivityStatus Error(Exception exception, string? description = null) => + new(ActivityStatusCode.Error, description ?? exception.Message, exception); +} diff --git a/src/Kurrent.Client/Core/Common/Diagnostics/Core/ActivityStatusCodeHelper.cs b/src/Kurrent.Client/Core/Common/Diagnostics/Core/ActivityStatusCodeHelper.cs new file mode 100644 index 000000000..592501d11 --- /dev/null +++ b/src/Kurrent.Client/Core/Common/Diagnostics/Core/ActivityStatusCodeHelper.cs @@ -0,0 +1,24 @@ +// ReSharper disable CheckNamespace + +using System.Diagnostics; +using System.Runtime.CompilerServices; + +using static System.Diagnostics.ActivityStatusCode; +using static System.StringComparison; + +namespace Kurrent.Diagnostics; + +static class ActivityStatusCodeHelper { + public const string UnsetStatusCodeTagValue = "UNSET"; + public const string OkStatusCodeTagValue = "OK"; + public const string ErrorStatusCodeTagValue = "ERROR"; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static string? GetTagValueForStatusCode(ActivityStatusCode statusCode) => + statusCode switch { + Unset => UnsetStatusCodeTagValue, + Error => ErrorStatusCodeTagValue, + Ok => OkStatusCodeTagValue, + _ => null + }; +} diff --git a/src/Kurrent.Client/Core/Common/Diagnostics/Core/ActivityTagsCollectionExtensions.cs b/src/Kurrent.Client/Core/Common/Diagnostics/Core/ActivityTagsCollectionExtensions.cs new file mode 100644 index 000000000..2c8c8b291 --- /dev/null +++ b/src/Kurrent.Client/Core/Common/Diagnostics/Core/ActivityTagsCollectionExtensions.cs @@ -0,0 +1,25 @@ +// ReSharper disable CheckNamespace + +using System.Diagnostics; +using System.Runtime.CompilerServices; + +namespace Kurrent.Diagnostics; + +static class ActivityTagsCollectionExtensions { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ActivityTagsCollection WithRequiredTag(this ActivityTagsCollection source, string key, object? value) { + source[key] = value ?? throw new ArgumentNullException(key); + return source; + } + + /// + /// - If the key previously existed in the collection and the value is , the collection item matching the key will get removed from the collection. + /// - If the key previously existed in the collection and the value is not , the value will replace the old value stored in the collection. + /// - Otherwise, a new item will get added to the collection. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ActivityTagsCollection WithOptionalTag(this ActivityTagsCollection source, string key, object? value) { + source[key] = value; + return source; + } +} diff --git a/src/Kurrent.Client/Core/Common/Diagnostics/Core/ExceptionExtensions.cs b/src/Kurrent.Client/Core/Common/Diagnostics/Core/ExceptionExtensions.cs new file mode 100644 index 000000000..7eb397251 --- /dev/null +++ b/src/Kurrent.Client/Core/Common/Diagnostics/Core/ExceptionExtensions.cs @@ -0,0 +1,25 @@ +// ReSharper disable CheckNamespace + +using System.Globalization; + +namespace Kurrent.Diagnostics; + +static class ExceptionExtensions { + /// + /// Returns a culture-independent string representation of the given object, + /// appropriate for diagnostics tracing. + /// + /// Exception to convert to string. + /// Exception as string with no culture. + public static string ToInvariantString(this Exception exception) { + var originalUiCulture = Thread.CurrentThread.CurrentUICulture; + + try { + Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture; + return exception.ToString(); + } + finally { + Thread.CurrentThread.CurrentUICulture = originalUiCulture; + } + } +} diff --git a/src/Kurrent.Client/Core/Common/Diagnostics/Core/Telemetry/TelemetryTags.cs b/src/Kurrent.Client/Core/Common/Diagnostics/Core/Telemetry/TelemetryTags.cs new file mode 100644 index 000000000..54487e3bd --- /dev/null +++ b/src/Kurrent.Client/Core/Common/Diagnostics/Core/Telemetry/TelemetryTags.cs @@ -0,0 +1,35 @@ +// ReSharper disable CheckNamespace + +namespace Kurrent.Diagnostics.Telemetry; + +// The attributes below match the specification of v1.24.0 of the Open Telemetry semantic conventions. +// Some attributes are ignored where not required or relevant. +// https://github.com/open-telemetry/semantic-conventions/blob/v1.24.0/docs/general/trace.md +// https://github.com/open-telemetry/semantic-conventions/blob/v1.24.0/docs/database/database-spans.md +// https://github.com/open-telemetry/semantic-conventions/blob/v1.24.0/docs/exceptions/exceptions-spans.md + +static partial class TelemetryTags { + public static class Database { + public const string User = "db.user"; + public const string System = "db.system"; + public const string Operation = "db.operation"; + } + + public static class Server { + public const string Address = "server.address"; + public const string Port = "server.port"; + public const string SocketAddress = "server.socket.address"; // replaces: "net.peer.ip" (AttributeNetPeerIp) + } + + public static class Exception { + public const string EventName = "exception"; + public const string Type = "exception.type"; + public const string Message = "exception.message"; + public const string Stacktrace = "exception.stacktrace"; + } + + public static class Otel { + public const string StatusCode = "otel.status_code"; + public const string StatusDescription = "otel.status_description"; + } +} diff --git a/src/Kurrent.Client/Core/Common/Diagnostics/Core/Tracing/TracingConstants.cs b/src/Kurrent.Client/Core/Common/Diagnostics/Core/Tracing/TracingConstants.cs new file mode 100644 index 000000000..1e94c9ef0 --- /dev/null +++ b/src/Kurrent.Client/Core/Common/Diagnostics/Core/Tracing/TracingConstants.cs @@ -0,0 +1,10 @@ +// ReSharper disable CheckNamespace + +namespace Kurrent.Diagnostics.Tracing; + +static partial class TracingConstants { + public static class Metadata { + public const string TraceId = "$traceId"; + public const string SpanId = "$spanId"; + } +} diff --git a/src/Kurrent.Client/Core/Common/Diagnostics/Core/Tracing/TracingMetadata.cs b/src/Kurrent.Client/Core/Common/Diagnostics/Core/Tracing/TracingMetadata.cs new file mode 100644 index 000000000..7580753a7 --- /dev/null +++ b/src/Kurrent.Client/Core/Common/Diagnostics/Core/Tracing/TracingMetadata.cs @@ -0,0 +1,32 @@ +// ReSharper disable CheckNamespace + +using System.Diagnostics; +using System.Text.Json.Serialization; + +namespace Kurrent.Diagnostics.Tracing; + +readonly record struct TracingMetadata( + [property: JsonPropertyName(TracingConstants.Metadata.TraceId)] + string? TraceId, + [property: JsonPropertyName(TracingConstants.Metadata.SpanId)] + string? SpanId +) { + public static readonly TracingMetadata None = new(null, null); + + [JsonIgnore] public bool IsValid => TraceId != null && SpanId != null; + + public ActivityContext? ToActivityContext(bool isRemote = true) { + try { + return IsValid + ? new ActivityContext( + ActivityTraceId.CreateFromString(new ReadOnlySpan(TraceId!.ToCharArray())), + ActivitySpanId.CreateFromString(new ReadOnlySpan(SpanId!.ToCharArray())), + ActivityTraceFlags.Recorded, + isRemote: isRemote + ) + : default; + } catch (Exception) { + return default; + } + } +} diff --git a/src/Kurrent.Client/Core/Common/Diagnostics/EventMetadataExtensions.cs b/src/Kurrent.Client/Core/Common/Diagnostics/EventMetadataExtensions.cs new file mode 100644 index 000000000..d45b53156 --- /dev/null +++ b/src/Kurrent.Client/Core/Common/Diagnostics/EventMetadataExtensions.cs @@ -0,0 +1,84 @@ +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Text.Json; +using Kurrent.Diagnostics; +using Kurrent.Diagnostics.Tracing; + +namespace EventStore.Client.Diagnostics; + +static class EventMetadataExtensions { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReadOnlySpan InjectTracingContext( + this ReadOnlyMemory eventMetadata, Activity? activity + ) => + eventMetadata.InjectTracingMetadata(activity?.GetTracingMetadata() ?? TracingMetadata.None); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ActivityContext? ExtractPropagationContext(this ReadOnlyMemory eventMetadata) => + eventMetadata.ExtractTracingMetadata().ToActivityContext(isRemote: true); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TracingMetadata ExtractTracingMetadata(this ReadOnlyMemory eventMetadata) { + if (eventMetadata.IsEmpty) + return TracingMetadata.None; + + var reader = new Utf8JsonReader(eventMetadata.Span); + try { + if (!JsonDocument.TryParseValue(ref reader, out var doc)) + return TracingMetadata.None; + + using (doc) { + if (!doc.RootElement.TryGetProperty(TracingConstants.Metadata.TraceId, out var traceId) + || !doc.RootElement.TryGetProperty(TracingConstants.Metadata.SpanId, out var spanId)) + return TracingMetadata.None; + + return new TracingMetadata(traceId.GetString(), spanId.GetString()); + } + } catch (Exception) { + return TracingMetadata.None; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static ReadOnlySpan InjectTracingMetadata( + this ReadOnlyMemory eventMetadata, TracingMetadata tracingMetadata + ) { + if (tracingMetadata == TracingMetadata.None || !tracingMetadata.IsValid) + return eventMetadata.Span; + + return eventMetadata.IsEmpty + ? JsonSerializer.SerializeToUtf8Bytes(tracingMetadata) + : TryInjectTracingMetadata(eventMetadata, tracingMetadata).ToArray(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static ReadOnlyMemory TryInjectTracingMetadata( + this ReadOnlyMemory utf8Json, TracingMetadata tracingMetadata + ) { + try { + using var doc = JsonDocument.Parse(utf8Json); + using var stream = new MemoryStream(); + using var writer = new Utf8JsonWriter(stream); + + writer.WriteStartObject(); + + if (doc.RootElement.ValueKind != JsonValueKind.Object) + return utf8Json; + + foreach (var prop in doc.RootElement.EnumerateObject()) + prop.WriteTo(writer); + + writer.WritePropertyName(TracingConstants.Metadata.TraceId); + writer.WriteStringValue(tracingMetadata.TraceId); + writer.WritePropertyName(TracingConstants.Metadata.SpanId); + writer.WriteStringValue(tracingMetadata.SpanId); + + writer.WriteEndObject(); + writer.Flush(); + + return stream.ToArray(); + } catch (Exception) { + return utf8Json; + } + } +} diff --git a/src/Kurrent.Client/Core/Common/Diagnostics/KurrentClientDiagnostics.cs b/src/Kurrent.Client/Core/Common/Diagnostics/KurrentClientDiagnostics.cs new file mode 100644 index 000000000..48e437463 --- /dev/null +++ b/src/Kurrent.Client/Core/Common/Diagnostics/KurrentClientDiagnostics.cs @@ -0,0 +1,8 @@ +using System.Diagnostics; + +namespace EventStore.Client.Diagnostics; + +public static class KurrentClientDiagnostics { + public const string InstrumentationName = "kurrent"; + public static readonly ActivitySource ActivitySource = new(InstrumentationName); +} diff --git a/src/Kurrent.Client/Core/Common/Diagnostics/Telemetry/TelemetryTags.cs b/src/Kurrent.Client/Core/Common/Diagnostics/Telemetry/TelemetryTags.cs new file mode 100644 index 000000000..e4e0b11f9 --- /dev/null +++ b/src/Kurrent.Client/Core/Common/Diagnostics/Telemetry/TelemetryTags.cs @@ -0,0 +1,12 @@ +// ReSharper disable CheckNamespace + +namespace Kurrent.Diagnostics.Telemetry; + +static partial class TelemetryTags { + public static class Kurrent { + public const string Stream = "db.kurrent.stream"; + public const string SubscriptionId = "db.kurrent.subscription.id"; + public const string EventId = "db.kurrent.event.id"; + public const string EventType = "db.kurrent.event.type"; + } +} diff --git a/src/Kurrent.Client/Core/Common/Diagnostics/Tracing/TracingConstants.cs b/src/Kurrent.Client/Core/Common/Diagnostics/Tracing/TracingConstants.cs new file mode 100644 index 000000000..357654570 --- /dev/null +++ b/src/Kurrent.Client/Core/Common/Diagnostics/Tracing/TracingConstants.cs @@ -0,0 +1,10 @@ +// ReSharper disable CheckNamespace + +namespace Kurrent.Diagnostics.Tracing; + +static partial class TracingConstants { + public static class Operations { + public const string Append = "streams.append"; + public const string Subscribe = "streams.subscribe"; + } +} diff --git a/src/Kurrent.Client/Core/Common/EnumerableTaskExtensions.cs b/src/Kurrent.Client/Core/Common/EnumerableTaskExtensions.cs new file mode 100644 index 000000000..5be066ab5 --- /dev/null +++ b/src/Kurrent.Client/Core/Common/EnumerableTaskExtensions.cs @@ -0,0 +1,11 @@ +using System.Diagnostics; + +namespace EventStore.Client; + +static class EnumerableTaskExtensions { + [DebuggerStepThrough] + public static Task WhenAll(this IEnumerable source) => Task.WhenAll(source); + + [DebuggerStepThrough] + public static Task WhenAll(this IEnumerable> source) => Task.WhenAll(source); +} diff --git a/src/Kurrent.Client/Core/Common/EpochExtensions.cs b/src/Kurrent.Client/Core/Common/EpochExtensions.cs new file mode 100644 index 000000000..0643bcecb --- /dev/null +++ b/src/Kurrent.Client/Core/Common/EpochExtensions.cs @@ -0,0 +1,25 @@ +namespace EventStore.Client; + +static class EpochExtensions { +#if NET + static readonly DateTime UnixEpoch = DateTime.UnixEpoch; +#else + const long TicksPerMillisecond = 10000; + const long TicksPerSecond = TicksPerMillisecond * 1000; + const long TicksPerMinute = TicksPerSecond * 60; + const long TicksPerHour = TicksPerMinute * 60; + const long TicksPerDay = TicksPerHour * 24; + const int DaysPerYear = 365; + const int DaysPer4Years = DaysPerYear * 4 + 1; + const int DaysPer100Years = DaysPer4Years * 25 - 1; + const int DaysPer400Years = DaysPer100Years * 4 + 1; + const int DaysTo1970 = DaysPer400Years * 4 + DaysPer100Years * 3 + DaysPer4Years * 17 + DaysPerYear; + const long UnixEpochTicks = DaysTo1970 * TicksPerDay; + + static readonly DateTime UnixEpoch = new(UnixEpochTicks, DateTimeKind.Utc); +#endif + + public static DateTime FromTicksSinceEpoch(this long value) => new(UnixEpoch.Ticks + value, DateTimeKind.Utc); + + public static long ToTicksSinceEpoch(this DateTime value) => (value - UnixEpoch).Ticks; +} diff --git a/src/Kurrent.Client/Core/Common/KurrentCallOptions.cs b/src/Kurrent.Client/Core/Common/KurrentCallOptions.cs new file mode 100644 index 000000000..0644cffea --- /dev/null +++ b/src/Kurrent.Client/Core/Common/KurrentCallOptions.cs @@ -0,0 +1,74 @@ +using Grpc.Core; +using static System.Threading.Timeout; + +namespace EventStore.Client; + +static class KurrentCallOptions { + // deadline falls back to infinity + public static CallOptions CreateStreaming( + KurrentClientSettings settings, + TimeSpan? deadline = null, UserCredentials? userCredentials = null, + CancellationToken cancellationToken = default + ) => + Create(settings, deadline, userCredentials, cancellationToken); + + // deadline falls back to connection DefaultDeadline + public static CallOptions CreateNonStreaming( + KurrentClientSettings settings, + CancellationToken cancellationToken + ) => + Create( + settings, + settings.DefaultDeadline, + settings.DefaultCredentials, + cancellationToken + ); + + public static CallOptions CreateNonStreaming( + KurrentClientSettings settings, TimeSpan? deadline, + UserCredentials? userCredentials, CancellationToken cancellationToken + ) => + Create( + settings, + deadline ?? settings.DefaultDeadline, + userCredentials, + cancellationToken + ); + + static CallOptions Create( + KurrentClientSettings settings, TimeSpan? deadline, + UserCredentials? userCredentials, CancellationToken cancellationToken + ) => + new( + cancellationToken: cancellationToken, + deadline: DeadlineAfter(deadline), + headers: new() { + { + Constants.Headers.RequiresLeader, + settings.ConnectivitySettings.NodePreference == NodePreference.Leader + ? bool.TrueString + : bool.FalseString + } + }, + credentials: (userCredentials ?? settings.DefaultCredentials) == null + ? null + : CallCredentials.FromInterceptor( + async (_, metadata) => { + var credentials = userCredentials ?? settings.DefaultCredentials; + + var authorizationHeader = await settings.OperationOptions + .GetAuthenticationHeaderValue(credentials!, CancellationToken.None) + .ConfigureAwait(false); + + metadata.Add(Constants.Headers.Authorization, authorizationHeader); + } + ) + ); + + static DateTime? DeadlineAfter(TimeSpan? timeoutAfter) => + !timeoutAfter.HasValue + ? new DateTime?() + : timeoutAfter.Value == TimeSpan.MaxValue || timeoutAfter.Value == InfiniteTimeSpan + ? DateTime.MaxValue + : DateTime.UtcNow.Add(timeoutAfter.Value); +} diff --git a/src/Kurrent.Client/Core/Common/MetadataExtensions.cs b/src/Kurrent.Client/Core/Common/MetadataExtensions.cs new file mode 100644 index 000000000..e7311c37f --- /dev/null +++ b/src/Kurrent.Client/Core/Common/MetadataExtensions.cs @@ -0,0 +1,29 @@ +using Grpc.Core; + +namespace EventStore.Client; + +static class MetadataExtensions { + public static bool TryGetValue(this Metadata metadata, string key, out string? value) { + value = default; + + foreach (var entry in metadata) { + if (entry.Key != key) + continue; + + value = entry.Value; + return true; + } + + return false; + } + + public static StreamRevision GetStreamRevision(this Metadata metadata, string key) => + metadata.TryGetValue(key, out var s) && ulong.TryParse(s, out var value) + ? new(value) + : StreamRevision.None; + + public static int GetIntValueOrDefault(this Metadata metadata, string key) => + metadata.TryGetValue(key, out var s) && int.TryParse(s, out var value) + ? value + : default; +} diff --git a/src/Kurrent.Client/Core/Common/Shims/Index.cs b/src/Kurrent.Client/Core/Common/Shims/Index.cs new file mode 100644 index 000000000..357bbd34d --- /dev/null +++ b/src/Kurrent.Client/Core/Common/Shims/Index.cs @@ -0,0 +1,110 @@ +#if !NET +using System.Runtime.CompilerServices; + +namespace System; + +/// Represent a type can be used to index a collection either from the start or the end. +/// +/// Index is used by the C# compiler to support the new index syntax +/// +/// int[] someArray = new int[5] { 1, 2, 3, 4, 5 } ; +/// int lastElement = someArray[^1]; // lastElement = 5 +/// +/// +readonly struct Index : IEquatable { + readonly int _value; + + /// Construct an Index using a value and indicating if the index is from the start or from the end. + /// The index value. it has to be zero or positive number. + /// Indicating if the index is from the start or from the end. + /// + /// If the Index constructed from the end, index value 1 means pointing at the last element and index value 0 means pointing at beyond last element. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Index(int value, bool fromEnd = false) { + if (value < 0) throw new ArgumentOutOfRangeException(nameof(value), "value must be non-negative"); + + if (fromEnd) + _value = ~value; + else + _value = value; + } + + // The following private constructors mainly created for perf reason to avoid the checks + Index(int value) => _value = value; + + /// Create an Index pointing at first element. + public static Index Start => new(0); + + /// Create an Index pointing at beyond last element. + public static Index End => new(~0); + + /// Create an Index from the start at the position indicated by the value. + /// The index value from the start. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Index FromStart(int value) { + if (value < 0) throw new ArgumentOutOfRangeException(nameof(value), "value must be non-negative"); + + return new Index(value); + } + + /// Create an Index from the end at the position indicated by the value. + /// The index value from the end. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Index FromEnd(int value) { + if (value < 0) throw new ArgumentOutOfRangeException(nameof(value), "value must be non-negative"); + + return new Index(~value); + } + + /// Returns the index value. + public int Value { + get { + if (_value < 0) + return ~_value; + else + return _value; + } + } + + /// Indicates whether the index is from the start or the end. + public bool IsFromEnd => _value < 0; + + /// Calculate the offset from the start using the giving collection length. + /// The length of the collection that the Index will be used with. length has to be a positive value + /// + /// For performance reason, we don't validate the input length parameter and the returned offset value against negative values. + /// we don't validate either the returned offset is greater than the input length. + /// It is expected Index will be used with collections which always have non negative length/count. If the returned offset is negative and + /// then used to index a collection will get out of range exception which will be same affect as the validation. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int GetOffset(int length) { + var offset = _value; + if (IsFromEnd) + // offset = length - (~value) + // offset = length + (~(~value) + 1) + // offset = length + value + 1 + offset += length + 1; + + return offset; + } + + /// Indicates whether the current Index object is equal to another object of the same type. + /// An object to compare with this object + public override bool Equals(object? value) => value is Index && _value == ((Index)value)._value; + + /// Indicates whether the current Index object is equal to another Index object. + /// An object to compare with this object + public bool Equals(Index other) => _value == other._value; + + /// Returns the hash code for this instance. + public override int GetHashCode() => _value; + + /// Converts integer number to an Index. + public static implicit operator Index(int value) => FromStart(value); + + /// Converts the value of the current Index object to its equivalent string representation. + public override string ToString() => IsFromEnd ? $"^{(uint)Value}" : ((uint)Value).ToString(); +} +#endif \ No newline at end of file diff --git a/src/Kurrent.Client/Core/Common/Shims/IsExternalInit.cs b/src/Kurrent.Client/Core/Common/Shims/IsExternalInit.cs new file mode 100644 index 000000000..7dc4fea3d --- /dev/null +++ b/src/Kurrent.Client/Core/Common/Shims/IsExternalInit.cs @@ -0,0 +1,10 @@ +#if !NET + +using System.ComponentModel; + +// ReSharper disable once CheckNamespace +namespace System.Runtime.CompilerServices; + +[EditorBrowsable(EditorBrowsableState.Never)] +class IsExternalInit{} +#endif diff --git a/src/Kurrent.Client/Core/Common/Shims/Range.cs b/src/Kurrent.Client/Core/Common/Shims/Range.cs new file mode 100644 index 000000000..3a0b34fde --- /dev/null +++ b/src/Kurrent.Client/Core/Common/Shims/Range.cs @@ -0,0 +1,75 @@ +#if !NET +// ReSharper disable CheckNamespace + +using System.Runtime.CompilerServices; + +namespace System; + +/// Represent a range has start and end indexes. +/// +/// Range is used by the C# compiler to support the range syntax. +/// +/// int[] someArray = new int[5] { 1, 2, 3, 4, 5 }; +/// int[] subArray1 = someArray[0..2]; // { 1, 2 } +/// int[] subArray2 = someArray[1..^0]; // { 2, 3, 4, 5 } +/// +/// +readonly struct Range : IEquatable { + /// Represent the inclusive start index of the Range. + public Index Start { get; } + + /// Represent the exclusive end index of the Range. + public Index End { get; } + + /// Construct a Range object using the start and end indexes. + /// Represent the inclusive start index of the range. + /// Represent the exclusive end index of the range. + public Range(Index start, Index end) { + Start = start; + End = end; + } + + /// Indicates whether the current Range object is equal to another object of the same type. + /// An object to compare with this object + public override bool Equals(object? value) => + value is Range r && + r.Start.Equals(Start) && + r.End.Equals(End); + + /// Indicates whether the current Range object is equal to another Range object. + /// An object to compare with this object + public bool Equals(Range other) => other.Start.Equals(Start) && other.End.Equals(End); + + /// Returns the hash code for this instance. + public override int GetHashCode() => Start.GetHashCode() * 31 + End.GetHashCode(); + + /// Converts the value of the current Range object to its equivalent string representation. + public override string ToString() => $"{Start}..{End}"; + + /// Create a Range object starting from start index to the end of the collection. + public static Range StartAt(Index start) => new(start, Index.End); + + /// Create a Range object starting from first element in the collection to the end Index. + public static Range EndAt(Index end) => new(Index.Start, end); + + /// Create a Range object starting from first element to the end. + public static Range All => new(Index.Start, Index.End); + + /// Calculate the start offset and length of range object using a collection length. + /// The length of the collection that the range will be used with. length has to be a positive value. + /// + /// For performance reason, we don't validate the input length parameter against negative values. + /// It is expected Range will be used with collections which always have non negative length/count. + /// We validate the range is inside the length scope though. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public (int Offset, int Length) GetOffsetAndLength(int length) { + var start = Start.GetOffset(length); + var end = End.GetOffset(length); + + if ((uint)end > (uint)length || (uint)start > (uint)end) throw new ArgumentOutOfRangeException(nameof(length)); + + return (start, end - start); + } +} +#endif \ No newline at end of file diff --git a/src/Kurrent.Client/Core/Common/Shims/TaskCompletionSource.cs b/src/Kurrent.Client/Core/Common/Shims/TaskCompletionSource.cs new file mode 100644 index 000000000..ad6573c4a --- /dev/null +++ b/src/Kurrent.Client/Core/Common/Shims/TaskCompletionSource.cs @@ -0,0 +1,11 @@ +#if !NET +// ReSharper disable CheckNamespace + +namespace System.Threading.Tasks; + +class TaskCompletionSource : TaskCompletionSource { + public void SetResult() => base.SetResult(null); + public bool TrySetResult() => base.TrySetResult(null); +} + +#endif \ No newline at end of file diff --git a/src/Kurrent.Client/Core/DefaultRequestVersionHandler.cs b/src/Kurrent.Client/Core/DefaultRequestVersionHandler.cs new file mode 100644 index 000000000..b6cda6a7f --- /dev/null +++ b/src/Kurrent.Client/Core/DefaultRequestVersionHandler.cs @@ -0,0 +1,14 @@ +using System; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +namespace EventStore.Client { + internal class DefaultRequestVersionHandler : DelegatingHandler { + public DefaultRequestVersionHandler(HttpMessageHandler innerHandler) : base(innerHandler) { } + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { + request.Version = new Version(2, 0); + return base.SendAsync(request, cancellationToken); + } + } +} diff --git a/src/Kurrent.Client/Core/EndPointExtensions.cs b/src/Kurrent.Client/Core/EndPointExtensions.cs new file mode 100644 index 000000000..8ebad38e0 --- /dev/null +++ b/src/Kurrent.Client/Core/EndPointExtensions.cs @@ -0,0 +1,41 @@ +using System; +using System.Net; + +namespace EventStore.Client { + internal static class EndPointExtensions { + public static string GetHost(this EndPoint endpoint) => + endpoint switch { + IPEndPoint ip => ip.Address.ToString(), + DnsEndPoint dns => dns.Host, + _ => throw new ArgumentOutOfRangeException(nameof(endpoint), endpoint?.GetType(), + "An invalid endpoint has been provided") + }; + + public static int GetPort(this EndPoint endpoint) => + endpoint switch { + IPEndPoint ip => ip.Port, + DnsEndPoint dns => dns.Port, + _ => throw new ArgumentOutOfRangeException(nameof(endpoint), endpoint?.GetType(), + "An invalid endpoint has been provided") + }; + + public static Uri ToUri(this EndPoint endPoint, bool https) => new UriBuilder { + Scheme = https ? Uri.UriSchemeHttps : Uri.UriSchemeHttp, + Host = endPoint.GetHost(), + Port = endPoint.GetPort() + }.Uri; + + public static string? ToHttpUrl(this EndPoint endPoint, string schema, string? rawUrl = null) => + endPoint switch { + IPEndPoint ipEndPoint => CreateHttpUrl(schema, ipEndPoint.Address.ToString(), ipEndPoint.Port, + rawUrl != null ? rawUrl.TrimStart('/') : string.Empty), + DnsEndPoint dnsEndpoint => CreateHttpUrl(schema, dnsEndpoint.Host, dnsEndpoint.Port, + rawUrl != null ? rawUrl.TrimStart('/') : string.Empty), + _ => null + }; + + private static string CreateHttpUrl(string schema, string host, int port, string path) { + return $"{schema}://{host}:{port}/{path}"; + } + } +} diff --git a/src/Kurrent.Client/Core/EventData.cs b/src/Kurrent.Client/Core/EventData.cs new file mode 100644 index 000000000..3849ff364 --- /dev/null +++ b/src/Kurrent.Client/Core/EventData.cs @@ -0,0 +1,65 @@ +using System; +using System.Net.Http.Headers; + +namespace EventStore.Client { + /// + /// Represents an event to be written. + /// + public sealed class EventData { + /// + /// The raw bytes of the event data. + /// + public readonly ReadOnlyMemory Data; + + /// + /// The of the event, used as part of the idempotent write check. + /// + public readonly Uuid EventId; + + /// + /// The raw bytes of the event metadata. + /// + public readonly ReadOnlyMemory Metadata; + + /// + /// The name of the event type. It is strongly recommended that these + /// use lowerCamelCase if projections are to be used. + /// + public readonly string Type; + + /// + /// The Content-Type of the . Valid values are 'application/json' and 'application/octet-stream'. + /// + public readonly string ContentType; + + /// + /// Constructs a new . + /// + /// The of the event, used as part of the idempotent write check. + /// The name of the event type. It is strongly recommended that these use lowerCamelCase if projections are to be used. + /// The raw bytes of the event data. + /// The raw bytes of the event metadata. + /// The Content-Type of the . Valid values are 'application/json' and 'application/octet-stream'. + /// + public EventData(Uuid eventId, string type, ReadOnlyMemory data, ReadOnlyMemory? metadata = null, + string contentType = Constants.Metadata.ContentTypes.ApplicationJson) { + if (eventId == Uuid.Empty) { + throw new ArgumentOutOfRangeException(nameof(eventId)); + } + + MediaTypeHeaderValue.Parse(contentType); + + if (contentType != Constants.Metadata.ContentTypes.ApplicationJson && + contentType != Constants.Metadata.ContentTypes.ApplicationOctetStream) { + throw new ArgumentOutOfRangeException(nameof(contentType), contentType, + $"Only {Constants.Metadata.ContentTypes.ApplicationJson} or {Constants.Metadata.ContentTypes.ApplicationOctetStream} are acceptable values."); + } + + EventId = eventId; + Type = type; + Data = data; + Metadata = metadata ?? Array.Empty(); + ContentType = contentType; + } + } +} diff --git a/src/Kurrent.Client/Core/EventRecord.cs b/src/Kurrent.Client/Core/EventRecord.cs new file mode 100644 index 000000000..531362661 --- /dev/null +++ b/src/Kurrent.Client/Core/EventRecord.cs @@ -0,0 +1,80 @@ +namespace EventStore.Client { + /// + /// Represents a previously written event. + /// + public class EventRecord { + /// + /// The stream that this event belongs to. + /// + public readonly string EventStreamId; + + /// + /// The representing this event. + /// + public readonly Uuid EventId; + + /// + /// The of this event in the stream. + /// + public readonly StreamPosition EventNumber; + + /// + /// The type of event this is. + /// + public readonly string EventType; + + /// + /// The raw bytes representing the data of this event. + /// + public readonly ReadOnlyMemory Data; + + /// + /// The raw bytes representing the metadata of this event. + /// + public readonly ReadOnlyMemory Metadata; + + /// + /// A UTC representing when this event was created in the system. + /// + public readonly DateTime Created; + + /// + /// The of this event in the $all stream. + /// + public readonly Position Position; + + /// + /// The Content-Type of the event's data. + /// + public readonly string ContentType; + + /// + /// Constructs a new . + /// + /// + /// + /// + /// + /// + /// + /// + public EventRecord( + string eventStreamId, + Uuid eventId, + StreamPosition eventNumber, + Position position, + IDictionary metadata, + ReadOnlyMemory data, + ReadOnlyMemory customMetadata) { + EventStreamId = eventStreamId; + EventId = eventId; + EventNumber = eventNumber; + Position = position; + Data = data; + Metadata = customMetadata; + EventType = metadata[Constants.Metadata.Type]; + Created = Convert.ToInt64(metadata[Constants.Metadata.Created]).FromTicksSinceEpoch(); + ContentType = metadata[Constants.Metadata.ContentType]; + } + } +} diff --git a/src/Kurrent.Client/Core/EventTypeFilter.cs b/src/Kurrent.Client/Core/EventTypeFilter.cs new file mode 100644 index 000000000..1e53e84e9 --- /dev/null +++ b/src/Kurrent.Client/Core/EventTypeFilter.cs @@ -0,0 +1,145 @@ +using System; +using System.Linq; +using System.Text.RegularExpressions; + +namespace EventStore.Client { + /// + /// A structure representing a filter on event types for read operations. + /// + public readonly struct EventTypeFilter : IEquatable, IEventFilter { + /// + /// An empty . + /// + public static readonly EventTypeFilter None = default; + + readonly PrefixFilterExpression[] _prefixes; + + + /// + public PrefixFilterExpression[] Prefixes => _prefixes ?? Array.Empty(); + + /// + public RegularFilterExpression Regex { get; } + + /// + public uint? MaxSearchWindow { get; } + + /// + /// An that excludes system events (i.e., those whose types start with $). + /// + /// + /// + public static EventTypeFilter ExcludeSystemEvents(uint maxSearchWindow = 32) => + new EventTypeFilter(maxSearchWindow, RegularFilterExpression.ExcludeSystemEvents); + + /// + /// Creates an from a single prefix. + /// + /// + /// + public static IEventFilter Prefix(string prefix) + => new EventTypeFilter(new PrefixFilterExpression(prefix)); + + /// + /// Creates an from multiple prefixes. + /// + /// + /// + public static IEventFilter Prefix(params string[] prefixes) + => new EventTypeFilter(Array.ConvertAll(prefixes, prefix => new PrefixFilterExpression(prefix))); + + /// + /// Creates an from a search window and multiple prefixes. + /// + /// + /// + /// + public static IEventFilter Prefix(uint maxSearchWindow, params string[] prefixes) + => new EventTypeFilter(maxSearchWindow, + Array.ConvertAll(prefixes, prefix => new PrefixFilterExpression(prefix))); + + /// + /// Creates an from a regular expression and a search window. + /// + /// + /// + /// + public static IEventFilter RegularExpression(string regex, uint maxSearchWindow = 32) + => new EventTypeFilter(maxSearchWindow, new RegularFilterExpression(regex)); + + /// + /// Creates an from a regular expression and a search window. + /// + /// + /// + /// + public static IEventFilter RegularExpression(Regex regex, uint maxSearchWindow = 32) + => new EventTypeFilter(maxSearchWindow, new RegularFilterExpression(regex)); + + EventTypeFilter(uint maxSearchWindow, RegularFilterExpression regex) { + if (maxSearchWindow == 0) { + throw new ArgumentOutOfRangeException(nameof(maxSearchWindow), + maxSearchWindow, $"{nameof(maxSearchWindow)} must be greater than 0."); + } + + Regex = regex; + _prefixes = Array.Empty(); + MaxSearchWindow = maxSearchWindow; + } + + EventTypeFilter(params PrefixFilterExpression[] prefixes) : this(32, prefixes) { } + + EventTypeFilter(uint maxSearchWindow, params PrefixFilterExpression[] prefixes) { + if (prefixes.Length == 0) { + throw new ArgumentException(); + } + + if (maxSearchWindow == 0) { + throw new ArgumentOutOfRangeException(nameof(maxSearchWindow), + maxSearchWindow, $"{nameof(maxSearchWindow)} must be greater than 0."); + } + + _prefixes = prefixes; + Regex = RegularFilterExpression.None; + MaxSearchWindow = maxSearchWindow; + } + + /// + public bool Equals(EventTypeFilter other) => + Prefixes == null || other.Prefixes == null + ? Prefixes == other.Prefixes && + Regex.Equals(other.Regex) && + MaxSearchWindow.Equals(other.MaxSearchWindow) + : Prefixes.SequenceEqual(other.Prefixes) && + Regex.Equals(other.Regex) && + MaxSearchWindow.Equals(other.MaxSearchWindow); + + /// + public override bool Equals(object? obj) => obj is EventTypeFilter other && Equals(other); + + /// + public override int GetHashCode() => HashCode.Hash.Combine(Prefixes).Combine(Regex).Combine(MaxSearchWindow); + + /// + /// Compares left and right for equality. + /// + /// + /// + /// True if left is equal to right. + public static bool operator ==(EventTypeFilter left, EventTypeFilter right) => left.Equals(right); + + /// + /// Compares left and right for inequality. + /// + /// + /// + /// True if left is not equal to right. + public static bool operator !=(EventTypeFilter left, EventTypeFilter right) => !left.Equals(right); + + /// + public override string ToString() => + this == None + ? "(none)" + : $"{nameof(EventTypeFilter)} {(Prefixes.Length == 0 ? Regex.ToString() : $"[{string.Join(", ", Prefixes)}]")}"; + } +} diff --git a/src/Kurrent.Client/Core/Exceptions/AccessDeniedException.cs b/src/Kurrent.Client/Core/Exceptions/AccessDeniedException.cs new file mode 100644 index 000000000..cfd69ee6d --- /dev/null +++ b/src/Kurrent.Client/Core/Exceptions/AccessDeniedException.cs @@ -0,0 +1,22 @@ +using System; + +namespace EventStore.Client { + /// + /// Exception thrown when a user is not authorised to carry out + /// an operation. + /// + public class AccessDeniedException : Exception { + /// + /// Constructs a new . + /// + public AccessDeniedException(string message, Exception innerException) : base(message, innerException) { + } + + /// + /// Constructs a new . + /// + public AccessDeniedException() : base("Access denied.") { + + } + } +} diff --git a/src/Kurrent.Client/Core/Exceptions/ConnectionString/ConnectionStringParseException.cs b/src/Kurrent.Client/Core/Exceptions/ConnectionString/ConnectionStringParseException.cs new file mode 100644 index 000000000..2cb8c1ac5 --- /dev/null +++ b/src/Kurrent.Client/Core/Exceptions/ConnectionString/ConnectionStringParseException.cs @@ -0,0 +1,15 @@ +using System; + +namespace EventStore.Client { + /// + /// The base exception that is thrown when an KurrentDB connection string could not be parsed. + /// + public class ConnectionStringParseException : Exception { + /// + /// Constructs a new . + /// + /// + /// The exception that is the cause of the current exception, or a null reference ( in Visual Basic) if no inner exception is specified. + public ConnectionStringParseException(string message, Exception? innerException = null) : base(message, innerException) { } + } +} diff --git a/src/Kurrent.Client/Core/Exceptions/ConnectionString/DuplicateKeyException.cs b/src/Kurrent.Client/Core/Exceptions/ConnectionString/DuplicateKeyException.cs new file mode 100644 index 000000000..5cc3c3812 --- /dev/null +++ b/src/Kurrent.Client/Core/Exceptions/ConnectionString/DuplicateKeyException.cs @@ -0,0 +1,13 @@ +namespace EventStore.Client { + /// + /// The exception that is thrown when a key in the KurrentDB connection string is duplicated. + /// + public class DuplicateKeyException : ConnectionStringParseException { + /// + /// Constructs a new . + /// + /// + public DuplicateKeyException(string key) + : base($"Duplicate key: '{key}'") { } + } +} diff --git a/src/Kurrent.Client/Core/Exceptions/ConnectionString/InvalidClientCertificateException.cs b/src/Kurrent.Client/Core/Exceptions/ConnectionString/InvalidClientCertificateException.cs new file mode 100644 index 000000000..1caf9f9fb --- /dev/null +++ b/src/Kurrent.Client/Core/Exceptions/ConnectionString/InvalidClientCertificateException.cs @@ -0,0 +1,14 @@ +namespace EventStore.Client { + /// + /// The exception that is thrown when a certificate is invalid or not found in the KurrentDB connection string. + /// + public class InvalidClientCertificateException : ConnectionStringParseException { + /// + /// Constructs a new . + /// + /// + /// + public InvalidClientCertificateException(string message, Exception? innerException = null) + : base(message, innerException) { } + } +} diff --git a/src/Kurrent.Client/Core/Exceptions/ConnectionString/InvalidHostException.cs b/src/Kurrent.Client/Core/Exceptions/ConnectionString/InvalidHostException.cs new file mode 100644 index 000000000..696a81c32 --- /dev/null +++ b/src/Kurrent.Client/Core/Exceptions/ConnectionString/InvalidHostException.cs @@ -0,0 +1,13 @@ +namespace EventStore.Client { + /// + /// The exception that is thrown when there is an invalid host in the KurrentDB connection string. + /// + public class InvalidHostException : ConnectionStringParseException { + /// + /// Constructs a new . + /// + /// + public InvalidHostException(string host) + : base($"Invalid host: '{host}'") { } + } +} diff --git a/src/Kurrent.Client/Core/Exceptions/ConnectionString/InvalidKeyValuePairException.cs b/src/Kurrent.Client/Core/Exceptions/ConnectionString/InvalidKeyValuePairException.cs new file mode 100644 index 000000000..d00e08445 --- /dev/null +++ b/src/Kurrent.Client/Core/Exceptions/ConnectionString/InvalidKeyValuePairException.cs @@ -0,0 +1,13 @@ +namespace EventStore.Client { + /// + /// The exception that is thrown when an invalid key value pair is found in an KurrentDB connection string. + /// + public class InvalidKeyValuePairException : ConnectionStringParseException { + /// + /// Constructs a new . + /// + /// + public InvalidKeyValuePairException(string keyValuePair) + : base($"Invalid key/value pair: '{keyValuePair}'") { } + } +} diff --git a/src/Kurrent.Client/Core/Exceptions/ConnectionString/InvalidSchemeException.cs b/src/Kurrent.Client/Core/Exceptions/ConnectionString/InvalidSchemeException.cs new file mode 100644 index 000000000..3b5cf2b18 --- /dev/null +++ b/src/Kurrent.Client/Core/Exceptions/ConnectionString/InvalidSchemeException.cs @@ -0,0 +1,14 @@ +namespace EventStore.Client { + /// + /// The exception that is thrown when an invalid scheme is defined in the KurrentDB connection string. + /// + public class InvalidSchemeException : ConnectionStringParseException { + /// + /// Constructs a new . + /// + /// + /// + public InvalidSchemeException(string scheme, string[] supportedSchemes) + : base($"Invalid scheme: '{scheme}'. Supported values are: {string.Join(",", supportedSchemes)}") { } + } +} diff --git a/src/Kurrent.Client/Core/Exceptions/ConnectionString/InvalidSettingException.cs b/src/Kurrent.Client/Core/Exceptions/ConnectionString/InvalidSettingException.cs new file mode 100644 index 000000000..71dcc6edc --- /dev/null +++ b/src/Kurrent.Client/Core/Exceptions/ConnectionString/InvalidSettingException.cs @@ -0,0 +1,12 @@ +namespace EventStore.Client { + /// + /// The exception that is thrown when an invalid setting is found in an KurrentDB connection string. + /// + public class InvalidSettingException : ConnectionStringParseException { + /// + /// Constructs a new . + /// + /// + public InvalidSettingException(string message) : base(message) { } + } +} diff --git a/src/Kurrent.Client/Core/Exceptions/ConnectionString/InvalidUserCredentialsException.cs b/src/Kurrent.Client/Core/Exceptions/ConnectionString/InvalidUserCredentialsException.cs new file mode 100644 index 000000000..1d4544a61 --- /dev/null +++ b/src/Kurrent.Client/Core/Exceptions/ConnectionString/InvalidUserCredentialsException.cs @@ -0,0 +1,13 @@ +namespace EventStore.Client { + /// + /// The exception that is thrown when an invalid is specified in the KurrentDB connection string. + /// + public class InvalidUserCredentialsException : ConnectionStringParseException { + /// + /// + /// + /// + public InvalidUserCredentialsException(string userInfo) + : base($"Invalid user credentials: '{userInfo}'. Username & password must be delimited by a colon") { } + } +} diff --git a/src/Kurrent.Client/Core/Exceptions/ConnectionString/NoSchemeException.cs b/src/Kurrent.Client/Core/Exceptions/ConnectionString/NoSchemeException.cs new file mode 100644 index 000000000..08432be97 --- /dev/null +++ b/src/Kurrent.Client/Core/Exceptions/ConnectionString/NoSchemeException.cs @@ -0,0 +1,12 @@ +namespace EventStore.Client { + /// + /// The exception that is thrown when no scheme was specified in the KurrentDB connection string. + /// + public class NoSchemeException : ConnectionStringParseException { + /// + /// Constructs a new . + /// + public NoSchemeException() + : base("Could not parse scheme from connection string") { } + } +} diff --git a/src/Kurrent.Client/Core/Exceptions/DiscoveryException.cs b/src/Kurrent.Client/Core/Exceptions/DiscoveryException.cs new file mode 100644 index 000000000..f3f490adb --- /dev/null +++ b/src/Kurrent.Client/Core/Exceptions/DiscoveryException.cs @@ -0,0 +1,33 @@ +using System; + +namespace EventStore.Client { + /// + /// The exception that is thrown when discovery fails. + /// + public class DiscoveryException : Exception { + /// + /// The configured number of discovery attempts. + /// + public int MaxDiscoverAttempts { get; } + + /// + /// Constructs a new . + /// + /// + /// + [Obsolete] + public DiscoveryException(string message, Exception? innerException = null) + : base(message, innerException) { + MaxDiscoverAttempts = 0; + } + + /// + /// Constructs a new . + /// + /// The configured number of discovery attempts. + public DiscoveryException(int maxDiscoverAttempts) : base( + $"Failed to discover candidate in {maxDiscoverAttempts} attempts.") { + MaxDiscoverAttempts = maxDiscoverAttempts; + } + } +} diff --git a/src/Kurrent.Client/Core/Exceptions/NotAuthenticatedException.cs b/src/Kurrent.Client/Core/Exceptions/NotAuthenticatedException.cs new file mode 100644 index 000000000..289cfa230 --- /dev/null +++ b/src/Kurrent.Client/Core/Exceptions/NotAuthenticatedException.cs @@ -0,0 +1,16 @@ +using System; + +namespace EventStore.Client { + /// + /// The exception that is thrown when a user is not authenticated. + /// + public class NotAuthenticatedException : Exception { + /// + /// Constructs a new . + /// + /// + /// + public NotAuthenticatedException(string message, Exception? innerException = null) : base(message, innerException) { + } + } +} diff --git a/src/Kurrent.Client/Core/Exceptions/NotLeaderException.cs b/src/Kurrent.Client/Core/Exceptions/NotLeaderException.cs new file mode 100644 index 000000000..4ad5ca32a --- /dev/null +++ b/src/Kurrent.Client/Core/Exceptions/NotLeaderException.cs @@ -0,0 +1,26 @@ +using System; +using System.Net; + +namespace EventStore.Client { + /// + /// The exception that is thrown when an operation requiring a leader node is made on a follower node. + /// + public class NotLeaderException : Exception { + + /// + /// The of the current leader node. + /// + public DnsEndPoint LeaderEndpoint { get; } + + /// + /// Constructs a new + /// + /// + /// + /// + public NotLeaderException(string host, int port, Exception? exception = null) : base( + $"Not leader. New leader at {host}:{port}.", exception) { + LeaderEndpoint = new DnsEndPoint(host, port); + } + } +} diff --git a/src/Kurrent.Client/Core/Exceptions/RequiredMetadataPropertyMissingException.cs b/src/Kurrent.Client/Core/Exceptions/RequiredMetadataPropertyMissingException.cs new file mode 100644 index 000000000..079eb0444 --- /dev/null +++ b/src/Kurrent.Client/Core/Exceptions/RequiredMetadataPropertyMissingException.cs @@ -0,0 +1,18 @@ +using System; + +namespace EventStore.Client { + /// + /// Exception thrown when a required metadata property is missing. + /// + public class RequiredMetadataPropertyMissingException : Exception { + /// + /// Constructs a new . + /// + /// + /// + public RequiredMetadataPropertyMissingException(string missingMetadataProperty, + Exception? innerException = null) : + base($"Required metadata property {missingMetadataProperty} is missing", innerException) { + } + } +} diff --git a/src/Kurrent.Client/Core/Exceptions/ScavengeNotFoundException.cs b/src/Kurrent.Client/Core/Exceptions/ScavengeNotFoundException.cs new file mode 100644 index 000000000..06a629661 --- /dev/null +++ b/src/Kurrent.Client/Core/Exceptions/ScavengeNotFoundException.cs @@ -0,0 +1,23 @@ +using System; + +namespace EventStore.Client { + /// + /// The exception that is thrown when attempting to see the status of a scavenge operation that does not exist. + /// + public class ScavengeNotFoundException : Exception { + /// + /// The id of the scavenge operation. + /// + public string? ScavengeId { get; } + + /// + /// Constructs a new . + /// + /// + /// + public ScavengeNotFoundException(string? scavengeId, Exception? exception = null) : base( + $"Scavenge not found. The currently running scavenge is {scavengeId ?? ""}.", exception) { + ScavengeId = scavengeId; + } + } +} diff --git a/src/Kurrent.Client/Core/Exceptions/StreamDeletedException.cs b/src/Kurrent.Client/Core/Exceptions/StreamDeletedException.cs new file mode 100644 index 000000000..0b892578f --- /dev/null +++ b/src/Kurrent.Client/Core/Exceptions/StreamDeletedException.cs @@ -0,0 +1,24 @@ +using System; + +namespace EventStore.Client { + /// + /// Exception thrown if an operation is attempted on a stream which + /// has been deleted. + /// + public class StreamDeletedException : Exception { + /// + /// The name of the deleted stream. + /// + public readonly string Stream; + + /// + /// Constructs a new instance of . + /// + /// The name of the deleted stream. + /// + public StreamDeletedException(string stream, Exception? exception = null) + : base($"Event stream '{stream}' is deleted.", exception) { + Stream = stream; + } + } +} diff --git a/src/Kurrent.Client/Core/Exceptions/StreamNotFoundException.cs b/src/Kurrent.Client/Core/Exceptions/StreamNotFoundException.cs new file mode 100644 index 000000000..dc844df36 --- /dev/null +++ b/src/Kurrent.Client/Core/Exceptions/StreamNotFoundException.cs @@ -0,0 +1,23 @@ +using System; + +namespace EventStore.Client { + /// + /// The exception that is thrown when an attempt is made to read or write to a stream that does not exist. + /// + public class StreamNotFoundException : Exception { + /// + /// The name of the stream. + /// + public readonly string Stream; + + /// + /// Constructs a new instance of . + /// + /// The name of the stream. + /// + public StreamNotFoundException(string stream, Exception? exception = null) + : base($"Event stream '{stream}' was not found.", exception) { + Stream = stream; + } + } +} diff --git a/src/Kurrent.Client/Core/Exceptions/UserNotFoundException.cs b/src/Kurrent.Client/Core/Exceptions/UserNotFoundException.cs new file mode 100644 index 000000000..3caa6cb68 --- /dev/null +++ b/src/Kurrent.Client/Core/Exceptions/UserNotFoundException.cs @@ -0,0 +1,23 @@ +using System; + +namespace EventStore.Client { + /// + /// The exception that is thrown when an operation is performed on an internal user that does not exist. + /// + public class UserNotFoundException : Exception { + /// + /// The login name of the user. + /// + public string LoginName { get; } + + /// + /// Constructs a new . + /// + /// + /// + public UserNotFoundException(string loginName, Exception? exception = null) + : base($"User '{loginName}' was not found.", exception) { + LoginName = loginName; + } + } +} diff --git a/src/Kurrent.Client/Core/Exceptions/WrongExpectedVersionException.cs b/src/Kurrent.Client/Core/Exceptions/WrongExpectedVersionException.cs new file mode 100644 index 000000000..ea2489e63 --- /dev/null +++ b/src/Kurrent.Client/Core/Exceptions/WrongExpectedVersionException.cs @@ -0,0 +1,66 @@ +using System; + +namespace EventStore.Client { + /// + /// Exception thrown if the expected version specified on an operation + /// does not match the version of the stream when the operation was attempted. + /// + public class WrongExpectedVersionException : Exception { + /// + /// The stream identifier. + /// + public string StreamName { get; } + + /// + /// If available, the expected version specified for the operation that failed. + /// + public long? ExpectedVersion { get; } + + /// + /// If available, the current version of the stream that the operation was attempted on. + /// + public long? ActualVersion { get; } + + /// + /// The current of the stream that the operation was attempted on. + /// + public StreamRevision ActualStreamRevision { get; } + + /// + /// If available, the expected version specified for the operation that failed. + /// + public StreamRevision ExpectedStreamRevision { get; } + + /// + /// Constructs a new instance of with the expected and actual versions if available. + /// + public WrongExpectedVersionException(string streamName, StreamRevision expectedStreamRevision, + StreamRevision actualStreamRevision, Exception? exception = null, string? message = null) : + base( + message ?? $"Append failed due to WrongExpectedVersion. Stream: {streamName}, Expected version: {expectedStreamRevision}, Actual version: {actualStreamRevision}", + exception) { + StreamName = streamName; + ActualStreamRevision = actualStreamRevision; + ExpectedStreamRevision = expectedStreamRevision; + ExpectedVersion = expectedStreamRevision == StreamRevision.None ? new long?() : expectedStreamRevision.ToInt64(); + ActualVersion = actualStreamRevision == StreamRevision.None ? new long?() : actualStreamRevision.ToInt64(); + } + + /// + /// Constructs a new instance of with the expected and actual versions if available. + /// + /// + /// + /// + /// + public WrongExpectedVersionException(string streamName, StreamState expectedStreamState, + StreamRevision actualStreamRevision, Exception? exception = null) : base( + $"Append failed due to WrongExpectedVersion. Stream: {streamName}, Expected state: {expectedStreamState}, Actual version: {actualStreamRevision}", + exception) { + StreamName = streamName; + ActualStreamRevision = actualStreamRevision; + ActualVersion = actualStreamRevision == StreamRevision.None ? new long?() : actualStreamRevision.ToInt64(); + ExpectedStreamRevision = StreamRevision.None; + } + } +} diff --git a/src/Kurrent.Client/Core/FromAll.cs b/src/Kurrent.Client/Core/FromAll.cs new file mode 100644 index 000000000..3be460659 --- /dev/null +++ b/src/Kurrent.Client/Core/FromAll.cs @@ -0,0 +1,100 @@ +using System; + +namespace EventStore.Client { + /// + /// A structure representing the logical position of a subscription to all. /> + /// + public readonly struct FromAll : IEquatable, IComparable, IComparable { + /// + /// Represents a when no events have been seen (i.e., the beginning). + /// + public static readonly FromAll Start = new(null); + + /// + /// Represents a to receive events written after the subscription is confirmed. + /// + public static readonly FromAll End = new(Position.End); + + /// + /// Returns a for the given . + /// + /// The . + /// + /// + public static FromAll After(Position position) => position == Position.End + ? throw new ArgumentException($"Use '{nameof(FromAll)}.{nameof(End)}.'", nameof(position)) + : new(position); + + private readonly Position? _value; + + private FromAll(Position? value) => _value = value; + + /// + /// Converts the to a . + /// It is not meant to be used directly from your code. + /// + /// + /// + public (ulong commitPosition, ulong preparePosition) ToUInt64() => this == Start + ? throw new InvalidOperationException( + $"{nameof(FromAll)}.{nameof(Start)} may not be converted.") + : (_value!.Value.CommitPosition, _value!.Value.PreparePosition); + + /// + public bool Equals(FromAll other) => Nullable.Equals(_value, other._value); + + /// + public override bool Equals(object? obj) => obj is FromAll other && Equals(other); + + /// + public override int GetHashCode() => _value.GetHashCode(); + +#pragma warning disable CS1591 + public static bool operator ==(FromAll left, FromAll right) => + Nullable.Equals(left, right); + + public static bool operator !=(FromAll left, FromAll right) => + !Nullable.Equals(left, right); + + public static bool operator >(FromAll left, FromAll right) => + left.CompareTo(right) > 0; + + public static bool operator <(FromAll left, FromAll right) => + left.CompareTo(right) < 0; + + public static bool operator >=(FromAll left, FromAll right) => + left.CompareTo(right) >= 0; + + public static bool operator <=(FromAll left, FromAll right) => + left.CompareTo(right) <= 0; +#pragma warning restore CS1591 + + /// + public int CompareTo(FromAll other) => (_value, other._value) switch { + (null, null) => 0, + (null, _) => -1, + (_, null) => 1, + _ => _value.Value.CompareTo(other._value.Value) + }; + + /// + public int CompareTo(object? obj) => obj switch { + null => 1, + FromAll other => CompareTo(other), + _ => throw new ArgumentException($"Object is not a {nameof(FromAll)}"), + }; + + /// + public override string ToString() { + if (_value is null) { + return "Start"; + } + + if (_value == Position.End) { + return "Live"; + } + + return _value.Value.ToString(); + } + } +} diff --git a/src/Kurrent.Client/Core/FromStream.cs b/src/Kurrent.Client/Core/FromStream.cs new file mode 100644 index 000000000..7cf99e081 --- /dev/null +++ b/src/Kurrent.Client/Core/FromStream.cs @@ -0,0 +1,100 @@ +using System; + +namespace EventStore.Client { + /// + /// A structure representing the logical position of a subscription. /> + /// + public readonly struct FromStream : IEquatable, IComparable, IComparable { + /// + /// Represents a when no events have been seen (i.e., the beginning). + /// + public static readonly FromStream Start = new(null); + + /// + /// Represents a to receive events written after the subscription is confirmed. + /// + public static readonly FromStream End = new(StreamPosition.End); + + private readonly StreamPosition? _value; + + /// + /// Returns a for the given . + /// + /// The . + /// + /// + public static FromStream After(StreamPosition streamPosition) => + streamPosition == StreamPosition.End + ? throw new ArgumentException($"Use '{nameof(FromStream)}.{nameof(End)}.'", nameof(streamPosition)) + : new(streamPosition); + + private FromStream(StreamPosition? value) => _value = value; + + /// + /// Converts the to a . It is not meant to be used directly from your code. + /// + /// + /// + public ulong ToUInt64() => this == Start + ? throw new InvalidOperationException( + $"{nameof(FromStream)}.{nameof(Start)} may not be converted.") + : _value!.Value.ToUInt64(); + + /// + public bool Equals(FromStream other) => Nullable.Equals(_value, other._value); + + /// + public override bool Equals(object? obj) => obj is FromStream other && Equals(other); + + /// + public override int GetHashCode() => _value.GetHashCode(); + +#pragma warning disable CS1591 + public static bool operator ==(FromStream left, FromStream right) => + left.Equals(right); + + public static bool operator !=(FromStream left, FromStream right) => + !left.Equals(right); + + public static bool operator <(FromStream left, FromStream right) => + left.CompareTo(right) < 0; + + public static bool operator >(FromStream left, FromStream right) => + left.CompareTo(right) > 0; + + public static bool operator <=(FromStream left, FromStream right) => + left.CompareTo(right) <= 0; + + public static bool operator >=(FromStream left, FromStream right) => + left.CompareTo(right) >= 0; +#pragma warning restore CS1591 + + /// + public int CompareTo(FromStream other) => (_value, other._value) switch { + (null, null) => 0, + (null, _) => -1, + (_, null) => 1, + _ => _value.Value.CompareTo(other._value.Value) + }; + + /// + public int CompareTo(object? obj) => obj switch { + null => 1, + FromStream other => CompareTo(other), + _ => throw new ArgumentException($"Object is not a {nameof(FromStream)}"), + }; + + /// + public override string ToString() { + if (_value is null) { + return "Start"; + } + + if (_value == StreamPosition.End) { + return "Live"; + } + + return _value.Value.ToString(); + } + } +} diff --git a/src/Kurrent.Client/Core/GossipChannelSelector.cs b/src/Kurrent.Client/Core/GossipChannelSelector.cs new file mode 100644 index 000000000..5471120b6 --- /dev/null +++ b/src/Kurrent.Client/Core/GossipChannelSelector.cs @@ -0,0 +1,99 @@ +using System; +using System.Linq; +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using Grpc.Core; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; + +namespace EventStore.Client { + // Thread safe + internal class GossipChannelSelector : IChannelSelector { + private readonly KurrentClientSettings _settings; + private readonly ChannelCache _channels; + private readonly IGossipClient _gossipClient; + private readonly ILogger _log; + private readonly NodeSelector _nodeSelector; + + public GossipChannelSelector( + KurrentClientSettings settings, + ChannelCache channelCache, + IGossipClient gossipClient) { + + _settings = settings; + _channels = channelCache; + _gossipClient = gossipClient; + _log = settings.LoggerFactory?.CreateLogger() ?? + new NullLogger(); + _nodeSelector = new(_settings); + } + + public ChannelBase SelectChannel(DnsEndPoint endPoint) { + return _channels.GetChannelInfo(endPoint); + } + + public async Task SelectChannelAsync(CancellationToken cancellationToken) { + var endPoint = await DiscoverAsync(cancellationToken).ConfigureAwait(false); + + _log.LogInformation("Successfully discovered candidate at {endPoint}.", endPoint); + + return _channels.GetChannelInfo(endPoint); + } + + private async Task DiscoverAsync(CancellationToken cancellationToken) { + for (var attempt = 1; attempt <= _settings.ConnectivitySettings.MaxDiscoverAttempts; attempt++) { + foreach (var kvp in _channels.GetRandomOrderSnapshot()) { + var endPointToGetGossip = kvp.Key; + var channelToGetGossip = kvp.Value; + + try { + var clusterInfo = await _gossipClient + .GetAsync(channelToGetGossip, cancellationToken) + .ConfigureAwait(false); + + var selectedEndpoint = _nodeSelector.SelectNode(clusterInfo); + + // Successfully selected an endpoint using this gossip! + // We want _channels to contain exactly the nodes in ClusterInfo. + // nodes no longer in the cluster can be forgotten. + // new nodes are added so we can use them to get gossip. + _channels.UpdateCache(clusterInfo.Members.Select(x => x.EndPoint)); + + return selectedEndpoint; + + } catch (Exception ex) { + _log.Log( + GetLogLevelForDiscoveryAttempt(attempt), + ex, + "Could not discover candidate from {endPoint}. Attempts remaining: {remainingAttempts}", + endPointToGetGossip, + _settings.ConnectivitySettings.MaxDiscoverAttempts - attempt); + } + } + + // couldn't select a node from any _channel. reseed the channels. + _channels.UpdateCache(_settings.ConnectivitySettings.GossipSeeds.Select(endPoint => + endPoint as DnsEndPoint ?? new DnsEndPoint(endPoint.GetHost(), endPoint.GetPort()))); + + await Task + .Delay(_settings.ConnectivitySettings.DiscoveryInterval, cancellationToken) + .ConfigureAwait(false); + } + + _log.LogError("Failed to discover candidate in {maxDiscoverAttempts} attempts.", + _settings.ConnectivitySettings.MaxDiscoverAttempts); + + throw new DiscoveryException(_settings.ConnectivitySettings.MaxDiscoverAttempts); + } + + private LogLevel GetLogLevelForDiscoveryAttempt(int attempt) => attempt switch { + _ when attempt == _settings.ConnectivitySettings.MaxDiscoverAttempts => + LogLevel.Error, + 1 => + LogLevel.Warning, + _ => + LogLevel.Debug + }; + } +} diff --git a/src/Kurrent.Client/Core/GrpcGossipClient.cs b/src/Kurrent.Client/Core/GrpcGossipClient.cs new file mode 100644 index 000000000..5291bfe6c --- /dev/null +++ b/src/Kurrent.Client/Core/GrpcGossipClient.cs @@ -0,0 +1,30 @@ +using System.Linq; +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using Grpc.Core; + +namespace EventStore.Client { + internal class GrpcGossipClient : IGossipClient { + private readonly KurrentClientSettings _settings; + + public GrpcGossipClient(KurrentClientSettings settings) { + _settings = settings; + } + + public async ValueTask GetAsync(ChannelBase channel, CancellationToken ct) { + var client = new Gossip.Gossip.GossipClient(channel); + using var call = client.ReadAsync( + new Empty(), + KurrentCallOptions.CreateNonStreaming(_settings, ct)); + var result = await call.ResponseAsync.ConfigureAwait(false); + + return new(result.Members.Select(x => + new ClusterMessages.MemberInfo( + Uuid.FromDto(x.InstanceId), + (ClusterMessages.VNodeState)x.State, + x.IsAlive, + new DnsEndPoint(x.HttpEndPoint.Address, (int)x.HttpEndPoint.Port))).ToArray()); + } + } +} diff --git a/src/Kurrent.Client/Core/GrpcServerCapabilitiesClient.cs b/src/Kurrent.Client/Core/GrpcServerCapabilitiesClient.cs new file mode 100644 index 000000000..1dc29dce0 --- /dev/null +++ b/src/Kurrent.Client/Core/GrpcServerCapabilitiesClient.cs @@ -0,0 +1,75 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Grpc.Core; + +namespace EventStore.Client { + internal class GrpcServerCapabilitiesClient : IServerCapabilitiesClient { + private readonly KurrentClientSettings _settings; + + public GrpcServerCapabilitiesClient(KurrentClientSettings settings) { + _settings = settings; + } + + public async Task GetAsync( + CallInvoker callInvoker, + CancellationToken cancellationToken) { + + var client = new ServerFeatures.ServerFeatures.ServerFeaturesClient(callInvoker); + using var call = client.GetSupportedMethodsAsync( + new(), + KurrentCallOptions.CreateNonStreaming( + _settings, + _settings.ConnectivitySettings.GossipTimeout, + null, + cancellationToken)); + + try { + var supportsBatchAppend = false; + var supportsPersistentSubscriptionsToAll = false; + var supportsPersistentSubscriptionsGetInfo = false; + var supportsPersistentSubscriptionsRestartSubsystem = false; + var supportsPersistentSubscriptionsReplayParked = false; + var supportsPersistentSubscriptionsList = false; + + var response = await call.ResponseAsync.ConfigureAwait(false); + + foreach (var supportedMethod in response.Methods) { + switch (supportedMethod.ServiceName, supportedMethod.MethodName) { + case ("event_store.client.streams.streams", "batchappend"): + supportsBatchAppend = true; + continue; + case ("event_store.client.persistent_subscriptions.persistentsubscriptions", "read"): + supportsPersistentSubscriptionsToAll = supportedMethod.Features.Contains("all"); + continue; + case ("event_store.client.persistent_subscriptions.persistentsubscriptions", "getinfo"): + supportsPersistentSubscriptionsGetInfo = true; + continue; + case ("event_store.client.persistent_subscriptions.persistentsubscriptions", "restartsubsystem"): + supportsPersistentSubscriptionsRestartSubsystem = true; + continue; + case ("event_store.client.persistent_subscriptions.persistentsubscriptions", "replayparked"): + supportsPersistentSubscriptionsReplayParked = true; + continue; + case ("event_store.client.persistent_subscriptions.persistentsubscriptions", "list"): + supportsPersistentSubscriptionsList = true; + continue; + } + } + + return new( + SupportsBatchAppend: supportsBatchAppend, + SupportsPersistentSubscriptionsToAll: supportsPersistentSubscriptionsToAll, + SupportsPersistentSubscriptionsGetInfo: supportsPersistentSubscriptionsGetInfo, + SupportsPersistentSubscriptionsRestartSubsystem: supportsPersistentSubscriptionsRestartSubsystem, + SupportsPersistentSubscriptionsReplayParked: supportsPersistentSubscriptionsReplayParked, + SupportsPersistentSubscriptionsList: supportsPersistentSubscriptionsList); + + } catch (Exception ex) when (ex.GetBaseException() is RpcException rpcException && + rpcException.StatusCode == StatusCode.Unimplemented) { + + return new(); + } + } + } +} diff --git a/src/Kurrent.Client/Core/HashCode.cs b/src/Kurrent.Client/Core/HashCode.cs new file mode 100644 index 000000000..31f9c2514 --- /dev/null +++ b/src/Kurrent.Client/Core/HashCode.cs @@ -0,0 +1,37 @@ +using System.Collections.Generic; +using System.Linq; + +namespace EventStore.Client { + #pragma warning disable 1591 + public readonly struct HashCode { + private readonly int _value; + + private HashCode(int value) { + _value = value; + } + + public static readonly HashCode Hash = default; + + public HashCode Combine(T? value) where T : struct => Combine(value ?? default); + + public HashCode Combine(T value) where T: struct { + unchecked { + return new HashCode((_value * 397) ^ value.GetHashCode()); + } + } + + public HashCode Combine(string? value){ + unchecked { + return new HashCode((_value * 397) ^ (value?.GetHashCode() ?? 0)); + } + } + + public HashCode Combine(IEnumerable? values) where T: struct => + (values ?? Enumerable.Empty()).Aggregate(Hash, (previous, value) => previous.Combine(value)); + + public HashCode Combine(IEnumerable? values) => + (values ?? Enumerable.Empty()).Aggregate(Hash, (previous, value) => previous.Combine(value)); + + public static implicit operator int(HashCode value) => value._value; + } +} diff --git a/src/Kurrent.Client/Core/HttpFallback.cs b/src/Kurrent.Client/Core/HttpFallback.cs new file mode 100644 index 000000000..0f2bfe02d --- /dev/null +++ b/src/Kurrent.Client/Core/HttpFallback.cs @@ -0,0 +1,143 @@ +using System; +using System.Net; +using System.Net.Http; +using System.Security.Cryptography.X509Certificates; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; + +namespace EventStore.Client { + internal class HttpFallback : IDisposable { + private readonly HttpClient _httpClient; + private readonly JsonSerializerOptions _jsonSettings; + private readonly UserCredentials? _defaultCredentials; + private readonly string _addressScheme; + + internal HttpFallback(KurrentClientSettings settings) { + _addressScheme = settings.ConnectivitySettings.ResolvedAddressOrDefault.Scheme; + _defaultCredentials = settings.DefaultCredentials; + + var handler = new HttpClientHandler(); + if (!settings.ConnectivitySettings.Insecure) { + handler.ClientCertificateOptions = ClientCertificateOption.Manual; + + if (settings.ConnectivitySettings.ClientCertificate is not null) + handler.ClientCertificates.Add(settings.ConnectivitySettings.ClientCertificate); + + handler.ServerCertificateCustomValidationCallback = settings.ConnectivitySettings.TlsVerifyCert switch { + false => delegate { return true; }, + true when settings.ConnectivitySettings.TlsCaFile is not null => (sender, certificate, chain, errors) => { + if (certificate is null || chain is null) return false; + + chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck; + +#if NET48 + chain.ChainPolicy.ExtraStore.Add(settings.ConnectivitySettings.TlsCaFile); +#else + chain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust; + chain.ChainPolicy.CustomTrustStore.Add(settings.ConnectivitySettings.TlsCaFile); +#endif + + return chain.Build(certificate); + }, + _ => null + }; + } + + _httpClient = new HttpClient(handler); + if (settings.DefaultDeadline.HasValue) { + _httpClient.Timeout = settings.DefaultDeadline.Value; + } + + _jsonSettings = new JsonSerializerOptions { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }; + } + + internal async Task HttpGetAsync(string path, ChannelInfo channelInfo, TimeSpan? deadline, + UserCredentials? userCredentials, Action onNotFound, CancellationToken cancellationToken) { + + var request = CreateRequest(path, HttpMethod.Get, channelInfo, userCredentials); + + var httpResult = await HttpSendAsync(request, onNotFound, deadline, cancellationToken).ConfigureAwait(false); + +#if NET + var json = await httpResult.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); +#else + var json = await httpResult.Content.ReadAsStringAsync().ConfigureAwait(false); +#endif + + var result = JsonSerializer.Deserialize(json, _jsonSettings); + if (result == null) { + throw new InvalidOperationException("Unable to deserialize response into object of type " + typeof(T)); + } + + return result; + } + + internal async Task HttpPostAsync(string path, string query, ChannelInfo channelInfo, TimeSpan? deadline, + UserCredentials? userCredentials, Action onNotFound, CancellationToken cancellationToken) { + + var request = CreateRequest(path, query, HttpMethod.Post, channelInfo, userCredentials); + + await HttpSendAsync(request, onNotFound, deadline, cancellationToken).ConfigureAwait(false); + } + + private async Task HttpSendAsync(HttpRequestMessage request, Action onNotFound, + TimeSpan? deadline, CancellationToken cancellationToken) { + + if (!deadline.HasValue) { + return await HttpSendAsync(request, onNotFound, cancellationToken).ConfigureAwait(false); + } + + using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + cts.CancelAfter(deadline.Value); + + return await HttpSendAsync(request, onNotFound, cts.Token).ConfigureAwait(false); + } + + async Task HttpSendAsync(HttpRequestMessage request, Action onNotFound, + CancellationToken cancellationToken) { + + var httpResult = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false); + if (httpResult.IsSuccessStatusCode) { + return httpResult; + } + + if (httpResult.StatusCode == HttpStatusCode.Unauthorized) { + throw new AccessDeniedException(); + } + + if (httpResult.StatusCode == HttpStatusCode.NotFound) { + onNotFound(); + } + + throw new Exception($"The HTTP request failed with status code: {httpResult.StatusCode}"); + } + + private HttpRequestMessage CreateRequest(string path, HttpMethod method, ChannelInfo channelInfo, + UserCredentials? credentials) => CreateRequest(path, query: "", method, channelInfo, credentials); + + private HttpRequestMessage CreateRequest(string path, string query, HttpMethod method, ChannelInfo channelInfo, + UserCredentials? credentials) { + + var uriBuilder = new UriBuilder($"{_addressScheme}://{channelInfo.Channel.Target}") { + Path = path, + Query = query + }; + + var httpRequest = new HttpRequestMessage(method, uriBuilder.Uri); + httpRequest.Headers.Add("accept", "application/json"); + credentials ??= _defaultCredentials; + if (credentials != null) { + httpRequest.Headers.Add(Constants.Headers.Authorization, credentials.ToString()); + } + + return httpRequest; + } + + public void Dispose() { + _httpClient.Dispose(); + } + } +} diff --git a/src/Kurrent.Client/Core/IChannelSelector.cs b/src/Kurrent.Client/Core/IChannelSelector.cs new file mode 100644 index 000000000..1765ccebc --- /dev/null +++ b/src/Kurrent.Client/Core/IChannelSelector.cs @@ -0,0 +1,14 @@ +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using Grpc.Core; + +namespace EventStore.Client { + internal interface IChannelSelector { + // Let the channel selector pick an endpoint. + Task SelectChannelAsync(CancellationToken cancellationToken); + + // Get a channel for the specified endpoint + ChannelBase SelectChannel(DnsEndPoint endPoint); + } +} diff --git a/src/Kurrent.Client/Core/IEventFilter.cs b/src/Kurrent.Client/Core/IEventFilter.cs new file mode 100644 index 000000000..eb290685e --- /dev/null +++ b/src/Kurrent.Client/Core/IEventFilter.cs @@ -0,0 +1,21 @@ +namespace EventStore.Client { + /// + /// An interface that represents a search filter, used for read operations. + /// + public interface IEventFilter { + /// + /// The s associated with this . + /// + PrefixFilterExpression[] Prefixes { get; } + + /// + /// The associated with this . + /// + RegularFilterExpression Regex { get; } + + /// + /// The maximum number of events to read that do not match the filter before the operation returns. + /// + uint? MaxSearchWindow { get; } + } +} diff --git a/src/Kurrent.Client/Core/IGossipClient.cs b/src/Kurrent.Client/Core/IGossipClient.cs new file mode 100644 index 000000000..80c0e9395 --- /dev/null +++ b/src/Kurrent.Client/Core/IGossipClient.cs @@ -0,0 +1,10 @@ +using System.Threading; +using System.Threading.Tasks; +using Grpc.Core; + +namespace EventStore.Client { + internal interface IGossipClient { + public ValueTask GetAsync(ChannelBase channel, + CancellationToken cancellationToken); + } +} diff --git a/src/Kurrent.Client/Core/IPosition.cs b/src/Kurrent.Client/Core/IPosition.cs new file mode 100644 index 000000000..e3c5da9bc --- /dev/null +++ b/src/Kurrent.Client/Core/IPosition.cs @@ -0,0 +1,7 @@ +namespace EventStore.Client { + /// + /// Represents the position in a stream or transaction file + /// + public interface IPosition { + } +} diff --git a/src/Kurrent.Client/Core/IServerCapabilitiesClient.cs b/src/Kurrent.Client/Core/IServerCapabilitiesClient.cs new file mode 100644 index 000000000..20943805d --- /dev/null +++ b/src/Kurrent.Client/Core/IServerCapabilitiesClient.cs @@ -0,0 +1,9 @@ +using System.Threading; +using System.Threading.Tasks; +using Grpc.Core; + +namespace EventStore.Client { + internal interface IServerCapabilitiesClient { + public Task GetAsync(CallInvoker callInvoker, CancellationToken cancellationToken); + } +} diff --git a/src/Kurrent.Client/Core/Interceptors/ConnectionNameInterceptor.cs b/src/Kurrent.Client/Core/Interceptors/ConnectionNameInterceptor.cs new file mode 100644 index 000000000..d709a2a3b --- /dev/null +++ b/src/Kurrent.Client/Core/Interceptors/ConnectionNameInterceptor.cs @@ -0,0 +1,45 @@ +using Grpc.Core; +using Grpc.Core.Interceptors; + +namespace EventStore.Client.Interceptors { + internal class ConnectionNameInterceptor : Interceptor { + private readonly string _connectionName; + + public ConnectionNameInterceptor(string connectionName) { + _connectionName = connectionName; + } + + public override AsyncUnaryCall AsyncUnaryCall(TRequest request, + ClientInterceptorContext context, + AsyncUnaryCallContinuation continuation) { + AddConnectionName(context); + return continuation(request, context); + } + + public override AsyncClientStreamingCall AsyncClientStreamingCall( + ClientInterceptorContext context, + AsyncClientStreamingCallContinuation continuation) { + AddConnectionName(context); + return continuation(context); + } + + public override AsyncServerStreamingCall AsyncServerStreamingCall( + TRequest request, + ClientInterceptorContext context, + AsyncServerStreamingCallContinuation continuation) { + AddConnectionName(context); + return continuation(request, context); + } + + public override AsyncDuplexStreamingCall AsyncDuplexStreamingCall( + ClientInterceptorContext context, + AsyncDuplexStreamingCallContinuation continuation) { + AddConnectionName(context); + return continuation(context); + } + + private void AddConnectionName(ClientInterceptorContext context) + where TRequest : class where TResponse : class => + context.Options.Headers?.Add(Constants.Headers.ConnectionName, _connectionName); + } +} diff --git a/src/Kurrent.Client/Core/Interceptors/ReportLeaderInterceptor.cs b/src/Kurrent.Client/Core/Interceptors/ReportLeaderInterceptor.cs new file mode 100644 index 000000000..6d9327858 --- /dev/null +++ b/src/Kurrent.Client/Core/Interceptors/ReportLeaderInterceptor.cs @@ -0,0 +1,126 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Grpc.Core; +using Grpc.Core.Interceptors; + +namespace EventStore.Client.Interceptors { + // this has become more general than just detecting leader changes. + // triggers the action on any rpc exception with StatusCode.Unavailable + internal class ReportLeaderInterceptor : Interceptor { + private readonly Action _onReconnectionRequired; + + private const TaskContinuationOptions ContinuationOptions = + TaskContinuationOptions.ExecuteSynchronously | TaskContinuationOptions.OnlyOnFaulted; + + internal ReportLeaderInterceptor(Action onReconnectionRequired) { + _onReconnectionRequired = onReconnectionRequired; + } + + public override AsyncUnaryCall AsyncUnaryCall(TRequest request, + ClientInterceptorContext context, + AsyncUnaryCallContinuation continuation) { + var response = continuation(request, context); + + response.ResponseAsync.ContinueWith(OnReconnectionRequired, ContinuationOptions); + + return new AsyncUnaryCall(response.ResponseAsync, response.ResponseHeadersAsync, + response.GetStatus, response.GetTrailers, response.Dispose); + } + + public override AsyncClientStreamingCall AsyncClientStreamingCall( + ClientInterceptorContext context, + AsyncClientStreamingCallContinuation continuation) { + var response = continuation(context); + + response.ResponseAsync.ContinueWith(OnReconnectionRequired, ContinuationOptions); + + return new AsyncClientStreamingCall( + new StreamWriter(response.RequestStream, OnReconnectionRequired), + response.ResponseAsync, + response.ResponseHeadersAsync, response.GetStatus, response.GetTrailers, response.Dispose); + } + + public override AsyncDuplexStreamingCall AsyncDuplexStreamingCall( + ClientInterceptorContext context, + AsyncDuplexStreamingCallContinuation continuation) { + var response = continuation(context); + + return new AsyncDuplexStreamingCall( + new StreamWriter(response.RequestStream, OnReconnectionRequired), + new StreamReader(response.ResponseStream, OnReconnectionRequired), + response.ResponseHeadersAsync, + response.GetStatus, response.GetTrailers, response.Dispose); + } + + public override AsyncServerStreamingCall AsyncServerStreamingCall( + TRequest request, ClientInterceptorContext context, + AsyncServerStreamingCallContinuation continuation) { + var response = continuation(request, context); + + return new AsyncServerStreamingCall( + new StreamReader(response.ResponseStream, OnReconnectionRequired), + response.ResponseHeadersAsync, + response.GetStatus, response.GetTrailers, response.Dispose); + } + + private void OnReconnectionRequired(Task task) { + ReconnectionRequired reconnectionRequired = task.Exception?.InnerException switch { + NotLeaderException ex => new ReconnectionRequired.NewLeader(ex.LeaderEndpoint), + RpcException { + StatusCode: StatusCode.Unavailable + // or StatusCode.Unknown or TODO: use RPC exceptions on server + } => ReconnectionRequired.Rediscover.Instance, + _ => ReconnectionRequired.None.Instance + }; + + if (reconnectionRequired is not ReconnectionRequired.None) + _onReconnectionRequired(reconnectionRequired); + } + + private class StreamWriter : IClientStreamWriter { + private readonly IClientStreamWriter _inner; + private readonly Action _reportNewLeader; + + public StreamWriter(IClientStreamWriter inner, Action reportNewLeader) { + _inner = inner; + _reportNewLeader = reportNewLeader; + } + + public WriteOptions? WriteOptions { + get => _inner.WriteOptions; + set => _inner.WriteOptions = value; + } + + public Task CompleteAsync() { + var task = _inner.CompleteAsync(); + task.ContinueWith(_reportNewLeader, ContinuationOptions); + return task; + } + + public Task WriteAsync(T message) { + var task = _inner.WriteAsync(message); + task.ContinueWith(_reportNewLeader, ContinuationOptions); + return task; + } + } + + private class StreamReader : IAsyncStreamReader { + private readonly IAsyncStreamReader _inner; + private readonly Action _reportNewLeader; + + public StreamReader(IAsyncStreamReader inner, Action reportNewLeader) { + _inner = inner; + _reportNewLeader = reportNewLeader; + } + + public Task MoveNext(CancellationToken cancellationToken) { + var task = _inner.MoveNext(cancellationToken); + task.ContinueWith(_reportNewLeader, ContinuationOptions); + return task; + } + + public T Current => _inner.Current; + } + } +} diff --git a/src/Kurrent.Client/Core/Interceptors/TypedExceptionInterceptor.cs b/src/Kurrent.Client/Core/Interceptors/TypedExceptionInterceptor.cs new file mode 100644 index 000000000..0f1368805 --- /dev/null +++ b/src/Kurrent.Client/Core/Interceptors/TypedExceptionInterceptor.cs @@ -0,0 +1,165 @@ +using Grpc.Core; +using Grpc.Core.Interceptors; +using static EventStore.Client.Constants; +using static Grpc.Core.StatusCode; + +namespace EventStore.Client.Interceptors; + +class TypedExceptionInterceptor : Interceptor { + static readonly Dictionary> DefaultExceptionMap = new() { + [Exceptions.AccessDenied] = ex => ex.ToAccessDeniedException(), + [Exceptions.NotLeader] = ex => ex.ToNotLeaderException(), + }; + + public TypedExceptionInterceptor(Dictionary> customExceptionMap) { +#if NET48 + var map = new Dictionary>(DefaultExceptionMap.Concat(customExceptionMap).ToDictionary(x => x.Key, x => x.Value)); +#else + var map = new Dictionary>(DefaultExceptionMap.Concat(customExceptionMap)); +#endif + ConvertRpcException = rpcEx => { + if (rpcEx.TryMapException(map, out var ex)) + throw ex; + + throw rpcEx.StatusCode switch { + Unavailable when rpcEx.Status.Detail == "Deadline Exceeded" => rpcEx.ToDeadlineExceededRpcException(), + Unauthenticated => rpcEx.ToNotAuthenticatedException(), + _ => rpcEx + }; + }; + } + + Func ConvertRpcException { get; } + + public override AsyncServerStreamingCall AsyncServerStreamingCall( + TRequest request, + ClientInterceptorContext context, + AsyncServerStreamingCallContinuation continuation + ) { + var response = continuation(request, context); + + return new AsyncServerStreamingCall( + response.ResponseStream.Apply(ConvertRpcException), + response.ResponseHeadersAsync, + response.GetStatus, + response.GetTrailers, + response.Dispose + ); + } + + public override AsyncClientStreamingCall AsyncClientStreamingCall( + ClientInterceptorContext context, + AsyncClientStreamingCallContinuation continuation + ) { + var response = continuation(context); + + return new AsyncClientStreamingCall( + response.RequestStream.Apply(ConvertRpcException), + response.ResponseAsync.Apply(ConvertRpcException), + response.ResponseHeadersAsync, + response.GetStatus, + response.GetTrailers, + response.Dispose + ); + } + + public override AsyncUnaryCall AsyncUnaryCall( + TRequest request, + ClientInterceptorContext context, + AsyncUnaryCallContinuation continuation + ) { + var response = continuation(request, context); + + return new AsyncUnaryCall( + response.ResponseAsync.Apply(ConvertRpcException), + response.ResponseHeadersAsync, + response.GetStatus, + response.GetTrailers, + response.Dispose + ); + } + + public override AsyncDuplexStreamingCall AsyncDuplexStreamingCall( + ClientInterceptorContext context, + AsyncDuplexStreamingCallContinuation continuation + ) { + var response = continuation(context); + + return new AsyncDuplexStreamingCall( + response.RequestStream, + response.ResponseStream.Apply(ConvertRpcException), + response.ResponseHeadersAsync, + response.GetStatus, + response.GetTrailers, + response.Dispose + ); + } +} + +static class RpcExceptionConversionExtensions { + public static IAsyncStreamReader Apply(this IAsyncStreamReader reader, Func convertException) => + new ExceptionConverterStreamReader(reader, convertException); + + public static Task Apply(this Task task, Func convertException) => + task.ContinueWith(t => t.Exception?.InnerException is RpcException ex ? throw convertException(ex) : t.Result); + + public static IClientStreamWriter Apply( + this IClientStreamWriter writer, Func convertException + ) => + new ExceptionConverterStreamWriter(writer, convertException); + + public static Task Apply(this Task task, Func convertException) => + task.ContinueWith(t => t.Exception?.InnerException is RpcException ex ? throw convertException(ex) : t); + + public static AccessDeniedException ToAccessDeniedException(this RpcException exception) => + new(exception.Message, exception); + + public static NotLeaderException ToNotLeaderException(this RpcException exception) { + var host = exception.Trailers.FirstOrDefault(x => x.Key == Exceptions.LeaderEndpointHost)?.Value!; + var port = exception.Trailers.GetIntValueOrDefault(Exceptions.LeaderEndpointPort); + return new NotLeaderException(host, port, exception); + } + + public static NotAuthenticatedException ToNotAuthenticatedException(this RpcException exception) => + new(exception.Message, exception); + + public static RpcException ToDeadlineExceededRpcException(this RpcException exception) => + new(new Status(DeadlineExceeded, exception.Status.Detail, exception.Status.DebugException)); + + public static bool TryMapException(this RpcException exception, Dictionary> map, out Exception createdException) { + if (exception.Trailers.TryGetValue(Exceptions.ExceptionKey, out var key) && map.TryGetValue(key!, out var factory)) { + createdException = factory.Invoke(exception); + return true; + } + + createdException = null!; + return false; + } +} + +class ExceptionConverterStreamReader(IAsyncStreamReader reader, Func convertException) : IAsyncStreamReader { + public TResponse Current => reader.Current; + + public async Task MoveNext(CancellationToken cancellationToken) { + try { + return await reader.MoveNext(cancellationToken).ConfigureAwait(false); + } + catch (RpcException ex) { + throw convertException(ex); + } + } +} + +class ExceptionConverterStreamWriter( + IClientStreamWriter writer, + Func convertException +) + : IClientStreamWriter { + public WriteOptions? WriteOptions { + get => writer.WriteOptions; + set => writer.WriteOptions = value; + } + + public Task WriteAsync(TRequest message) => writer.WriteAsync(message).Apply(convertException); + public Task CompleteAsync() => writer.CompleteAsync().Apply(convertException); +} diff --git a/src/Kurrent.Client/Core/KurrentClientBase.cs b/src/Kurrent.Client/Core/KurrentClientBase.cs new file mode 100644 index 000000000..923c6f334 --- /dev/null +++ b/src/Kurrent.Client/Core/KurrentClientBase.cs @@ -0,0 +1,151 @@ +using EventStore.Client.Interceptors; +using Grpc.Core; +using Grpc.Core.Interceptors; +using Enum = System.Enum; + +namespace EventStore.Client { + /// + /// The base class used by clients used to communicate with the KurrentDB. + /// + public abstract class KurrentClientBase : + IDisposable, // for grpc.net we can dispose synchronously, but not for grpc.core + IAsyncDisposable { + private readonly Dictionary> _exceptionMap; + private readonly CancellationTokenSource _cts; + private readonly ChannelCache _channelCache; + private readonly SharingProvider _channelInfoProvider; + private readonly Lazy _httpFallback; + + /// The name of the connection. + public string ConnectionName { get; } + + /// The . + protected KurrentClientSettings Settings { get; } + + /// Constructs a new . + protected KurrentClientBase( + KurrentClientSettings? settings, + Dictionary> exceptionMap + ) { + Settings = settings ?? new KurrentClientSettings(); + _exceptionMap = exceptionMap; + _cts = new CancellationTokenSource(); + _channelCache = new(Settings); + _httpFallback = new Lazy(() => new HttpFallback(Settings)); + + ConnectionName = Settings.ConnectionName ?? $"ES-{Guid.NewGuid()}"; + + var channelSelector = new ChannelSelector(Settings, _channelCache); + _channelInfoProvider = new SharingProvider( + factory: (endPoint, onBroken) => + GetChannelInfoExpensive(endPoint, onBroken, channelSelector, _cts.Token), + factoryRetryDelay: Settings.ConnectivitySettings.DiscoveryInterval, + initialInput: ReconnectionRequired.Rediscover.Instance, + loggerFactory: Settings.LoggerFactory + ); + } + + // Select a channel and query its capabilities. This is an expensive call that + // we don't want to do often. + private async Task GetChannelInfoExpensive( + ReconnectionRequired reconnectionRequired, + Action onReconnectionRequired, + IChannelSelector channelSelector, + CancellationToken cancellationToken + ) { + var channel = reconnectionRequired switch { + ReconnectionRequired.Rediscover => await channelSelector.SelectChannelAsync(cancellationToken) + .ConfigureAwait(false), + ReconnectionRequired.NewLeader (var endPoint) => channelSelector.SelectChannel(endPoint), + _ => throw new ArgumentException(null, nameof(reconnectionRequired)) + }; + + var invoker = channel.CreateCallInvoker() + .Intercept(new TypedExceptionInterceptor(_exceptionMap)) + .Intercept(new ConnectionNameInterceptor(ConnectionName)) + .Intercept(new ReportLeaderInterceptor(onReconnectionRequired)); + + if (Settings.Interceptors is not null) { + foreach (var interceptor in Settings.Interceptors) { + invoker = invoker.Intercept(interceptor); + } + } + + var caps = await new GrpcServerCapabilitiesClient(Settings) + .GetAsync(invoker, cancellationToken) + .ConfigureAwait(false); + + return new(channel, caps, invoker); + } + + /// Gets the current channel info. + protected async ValueTask GetChannelInfo(CancellationToken cancellationToken) => + await _channelInfoProvider.CurrentAsync.WithCancellation(cancellationToken).ConfigureAwait(false); + + /// + /// Only exists so that we can manually trigger rediscovery in the tests + /// in cases where the server doesn't yet let the client know that it needs to. + /// note if rediscovery is already in progress it will continue, not restart. + /// + internal Task RediscoverAsync() { + _channelInfoProvider.Reset(); + return Task.CompletedTask; + } + + /// Returns the result of an HTTP Get request based on the client settings. + protected async Task HttpGet( + string path, Action onNotFound, ChannelInfo channelInfo, + TimeSpan? deadline, UserCredentials? userCredentials, CancellationToken cancellationToken + ) { + return await _httpFallback.Value + .HttpGetAsync(path, channelInfo, deadline, userCredentials, onNotFound, cancellationToken) + .ConfigureAwait(false); + } + + /// Executes an HTTP Post request based on the client settings. + protected async Task HttpPost( + string path, string query, Action onNotFound, ChannelInfo channelInfo, + TimeSpan? deadline, UserCredentials? userCredentials, CancellationToken cancellationToken + ) { + await _httpFallback.Value + .HttpPostAsync( + path, + query, + channelInfo, + deadline, + userCredentials, + onNotFound, + cancellationToken + ) + .ConfigureAwait(false); + } + + /// + public virtual void Dispose() { + _channelInfoProvider.Dispose(); + _cts.Cancel(); + _cts.Dispose(); + _channelCache.Dispose(); + + if (_httpFallback.IsValueCreated) { + _httpFallback.Value.Dispose(); + } + } + + /// + public virtual async ValueTask DisposeAsync() { + _channelInfoProvider.Dispose(); + _cts.Cancel(); + _cts.Dispose(); + await _channelCache.DisposeAsync().ConfigureAwait(false); + + if (_httpFallback.IsValueCreated) { + _httpFallback.Value.Dispose(); + } + } + + /// Returns an InvalidOperation exception. + protected Exception InvalidOption(T option) where T : Enum => + new InvalidOperationException($"The {typeof(T).Name} {option:x} was not valid."); + } +} diff --git a/src/Kurrent.Client/Core/KurrentClientConnectivitySettings.cs b/src/Kurrent.Client/Core/KurrentClientConnectivitySettings.cs new file mode 100644 index 000000000..4ea90dcaf --- /dev/null +++ b/src/Kurrent.Client/Core/KurrentClientConnectivitySettings.cs @@ -0,0 +1,128 @@ +using System; +using System.Net; +using System.Security.Cryptography.X509Certificates; + +namespace EventStore.Client { + /// + /// A class used to describe how to connect to an instance of KurrentDB. + /// + public class KurrentClientConnectivitySettings { + private const int DefaultPort = 2113; + private bool _insecure; + private Uri? _address; + + /// + /// The of the KurrentDB. Use this when connecting to a single node. + /// + public Uri? Address { + get => IsSingleNode ? _address : null; + set => _address = value; + } + + internal Uri ResolvedAddressOrDefault => Address ?? DefaultAddress; + + private Uri DefaultAddress => + new UriBuilder { + Scheme = _insecure ? Uri.UriSchemeHttp : Uri.UriSchemeHttps, + Port = DefaultPort + }.Uri; + + /// + /// The maximum number of times to attempt discovery. + /// + public int MaxDiscoverAttempts { get; set; } + + /// + /// An array of s used to seed gossip. + /// + public EndPoint[] GossipSeeds => + ((object?)DnsGossipSeeds ?? IpGossipSeeds) switch { + DnsEndPoint[] dns => Array.ConvertAll(dns, x => x), + IPEndPoint[] ip => Array.ConvertAll(ip, x => x), + _ => Array.Empty() + }; + + /// + /// An array of s to use for seeding gossip. This will be checked before . + /// + public DnsEndPoint[]? DnsGossipSeeds { get; set; } + + /// + /// An array of s to use for seeding gossip. This will be checked after . + /// + public IPEndPoint[]? IpGossipSeeds { get; set; } + + /// + /// The after which an attempt to discover gossip will fail. + /// + public TimeSpan GossipTimeout { get; set; } + + /// + /// Whether or not to use HTTPS when communicating via gossip. + /// + [Obsolete] + public bool GossipOverHttps { get; set; } = true; + + /// + /// The polling interval used to discover the . + /// + public TimeSpan DiscoveryInterval { get; set; } + + /// + /// The to use when connecting. + /// + public NodePreference NodePreference { get; set; } + + /// + /// The optional amount of time to wait after which a keepalive ping is sent on the transport. + /// + public TimeSpan KeepAliveInterval { get; set; } = TimeSpan.FromSeconds(10); + + /// + /// The optional amount of time to wait after which a sent keepalive ping is considered timed out. + /// + public TimeSpan KeepAliveTimeout { get; set; } = TimeSpan.FromSeconds(10); + + /// + /// True if pointing to a single KurrentDB node. + /// + public bool IsSingleNode => GossipSeeds.Length == 0; + + /// + /// True if communicating over an insecure channel; otherwise false. + /// + public bool Insecure { + get => IsSingleNode ? string.Equals(Address?.Scheme, Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase) : _insecure; + set => _insecure = value; + } + + /// + /// True if certificates will be validated; otherwise false. + /// + public bool TlsVerifyCert { get; set; } = true; + + /// + /// Path to a certificate file for secure connection. Not required for enabling secure connection. Useful for self-signed certificate + /// that are not installed on the system trust store. + /// + public X509Certificate2? TlsCaFile { get; set; } + + /// + /// Client certificate used for user authentication. + /// + public X509Certificate2? ClientCertificate { get; set; } + + /// + /// The default . + /// + public static KurrentClientConnectivitySettings Default => new KurrentClientConnectivitySettings { + MaxDiscoverAttempts = 10, + GossipTimeout = TimeSpan.FromSeconds(5), + DiscoveryInterval = TimeSpan.FromMilliseconds(100), + NodePreference = NodePreference.Leader, + KeepAliveInterval = TimeSpan.FromSeconds(10), + KeepAliveTimeout = TimeSpan.FromSeconds(10), + TlsVerifyCert = true, + }; + } +} diff --git a/src/Kurrent.Client/Core/KurrentClientOperationOptions.cs b/src/Kurrent.Client/Core/KurrentClientOperationOptions.cs new file mode 100644 index 000000000..36c8d5869 --- /dev/null +++ b/src/Kurrent.Client/Core/KurrentClientOperationOptions.cs @@ -0,0 +1,46 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace EventStore.Client { + /// + /// A class representing the options to apply to an individual operation. + /// + public class KurrentClientOperationOptions { + /// + /// Whether or not to immediately throw a when an append fails. + /// + public bool ThrowOnAppendFailure { get; set; } + + /// + /// The batch size, in bytes. + /// + public int BatchAppendSize { get; set; } + + /// + /// A callback function to extract the authorize header value from the used in the operation. + /// + public Func> GetAuthenticationHeaderValue { get; set; } = + null!; + + /// + /// The default . + /// + public static KurrentClientOperationOptions Default => new() { + ThrowOnAppendFailure = true, + GetAuthenticationHeaderValue = (userCredentials, _) => new ValueTask(userCredentials.ToString()), + BatchAppendSize = 3 * 1024 * 1024 + }; + + + /// + /// Clones a copy of the current . + /// + /// + public KurrentClientOperationOptions Clone() => new() { + ThrowOnAppendFailure = ThrowOnAppendFailure, + GetAuthenticationHeaderValue = GetAuthenticationHeaderValue, + BatchAppendSize = BatchAppendSize + }; + } +} diff --git a/src/Kurrent.Client/Core/KurrentClientSettings.ConnectionString.cs b/src/Kurrent.Client/Core/KurrentClientSettings.ConnectionString.cs new file mode 100644 index 000000000..307794019 --- /dev/null +++ b/src/Kurrent.Client/Core/KurrentClientSettings.ConnectionString.cs @@ -0,0 +1,405 @@ +using System.Net; +using System.Net.Http; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using Timeout_ = System.Threading.Timeout; + +namespace EventStore.Client { + public partial class KurrentClientSettings { + /// + /// Creates client settings from a connection string + /// + /// + /// + public static KurrentClientSettings Create(string connectionString) => + ConnectionStringParser.Parse(connectionString); + + private static class ConnectionStringParser { + private const string SchemeSeparator = "://"; + private const string UserInfoSeparator = "@"; + private const string Colon = ":"; + private const string Slash = "/"; + private const string Comma = ","; + private const string Ampersand = "&"; + private const string Equal = "="; + private const string QuestionMark = "?"; + + private const string Tls = nameof(Tls); + private const string ConnectionName = nameof(ConnectionName); + private const string MaxDiscoverAttempts = nameof(MaxDiscoverAttempts); + private const string DiscoveryInterval = nameof(DiscoveryInterval); + private const string GossipTimeout = nameof(GossipTimeout); + private const string NodePreference = nameof(NodePreference); + private const string TlsVerifyCert = nameof(TlsVerifyCert); + private const string TlsCaFile = nameof(TlsCaFile); + private const string DefaultDeadline = nameof(DefaultDeadline); + private const string ThrowOnAppendFailure = nameof(ThrowOnAppendFailure); + private const string KeepAliveInterval = nameof(KeepAliveInterval); + private const string KeepAliveTimeout = nameof(KeepAliveTimeout); + private const string UserCertFile = nameof(UserCertFile); + private const string UserKeyFile = nameof(UserKeyFile); + + private const string UriSchemeDiscover = "esdb+discover"; + + private static readonly string[] Schemes = { "esdb", UriSchemeDiscover }; + private static readonly int DefaultPort = KurrentClientConnectivitySettings.Default.ResolvedAddressOrDefault.Port; + private static readonly bool DefaultUseTls = true; + + private static readonly Dictionary SettingsType = + new(StringComparer.InvariantCultureIgnoreCase) { + { ConnectionName, typeof(string) }, + { MaxDiscoverAttempts, typeof(int) }, + { DiscoveryInterval, typeof(int) }, + { GossipTimeout, typeof(int) }, + { NodePreference, typeof(string) }, + { Tls, typeof(bool) }, + { TlsVerifyCert, typeof(bool) }, + { TlsCaFile, typeof(string) }, + { DefaultDeadline, typeof(int) }, + { ThrowOnAppendFailure, typeof(bool) }, + { KeepAliveInterval, typeof(int) }, + { KeepAliveTimeout, typeof(int) }, + { UserCertFile, typeof(string) }, + { UserKeyFile, typeof(string) }, + }; + + public static KurrentClientSettings Parse(string connectionString) { + var currentIndex = 0; + var schemeIndex = connectionString.IndexOf(SchemeSeparator, currentIndex, StringComparison.Ordinal); + if (schemeIndex == -1) + throw new NoSchemeException(); + + var scheme = ParseScheme(connectionString.Substring(0, schemeIndex)); + + currentIndex = schemeIndex + SchemeSeparator.Length; + var userInfoIndex = connectionString.IndexOf(UserInfoSeparator, currentIndex, StringComparison.Ordinal); + (string user, string pass)? userInfo = null; + if (userInfoIndex != -1) { + userInfo = ParseUserInfo(connectionString.Substring(currentIndex, userInfoIndex - currentIndex)); + currentIndex = userInfoIndex + UserInfoSeparator.Length; + } + + var slashIndex = connectionString.IndexOf(Slash, currentIndex, StringComparison.Ordinal); + var questionMarkIndex = connectionString.IndexOf(QuestionMark, currentIndex, StringComparison.Ordinal); + var endIndex = connectionString.Length; + + //for simpler substring operations: + if (slashIndex == -1) slashIndex = int.MaxValue; + if (questionMarkIndex == -1) questionMarkIndex = int.MaxValue; + + var hostSeparatorIndex = Math.Min(Math.Min(slashIndex, questionMarkIndex), endIndex); + var hosts = ParseHosts(connectionString.Substring(currentIndex, hostSeparatorIndex - currentIndex)); + currentIndex = hostSeparatorIndex; + + string path = ""; + if (slashIndex != int.MaxValue) + path = connectionString.Substring( + currentIndex, + Math.Min(questionMarkIndex, endIndex) - currentIndex + ); + + if (path != "" && path != "/") + throw new ConnectionStringParseException( + $"The specified path must be either an empty string or a forward slash (/) but the following path was found instead: '{path}'" + ); + + var options = new Dictionary(); + if (questionMarkIndex != int.MaxValue) { + currentIndex = questionMarkIndex + QuestionMark.Length; + options = ParseKeyValuePairs(connectionString.Substring(currentIndex)); + } + + return CreateSettings(scheme, userInfo, hosts, options); + } + + private static KurrentClientSettings CreateSettings( + string scheme, (string user, string pass)? userInfo, + EndPoint[] hosts, Dictionary options + ) { + var settings = new KurrentClientSettings { + ConnectivitySettings = KurrentClientConnectivitySettings.Default, + OperationOptions = KurrentClientOperationOptions.Default + }; + + if (userInfo.HasValue) + settings.DefaultCredentials = new UserCredentials(userInfo.Value.user, userInfo.Value.pass); + + var typedOptions = new Dictionary(StringComparer.InvariantCultureIgnoreCase); + foreach (var kv in options) { + if (!SettingsType.TryGetValue(kv.Key, out var type)) + throw new InvalidSettingException($"Unknown option: {kv.Key}"); + + if (type == typeof(int)) { + if (!int.TryParse(kv.Value, out var intValue)) + throw new InvalidSettingException($"{kv.Key} must be an integer value"); + + typedOptions.Add(kv.Key, intValue); + } else if (type == typeof(bool)) { + if (!bool.TryParse(kv.Value, out var boolValue)) + throw new InvalidSettingException($"{kv.Key} must be either true or false"); + + typedOptions.Add(kv.Key, boolValue); + } else if (type == typeof(string)) { + typedOptions.Add(kv.Key, kv.Value); + } + } + + if (typedOptions.TryGetValue(ConnectionName, out object? connectionName)) + settings.ConnectionName = (string)connectionName; + + if (typedOptions.TryGetValue(MaxDiscoverAttempts, out object? maxDiscoverAttempts)) + settings.ConnectivitySettings.MaxDiscoverAttempts = (int)maxDiscoverAttempts; + + if (typedOptions.TryGetValue(DiscoveryInterval, out object? discoveryInterval)) + settings.ConnectivitySettings.DiscoveryInterval = TimeSpan.FromMilliseconds((int)discoveryInterval); + + if (typedOptions.TryGetValue(GossipTimeout, out object? gossipTimeout)) + settings.ConnectivitySettings.GossipTimeout = TimeSpan.FromMilliseconds((int)gossipTimeout); + + if (typedOptions.TryGetValue(NodePreference, out object? nodePreference)) { + settings.ConnectivitySettings.NodePreference = ((string)nodePreference).ToLowerInvariant() switch { + "leader" => EventStore.Client.NodePreference.Leader, + "follower" => EventStore.Client.NodePreference.Follower, + "random" => EventStore.Client.NodePreference.Random, + "readonlyreplica" => EventStore.Client.NodePreference.ReadOnlyReplica, + _ => throw new InvalidSettingException($"Invalid NodePreference: {nodePreference}") + }; + } + + var useTls = DefaultUseTls; + if (typedOptions.TryGetValue(Tls, out object? tls)) { + useTls = (bool)tls; + } + + if (typedOptions.TryGetValue(DefaultDeadline, out object? operationTimeout)) + settings.DefaultDeadline = TimeSpan.FromMilliseconds((int)operationTimeout); + + if (typedOptions.TryGetValue(ThrowOnAppendFailure, out object? throwOnAppendFailure)) + settings.OperationOptions.ThrowOnAppendFailure = (bool)throwOnAppendFailure; + + if (typedOptions.TryGetValue(KeepAliveInterval, out var keepAliveIntervalMs)) { + settings.ConnectivitySettings.KeepAliveInterval = keepAliveIntervalMs switch { + -1 => Timeout_.InfiniteTimeSpan, + int value and >= 0 => TimeSpan.FromMilliseconds(value), + _ => throw new InvalidSettingException($"Invalid KeepAliveInterval: {keepAliveIntervalMs}") + }; + } + + if (typedOptions.TryGetValue(KeepAliveTimeout, out var keepAliveTimeoutMs)) { + settings.ConnectivitySettings.KeepAliveTimeout = keepAliveTimeoutMs switch { + -1 => Timeout_.InfiniteTimeSpan, + int value and >= 0 => TimeSpan.FromMilliseconds(value), + _ => throw new InvalidSettingException($"Invalid KeepAliveTimeout: {keepAliveTimeoutMs}") + }; + } + + settings.ConnectivitySettings.Insecure = !useTls; + + if (hosts.Length == 1 && scheme != UriSchemeDiscover) { + settings.ConnectivitySettings.Address = hosts[0].ToUri(useTls); + } else { + if (hosts.Any(x => x is DnsEndPoint)) + settings.ConnectivitySettings.DnsGossipSeeds = + Array.ConvertAll(hosts, x => new DnsEndPoint(x.GetHost(), x.GetPort())); + else + settings.ConnectivitySettings.IpGossipSeeds = Array.ConvertAll(hosts, x => (IPEndPoint)x); + } + + if (typedOptions.TryGetValue(TlsVerifyCert, out var tlsVerifyCert)) { + settings.ConnectivitySettings.TlsVerifyCert = (bool)tlsVerifyCert; + } + + if (typedOptions.TryGetValue(TlsCaFile, out var tlsCaFile)) { + var tlsCaFilePath = Path.GetFullPath((string)tlsCaFile); + if (!string.IsNullOrEmpty(tlsCaFilePath) && !File.Exists(tlsCaFilePath)) { + throw new InvalidClientCertificateException($"Failed to load certificate. File was not found."); + } + + try { +#if NET9_0_OR_GREATER + settings.ConnectivitySettings.TlsCaFile = X509CertificateLoader.LoadCertificateFromFile(tlsCaFilePath); +#else + settings.ConnectivitySettings.TlsCaFile = new X509Certificate2(tlsCaFilePath); +#endif + } catch (CryptographicException) { + throw new InvalidClientCertificateException("Failed to load certificate. Invalid file format."); + } + } + + ConfigureClientCertificate(settings, typedOptions); + + settings.CreateHttpMessageHandler = CreateDefaultHandler; + + return settings; + +#if NET48 + HttpMessageHandler CreateDefaultHandler() { + if (settings.CreateHttpMessageHandler is not null) + return settings.CreateHttpMessageHandler.Invoke(); + + var handler = new WinHttpHandler { + TcpKeepAliveEnabled = true, + TcpKeepAliveTime = settings.ConnectivitySettings.KeepAliveTimeout, + TcpKeepAliveInterval = settings.ConnectivitySettings.KeepAliveInterval, + EnableMultipleHttp2Connections = true + }; + + if (settings.ConnectivitySettings.Insecure) return handler; + + if (settings.ConnectivitySettings.ClientCertificate is not null) + handler.ClientCertificates.Add(settings.ConnectivitySettings.ClientCertificate); + + handler.ServerCertificateValidationCallback = settings.ConnectivitySettings.TlsVerifyCert switch { + false => delegate { return true; }, + true when settings.ConnectivitySettings.TlsCaFile is not null => (sender, certificate, chain, errors) => { + if (chain is null) return false; + + chain.ChainPolicy.ExtraStore.Add(settings.ConnectivitySettings.TlsCaFile); + return chain.Build(certificate); + }, + _ => null + }; + + return handler; + } +#else + HttpMessageHandler CreateDefaultHandler() { + var handler = new SocketsHttpHandler { + KeepAlivePingDelay = settings.ConnectivitySettings.KeepAliveInterval, + KeepAlivePingTimeout = settings.ConnectivitySettings.KeepAliveTimeout, + EnableMultipleHttp2Connections = true + }; + + if (settings.ConnectivitySettings.Insecure) + return handler; + + if (settings.ConnectivitySettings.ClientCertificate is not null) { + handler.SslOptions.ClientCertificates = new X509CertificateCollection { + settings.ConnectivitySettings.ClientCertificate + }; + } + + handler.SslOptions.RemoteCertificateValidationCallback = settings.ConnectivitySettings.TlsVerifyCert switch { + false => delegate { return true; }, + true when settings.ConnectivitySettings.TlsCaFile is not null => (sender, certificate, chain, errors) => { + if (certificate is not X509Certificate2 peerCertificate || chain is null) return false; + + chain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust; + chain.ChainPolicy.CustomTrustStore.Add(settings.ConnectivitySettings.TlsCaFile); + return chain.Build(peerCertificate); + }, + _ => null + }; + + return handler; + } +#endif + } + + static void ConfigureClientCertificate(KurrentClientSettings settings, IReadOnlyDictionary options) { + var certPemFilePath = GetOptionValueAsString(UserCertFile); + var keyPemFilePath = GetOptionValueAsString(UserKeyFile); + + if (string.IsNullOrEmpty(certPemFilePath) && string.IsNullOrEmpty(keyPemFilePath)) + return; + + if (string.IsNullOrEmpty(certPemFilePath) || string.IsNullOrEmpty(keyPemFilePath)) + throw new InvalidClientCertificateException("Invalid client certificate settings. Both UserCertFile and UserKeyFile must be set."); + + if (!File.Exists(certPemFilePath)) + throw new InvalidClientCertificateException( + $"Invalid client certificate settings. The specified UserCertFile does not exist: {certPemFilePath}" + ); + + if (!File.Exists(keyPemFilePath)) + throw new InvalidClientCertificateException( + $"Invalid client certificate settings. The specified UserKeyFile does not exist: {keyPemFilePath}" + ); + + try { + settings.ConnectivitySettings.ClientCertificate = + X509Certificates.CreateFromPemFile(certPemFilePath, keyPemFilePath); + } catch (Exception ex) { + throw new InvalidClientCertificateException("Failed to create client certificate.", ex); + } + + return; + + string GetOptionValueAsString(string key) => options.TryGetValue(key, out var value) ? (string)value : ""; + } + + private static string ParseScheme(string s) => + !Schemes.Contains(s) ? throw new InvalidSchemeException(s, Schemes) : s; + + private static (string, string) ParseUserInfo(string s) { + var tokens = s.Split(Colon[0]); + if (tokens.Length != 2) throw new InvalidUserCredentialsException(s); + + return (tokens[0], tokens[1]); + } + + private static EndPoint[] ParseHosts(string s) { + var hostsTokens = s.Split(Comma[0]); + var hosts = new List(); + foreach (var hostToken in hostsTokens) { + var hostPortToken = hostToken.Split(Colon[0]); + string host; + int port; + switch (hostPortToken.Length) { + case 1: + host = hostPortToken[0]; + port = DefaultPort; + break; + + case 2: { + host = hostPortToken[0]; + if (!int.TryParse(hostPortToken[1], out port)) + throw new InvalidHostException(hostToken); + + break; + } + + default: + throw new InvalidHostException(hostToken); + } + + if (host.Length == 0) { + throw new InvalidHostException(hostToken); + } + + if (IPAddress.TryParse(host, out IPAddress? ip)) { + hosts.Add(new IPEndPoint(ip, port)); + } else { + hosts.Add(new DnsEndPoint(host, port)); + } + } + + return hosts.ToArray(); + } + + private static Dictionary ParseKeyValuePairs(string s) { + var options = new Dictionary(StringComparer.InvariantCultureIgnoreCase); + var optionsTokens = s.Split(Ampersand[0]); + foreach (var optionToken in optionsTokens) { + var (key, val) = ParseKeyValuePair(optionToken); + try { + options.Add(key, val); + } catch (ArgumentException) { + throw new DuplicateKeyException(key); + } + } + + return options; + } + + private static (string, string) ParseKeyValuePair(string s) { + var keyValueToken = s.Split(Equal[0]); + if (keyValueToken.Length != 2) { + throw new InvalidKeyValuePairException(s); + } + + return (keyValueToken[0], keyValueToken[1]); + } + } + } +} diff --git a/src/Kurrent.Client/Core/KurrentClientSettings.cs b/src/Kurrent.Client/Core/KurrentClientSettings.cs new file mode 100644 index 000000000..aed914074 --- /dev/null +++ b/src/Kurrent.Client/Core/KurrentClientSettings.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; +using Grpc.Core; +using Grpc.Core.Interceptors; + +using Microsoft.Extensions.Logging; + +namespace EventStore.Client { + /// + /// A class that represents the settings to use for operations made from an implementation of . + /// + public partial class KurrentClientSettings { + /// + /// An optional list of s to use. + /// + public IEnumerable? Interceptors { get; set; } + + /// + /// The name of the connection. + /// + public string? ConnectionName { get; set; } + + /// + /// An optional factory. + /// + public Func? CreateHttpMessageHandler { get; set; } + + /// + /// An optional to use. + /// + public ILoggerFactory? LoggerFactory { get; set; } + + /// + /// The optional to use when creating the . + /// + public ChannelCredentials? ChannelCredentials { get; set; } + + /// + /// The default to use. + /// + public KurrentClientOperationOptions OperationOptions { get; set; } = + KurrentClientOperationOptions.Default; + + /// + /// The to use. + /// + public KurrentClientConnectivitySettings ConnectivitySettings { get; set; } = + KurrentClientConnectivitySettings.Default; + + /// + /// The optional to use if none have been supplied to the operation. + /// + public UserCredentials? DefaultCredentials { get; set; } + + /// + /// The default deadline for calls. Will not be applied to reads or subscriptions. + /// + public TimeSpan? DefaultDeadline { get; set; } = TimeSpan.FromSeconds(10); + } +} diff --git a/src/Kurrent.Client/Core/NodePreference.cs b/src/Kurrent.Client/Core/NodePreference.cs new file mode 100644 index 000000000..530edb87d --- /dev/null +++ b/src/Kurrent.Client/Core/NodePreference.cs @@ -0,0 +1,26 @@ +namespace EventStore.Client { + /// + /// Indicates the preferred KurrentDB node type to connect to. + /// + public enum NodePreference { + /// + /// When attempting connection, prefers leader node. + /// + Leader, + + /// + /// When attempting connection, prefers follower node. + /// + Follower, + + /// + /// When attempting connection, has no node preference. + /// + Random, + + /// + /// When attempting connection, prefers read only replicas. + /// + ReadOnlyReplica + } +} diff --git a/src/Kurrent.Client/Core/NodePreferenceComparers.cs b/src/Kurrent.Client/Core/NodePreferenceComparers.cs new file mode 100644 index 000000000..07e8a1cc5 --- /dev/null +++ b/src/Kurrent.Client/Core/NodePreferenceComparers.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; + +namespace EventStore.Client { + internal static class NodePreferenceComparers { + public static readonly IComparer Leader = new Comparer(state => + state switch { + ClusterMessages.VNodeState.Leader => 0, + ClusterMessages.VNodeState.Follower => 1, + ClusterMessages.VNodeState.ReadOnlyReplica => 2, + ClusterMessages.VNodeState.PreReadOnlyReplica => 3, + ClusterMessages.VNodeState.ReadOnlyLeaderless => 4, + _ => int.MaxValue + }); + + public static readonly IComparer Follower = new Comparer(state => + state switch { + ClusterMessages.VNodeState.Follower => 0, + ClusterMessages.VNodeState.Leader => 1, + ClusterMessages.VNodeState.ReadOnlyReplica => 2, + ClusterMessages.VNodeState.PreReadOnlyReplica => 3, + ClusterMessages.VNodeState.ReadOnlyLeaderless => 4, + _ => int.MaxValue + }); + + public static readonly IComparer ReadOnlyReplica = new Comparer(state => + state switch { + ClusterMessages.VNodeState.ReadOnlyReplica => 0, + ClusterMessages.VNodeState.PreReadOnlyReplica => 1, + ClusterMessages.VNodeState.ReadOnlyLeaderless => 2, + ClusterMessages.VNodeState.Leader => 3, + ClusterMessages.VNodeState.Follower => 4, + _ => int.MaxValue + }); + + public static readonly IComparer None = new Comparer(_ => 0); + + private class Comparer : IComparer { + private readonly Func _getPriority; + + public Comparer(Func getPriority) { + _getPriority = getPriority; + } + + public int Compare(ClusterMessages.VNodeState left, ClusterMessages.VNodeState right) => + _getPriority(left).CompareTo(_getPriority(right)); + } + } +} diff --git a/src/Kurrent.Client/Core/NodeSelector.cs b/src/Kurrent.Client/Core/NodeSelector.cs new file mode 100644 index 000000000..661e1e190 --- /dev/null +++ b/src/Kurrent.Client/Core/NodeSelector.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; + +namespace EventStore.Client { + // Selects a node to connect to from a ClusterInfo, based on the node preference. + // Deals with endpoints, no grpc here. + // Thread safe. + internal class NodeSelector { + private static readonly ClusterMessages.VNodeState[] _notAllowedStates = { + ClusterMessages.VNodeState.Manager, + ClusterMessages.VNodeState.ShuttingDown, + ClusterMessages.VNodeState.Shutdown, + ClusterMessages.VNodeState.Unknown, + ClusterMessages.VNodeState.Initializing, + ClusterMessages.VNodeState.CatchingUp, + ClusterMessages.VNodeState.ResigningLeader, + ClusterMessages.VNodeState.PreLeader, + ClusterMessages.VNodeState.PreReplica, + ClusterMessages.VNodeState.PreReadOnlyReplica, + ClusterMessages.VNodeState.Clone, + ClusterMessages.VNodeState.DiscoverLeader, + }; + + private readonly Random _random; + private readonly IComparer? _nodeStateComparer; + + public NodeSelector(KurrentClientSettings settings) { + _random = new Random(0); + _nodeStateComparer = settings.ConnectivitySettings.NodePreference switch { + NodePreference.Leader => NodePreferenceComparers.Leader, + NodePreference.Follower => NodePreferenceComparers.Follower, + NodePreference.ReadOnlyReplica => NodePreferenceComparers.ReadOnlyReplica, + _ => NodePreferenceComparers.None + }; + } + + public DnsEndPoint SelectNode(ClusterMessages.ClusterInfo clusterInfo) { + if (clusterInfo.Members.Length == 0) { + throw new Exception("No nodes in cluster info."); + } + + lock (_random) { + var node = clusterInfo.Members + .Where(IsConnectable) + .OrderBy(node => node.State, _nodeStateComparer) + .ThenBy(_ => _random.Next()) + .FirstOrDefault(); + + if (node is null) { + throw new Exception("No nodes are in a connectable state."); + } + + return node.EndPoint; + } + } + + private static bool IsConnectable(ClusterMessages.MemberInfo node) => + node.IsAlive && + !_notAllowedStates.Contains(node.State); + } +} diff --git a/src/Kurrent.Client/Core/Position.cs b/src/Kurrent.Client/Core/Position.cs new file mode 100644 index 000000000..169439804 --- /dev/null +++ b/src/Kurrent.Client/Core/Position.cs @@ -0,0 +1,200 @@ +using System; + +namespace EventStore.Client { + /// + /// A structure referring to a potential logical record position + /// in the Event Store transaction file. + /// + public readonly struct Position : IEquatable, IComparable, IComparable, IPosition { + /// + /// Position representing the start of the transaction file + /// + public static readonly Position Start = new Position(0, 0); + + /// + /// Position representing the end of the transaction file + /// + public static readonly Position End = new Position(ulong.MaxValue, ulong.MaxValue); + + /// + /// The commit position of the record + /// + public readonly ulong CommitPosition; + + /// + /// The prepare position of the record. + /// + public readonly ulong PreparePosition; + + /// + /// Constructs a position with the given commit and prepare positions. + /// It is not guaranteed that the position is actually the start of a + /// record in the transaction file. + /// + /// The commit position cannot be less than the prepare position. + /// + /// The commit position of the record. + /// The prepare position of the record. + public Position(ulong commitPosition, ulong preparePosition) { + if (commitPosition < preparePosition) + throw new ArgumentOutOfRangeException( + nameof(commitPosition), + "The commit position cannot be less than the prepare position"); + + if (commitPosition > long.MaxValue && commitPosition != ulong.MaxValue) { + throw new ArgumentOutOfRangeException(nameof(commitPosition)); + } + + + if (preparePosition > long.MaxValue && preparePosition != ulong.MaxValue) { + throw new ArgumentOutOfRangeException(nameof(preparePosition)); + } + + CommitPosition = commitPosition; + PreparePosition = preparePosition; + } + + /// + /// Compares whether p1 < p2. + /// + /// A . + /// A . + /// True if p1 < p2. + public static bool operator <(Position p1, Position p2) => + p1.CommitPosition < p2.CommitPosition || + p1.CommitPosition == p2.CommitPosition && p1.PreparePosition < p2.PreparePosition; + + + /// + /// Compares whether p1 > p2. + /// + /// A . + /// A . + /// True if p1 > p2. + public static bool operator >(Position p1, Position p2) => + p1.CommitPosition > p2.CommitPosition || + p1.CommitPosition == p2.CommitPosition && p1.PreparePosition > p2.PreparePosition; + + /// + /// Compares whether p1 >= p2. + /// + /// A . + /// A . + /// True if p1 >= p2. + public static bool operator >=(Position p1, Position p2) => p1 > p2 || p1 == p2; + + /// + /// Compares whether p1 <= p2. + /// + /// A . + /// A . + /// True if p1 <= p2. + public static bool operator <=(Position p1, Position p2) => p1 < p2 || p1 == p2; + + /// + /// Compares p1 and p2 for equality. + /// + /// A . + /// A . + /// True if p1 is equal to p2. + public static bool operator ==(Position p1, Position p2) => + Equals(p1, p2); + + /// + /// Compares p1 and p2 for equality. + /// + /// A . + /// A . + /// True if p1 is not equal to p2. + public static bool operator !=(Position p1, Position p2) => !(p1 == p2); + + /// + public int CompareTo(Position other) => this == other ? 0 : this > other ? 1 : -1; + + /// + public int CompareTo(object? obj) => obj switch { + null => 1, + Position other => CompareTo(other), + _ => throw new ArgumentException("Object is not a Position"), + }; + + /// + /// Indicates whether this instance and a specified object are equal. + /// + /// + /// true if and this instance are the same type and represent the same value; otherwise, false. + /// + /// Another object to compare to. 2 + public override bool Equals(object? obj) => obj is Position position && Equals(position); + + /// + /// Compares this instance of for equality + /// with another instance. + /// + /// A + /// True if this instance is equal to the other instance. + public bool Equals(Position other) => + CommitPosition == other.CommitPosition && PreparePosition == other.PreparePosition; + + /// + /// Returns the hash code for this instance. + /// + /// + /// A 32-bit signed integer that is the hash code for this instance. + /// + /// 2 + public override int GetHashCode() => HashCode.Hash.Combine(CommitPosition).Combine(PreparePosition); + + /// + /// Returns the fully qualified type name of this instance. + /// + /// + /// A containing a fully qualified type name. + /// + /// 2 + public override string ToString() => $"C:{CommitPosition}/P:{PreparePosition}"; + + /// + /// Tries to convert the string representation of a to its equivalent. + /// A return value indicates whether the conversion succeeded or failed. + /// + /// A string that represents the to convert. + /// Contains the that is equivalent to the string + /// representation, if the conversion succeeded, or null if the conversion failed. + /// true if the value was converted successfully; otherwise, false. + public static bool TryParse(string value, out Position? position) { + position = null; + var parts = value.Split('/'); + + if (parts.Length != 2) { + return false; + } + + if (!TryParsePosition("C", parts[0], out var commitPosition)) { + return false; + } + + if (!TryParsePosition("P", parts[1], out var preparePosition)) { + return false; + } + + position = new Position(commitPosition, preparePosition); + return true; + + static bool TryParsePosition(string expectedPrefix, string v, out ulong p) { + p = 0; + + var prts = v.Split(':'); + if (prts.Length != 2) { + return false; + } + + if (prts[0] != expectedPrefix) { + return false; + } + + return ulong.TryParse(prts[1], out p); + } + } + } +} diff --git a/src/Kurrent.Client/Core/PrefixFilterExpression.cs b/src/Kurrent.Client/Core/PrefixFilterExpression.cs new file mode 100644 index 000000000..ea154aee3 --- /dev/null +++ b/src/Kurrent.Client/Core/PrefixFilterExpression.cs @@ -0,0 +1,64 @@ +using System; + +namespace EventStore.Client { + /// + /// A structure representing a prefix (i.e., starts with) filter. + /// + public readonly struct PrefixFilterExpression : IEquatable { + /// + /// An empty . + /// + public static readonly PrefixFilterExpression None = default; + + private readonly string? _value; + + /// + /// Constructs a new . + /// + /// + /// + public PrefixFilterExpression(string value) { + if (value == null) { + throw new ArgumentNullException(nameof(value)); + } + + _value = value; + } + + /// + public bool Equals(PrefixFilterExpression other) => string.Equals(_value, other._value); + + /// + public override bool Equals(object? obj) => obj is PrefixFilterExpression other && Equals(other); + + /// + public override int GetHashCode() => _value?.GetHashCode() ?? 0; + + /// + /// Compares left and right for equality. + /// + /// + /// + /// True if left is equal to right. + public static bool operator ==(PrefixFilterExpression left, PrefixFilterExpression right) => left.Equals(right); + + /// + /// Compares left and right for inequality. + /// + /// + /// + /// True if left is not equal to right. + public static bool operator !=(PrefixFilterExpression left, PrefixFilterExpression right) => + !left.Equals(right); + + /// + /// Converts the to a . + /// + /// + /// + public static implicit operator string?(PrefixFilterExpression value) => value._value; + + /// + public override string? ToString() => _value; + } +} diff --git a/src/Kurrent.Client/Core/ReconnectionRequired.cs b/src/Kurrent.Client/Core/ReconnectionRequired.cs new file mode 100644 index 000000000..bf448971d --- /dev/null +++ b/src/Kurrent.Client/Core/ReconnectionRequired.cs @@ -0,0 +1,15 @@ +using System.Net; + +namespace EventStore.Client { + internal abstract record ReconnectionRequired { + public record None : ReconnectionRequired { + public static None Instance = new(); + } + + public record Rediscover : ReconnectionRequired { + public static Rediscover Instance = new(); + } + + public record NewLeader(DnsEndPoint EndPoint) : ReconnectionRequired; + } +} diff --git a/src/Kurrent.Client/Core/RegularFilterExpression.cs b/src/Kurrent.Client/Core/RegularFilterExpression.cs new file mode 100644 index 000000000..b8e6a42b8 --- /dev/null +++ b/src/Kurrent.Client/Core/RegularFilterExpression.cs @@ -0,0 +1,86 @@ +using System; +using System.Text.RegularExpressions; + +namespace EventStore.Client { + /// + /// A structure representing a regular expression filter. + /// + public readonly struct RegularFilterExpression : IEquatable { + /// + /// An empty . + /// + public static readonly RegularFilterExpression None = default; + + /// + /// A that excludes system events (i.e., those whose types start with $). + /// + /// + public static readonly RegularFilterExpression ExcludeSystemEvents = + new RegularFilterExpression(new Regex(@"^[^\$].*")); + + private readonly string? _value; + + /// + /// Constructs a new . + /// + /// + /// + public RegularFilterExpression(string value) { + if (value == null) { + throw new ArgumentNullException(nameof(value)); + } + + _value = value; + } + + /// + /// Constructs a new . + /// + /// + /// + public RegularFilterExpression(Regex value) { + if (value == null) { + throw new ArgumentNullException(nameof(value)); + } + + _value = value.ToString(); + } + + /// + public bool Equals(RegularFilterExpression other) => string.Equals(_value, other._value); + + /// + public override bool Equals(object? obj) => obj is RegularFilterExpression other && Equals(other); + + /// + public override int GetHashCode() => _value?.GetHashCode() ?? 0; + + /// + /// Compares left and right for equality. + /// + /// + /// + /// True if left is equal to right. + public static bool operator ==(RegularFilterExpression left, RegularFilterExpression right) => + left.Equals(right); + + /// + /// Compares left and right for inequality. + /// + /// + /// + /// True if left is not equal to right. + public static bool operator !=(RegularFilterExpression left, RegularFilterExpression right) => + !left.Equals(right); + + /// + /// Converts a to a . + /// + /// + /// + public static implicit operator string?(RegularFilterExpression value) => value._value; + + /// + public override string? ToString() => _value; + } +} diff --git a/src/Kurrent.Client/Core/ResolvedEvent.cs b/src/Kurrent.Client/Core/ResolvedEvent.cs new file mode 100644 index 000000000..25ca13a78 --- /dev/null +++ b/src/Kurrent.Client/Core/ResolvedEvent.cs @@ -0,0 +1,60 @@ +namespace EventStore.Client { + /// + /// A structure representing a single event or a resolved link event. + /// + public readonly struct ResolvedEvent { + /// + /// If this represents a link event, the + /// will be the resolved link event, otherwise it will be the single event. + /// + public readonly EventRecord Event; + + /// + /// The link event if this is a link event. + /// + public readonly EventRecord? Link; + + /// + /// Returns the event that was read or which triggered the subscription. + /// + /// If this represents a link event, the + /// will be the , otherwise it will be . + /// + public EventRecord OriginalEvent => Link ?? Event; + + /// + /// Position of the if available. + /// + public readonly Position? OriginalPosition; + + /// + /// The stream name of the . + /// + public string OriginalStreamId => OriginalEvent.EventStreamId; + + /// + /// The in the stream of the . + /// + public StreamPosition OriginalEventNumber => OriginalEvent.EventNumber; + + /// + /// Indicates whether this is a resolved link + /// event. + /// + public bool IsResolved => Link != null && Event != null; + + /// + /// Constructs a new . + /// + /// + /// + /// + public ResolvedEvent(EventRecord @event, EventRecord? link, ulong? commitPosition) { + Event = @event; + Link = link; + OriginalPosition = commitPosition.HasValue + ? new Position(commitPosition.Value, (link ?? @event).Position.PreparePosition) + : new Position?(); + } + } +} diff --git a/src/Kurrent.Client/Core/ServerCapabilities.cs b/src/Kurrent.Client/Core/ServerCapabilities.cs new file mode 100644 index 000000000..aa63c4e3d --- /dev/null +++ b/src/Kurrent.Client/Core/ServerCapabilities.cs @@ -0,0 +1,10 @@ +namespace EventStore.Client { +#pragma warning disable 1591 + public record ServerCapabilities( + bool SupportsBatchAppend = false, + bool SupportsPersistentSubscriptionsToAll = false, + bool SupportsPersistentSubscriptionsGetInfo = false, + bool SupportsPersistentSubscriptionsRestartSubsystem = false, + bool SupportsPersistentSubscriptionsReplayParked = false, + bool SupportsPersistentSubscriptionsList = false); +} diff --git a/src/Kurrent.Client/Core/SharingProvider.cs b/src/Kurrent.Client/Core/SharingProvider.cs new file mode 100644 index 000000000..67911a618 --- /dev/null +++ b/src/Kurrent.Client/Core/SharingProvider.cs @@ -0,0 +1,111 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; + +namespace EventStore.Client { + internal class SharingProvider { + protected ILogger Log { get; } + + public SharingProvider(ILoggerFactory? loggerFactory) { + Log = loggerFactory?.CreateLogger() ?? + new NullLogger(); + } + } + + // Given a factory for items of type TOutput, where the items: + // - are expensive to produce + // - can be shared by consumers + // - can break + // - can fail to be successfully produced by the factory to begin with. + // + // This class will make minimal use of the factory to provide items to consumers. + // The Factory can produce and return an item, or it can throw an exception. + // We pass the factory a OnBroken callback to be called later if that instance becomes broken. + // the OnBroken callback can be called multiple times, the factory will be called once. + // the argument to the OnBroken callback is the input to construct the next item. + // + // The factory will not be called multiple times concurrently so does not need to be + // thread safe, but it does need to terminate. + // + // This class is thread safe. + + internal class SharingProvider : SharingProvider, IDisposable { + private readonly Func, Task> _factory; + private readonly TimeSpan _factoryRetryDelay; + private readonly TInput _initialInput; + private TaskCompletionSource _currentBox; + private bool _disposed; + + public SharingProvider( + Func, Task> factory, + TimeSpan factoryRetryDelay, + TInput initialInput, + ILoggerFactory? loggerFactory = null) : base(loggerFactory) { + + _factory = factory; + _factoryRetryDelay = factoryRetryDelay; + _initialInput = initialInput; + _currentBox = new(TaskCreationOptions.RunContinuationsAsynchronously); + _ = FillBoxAsync(_currentBox, input: initialInput); + } + + public Task CurrentAsync => _currentBox.Task; + + public void Reset() { + OnBroken(_currentBox, _initialInput); + } + + // Call this to return a box containing a defective item, or indeed no item at all. + // A new box will be produced and filled if necessary. + private void OnBroken(TaskCompletionSource brokenBox, TInput input) { + if (!brokenBox.Task.IsCompleted) { + // factory is still working on this box. don't create a new box to fill + // or we would have to require the factory be thread safe. + Log.LogDebug("{type} returned to factory. Production already in progress.", typeof(TOutput).Name); + return; + } + + // replace _currentBox with a new one, but only if it is the broken one. + var originalBox = Interlocked.CompareExchange( + location1: ref _currentBox, + value: new(TaskCreationOptions.RunContinuationsAsynchronously), + comparand: brokenBox); + + if (originalBox == brokenBox) { + // replaced the _currentBox, call the factory to fill it. + Log.LogDebug("{type} returned to factory. Producing a new one.", typeof(TOutput).Name); + _ = FillBoxAsync(_currentBox, input); + } else { + // did not replace. a new one was created previously. do nothing. + Log.LogDebug("{type} returned to factory. Production already complete.", typeof(TOutput).Name); + } + } + + private async Task FillBoxAsync(TaskCompletionSource box, TInput input) { + if (_disposed) { + Log.LogDebug("{type} will not be produced, factory is closed!", typeof(TOutput).Name); + box.TrySetException(new ObjectDisposedException(GetType().ToString())); + return; + } + + try { + Log.LogDebug("{type} being produced...", typeof(TOutput).Name); + var item = await _factory(input, x => OnBroken(box, x)).ConfigureAwait(false); + box.TrySetResult(item); + Log.LogDebug("{type} produced!", typeof(TOutput).Name); + } catch (Exception ex) { + await Task.Yield(); // avoid risk of stack overflow + Log.LogDebug(ex, "{type} production failed. Retrying in {delay}", typeof(TOutput).Name, _factoryRetryDelay); + await Task.Delay(_factoryRetryDelay).ConfigureAwait(false); + box.TrySetException(ex); + OnBroken(box, _initialInput); + } + } + + public void Dispose() { + _disposed = true; + } + } +} diff --git a/src/Kurrent.Client/Core/SingleNodeChannelSelector.cs b/src/Kurrent.Client/Core/SingleNodeChannelSelector.cs new file mode 100644 index 000000000..83449e689 --- /dev/null +++ b/src/Kurrent.Client/Core/SingleNodeChannelSelector.cs @@ -0,0 +1,36 @@ +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using Grpc.Core; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; + +namespace EventStore.Client { + internal class SingleNodeChannelSelector : IChannelSelector { + private readonly ILogger _log; + private readonly ChannelCache _channelCache; + private readonly DnsEndPoint _endPoint; + + public SingleNodeChannelSelector( + KurrentClientSettings settings, + ChannelCache channelCache) { + + _log = settings.LoggerFactory?.CreateLogger() ?? + new NullLogger(); + + _channelCache = channelCache; + + var uri = settings.ConnectivitySettings.ResolvedAddressOrDefault; + _endPoint = new DnsEndPoint(host: uri.Host, port: uri.Port); + } + + public Task SelectChannelAsync(CancellationToken cancellationToken) => + Task.FromResult(SelectChannel(_endPoint)); + + public ChannelBase SelectChannel(DnsEndPoint endPoint) { + _log.LogInformation("Selected {endPoint}.", endPoint); + + return _channelCache.GetChannelInfo(endPoint); + } + } +} diff --git a/src/Kurrent.Client/Core/SingleNodeHttpHandler.cs b/src/Kurrent.Client/Core/SingleNodeHttpHandler.cs new file mode 100644 index 000000000..0c346a00f --- /dev/null +++ b/src/Kurrent.Client/Core/SingleNodeHttpHandler.cs @@ -0,0 +1,22 @@ +using System; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +namespace EventStore.Client { + internal class SingleNodeHttpHandler : DelegatingHandler { + private readonly KurrentClientSettings _settings; + + public SingleNodeHttpHandler(KurrentClientSettings settings) { + _settings = settings; + } + + protected override Task SendAsync(HttpRequestMessage request, + CancellationToken cancellationToken) { + request.RequestUri = new UriBuilder(request.RequestUri!) { + Scheme = _settings.ConnectivitySettings.ResolvedAddressOrDefault.Scheme + }.Uri; + return base.SendAsync(request, cancellationToken); + } + } +} diff --git a/src/Kurrent.Client/Core/StreamFilter.cs b/src/Kurrent.Client/Core/StreamFilter.cs new file mode 100644 index 000000000..8ed70e7bb --- /dev/null +++ b/src/Kurrent.Client/Core/StreamFilter.cs @@ -0,0 +1,134 @@ +using System; +using System.Linq; +using System.Text.RegularExpressions; + +namespace EventStore.Client { + /// + /// A structure representing a filter on stream names for read operations. + /// + public readonly struct StreamFilter : IEquatable, IEventFilter { + /// + /// An empty . + /// + public static readonly StreamFilter None = default; + + readonly PrefixFilterExpression[] _prefixes; + + /// + public PrefixFilterExpression[] Prefixes => _prefixes ?? Array.Empty(); + + /// + public RegularFilterExpression Regex { get; } + + /// + public uint? MaxSearchWindow { get; } + + /// + /// Creates a from a single prefix. + /// + /// + /// + public static IEventFilter Prefix(string prefix) + => new StreamFilter(new PrefixFilterExpression(prefix)); + + /// + /// Creates a from multiple prefixes. + /// + /// + /// + public static IEventFilter Prefix(params string[] prefixes) + => new StreamFilter(Array.ConvertAll(prefixes, prefix => new PrefixFilterExpression(prefix))); + + /// + /// Creates a from a search window and multiple prefixes. + /// + /// + /// + /// + public static IEventFilter Prefix(uint maxSearchWindow, params string[] prefixes) + => new StreamFilter(maxSearchWindow, + Array.ConvertAll(prefixes, prefix => new PrefixFilterExpression(prefix))); + + /// + /// Creates a from a regular expression and a search window. + /// + /// + /// + /// + public static IEventFilter RegularExpression(string regex, uint maxSearchWindow = 32) + => new StreamFilter(maxSearchWindow, new RegularFilterExpression(regex)); + + /// + /// Creates a from a regular expression and a search window. + /// + /// + /// + /// + public static IEventFilter RegularExpression(Regex regex, uint maxSearchWindow = 32) + => new StreamFilter(maxSearchWindow, new RegularFilterExpression(regex)); + + StreamFilter(RegularFilterExpression regex) : this(default, regex) { } + + StreamFilter(uint maxSearchWindow, RegularFilterExpression regex) { + if (maxSearchWindow == 0) { + throw new ArgumentOutOfRangeException(nameof(maxSearchWindow), + maxSearchWindow, $"{nameof(maxSearchWindow)} must be greater than 0."); + } + + Regex = regex; + _prefixes = Array.Empty(); + MaxSearchWindow = maxSearchWindow; + } + + StreamFilter(params PrefixFilterExpression[] prefixes) : this(32, prefixes) { } + + StreamFilter(uint maxSearchWindow, params PrefixFilterExpression[] prefixes) { + if (prefixes.Length == 0) { + throw new ArgumentException(); + } + + if (maxSearchWindow == 0) { + throw new ArgumentOutOfRangeException(nameof(maxSearchWindow), + maxSearchWindow, $"{nameof(maxSearchWindow)} must be greater than 0."); + } + + _prefixes = prefixes; + Regex = RegularFilterExpression.None; + MaxSearchWindow = maxSearchWindow; + } + + /// + public bool Equals(StreamFilter other) => + Prefixes.SequenceEqual(other.Prefixes) && + Regex.Equals(other.Regex) && + MaxSearchWindow.Equals(other.MaxSearchWindow); + + /// + public override bool Equals(object? obj) => obj is StreamFilter other && Equals(other); + + /// + public override int GetHashCode() => HashCode.Hash.Combine(Prefixes).Combine(Regex).Combine(MaxSearchWindow); + + /// + /// Compares left and right for equality. + /// + /// + /// + /// True if left is equal to right. + public static bool operator ==(StreamFilter left, StreamFilter right) => left.Equals(right); + + /// + /// Compares left and right for inequality. + /// + /// + /// + /// True if left is not equal to right. + public static bool operator !=(StreamFilter left, StreamFilter right) => !left.Equals(right); + + /// + public override string ToString() => + this == None + ? "(none)" + : $"{nameof(StreamFilter)} {(Prefixes.Length == 0 ? Regex.ToString() : $"[{string.Join(", ", Prefixes)}]")}"; + } +} diff --git a/src/Kurrent.Client/Core/StreamIdentifier.cs b/src/Kurrent.Client/Core/StreamIdentifier.cs new file mode 100644 index 000000000..9ca6c9e7e --- /dev/null +++ b/src/Kurrent.Client/Core/StreamIdentifier.cs @@ -0,0 +1,28 @@ +using System.Text; +using Google.Protobuf; + +namespace EventStore.Client { +#pragma warning disable 1591 + internal partial class StreamIdentifier { + private string? _cached; + + public static implicit operator string?(StreamIdentifier? source) { + if (source == null) { + return null; + } + if (source._cached != null || source.StreamName.IsEmpty) return source._cached; + +#if NET + var tmp = Encoding.UTF8.GetString(source.StreamName.Span); +#else + var tmp = Encoding.UTF8.GetString(source.StreamName.ToByteArray()); +#endif + //this doesn't have to be thread safe, its just a cache in case the identifier is turned into a string several times + source._cached = tmp; + return source._cached; + } + + public static implicit operator StreamIdentifier(string source) => + new() {StreamName = ByteString.CopyFromUtf8(source)}; + } +} diff --git a/src/Kurrent.Client/Core/StreamPosition.cs b/src/Kurrent.Client/Core/StreamPosition.cs new file mode 100644 index 000000000..7c196c7da --- /dev/null +++ b/src/Kurrent.Client/Core/StreamPosition.cs @@ -0,0 +1,201 @@ +using System; + +namespace EventStore.Client { + /// + /// A structure referring to an 's position within a stream. + /// + public readonly struct StreamPosition : IEquatable, IComparable, IComparable, IPosition { + private readonly ulong _value; + + /// + /// The beginning (i.e., the first event) of a stream. + /// + public static readonly StreamPosition Start = new StreamPosition(0); + + /// + /// The end of a stream. Use this when reading a stream backwards, or subscribing live to a stream. + /// + public static readonly StreamPosition End = new StreamPosition(ulong.MaxValue); + + /// + /// Converts a to a . It is not meant to be used directly from your code. + /// + /// + /// + public static StreamPosition FromInt64(long value) => + value == -1 ? End : new StreamPosition(Convert.ToUInt64(value)); + + /// + /// Creates a from a . + /// + /// + /// + public static StreamPosition FromStreamRevision(StreamRevision revision) => revision.ToUInt64() switch { + ulong.MaxValue => throw new ArgumentOutOfRangeException(nameof(revision)), + _ => new StreamPosition(revision.ToUInt64()) + }; + + /// + /// Constructs a new . + /// + /// + /// + public StreamPosition(ulong value) { + if (value > long.MaxValue && value != ulong.MaxValue) { + throw new ArgumentOutOfRangeException(nameof(value)); + } + + _value = value; + } + + /// + /// Advance to the next . + /// + /// + public StreamPosition Next() => this + 1; + + /// + public int CompareTo(StreamPosition other) => _value.CompareTo(other._value); + + /// + public int CompareTo(object? obj) => obj switch { + null => 1, + StreamPosition other => CompareTo(other), + _ => throw new ArgumentException("Object is not a StreamPosition"), + }; + + /// + public bool Equals(StreamPosition other) => _value == other._value; + + /// + public override bool Equals(object? obj) => obj is StreamPosition other && Equals(other); + + /// + public override int GetHashCode() => _value.GetHashCode(); + + /// + /// Compares left and right for equality. + /// + /// + /// + /// True if left is equal to right. + public static bool operator ==(StreamPosition left, StreamPosition right) => left.Equals(right); + + /// + /// Compares left and right for inequality. + /// + /// + /// + /// True if left is not equal to right. + public static bool operator !=(StreamPosition left, StreamPosition right) => !left.Equals(right); + + /// + /// Adds right to left. + /// + /// + /// + /// + public static StreamPosition operator +(StreamPosition left, ulong right) { + checked { + return new StreamPosition(left._value + right); + } + } + + /// + /// Adds right to left. + /// + /// + /// + /// + public static StreamPosition operator +(ulong left, StreamPosition right) { + checked { + return new StreamPosition(left + right._value); + } + } + + /// + /// Subtracts right from left. + /// + /// + /// + /// + public static StreamPosition operator -(StreamPosition left, ulong right) { + checked { + return new StreamPosition(left._value - right); + } + } + + /// + /// Subtracts right from left. + /// + /// + /// + /// + public static StreamPosition operator -(ulong left, StreamPosition right) { + checked { + return new StreamPosition(left - right._value); + } + } + + /// + /// Compares whether left > right. + /// + /// + /// + /// + public static bool operator >(StreamPosition left, StreamPosition right) => left._value > right._value; + + /// + /// Compares whether left < right. + /// + /// + /// + /// + public static bool operator <(StreamPosition left, StreamPosition right) => left._value < right._value; + + /// + /// Compares whether left >= right. + /// + /// + /// + /// + public static bool operator >=(StreamPosition left, StreamPosition right) => left._value >= right._value; + + /// + /// Compares whether left <= right. + /// + /// + /// + /// + public static bool operator <=(StreamPosition left, StreamPosition right) => left._value <= right._value; + + /// + /// Converts the to a . It is not meant to be used directly from your code. + /// + /// + public long ToInt64() => Equals(End) ? -1 : Convert.ToInt64(_value); + + /// + /// Converts a to a . + /// + /// + /// + public static implicit operator ulong(StreamPosition streamPosition) => streamPosition._value; + + /// + /// Converts a to a . + /// + /// + /// + public static implicit operator StreamPosition(ulong value) => new StreamPosition(value); + + /// + public override string ToString() => this == End ? "End" : _value.ToString(); + + /// + /// Converts the to a . + /// + /// + public ulong ToUInt64() => _value; + } +} diff --git a/src/Kurrent.Client/Core/StreamRevision.cs b/src/Kurrent.Client/Core/StreamRevision.cs new file mode 100644 index 000000000..89b553f53 --- /dev/null +++ b/src/Kurrent.Client/Core/StreamRevision.cs @@ -0,0 +1,196 @@ +using System; + +namespace EventStore.Client { + /// + /// A structure referring to the expected stream revision when writing to a stream. + /// + public readonly struct StreamRevision : IEquatable, IComparable, IComparable { + private readonly ulong _value; + + /// + /// Represents no , i.e., when a stream does not exist. + /// + public static readonly StreamRevision None = new StreamRevision(ulong.MaxValue); + + /// + /// Converts a to a . It is not meant to be used directly from your code. + /// + /// + /// + public static StreamRevision FromInt64(long value) => + value == -1 ? None : new StreamRevision(Convert.ToUInt64(value)); + + /// + /// Creates a new from the given . + /// + /// + /// + public static StreamRevision FromStreamPosition(StreamPosition position) => position.ToUInt64() switch { + ulong.MaxValue => throw new ArgumentOutOfRangeException(nameof(position)), + _ => new StreamRevision(position.ToUInt64()) + }; + + /// + /// Constructs a new . + /// + /// + /// + public StreamRevision(ulong value) { + if (value > long.MaxValue && value != ulong.MaxValue) { + throw new ArgumentOutOfRangeException(nameof(value)); + } + + _value = value; + } + + /// + /// Advances the to the next revision. + /// + /// + public StreamRevision Next() => this + 1; + + /// + public int CompareTo(StreamRevision other) => _value.CompareTo(other._value); + + /// + public int CompareTo(object? obj) => obj switch { + null => 1, + StreamRevision other => CompareTo(other), + _ => throw new ArgumentException("Object is not a StreamRevision"), + }; + + /// + public bool Equals(StreamRevision other) => _value == other._value; + + /// + public override bool Equals(object? obj) => obj is StreamRevision other && Equals(other); + + /// + public override int GetHashCode() => _value.GetHashCode(); + + /// + /// Compares left and right for equality. + /// + /// + /// + /// True if left is equal to right. + public static bool operator ==(StreamRevision left, StreamRevision right) => left.Equals(right); + + /// + /// Compares left and right for inequality. + /// + /// + /// + /// True if left is not equal to right. + public static bool operator !=(StreamRevision left, StreamRevision right) => !left.Equals(right); + + /// + /// Adds right to left. + /// + /// + /// + /// + public static StreamRevision operator +(StreamRevision left, ulong right) { + checked { + return new StreamRevision(left._value + right); + } + } + + /// + /// Adds right to left. + /// + /// + /// + /// + public static StreamRevision operator +(ulong left, StreamRevision right) { + checked { + return new StreamRevision(left + right._value); + } + } + + /// + /// Subtracts right from left. + /// + /// + /// + /// + public static StreamRevision operator -(StreamRevision left, ulong right) { + checked { + return new StreamRevision(left._value - right); + } + } + + /// + /// Subtracts right from left. + /// + /// + /// + /// + public static StreamRevision operator -(ulong left, StreamRevision right) { + checked { + return new StreamRevision(left - right._value); + } + } + + /// + /// Compares whether left > right. + /// + /// + /// + /// + public static bool operator >(StreamRevision left, StreamRevision right) => left._value > right._value; + + /// + /// Compares whether left < right. + /// + /// + /// + /// + public static bool operator <(StreamRevision left, StreamRevision right) => left._value < right._value; + + /// + /// Compares whether left >= right. + /// + /// + /// + /// + public static bool operator >=(StreamRevision left, StreamRevision right) => left._value >= right._value; + + /// + /// Compares whether left <= right. + /// + /// + /// + /// + public static bool operator <=(StreamRevision left, StreamRevision right) => left._value <= right._value; + + /// + /// Converts the to a . It is not meant to be used directly from your code. + /// + /// + public long ToInt64() => Equals(None) ? -1 : Convert.ToInt64(_value); + + /// + /// Converts a to a . + /// + /// + /// + public static implicit operator ulong(StreamRevision streamRevision) => streamRevision._value; + + /// + /// Converts a to a . + /// + /// + /// + public static implicit operator StreamRevision(ulong value) => new StreamRevision(value); + + /// + public override string ToString() => this == None ? nameof(None) : _value.ToString(); + + /// + /// Converts the to a . + /// + /// + public ulong ToUInt64() => _value; + } +} diff --git a/src/Kurrent.Client/Core/StreamState.cs b/src/Kurrent.Client/Core/StreamState.cs new file mode 100644 index 000000000..a0a021726 --- /dev/null +++ b/src/Kurrent.Client/Core/StreamState.cs @@ -0,0 +1,88 @@ +using System; + +namespace EventStore.Client { + /// + /// A structure that represents the state the stream should be in when writing. + /// + public readonly struct StreamState : IEquatable { + /// + /// The stream should not exist. + /// + public static readonly StreamState NoStream = new StreamState(Constants.NoStream); + + /// + /// The stream may or may not exist. + /// + public static readonly StreamState Any = new StreamState(Constants.Any); + + /// + /// The stream must exist. + /// + public static readonly StreamState StreamExists = new StreamState(Constants.StreamExists); + + private readonly int _value; + + private static class Constants { + public const int NoStream = 1; + public const int Any = 2; + public const int StreamExists = 4; + } + + internal StreamState(int value) { + switch (value) { + case Constants.NoStream: + case Constants.Any: + case Constants.StreamExists: + _value = value; + return; + default: + throw new ArgumentOutOfRangeException(nameof(value)); + } + } + + /// + public bool Equals(StreamState other) => _value == other._value; + + /// + public override bool Equals(object? obj) => obj is StreamState other && Equals(other); + + /// + public override int GetHashCode() => HashCode.Hash.Combine(_value); + + /// + /// Compares left and right for equality. + /// + /// + /// + /// Returns True when left and right are equal. + public static bool operator ==(StreamState left, StreamState right) => left.Equals(right); + + /// + /// Compares left and right for inequality. + /// + /// + /// + /// Returns True when left and right are not equal. + public static bool operator !=(StreamState left, StreamState right) => !left.Equals(right); + + /// + /// Converts the to a . It is not meant to be used directly from your code. + /// + /// + public long ToInt64() => -Convert.ToInt64(_value); + + /// + /// Converts the to an . It is not meant to be used directly from your code. + /// + /// + public static implicit operator int(StreamState streamState) => streamState._value; + + /// + public override string ToString() => _value switch { + Constants.NoStream => nameof(NoStream), + Constants.Any => nameof(Any), + Constants.StreamExists => nameof(StreamExists), + _ => _value.ToString() + }; + } +} diff --git a/src/Kurrent.Client/Core/SubscriptionDroppedReason.cs b/src/Kurrent.Client/Core/SubscriptionDroppedReason.cs new file mode 100644 index 000000000..1865a5d44 --- /dev/null +++ b/src/Kurrent.Client/Core/SubscriptionDroppedReason.cs @@ -0,0 +1,19 @@ +namespace EventStore.Client { + /// + /// Represents the reason subscription was dropped. + /// + public enum SubscriptionDroppedReason { + /// + /// Subscription was dropped because the subscription was disposed. + /// + Disposed, + /// + /// Subscription was dropped because of an error in user code. + /// + SubscriberError, + /// + /// Subscription was dropped because of a server error. + /// + ServerError + } +} diff --git a/src/Kurrent.Client/Core/SystemRoles.cs b/src/Kurrent.Client/Core/SystemRoles.cs new file mode 100644 index 000000000..b9cb068f7 --- /dev/null +++ b/src/Kurrent.Client/Core/SystemRoles.cs @@ -0,0 +1,21 @@ +namespace EventStore.Client { + /// + /// Roles used by the system. + /// + public static class SystemRoles { + /// + /// The $admins role. + /// + public const string Admins = "$admins"; + + /// + /// The $ops role. + /// + public const string Operations = "$ops"; + + /// + /// The $all role. + /// + public const string All = "$all"; + } +} diff --git a/src/Kurrent.Client/Core/SystemStreams.cs b/src/Kurrent.Client/Core/SystemStreams.cs new file mode 100644 index 000000000..c8b72503a --- /dev/null +++ b/src/Kurrent.Client/Core/SystemStreams.cs @@ -0,0 +1,54 @@ +namespace EventStore.Client { + /// + /// A collection of constants and methods to identify streams. + /// + public static class SystemStreams { + /// + /// A stream containing all events in the KurrentDB transaction file. + /// + public const string AllStream = "$all"; + + /// + /// A stream containing links pointing to each stream in the KurrentDB. + /// + public const string StreamsStream = "$streams"; + + /// + /// A stream containing system settings. + /// + public const string SettingsStream = "$settings"; + + /// + /// A stream containing statistics. + /// + public const string StatsStreamPrefix = "$stats"; + + /// + /// Returns True if the stream is a system stream. + /// + /// + /// + public static bool IsSystemStream(string streamId) => streamId.Length != 0 && streamId[0] == '$'; + + /// + /// Returns the metadata stream of the stream. + /// + /// + /// + public static string MetastreamOf(string streamId) => "$$" + streamId; + + /// + /// Returns true if the stream is a metadata stream. + /// + /// + /// + public static bool IsMetastream(string streamId) => streamId[..2] == "$$"; + + /// + /// Returns the original stream of the metadata stream. + /// + /// + /// + public static string OriginalStreamOf(string metastreamId) => metastreamId[2..]; + } +} diff --git a/src/Kurrent.Client/Core/TaskExtensions.cs b/src/Kurrent.Client/Core/TaskExtensions.cs new file mode 100644 index 000000000..5511b1c20 --- /dev/null +++ b/src/Kurrent.Client/Core/TaskExtensions.cs @@ -0,0 +1,22 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace EventStore.Client { + internal static class TaskExtensions { + // To give up waiting for the task, cancel the token. + // obvs this wouldn't cancel the task itself. + public static async ValueTask WithCancellation(this Task task, CancellationToken cancellationToken) { + if (task.Status == TaskStatus.RanToCompletion) + return task.Result; + + await Task + .WhenAny( + task, + Task.Delay(-1, cancellationToken)) + .ConfigureAwait(false); + + cancellationToken.ThrowIfCancellationRequested(); + return await task.ConfigureAwait(false); + } + } +} diff --git a/src/Kurrent.Client/Core/UserCredentials.cs b/src/Kurrent.Client/Core/UserCredentials.cs new file mode 100644 index 000000000..6f9012951 --- /dev/null +++ b/src/Kurrent.Client/Core/UserCredentials.cs @@ -0,0 +1,54 @@ +using System.Net.Http.Headers; +using System.Text; +using static System.Convert; + +namespace EventStore.Client { + /// + /// Represents either a username/password pair or a JWT token used for authentication and + /// authorization to perform operations on the KurrentDB. + /// + public class UserCredentials { + // ReSharper disable once InconsistentNaming + static readonly UTF8Encoding UTF8NoBom = new UTF8Encoding(false); + + /// + /// Constructs a new . + /// + public UserCredentials(string username, string password) { + Username = username; + Password = password; + + Authorization = new( + Constants.Headers.BasicScheme, + ToBase64String(UTF8NoBom.GetBytes($"{username}:{password}")) + ); + } + + /// + /// Constructs a new . + /// + public UserCredentials(string bearerToken) { + Authorization = new(Constants.Headers.BearerScheme, bearerToken); + } + + AuthenticationHeaderValue Authorization { get; } + + /// + /// The username + /// + public string? Username { get; } + + /// + /// The password + /// + public string? Password { get; } + + /// + public override string ToString() => Authorization.ToString(); + + /// + /// Implicitly convert a to a . + /// + public static implicit operator string(UserCredentials self) => self.ToString(); + } +} diff --git a/src/Kurrent.Client/Core/Uuid.cs b/src/Kurrent.Client/Core/Uuid.cs new file mode 100644 index 000000000..cbe7a7686 --- /dev/null +++ b/src/Kurrent.Client/Core/Uuid.cs @@ -0,0 +1,203 @@ +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace EventStore.Client { + /// + /// An RFC-4122 compliant v4 UUID. + /// + public readonly struct Uuid : IEquatable { + /// + /// Represents the empty (00000000-0000-0000-0000-000000000000) . + /// + /// + /// This reorders the bits in System.Guid to improve interop with other languages. See: https://stackoverflow.com/a/16722909 + /// + public static readonly Uuid Empty = new Uuid(Guid.Empty); + + private readonly long _lsb; + private readonly long _msb; + + /// + /// Creates a new, randomized . + /// + /// + public static Uuid NewUuid() => new Uuid(Guid.NewGuid()); + + /// + /// Converts a to a . + /// + /// + /// + public static Uuid FromGuid(Guid value) => new Uuid(value); + + /// + /// Parses a into a . + /// + /// + /// + public static Uuid Parse(string value) => new Uuid(value); + + /// + /// Creates a from a pair of . + /// + /// The representing the most significant bits. + /// The representing the least significant bits. + /// + public static Uuid FromInt64(long msb, long lsb) => new Uuid(msb, lsb); + + /// + /// Creates a from the gRPC wire format. + /// + /// + /// + internal static Uuid FromDto(UUID dto) => + dto == null + ? throw new ArgumentNullException(nameof(dto)) + : dto.ValueCase switch { + UUID.ValueOneofCase.String => new Uuid(dto.String), + UUID.ValueOneofCase.Structured => new Uuid(dto.Structured.MostSignificantBits, + dto.Structured.LeastSignificantBits), + _ => throw new ArgumentException($"Invalid argument: {dto.ValueCase}", nameof(dto)) + }; + + private Uuid(Guid value) { + if (!BitConverter.IsLittleEndian) { + throw new NotSupportedException(); + } + + Span data = stackalloc byte[16]; + + if (!TryWriteGuidBytes(value, data)) { + throw new InvalidOperationException(); + } + + data[..8].Reverse(); + data[..2].Reverse(); + data.Slice(2, 2).Reverse(); + data.Slice(4, 4).Reverse(); + data[8..].Reverse(); + + _msb = BitConverterToInt64(data); + _lsb = BitConverterToInt64(data[8..]); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static long BitConverterToInt64(ReadOnlySpan value) + { +#if NET + return BitConverter.ToInt64(value); +#else + return Unsafe.ReadUnaligned(ref MemoryMarshal.GetReference(value)); +#endif + } + + private Uuid(string value) : this(value == null + ? throw new ArgumentNullException(nameof(value)) + : Guid.Parse(value)) { + } + + private Uuid(long msb, long lsb) { + _msb = msb; + _lsb = lsb; + } + + /// + /// Converts the to the gRPC wire format. + /// + /// + internal UUID ToDto() => + new UUID { + Structured = new UUID.Types.Structured { + LeastSignificantBits = _lsb, + MostSignificantBits = _msb + } + }; + + + /// + public bool Equals(Uuid other) => _lsb == other._lsb && _msb == other._msb; + + /// + public override bool Equals(object? obj) => obj is Uuid other && Equals(other); + + /// + public override int GetHashCode() => HashCode.Hash.Combine(_lsb).Combine(_msb); + + /// + /// Compares left and right for equality. + /// + /// A + /// A + /// True if left is equal to right. + public static bool operator ==(Uuid left, Uuid right) => left.Equals(right); + + /// + /// Compares left and right for inequality. + /// + /// A + /// A + /// True if left is not equal to right. + public static bool operator !=(Uuid left, Uuid right) => !left.Equals(right); + + /// + public override string ToString() => ToGuid().ToString(); + + /// + /// Converts the to a based on the supplied format. + /// + /// + /// + public string ToString(string format) => ToGuid().ToString(format); + + /// + /// Converts the to a . + /// + /// + public Guid ToGuid() { + if (!BitConverter.IsLittleEndian) { + throw new NotSupportedException(); + } + + Span data = stackalloc byte[16]; + if (!TryWriteBytes(data, _msb) || + !TryWriteBytes(data[8..], _lsb)) { + throw new InvalidOperationException(); + } + + data[..8].Reverse(); + data[..4].Reverse(); + data.Slice(4, 2).Reverse(); + data.Slice(6, 2).Reverse(); + data[8..].Reverse(); + +#if NET + return new Guid(data); +#else + return new Guid(data.ToArray()); +#endif + } + private static bool TryWriteBytes(Span destination, long value) + { + if (destination.Length < sizeof(long)) + return false; + + Unsafe.WriteUnaligned(ref MemoryMarshal.GetReference(destination), value); + return true; + } + + private bool TryWriteGuidBytes(Guid value, Span destination) + { +#if NET + return value.TryWriteBytes(destination); +#else + if (destination.Length < 16) + return false; + + var bytes = value.ToByteArray(); + bytes.CopyTo(destination); + return true; +#endif + } + } +} diff --git a/src/Kurrent.Client/Core/protos/code.proto b/src/Kurrent.Client/Core/protos/code.proto new file mode 100644 index 000000000..98ae0ac18 --- /dev/null +++ b/src/Kurrent.Client/Core/protos/code.proto @@ -0,0 +1,186 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package google.rpc; + +option go_package = "google.golang.org/genproto/googleapis/rpc/code;code"; +option java_multiple_files = true; +option java_outer_classname = "CodeProto"; +option java_package = "com.google.rpc"; +option objc_class_prefix = "RPC"; + +// The canonical error codes for gRPC APIs. +// +// +// Sometimes multiple error codes may apply. Services should return +// the most specific error code that applies. For example, prefer +// `OUT_OF_RANGE` over `FAILED_PRECONDITION` if both codes apply. +// Similarly prefer `NOT_FOUND` or `ALREADY_EXISTS` over `FAILED_PRECONDITION`. +enum Code { + // Not an error; returned on success + // + // HTTP Mapping: 200 OK + OK = 0; + + // The operation was cancelled, typically by the caller. + // + // HTTP Mapping: 499 Client Closed Request + CANCELLED = 1; + + // Unknown error. For example, this error may be returned when + // a `Status` value received from another address space belongs to + // an error space that is not known in this address space. Also + // errors raised by APIs that do not return enough error information + // may be converted to this error. + // + // HTTP Mapping: 500 Internal Server Error + UNKNOWN = 2; + + // The client specified an invalid argument. Note that this differs + // from `FAILED_PRECONDITION`. `INVALID_ARGUMENT` indicates arguments + // that are problematic regardless of the state of the system + // (e.g., a malformed file name). + // + // HTTP Mapping: 400 Bad Request + INVALID_ARGUMENT = 3; + + // The deadline expired before the operation could complete. For operations + // that change the state of the system, this error may be returned + // even if the operation has completed successfully. For example, a + // successful response from a server could have been delayed long + // enough for the deadline to expire. + // + // HTTP Mapping: 504 Gateway Timeout + DEADLINE_EXCEEDED = 4; + + // Some requested entity (e.g., file or directory) was not found. + // + // Note to server developers: if a request is denied for an entire class + // of users, such as gradual feature rollout or undocumented whitelist, + // `NOT_FOUND` may be used. If a request is denied for some users within + // a class of users, such as user-based access control, `PERMISSION_DENIED` + // must be used. + // + // HTTP Mapping: 404 Not Found + NOT_FOUND = 5; + + // The entity that a client attempted to create (e.g., file or directory) + // already exists. + // + // HTTP Mapping: 409 Conflict + ALREADY_EXISTS = 6; + + // The caller does not have permission to execute the specified + // operation. `PERMISSION_DENIED` must not be used for rejections + // caused by exhausting some resource (use `RESOURCE_EXHAUSTED` + // instead for those errors). `PERMISSION_DENIED` must not be + // used if the caller can not be identified (use `UNAUTHENTICATED` + // instead for those errors). This error code does not imply the + // request is valid or the requested entity exists or satisfies + // other pre-conditions. + // + // HTTP Mapping: 403 Forbidden + PERMISSION_DENIED = 7; + + // The request does not have valid authentication credentials for the + // operation. + // + // HTTP Mapping: 401 Unauthorized + UNAUTHENTICATED = 16; + + // Some resource has been exhausted, perhaps a per-user quota, or + // perhaps the entire file system is out of space. + // + // HTTP Mapping: 429 Too Many Requests + RESOURCE_EXHAUSTED = 8; + + // The operation was rejected because the system is not in a state + // required for the operation's execution. For example, the directory + // to be deleted is non-empty, an rmdir operation is applied to + // a non-directory, etc. + // + // Service implementors can use the following guidelines to decide + // between `FAILED_PRECONDITION`, `ABORTED`, and `UNAVAILABLE`: + // (a) Use `UNAVAILABLE` if the client can retry just the failing call. + // (b) Use `ABORTED` if the client should retry at a higher level + // (e.g., when a client-specified test-and-set fails, indicating the + // client should restart a read-modify-write sequence). + // (c) Use `FAILED_PRECONDITION` if the client should not retry until + // the system state has been explicitly fixed. E.g., if an "rmdir" + // fails because the directory is non-empty, `FAILED_PRECONDITION` + // should be returned since the client should not retry unless + // the files are deleted from the directory. + // + // HTTP Mapping: 400 Bad Request + FAILED_PRECONDITION = 9; + + // The operation was aborted, typically due to a concurrency issue such as + // a sequencer check failure or transaction abort. + // + // See the guidelines above for deciding between `FAILED_PRECONDITION`, + // `ABORTED`, and `UNAVAILABLE`. + // + // HTTP Mapping: 409 Conflict + ABORTED = 10; + + // The operation was attempted past the valid range. E.g., seeking or + // reading past end-of-file. + // + // Unlike `INVALID_ARGUMENT`, this error indicates a problem that may + // be fixed if the system state changes. For example, a 32-bit file + // system will generate `INVALID_ARGUMENT` if asked to read at an + // offset that is not in the range [0,2^32-1], but it will generate + // `OUT_OF_RANGE` if asked to read from an offset past the current + // file size. + // + // There is a fair bit of overlap between `FAILED_PRECONDITION` and + // `OUT_OF_RANGE`. We recommend using `OUT_OF_RANGE` (the more specific + // error) when it applies so that callers who are iterating through + // a space can easily look for an `OUT_OF_RANGE` error to detect when + // they are done. + // + // HTTP Mapping: 400 Bad Request + OUT_OF_RANGE = 11; + + // The operation is not implemented or is not supported/enabled in this + // service. + // + // HTTP Mapping: 501 Not Implemented + UNIMPLEMENTED = 12; + + // Internal errors. This means that some invariants expected by the + // underlying system have been broken. This error code is reserved + // for serious errors. + // + // HTTP Mapping: 500 Internal Server Error + INTERNAL = 13; + + // The service is currently unavailable. This is most likely a + // transient condition, which can be corrected by retrying with + // a backoff. Note that it is not always safe to retry + // non-idempotent operations. + // + // See the guidelines above for deciding between `FAILED_PRECONDITION`, + // `ABORTED`, and `UNAVAILABLE`. + // + // HTTP Mapping: 503 Service Unavailable + UNAVAILABLE = 14; + + // Unrecoverable data loss or corruption. + // + // HTTP Mapping: 500 Internal Server Error + DATA_LOSS = 15; +} diff --git a/src/Kurrent.Client/Core/protos/gossip.proto b/src/Kurrent.Client/Core/protos/gossip.proto new file mode 100644 index 000000000..f4ea9bcd4 --- /dev/null +++ b/src/Kurrent.Client/Core/protos/gossip.proto @@ -0,0 +1,44 @@ +syntax = "proto3"; +package event_store.client.gossip; +option java_package = "io.kurrent.client.gossip"; + +import "shared.proto"; + +service Gossip { + rpc Read (event_store.client.Empty) returns (ClusterInfo); +} + +message ClusterInfo { + repeated MemberInfo members = 1; +} + +message EndPoint { + string address = 1; + uint32 port = 2; +} + +message MemberInfo { + enum VNodeState { + Initializing = 0; + DiscoverLeader = 1; + Unknown = 2; + PreReplica = 3; + CatchingUp = 4; + Clone = 5; + Follower = 6; + PreLeader = 7; + Leader = 8; + Manager = 9; + ShuttingDown = 10; + Shutdown = 11; + ReadOnlyLeaderless = 12; + PreReadOnlyReplica = 13; + ReadOnlyReplica = 14; + ResigningLeader = 15; + } + event_store.client.UUID instance_id = 1; + int64 time_stamp = 2; + VNodeState state = 3; + bool is_alive = 4; + EndPoint http_end_point = 5; +} diff --git a/src/Kurrent.Client/Core/protos/operations.proto b/src/Kurrent.Client/Core/protos/operations.proto new file mode 100644 index 000000000..f4f9ae3c3 --- /dev/null +++ b/src/Kurrent.Client/Core/protos/operations.proto @@ -0,0 +1,45 @@ +syntax = "proto3"; +package event_store.client.operations; +option java_package = "io.kurrent.client.operations"; + +import "shared.proto"; + +service Operations { + rpc StartScavenge (StartScavengeReq) returns (ScavengeResp); + rpc StopScavenge (StopScavengeReq) returns (ScavengeResp); + rpc Shutdown (Empty) returns (Empty); + rpc MergeIndexes (Empty) returns (Empty); + rpc ResignNode (Empty) returns (Empty); + rpc SetNodePriority (SetNodePriorityReq) returns (Empty); + rpc RestartPersistentSubscriptions (Empty) returns (Empty); +} + +message StartScavengeReq { + Options options = 1; + message Options { + int32 thread_count = 1; + int32 start_from_chunk = 2; + } +} + +message StopScavengeReq { + Options options = 1; + message Options { + string scavenge_id = 1; + } +} + +message ScavengeResp { + string scavenge_id = 1; + ScavengeResult scavenge_result = 2; + + enum ScavengeResult { + Started = 0; + InProgress = 1; + Stopped = 2; + } +} + +message SetNodePriorityReq { + int32 priority = 1; +} diff --git a/src/Kurrent.Client/Core/protos/persistentsubscriptions.proto b/src/Kurrent.Client/Core/protos/persistentsubscriptions.proto new file mode 100644 index 000000000..ac63a3a6d --- /dev/null +++ b/src/Kurrent.Client/Core/protos/persistentsubscriptions.proto @@ -0,0 +1,370 @@ +syntax = "proto3"; +package event_store.client.persistent_subscriptions; +option java_package = "io.kurrent.dbclient.proto.persistentsubscriptions"; + +import "shared.proto"; + +service PersistentSubscriptions { + rpc Create (CreateReq) returns (CreateResp); + rpc Update (UpdateReq) returns (UpdateResp); + rpc Delete (DeleteReq) returns (DeleteResp); + rpc Read (stream ReadReq) returns (stream ReadResp); + rpc GetInfo (GetInfoReq) returns (GetInfoResp); + rpc ReplayParked (ReplayParkedReq) returns (ReplayParkedResp); + rpc List (ListReq) returns (ListResp); + rpc RestartSubsystem (event_store.client.Empty) returns (event_store.client.Empty); +} + +message ReadReq { + oneof content { + Options options = 1; + Ack ack = 2; + Nack nack = 3; + } + + message Options { + oneof stream_option { + event_store.client.StreamIdentifier stream_identifier = 1; + event_store.client.Empty all = 5; + } + + string group_name = 2; + int32 buffer_size = 3; + UUIDOption uuid_option = 4; + + message UUIDOption { + oneof content { + event_store.client.Empty structured = 1; + event_store.client.Empty string = 2; + } + } + } + + message Ack { + bytes id = 1; + repeated event_store.client.UUID ids = 2; + } + + message Nack { + bytes id = 1; + repeated event_store.client.UUID ids = 2; + Action action = 3; + string reason = 4; + + enum Action { + Unknown = 0; + Park = 1; + Retry = 2; + Skip = 3; + Stop = 4; + } + } +} + +message ReadResp { + oneof content { + ReadEvent event = 1; + SubscriptionConfirmation subscription_confirmation = 2; + } + message ReadEvent { + RecordedEvent event = 1; + RecordedEvent link = 2; + oneof position { + uint64 commit_position = 3; + event_store.client.Empty no_position = 4; + } + oneof count { + int32 retry_count = 5; + event_store.client.Empty no_retry_count = 6; + } + message RecordedEvent { + event_store.client.UUID id = 1; + event_store.client.StreamIdentifier stream_identifier = 2; + uint64 stream_revision = 3; + uint64 prepare_position = 4; + uint64 commit_position = 5; + map metadata = 6; + bytes custom_metadata = 7; + bytes data = 8; + } + } + message SubscriptionConfirmation { + string subscription_id = 1; + } +} + +message CreateReq { + Options options = 1; + + message Options { + oneof stream_option { + StreamOptions stream = 4; + AllOptions all = 5; + } + event_store.client.StreamIdentifier stream_identifier = 1 [deprecated=true]; + string group_name = 2; + Settings settings = 3; + } + + message StreamOptions { + event_store.client.StreamIdentifier stream_identifier = 1; + oneof revision_option { + uint64 revision = 2; + event_store.client.Empty start = 3; + event_store.client.Empty end = 4; + } + } + + message AllOptions { + oneof all_option { + Position position = 1; + event_store.client.Empty start = 2; + event_store.client.Empty end = 3; + } + oneof filter_option { + FilterOptions filter = 4; + event_store.client.Empty no_filter = 5; + } + message FilterOptions { + oneof filter { + Expression stream_identifier = 1; + Expression event_type = 2; + } + oneof window { + uint32 max = 3; + event_store.client.Empty count = 4; + } + uint32 checkpointIntervalMultiplier = 5; + + message Expression { + string regex = 1; + repeated string prefix = 2; + } + } + } + + message Position { + uint64 commit_position = 1; + uint64 prepare_position = 2; + } + + message Settings { + bool resolve_links = 1; + uint64 revision = 2 [deprecated = true]; + bool extra_statistics = 3; + int32 max_retry_count = 5; + int32 min_checkpoint_count = 7; + int32 max_checkpoint_count = 8; + int32 max_subscriber_count = 9; + int32 live_buffer_size = 10; + int32 read_batch_size = 11; + int32 history_buffer_size = 12; + ConsumerStrategy named_consumer_strategy = 13 [deprecated = true]; + oneof message_timeout { + int64 message_timeout_ticks = 4; + int32 message_timeout_ms = 14; + } + oneof checkpoint_after { + int64 checkpoint_after_ticks = 6; + int32 checkpoint_after_ms = 15; + } + string consumer_strategy = 16; + } + + enum ConsumerStrategy { + DispatchToSingle = 0; + RoundRobin = 1; + Pinned = 2; + } +} + +message CreateResp { +} + +message UpdateReq { + Options options = 1; + + message Options { + oneof stream_option { + StreamOptions stream = 4; + AllOptions all = 5; + } + event_store.client.StreamIdentifier stream_identifier = 1 [deprecated = true]; + string group_name = 2; + Settings settings = 3; + } + + message StreamOptions { + event_store.client.StreamIdentifier stream_identifier = 1; + oneof revision_option { + uint64 revision = 2; + event_store.client.Empty start = 3; + event_store.client.Empty end = 4; + } + } + + message AllOptions { + oneof all_option { + Position position = 1; + event_store.client.Empty start = 2; + event_store.client.Empty end = 3; + } + } + + message Position { + uint64 commit_position = 1; + uint64 prepare_position = 2; + } + + message Settings { + bool resolve_links = 1; + uint64 revision = 2 [deprecated = true]; + bool extra_statistics = 3; + int32 max_retry_count = 5; + int32 min_checkpoint_count = 7; + int32 max_checkpoint_count = 8; + int32 max_subscriber_count = 9; + int32 live_buffer_size = 10; + int32 read_batch_size = 11; + int32 history_buffer_size = 12; + ConsumerStrategy named_consumer_strategy = 13; + oneof message_timeout { + int64 message_timeout_ticks = 4; + int32 message_timeout_ms = 14; + } + oneof checkpoint_after { + int64 checkpoint_after_ticks = 6; + int32 checkpoint_after_ms = 15; + } + } + + enum ConsumerStrategy { + DispatchToSingle = 0; + RoundRobin = 1; + Pinned = 2; + } +} + +message UpdateResp { +} + +message DeleteReq { + Options options = 1; + + message Options { + oneof stream_option { + event_store.client.StreamIdentifier stream_identifier = 1; + event_store.client.Empty all = 3; + } + + string group_name = 2; + } +} + +message DeleteResp { +} + +message GetInfoReq { + Options options = 1; + + message Options { + oneof stream_option { + event_store.client.StreamIdentifier stream_identifier = 1; + event_store.client.Empty all = 2; + } + + string group_name = 3; + } +} + +message GetInfoResp { + SubscriptionInfo subscription_info = 1; +} + +message SubscriptionInfo { + string event_source = 1; + string group_name = 2; + string status = 3; + repeated ConnectionInfo connections = 4; + int32 average_per_second = 5; + int64 total_items = 6; + int64 count_since_last_measurement = 7; + string last_checkpointed_event_position = 8; + string last_known_event_position = 9; + bool resolve_link_tos = 10; + string start_from = 11; + int32 message_timeout_milliseconds = 12; + bool extra_statistics = 13; + int32 max_retry_count = 14; + int32 live_buffer_size = 15; + int32 buffer_size = 16; + int32 read_batch_size = 17; + int32 check_point_after_milliseconds = 18; + int32 min_check_point_count = 19; + int32 max_check_point_count = 20; + int32 read_buffer_count = 21; + int64 live_buffer_count = 22; + int32 retry_buffer_count = 23; + int32 total_in_flight_messages = 24; + int32 outstanding_messages_count = 25; + string named_consumer_strategy = 26; + int32 max_subscriber_count = 27; + int64 parked_message_count = 28; + + message ConnectionInfo { + string from = 1; + string username = 2; + int32 average_items_per_second = 3; + int64 total_items = 4; + int64 count_since_last_measurement = 5; + repeated Measurement observed_measurements = 6; + int32 available_slots = 7; + int32 in_flight_messages = 8; + string connection_name = 9; + } + + message Measurement { + string key = 1; + int64 value = 2; + } +} + +message ReplayParkedReq { + Options options = 1; + + message Options { + string group_name = 1; + oneof stream_option { + event_store.client.StreamIdentifier stream_identifier = 2; + event_store.client.Empty all = 3; + } + oneof stop_at_option { + int64 stop_at = 4; + event_store.client.Empty no_limit = 5; + } + } +} + +message ReplayParkedResp { +} + +message ListReq { + Options options = 1; + + message Options { + oneof list_option { + event_store.client.Empty list_all_subscriptions = 1; + StreamOption list_for_stream = 2; + } + } + message StreamOption { + oneof stream_option { + event_store.client.StreamIdentifier stream = 1; + event_store.client.Empty all = 2; + } + } +} + +message ListResp { + repeated SubscriptionInfo subscriptions = 1; +} diff --git a/src/Kurrent.Client/Core/protos/projectionmanagement.proto b/src/Kurrent.Client/Core/protos/projectionmanagement.proto new file mode 100644 index 000000000..f16877012 --- /dev/null +++ b/src/Kurrent.Client/Core/protos/projectionmanagement.proto @@ -0,0 +1,174 @@ +syntax = "proto3"; +package event_store.client.projections; +option java_package = "io.kurrent.client.projections"; + +import "google/protobuf/struct.proto"; +import "shared.proto"; + +service Projections { + rpc Create (CreateReq) returns (CreateResp); + rpc Update (UpdateReq) returns (UpdateResp); + rpc Delete (DeleteReq) returns (DeleteResp); + rpc Statistics (StatisticsReq) returns (stream StatisticsResp); + rpc Disable (DisableReq) returns (DisableResp); + rpc Enable (EnableReq) returns (EnableResp); + rpc Reset (ResetReq) returns (ResetResp); + rpc State (StateReq) returns (StateResp); + rpc Result (ResultReq) returns (ResultResp); + rpc RestartSubsystem (Empty) returns (Empty); +} + +message CreateReq { + Options options = 1; + + message Options { + oneof mode { + event_store.client.Empty one_time = 1; + Transient transient = 2; + Continuous continuous = 3; + } + string query = 4; + + message Transient { + string name = 1; + } + message Continuous { + string name = 1; + bool track_emitted_streams = 2; + } + } +} + +message CreateResp { +} + +message UpdateReq { + Options options = 1; + + message Options { + string name = 1; + string query = 2; + oneof emit_option { + bool emit_enabled = 3; + event_store.client.Empty no_emit_options = 4; + } + } +} + +message UpdateResp { +} + +message DeleteReq { + Options options = 1; + + message Options { + string name = 1; + bool delete_emitted_streams = 2; + bool delete_state_stream = 3; + bool delete_checkpoint_stream = 4; + } +} + +message DeleteResp { +} + +message StatisticsReq { + Options options = 1; + message Options { + oneof mode { + string name = 1; + event_store.client.Empty all = 2; + event_store.client.Empty transient = 3; + event_store.client.Empty continuous = 4; + event_store.client.Empty one_time = 5; + } + } +} + +message StatisticsResp { + Details details = 1; + + message Details { + int64 coreProcessingTime = 1; + int64 version = 2; + int64 epoch = 3; + string effectiveName = 4; + int32 writesInProgress = 5; + int32 readsInProgress = 6; + int32 partitionsCached = 7; + string status = 8; + string stateReason = 9; + string name = 10; + string mode = 11; + string position = 12; + float progress = 13; + string lastCheckpoint = 14; + int64 eventsProcessedAfterRestart = 15; + string checkpointStatus = 16; + int64 bufferedEvents = 17; + int32 writePendingEventsBeforeCheckpoint = 18; + int32 writePendingEventsAfterCheckpoint = 19; + } +} + +message StateReq { + Options options = 1; + + message Options { + string name = 1; + string partition = 2; + } +} + +message StateResp { + google.protobuf.Value state = 1; +} + +message ResultReq { + Options options = 1; + + message Options { + string name = 1; + string partition = 2; + } +} + +message ResultResp { + google.protobuf.Value result = 1; +} + +message ResetReq { + Options options = 1; + + message Options { + string name = 1; + bool write_checkpoint = 2; + } +} + +message ResetResp { +} + + +message EnableReq { + Options options = 1; + + message Options { + string name = 1; + } +} + +message EnableResp { +} + +message DisableReq { + Options options = 1; + + message Options { + string name = 1; + bool write_checkpoint = 2; + } +} + +message DisableResp { +} diff --git a/src/Kurrent.Client/Core/protos/serverfeatures.proto b/src/Kurrent.Client/Core/protos/serverfeatures.proto new file mode 100644 index 000000000..61c4ab773 --- /dev/null +++ b/src/Kurrent.Client/Core/protos/serverfeatures.proto @@ -0,0 +1,19 @@ +syntax = "proto3"; +package event_store.client.server_features; +option java_package = "io.kurrent.dbclient.proto.serverfeatures"; +import "shared.proto"; + +service ServerFeatures { + rpc GetSupportedMethods (event_store.client.Empty) returns (SupportedMethods); +} + +message SupportedMethods { + repeated SupportedMethod methods = 1; + string event_store_server_version = 2; +} + +message SupportedMethod { + string method_name = 1; + string service_name = 2; + repeated string features = 3; +} diff --git a/src/Kurrent.Client/Core/protos/shared.proto b/src/Kurrent.Client/Core/protos/shared.proto new file mode 100644 index 000000000..24780afc3 --- /dev/null +++ b/src/Kurrent.Client/Core/protos/shared.proto @@ -0,0 +1,61 @@ +syntax = "proto3"; +package event_store.client; +option java_package = "io.kurrent.dbclient.proto.shared"; +import "google/protobuf/empty.proto"; + +message UUID { + oneof value { + Structured structured = 1; + string string = 2; + } + + message Structured { + int64 most_significant_bits = 1; + int64 least_significant_bits = 2; + } +} +message Empty { +} + +message StreamIdentifier { + reserved 1 to 2; + bytes stream_name = 3; +} + +message AllStreamPosition { + uint64 commit_position = 1; + uint64 prepare_position = 2; +} + +message WrongExpectedVersion { + oneof current_stream_revision_option { + uint64 current_stream_revision = 1; + google.protobuf.Empty current_no_stream = 2; + } + oneof expected_stream_position_option { + uint64 expected_stream_position = 3; + google.protobuf.Empty expected_any = 4; + google.protobuf.Empty expected_stream_exists = 5; + google.protobuf.Empty expected_no_stream = 6; + } +} + +message AccessDenied {} + +message StreamDeleted { + StreamIdentifier stream_identifier = 1; +} + +message Timeout {} + +message Unknown {} + +message InvalidTransaction {} + +message MaximumAppendSizeExceeded { + uint32 maxAppendSize = 1; +} + +message BadRequest { + string message = 1; +} diff --git a/src/Kurrent.Client/Core/protos/status.proto b/src/Kurrent.Client/Core/protos/status.proto new file mode 100644 index 000000000..65eced268 --- /dev/null +++ b/src/Kurrent.Client/Core/protos/status.proto @@ -0,0 +1,48 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package google.rpc; + +import "google/protobuf/any.proto"; +import "code.proto"; + +option cc_enable_arenas = true; +option go_package = "google.golang.org/genproto/googleapis/rpc/status;status"; +option java_multiple_files = true; +option java_outer_classname = "StatusProto"; +option java_package = "com.google.rpc"; +option objc_class_prefix = "RPC"; + +// The `Status` type defines a logical error model that is suitable for +// different programming environments, including REST APIs and RPC APIs. It is +// used by [gRPC](https://github.com/grpc). Each `Status` message contains +// three pieces of data: error code, error message, and error details. +// +// You can find out more about this error model and how to work with it in the +// [API Design Guide](https://cloud.google.com/apis/design/errors). +message Status { + // The status code, which should be an enum value of [google.rpc.Code][google.rpc.Code]. + google.rpc.Code code = 1; + + // A developer-facing error message, which should be in English. Any + // user-facing error message should be localized and sent in the + // [google.rpc.Status.details][google.rpc.Status.details] field, or localized by the client. + string message = 2; + + // A list of messages that carry the error details. There is a common set of + // message types for APIs to use. + google.protobuf.Any details = 3; +} diff --git a/src/Kurrent.Client/Core/protos/streams.proto b/src/Kurrent.Client/Core/protos/streams.proto new file mode 100644 index 000000000..0eb05295c --- /dev/null +++ b/src/Kurrent.Client/Core/protos/streams.proto @@ -0,0 +1,316 @@ +syntax = "proto3"; +package event_store.client.streams; +option java_package = "io.kurrent.dbclient.proto.streams"; + +import "shared.proto"; +import "status.proto"; +import "google/protobuf/duration.proto"; +import "google/protobuf/empty.proto"; +import "google/protobuf/timestamp.proto"; + +service Streams { + rpc Read (ReadReq) returns (stream ReadResp); + rpc Append (stream AppendReq) returns (AppendResp); + rpc Delete (DeleteReq) returns (DeleteResp); + rpc Tombstone (TombstoneReq) returns (TombstoneResp); + rpc BatchAppend (stream BatchAppendReq) returns (stream BatchAppendResp); +} + +message ReadReq { + Options options = 1; + + message Options { + oneof stream_option { + StreamOptions stream = 1; + AllOptions all = 2; + } + ReadDirection read_direction = 3; + bool resolve_links = 4; + oneof count_option { + uint64 count = 5; + SubscriptionOptions subscription = 6; + } + oneof filter_option { + FilterOptions filter = 7; + event_store.client.Empty no_filter = 8; + } + UUIDOption uuid_option = 9; + ControlOption control_option = 10; + + enum ReadDirection { + Forwards = 0; + Backwards = 1; + } + message StreamOptions { + event_store.client.StreamIdentifier stream_identifier = 1; + oneof revision_option { + uint64 revision = 2; + event_store.client.Empty start = 3; + event_store.client.Empty end = 4; + } + } + message AllOptions { + oneof all_option { + Position position = 1; + event_store.client.Empty start = 2; + event_store.client.Empty end = 3; + } + } + message SubscriptionOptions { + } + message Position { + uint64 commit_position = 1; + uint64 prepare_position = 2; + } + message FilterOptions { + oneof filter { + Expression stream_identifier = 1; + Expression event_type = 2; + } + oneof window { + uint32 max = 3; + event_store.client.Empty count = 4; + } + uint32 checkpointIntervalMultiplier = 5; + + message Expression { + string regex = 1; + repeated string prefix = 2; + } + } + message UUIDOption { + oneof content { + event_store.client.Empty structured = 1; + event_store.client.Empty string = 2; + } + } + message ControlOption { + uint32 compatibility = 1; + } + } +} + +message ReadResp { + oneof content { + ReadEvent event = 1; + SubscriptionConfirmation confirmation = 2; + Checkpoint checkpoint = 3; + StreamNotFound stream_not_found = 4; + uint64 first_stream_position = 5; + uint64 last_stream_position = 6; + AllStreamPosition last_all_stream_position = 7; + CaughtUp caught_up = 8; + FellBehind fell_behind = 9; + } + + message CaughtUp {} + + message FellBehind {} + + message ReadEvent { + RecordedEvent event = 1; + RecordedEvent link = 2; + oneof position { + uint64 commit_position = 3; + event_store.client.Empty no_position = 4; + } + + message RecordedEvent { + event_store.client.UUID id = 1; + event_store.client.StreamIdentifier stream_identifier = 2; + uint64 stream_revision = 3; + uint64 prepare_position = 4; + uint64 commit_position = 5; + map metadata = 6; + bytes custom_metadata = 7; + bytes data = 8; + } + } + message SubscriptionConfirmation { + string subscription_id = 1; + } + message Checkpoint { + uint64 commit_position = 1; + uint64 prepare_position = 2; + } + message StreamNotFound { + event_store.client.StreamIdentifier stream_identifier = 1; + } +} + +message AppendReq { + oneof content { + Options options = 1; + ProposedMessage proposed_message = 2; + } + + message Options { + event_store.client.StreamIdentifier stream_identifier = 1; + oneof expected_stream_revision { + uint64 revision = 2; + event_store.client.Empty no_stream = 3; + event_store.client.Empty any = 4; + event_store.client.Empty stream_exists = 5; + } + } + message ProposedMessage { + event_store.client.UUID id = 1; + map metadata = 2; + bytes custom_metadata = 3; + bytes data = 4; + } +} + +message AppendResp { + oneof result { + Success success = 1; + WrongExpectedVersion wrong_expected_version = 2; + } + + message Position { + uint64 commit_position = 1; + uint64 prepare_position = 2; + } + + message Success { + oneof current_revision_option { + uint64 current_revision = 1; + event_store.client.Empty no_stream = 2; + } + oneof position_option { + Position position = 3; + event_store.client.Empty no_position = 4; + } + } + + message WrongExpectedVersion { + oneof current_revision_option_20_6_0 { + uint64 current_revision_20_6_0 = 1; + event_store.client.Empty no_stream_20_6_0 = 2; + } + oneof expected_revision_option_20_6_0 { + uint64 expected_revision_20_6_0 = 3; + event_store.client.Empty any_20_6_0 = 4; + event_store.client.Empty stream_exists_20_6_0 = 5; + } + oneof current_revision_option { + uint64 current_revision = 6; + event_store.client.Empty current_no_stream = 7; + } + oneof expected_revision_option { + uint64 expected_revision = 8; + event_store.client.Empty expected_any = 9; + event_store.client.Empty expected_stream_exists = 10; + event_store.client.Empty expected_no_stream = 11; + } + + } +} + +message BatchAppendReq { + event_store.client.UUID correlation_id = 1; + Options options = 2; + repeated ProposedMessage proposed_messages = 3; + bool is_final = 4; + + message Options { + event_store.client.StreamIdentifier stream_identifier = 1; + oneof expected_stream_position { + uint64 stream_position = 2; + google.protobuf.Empty no_stream = 3; + google.protobuf.Empty any = 4; + google.protobuf.Empty stream_exists = 5; + } + oneof deadline_option { + google.protobuf.Timestamp deadline_21_10_0 = 6; + google.protobuf.Duration deadline = 7; + } + } + + message ProposedMessage { + event_store.client.UUID id = 1; + map metadata = 2; + bytes custom_metadata = 3; + bytes data = 4; + } +} + +message BatchAppendResp { + event_store.client.UUID correlation_id = 1; + oneof result { + google.rpc.Status error = 2; + Success success = 3; + } + + event_store.client.StreamIdentifier stream_identifier = 4; + + oneof expected_stream_position { + uint64 stream_position = 5; + google.protobuf.Empty no_stream = 6; + google.protobuf.Empty any = 7; + google.protobuf.Empty stream_exists = 8; + } + + message Success { + oneof current_revision_option { + uint64 current_revision = 1; + google.protobuf.Empty no_stream = 2; + } + oneof position_option { + event_store.client.AllStreamPosition position = 3; + google.protobuf.Empty no_position = 4; + } + } +} + +message DeleteReq { + Options options = 1; + + message Options { + event_store.client.StreamIdentifier stream_identifier = 1; + oneof expected_stream_revision { + uint64 revision = 2; + event_store.client.Empty no_stream = 3; + event_store.client.Empty any = 4; + event_store.client.Empty stream_exists = 5; + } + } +} + +message DeleteResp { + oneof position_option { + Position position = 1; + event_store.client.Empty no_position = 2; + } + + message Position { + uint64 commit_position = 1; + uint64 prepare_position = 2; + } +} + +message TombstoneReq { + Options options = 1; + + message Options { + event_store.client.StreamIdentifier stream_identifier = 1; + oneof expected_stream_revision { + uint64 revision = 2; + event_store.client.Empty no_stream = 3; + event_store.client.Empty any = 4; + event_store.client.Empty stream_exists = 5; + } + } +} + +message TombstoneResp { + oneof position_option { + Position position = 1; + event_store.client.Empty no_position = 2; + } + + message Position { + uint64 commit_position = 1; + uint64 prepare_position = 2; + } +} diff --git a/src/Kurrent.Client/Core/protos/usermanagement.proto b/src/Kurrent.Client/Core/protos/usermanagement.proto new file mode 100644 index 000000000..4b55251fd --- /dev/null +++ b/src/Kurrent.Client/Core/protos/usermanagement.proto @@ -0,0 +1,119 @@ +syntax = "proto3"; +package event_store.client.users; +option java_package = "io.kurrent.client.users"; + +service Users { + rpc Create (CreateReq) returns (CreateResp); + rpc Update (UpdateReq) returns (UpdateResp); + rpc Delete (DeleteReq) returns (DeleteResp); + rpc Disable (DisableReq) returns (DisableResp); + rpc Enable (EnableReq) returns (EnableResp); + rpc Details (DetailsReq) returns (stream DetailsResp); + rpc ChangePassword (ChangePasswordReq) returns (ChangePasswordResp); + rpc ResetPassword (ResetPasswordReq) returns (ResetPasswordResp); +} + +message CreateReq { + Options options = 1; + message Options { + string login_name = 1; + string password = 2; + string full_name = 3; + repeated string groups = 4; + } +} + +message CreateResp { + +} + +message UpdateReq { + Options options = 1; + message Options { + string login_name = 1; + string password = 2; + string full_name = 3; + repeated string groups = 4; + } +} + +message UpdateResp { + +} + +message DeleteReq { + Options options = 1; + message Options { + string login_name = 1; + } +} + +message DeleteResp { + +} + +message EnableReq { + Options options = 1; + message Options { + string login_name = 1; + } +} + +message EnableResp { + +} + +message DisableReq { + Options options = 1; + message Options { + string login_name = 1; + } +} + +message DisableResp { +} + +message DetailsReq { + Options options = 1; + message Options { + string login_name = 1; + } +} + +message DetailsResp { + UserDetails user_details = 1; + message UserDetails { + string login_name = 1; + string full_name = 2; + repeated string groups = 3; + DateTime last_updated = 4; + bool disabled = 5; + + message DateTime { + int64 ticks_since_epoch = 1; + } + } +} + +message ChangePasswordReq { + Options options = 1; + message Options { + string login_name = 1; + string current_password = 2; + string new_password = 3; + } +} + +message ChangePasswordResp { +} + +message ResetPasswordReq { + Options options = 1; + message Options { + string login_name = 1; + string new_password = 2; + } +} + +message ResetPasswordResp { +} diff --git a/src/Kurrent.Client/Kurrent.Client.csproj b/src/Kurrent.Client/Kurrent.Client.csproj new file mode 100644 index 000000000..e6652b773 --- /dev/null +++ b/src/Kurrent.Client/Kurrent.Client.csproj @@ -0,0 +1,112 @@ + + + + Kurrent.Client + The base GRPC client library for the Kurrent Platform. Get the open source or commercial versions of KurrentDB from https://kurrent.io/ + Kurrent.Client + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + MSBuild:Compile + + + MSBuild:Compile + + + MSBuild:Compile + + + MSBuild:Compile + + + MSBuild:Compile + + + MSBuild:Compile + + + MSBuild:Compile + + + MSBuild:Compile + + + MSBuild:Compile + + + MSBuild:Compile + + + MSBuild:Compile + + + MSBuild:Compile + + + MSBuild:Compile + + + MSBuild:Compile + + + MSBuild:Compile + + + MSBuild:Compile + + + MSBuild:Compile + + + MSBuild:Compile + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Kurrent.Client/OpenTelemetry/TracerProviderBuilderExtensions.cs b/src/Kurrent.Client/OpenTelemetry/TracerProviderBuilderExtensions.cs new file mode 100644 index 000000000..dd1478b18 --- /dev/null +++ b/src/Kurrent.Client/OpenTelemetry/TracerProviderBuilderExtensions.cs @@ -0,0 +1,19 @@ +using EventStore.Client.Diagnostics; +using JetBrains.Annotations; +using OpenTelemetry.Trace; + +namespace EventStore.Client.Extensions.OpenTelemetry; + +/// +/// Extension methods used to facilitate tracing instrumentation of the EventStore Client. +/// +[PublicAPI] +public static class TracerProviderBuilderExtensions { + /// + /// Adds the EventStore client ActivitySource name to the list of subscribed sources on the + /// + /// being configured. + /// The instance of to chain configuration. + public static TracerProviderBuilder AddEventStoreClientInstrumentation(this TracerProviderBuilder builder) => + builder.AddSource(KurrentClientDiagnostics.InstrumentationName); +} diff --git a/src/Kurrent.Client/Operations/DatabaseScavengeResult.cs b/src/Kurrent.Client/Operations/DatabaseScavengeResult.cs new file mode 100644 index 000000000..9a09143fa --- /dev/null +++ b/src/Kurrent.Client/Operations/DatabaseScavengeResult.cs @@ -0,0 +1,81 @@ +using System; + +namespace EventStore.Client { + /// + /// A structure representing the result of a scavenge operation. + /// + public readonly struct DatabaseScavengeResult : IEquatable { + /// + /// The ID of the scavenge operation. + /// + public string ScavengeId { get; } + + /// + /// The of the scavenge operation. + /// + public ScavengeResult Result { get; } + + /// + /// A scavenge operation that has started. + /// + /// + /// + public static DatabaseScavengeResult Started(string scavengeId) => + new DatabaseScavengeResult(scavengeId, ScavengeResult.Started); + + /// + /// A scavenge operation that has stopped. + /// + /// + /// + public static DatabaseScavengeResult Stopped(string scavengeId) => + new DatabaseScavengeResult(scavengeId, ScavengeResult.Stopped); + + /// + /// A scavenge operation that is currently in progress. + /// + /// + /// + public static DatabaseScavengeResult InProgress(string scavengeId) => + new DatabaseScavengeResult(scavengeId, ScavengeResult.InProgress); + + /// + /// A scavenge operation whose state is unknown. + /// + /// + /// + public static DatabaseScavengeResult Unknown(string scavengeId) => + new DatabaseScavengeResult(scavengeId, ScavengeResult.Unknown); + + private DatabaseScavengeResult(string scavengeId, ScavengeResult result) { + ScavengeId = scavengeId; + Result = result; + } + + /// + public bool Equals(DatabaseScavengeResult other) => ScavengeId == other.ScavengeId && Result == other.Result; + + /// + public override bool Equals(object? obj) => obj is DatabaseScavengeResult other && Equals(other); + + /// + public override int GetHashCode() => HashCode.Hash.Combine(ScavengeId).Combine(Result); + + /// + /// Compares left and right for equality. + /// + /// + /// + /// True if left is equal to right. + public static bool operator ==(DatabaseScavengeResult left, DatabaseScavengeResult right) => left.Equals(right); + + /// + /// Compares left and right for inequality. + /// + /// + /// + /// True if left is not equal to right. + public static bool operator !=(DatabaseScavengeResult left, DatabaseScavengeResult right) => + !left.Equals(right); + } +} diff --git a/src/Kurrent.Client/Operations/KurrentOperationsClient.Admin.cs b/src/Kurrent.Client/Operations/KurrentOperationsClient.Admin.cs new file mode 100644 index 000000000..368fdca13 --- /dev/null +++ b/src/Kurrent.Client/Operations/KurrentOperationsClient.Admin.cs @@ -0,0 +1,103 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using EventStore.Client.Operations; + +namespace EventStore.Client { + public partial class KurrentOperationsClient { + private static readonly Empty EmptyResult = new Empty(); + + /// + /// Shuts down the KurrentDB node. + /// + /// + /// + /// + /// + public async Task ShutdownAsync( + TimeSpan? deadline = null, + UserCredentials? userCredentials = null, + CancellationToken cancellationToken = default) { + var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); + using var call = new Operations.Operations.OperationsClient( + channelInfo.CallInvoker).ShutdownAsync(EmptyResult, + KurrentCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); + await call.ResponseAsync.ConfigureAwait(false); + } + + /// + /// Initiates an index merge operation. + /// + /// + /// + /// + /// + public async Task MergeIndexesAsync( + TimeSpan? deadline = null, + UserCredentials? userCredentials = null, + CancellationToken cancellationToken = default) { + var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); + using var call = new Operations.Operations.OperationsClient( + channelInfo.CallInvoker).MergeIndexesAsync(EmptyResult, + KurrentCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); + await call.ResponseAsync.ConfigureAwait(false); + } + + /// + /// Resigns a node. + /// + /// + /// + /// + /// + public async Task ResignNodeAsync( + TimeSpan? deadline = null, + UserCredentials? userCredentials = null, + CancellationToken cancellationToken = default) { + var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); + using var call = new Operations.Operations.OperationsClient( + channelInfo.CallInvoker).ResignNodeAsync(EmptyResult, + KurrentCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); + await call.ResponseAsync.ConfigureAwait(false); + } + + /// + /// Sets the node priority. + /// + /// + /// + /// + /// + /// + public async Task SetNodePriorityAsync(int nodePriority, + TimeSpan? deadline = null, + UserCredentials? userCredentials = null, + CancellationToken cancellationToken = default) { + var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); + using var call = new Operations.Operations.OperationsClient( + channelInfo.CallInvoker).SetNodePriorityAsync( + new SetNodePriorityReq {Priority = nodePriority}, + KurrentCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); + await call.ResponseAsync.ConfigureAwait(false); + } + + /// + /// Restart persistent subscriptions + /// + /// + /// + /// + /// + public async Task RestartPersistentSubscriptions( + TimeSpan? deadline = null, + UserCredentials? userCredentials = null, + CancellationToken cancellationToken = default) { + var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); + using var call = new Operations.Operations.OperationsClient( + channelInfo.CallInvoker).RestartPersistentSubscriptionsAsync( + EmptyResult, + KurrentCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); + await call.ResponseAsync.ConfigureAwait(false); + } + } +} diff --git a/src/Kurrent.Client/Operations/KurrentOperationsClient.Scavenge.cs b/src/Kurrent.Client/Operations/KurrentOperationsClient.Scavenge.cs new file mode 100644 index 000000000..eb2c88c54 --- /dev/null +++ b/src/Kurrent.Client/Operations/KurrentOperationsClient.Scavenge.cs @@ -0,0 +1,82 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using EventStore.Client.Operations; + +namespace EventStore.Client { + public partial class KurrentOperationsClient { + /// + /// Starts a scavenge operation. + /// + /// + /// + /// + /// + /// + /// + /// + public async Task StartScavengeAsync( + int threadCount = 1, + int startFromChunk = 0, + TimeSpan? deadline = null, + UserCredentials? userCredentials = null, + CancellationToken cancellationToken = default) { + if (threadCount <= 0) { + throw new ArgumentOutOfRangeException(nameof(threadCount)); + } + + if (startFromChunk < 0) { + throw new ArgumentOutOfRangeException(nameof(startFromChunk)); + } + + var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); + using var call = new Operations.Operations.OperationsClient( + channelInfo.CallInvoker).StartScavengeAsync( + new StartScavengeReq { + Options = new StartScavengeReq.Types.Options { + ThreadCount = threadCount, + StartFromChunk = startFromChunk + } + }, + KurrentCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); + var result = await call.ResponseAsync.ConfigureAwait(false); + + return result.ScavengeResult switch { + ScavengeResp.Types.ScavengeResult.Started => DatabaseScavengeResult.Started(result.ScavengeId), + ScavengeResp.Types.ScavengeResult.Stopped => DatabaseScavengeResult.Stopped(result.ScavengeId), + ScavengeResp.Types.ScavengeResult.InProgress => DatabaseScavengeResult.InProgress(result.ScavengeId), + _ => DatabaseScavengeResult.Unknown(result.ScavengeId) + }; + } + + /// + /// Stops a scavenge operation. + /// + /// + /// + /// + /// + /// + public async Task StopScavengeAsync( + string scavengeId, + TimeSpan? deadline = null, + UserCredentials? userCredentials = null, + CancellationToken cancellationToken = default) { + var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); + var result = await new Operations.Operations.OperationsClient( + channelInfo.CallInvoker).StopScavengeAsync(new StopScavengeReq { + Options = new StopScavengeReq.Types.Options { + ScavengeId = scavengeId + } + }, KurrentCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)) + .ResponseAsync.ConfigureAwait(false); + + return result.ScavengeResult switch { + ScavengeResp.Types.ScavengeResult.Started => DatabaseScavengeResult.Started(result.ScavengeId), + ScavengeResp.Types.ScavengeResult.Stopped => DatabaseScavengeResult.Stopped(result.ScavengeId), + ScavengeResp.Types.ScavengeResult.InProgress => DatabaseScavengeResult.InProgress(result.ScavengeId), + _ => DatabaseScavengeResult.Unknown(result.ScavengeId) + }; + } + } +} diff --git a/src/Kurrent.Client/Operations/KurrentOperationsClient.cs b/src/Kurrent.Client/Operations/KurrentOperationsClient.cs new file mode 100644 index 000000000..d392aa578 --- /dev/null +++ b/src/Kurrent.Client/Operations/KurrentOperationsClient.cs @@ -0,0 +1,33 @@ +using Grpc.Core; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; + +namespace EventStore.Client; + +/// +/// The client used to perform maintenance and other administrative tasks on the KurrentDB. +/// +public sealed partial class KurrentOperationsClient : KurrentClientBase { + static readonly Dictionary> ExceptionMap = + new() { + [Constants.Exceptions.ScavengeNotFound] = ex => new ScavengeNotFoundException( + ex.Trailers.FirstOrDefault(x => x.Key == Constants.Exceptions.ScavengeId)?.Value + ) + }; + + readonly ILogger _log; + + /// + /// Constructs a new . This method is not intended to be called directly in your code. + /// + /// + public KurrentOperationsClient(IOptions options) : this(options.Value) { } + + /// + /// Constructs a new . + /// + /// + public KurrentOperationsClient(KurrentClientSettings? settings = null) : base(settings, ExceptionMap) => + _log = Settings.LoggerFactory?.CreateLogger() ?? new NullLogger(); +} diff --git a/src/Kurrent.Client/Operations/KurrentOperationsClientServiceCollectionExtensions.cs b/src/Kurrent.Client/Operations/KurrentOperationsClientServiceCollectionExtensions.cs new file mode 100644 index 000000000..fc3ce9505 --- /dev/null +++ b/src/Kurrent.Client/Operations/KurrentOperationsClientServiceCollectionExtensions.cs @@ -0,0 +1,73 @@ +// ReSharper disable CheckNamespace + +using System; +using System.Net.Http; +using EventStore.Client; +using Grpc.Core.Interceptors; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Logging; + +namespace Microsoft.Extensions.DependencyInjection { + /// + /// A set of extension methods for which provide support for an . + /// + public static class KurrentOperationsClientServiceCollectionExtensions { + + /// + /// Adds an to the . + /// + /// + /// + /// + /// + /// + public static IServiceCollection AddKurrentOperationsClient(this IServiceCollection services, Uri address, + Func? createHttpMessageHandler = null) + => services.AddKurrentOperationsClient(options => { + options.ConnectivitySettings.Address = address; + options.CreateHttpMessageHandler = createHttpMessageHandler; + }); + + /// + /// Adds an to the . + /// + /// + /// + /// + /// + public static IServiceCollection AddKurrentOperationsClient(this IServiceCollection services, + Action? configureOptions = null) => + services.AddKurrentOperationsClient(new KurrentClientSettings(), configureOptions); + + /// + /// Adds an to the . + /// + /// + /// + /// + /// + /// + public static IServiceCollection AddKurrentOperationsClient(this IServiceCollection services, + string connectionString, Action? configureOptions = null) => + services.AddKurrentOperationsClient(KurrentClientSettings.Create(connectionString), configureOptions); + + private static IServiceCollection AddKurrentOperationsClient(this IServiceCollection services, + KurrentClientSettings options, Action? configureOptions) { + if (services == null) { + throw new ArgumentNullException(nameof(services)); + } + + configureOptions?.Invoke(options); + + services.TryAddSingleton(provider => { + options.LoggerFactory ??= provider.GetService(); + options.Interceptors ??= provider.GetServices(); + + return new KurrentOperationsClient(options); + }); + + return services; + } + } +} +// ReSharper restore CheckNamespace diff --git a/src/Kurrent.Client/Operations/ScavengeResult.cs b/src/Kurrent.Client/Operations/ScavengeResult.cs new file mode 100644 index 000000000..6b8802d85 --- /dev/null +++ b/src/Kurrent.Client/Operations/ScavengeResult.cs @@ -0,0 +1,25 @@ +namespace EventStore.Client { + /// + /// An enumeration that represents the result of a scavenge operation. + /// + public enum ScavengeResult { + /// + /// The scavenge operation has started. + /// + Started, + /// + /// The scavenge operation is in progress. + /// + InProgress, + + /// + /// The scavenge operation has stopped. + /// + Stopped, + + /// + /// The status of the scavenge operation was unknown. + /// + Unknown + } +} diff --git a/src/Kurrent.Client/PersistentSubscriptions/KurrentPersistentSubscriptionsClient.Create.cs b/src/Kurrent.Client/PersistentSubscriptions/KurrentPersistentSubscriptionsClient.Create.cs new file mode 100644 index 000000000..80987b3ce --- /dev/null +++ b/src/Kurrent.Client/PersistentSubscriptions/KurrentPersistentSubscriptionsClient.Create.cs @@ -0,0 +1,252 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using EventStore.Client.PersistentSubscriptions; + +namespace EventStore.Client { + partial class KurrentPersistentSubscriptionsClient { + private static readonly IDictionary NamedConsumerStrategyToCreateProto + = new Dictionary { + [SystemConsumerStrategies.DispatchToSingle] = CreateReq.Types.ConsumerStrategy.DispatchToSingle, + [SystemConsumerStrategies.RoundRobin] = CreateReq.Types.ConsumerStrategy.RoundRobin, + [SystemConsumerStrategies.Pinned] = CreateReq.Types.ConsumerStrategy.Pinned, + }; + + private static CreateReq.Types.StreamOptions StreamOptionsForCreateProto(string streamName, StreamPosition position) { + if (position == StreamPosition.Start) { + return new CreateReq.Types.StreamOptions { + StreamIdentifier = streamName, + Start = new Empty() + }; + } + + if (position == StreamPosition.End) { + return new CreateReq.Types.StreamOptions { + StreamIdentifier = streamName, + End = new Empty() + }; + } + + return new CreateReq.Types.StreamOptions { + StreamIdentifier = streamName, + Revision = position.ToUInt64() + }; + } + + private static CreateReq.Types.AllOptions AllOptionsForCreateProto(Position position, IEventFilter? filter) { + var allFilter = GetFilterOptions(filter); + CreateReq.Types.AllOptions allOptions; + if (position == Position.Start) { + allOptions = new CreateReq.Types.AllOptions { + Start = new Empty(), + }; + } else if (position == Position.End) { + allOptions = new CreateReq.Types.AllOptions { + End = new Empty() + }; + } else { + allOptions = new CreateReq.Types.AllOptions { + Position = new CreateReq.Types.Position { + CommitPosition = position.CommitPosition, + PreparePosition = position.PreparePosition + } + }; + } + + if (allFilter is null) { + allOptions.NoFilter = new Empty(); + } else { + allOptions.Filter = allFilter; + } + + return allOptions; + } + + private static CreateReq.Types.AllOptions.Types.FilterOptions? GetFilterOptions(IEventFilter? filter) { + if (filter == null) { + return null; + } + + var options = filter switch { + StreamFilter _ => new CreateReq.Types.AllOptions.Types.FilterOptions { + StreamIdentifier = (filter.Prefixes, filter.Regex) switch { + (PrefixFilterExpression[] _, RegularFilterExpression _) + when (filter.Prefixes?.Length ?? 0) == 0 && + filter.Regex != RegularFilterExpression.None => + new CreateReq.Types.AllOptions.Types.FilterOptions.Types.Expression + {Regex = filter.Regex}, + (PrefixFilterExpression[] _, RegularFilterExpression _) + when (filter.Prefixes?.Length ?? 0) != 0 && + filter.Regex == RegularFilterExpression.None => + new CreateReq.Types.AllOptions.Types.FilterOptions.Types.Expression { + Prefix = {Array.ConvertAll(filter.Prefixes!, e => e.ToString())} + }, + _ => throw new InvalidOperationException() + } + }, + EventTypeFilter _ => new CreateReq.Types.AllOptions.Types.FilterOptions { + EventType = (filter.Prefixes, filter.Regex) switch { + (PrefixFilterExpression[] _, RegularFilterExpression _) + when (filter.Prefixes?.Length ?? 0) == 0 && + filter.Regex != RegularFilterExpression.None => + new CreateReq.Types.AllOptions.Types.FilterOptions.Types.Expression + {Regex = filter.Regex}, + (PrefixFilterExpression[] _, RegularFilterExpression _) + when (filter.Prefixes?.Length ?? 0) != 0 && + filter.Regex == RegularFilterExpression.None => + new CreateReq.Types.AllOptions.Types.FilterOptions.Types.Expression { + Prefix = {Array.ConvertAll(filter.Prefixes!, e => e.ToString())} + }, + _ => throw new InvalidOperationException() + } + }, + _ => throw new InvalidOperationException() + }; + + if (filter.MaxSearchWindow.HasValue) { + options.Max = filter.MaxSearchWindow.Value; + } else { + options.Count = new Empty(); + } + + return options; + } + + + /// + /// Creates a persistent subscription. + /// + /// + public async Task CreateToStreamAsync(string streamName, string groupName, PersistentSubscriptionSettings settings, + TimeSpan? deadline = null, UserCredentials? userCredentials = null, + CancellationToken cancellationToken = default) => + await CreateInternalAsync(streamName, groupName, null, settings, deadline, userCredentials, + cancellationToken) + .ConfigureAwait(false); + + /// + /// Creates a persistent subscription. + /// + /// + [Obsolete("CreateAsync is no longer supported. Use CreateToStreamAsync instead.", false)] + public async Task CreateAsync(string streamName, string groupName, PersistentSubscriptionSettings settings, + TimeSpan? deadline = null, UserCredentials? userCredentials = null, + CancellationToken cancellationToken = default) => + await CreateInternalAsync(streamName, groupName, null, settings, deadline, userCredentials, + cancellationToken) + .ConfigureAwait(false); + + /// + /// Creates a filtered persistent subscription to $all. + /// + public async Task CreateToAllAsync(string groupName, IEventFilter eventFilter, + PersistentSubscriptionSettings settings, TimeSpan? deadline = null, UserCredentials? userCredentials = null, + CancellationToken cancellationToken = default) => + await CreateInternalAsync(SystemStreams.AllStream, groupName, eventFilter, settings, deadline, + userCredentials, cancellationToken) + .ConfigureAwait(false); + + /// + /// Creates a persistent subscription to $all. + /// + public async Task CreateToAllAsync(string groupName, PersistentSubscriptionSettings settings, + TimeSpan? deadline = null, UserCredentials? userCredentials = null, + CancellationToken cancellationToken = default) => + await CreateInternalAsync(SystemStreams.AllStream, groupName, null, settings, deadline, userCredentials, + cancellationToken) + .ConfigureAwait(false); + + private async Task CreateInternalAsync(string streamName, string groupName, IEventFilter? eventFilter, + PersistentSubscriptionSettings settings, TimeSpan? deadline, UserCredentials? userCredentials, + CancellationToken cancellationToken) { + if (streamName is null) { + throw new ArgumentNullException(nameof(streamName)); + } + + if (groupName is null) { + throw new ArgumentNullException(nameof(groupName)); + } + + if (settings is null) { + throw new ArgumentNullException(nameof(settings)); + } + + if (settings.ConsumerStrategyName is null) { + throw new ArgumentNullException(nameof(settings.ConsumerStrategyName)); + } + + if (streamName != SystemStreams.AllStream && settings.StartFrom != null && + settings.StartFrom is not StreamPosition) { + throw new ArgumentException( + $"{nameof(settings.StartFrom)} must be of type '{nameof(StreamPosition)}' when subscribing to a stream"); + } + + if (streamName == SystemStreams.AllStream && settings.StartFrom != null && + settings.StartFrom is not Position) { + throw new ArgumentException( + $"{nameof(settings.StartFrom)} must be of type '{nameof(Position)}' when subscribing to {SystemStreams.AllStream}"); + } + + if (eventFilter != null && streamName != SystemStreams.AllStream) { + throw new ArgumentException( + $"Filters are only supported when subscribing to {SystemStreams.AllStream}"); + } + + if (!NamedConsumerStrategyToCreateProto.ContainsKey(settings.ConsumerStrategyName)) { + throw new ArgumentException( + "The specified consumer strategy is not supported, specify one of the SystemConsumerStrategies"); + } + + var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); + + if (streamName == SystemStreams.AllStream && + !channelInfo.ServerCapabilities.SupportsPersistentSubscriptionsToAll) { + throw new InvalidOperationException("The server does not support persistent subscriptions to $all."); + } + + using var call = new PersistentSubscriptions.PersistentSubscriptions.PersistentSubscriptionsClient( + channelInfo.CallInvoker).CreateAsync(new CreateReq { + Options = new CreateReq.Types.Options { + Stream = streamName != SystemStreams.AllStream + ? StreamOptionsForCreateProto(streamName, + (StreamPosition)(settings.StartFrom ?? StreamPosition.End)) + : null, + All = streamName == SystemStreams.AllStream + ? AllOptionsForCreateProto((Position)(settings.StartFrom ?? Position.End), eventFilter) + : null, +#pragma warning disable 612 + StreamIdentifier = + streamName != SystemStreams.AllStream + ? streamName + : string.Empty, /*for backwards compatibility*/ +#pragma warning restore 612 + GroupName = groupName, + Settings = new CreateReq.Types.Settings { +#pragma warning disable 612 + Revision = streamName != SystemStreams.AllStream + ? ((StreamPosition)(settings.StartFrom ?? StreamPosition.End)).ToUInt64() + : default, /*for backwards compatibility*/ +#pragma warning restore 612 + CheckpointAfterMs = (int)settings.CheckPointAfter.TotalMilliseconds, + ExtraStatistics = settings.ExtraStatistics, + MessageTimeoutMs = (int)settings.MessageTimeout.TotalMilliseconds, + ResolveLinks = settings.ResolveLinkTos, + HistoryBufferSize = settings.HistoryBufferSize, + LiveBufferSize = settings.LiveBufferSize, + MaxCheckpointCount = settings.CheckPointUpperBound, + MaxRetryCount = settings.MaxRetryCount, + MaxSubscriberCount = settings.MaxSubscriberCount, + MinCheckpointCount = settings.CheckPointLowerBound, +#pragma warning disable 612 + /*for backwards compatibility*/ + NamedConsumerStrategy = NamedConsumerStrategyToCreateProto[settings.ConsumerStrategyName], +#pragma warning restore 612 + ReadBatchSize = settings.ReadBatchSize + } + } + }, KurrentCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); + await call.ResponseAsync.ConfigureAwait(false); + } + } +} diff --git a/src/Kurrent.Client/PersistentSubscriptions/KurrentPersistentSubscriptionsClient.Delete.cs b/src/Kurrent.Client/PersistentSubscriptions/KurrentPersistentSubscriptionsClient.Delete.cs new file mode 100644 index 000000000..d84c0e61a --- /dev/null +++ b/src/Kurrent.Client/PersistentSubscriptions/KurrentPersistentSubscriptionsClient.Delete.cs @@ -0,0 +1,55 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using EventStore.Client.PersistentSubscriptions; + +namespace EventStore.Client { + partial class KurrentPersistentSubscriptionsClient { + /// + /// Deletes a persistent subscription. + /// + [Obsolete("DeleteAsync is no longer supported. Use DeleteToStreamAsync instead.", false)] + public Task DeleteAsync(string streamName, string groupName, TimeSpan? deadline = null, + UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) => + DeleteToStreamAsync(streamName, groupName, deadline, userCredentials, cancellationToken); + + /// + /// Deletes a persistent subscription. + /// + public async Task DeleteToStreamAsync(string streamName, string groupName, TimeSpan? deadline = null, + UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) { + var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); + + if (streamName == SystemStreams.AllStream && + !channelInfo.ServerCapabilities.SupportsPersistentSubscriptionsToAll) { + throw new NotSupportedException("The server does not support persistent subscriptions to $all."); + } + + var deleteOptions = new DeleteReq.Types.Options { + GroupName = groupName + }; + + if (streamName == SystemStreams.AllStream) { + deleteOptions.All = new Empty(); + } else { + deleteOptions.StreamIdentifier = streamName; + } + + using var call = + new PersistentSubscriptions.PersistentSubscriptions.PersistentSubscriptionsClient( + channelInfo.CallInvoker) + .DeleteAsync(new DeleteReq {Options = deleteOptions}, + KurrentCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, + cancellationToken)); + await call.ResponseAsync.ConfigureAwait(false); + } + + /// + /// Deletes a persistent subscription to $all. + /// + public async Task DeleteToAllAsync(string groupName, TimeSpan? deadline = null, + UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) => + await DeleteToStreamAsync(SystemStreams.AllStream, groupName, deadline, userCredentials, cancellationToken) + .ConfigureAwait(false); + } +} diff --git a/src/Kurrent.Client/PersistentSubscriptions/KurrentPersistentSubscriptionsClient.Info.cs b/src/Kurrent.Client/PersistentSubscriptions/KurrentPersistentSubscriptionsClient.Info.cs new file mode 100644 index 000000000..78ececf72 --- /dev/null +++ b/src/Kurrent.Client/PersistentSubscriptions/KurrentPersistentSubscriptionsClient.Info.cs @@ -0,0 +1,77 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using EventStore.Client.PersistentSubscriptions; +using Grpc.Core; + +#nullable enable +namespace EventStore.Client { + partial class KurrentPersistentSubscriptionsClient { + /// + /// Gets the status of a persistent subscription to $all + /// + public async Task GetInfoToAllAsync(string groupName, TimeSpan? deadline = null, + UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) { + + var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); + if (channelInfo.ServerCapabilities.SupportsPersistentSubscriptionsGetInfo) { + var req = new GetInfoReq() { + Options = new GetInfoReq.Types.Options{ + GroupName = groupName, + All = new Empty() + } + }; + + return await GetInfoGrpcAsync(req, deadline, userCredentials, channelInfo.CallInvoker, cancellationToken) + .ConfigureAwait(false); + } + + throw new NotSupportedException("The server does not support getting persistent subscription details for $all"); + } + + /// + /// Gets the status of a persistent subscription to a stream + /// + public async Task GetInfoToStreamAsync(string streamName, string groupName, + TimeSpan? deadline = null, UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) { + + var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); + if (channelInfo.ServerCapabilities.SupportsPersistentSubscriptionsGetInfo) { + var req = new GetInfoReq() { + Options = new GetInfoReq.Types.Options { + GroupName = groupName, + StreamIdentifier = streamName + } + }; + + return await GetInfoGrpcAsync(req, deadline, userCredentials, channelInfo.CallInvoker, cancellationToken) + .ConfigureAwait(false); + } + + return await GetInfoHttpAsync(streamName, groupName, channelInfo, deadline, userCredentials, cancellationToken) + .ConfigureAwait(false); + } + + private async Task GetInfoGrpcAsync(GetInfoReq req, TimeSpan? deadline, + UserCredentials? userCredentials, CallInvoker callInvoker, CancellationToken cancellationToken) { + + var result = await new PersistentSubscriptions.PersistentSubscriptions.PersistentSubscriptionsClient(callInvoker) + .GetInfoAsync(req, KurrentCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)) + .ConfigureAwait(false); + + return PersistentSubscriptionInfo.From(result.SubscriptionInfo); + } + + private async Task GetInfoHttpAsync(string streamName, string groupName, + ChannelInfo channelInfo, TimeSpan? deadline, UserCredentials? userCredentials, CancellationToken cancellationToken) { + + var path = $"/subscriptions/{UrlEncode(streamName)}/{UrlEncode(groupName)}/info"; + var result = await HttpGet(path, + onNotFound: () => throw new PersistentSubscriptionNotFoundException(streamName, groupName), + channelInfo, deadline, userCredentials, cancellationToken) + .ConfigureAwait(false); + + return PersistentSubscriptionInfo.From(result); + } + } +} diff --git a/src/Kurrent.Client/PersistentSubscriptions/KurrentPersistentSubscriptionsClient.List.cs b/src/Kurrent.Client/PersistentSubscriptions/KurrentPersistentSubscriptionsClient.List.cs new file mode 100644 index 000000000..a2c92b733 --- /dev/null +++ b/src/Kurrent.Client/PersistentSubscriptions/KurrentPersistentSubscriptionsClient.List.cs @@ -0,0 +1,106 @@ +using EventStore.Client.PersistentSubscriptions; +using Grpc.Core; + +#nullable enable +namespace EventStore.Client { + partial class KurrentPersistentSubscriptionsClient { + /// + /// Lists persistent subscriptions to $all. + /// + public async Task> ListToAllAsync(TimeSpan? deadline = null, + UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) { + + var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); + if (channelInfo.ServerCapabilities.SupportsPersistentSubscriptionsList) { + var req = new ListReq() { + Options = new ListReq.Types.Options{ + ListForStream = new ListReq.Types.StreamOption() { + All = new Empty() + } + } + }; + + return await ListGrpcAsync(req, deadline, userCredentials, channelInfo.CallInvoker, cancellationToken) + .ConfigureAwait(false); + } + + throw new NotSupportedException("The server does not support listing the persistent subscriptions."); + } + + /// + /// Lists persistent subscriptions to the specified stream. + /// + public async Task> ListToStreamAsync(string streamName, TimeSpan? deadline = null, + UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) { + + var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); + if (channelInfo.ServerCapabilities.SupportsPersistentSubscriptionsList) { + var req = new ListReq() { + Options = new ListReq.Types.Options { + ListForStream = new ListReq.Types.StreamOption() { + Stream = streamName + } + } + }; + + return await ListGrpcAsync(req, deadline, userCredentials, channelInfo.CallInvoker, cancellationToken) + .ConfigureAwait(false); + } + + return await ListHttpAsync(streamName, channelInfo, deadline, userCredentials, cancellationToken) + .ConfigureAwait(false); + } + + /// + /// Lists all persistent subscriptions. + /// + public async Task> ListAllAsync(TimeSpan? deadline = null, + UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) { + + var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); + if (channelInfo.ServerCapabilities.SupportsPersistentSubscriptionsList) { + var req = new ListReq() { + Options = new ListReq.Types.Options { + ListAllSubscriptions = new Empty() + } + }; + + return await ListGrpcAsync(req, deadline, userCredentials, channelInfo.CallInvoker, cancellationToken) + .ConfigureAwait(false); + } + + try { + var result = await HttpGet>("/subscriptions", + onNotFound: () => throw new PersistentSubscriptionNotFoundException(string.Empty, string.Empty), + channelInfo, deadline, userCredentials, cancellationToken) + .ConfigureAwait(false); + + return result.Select(PersistentSubscriptionInfo.From); + } catch (AccessDeniedException ex) when (userCredentials != null) { // Required to get same gRPC behavior. + throw new NotAuthenticatedException(ex.Message, ex); + } + } + + private async Task> ListGrpcAsync(ListReq req, TimeSpan? deadline, + UserCredentials? userCredentials, CallInvoker callInvoker, CancellationToken cancellationToken) { + + using var call = new PersistentSubscriptions.PersistentSubscriptions.PersistentSubscriptionsClient(callInvoker) + .ListAsync(req, KurrentCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); + + ListResp? response = await call.ResponseAsync.ConfigureAwait(false); + + return response.Subscriptions.Select(PersistentSubscriptionInfo.From); + } + + private async Task> ListHttpAsync(string streamName, + ChannelInfo channelInfo, TimeSpan? deadline, UserCredentials? userCredentials, CancellationToken cancellationToken) { + + var path = $"/subscriptions/{UrlEncode(streamName)}"; + var result = await HttpGet>(path, + onNotFound: () => throw new PersistentSubscriptionNotFoundException(streamName, string.Empty), + channelInfo, deadline, userCredentials, cancellationToken) + .ConfigureAwait(false); + return result.Select(PersistentSubscriptionInfo.From); + } + } +} diff --git a/src/Kurrent.Client/PersistentSubscriptions/KurrentPersistentSubscriptionsClient.Read.cs b/src/Kurrent.Client/PersistentSubscriptions/KurrentPersistentSubscriptionsClient.Read.cs new file mode 100644 index 000000000..779413111 --- /dev/null +++ b/src/Kurrent.Client/PersistentSubscriptions/KurrentPersistentSubscriptionsClient.Read.cs @@ -0,0 +1,478 @@ +using System.Threading.Channels; +using EventStore.Client.PersistentSubscriptions; +using EventStore.Client.Diagnostics; +using Grpc.Core; + +using static EventStore.Client.PersistentSubscriptions.PersistentSubscriptions; +using static EventStore.Client.PersistentSubscriptions.ReadResp.ContentOneofCase; + +namespace EventStore.Client { + partial class KurrentPersistentSubscriptionsClient { + /// + /// Subscribes to a persistent subscription. + /// + /// + /// + /// + [Obsolete("SubscribeAsync is no longer supported. Use SubscribeToStream with manual acks instead.", false)] + public async Task SubscribeAsync( + string streamName, string groupName, + Func eventAppeared, + Action? subscriptionDropped = null, + UserCredentials? userCredentials = null, int bufferSize = 10, bool autoAck = true, + CancellationToken cancellationToken = default + ) { + if (autoAck) { + throw new InvalidOperationException( + $"AutoAck is no longer supported. Please use {nameof(SubscribeToStream)} with manual acks instead." + ); + } + + return await PersistentSubscription + .Confirm( + SubscribeToStream(streamName, groupName, bufferSize, userCredentials, cancellationToken), + eventAppeared, + subscriptionDropped ?? delegate { }, + _log, + userCredentials, + cancellationToken + ) + .ConfigureAwait(false); + } + + /// + /// Subscribes to a persistent subscription. Messages must be manually acknowledged + /// + /// + /// + /// + public async Task SubscribeToStreamAsync( + string streamName, string groupName, + Func eventAppeared, + Action? subscriptionDropped = null, + UserCredentials? userCredentials = null, int bufferSize = 10, + CancellationToken cancellationToken = default + ) { + return await PersistentSubscription + .Confirm( + SubscribeToStream(streamName, groupName, bufferSize, userCredentials, cancellationToken), + eventAppeared, + subscriptionDropped ?? delegate { }, + _log, + userCredentials, + cancellationToken + ) + .ConfigureAwait(false); + } + + /// + /// Subscribes to a persistent subscription. Messages must be manually acknowledged. + /// + /// The name of the stream to read events from. + /// The name of the persistent subscription group. + /// The size of the buffer. + /// The optional user credentials to perform operation with. + /// The optional . + /// + public PersistentSubscriptionResult SubscribeToStream( + string streamName, string groupName, int bufferSize = 10, + UserCredentials? userCredentials = null, CancellationToken cancellationToken = default + ) { + if (streamName == null) { + throw new ArgumentNullException(nameof(streamName)); + } + + if (groupName == null) { + throw new ArgumentNullException(nameof(groupName)); + } + + if (streamName == string.Empty) { + throw new ArgumentException($"{nameof(streamName)} may not be empty.", nameof(streamName)); + } + + if (groupName == string.Empty) { + throw new ArgumentException($"{nameof(groupName)} may not be empty.", nameof(groupName)); + } + + if (bufferSize <= 0) { + throw new ArgumentOutOfRangeException(nameof(bufferSize)); + } + + var readOptions = new ReadReq.Types.Options { + BufferSize = bufferSize, + GroupName = groupName, + UuidOption = new ReadReq.Types.Options.Types.UUIDOption { Structured = new Empty() } + }; + + if (streamName == SystemStreams.AllStream) { + readOptions.All = new Empty(); + } else { + readOptions.StreamIdentifier = streamName; + } + + return new PersistentSubscriptionResult( + streamName, + groupName, + async ct => { + var channelInfo = await GetChannelInfo(ct).ConfigureAwait(false); + + if (streamName == SystemStreams.AllStream && + !channelInfo.ServerCapabilities.SupportsPersistentSubscriptionsToAll) { + throw new NotSupportedException( + "The server does not support persistent subscriptions to $all." + ); + } + + return channelInfo; + }, + new() { Options = readOptions }, + Settings, + userCredentials, + cancellationToken + ); + } + + /// + /// Subscribes to a persistent subscription to $all. Messages must be manually acknowledged + /// + public async Task SubscribeToAllAsync( + string groupName, + Func eventAppeared, + Action? subscriptionDropped = null, + UserCredentials? userCredentials = null, int bufferSize = 10, + CancellationToken cancellationToken = default + ) => + await SubscribeToStreamAsync( + SystemStreams.AllStream, + groupName, + eventAppeared, + subscriptionDropped, + userCredentials, + bufferSize, + cancellationToken + ) + .ConfigureAwait(false); + + /// + /// Subscribes to a persistent subscription to $all. Messages must be manually acknowledged. + /// + /// The name of the persistent subscription group. + /// The size of the buffer. + /// The optional user credentials to perform operation with. + /// The optional . + /// + public PersistentSubscriptionResult SubscribeToAll( + string groupName, int bufferSize = 10, + UserCredentials? userCredentials = null, CancellationToken cancellationToken = default + ) => + SubscribeToStream(SystemStreams.AllStream, groupName, bufferSize, userCredentials, cancellationToken); + + /// + public class PersistentSubscriptionResult : IAsyncEnumerable, IAsyncDisposable, IDisposable { + const int MaxEventIdLength = 2000; + + readonly ReadReq _request; + readonly Channel _channel; + readonly CancellationTokenSource _cts; + readonly CallOptions _callOptions; + + AsyncDuplexStreamingCall? _call; + int _messagesEnumerated; + + /// + /// The server-generated unique identifier for the subscription. + /// + public string? SubscriptionId { get; private set; } + + /// + /// The name of the stream to read events from. + /// + public string StreamName { get; } + + /// + /// The name of the persistent subscription group. + /// + public string GroupName { get; } + + /// + /// An . Do not enumerate more than once. + /// + public IAsyncEnumerable Messages { + get { + if (Interlocked.Exchange(ref _messagesEnumerated, 1) == 1) + throw new InvalidOperationException("Messages may only be enumerated once."); + + return GetMessages(); + + async IAsyncEnumerable GetMessages() { + try { + await foreach (var message in _channel.Reader.ReadAllAsync(_cts.Token)) { + if (message is PersistentSubscriptionMessage.SubscriptionConfirmation(var subscriptionId)) + SubscriptionId = subscriptionId; + + yield return message; + } + } + finally { + _cts.Cancel(); + } + } + } + } + + internal PersistentSubscriptionResult( + string streamName, string groupName, + Func> selectChannelInfo, + ReadReq request, KurrentClientSettings settings, UserCredentials? userCredentials, + CancellationToken cancellationToken + ) { + StreamName = streamName; + GroupName = groupName; + + _request = request; + + _callOptions = KurrentCallOptions.CreateStreaming( + settings, + userCredentials: userCredentials, + cancellationToken: cancellationToken + ); + + _channel = Channel.CreateBounded(ReadBoundedChannelOptions); + + _cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + + _ = PumpMessages(); + + return; + + async Task PumpMessages() { + try { + var channelInfo = await selectChannelInfo(_cts.Token).ConfigureAwait(false); + var client = new PersistentSubscriptionsClient(channelInfo.CallInvoker); + + _call = client.Read(_callOptions); + + await _call.RequestStream.WriteAsync(_request).ConfigureAwait(false); + + await foreach (var response in _call.ResponseStream.ReadAllAsync(_cts.Token).ConfigureAwait(false)) { + PersistentSubscriptionMessage subscriptionMessage = response.ContentCase switch { + SubscriptionConfirmation => new PersistentSubscriptionMessage.SubscriptionConfirmation( + response.SubscriptionConfirmation.SubscriptionId + ), + Event => new PersistentSubscriptionMessage.Event( + ConvertToResolvedEvent(response), + response.Event.CountCase switch { + ReadResp.Types.ReadEvent.CountOneofCase.RetryCount => response.Event.RetryCount, + _ => null + } + ), + _ => PersistentSubscriptionMessage.Unknown.Instance + }; + + if (subscriptionMessage is PersistentSubscriptionMessage.Event evnt) + KurrentClientDiagnostics.ActivitySource.TraceSubscriptionEvent( + SubscriptionId, + evnt.ResolvedEvent, + channelInfo, + settings, + userCredentials + ); + + await _channel.Writer.WriteAsync(subscriptionMessage, _cts.Token).ConfigureAwait(false); + } + + _channel.Writer.TryComplete(); + } catch (Exception ex) { +#if NET48 + switch (ex) { + // The gRPC client for .NET 48 uses WinHttpHandler under the hood for sending HTTP requests. + // In certain scenarios, this can lead to exceptions of type WinHttpException being thrown. + // One such scenario is when the server abruptly closes the connection, which results in a WinHttpException with the error code 12030. + // Additionally, there are cases where the server response does not include the 'grpc-status' header. + // The absence of this header leads to an RpcException with the status code 'Cancelled' and the message "No grpc-status found on response". + // The switch statement below handles these specific exceptions and translates them into the appropriate + // PersistentSubscriptionDroppedByServerException exception. + case RpcException { StatusCode: StatusCode.Unavailable } rex1 when rex1.Status.Detail.Contains("WinHttpException: Error 12030"): + case RpcException { StatusCode: StatusCode.Cancelled } rex2 + when rex2.Status.Detail.Contains("No grpc-status found on response"): + ex = new PersistentSubscriptionDroppedByServerException(StreamName, GroupName, ex); + break; + } +#endif + if (ex is PersistentSubscriptionNotFoundException) { + await _channel.Writer + .WriteAsync(PersistentSubscriptionMessage.NotFound.Instance, cancellationToken) + .ConfigureAwait(false); + + _channel.Writer.TryComplete(); + return; + } + + _channel.Writer.TryComplete(ex); + } + } + } + + /// + /// Acknowledge that a message has completed processing (this will tell the server it has been processed). + /// + /// There is no need to ack a message if you have Auto Ack enabled. + /// The of the s to acknowledge. There should not be more than 2000 to ack at a time. + public Task Ack(params Uuid[] eventIds) => AckInternal(eventIds); + + /// + /// Acknowledge that a message has completed processing (this will tell the server it has been processed). + /// + /// There is no need to ack a message if you have Auto Ack enabled. + /// The of the s to acknowledge. There should not be more than 2000 to ack at a time. + public Task Ack(IEnumerable eventIds) => Ack(eventIds.ToArray()); + + /// + /// Acknowledge that a message has completed processing (this will tell the server it has been processed). + /// + /// There is no need to ack a message if you have Auto Ack enabled. + /// The s to acknowledge. There should not be more than 2000 to ack at a time. + public Task Ack(params ResolvedEvent[] resolvedEvents) => + Ack(Array.ConvertAll(resolvedEvents, resolvedEvent => resolvedEvent.OriginalEvent.EventId)); + + /// + /// Acknowledge that a message has completed processing (this will tell the server it has been processed). + /// + /// There is no need to ack a message if you have Auto Ack enabled. + /// The s to acknowledge. There should not be more than 2000 to ack at a time. + public Task Ack(IEnumerable resolvedEvents) => + Ack(resolvedEvents.Select(resolvedEvent => resolvedEvent.OriginalEvent.EventId)); + + /// + /// Acknowledge that a message has failed processing (this will tell the server it has not been processed). + /// + /// The to take. + /// A reason given. + /// The of the s to nak. There should not be more than 2000 to nak at a time. + /// The number of eventIds exceeded the limit of 2000. + public Task Nack(PersistentSubscriptionNakEventAction action, string reason, params Uuid[] eventIds) => + NackInternal(eventIds, action, reason); + + /// + /// Acknowledge that a message has failed processing (this will tell the server it has not been processed). + /// + /// The to take. + /// A reason given. + /// The s to nak. There should not be more than 2000 to nak at a time. + /// The number of resolvedEvents exceeded the limit of 2000. + public Task Nack(PersistentSubscriptionNakEventAction action, string reason, params ResolvedEvent[] resolvedEvents) => + Nack(action, reason, Array.ConvertAll(resolvedEvents, re => re.OriginalEvent.EventId)); + + static ResolvedEvent ConvertToResolvedEvent(ReadResp response) => new( + ConvertToEventRecord(response.Event.Event)!, + ConvertToEventRecord(response.Event.Link), + response.Event.PositionCase switch { + ReadResp.Types.ReadEvent.PositionOneofCase.CommitPosition => response.Event.CommitPosition, + _ => null + } + ); + + Task AckInternal(params Uuid[] eventIds) { + if (eventIds.Length > MaxEventIdLength) { + throw new ArgumentException( + $"The number of eventIds exceeds the maximum length of {MaxEventIdLength}.", + nameof(eventIds) + ); + } + + return _call is null + ? throw new InvalidOperationException() + : _call.RequestStream.WriteAsync( + new ReadReq { + Ack = new ReadReq.Types.Ack { + Ids = { + Array.ConvertAll(eventIds, id => id.ToDto()) + } + } + } + ); + } + + Task NackInternal(Uuid[] eventIds, PersistentSubscriptionNakEventAction action, string reason) { + if (eventIds.Length > MaxEventIdLength) { + throw new ArgumentException( + $"The number of eventIds exceeds the maximum length of {MaxEventIdLength}.", + nameof(eventIds) + ); + } + + return _call is null + ? throw new InvalidOperationException() + : _call.RequestStream.WriteAsync( + new ReadReq { + Nack = new ReadReq.Types.Nack { + Ids = { + Array.ConvertAll(eventIds, id => id.ToDto()) + }, + Action = action switch { + PersistentSubscriptionNakEventAction.Park => ReadReq.Types.Nack.Types.Action.Park, + PersistentSubscriptionNakEventAction.Retry => ReadReq.Types.Nack.Types.Action.Retry, + PersistentSubscriptionNakEventAction.Skip => ReadReq.Types.Nack.Types.Action.Skip, + PersistentSubscriptionNakEventAction.Stop => ReadReq.Types.Nack.Types.Action.Stop, + _ => ReadReq.Types.Nack.Types.Action.Unknown + }, + Reason = reason + } + } + ); + } + + static EventRecord? ConvertToEventRecord(ReadResp.Types.ReadEvent.Types.RecordedEvent? e) => + e is null + ? null + : new EventRecord( + e.StreamIdentifier!, + Uuid.FromDto(e.Id), + new StreamPosition(e.StreamRevision), + new Position(e.CommitPosition, e.PreparePosition), + e.Metadata, + e.Data.ToByteArray(), + e.CustomMetadata.ToByteArray() + ); + + /// + public async ValueTask DisposeAsync() { + await CastAndDispose(_cts).ConfigureAwait(false); + await CastAndDispose(_call).ConfigureAwait(false); + + return; + + static async Task CastAndDispose(IDisposable? resource) { + switch (resource) { + case null: + return; + + case IAsyncDisposable resourceAsyncDisposable: + await resourceAsyncDisposable.DisposeAsync().ConfigureAwait(false); + break; + + default: + resource.Dispose(); + break; + } + } + } + + /// + public void Dispose() { + _cts.Dispose(); + _call?.Dispose(); + } + + /// + public async IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = default) { + await foreach (var message in Messages.WithCancellation(cancellationToken)) { + if (message is not PersistentSubscriptionMessage.Event(var resolvedEvent, _)) + continue; + + yield return resolvedEvent; + } + } + } + } +} diff --git a/src/Kurrent.Client/PersistentSubscriptions/KurrentPersistentSubscriptionsClient.ReplayParked.cs b/src/Kurrent.Client/PersistentSubscriptions/KurrentPersistentSubscriptionsClient.ReplayParked.cs new file mode 100644 index 000000000..e13d4a4ad --- /dev/null +++ b/src/Kurrent.Client/PersistentSubscriptions/KurrentPersistentSubscriptionsClient.ReplayParked.cs @@ -0,0 +1,94 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using EventStore.Client.PersistentSubscriptions; +using Grpc.Core; +using NotSupportedException = System.NotSupportedException; + +#nullable enable +namespace EventStore.Client { + partial class KurrentPersistentSubscriptionsClient { + /// + /// Retry the parked messages of the persistent subscription + /// + public async Task ReplayParkedMessagesToAllAsync(string groupName, long? stopAt = null, TimeSpan? deadline = null, + UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) { + + var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); + if (channelInfo.ServerCapabilities.SupportsPersistentSubscriptionsReplayParked) { + var req = new ReplayParkedReq() { + Options = new ReplayParkedReq.Types.Options{ + GroupName = groupName, + All = new Empty() + }, + }; + + await ReplayParkedGrpcAsync(req, stopAt, deadline, userCredentials, channelInfo.CallInvoker, cancellationToken) + .ConfigureAwait(false); + + return; + } + + if (channelInfo.ServerCapabilities.SupportsPersistentSubscriptionsToAll) { + await ReplayParkedHttpAsync(SystemStreams.AllStream, groupName, stopAt, channelInfo, + deadline, userCredentials, cancellationToken) + .ConfigureAwait(false); + + return; + } + + throw new NotSupportedException("The server does not support persistent subscriptions to $all."); + } + + /// + /// Retry the parked messages of the persistent subscription + /// + public async Task ReplayParkedMessagesToStreamAsync(string streamName, string groupName, long? stopAt=null, + TimeSpan? deadline=null, UserCredentials? userCredentials=null, CancellationToken cancellationToken=default) { + + var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); + if (channelInfo.ServerCapabilities.SupportsPersistentSubscriptionsReplayParked) { + var req = new ReplayParkedReq() { + Options = new ReplayParkedReq.Types.Options { + GroupName = groupName, + StreamIdentifier = streamName + }, + }; + + await ReplayParkedGrpcAsync(req, stopAt, deadline, userCredentials, channelInfo.CallInvoker, cancellationToken) + .ConfigureAwait(false); + + return; + } + + await ReplayParkedHttpAsync(streamName, groupName, stopAt, channelInfo, deadline, userCredentials, cancellationToken) + .ConfigureAwait(false); + } + + private async Task ReplayParkedGrpcAsync(ReplayParkedReq req, long? numberOfEvents, TimeSpan? deadline, + UserCredentials? userCredentials, CallInvoker callInvoker, CancellationToken cancellationToken) { + + if (numberOfEvents.HasValue) { + req.Options.StopAt = numberOfEvents.Value; + } else { + req.Options.NoLimit = new Empty(); + } + + await new PersistentSubscriptions.PersistentSubscriptions.PersistentSubscriptionsClient(callInvoker) + .ReplayParkedAsync(req, KurrentCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)) + .ConfigureAwait(false); + } + + private async Task ReplayParkedHttpAsync(string streamName, string groupName, long? numberOfEvents, + ChannelInfo channelInfo, TimeSpan? deadline, UserCredentials? userCredentials, CancellationToken cancellationToken) { + + var path = $"/subscriptions/{UrlEncode(streamName)}/{UrlEncode(groupName)}/replayParked"; + var query = numberOfEvents.HasValue ? $"stopAt={numberOfEvents.Value}":""; + + await HttpPost(path, query, + onNotFound: () => throw new PersistentSubscriptionNotFoundException(streamName, groupName), + channelInfo, deadline, userCredentials, cancellationToken) + .ConfigureAwait(false); + } + } +} diff --git a/src/Kurrent.Client/PersistentSubscriptions/KurrentPersistentSubscriptionsClient.RestartSubsystem.cs b/src/Kurrent.Client/PersistentSubscriptions/KurrentPersistentSubscriptionsClient.RestartSubsystem.cs new file mode 100644 index 000000000..a82587442 --- /dev/null +++ b/src/Kurrent.Client/PersistentSubscriptions/KurrentPersistentSubscriptionsClient.RestartSubsystem.cs @@ -0,0 +1,32 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +#nullable enable +namespace EventStore.Client { + partial class KurrentPersistentSubscriptionsClient { + /// + /// Restarts the persistent subscriptions subsystem. + /// + public async Task RestartSubsystemAsync(TimeSpan? deadline = null, UserCredentials? userCredentials = null, + CancellationToken cancellationToken = default) { + + var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); + if (channelInfo.ServerCapabilities.SupportsPersistentSubscriptionsRestartSubsystem) { + await new PersistentSubscriptions.PersistentSubscriptions.PersistentSubscriptionsClient(channelInfo.CallInvoker) + .RestartSubsystemAsync(new Empty(), KurrentCallOptions + .CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)) + .ConfigureAwait(false); + return; + } + + await HttpPost( + path: "/subscriptions/restart", + query: "", + onNotFound: () => + throw new Exception("Unexpected exception while restarting the persistent subscription subsystem."), + channelInfo, deadline, userCredentials, cancellationToken) + .ConfigureAwait(false); + } + } +} diff --git a/src/Kurrent.Client/PersistentSubscriptions/KurrentPersistentSubscriptionsClient.Update.cs b/src/Kurrent.Client/PersistentSubscriptions/KurrentPersistentSubscriptionsClient.Update.cs new file mode 100644 index 000000000..19eb67d4b --- /dev/null +++ b/src/Kurrent.Client/PersistentSubscriptions/KurrentPersistentSubscriptionsClient.Update.cs @@ -0,0 +1,161 @@ +using EventStore.Client.PersistentSubscriptions; + +namespace EventStore.Client { + public partial class KurrentPersistentSubscriptionsClient { + private static readonly IDictionary NamedConsumerStrategyToUpdateProto + = new Dictionary { + [SystemConsumerStrategies.DispatchToSingle] = UpdateReq.Types.ConsumerStrategy.DispatchToSingle, + [SystemConsumerStrategies.RoundRobin] = UpdateReq.Types.ConsumerStrategy.RoundRobin, + [SystemConsumerStrategies.Pinned] = UpdateReq.Types.ConsumerStrategy.Pinned, + }; + + private static UpdateReq.Types.StreamOptions StreamOptionsForUpdateProto(string streamName, + StreamPosition position) { + if (position == StreamPosition.Start) { + return new UpdateReq.Types.StreamOptions { + StreamIdentifier = streamName, + Start = new Empty() + }; + } + + if (position == StreamPosition.End) { + return new UpdateReq.Types.StreamOptions { + StreamIdentifier = streamName, + End = new Empty() + }; + } + + return new UpdateReq.Types.StreamOptions { + StreamIdentifier = streamName, + Revision = position.ToUInt64() + }; + } + + private static UpdateReq.Types.AllOptions AllOptionsForUpdateProto(Position position) { + if (position == Position.Start) { + return new UpdateReq.Types.AllOptions { + Start = new Empty() + }; + } + + if (position == Position.End) { + return new UpdateReq.Types.AllOptions { + End = new Empty() + }; + } + + return new UpdateReq.Types.AllOptions { + Position = new UpdateReq.Types.Position { + CommitPosition = position.CommitPosition, + PreparePosition = position.PreparePosition + } + }; + } + + + /// + /// Updates a persistent subscription. + /// + /// + [Obsolete("UpdateAsync is no longer supported. Use UpdateToStreamAsync instead.", false)] + public Task UpdateAsync(string streamName, string groupName, PersistentSubscriptionSettings settings, + TimeSpan? deadline = null, UserCredentials? userCredentials = null, + CancellationToken cancellationToken = default) => + UpdateToStreamAsync(streamName, groupName, settings, deadline, userCredentials, cancellationToken); + + /// + /// Updates a persistent subscription. + /// + /// + public async Task UpdateToStreamAsync(string streamName, string groupName, PersistentSubscriptionSettings settings, + TimeSpan? deadline = null, UserCredentials? userCredentials = null, + CancellationToken cancellationToken = default) { + if (streamName is null) { + throw new ArgumentNullException(nameof(streamName)); + } + + if (groupName is null) { + throw new ArgumentNullException(nameof(groupName)); + } + + if (settings is null) { + throw new ArgumentNullException(nameof(settings)); + } + + if (settings.ConsumerStrategyName is null) { + throw new ArgumentNullException(nameof(settings.ConsumerStrategyName)); + } + + if (streamName != SystemStreams.AllStream && settings.StartFrom is not null && + settings.StartFrom is not StreamPosition) { + throw new ArgumentException( + $"{nameof(settings.StartFrom)} must be of type '{nameof(StreamPosition)}' when subscribing to a stream"); + } + + if (streamName == SystemStreams.AllStream && settings.StartFrom is not null && + settings.StartFrom is not Position) { + throw new ArgumentException( + $"{nameof(settings.StartFrom)} must be of type '{nameof(Position)}' when subscribing to {SystemStreams.AllStream}"); + } + + var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); + + if (streamName == SystemStreams.AllStream && + !channelInfo.ServerCapabilities.SupportsPersistentSubscriptionsToAll) { + throw new InvalidOperationException("The server does not support persistent subscriptions to $all."); + } + + using var call = new PersistentSubscriptions.PersistentSubscriptions.PersistentSubscriptionsClient(channelInfo.CallInvoker) + .UpdateAsync(new UpdateReq { + Options = new UpdateReq.Types.Options { + GroupName = groupName, + Stream = streamName != SystemStreams.AllStream + ? StreamOptionsForUpdateProto(streamName, + (StreamPosition)(settings.StartFrom ?? StreamPosition.End)) + : null, + All = streamName == SystemStreams.AllStream + ? AllOptionsForUpdateProto((Position)(settings.StartFrom ?? Position.End)) + : null, +#pragma warning disable 612 + StreamIdentifier = + streamName != SystemStreams.AllStream + ? streamName + : string.Empty, /*for backwards compatibility*/ +#pragma warning restore 612 + Settings = new UpdateReq.Types.Settings { +#pragma warning disable 612 + Revision = streamName != SystemStreams.AllStream + ? ((StreamPosition)(settings.StartFrom ?? StreamPosition.End)).ToUInt64() + : default, /*for backwards compatibility*/ +#pragma warning restore 612 + CheckpointAfterMs = (int)settings.CheckPointAfter.TotalMilliseconds, + ExtraStatistics = settings.ExtraStatistics, + MessageTimeoutMs = (int)settings.MessageTimeout.TotalMilliseconds, + ResolveLinks = settings.ResolveLinkTos, + HistoryBufferSize = settings.HistoryBufferSize, + LiveBufferSize = settings.LiveBufferSize, + MaxCheckpointCount = settings.CheckPointUpperBound, + MaxRetryCount = settings.MaxRetryCount, + MaxSubscriberCount = settings.MaxSubscriberCount, + MinCheckpointCount = settings.CheckPointLowerBound, + NamedConsumerStrategy = + NamedConsumerStrategyToUpdateProto[settings.ConsumerStrategyName], + ReadBatchSize = settings.ReadBatchSize + } + } + }, + KurrentCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); + await call.ResponseAsync.ConfigureAwait(false); + } + + /// + /// Updates a persistent subscription to $all. + /// + public async Task UpdateToAllAsync(string groupName, PersistentSubscriptionSettings settings, + TimeSpan? deadline = null, UserCredentials? userCredentials = null, + CancellationToken cancellationToken = default) => + await UpdateToStreamAsync(SystemStreams.AllStream, groupName, settings, deadline, userCredentials, + cancellationToken) + .ConfigureAwait(false); + } +} diff --git a/src/Kurrent.Client/PersistentSubscriptions/KurrentPersistentSubscriptionsClient.cs b/src/Kurrent.Client/PersistentSubscriptions/KurrentPersistentSubscriptionsClient.cs new file mode 100644 index 000000000..070f32698 --- /dev/null +++ b/src/Kurrent.Client/PersistentSubscriptions/KurrentPersistentSubscriptionsClient.cs @@ -0,0 +1,46 @@ +using System.Text.Encodings.Web; +using System.Threading.Channels; +using Grpc.Core; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; + +namespace EventStore.Client { + /// + /// The client used to manage persistent subscriptions in the KurrentDB. + /// + public sealed partial class KurrentPersistentSubscriptionsClient : KurrentClientBase { + private static BoundedChannelOptions ReadBoundedChannelOptions = new (1) { + SingleReader = true, + SingleWriter = true, + AllowSynchronousContinuations = true + }; + + private readonly ILogger _log; + + /// + /// Constructs a new . + /// + public KurrentPersistentSubscriptionsClient(KurrentClientSettings? settings) : base(settings, + new Dictionary> { + [Constants.Exceptions.PersistentSubscriptionDoesNotExist] = ex => new + PersistentSubscriptionNotFoundException( + ex.Trailers.First(x => x.Key == Constants.Exceptions.StreamName).Value, + ex.Trailers.FirstOrDefault(x => x.Key == Constants.Exceptions.GroupName)?.Value ?? "", ex), + [Constants.Exceptions.MaximumSubscribersReached] = ex => new + MaximumSubscribersReachedException( + ex.Trailers.First(x => x.Key == Constants.Exceptions.StreamName).Value, + ex.Trailers.First(x => x.Key == Constants.Exceptions.GroupName).Value, ex), + [Constants.Exceptions.PersistentSubscriptionDropped] = ex => new + PersistentSubscriptionDroppedByServerException( + ex.Trailers.First(x => x.Key == Constants.Exceptions.StreamName).Value, + ex.Trailers.First(x => x.Key == Constants.Exceptions.GroupName).Value, ex) + }) { + _log = Settings.LoggerFactory?.CreateLogger() + ?? new NullLogger(); + } + + private static string UrlEncode(string s) { + return UrlEncoder.Default.Encode(s); + } + } +} diff --git a/src/Kurrent.Client/PersistentSubscriptions/KurrentPersistentSubscriptionsClientCollectionExtensions.cs b/src/Kurrent.Client/PersistentSubscriptions/KurrentPersistentSubscriptionsClientCollectionExtensions.cs new file mode 100644 index 000000000..9ed65e091 --- /dev/null +++ b/src/Kurrent.Client/PersistentSubscriptions/KurrentPersistentSubscriptionsClientCollectionExtensions.cs @@ -0,0 +1,61 @@ +// ReSharper disable CheckNamespace + +using System; +using System.Net.Http; +using EventStore.Client; +using Grpc.Core.Interceptors; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Logging; + +namespace Microsoft.Extensions.DependencyInjection { + /// + /// A set of extension methods for which provide support for an . + /// + public static class KurrentPersistentSubscriptionsClientCollectionExtensions { + /// + /// Adds an to the . + /// + /// + public static IServiceCollection AddKurrentPersistentSubscriptionsClient(this IServiceCollection services, + Uri address, Func? createHttpMessageHandler = null) + => services.AddKurrentPersistentSubscriptionsClient(options => { + options.ConnectivitySettings.Address = address; + options.CreateHttpMessageHandler = createHttpMessageHandler; + }); + + /// + /// Adds an to the . + /// + /// + public static IServiceCollection AddKurrentPersistentSubscriptionsClient(this IServiceCollection services, + Action? configureSettings = null) => + services.AddKurrentPersistentSubscriptionsClient(new KurrentClientSettings(), + configureSettings); + + /// + /// Adds an to the . + /// + /// + public static IServiceCollection AddKurrentPersistentSubscriptionsClient(this IServiceCollection services, + string connectionString, Action? configureSettings = null) => + services.AddKurrentPersistentSubscriptionsClient(KurrentClientSettings.Create(connectionString), + configureSettings); + + private static IServiceCollection AddKurrentPersistentSubscriptionsClient(this IServiceCollection services, + KurrentClientSettings settings, Action? configureSettings) { + if (services == null) { + throw new ArgumentNullException(nameof(services)); + } + + configureSettings?.Invoke(settings); + services.TryAddSingleton(provider => { + settings.LoggerFactory ??= provider.GetService(); + settings.Interceptors ??= provider.GetServices(); + + return new KurrentPersistentSubscriptionsClient(settings); + }); + return services; + } + } +} +// ReSharper restore CheckNamespace diff --git a/src/Kurrent.Client/PersistentSubscriptions/MaximumSubscribersReachedException.cs b/src/Kurrent.Client/PersistentSubscriptions/MaximumSubscribersReachedException.cs new file mode 100644 index 000000000..879378f85 --- /dev/null +++ b/src/Kurrent.Client/PersistentSubscriptions/MaximumSubscribersReachedException.cs @@ -0,0 +1,30 @@ +using System; + +namespace EventStore.Client { + /// + /// The exception that is thrown when the maximum number of subscribers on a persistent subscription is exceeded. + /// + public class MaximumSubscribersReachedException : Exception { + /// + /// The stream name. + /// + public readonly string StreamName; + /// + /// The group name. + /// + public readonly string GroupName; + + /// + /// Constructs a new . + /// + /// + public MaximumSubscribersReachedException(string streamName, string groupName, Exception? exception = null) + : base($"Maximum subscriptions reached for subscription group '{groupName}' on stream '{streamName}.'", + exception) { + if (streamName == null) throw new ArgumentNullException(nameof(streamName)); + if (groupName == null) throw new ArgumentNullException(nameof(groupName)); + StreamName = streamName; + GroupName = groupName; + } + } +} diff --git a/src/Kurrent.Client/PersistentSubscriptions/PersistentSubscription.cs b/src/Kurrent.Client/PersistentSubscriptions/PersistentSubscription.cs new file mode 100644 index 000000000..637f54e30 --- /dev/null +++ b/src/Kurrent.Client/PersistentSubscriptions/PersistentSubscription.cs @@ -0,0 +1,205 @@ +using EventStore.Client.PersistentSubscriptions; +using Grpc.Core; +using Microsoft.Extensions.Logging; + +namespace EventStore.Client { + /// + /// Represents a persistent subscription connection. + /// + public class PersistentSubscription : IDisposable { + private readonly KurrentPersistentSubscriptionsClient.PersistentSubscriptionResult _persistentSubscriptionResult; + private readonly IAsyncEnumerator _enumerator; + private readonly Func _eventAppeared; + private readonly Action _subscriptionDropped; + private readonly ILogger _log; + private readonly CancellationTokenSource _cts; + + private int _subscriptionDroppedInvoked; + + /// + /// The Subscription Id. + /// + public string SubscriptionId { get; } + + internal static async Task Confirm( + KurrentPersistentSubscriptionsClient.PersistentSubscriptionResult persistentSubscriptionResult, + Func eventAppeared, + Action subscriptionDropped, + ILogger log, UserCredentials? userCredentials, CancellationToken cancellationToken = default) { + var enumerator = persistentSubscriptionResult + .Messages + .GetAsyncEnumerator(cancellationToken); + + var result = await enumerator.MoveNextAsync(cancellationToken).ConfigureAwait(false); + + return (result, enumerator.Current) switch { + (true, PersistentSubscriptionMessage.SubscriptionConfirmation (var subscriptionId)) => + new PersistentSubscription(persistentSubscriptionResult, enumerator, subscriptionId, eventAppeared, + subscriptionDropped, log, cancellationToken), + (true, PersistentSubscriptionMessage.NotFound) => + throw new PersistentSubscriptionNotFoundException(persistentSubscriptionResult.StreamName, + persistentSubscriptionResult.GroupName), + _ => throw new InvalidOperationException("Subscription could not be confirmed.") + }; + } + + // PersistentSubscription takes responsibility for disposing the call and the disposable + private PersistentSubscription( + KurrentPersistentSubscriptionsClient.PersistentSubscriptionResult persistentSubscriptionResult, + IAsyncEnumerator enumerator, string subscriptionId, + Func eventAppeared, + Action subscriptionDropped, ILogger log, + CancellationToken cancellationToken) { + _persistentSubscriptionResult = persistentSubscriptionResult; + _enumerator = enumerator; + SubscriptionId = subscriptionId; + _eventAppeared = eventAppeared; + _subscriptionDropped = subscriptionDropped; + _log = log; + _cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + + Task.Run(Subscribe, _cts.Token); + } + + /// + /// Acknowledge that a message has completed processing (this will tell the server it has been processed). + /// + /// There is no need to ack a message if you have Auto Ack enabled. + /// The of the s to acknowledge. There should not be more than 2000 to ack at a time. + public Task Ack(params Uuid[] eventIds) => AckInternal(eventIds); + + /// + /// Acknowledge that a message has completed processing (this will tell the server it has been processed). + /// + /// There is no need to ack a message if you have Auto Ack enabled. + /// The of the s to acknowledge. There should not be more than 2000 to ack at a time. + public Task Ack(IEnumerable eventIds) => Ack(eventIds.ToArray()); + + /// + /// Acknowledge that a message has completed processing (this will tell the server it has been processed). + /// + /// There is no need to ack a message if you have Auto Ack enabled. + /// The s to acknowledge. There should not be more than 2000 to ack at a time. + public Task Ack(params ResolvedEvent[] resolvedEvents) => + Ack(Array.ConvertAll(resolvedEvents, resolvedEvent => resolvedEvent.OriginalEvent.EventId)); + + /// + /// Acknowledge that a message has completed processing (this will tell the server it has been processed). + /// + /// There is no need to ack a message if you have Auto Ack enabled. + /// The s to acknowledge. There should not be more than 2000 to ack at a time. + public Task Ack(IEnumerable resolvedEvents) => + Ack(resolvedEvents.Select(resolvedEvent => resolvedEvent.OriginalEvent.EventId)); + + + /// + /// Acknowledge that a message has failed processing (this will tell the server it has not been processed). + /// + /// The to take. + /// A reason given. + /// The of the s to nak. There should not be more than 2000 to nak at a time. + /// The number of eventIds exceeded the limit of 2000. + public Task Nack(PersistentSubscriptionNakEventAction action, string reason, params Uuid[] eventIds) => NackInternal(eventIds, action, reason); + + /// + /// Acknowledge that a message has failed processing (this will tell the server it has not been processed). + /// + /// The to take. + /// A reason given. + /// The s to nak. There should not be more than 2000 to nak at a time. + /// The number of resolvedEvents exceeded the limit of 2000. + public Task Nack(PersistentSubscriptionNakEventAction action, string reason, + params ResolvedEvent[] resolvedEvents) => + Nack(action, reason, + Array.ConvertAll(resolvedEvents, resolvedEvent => resolvedEvent.OriginalEvent.EventId)); + + /// + public void Dispose() => SubscriptionDropped(SubscriptionDroppedReason.Disposed); + + private async Task Subscribe() { + _log.LogDebug("Persistent Subscription {subscriptionId} confirmed.", SubscriptionId); + + try { + while (await _enumerator.MoveNextAsync(_cts.Token).ConfigureAwait(false)) { + if (_enumerator.Current is not PersistentSubscriptionMessage.Event(var resolvedEvent, var retryCount)) { + continue; + } + + if (_enumerator.Current is PersistentSubscriptionMessage.NotFound) { + if (_subscriptionDroppedInvoked != 0) { + return; + } + SubscriptionDropped(SubscriptionDroppedReason.ServerError, + new PersistentSubscriptionNotFoundException( + _persistentSubscriptionResult.StreamName, _persistentSubscriptionResult.GroupName)); + return; + } + + _log.LogTrace( + "Persistent Subscription {subscriptionId} received event {streamName}@{streamRevision} {position}", + SubscriptionId, resolvedEvent.OriginalEvent.EventStreamId, + resolvedEvent.OriginalEvent.EventNumber, resolvedEvent.OriginalEvent.Position); + + try { + await _eventAppeared( + this, + resolvedEvent, + retryCount, + _cts.Token).ConfigureAwait(false); + } catch (Exception ex) when (ex is ObjectDisposedException or OperationCanceledException) { + if (_subscriptionDroppedInvoked != 0) { + return; + } + + _log.LogWarning(ex, + "Persistent Subscription {subscriptionId} was dropped because cancellation was requested by another caller.", + SubscriptionId); + + SubscriptionDropped(SubscriptionDroppedReason.Disposed); + + return; + } catch (Exception ex) { + _log.LogError(ex, + "Persistent Subscription {subscriptionId} was dropped because the subscriber made an error.", + SubscriptionId); + SubscriptionDropped(SubscriptionDroppedReason.SubscriberError, ex); + + return; + } + } + } catch (Exception ex) { + if (_subscriptionDroppedInvoked == 0) { + _log.LogError(ex, + "Persistent Subscription {subscriptionId} was dropped because an error occurred on the server.", + SubscriptionId); + SubscriptionDropped(SubscriptionDroppedReason.ServerError, ex); + } + } finally { + if (_subscriptionDroppedInvoked == 0) { + _log.LogError( + "Persistent Subscription {subscriptionId} was unexpectedly terminated.", + SubscriptionId); + SubscriptionDropped(SubscriptionDroppedReason.ServerError); + } + } + } + + private void SubscriptionDropped(SubscriptionDroppedReason reason, Exception? ex = null) { + if (Interlocked.CompareExchange(ref _subscriptionDroppedInvoked, 1, 0) == 1) { + return; + } + + try { + _subscriptionDropped.Invoke(this, reason, ex); + } finally { + _persistentSubscriptionResult.Dispose(); + _cts.Dispose(); + } + } + + private Task AckInternal(params Uuid[] ids) => _persistentSubscriptionResult.Ack(ids); + + private Task NackInternal(Uuid[] ids, PersistentSubscriptionNakEventAction action, string reason) => + _persistentSubscriptionResult.Nack(action, reason, ids); + } +} diff --git a/src/Kurrent.Client/PersistentSubscriptions/PersistentSubscriptionDroppedByServerException.cs b/src/Kurrent.Client/PersistentSubscriptions/PersistentSubscriptionDroppedByServerException.cs new file mode 100644 index 000000000..b25ea4f72 --- /dev/null +++ b/src/Kurrent.Client/PersistentSubscriptions/PersistentSubscriptionDroppedByServerException.cs @@ -0,0 +1,31 @@ +using System; + +namespace EventStore.Client { + /// + /// The exception that is thrown when the KurrentDB drops a persistent subscription. + /// + public class PersistentSubscriptionDroppedByServerException : Exception { + /// + /// The stream name. + /// + public readonly string StreamName; + + /// + /// The group name. + /// + public readonly string GroupName; + + /// + /// Constructs a new . + /// + /// + public PersistentSubscriptionDroppedByServerException(string streamName, string groupName, + Exception? exception = null) + : base($"Subscription group '{groupName}' on stream '{streamName}' was dropped.", exception) { + if (streamName == null) throw new ArgumentNullException(nameof(streamName)); + if (groupName == null) throw new ArgumentNullException(nameof(groupName)); + StreamName = streamName; + GroupName = groupName; + } + } +} diff --git a/src/Kurrent.Client/PersistentSubscriptions/PersistentSubscriptionExtraStatistic.cs b/src/Kurrent.Client/PersistentSubscriptions/PersistentSubscriptionExtraStatistic.cs new file mode 100644 index 000000000..5b41893c4 --- /dev/null +++ b/src/Kurrent.Client/PersistentSubscriptions/PersistentSubscriptionExtraStatistic.cs @@ -0,0 +1,23 @@ +namespace EventStore.Client; + +/// +/// Provides the definitions of the available extra statistics. +/// +public static class PersistentSubscriptionExtraStatistic { +#pragma warning disable CS1591 + public const string Highest = "Highest"; + public const string Mean = "Mean"; + public const string Median = "Median"; + public const string Fastest = "Fastest"; + public const string Quintile1 = "Quintile 1"; + public const string Quintile2 = "Quintile 2"; + public const string Quintile3 = "Quintile 3"; + public const string Quintile4 = "Quintile 4"; + public const string Quintile5 = "Quintile 5"; + public const string NinetyPercent = "90%"; + public const string NinetyFivePercent = "95%"; + public const string NinetyNinePercent = "99%"; + public const string NinetyNinePointFivePercent = "99.5%"; + public const string NinetyNinePointNinePercent = "99.9%"; +#pragma warning restore CS1591 +} diff --git a/src/Kurrent.Client/PersistentSubscriptions/PersistentSubscriptionInfo.cs b/src/Kurrent.Client/PersistentSubscriptions/PersistentSubscriptionInfo.cs new file mode 100644 index 000000000..40af4cdf2 --- /dev/null +++ b/src/Kurrent.Client/PersistentSubscriptions/PersistentSubscriptionInfo.cs @@ -0,0 +1,224 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using EventStore.Client.PersistentSubscriptions; +using Google.Protobuf.Collections; + +namespace EventStore.Client { + /// + /// Provides the details for a persistent subscription. + /// + /// The source of events for the subscription. + /// The group name given on creation. + /// The current status of the subscription. + /// Active connections to the subscription. + /// The settings used to create the persistant subscription. + /// Live statistics for the persistent subscription. + public record PersistentSubscriptionInfo(string EventSource, string GroupName, string Status, + IEnumerable Connections, + PersistentSubscriptionSettings? Settings, PersistentSubscriptionStats Stats) { + + internal static PersistentSubscriptionInfo From(SubscriptionInfo info) { + IPosition? startFrom = null; + IPosition? lastCheckpointedEventPosition = null; + IPosition? lastKnownEventPosition = null; + if (info.EventSource == SystemStreams.AllStream) { + if (Position.TryParse(info.StartFrom, out var position)) { + startFrom = position; + } + if (Position.TryParse(info.LastCheckpointedEventPosition, out position)) { + lastCheckpointedEventPosition = position; + } + if (Position.TryParse(info.LastKnownEventPosition, out position)) { + lastKnownEventPosition = position; + } + } else { + if (long.TryParse(info.StartFrom, out var streamPosition)) { + startFrom = StreamPosition.FromInt64(streamPosition); + } + if (ulong.TryParse(info.LastCheckpointedEventPosition, out var position)) { + lastCheckpointedEventPosition = new StreamPosition(position); + } + if (ulong.TryParse(info.LastKnownEventPosition, out position)) { + lastKnownEventPosition = new StreamPosition(position); + } + } + + return new PersistentSubscriptionInfo( + EventSource: info.EventSource, + GroupName: info.GroupName, + Status: info.Status, + Connections: From(info.Connections), + Settings: new PersistentSubscriptionSettings( + resolveLinkTos: info.ResolveLinkTos, + startFrom: startFrom, + extraStatistics: info.ExtraStatistics, + messageTimeout: TimeSpan.FromMilliseconds(info.MessageTimeoutMilliseconds), + maxRetryCount: info.MaxRetryCount, + liveBufferSize: info.LiveBufferSize, + readBatchSize: info.ReadBatchSize, + historyBufferSize: info.BufferSize, + checkPointAfter: TimeSpan.FromMilliseconds(info.CheckPointAfterMilliseconds), + checkPointLowerBound: info.MinCheckPointCount, + checkPointUpperBound: info.MaxCheckPointCount, + maxSubscriberCount: info.MaxSubscriberCount, + consumerStrategyName: info.NamedConsumerStrategy + ), + Stats: new PersistentSubscriptionStats( + AveragePerSecond: info.AveragePerSecond, + TotalItems: info.TotalItems, + CountSinceLastMeasurement: info.CountSinceLastMeasurement, + ReadBufferCount: info.ReadBufferCount, + LiveBufferCount: info.LiveBufferCount, + RetryBufferCount: info.RetryBufferCount, + TotalInFlightMessages: info.TotalInFlightMessages, + OutstandingMessagesCount: info.OutstandingMessagesCount, + ParkedMessageCount: info.ParkedMessageCount, + LastCheckpointedEventPosition: lastCheckpointedEventPosition, + LastKnownEventPosition: lastKnownEventPosition + ) + ); + } + + internal static PersistentSubscriptionInfo From(PersistentSubscriptionDto info) { + PersistentSubscriptionSettings? settings = null; + if (info.Config != null) { + settings = new PersistentSubscriptionSettings( + resolveLinkTos: info.Config.ResolveLinktos, + // we only need to support StreamPosition as $all was never implemented in http api. + startFrom: new StreamPosition(info.Config.StartFrom), + extraStatistics: info.Config.ExtraStatistics, + messageTimeout: TimeSpan.FromMilliseconds(info.Config.MessageTimeoutMilliseconds), + maxRetryCount: info.Config.MaxRetryCount, + liveBufferSize: info.Config.LiveBufferSize, + readBatchSize: info.Config.ReadBatchSize, + historyBufferSize: info.Config.BufferSize, + checkPointAfter: TimeSpan.FromMilliseconds(info.Config.CheckPointAfterMilliseconds), + checkPointLowerBound: info.Config.MinCheckPointCount, + checkPointUpperBound: info.Config.MaxCheckPointCount, + maxSubscriberCount: info.Config.MaxSubscriberCount, + consumerStrategyName: info.Config.NamedConsumerStrategy + ); + } + + return new PersistentSubscriptionInfo( + EventSource: info.EventStreamId, + GroupName: info.GroupName, + Status: info.Status, + Connections: PersistentSubscriptionConnectionInfo.CreateFrom(info.Connections), + Settings: settings, + Stats: new PersistentSubscriptionStats( + AveragePerSecond: (int)info.AverageItemsPerSecond, + TotalItems: info.TotalItemsProcessed, + CountSinceLastMeasurement: info.CountSinceLastMeasurement, + ReadBufferCount: info.ReadBufferCount, + LiveBufferCount: info.LiveBufferCount, + RetryBufferCount: info.RetryBufferCount, + TotalInFlightMessages: info.TotalInFlightMessages, + OutstandingMessagesCount: info.OutstandingMessagesCount, + ParkedMessageCount: info.ParkedMessageCount, + LastCheckpointedEventPosition: StreamPosition.FromInt64(info.LastProcessedEventNumber), + LastKnownEventPosition: StreamPosition.FromInt64(info.LastKnownEventNumber) + ) + ); + } + + private static IEnumerable From( + RepeatedField connections) { + foreach (var conn in connections) { + yield return new PersistentSubscriptionConnectionInfo( + From: conn.From, + Username: conn.Username, + AverageItemsPerSecond: conn.AverageItemsPerSecond, + TotalItems: conn.TotalItems, + CountSinceLastMeasurement: conn.CountSinceLastMeasurement, + AvailableSlots: conn.AvailableSlots, + InFlightMessages: conn.InFlightMessages, + ConnectionName: conn.ConnectionName, + ExtraStatistics: From(conn.ObservedMeasurements) + ); + } + } + + private static IDictionary From(IEnumerable measurements) => + measurements.ToDictionary(k => k.Key, v => v.Value); + } + + /// + /// Provides the statistics of a persistent subscription. + /// + /// Average number of events per second. + /// Total number of events processed by subscription. + /// Number of events seen since last measurement on this connection (used as the basis for ). + /// Number of events in the read buffer. + /// Number of events in the live buffer. + /// Number of events in the retry buffer. + /// Current in flight messages across all connections. + /// Current number of outstanding messages. + /// The current number of parked messages. + /// The of the last checkpoint. This will be null if there are no checkpoints. + /// The of the last known event. This will be undefined if no events have been received yet. + public record PersistentSubscriptionStats( + int AveragePerSecond, long TotalItems, long CountSinceLastMeasurement, int ReadBufferCount, + long LiveBufferCount, int RetryBufferCount, int TotalInFlightMessages, int OutstandingMessagesCount, + long ParkedMessageCount, IPosition? LastCheckpointedEventPosition, IPosition? LastKnownEventPosition); + + /// + /// Provides the details of a persistent subscription connection. + /// + /// Origin of this connection. + /// Connection username. + /// The name of the connection. + /// Average events per second on this connection. + /// Total items on this connection. + /// Number of items seen since last measurement on this connection (used as the basis for averageItemsPerSecond). + /// Number of available slots. + /// Number of in flight messages on this connection. + /// Timing measurements for the connection. Can be enabled with the ExtraStatistics setting. + public record PersistentSubscriptionConnectionInfo(string From, string Username, string ConnectionName, int AverageItemsPerSecond, + long TotalItems, long CountSinceLastMeasurement, int AvailableSlots, int InFlightMessages, + IDictionary ExtraStatistics) { + + internal static IEnumerable CreateFrom( + IEnumerable connections) { + + foreach (var connection in connections) { + yield return CreateFrom(connection); + } + } + + private static PersistentSubscriptionConnectionInfo CreateFrom(PersistentSubscriptionConnectionInfoDto connection) { + return new PersistentSubscriptionConnectionInfo( + From: connection.From, + Username: connection.Username, + ConnectionName: connection.ConnectionName, + AverageItemsPerSecond: (int)connection.AverageItemsPerSecond, + TotalItems: connection.TotalItems, + CountSinceLastMeasurement: connection.CountSinceLastMeasurement, + AvailableSlots: connection.AvailableSlots, + InFlightMessages: connection.InFlightMessages, + ExtraStatistics: CreateFrom(connection.ExtraStatistics) + ); + } + + private static IDictionary CreateFrom(IEnumerable extraStatistics) => + extraStatistics.ToDictionary(k => k.Key, v => v.Value); + } + + internal record PersistentSubscriptionDto(string EventStreamId, string GroupName, + string Status, float AverageItemsPerSecond, long TotalItemsProcessed, long CountSinceLastMeasurement, + long LastProcessedEventNumber, long LastKnownEventNumber, int ReadBufferCount, long LiveBufferCount, + int RetryBufferCount, int TotalInFlightMessages, int OutstandingMessagesCount, int ParkedMessageCount, + PersistentSubscriptionConfig? Config, IEnumerable Connections); + + internal record PersistentSubscriptionConfig(bool ResolveLinktos, ulong StartFrom, string StartPosition, + int MessageTimeoutMilliseconds, bool ExtraStatistics, int MaxRetryCount, int LiveBufferSize, int BufferSize, + int ReadBatchSize, int CheckPointAfterMilliseconds, int MinCheckPointCount, int MaxCheckPointCount, + int MaxSubscriberCount, string NamedConsumerStrategy); + + internal record PersistentSubscriptionConnectionInfoDto(string From, string Username, float AverageItemsPerSecond, + long TotalItems, long CountSinceLastMeasurement, int AvailableSlots, int InFlightMessages, string ConnectionName, + IEnumerable ExtraStatistics); + + internal record PersistentSubscriptionMeasurementInfoDto(string Key, long Value); +} diff --git a/src/Kurrent.Client/PersistentSubscriptions/PersistentSubscriptionMessage.cs b/src/Kurrent.Client/PersistentSubscriptions/PersistentSubscriptionMessage.cs new file mode 100644 index 000000000..7cd0d848d --- /dev/null +++ b/src/Kurrent.Client/PersistentSubscriptions/PersistentSubscriptionMessage.cs @@ -0,0 +1,33 @@ +namespace EventStore.Client { + /// + /// The base record of all stream messages. + /// + public abstract record PersistentSubscriptionMessage { + /// + /// A that represents a . + /// + /// The . + /// The number of times the has been retried. + public record Event(ResolvedEvent ResolvedEvent, int? RetryCount) : PersistentSubscriptionMessage; + + /// + /// A representing a stream that was not found. + /// + public record NotFound : PersistentSubscriptionMessage { + internal static readonly NotFound Instance = new(); + } + + /// + /// A indicating that the subscription is ready to send additional messages. + /// + /// The unique identifier of the subscription. + public record SubscriptionConfirmation(string SubscriptionId) : PersistentSubscriptionMessage; + + /// + /// A that could not be identified, usually indicating a lower client compatibility level than the server supports. + /// + public record Unknown : PersistentSubscriptionMessage { + internal static readonly Unknown Instance = new(); + } + } +} diff --git a/src/Kurrent.Client/PersistentSubscriptions/PersistentSubscriptionNakEventAction.cs b/src/Kurrent.Client/PersistentSubscriptions/PersistentSubscriptionNakEventAction.cs new file mode 100644 index 000000000..ce24b6552 --- /dev/null +++ b/src/Kurrent.Client/PersistentSubscriptions/PersistentSubscriptionNakEventAction.cs @@ -0,0 +1,31 @@ +namespace EventStore.Client { + /// + /// Actions to be taken by server in the case of a client NAK + /// + public enum PersistentSubscriptionNakEventAction { + /// + /// Client unknown on action. Let server decide + /// + Unknown = 0, + + /// + /// Park message do not resend. Put on poison queue + /// + Park = 1, + + /// + /// Explicitly retry the message. + /// + Retry = 2, + + /// + /// Skip this message do not resend do not put in poison queue + /// + Skip = 3, + + /// + /// Stop the subscription. + /// + Stop = 4 + } +} diff --git a/src/Kurrent.Client/PersistentSubscriptions/PersistentSubscriptionNotFoundException.cs b/src/Kurrent.Client/PersistentSubscriptions/PersistentSubscriptionNotFoundException.cs new file mode 100644 index 000000000..81e023ade --- /dev/null +++ b/src/Kurrent.Client/PersistentSubscriptions/PersistentSubscriptionNotFoundException.cs @@ -0,0 +1,29 @@ +using System; + +namespace EventStore.Client { + /// + /// The exception that is thrown when a persistent subscription is not found. + /// + public class PersistentSubscriptionNotFoundException : Exception { + /// + /// The stream name. + /// + public readonly string StreamName; + /// + /// The group name. + /// + public readonly string GroupName; + + /// + /// Constructs a new . + /// + /// + public PersistentSubscriptionNotFoundException(string streamName, string groupName, Exception? exception = null) + : base($"Subscription group '{groupName}' on stream '{streamName}' does not exist.", exception) { + if (streamName == null) throw new ArgumentNullException(nameof(streamName)); + if (groupName == null) throw new ArgumentNullException(nameof(groupName)); + StreamName = streamName; + GroupName = groupName; + } + } +} diff --git a/src/Kurrent.Client/PersistentSubscriptions/PersistentSubscriptionSettings.cs b/src/Kurrent.Client/PersistentSubscriptions/PersistentSubscriptionSettings.cs new file mode 100644 index 000000000..a36bfff87 --- /dev/null +++ b/src/Kurrent.Client/PersistentSubscriptions/PersistentSubscriptionSettings.cs @@ -0,0 +1,112 @@ +using System; + +namespace EventStore.Client { + /// + /// A class representing the settings of a persistent subscription. + /// + public sealed class PersistentSubscriptionSettings { + /// + /// Whether the should resolve linkTo events to their linked events. + /// + public readonly bool ResolveLinkTos; + + /// + /// Which event position in the stream or transaction file the subscription should start from. + /// + public readonly IPosition? StartFrom; + + /// + /// Whether to track latency statistics on this subscription. + /// + public readonly bool ExtraStatistics; + + /// + /// The amount of time after which to consider a message as timed out and retried. + /// + public readonly TimeSpan MessageTimeout; + + /// + /// The maximum number of retries (due to timeout) before a message is considered to be parked. + /// + public readonly int MaxRetryCount; + + /// + /// The size of the buffer (in-memory) listening to live messages as they happen before paging occurs. + /// + public readonly int LiveBufferSize; + + /// + /// The number of events read at a time when paging through history. + /// + public readonly int ReadBatchSize; + + /// + /// The number of events to cache when paging through history. + /// + public readonly int HistoryBufferSize; + + /// + /// The amount of time to try to checkpoint after. + /// + public readonly TimeSpan CheckPointAfter; + + /// + /// The minimum number of messages to process before a checkpoint may be written. + /// + public readonly int CheckPointLowerBound; + + /// + /// The maximum number of messages not checkpointed before forcing a checkpoint. + /// + public readonly int CheckPointUpperBound; + + /// + /// The maximum number of subscribers allowed. + /// + public readonly int MaxSubscriberCount; + + /// + /// The strategy to use for distributing events to client consumers. See for system supported strategies. + /// + public readonly string ConsumerStrategyName; + + /// + /// Constructs a new . + /// + /// + public PersistentSubscriptionSettings(bool resolveLinkTos = false, IPosition? startFrom = null, + bool extraStatistics = false, TimeSpan? messageTimeout = null, int maxRetryCount = 10, + int liveBufferSize = 500, int readBatchSize = 20, int historyBufferSize = 500, + TimeSpan? checkPointAfter = null, int checkPointLowerBound = 10, int checkPointUpperBound = 1000, + int maxSubscriberCount = 0, string consumerStrategyName = SystemConsumerStrategies.RoundRobin) { + messageTimeout ??= TimeSpan.FromSeconds(30); + checkPointAfter ??= TimeSpan.FromSeconds(2); + + if (messageTimeout.Value < TimeSpan.Zero || messageTimeout.Value.TotalMilliseconds > int.MaxValue) { + throw new ArgumentOutOfRangeException( + nameof(messageTimeout), + $"{nameof(messageTimeout)} must be greater than {TimeSpan.Zero} and less than or equal to {TimeSpan.FromMilliseconds(int.MaxValue)}"); + } + + if (checkPointAfter.Value < TimeSpan.Zero || checkPointAfter.Value.TotalMilliseconds > int.MaxValue) { + throw new ArgumentOutOfRangeException( + nameof(checkPointAfter), + $"{nameof(checkPointAfter)} must be greater than {TimeSpan.Zero} and less than or equal to {TimeSpan.FromMilliseconds(int.MaxValue)}"); + } + + ResolveLinkTos = resolveLinkTos; + StartFrom = startFrom; + ExtraStatistics = extraStatistics; + MessageTimeout = messageTimeout.Value; + MaxRetryCount = maxRetryCount; + LiveBufferSize = liveBufferSize; + ReadBatchSize = readBatchSize; + HistoryBufferSize = historyBufferSize; + CheckPointAfter = checkPointAfter.Value; + CheckPointLowerBound = checkPointLowerBound; + CheckPointUpperBound = checkPointUpperBound; + MaxSubscriberCount = maxSubscriberCount; + ConsumerStrategyName = consumerStrategyName; + } + } +} diff --git a/src/Kurrent.Client/PersistentSubscriptions/SystemConsumerStrategies.cs b/src/Kurrent.Client/PersistentSubscriptions/SystemConsumerStrategies.cs new file mode 100644 index 000000000..6183461ca --- /dev/null +++ b/src/Kurrent.Client/PersistentSubscriptions/SystemConsumerStrategies.cs @@ -0,0 +1,22 @@ +namespace EventStore.Client { + /// + /// System supported consumer strategies for use with persistent subscriptions. + /// + public static class SystemConsumerStrategies { + /// + /// Distributes events to a single client until it is full. Then round robin to the next client. + /// + public const string DispatchToSingle = nameof(DispatchToSingle); + + /// + /// Distribute events to each client in a round robin fashion. + /// + public const string RoundRobin = nameof(RoundRobin); + + /// + /// Distribute events of the same streamId to the same client until it disconnects on a best efforts basis. + /// Designed to be used with indexes such as the category projection. + /// + public const string Pinned = nameof(Pinned); + } +} diff --git a/src/Kurrent.Client/ProjectionManagement/KurrentProjectionManagementClient.Control.cs b/src/Kurrent.Client/ProjectionManagement/KurrentProjectionManagementClient.Control.cs new file mode 100644 index 000000000..ff6ba9619 --- /dev/null +++ b/src/Kurrent.Client/ProjectionManagement/KurrentProjectionManagementClient.Control.cs @@ -0,0 +1,102 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using EventStore.Client.Projections; + +namespace EventStore.Client { + public partial class KurrentProjectionManagementClient { + /// + /// Enables a projection. + /// + /// + /// + /// + /// + /// + public async Task EnableAsync(string name, TimeSpan? deadline = null, UserCredentials? userCredentials = null, + CancellationToken cancellationToken = default) { + var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); + using var call = new Projections.Projections.ProjectionsClient( + channelInfo.CallInvoker).EnableAsync(new EnableReq { + Options = new EnableReq.Types.Options { + Name = name + } + }, KurrentCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); + await call.ResponseAsync.ConfigureAwait(false); + } + + /// + /// Resets a projection. This will re-emit events. Streams that are written to from the projection will also be soft deleted. + /// + /// + /// + /// + /// + /// + public async Task ResetAsync(string name, TimeSpan? deadline = null, UserCredentials? userCredentials = null, + CancellationToken cancellationToken = default) { + var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); + using var call = new Projections.Projections.ProjectionsClient( + channelInfo.CallInvoker).ResetAsync(new ResetReq { + Options = new ResetReq.Types.Options { + Name = name, + WriteCheckpoint = true + } + }, KurrentCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); + await call.ResponseAsync.ConfigureAwait(false); + } + + /// + /// Aborts a projection. Does not save the projection's checkpoint. + /// + /// + /// + /// + /// + /// + public Task AbortAsync(string name, TimeSpan? deadline = null, UserCredentials? userCredentials = null, + CancellationToken cancellationToken = default) => + DisableInternalAsync(name, false, deadline, userCredentials, cancellationToken); + + /// + /// Disables a projection. Saves the projection's checkpoint. + /// + /// + /// + /// + /// + /// + public Task DisableAsync(string name, TimeSpan? deadline = null, UserCredentials? userCredentials = null, + CancellationToken cancellationToken = default) => + DisableInternalAsync(name, true, deadline, userCredentials, cancellationToken); + + /// + /// Restarts the projection subsystem. + /// + /// + /// + /// + /// + public async Task RestartSubsystemAsync(TimeSpan? deadline = null, UserCredentials? userCredentials = null, + CancellationToken cancellationToken = default) { + var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); + using var call = new Projections.Projections.ProjectionsClient( + channelInfo.CallInvoker).RestartSubsystemAsync(new Empty(), + KurrentCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); + await call.ResponseAsync.ConfigureAwait(false); + } + + private async Task DisableInternalAsync(string name, bool writeCheckpoint, TimeSpan? deadline, + UserCredentials? userCredentials, CancellationToken cancellationToken) { + var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); + using var call = new Projections.Projections.ProjectionsClient( + channelInfo.CallInvoker).DisableAsync(new DisableReq { + Options = new DisableReq.Types.Options { + Name = name, + WriteCheckpoint = writeCheckpoint + } + }, KurrentCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); + await call.ResponseAsync.ConfigureAwait(false); + } + } +} diff --git a/src/Kurrent.Client/ProjectionManagement/KurrentProjectionManagementClient.Create.cs b/src/Kurrent.Client/ProjectionManagement/KurrentProjectionManagementClient.Create.cs new file mode 100644 index 000000000..cae9b400e --- /dev/null +++ b/src/Kurrent.Client/ProjectionManagement/KurrentProjectionManagementClient.Create.cs @@ -0,0 +1,80 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using EventStore.Client.Projections; + +namespace EventStore.Client { + public partial class KurrentProjectionManagementClient { + /// + /// Creates a one-time projection. + /// + /// + /// + /// + /// + /// + public async Task CreateOneTimeAsync(string query, TimeSpan? deadline = null, + UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) { + var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); + using var call = new Projections.Projections.ProjectionsClient( + channelInfo.CallInvoker).CreateAsync(new CreateReq { + Options = new CreateReq.Types.Options { + OneTime = new Empty(), + Query = query + } + }, KurrentCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); + await call.ResponseAsync.ConfigureAwait(false); + } + + /// + /// Creates a continuous projection. + /// + /// + /// + /// + /// + /// + /// + /// + public async Task CreateContinuousAsync(string name, string query, bool trackEmittedStreams = false, + TimeSpan? deadline = null, UserCredentials? userCredentials = null, + CancellationToken cancellationToken = default) { + var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); + using var call = new Projections.Projections.ProjectionsClient( + channelInfo.CallInvoker).CreateAsync(new CreateReq { + Options = new CreateReq.Types.Options { + Continuous = new CreateReq.Types.Options.Types.Continuous { + Name = name, + TrackEmittedStreams = trackEmittedStreams + }, + Query = query + } + }, KurrentCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); + await call.ResponseAsync.ConfigureAwait(false); + } + + /// + /// Creates a transient projection. + /// + /// + /// + /// + /// + /// + /// + public async Task CreateTransientAsync(string name, string query, TimeSpan? deadline = null, + UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) { + var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); + using var call = new Projections.Projections.ProjectionsClient( + channelInfo.CallInvoker).CreateAsync(new CreateReq { + Options = new CreateReq.Types.Options { + Transient = new CreateReq.Types.Options.Types.Transient { + Name = name + }, + Query = query + } + }, KurrentCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); + await call.ResponseAsync.ConfigureAwait(false); + } + } +} diff --git a/src/Kurrent.Client/ProjectionManagement/KurrentProjectionManagementClient.State.cs b/src/Kurrent.Client/ProjectionManagement/KurrentProjectionManagementClient.State.cs new file mode 100644 index 000000000..ca24d6ef9 --- /dev/null +++ b/src/Kurrent.Client/ProjectionManagement/KurrentProjectionManagementClient.State.cs @@ -0,0 +1,205 @@ +using System; +using System.IO; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using EventStore.Client.Projections; +using Google.Protobuf.WellKnownTypes; +using Type = System.Type; + +namespace EventStore.Client { + public partial class KurrentProjectionManagementClient { + static readonly JsonSerializerOptions DefaultJsonSerializerOptions = new JsonSerializerOptions(); + + /// + /// Gets the result of a projection as an untyped document. + /// + /// + /// + /// + /// + /// + /// + public async Task GetResultAsync(string name, string? partition = null, + TimeSpan? deadline = null, UserCredentials? userCredentials = null, + CancellationToken cancellationToken = default) { + var value = await GetResultInternalAsync(name, partition, deadline, userCredentials, cancellationToken) + .ConfigureAwait(false); + +#if NET + await using var stream = new MemoryStream(); +#else + using var stream = new MemoryStream(); +#endif + await using var writer = new Utf8JsonWriter(stream); + var serializer = new ValueSerializer(); + serializer.Write(writer, value, DefaultJsonSerializerOptions); + await writer.FlushAsync(cancellationToken).ConfigureAwait(false); + stream.Position = 0; + + return JsonDocument.Parse(stream); + } + + /// + /// Gets the result of a projection. + /// + /// + /// + /// + /// + /// + /// + /// + /// + public async Task GetResultAsync(string name, string? partition = null, + JsonSerializerOptions? serializerOptions = null, + TimeSpan? deadline = null, UserCredentials? userCredentials = null, + CancellationToken cancellationToken = default) { + var value = await GetResultInternalAsync(name, partition, deadline, userCredentials, cancellationToken) + .ConfigureAwait(false); +#if NET + await using var stream = new MemoryStream(); +#else + using var stream = new MemoryStream(); +#endif + await using var writer = new Utf8JsonWriter(stream); + var serializer = new ValueSerializer(); + serializer.Write(writer, value, DefaultJsonSerializerOptions); + await writer.FlushAsync(cancellationToken).ConfigureAwait(false); + stream.Position = 0; + + return JsonSerializer.Deserialize(stream.ToArray(), serializerOptions)!; + } + + private async ValueTask GetResultInternalAsync(string name, string? partition, + TimeSpan? deadline, UserCredentials? userCredentials, CancellationToken cancellationToken) { + var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); + using var call = new Projections.Projections.ProjectionsClient( + channelInfo.CallInvoker).ResultAsync(new ResultReq { + Options = new ResultReq.Types.Options { + Name = name, + Partition = partition ?? string.Empty + } + }, KurrentCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); + + var response = await call.ResponseAsync.ConfigureAwait(false); + return response.Result; + } + + /// + /// Gets the state of a projection as an untyped document. + /// + /// + /// + /// + /// + /// + /// + public async Task GetStateAsync(string name, string? partition = null, + TimeSpan? deadline = null, UserCredentials? userCredentials = null, + CancellationToken cancellationToken = default) { + var value = await GetStateInternalAsync(name, partition, deadline, userCredentials, cancellationToken) + .ConfigureAwait(false); + +#if NET + await using var stream = new MemoryStream(); +#else + using var stream = new MemoryStream(); +#endif + await using var writer = new Utf8JsonWriter(stream); + var serializer = new ValueSerializer(); + serializer.Write(writer, value, DefaultJsonSerializerOptions); + stream.Position = 0; + await writer.FlushAsync(cancellationToken).ConfigureAwait(false); + + return JsonDocument.Parse(stream); + } + + /// + /// Gets the state of a projection. + /// + /// + /// + /// + /// + /// + /// + /// + /// + public async Task GetStateAsync(string name, string? partition = null, + JsonSerializerOptions? serializerOptions = null, TimeSpan? deadline = null, + UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) { + var value = await GetStateInternalAsync(name, partition, deadline, userCredentials, cancellationToken) + .ConfigureAwait(false); + +#if NET + await using var stream = new MemoryStream(); +#else + using var stream = new MemoryStream(); +#endif + await using var writer = new Utf8JsonWriter(stream); + var serializer = new ValueSerializer(); + serializer.Write(writer, value, DefaultJsonSerializerOptions); + await writer.FlushAsync(cancellationToken).ConfigureAwait(false); + stream.Position = 0; + + return JsonSerializer.Deserialize(stream.ToArray(), serializerOptions)!; + } + + private async ValueTask GetStateInternalAsync(string name, string? partition, TimeSpan? deadline, + UserCredentials? userCredentials, CancellationToken cancellationToken) { + var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); + using var call = new Projections.Projections.ProjectionsClient( + channelInfo.CallInvoker).StateAsync(new StateReq { + Options = new StateReq.Types.Options { + Name = name, + Partition = partition ?? string.Empty + } + }, KurrentCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); + + var response = await call.ResponseAsync.ConfigureAwait(false); + return response.State; + } + + private class ValueSerializer : System.Text.Json.Serialization.JsonConverter { + public override Value Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => + throw new NotSupportedException(); + + public override void Write(Utf8JsonWriter writer, Value value, JsonSerializerOptions options) { + switch (value.KindCase) { + case Value.KindOneofCase.None: + break; + case Value.KindOneofCase.BoolValue: + writer.WriteBooleanValue(value.BoolValue); + break; + case Value.KindOneofCase.NullValue: + writer.WriteNullValue(); + break; + case Value.KindOneofCase.NumberValue: + writer.WriteNumberValue(value.NumberValue); + break; + case Value.KindOneofCase.StringValue: + writer.WriteStringValue(value.StringValue); + break; + case Value.KindOneofCase.ListValue: + writer.WriteStartArray(); + foreach (var item in value.ListValue.Values) { + Write(writer, item, options); + } + + writer.WriteEndArray(); + break; + case Value.KindOneofCase.StructValue: + writer.WriteStartObject(); + foreach (var map in value.StructValue.Fields) { + writer.WritePropertyName(map.Key); + Write(writer, map.Value, options); + } + + writer.WriteEndObject(); + break; + } + } + } + } +} diff --git a/src/Kurrent.Client/ProjectionManagement/KurrentProjectionManagementClient.Statistics.cs b/src/Kurrent.Client/ProjectionManagement/KurrentProjectionManagementClient.Statistics.cs new file mode 100644 index 000000000..425831165 --- /dev/null +++ b/src/Kurrent.Client/ProjectionManagement/KurrentProjectionManagementClient.Statistics.cs @@ -0,0 +1,104 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using EventStore.Client.Projections; +using Grpc.Core; + +namespace EventStore.Client { + public partial class KurrentProjectionManagementClient { + /// + /// List the of all one-time projections. + /// + /// + /// + /// + /// + public IAsyncEnumerable ListOneTimeAsync(TimeSpan? deadline = null, + UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) => + ListInternalAsync(new StatisticsReq.Types.Options { + OneTime = new Empty() + }, + KurrentCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken), + cancellationToken); + + /// + /// List the of all continuous projections. + /// + /// + /// + /// + /// + public IAsyncEnumerable ListContinuousAsync(TimeSpan? deadline = null, + UserCredentials? userCredentials = null, + CancellationToken cancellationToken = default) => + ListInternalAsync(new StatisticsReq.Types.Options { + Continuous = new Empty() + }, + KurrentCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken), + cancellationToken); + + /// + /// Gets the status of a projection. + /// + /// + /// + /// + /// + /// + public Task GetStatusAsync(string name, + TimeSpan? deadline = null, UserCredentials? userCredentials = null, + CancellationToken cancellationToken = default) => ListInternalAsync(new StatisticsReq.Types.Options { + Name = name + }, + KurrentCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken), + cancellationToken) + .FirstOrDefaultAsync(cancellationToken).AsTask(); + + /// + /// List the of all projections. + /// + /// + /// + /// + /// + public IAsyncEnumerable ListAllAsync(TimeSpan? deadline = null, + UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) => + ListInternalAsync(new StatisticsReq.Types.Options { + All = new Empty() + }, + KurrentCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken), + cancellationToken); + + private async IAsyncEnumerable ListInternalAsync(StatisticsReq.Types.Options options, + CallOptions callOptions, + [EnumeratorCancellation] CancellationToken cancellationToken) { + var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); + using var call = new Projections.Projections.ProjectionsClient( + channelInfo.CallInvoker).Statistics(new StatisticsReq { + Options = options + }, callOptions); + + await foreach (var projectionDetails in call.ResponseStream + .ReadAllAsync(cancellationToken) + .Select(ConvertToProjectionDetails) + .WithCancellation(cancellationToken) + .ConfigureAwait(false)) { + yield return projectionDetails; + } + } + + private static ProjectionDetails ConvertToProjectionDetails(StatisticsResp response) { + var details = response.Details; + + return new ProjectionDetails(details.CoreProcessingTime, details.Version, details.Epoch, + details.EffectiveName, details.WritesInProgress, details.ReadsInProgress, details.PartitionsCached, + details.Status, details.StateReason, details.Name, details.Mode, details.Position, details.Progress, + details.LastCheckpoint, details.EventsProcessedAfterRestart, details.CheckpointStatus, + details.BufferedEvents, details.WritePendingEventsBeforeCheckpoint, + details.WritePendingEventsAfterCheckpoint); + } + } +} diff --git a/src/Kurrent.Client/ProjectionManagement/KurrentProjectionManagementClient.Update.cs b/src/Kurrent.Client/ProjectionManagement/KurrentProjectionManagementClient.Update.cs new file mode 100644 index 000000000..368ff3be4 --- /dev/null +++ b/src/Kurrent.Client/ProjectionManagement/KurrentProjectionManagementClient.Update.cs @@ -0,0 +1,40 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using EventStore.Client.Projections; + +namespace EventStore.Client { + public partial class KurrentProjectionManagementClient { + /// + /// Updates a projection. + /// + /// + /// + /// + /// + /// + /// + /// + public async Task UpdateAsync(string name, string query, bool? emitEnabled = null, + TimeSpan? deadline = null, UserCredentials? userCredentials = null, + CancellationToken cancellationToken = default) { + var options = new UpdateReq.Types.Options { + Name = name, + Query = query + }; + if (emitEnabled.HasValue) { + options.EmitEnabled = emitEnabled.Value; + } else { + options.NoEmitOptions = new Empty(); + } + + var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); + using var call = new Projections.Projections.ProjectionsClient( + channelInfo.CallInvoker).UpdateAsync(new UpdateReq { + Options = options + }, KurrentCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); + + await call.ResponseAsync.ConfigureAwait(false); + } + } +} diff --git a/src/Kurrent.Client/ProjectionManagement/KurrentProjectionManagementClient.cs b/src/Kurrent.Client/ProjectionManagement/KurrentProjectionManagementClient.cs new file mode 100644 index 000000000..12e1abf18 --- /dev/null +++ b/src/Kurrent.Client/ProjectionManagement/KurrentProjectionManagementClient.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using Grpc.Core; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; + +namespace EventStore.Client { + /// + ///The client used to manage projections on the KurrentDB. + /// + public sealed partial class KurrentProjectionManagementClient : KurrentClientBase { + private readonly ILogger _log; + + /// + /// Constructs a new . This method is not intended to be called directly from your code. + /// + /// + public KurrentProjectionManagementClient(IOptions options) : this(options.Value) { + } + + /// + /// Constructs a new . + /// + /// + public KurrentProjectionManagementClient(KurrentClientSettings? settings) : base(settings, + new Dictionary>()) { + _log = settings?.LoggerFactory?.CreateLogger() ?? + new NullLogger(); + } + } +} diff --git a/src/Kurrent.Client/ProjectionManagement/KurrentProjectionManagementClientCollectionExtensions.cs b/src/Kurrent.Client/ProjectionManagement/KurrentProjectionManagementClientCollectionExtensions.cs new file mode 100644 index 000000000..5fe3932b7 --- /dev/null +++ b/src/Kurrent.Client/ProjectionManagement/KurrentProjectionManagementClientCollectionExtensions.cs @@ -0,0 +1,74 @@ +// ReSharper disable CheckNamespace + +using System; +using System.Net.Http; +using EventStore.Client; +using Grpc.Core.Interceptors; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Logging; + +namespace Microsoft.Extensions.DependencyInjection { + /// + /// A set of extension methods for which provide support for an . + /// + public static class KurrentProjectionManagementClientCollectionExtensions { + /// + /// Adds an to the . + /// + /// + /// + /// + /// + /// + public static IServiceCollection AddKurrentProjectionManagementClient(this IServiceCollection services, + Uri address, + Func? createHttpMessageHandler = null) + => services.AddKurrentProjectionManagementClient(options => { + options.ConnectivitySettings.Address = address; + options.CreateHttpMessageHandler = createHttpMessageHandler; + }); + + /// + /// Adds an to the . + /// + /// + /// + /// + /// + public static IServiceCollection AddKurrentProjectionManagementClient(this IServiceCollection services, + Action? configureSettings = null) => + services.AddKurrentProjectionManagementClient(new KurrentClientSettings(), configureSettings); + + /// + /// Adds an to the . + /// + /// + /// + /// + /// + /// + public static IServiceCollection AddKurrentProjectionManagementClient(this IServiceCollection services, + string connectionString, Action? configureSettings = null) => + services.AddKurrentProjectionManagementClient(KurrentClientSettings.Create(connectionString), + configureSettings); + + private static IServiceCollection AddKurrentProjectionManagementClient(this IServiceCollection services, + KurrentClientSettings settings, Action? configureSettings) { + if (services == null) { + throw new ArgumentNullException(nameof(services)); + } + + configureSettings?.Invoke(settings); + + services.TryAddSingleton(provider => { + settings.LoggerFactory ??= provider.GetService(); + settings.Interceptors ??= provider.GetServices(); + + return new KurrentProjectionManagementClient(settings); + }); + + return services; + } + } +} +// ReSharper restore CheckNamespace diff --git a/src/Kurrent.Client/ProjectionManagement/ProjectionDetails.cs b/src/Kurrent.Client/ProjectionManagement/ProjectionDetails.cs new file mode 100644 index 000000000..c604362b1 --- /dev/null +++ b/src/Kurrent.Client/ProjectionManagement/ProjectionDetails.cs @@ -0,0 +1,164 @@ +namespace EventStore.Client { + /// + /// Provides the details for a projection. + /// + public sealed class ProjectionDetails { + /// + /// The CoreProcessingTime + /// + public readonly long CoreProcessingTime; + + /// + /// The projection version + /// + public readonly long Version; + + /// + /// The Epoch + /// + public readonly long Epoch; + + /// + /// The projection EffectiveName + /// + public readonly string EffectiveName; + + /// + /// The projection WritesInProgress + /// + public readonly int WritesInProgress; + + /// + /// The projection ReadsInProgress + /// + public readonly int ReadsInProgress; + + /// + /// The projection PartitionsCached + /// + public readonly int PartitionsCached; + + /// + /// The projection Status + /// + public readonly string Status; + + /// + /// The projection StateReason + /// + public readonly string StateReason; + + /// + /// The projection Name + /// + public readonly string Name; + + /// + /// The projection Mode + /// + public readonly string Mode; + + /// + /// The projection Position + /// + public readonly string Position; + + /// + /// The projection Progress + /// + public readonly float Progress; + + /// + /// LastCheckpoint + /// + public readonly string LastCheckpoint; + + /// + /// The projection EventsProcessedAfterRestart + /// + public readonly long EventsProcessedAfterRestart; + + /// + /// The projection CheckpointStatus + /// + public readonly string CheckpointStatus; + + /// + /// The projection BufferedEvents + /// + public readonly long BufferedEvents; + + /// + /// The projection WritePendingEventsBeforeCheckpoint + /// + public readonly int WritePendingEventsBeforeCheckpoint; + + /// + /// The projection WritePendingEventsAfterCheckpoint + /// + public readonly int WritePendingEventsAfterCheckpoint; + + /// + /// create a new class. + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public ProjectionDetails( + long coreProcessingTime, + long version, + long epoch, + string effectiveName, + int writesInProgress, + int readsInProgress, + int partitionsCached, + string status, + string stateReason, + string name, + string mode, + string position, + float progress, + string lastCheckpoint, + long eventsProcessedAfterRestart, + string checkpointStatus, + long bufferedEvents, + int writePendingEventsBeforeCheckpoint, + int writePendingEventsAfterCheckpoint) { + CoreProcessingTime = coreProcessingTime; + Version = version; + Epoch = epoch; + EffectiveName = effectiveName; + WritesInProgress = writesInProgress; + ReadsInProgress = readsInProgress; + PartitionsCached = partitionsCached; + Status = status; + StateReason = stateReason; + Name = name; + Mode = mode; + Position = position; + Progress = progress; + LastCheckpoint = lastCheckpoint; + EventsProcessedAfterRestart = eventsProcessedAfterRestart; + CheckpointStatus = checkpointStatus; + BufferedEvents = bufferedEvents; + WritePendingEventsBeforeCheckpoint = writePendingEventsBeforeCheckpoint; + WritePendingEventsAfterCheckpoint = writePendingEventsAfterCheckpoint; + } + } +} diff --git a/src/Kurrent.Client/Streams/ConditionalWriteResult.cs b/src/Kurrent.Client/Streams/ConditionalWriteResult.cs new file mode 100644 index 000000000..f51ef577b --- /dev/null +++ b/src/Kurrent.Client/Streams/ConditionalWriteResult.cs @@ -0,0 +1,87 @@ +using System; + +namespace EventStore.Client { + /// + /// A structure that represents the result of a conditional write. + /// + public readonly struct ConditionalWriteResult : IEquatable { + /// + /// Indicates that the stream the operation is targeting was deleted. + /// + public static readonly ConditionalWriteResult StreamDeleted = + new ConditionalWriteResult(StreamRevision.None, Position.End, ConditionalWriteStatus.StreamDeleted); + + /// + /// The correct expected version to use when writing to the stream next. + /// + public long NextExpectedVersion { get; } + + /// + /// The of the write in the transaction file. + /// + public Position LogPosition { get; } + + /// + /// The . + /// + public ConditionalWriteStatus Status { get; } + + /// + /// The correct to use when writing to the stream next. + /// + public StreamRevision NextExpectedStreamRevision { get; } + + private ConditionalWriteResult(StreamRevision nextExpectedStreamRevision, Position logPosition, + ConditionalWriteStatus status = ConditionalWriteStatus.Succeeded) { + NextExpectedStreamRevision = nextExpectedStreamRevision; + NextExpectedVersion = nextExpectedStreamRevision.ToInt64(); + LogPosition = logPosition; + Status = status; + } + + internal static ConditionalWriteResult FromWriteResult(IWriteResult writeResult) + => writeResult switch { + WrongExpectedVersionResult wrongExpectedVersion => + new ConditionalWriteResult(wrongExpectedVersion.NextExpectedStreamRevision, Position.End, + ConditionalWriteStatus.VersionMismatch), + _ => new ConditionalWriteResult(writeResult.NextExpectedStreamRevision, writeResult.LogPosition) + }; + + internal static ConditionalWriteResult FromWrongExpectedVersion(WrongExpectedVersionException ex) + => new ConditionalWriteResult(ex.ExpectedStreamRevision, Position.End, + ConditionalWriteStatus.VersionMismatch); + + /// + public bool Equals(ConditionalWriteResult other) => + NextExpectedStreamRevision == other.NextExpectedStreamRevision && + LogPosition.Equals(other.LogPosition) && + Status == other.Status; + + /// + public override bool Equals(object? obj) => obj is ConditionalWriteResult other && Equals(other); + + /// + public override int GetHashCode() => + HashCode.Hash.Combine(NextExpectedVersion).Combine(LogPosition).Combine(Status); + + /// + /// Compares left and right for equality. + /// + /// + /// + /// True if left is equal to right. + public static bool operator ==(ConditionalWriteResult left, ConditionalWriteResult right) => left.Equals(right); + + /// + /// Compares left and right for inequality. + /// + /// + /// + /// True if left is not equal to right. + public static bool operator !=(ConditionalWriteResult left, ConditionalWriteResult right) => + !left.Equals(right); + + /// + public override string ToString() => $"{Status}:{NextExpectedVersion}:{LogPosition}"; + } +} diff --git a/src/Kurrent.Client/Streams/ConditionalWriteStatus.cs b/src/Kurrent.Client/Streams/ConditionalWriteStatus.cs new file mode 100644 index 000000000..93bf82fe7 --- /dev/null +++ b/src/Kurrent.Client/Streams/ConditionalWriteStatus.cs @@ -0,0 +1,21 @@ +namespace EventStore.Client { + /// + /// The reason why a conditional write fails + /// + public enum ConditionalWriteStatus { + /// + /// The write operation succeeded + /// + Succeeded = 0, + + /// + /// The expected version does not match actual stream version + /// + VersionMismatch = 1, + + /// + /// The stream has been deleted + /// + StreamDeleted = 2 + } +} diff --git a/src/Kurrent.Client/Streams/DeadLine.cs b/src/Kurrent.Client/Streams/DeadLine.cs new file mode 100644 index 000000000..b4ba29b49 --- /dev/null +++ b/src/Kurrent.Client/Streams/DeadLine.cs @@ -0,0 +1,12 @@ +using System; + +namespace EventStore.Client { +#pragma warning disable CS1591 + public static class DeadLine { +#pragma warning restore CS1591 + /// + /// Represents no deadline (i.e., wait infinitely) + /// + public static TimeSpan? None = null; + } +} diff --git a/src/Kurrent.Client/Streams/DeleteResult.cs b/src/Kurrent.Client/Streams/DeleteResult.cs new file mode 100644 index 000000000..ea5195b1d --- /dev/null +++ b/src/Kurrent.Client/Streams/DeleteResult.cs @@ -0,0 +1,46 @@ +using System; + +namespace EventStore.Client { + /// + /// A structure that represents the result of a delete operation. + /// + public readonly struct DeleteResult : IEquatable { + /// + public bool Equals(DeleteResult other) => LogPosition.Equals(other.LogPosition); + + /// + public override bool Equals(object? obj) => obj is DeleteResult other && Equals(other); + + /// + public override int GetHashCode() => LogPosition.GetHashCode(); + + /// + /// Compares left and right for equality. + /// + /// + /// + /// True if left is equal to right. + public static bool operator ==(DeleteResult left, DeleteResult right) => left.Equals(right); + + /// + /// Compares left and right for inequality. + /// + /// + /// + /// True if left is not equal to right. + public static bool operator !=(DeleteResult left, DeleteResult right) => !left.Equals(right); + + /// + /// The of the delete in the transaction file. + /// + public readonly Position LogPosition; + + /// + /// Constructs a new . + /// + /// + public DeleteResult(Position logPosition) { + LogPosition = logPosition; + } + } +} diff --git a/src/Kurrent.Client/Streams/Direction.cs b/src/Kurrent.Client/Streams/Direction.cs new file mode 100644 index 000000000..40e9489da --- /dev/null +++ b/src/Kurrent.Client/Streams/Direction.cs @@ -0,0 +1,16 @@ +namespace EventStore.Client { + /// + /// An enumeration that indicates the direction of the read operation. + /// + public enum Direction { + /// + /// Read backwards. + /// + Backwards, + + /// + /// Read forwards. + /// + Forwards + } +} diff --git a/src/Kurrent.Client/Streams/IWriteResult.cs b/src/Kurrent.Client/Streams/IWriteResult.cs new file mode 100644 index 000000000..8fe9d530c --- /dev/null +++ b/src/Kurrent.Client/Streams/IWriteResult.cs @@ -0,0 +1,23 @@ +using System; + +namespace EventStore.Client { + /// + /// An interface representing the result of a write operation. + /// + public interface IWriteResult { + /// + /// The version the stream is currently at. + /// + [Obsolete("Please use NextExpectedStreamRevision instead. This property will be removed in a future version.", + true)] + long NextExpectedVersion { get; } + /// + /// The of the in the transaction file. + /// + Position LogPosition { get; } + /// + /// The the stream is currently at. + /// + StreamRevision NextExpectedStreamRevision { get; } + } +} diff --git a/src/Kurrent.Client/Streams/InvalidTransactionException.cs b/src/Kurrent.Client/Streams/InvalidTransactionException.cs new file mode 100644 index 000000000..f71505671 --- /dev/null +++ b/src/Kurrent.Client/Streams/InvalidTransactionException.cs @@ -0,0 +1,31 @@ +using System; +using System.Runtime.Serialization; + +namespace EventStore.Client; + +/// +/// Exception thrown if there is an attempt to operate inside a +/// transaction which does not exist. +/// +public class InvalidTransactionException : Exception { + /// + /// Constructs a new . + /// + public InvalidTransactionException() { } + + /// + /// Constructs a new . + /// + public InvalidTransactionException(string message) : base(message) { } + + /// + /// Constructs a new . + /// + public InvalidTransactionException(string message, Exception innerException) : base(message, innerException) { } + + /// + /// Constructs a new . + /// + [Obsolete("Obsolete")] + protected InvalidTransactionException(SerializationInfo info, StreamingContext context) : base(info, context) { } +} diff --git a/src/Kurrent.Client/Streams/KurrentClient.Append.cs b/src/Kurrent.Client/Streams/KurrentClient.Append.cs new file mode 100644 index 000000000..39d6f3066 --- /dev/null +++ b/src/Kurrent.Client/Streams/KurrentClient.Append.cs @@ -0,0 +1,430 @@ +using System.Collections.Concurrent; +using System.Diagnostics; +using System.Threading.Channels; +using Google.Protobuf; +using EventStore.Client.Streams; +using Grpc.Core; +using Microsoft.Extensions.Logging; +using EventStore.Client.Diagnostics; +using Kurrent.Diagnostics; +using Kurrent.Diagnostics.Telemetry; +using Kurrent.Diagnostics.Tracing; +using static EventStore.Client.Streams.AppendResp.Types.WrongExpectedVersion; +using static EventStore.Client.Streams.Streams; + +namespace EventStore.Client { + public partial class KurrentClient { + /// + /// Appends events asynchronously to a stream. + /// + /// The name of the stream to append events to. + /// The expected of the stream to append to. + /// An to append to the stream. + /// An to configure the operation's options. + /// + /// The for the operation. + /// The optional . + /// + public async Task AppendToStreamAsync( + string streamName, + StreamRevision expectedRevision, + IEnumerable eventData, + Action? configureOperationOptions = null, + TimeSpan? deadline = null, + UserCredentials? userCredentials = null, + CancellationToken cancellationToken = default + ) { + var options = Settings.OperationOptions.Clone(); + configureOperationOptions?.Invoke(options); + + _log.LogDebug("Append to stream - {streamName}@{expectedRevision}.", streamName, expectedRevision); + + var task = userCredentials is null && await BatchAppender.IsUsable().ConfigureAwait(false) + ? BatchAppender.Append(streamName, expectedRevision, eventData, deadline, cancellationToken) + : AppendToStreamInternal( + await GetChannelInfo(cancellationToken).ConfigureAwait(false), + new AppendReq { + Options = new() { + StreamIdentifier = streamName, + Revision = expectedRevision + } + }, + eventData, + options, + deadline, + userCredentials, + cancellationToken + ); + + return (await task.ConfigureAwait(false)).OptionallyThrowWrongExpectedVersionException(options); + } + + /// + /// Appends events asynchronously to a stream. + /// + /// The name of the stream to append events to. + /// The expected of the stream to append to. + /// An to append to the stream. + /// An to configure the operation's options. + /// + /// The for the operation. + /// The optional . + /// + public async Task AppendToStreamAsync( + string streamName, + StreamState expectedState, + IEnumerable eventData, + Action? configureOperationOptions = null, + TimeSpan? deadline = null, + UserCredentials? userCredentials = null, + CancellationToken cancellationToken = default + ) { + var operationOptions = Settings.OperationOptions.Clone(); + configureOperationOptions?.Invoke(operationOptions); + + _log.LogDebug("Append to stream - {streamName}@{expectedState}.", streamName, expectedState); + + var task = + userCredentials == null && await BatchAppender.IsUsable().ConfigureAwait(false) + ? BatchAppender.Append(streamName, expectedState, eventData, deadline, cancellationToken) + : AppendToStreamInternal( + await GetChannelInfo(cancellationToken).ConfigureAwait(false), + new AppendReq { + Options = new() { + StreamIdentifier = streamName + } + }.WithAnyStreamRevision(expectedState), + eventData, + operationOptions, + deadline, + userCredentials, + cancellationToken + ); + + return (await task.ConfigureAwait(false)).OptionallyThrowWrongExpectedVersionException(operationOptions); + } + + ValueTask AppendToStreamInternal( + ChannelInfo channelInfo, + AppendReq header, + IEnumerable eventData, + KurrentClientOperationOptions operationOptions, + TimeSpan? deadline, + UserCredentials? userCredentials, + CancellationToken cancellationToken + ) { + var tags = new ActivityTagsCollection() + .WithRequiredTag(TelemetryTags.Kurrent.Stream, header.Options.StreamIdentifier.StreamName.ToStringUtf8()) + .WithGrpcChannelServerTags(channelInfo) + .WithClientSettingsServerTags(Settings) + .WithOptionalTag(TelemetryTags.Database.User, userCredentials?.Username ?? Settings.DefaultCredentials?.Username); + + return KurrentClientDiagnostics.ActivitySource.TraceClientOperation(Operation, TracingConstants.Operations.Append, tags); + + async ValueTask Operation() { + using var call = new StreamsClient(channelInfo.CallInvoker) + .Append(KurrentCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); + + await call.RequestStream + .WriteAsync(header) + .ConfigureAwait(false); + + foreach (var e in eventData) { + var appendReq = new AppendReq { + ProposedMessage = new() { + Id = e.EventId.ToDto(), + Data = ByteString.CopyFrom(e.Data.Span), + CustomMetadata = ByteString.CopyFrom(e.Metadata.InjectTracingContext(Activity.Current)), + Metadata = { + { Constants.Metadata.Type, e.Type }, + { Constants.Metadata.ContentType, e.ContentType } + } + } + }; + + await call.RequestStream.WriteAsync(appendReq).ConfigureAwait(false); + } + + await call.RequestStream.CompleteAsync().ConfigureAwait(false); + + var response = await call.ResponseAsync.ConfigureAwait(false); + + if (response.Success is not null) + return HandleSuccessAppend(response, header); + + if (response.WrongExpectedVersion is null) + throw new InvalidOperationException("The operation completed with an unexpected result."); + + return HandleWrongExpectedRevision(response, header, operationOptions); + } + } + + IWriteResult HandleSuccessAppend(AppendResp response, AppendReq header) { + var currentRevision = response.Success.CurrentRevisionOptionCase == AppendResp.Types.Success.CurrentRevisionOptionOneofCase.NoStream + ? StreamRevision.None + : new StreamRevision(response.Success.CurrentRevision); + + var position = response.Success.PositionOptionCase == AppendResp.Types.Success.PositionOptionOneofCase.Position + ? new Position(response.Success.Position.CommitPosition, response.Success.Position.PreparePosition) + : default; + + _log.LogDebug( + "Append to stream succeeded - {streamName}@{logPosition}/{nextExpectedVersion}.", + header.Options.StreamIdentifier, + position, + currentRevision + ); + + return new SuccessResult(currentRevision, position); + } + + IWriteResult HandleWrongExpectedRevision( + AppendResp response, AppendReq header, KurrentClientOperationOptions operationOptions + ) { + var actualStreamRevision = response.WrongExpectedVersion.CurrentRevisionOptionCase == CurrentRevisionOptionOneofCase.CurrentRevision + ? new StreamRevision(response.WrongExpectedVersion.CurrentRevision) + : StreamRevision.None; + + _log.LogDebug( + "Append to stream failed with Wrong Expected Version - {streamName}/{expectedRevision}/{currentRevision}", + header.Options.StreamIdentifier, + new StreamRevision(header.Options.Revision), + actualStreamRevision + ); + + if (operationOptions.ThrowOnAppendFailure) { + if (response.WrongExpectedVersion.ExpectedRevisionOptionCase == ExpectedRevisionOptionOneofCase.ExpectedRevision) { + throw new WrongExpectedVersionException( + header.Options.StreamIdentifier!, + new StreamRevision(response.WrongExpectedVersion.ExpectedRevision), + actualStreamRevision + ); + } + + var expectedStreamState = response.WrongExpectedVersion.ExpectedRevisionOptionCase switch { + ExpectedRevisionOptionOneofCase.ExpectedAny => StreamState.Any, + ExpectedRevisionOptionOneofCase.ExpectedNoStream => StreamState.NoStream, + ExpectedRevisionOptionOneofCase.ExpectedStreamExists => StreamState.StreamExists, + _ => StreamState.Any + }; + + throw new WrongExpectedVersionException( + header.Options.StreamIdentifier!, + expectedStreamState, + actualStreamRevision + ); + } + + var expectedRevision = response.WrongExpectedVersion.ExpectedRevisionOptionCase == ExpectedRevisionOptionOneofCase.ExpectedRevision + ? new StreamRevision(response.WrongExpectedVersion.ExpectedRevision) + : StreamRevision.None; + + return new WrongExpectedVersionResult( + header.Options.StreamIdentifier!, + expectedRevision, + actualStreamRevision + ); + } + + class StreamAppender : IDisposable { + readonly KurrentClientSettings _settings; + readonly CancellationToken _cancellationToken; + readonly Action _onException; + readonly Channel _channel; + readonly ConcurrentDictionary> _pendingRequests; + readonly TaskCompletionSource _isUsable; + + ChannelInfo? _channelInfo; + AsyncDuplexStreamingCall? _call; + + public StreamAppender( + KurrentClientSettings settings, + ValueTask channelInfoTask, + CancellationToken cancellationToken, + Action onException + ) { + _settings = settings; + _cancellationToken = cancellationToken; + _onException = onException; + _channel = Channel.CreateBounded(10000); + _pendingRequests = new ConcurrentDictionary>(); + _isUsable = new TaskCompletionSource(); + + _ = Task.Run(() => Duplex(channelInfoTask), cancellationToken); + } + + public ValueTask Append( + string streamName, StreamRevision expectedStreamPosition, + IEnumerable events, TimeSpan? timeoutAfter, + CancellationToken cancellationToken = default + ) => + AppendInternal( + BatchAppendReq.Types.Options.Create(streamName, expectedStreamPosition, timeoutAfter), + events, + cancellationToken + ); + + public ValueTask Append( + string streamName, StreamState expectedStreamState, + IEnumerable events, TimeSpan? timeoutAfter, + CancellationToken cancellationToken = default + ) => + AppendInternal( + BatchAppendReq.Types.Options.Create(streamName, expectedStreamState, timeoutAfter), + events, + cancellationToken + ); + + public Task IsUsable() => _isUsable.Task; + + ValueTask AppendInternal( + BatchAppendReq.Types.Options options, + IEnumerable events, + CancellationToken cancellationToken + ) { + var tags = new ActivityTagsCollection() + .WithRequiredTag(TelemetryTags.Kurrent.Stream, options.StreamIdentifier.StreamName.ToStringUtf8()) + .WithGrpcChannelServerTags(_channelInfo) + .WithClientSettingsServerTags(_settings) + .WithOptionalTag(TelemetryTags.Database.User, _settings.DefaultCredentials?.Username); + + return KurrentClientDiagnostics.ActivitySource.TraceClientOperation( + Operation, + TracingConstants.Operations.Append, + tags + ); + + async ValueTask Operation() { + var correlationId = Uuid.NewUuid(); + + var complete = _pendingRequests.GetOrAdd(correlationId, new TaskCompletionSource()); + + try { + foreach (var appendRequest in GetRequests(events, options, correlationId)) + await _channel.Writer.WriteAsync(appendRequest, cancellationToken).ConfigureAwait(false); + } + catch (ChannelClosedException ex) { + // channel is closed, our tcs won't necessarily get completed, don't wait for it. + throw ex.InnerException ?? ex; + } + + return await complete.Task.ConfigureAwait(false); + } + } + + async Task Duplex(ValueTask channelInfoTask) { + try { + _channelInfo = await channelInfoTask.ConfigureAwait(false); + if (!_channelInfo.ServerCapabilities.SupportsBatchAppend) { + _channel.Writer.TryComplete(new NotSupportedException("Server does not support batch append")); + _isUsable.TrySetResult(false); + return; + } + + _call = new StreamsClient(_channelInfo.CallInvoker).BatchAppend( + KurrentCallOptions.CreateStreaming( + _settings, + userCredentials: _settings.DefaultCredentials, + cancellationToken: _cancellationToken + ) + ); + + _ = Task.Run(Send, _cancellationToken); + _ = Task.Run(Receive, _cancellationToken); + + _isUsable.TrySetResult(true); + } + catch (Exception ex) { + _isUsable.TrySetException(ex); + _onException(ex); + } + + return; + + async Task Send() { + if (_call is null) return; + + await foreach (var appendRequest in _channel.Reader.ReadAllAsync(_cancellationToken).ConfigureAwait(false)) + await _call.RequestStream.WriteAsync(appendRequest).ConfigureAwait(false); + + await _call.RequestStream.CompleteAsync().ConfigureAwait(false); + } + + async Task Receive() { + if (_call is null) return; + + try { + await foreach (var response in _call.ResponseStream.ReadAllAsync(_cancellationToken).ConfigureAwait(false)) { + if (!_pendingRequests.TryRemove(Uuid.FromDto(response.CorrelationId), out var writeResult)) { + continue; // TODO: Log? + } + + try { + writeResult.TrySetResult(response.ToWriteResult()); + } + catch (Exception ex) { + writeResult.TrySetException(ex); + } + } + } + catch (Exception ex) { + // signal that no tcs added to _pendingRequests after this point will necessarily complete + _channel.Writer.TryComplete(ex); + + // complete whatever tcs's we have + foreach (var request in _pendingRequests) + request.Value.TrySetException(ex); + + _onException(ex); + } + } + } + + IEnumerable GetRequests(IEnumerable events, BatchAppendReq.Types.Options options, Uuid correlationId) { + var batchSize = 0; + var first = true; + var correlationIdDto = correlationId.ToDto(); + var proposedMessages = new List(); + + foreach (var eventData in events) { + var proposedMessage = new BatchAppendReq.Types.ProposedMessage { + Data = ByteString.CopyFrom(eventData.Data.Span), + CustomMetadata = ByteString.CopyFrom(eventData.Metadata.InjectTracingContext(Activity.Current)), + Id = eventData.EventId.ToDto(), + Metadata = { + { Constants.Metadata.Type, eventData.Type }, + { Constants.Metadata.ContentType, eventData.ContentType } + } + }; + + proposedMessages.Add(proposedMessage); + + if ((batchSize += proposedMessage.CalculateSize()) < _settings.OperationOptions.BatchAppendSize) + continue; + + yield return new BatchAppendReq { + ProposedMessages = { proposedMessages }, + CorrelationId = correlationIdDto, + Options = first ? options : null + }; + + first = false; + proposedMessages.Clear(); + batchSize = 0; + } + + yield return new BatchAppendReq { + ProposedMessages = { proposedMessages }, + IsFinal = true, + CorrelationId = correlationIdDto, + Options = first ? options : null + }; + } + + public void Dispose() { + _channel.Writer.TryComplete(); + _call?.Dispose(); + } + } + } +} diff --git a/src/Kurrent.Client/Streams/KurrentClient.Delete.cs b/src/Kurrent.Client/Streams/KurrentClient.Delete.cs new file mode 100644 index 000000000..50a174e25 --- /dev/null +++ b/src/Kurrent.Client/Streams/KurrentClient.Delete.cs @@ -0,0 +1,65 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using EventStore.Client.Streams; +using Microsoft.Extensions.Logging; + +namespace EventStore.Client { + public partial class KurrentClient { + /// + /// Deletes a stream asynchronously. + /// + /// The name of the stream to delete. + /// The expected of the stream being deleted. + /// The maximum time to wait before terminating the call. + /// The optional to perform operation with. + /// The optional . + /// + public Task DeleteAsync( + string streamName, + StreamRevision expectedRevision, + TimeSpan? deadline = null, + UserCredentials? userCredentials = null, + CancellationToken cancellationToken = default) => + DeleteInternal(new DeleteReq { + Options = new DeleteReq.Types.Options { + StreamIdentifier = streamName, + Revision = expectedRevision + } + }, deadline, userCredentials, cancellationToken); + + /// + /// Deletes a stream asynchronously. + /// + /// The name of the stream to delete. + /// The expected of the stream being deleted. + /// The maximum time to wait before terminating the call. + /// The optional to perform operation with. + /// The optional . + /// + public Task DeleteAsync( + string streamName, + StreamState expectedState, + TimeSpan? deadline = null, + UserCredentials? userCredentials = null, + CancellationToken cancellationToken = default) => DeleteInternal(new DeleteReq { + Options = new DeleteReq.Types.Options { + StreamIdentifier = streamName + } + }.WithAnyStreamRevision(expectedState), deadline, userCredentials, cancellationToken); + + private async Task DeleteInternal(DeleteReq request, + TimeSpan? deadline, + UserCredentials? userCredentials, + CancellationToken cancellationToken) { + _log.LogDebug("Deleting stream {streamName}.", request.Options.StreamIdentifier); + var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); + using var call = new Streams.Streams.StreamsClient( + channelInfo.CallInvoker).DeleteAsync(request, + KurrentCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); + var result = await call.ResponseAsync.ConfigureAwait(false); + + return new DeleteResult(new Position(result.Position.CommitPosition, result.Position.PreparePosition)); + } + } +} diff --git a/src/Kurrent.Client/Streams/KurrentClient.Metadata.cs b/src/Kurrent.Client/Streams/KurrentClient.Metadata.cs new file mode 100644 index 000000000..f8185607c --- /dev/null +++ b/src/Kurrent.Client/Streams/KurrentClient.Metadata.cs @@ -0,0 +1,106 @@ +using System; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using EventStore.Client.Streams; +using Microsoft.Extensions.Logging; + +namespace EventStore.Client { + public partial class KurrentClient { + /// + /// Asynchronously reads the metadata for a stream + /// + /// The name of the stream to read the metadata for. + /// + /// The optional to perform operation with. + /// The optional . + /// + public async Task GetStreamMetadataAsync(string streamName, TimeSpan? deadline = null, + UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) { + _log.LogDebug("Read stream metadata for {streamName}.", streamName); + + try { + var result = ReadStreamAsync(Direction.Backwards, SystemStreams.MetastreamOf(streamName), + StreamPosition.End, 1, false, deadline, userCredentials, cancellationToken); + await foreach (var message in result.Messages.ConfigureAwait(false)) { + if (message is not StreamMessage.Event(var resolvedEvent)) { + continue; + } + + return StreamMetadataResult.Create(streamName, resolvedEvent.OriginalEventNumber, + JsonSerializer.Deserialize(resolvedEvent.Event.Data.Span, + StreamMetadataJsonSerializerOptions)); + } + + } catch (StreamNotFoundException) { + } + _log.LogWarning("Stream metadata for {streamName} not found.", streamName); + return StreamMetadataResult.None(streamName); + } + + /// + /// Asynchronously sets the metadata for a stream. + /// + /// The name of the stream to set metadata for. + /// The of the stream to append to. + /// A representing the new metadata. + /// An to configure the operation's options. + /// + /// The optional to perform operation with. + /// The optional . + /// + public Task SetStreamMetadataAsync(string streamName, StreamState expectedState, + StreamMetadata metadata, Action? configureOperationOptions = null, + TimeSpan? deadline = null, UserCredentials? userCredentials = null, + CancellationToken cancellationToken = default) { + var options = Settings.OperationOptions.Clone(); + configureOperationOptions?.Invoke(options); + + return SetStreamMetadataInternal(metadata, new AppendReq { + Options = new AppendReq.Types.Options { + StreamIdentifier = SystemStreams.MetastreamOf(streamName) + } + }.WithAnyStreamRevision(expectedState), options, deadline, userCredentials, cancellationToken); + } + + /// + /// Asynchronously sets the metadata for a stream. + /// + /// The name of the stream to set metadata for. + /// The of the stream to append to. + /// A representing the new metadata. + /// An to configure the operation's options. + /// + /// The optional to perform operation with. + /// The optional . + /// + public Task SetStreamMetadataAsync(string streamName, StreamRevision expectedRevision, + StreamMetadata metadata, Action? configureOperationOptions = null, + TimeSpan? deadline = null, UserCredentials? userCredentials = null, + CancellationToken cancellationToken = default) { + var options = Settings.OperationOptions.Clone(); + configureOperationOptions?.Invoke(options); + + return SetStreamMetadataInternal(metadata, new AppendReq { + Options = new AppendReq.Types.Options { + StreamIdentifier = SystemStreams.MetastreamOf(streamName), + Revision = expectedRevision + } + }, options, deadline, userCredentials, cancellationToken); + } + + private async Task SetStreamMetadataInternal(StreamMetadata metadata, + AppendReq appendReq, + KurrentClientOperationOptions operationOptions, + TimeSpan? deadline, + UserCredentials? userCredentials, + CancellationToken cancellationToken) { + + var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); + return await AppendToStreamInternal(channelInfo, appendReq, new[] { + new EventData(Uuid.NewUuid(), SystemEventTypes.StreamMetadata, + JsonSerializer.SerializeToUtf8Bytes(metadata, StreamMetadataJsonSerializerOptions)), + }, operationOptions, deadline, userCredentials, cancellationToken).ConfigureAwait(false); + } + } +} diff --git a/src/Kurrent.Client/Streams/KurrentClient.Read.cs b/src/Kurrent.Client/Streams/KurrentClient.Read.cs new file mode 100644 index 000000000..0523d9516 --- /dev/null +++ b/src/Kurrent.Client/Streams/KurrentClient.Read.cs @@ -0,0 +1,468 @@ +using System.Threading.Channels; +using EventStore.Client.Streams; +using Grpc.Core; +using static EventStore.Client.Streams.ReadResp; +using static EventStore.Client.Streams.ReadResp.ContentOneofCase; + +namespace EventStore.Client { + public partial class KurrentClient { + /// + /// Asynchronously reads all events. + /// + /// The in which to read. + /// The to start reading from. + /// The maximum count to read. + /// Whether to resolve LinkTo events automatically. + /// + /// The optional to perform operation with. + /// The optional . + /// + public ReadAllStreamResult ReadAllAsync( + Direction direction, + Position position, + long maxCount = long.MaxValue, + bool resolveLinkTos = false, + TimeSpan? deadline = null, + UserCredentials? userCredentials = null, + CancellationToken cancellationToken = default + ) => ReadAllAsync( + direction, + position, + eventFilter: null, + maxCount, + resolveLinkTos, + deadline, + userCredentials, + cancellationToken + ); + + /// + /// Asynchronously reads all events with filtering. + /// + /// The in which to read. + /// The to start reading from. + /// The to apply. + /// The maximum count to read. + /// Whether to resolve LinkTo events automatically. + /// + /// The optional to perform operation with. + /// The optional . + /// + public ReadAllStreamResult ReadAllAsync( + Direction direction, + Position position, + IEventFilter? eventFilter, + long maxCount = long.MaxValue, + bool resolveLinkTos = false, + TimeSpan? deadline = null, + UserCredentials? userCredentials = null, + CancellationToken cancellationToken = default + ) { + if (maxCount <= 0) + throw new ArgumentOutOfRangeException(nameof(maxCount)); + + var readReq = new ReadReq { + Options = new() { + ReadDirection = direction switch { + Direction.Backwards => ReadReq.Types.Options.Types.ReadDirection.Backwards, + Direction.Forwards => ReadReq.Types.Options.Types.ReadDirection.Forwards, + _ => throw InvalidOption(direction) + }, + ResolveLinks = resolveLinkTos, + All = new() { + Position = new() { + CommitPosition = position.CommitPosition, + PreparePosition = position.PreparePosition + } + }, + Count = (ulong)maxCount, + UuidOption = new() { Structured = new() }, + ControlOption = new() { Compatibility = 1 }, + Filter = GetFilterOptions(eventFilter) + } + }; + + return new ReadAllStreamResult( + async _ => { + var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); + return channelInfo.CallInvoker; + }, + readReq, + Settings, + deadline, + userCredentials, + cancellationToken + ); + } + + /// + /// A class that represents the result of a read operation on the $all stream. You may either enumerate this instance directly or . Do not enumerate more than once. + /// + public class ReadAllStreamResult : IAsyncEnumerable { + readonly Channel _channel; + readonly CancellationTokenSource _cts; + + int _messagesEnumerated; + + /// + /// The last of the $all stream, if available. + /// + public Position? LastPosition { get; private set; } + + /// + /// An . Do not enumerate more than once. + /// + public IAsyncEnumerable Messages { + get { + return GetMessages(); + + async IAsyncEnumerable GetMessages() { + if (Interlocked.Exchange(ref _messagesEnumerated, 1) == 1) { + throw new InvalidOperationException("Messages may only be enumerated once."); + } + + try { + await foreach (var message in _channel.Reader.ReadAllAsync(_cts.Token) + .ConfigureAwait(false)) { + if (message is StreamMessage.LastAllStreamPosition(var position)) { + LastPosition = position; + } + + yield return message; + } + } + finally { + _cts.Cancel(); + } + } + } + } + + internal ReadAllStreamResult( + Func> selectCallInvoker, ReadReq request, + KurrentClientSettings settings, TimeSpan? deadline, UserCredentials? userCredentials, + CancellationToken cancellationToken + ) { + var callOptions = KurrentCallOptions.CreateStreaming( + settings, + deadline, + userCredentials, + cancellationToken + ); + + _channel = Channel.CreateBounded(ReadBoundedChannelOptions); + + _cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + var linkedCancellationToken = _cts.Token; + + if (request.Options.FilterOptionCase == ReadReq.Types.Options.FilterOptionOneofCase.None) + request.Options.NoFilter = new(); + + _ = PumpMessages(); + + return; + + async Task PumpMessages() { + try { + var callInvoker = await selectCallInvoker(linkedCancellationToken).ConfigureAwait(false); + var client = new Streams.Streams.StreamsClient(callInvoker); + using var call = client.Read(request, callOptions); + await foreach (var response in call.ResponseStream.ReadAllAsync(linkedCancellationToken) + .ConfigureAwait(false)) { + await _channel.Writer.WriteAsync( + response.ContentCase switch { + StreamNotFound => StreamMessage.NotFound.Instance, + Event => new StreamMessage.Event(ConvertToResolvedEvent(response.Event)), + FirstStreamPosition => new StreamMessage.FirstStreamPosition(new StreamPosition(response.FirstStreamPosition)), + LastStreamPosition => new StreamMessage.LastStreamPosition(new StreamPosition(response.LastStreamPosition)), + LastAllStreamPosition => new StreamMessage.LastAllStreamPosition( + new Position( + response.LastAllStreamPosition.CommitPosition, + response.LastAllStreamPosition.PreparePosition + ) + ), + _ => StreamMessage.Unknown.Instance + }, + linkedCancellationToken + ).ConfigureAwait(false); + } + + _channel.Writer.Complete(); + } + catch (Exception ex) { + _channel.Writer.TryComplete(ex); + } + } + } + + /// + public async IAsyncEnumerator GetAsyncEnumerator( + CancellationToken cancellationToken = default + ) { + try { + await foreach (var message in _channel.Reader.ReadAllAsync(cancellationToken).ConfigureAwait(false)) { + if (message is not StreamMessage.Event e) { + continue; + } + + yield return e.ResolvedEvent; + } + } + finally { + _cts.Cancel(); + } + } + } + + /// + /// Asynchronously reads all the events from a stream. + /// + /// The result could also be inspected as a means to avoid handling exceptions as the would indicate whether or not the stream is readable./> + /// + /// The in which to read. + /// The name of the stream to read. + /// The to start reading from. + /// The number of events to read from the stream. + /// Whether to resolve LinkTo events automatically. + /// + /// The optional to perform operation with. + /// The optional . + /// + public ReadStreamResult ReadStreamAsync( + Direction direction, + string streamName, + StreamPosition revision, + long maxCount = long.MaxValue, + bool resolveLinkTos = false, + TimeSpan? deadline = null, + UserCredentials? userCredentials = null, + CancellationToken cancellationToken = default + ) { + if (maxCount <= 0) + throw new ArgumentOutOfRangeException(nameof(maxCount)); + + return new ReadStreamResult( + async _ => { + var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); + return channelInfo.CallInvoker; + }, + new ReadReq { + Options = new() { + ReadDirection = direction switch { + Direction.Backwards => ReadReq.Types.Options.Types.ReadDirection.Backwards, + Direction.Forwards => ReadReq.Types.Options.Types.ReadDirection.Forwards, + _ => throw InvalidOption(direction) + }, + ResolveLinks = resolveLinkTos, + Stream = ReadReq.Types.Options.Types.StreamOptions.FromStreamNameAndRevision( + streamName, + revision + ), + Count = (ulong)maxCount, + UuidOption = new() { Structured = new() }, + NoFilter = new(), + ControlOption = new() { Compatibility = 1 } + } + }, + Settings, + deadline, + userCredentials, + cancellationToken + ); + } + + /// + /// A class that represents the result of a read operation on a stream. You may either enumerate this instance directly or . Do not enumerate more than once. + /// + public class ReadStreamResult : IAsyncEnumerable { + readonly Channel _channel; + readonly CancellationTokenSource _cts; + + int _messagesEnumerated; + + /// + /// The name of the stream. + /// + public string StreamName { get; } + + /// + /// The of the first message in this stream. Will only be filled once has been enumerated. + /// + public StreamPosition? FirstStreamPosition { get; private set; } + + /// + /// The of the last message in this stream. Will only be filled once has been enumerated. + /// + public StreamPosition? LastStreamPosition { get; private set; } + + /// + /// An . Do not enumerate more than once. + /// + public IAsyncEnumerable Messages { + get { + return GetMessages(); + + async IAsyncEnumerable GetMessages() { + if (Interlocked.Exchange(ref _messagesEnumerated, 1) == 1) { + throw new InvalidOperationException("Messages may only be enumerated once."); + } + + try { + await foreach (var message in _channel.Reader.ReadAllAsync(_cts.Token).ConfigureAwait(false)) { + switch (message) { + case StreamMessage.FirstStreamPosition(var streamPosition): + FirstStreamPosition = streamPosition; + break; + + case StreamMessage.LastStreamPosition(var lastStreamPosition): + LastStreamPosition = lastStreamPosition; + break; + + default: + break; + } + + yield return message; + } + } + finally { + _cts.Cancel(); + } + } + } + } + + /// + /// The . + /// + public Task ReadState { get; } + + internal ReadStreamResult( + Func> selectCallInvoker, ReadReq request, + KurrentClientSettings settings, TimeSpan? deadline, UserCredentials? userCredentials, + CancellationToken cancellationToken + ) { + var callOptions = KurrentCallOptions.CreateStreaming( + settings, + deadline, + userCredentials, + cancellationToken + ); + + _channel = Channel.CreateBounded(ReadBoundedChannelOptions); + + StreamName = request.Options.Stream.StreamIdentifier!; + + var tcs = new TaskCompletionSource(); + _cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + var linkedCancellationToken = _cts.Token; +#pragma warning disable CS0612 + ReadState = tcs.Task; +#pragma warning restore CS0612 + + _ = PumpMessages(); + + return; + + async Task PumpMessages() { + var firstMessageRead = false; + + try { + var callInvoker = await selectCallInvoker(linkedCancellationToken).ConfigureAwait(false); + var client = new Streams.Streams.StreamsClient(callInvoker); + using var call = client.Read(request, callOptions); + + await foreach (var response in call.ResponseStream.ReadAllAsync(linkedCancellationToken) + .ConfigureAwait(false)) { + if (!firstMessageRead) { + firstMessageRead = true; + + if (response.ContentCase != StreamNotFound || request.Options.Stream == null) { + await _channel.Writer.WriteAsync(StreamMessage.Ok.Instance, linkedCancellationToken) + .ConfigureAwait(false); + + tcs.SetResult(Client.ReadState.Ok); + } + else { + tcs.SetResult(Client.ReadState.StreamNotFound); + } + } + + await _channel.Writer.WriteAsync( + response.ContentCase switch { + StreamNotFound => StreamMessage.NotFound.Instance, + Event => new StreamMessage.Event(ConvertToResolvedEvent(response.Event)), + ContentOneofCase.FirstStreamPosition => new StreamMessage.FirstStreamPosition( + new StreamPosition(response.FirstStreamPosition) + ), + ContentOneofCase.LastStreamPosition => new StreamMessage.LastStreamPosition( + new StreamPosition(response.LastStreamPosition) + ), + LastAllStreamPosition => new StreamMessage.LastAllStreamPosition( + new Position( + response.LastAllStreamPosition.CommitPosition, + response.LastAllStreamPosition.PreparePosition + ) + ), + _ => StreamMessage.Unknown.Instance + }, + linkedCancellationToken + ).ConfigureAwait(false); + } + + _channel.Writer.Complete(); + } + catch (Exception ex) { + tcs.TrySetException(ex); + _channel.Writer.TryComplete(ex); + } + } + } + + /// + public async IAsyncEnumerator GetAsyncEnumerator( + CancellationToken cancellationToken = default + ) { + try { + await foreach (var message in _channel.Reader.ReadAllAsync(cancellationToken).ConfigureAwait(false)) { + if (message is StreamMessage.NotFound) { + throw new StreamNotFoundException(StreamName); + } + + if (message is not StreamMessage.Event e) { + continue; + } + + yield return e.ResolvedEvent; + } + } + finally { + _cts.Cancel(); + } + } + } + + static ResolvedEvent ConvertToResolvedEvent(ReadResp.Types.ReadEvent readEvent) => + new ResolvedEvent( + ConvertToEventRecord(readEvent.Event)!, + ConvertToEventRecord(readEvent.Link), + readEvent.PositionCase switch { + ReadResp.Types.ReadEvent.PositionOneofCase.CommitPosition => readEvent.CommitPosition, + _ => null + } + ); + + static EventRecord? ConvertToEventRecord(ReadResp.Types.ReadEvent.Types.RecordedEvent? e) => + e == null + ? null + : new EventRecord( + e.StreamIdentifier!, + Uuid.FromDto(e.Id), + new StreamPosition(e.StreamRevision), + new Position(e.CommitPosition, e.PreparePosition), + e.Metadata, + e.Data.ToByteArray(), + e.CustomMetadata.ToByteArray() + ); + } +} diff --git a/src/Kurrent.Client/Streams/KurrentClient.Subscriptions.cs b/src/Kurrent.Client/Streams/KurrentClient.Subscriptions.cs new file mode 100644 index 000000000..92adb172b --- /dev/null +++ b/src/Kurrent.Client/Streams/KurrentClient.Subscriptions.cs @@ -0,0 +1,302 @@ +using System.Threading.Channels; +using EventStore.Client.Diagnostics; +using EventStore.Client.Streams; +using Grpc.Core; + +using static EventStore.Client.Streams.ReadResp.ContentOneofCase; + +namespace EventStore.Client { + public partial class KurrentClient { + /// + /// Subscribes to all events. + /// + /// A (exclusive of) to start the subscription from. + /// A Task invoked and awaited when a new event is received over the subscription. + /// Whether to resolve LinkTo events automatically. + /// An action invoked if the subscription is dropped. + /// The optional to apply. + /// The optional user credentials to perform operation with. + /// The optional . + /// + public Task SubscribeToAllAsync( + FromAll start, + Func eventAppeared, + bool resolveLinkTos = false, + Action? subscriptionDropped = default, + SubscriptionFilterOptions? filterOptions = null, + UserCredentials? userCredentials = null, + CancellationToken cancellationToken = default + ) => StreamSubscription.Confirm( + SubscribeToAll(start, resolveLinkTos, filterOptions, userCredentials, cancellationToken), + eventAppeared, + subscriptionDropped, + _log, + filterOptions?.CheckpointReached, + cancellationToken: cancellationToken + ); + + /// + /// Subscribes to all events. + /// + /// A (exclusive of) to start the subscription from. + /// Whether to resolve LinkTo events automatically. + /// The optional to apply. + /// The optional user credentials to perform operation with. + /// The optional . + /// + public StreamSubscriptionResult SubscribeToAll( + FromAll start, + bool resolveLinkTos = false, + SubscriptionFilterOptions? filterOptions = null, + UserCredentials? userCredentials = null, + CancellationToken cancellationToken = default + ) => new( + async _ => await GetChannelInfo(cancellationToken).ConfigureAwait(false), + new ReadReq { + Options = new ReadReq.Types.Options { + ReadDirection = ReadReq.Types.Options.Types.ReadDirection.Forwards, + ResolveLinks = resolveLinkTos, + All = ReadReq.Types.Options.Types.AllOptions.FromSubscriptionPosition(start), + Subscription = new ReadReq.Types.Options.Types.SubscriptionOptions(), + Filter = GetFilterOptions(filterOptions)!, + UuidOption = new() { Structured = new() } + } + }, + Settings, + userCredentials, + cancellationToken + ); + + /// + /// Subscribes to a stream from a checkpoint. + /// + /// A (exclusive of) to start the subscription from. + /// The name of the stream to read events from. + /// A Task invoked and awaited when a new event is received over the subscription. + /// Whether to resolve LinkTo events automatically. + /// An action invoked if the subscription is dropped. + /// The optional user credentials to perform operation with. + /// The optional . + /// + public Task SubscribeToStreamAsync( + string streamName, + FromStream start, + Func eventAppeared, + bool resolveLinkTos = false, + Action? subscriptionDropped = default, + UserCredentials? userCredentials = null, + CancellationToken cancellationToken = default + ) => StreamSubscription.Confirm( + SubscribeToStream(streamName, start, resolveLinkTos, userCredentials, cancellationToken), + eventAppeared, + subscriptionDropped, + _log, + cancellationToken: cancellationToken + ); + + /// + /// Subscribes to a stream from a checkpoint. + /// + /// A (exclusive of) to start the subscription from. + /// The name of the stream to read events from. + /// Whether to resolve LinkTo events automatically. + /// The optional user credentials to perform operation with. + /// The optional . + /// + public StreamSubscriptionResult SubscribeToStream( + string streamName, + FromStream start, + bool resolveLinkTos = false, + UserCredentials? userCredentials = null, + CancellationToken cancellationToken = default + ) => new( + async _ => await GetChannelInfo(cancellationToken).ConfigureAwait(false), + new ReadReq { + Options = new ReadReq.Types.Options { + ReadDirection = ReadReq.Types.Options.Types.ReadDirection.Forwards, + ResolveLinks = resolveLinkTos, + Stream = ReadReq.Types.Options.Types.StreamOptions.FromSubscriptionPosition(streamName, start), + Subscription = new ReadReq.Types.Options.Types.SubscriptionOptions(), + UuidOption = new() { Structured = new() } + } + }, + Settings, + userCredentials, + cancellationToken + ); + + /// + /// A class that represents the result of a subscription operation. You may either enumerate this instance directly or . Do not enumerate more than once. + /// + public class StreamSubscriptionResult : IAsyncEnumerable, IAsyncDisposable, IDisposable { + private readonly ReadReq _request; + private readonly Channel _channel; + private readonly CancellationTokenSource _cts; + private readonly CallOptions _callOptions; + private readonly KurrentClientSettings _settings; + private AsyncServerStreamingCall? _call; + + private int _messagesEnumerated; + + /// + /// The server-generated unique identifier for the subscription. + /// + public string? SubscriptionId { get; private set; } + + /// + /// An . Do not enumerate more than once. + /// + public IAsyncEnumerable Messages { + get { + if (Interlocked.Exchange(ref _messagesEnumerated, 1) == 1) + throw new InvalidOperationException("Messages may only be enumerated once."); + + return GetMessages(); + + async IAsyncEnumerable GetMessages() { + try { + await foreach (var message in _channel.Reader.ReadAllAsync(_cts.Token)) { + if (message is StreamMessage.SubscriptionConfirmation(var subscriptionId)) + SubscriptionId = subscriptionId; + + yield return message; + } + } + finally { +#if NET8_0_OR_GREATER + await _cts.CancelAsync().ConfigureAwait(false); +#else + _cts.Cancel(); +#endif + } + } + } + } + + internal StreamSubscriptionResult( + Func> selectChannelInfo, + ReadReq request, KurrentClientSettings settings, UserCredentials? userCredentials, + CancellationToken cancellationToken + ) { + _request = request; + _settings = settings; + + _callOptions = KurrentCallOptions.CreateStreaming( + settings, + userCredentials: userCredentials, + cancellationToken: cancellationToken + ); + + _channel = Channel.CreateBounded(ReadBoundedChannelOptions); + + _cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + + if (_request.Options.FilterOptionCase == ReadReq.Types.Options.FilterOptionOneofCase.None) { + _request.Options.NoFilter = new(); + } + + _ = PumpMessages(); + + return; + + async Task PumpMessages() { + try { + var channelInfo = await selectChannelInfo(_cts.Token).ConfigureAwait(false); + var client = new Streams.Streams.StreamsClient(channelInfo.CallInvoker); + _call = client.Read(_request, _callOptions); + await foreach (var response in _call.ResponseStream.ReadAllAsync(_cts.Token).ConfigureAwait(false)) { + StreamMessage subscriptionMessage = + response.ContentCase switch { + Confirmation => new StreamMessage.SubscriptionConfirmation(response.Confirmation.SubscriptionId), + Event => new StreamMessage.Event(ConvertToResolvedEvent(response.Event)), + FirstStreamPosition => new StreamMessage.FirstStreamPosition(new StreamPosition(response.FirstStreamPosition)), + LastStreamPosition => new StreamMessage.LastStreamPosition(new StreamPosition(response.LastStreamPosition)), + LastAllStreamPosition => new StreamMessage.LastAllStreamPosition( + new Position( + response.LastAllStreamPosition.CommitPosition, + response.LastAllStreamPosition.PreparePosition + ) + ), + Checkpoint => new StreamMessage.AllStreamCheckpointReached( + new Position( + response.Checkpoint.CommitPosition, + response.Checkpoint.PreparePosition + ) + ), + CaughtUp => StreamMessage.CaughtUp.Instance, + FellBehind => StreamMessage.FellBehind.Instance, + _ => StreamMessage.Unknown.Instance + }; + + if (subscriptionMessage is StreamMessage.Event evt) + KurrentClientDiagnostics.ActivitySource.TraceSubscriptionEvent( + SubscriptionId, + evt.ResolvedEvent, + channelInfo, + _settings, + userCredentials + ); + + await _channel.Writer + .WriteAsync(subscriptionMessage, _cts.Token) + .ConfigureAwait(false); + } + + _channel.Writer.Complete(); + } catch (Exception ex) { + _channel.Writer.TryComplete(ex); + } + } + } + + /// + public async ValueTask DisposeAsync() { + //TODO SS: Check if `CastAndDispose` is still relevant + await CastAndDispose(_cts).ConfigureAwait(false); + await CastAndDispose(_call).ConfigureAwait(false); + + return; + + static async ValueTask CastAndDispose(IDisposable? resource) { + switch (resource) { + case null: + return; + + case IAsyncDisposable disposable: + await disposable.DisposeAsync().ConfigureAwait(false); + break; + + default: + resource.Dispose(); + break; + } + } + } + + /// + public void Dispose() { + _cts.Dispose(); + _call?.Dispose(); + } + + /// + public async IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = default) { + try { + await foreach (var message in _channel.Reader.ReadAllAsync(cancellationToken).ConfigureAwait(false)) { + if (message is not StreamMessage.Event e) + continue; + + yield return e.ResolvedEvent; + } + } + finally { +#if NET8_0_OR_GREATER + await _cts.CancelAsync().ConfigureAwait(false); +#else + _cts.Cancel(); +#endif + } + } + } + } +} diff --git a/src/Kurrent.Client/Streams/KurrentClient.Tombstone.cs b/src/Kurrent.Client/Streams/KurrentClient.Tombstone.cs new file mode 100644 index 000000000..26d62dad9 --- /dev/null +++ b/src/Kurrent.Client/Streams/KurrentClient.Tombstone.cs @@ -0,0 +1,63 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using EventStore.Client.Streams; +using Microsoft.Extensions.Logging; + +namespace EventStore.Client { + public partial class KurrentClient { + /// + /// Tombstones a stream asynchronously. Note: Tombstoned streams can never be recreated. + /// + /// The name of the stream to tombstone. + /// The expected of the stream being deleted. + /// + /// The optional to perform operation with. + /// The optional . + /// + public Task TombstoneAsync( + string streamName, + StreamRevision expectedRevision, + TimeSpan? deadline = null, + UserCredentials? userCredentials = null, + CancellationToken cancellationToken = default) => TombstoneInternal(new TombstoneReq { + Options = new TombstoneReq.Types.Options { + StreamIdentifier = streamName, + Revision = expectedRevision + } + }, deadline, userCredentials, cancellationToken); + + /// + /// Tombstones a stream asynchronously. Note: Tombstoned streams can never be recreated. + /// + /// The name of the stream to tombstone. + /// The expected of the stream being deleted. + /// + /// The optional to perform operation with. + /// The optional . + /// + public Task TombstoneAsync( + string streamName, + StreamState expectedState, + TimeSpan? deadline = null, + UserCredentials? userCredentials = null, + CancellationToken cancellationToken = default) => TombstoneInternal(new TombstoneReq { + Options = new TombstoneReq.Types.Options { + StreamIdentifier = streamName + } + }.WithAnyStreamRevision(expectedState), deadline, userCredentials, cancellationToken); + + private async Task TombstoneInternal(TombstoneReq request, TimeSpan? deadline, + UserCredentials? userCredentials, CancellationToken cancellationToken) { + _log.LogDebug("Tombstoning stream {streamName}.", request.Options.StreamIdentifier); + + var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); + using var call = new Streams.Streams.StreamsClient( + channelInfo.CallInvoker).TombstoneAsync(request, + KurrentCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); + var result = await call.ResponseAsync.ConfigureAwait(false); + + return new DeleteResult(new Position(result.Position.CommitPosition, result.Position.PreparePosition)); + } + } +} diff --git a/src/Kurrent.Client/Streams/KurrentClient.cs b/src/Kurrent.Client/Streams/KurrentClient.cs new file mode 100644 index 000000000..3dccf53ee --- /dev/null +++ b/src/Kurrent.Client/Streams/KurrentClient.cs @@ -0,0 +1,166 @@ +using System.Text.Json; +using System.Threading.Channels; +using Grpc.Core; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; +using ReadReq = EventStore.Client.Streams.ReadReq; + +namespace EventStore.Client { + /// + /// The client used for operations on streams. + /// + public sealed partial class KurrentClient : KurrentClientBase { + static readonly JsonSerializerOptions StreamMetadataJsonSerializerOptions = new() { + Converters = { + StreamMetadataJsonConverter.Instance + }, + }; + + static BoundedChannelOptions ReadBoundedChannelOptions = new(1) { + SingleReader = true, + SingleWriter = true, + AllowSynchronousContinuations = true + }; + + readonly ILogger _log; + Lazy _batchAppenderLazy; + StreamAppender BatchAppender => _batchAppenderLazy.Value; + readonly CancellationTokenSource _disposedTokenSource; + + static readonly Dictionary> ExceptionMap = new() { + [Constants.Exceptions.InvalidTransaction] = ex => new InvalidTransactionException(ex.Message, ex), + [Constants.Exceptions.StreamDeleted] = ex => new StreamDeletedException( + ex.Trailers.FirstOrDefault(x => x.Key == Constants.Exceptions.StreamName)?.Value ?? "", + ex + ), + [Constants.Exceptions.WrongExpectedVersion] = ex => new WrongExpectedVersionException( + ex.Trailers.FirstOrDefault(x => x.Key == Constants.Exceptions.StreamName)?.Value!, + ex.Trailers.GetStreamRevision(Constants.Exceptions.ExpectedVersion), + ex.Trailers.GetStreamRevision(Constants.Exceptions.ActualVersion), + ex, + ex.Message + ), + [Constants.Exceptions.MaximumAppendSizeExceeded] = ex => new MaximumAppendSizeExceededException( + ex.Trailers.GetIntValueOrDefault(Constants.Exceptions.MaximumAppendSize), + ex + ), + [Constants.Exceptions.StreamNotFound] = ex => new StreamNotFoundException( + ex.Trailers.FirstOrDefault(x => x.Key == Constants.Exceptions.StreamName)?.Value!, + ex + ), + [Constants.Exceptions.MissingRequiredMetadataProperty] = ex => new RequiredMetadataPropertyMissingException( + ex.Trailers.FirstOrDefault(x => x.Key == Constants.Exceptions.MissingRequiredMetadataProperty)?.Value!, + ex + ), + }; + + /// + /// Constructs a new . This is not intended to be called directly from your code. + /// + /// + public KurrentClient(IOptions options) : this(options.Value) { } + + /// + /// Constructs a new . + /// + /// + public KurrentClient(KurrentClientSettings? settings = null) : base(settings, ExceptionMap) { + _log = Settings.LoggerFactory?.CreateLogger() ?? new NullLogger(); + _disposedTokenSource = new CancellationTokenSource(); + _batchAppenderLazy = new Lazy(CreateStreamAppender); + } + + void SwapStreamAppender(Exception ex) => + Interlocked.Exchange(ref _batchAppenderLazy, new Lazy(CreateStreamAppender)).Value + .Dispose(); + + // todo: might be nice to have two different kinds of appenders and we decide which to instantiate according to the server caps. + StreamAppender CreateStreamAppender() => new StreamAppender( + Settings, + GetChannelInfo(_disposedTokenSource.Token), + _disposedTokenSource.Token, + SwapStreamAppender + ); + + static ReadReq.Types.Options.Types.FilterOptions? GetFilterOptions( + IEventFilter? filter, uint checkpointInterval = 0 + ) { + if (filter == null + || filter.Equals(StreamFilter.None) + || filter.Equals(EventTypeFilter.None)) + return null; + + var options = filter switch { + StreamFilter => new ReadReq.Types.Options.Types.FilterOptions { + StreamIdentifier = (filter.Prefixes, filter.Regex) switch { + (_, _) + when (filter.Prefixes?.Length ?? 0) == 0 && + filter.Regex != RegularFilterExpression.None => + new ReadReq.Types.Options.Types.FilterOptions.Types.Expression + { Regex = filter.Regex }, + (_, _) + when (filter.Prefixes?.Length ?? 0) != 0 && + filter.Regex == RegularFilterExpression.None => + new ReadReq.Types.Options.Types.FilterOptions.Types.Expression { + Prefix = { Array.ConvertAll(filter.Prefixes!, e => e.ToString()) } + }, + _ => throw new InvalidOperationException() + } + }, + EventTypeFilter => new ReadReq.Types.Options.Types.FilterOptions { + EventType = (filter.Prefixes, filter.Regex) switch { + (_, _) + when (filter.Prefixes?.Length ?? 0) == 0 && + filter.Regex != RegularFilterExpression.None => + new ReadReq.Types.Options.Types.FilterOptions.Types.Expression + { Regex = filter.Regex }, + (_, _) + when (filter.Prefixes?.Length ?? 0) != 0 && + filter.Regex == RegularFilterExpression.None => + new ReadReq.Types.Options.Types.FilterOptions.Types.Expression { + Prefix = { Array.ConvertAll(filter.Prefixes!, e => e.ToString()) } + }, + _ => throw new InvalidOperationException() + } + }, + _ => null + }; + + if (options == null) + return null; + + if (filter.MaxSearchWindow.HasValue) + options.Max = filter.MaxSearchWindow.Value; + else + options.Count = new Empty(); + + options.CheckpointIntervalMultiplier = checkpointInterval; + + return options; + } + + static ReadReq.Types.Options.Types.FilterOptions? GetFilterOptions( + SubscriptionFilterOptions? filterOptions + ) + => filterOptions == null ? null : GetFilterOptions(filterOptions.Filter, filterOptions.CheckpointInterval); + + /// + public override void Dispose() { + if (_batchAppenderLazy.IsValueCreated) + _batchAppenderLazy.Value.Dispose(); + + _disposedTokenSource.Dispose(); + base.Dispose(); + } + + /// + public override async ValueTask DisposeAsync() { + if (_batchAppenderLazy.IsValueCreated) + _batchAppenderLazy.Value.Dispose(); + + _disposedTokenSource.Dispose(); + await base.DisposeAsync().ConfigureAwait(false); + } + } +} diff --git a/src/Kurrent.Client/Streams/KurrentClientExtensions.cs b/src/Kurrent.Client/Streams/KurrentClientExtensions.cs new file mode 100644 index 000000000..690fa89bd --- /dev/null +++ b/src/Kurrent.Client/Streams/KurrentClientExtensions.cs @@ -0,0 +1,109 @@ +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; + +namespace EventStore.Client { + /// + /// A set of extension methods for an . + /// + public static class KurrentClientExtensions { + private static readonly JsonSerializerOptions SystemSettingsJsonSerializerOptions = new JsonSerializerOptions { + Converters = { + SystemSettingsJsonConverter.Instance + }, + }; + + /// + /// Writes to the $settings stream. + /// + /// + /// + /// + /// + /// + /// + /// + public static Task SetSystemSettingsAsync( + this KurrentClient client, + SystemSettings settings, + TimeSpan? deadline = null, UserCredentials? userCredentials = null, + CancellationToken cancellationToken = default) { + if (client == null) throw new ArgumentNullException(nameof(client)); + return client.AppendToStreamAsync(SystemStreams.SettingsStream, StreamState.Any, + new[] { + new EventData(Uuid.NewUuid(), SystemEventTypes.Settings, + JsonSerializer.SerializeToUtf8Bytes(settings, SystemSettingsJsonSerializerOptions)) + }, deadline: deadline, userCredentials: userCredentials, cancellationToken: cancellationToken); + } + + /// + /// Appends to a stream conditionally. + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public static async Task ConditionalAppendToStreamAsync( + this KurrentClient client, + string streamName, + StreamRevision expectedRevision, + IEnumerable eventData, + TimeSpan? deadline = null, + UserCredentials? userCredentials = null, + CancellationToken cancellationToken = default) { + if (client == null) { + throw new ArgumentNullException(nameof(client)); + } + try { + var result = await client.AppendToStreamAsync(streamName, expectedRevision, eventData, + options => options.ThrowOnAppendFailure = false, deadline, userCredentials, cancellationToken) + .ConfigureAwait(false); + return ConditionalWriteResult.FromWriteResult(result); + } catch (StreamDeletedException) { + return ConditionalWriteResult.StreamDeleted; + } + } + + /// + /// Appends to a stream conditionally. + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public static async Task ConditionalAppendToStreamAsync( + this KurrentClient client, + string streamName, + StreamState expectedState, + IEnumerable eventData, + TimeSpan? deadline = null, + UserCredentials? userCredentials = null, + CancellationToken cancellationToken = default) { + if (client == null) { + throw new ArgumentNullException(nameof(client)); + } + try { + var result = await client.AppendToStreamAsync(streamName, expectedState, eventData, + options => options.ThrowOnAppendFailure = false, deadline, userCredentials, cancellationToken) + .ConfigureAwait(false); + return ConditionalWriteResult.FromWriteResult(result); + } catch (StreamDeletedException) { + return ConditionalWriteResult.StreamDeleted; + } catch (WrongExpectedVersionException ex) { + return ConditionalWriteResult.FromWrongExpectedVersion(ex); + } + } + } +} diff --git a/src/Kurrent.Client/Streams/KurrentClientServiceCollectionExtensions.cs b/src/Kurrent.Client/Streams/KurrentClientServiceCollectionExtensions.cs new file mode 100644 index 000000000..b2832fa56 --- /dev/null +++ b/src/Kurrent.Client/Streams/KurrentClientServiceCollectionExtensions.cs @@ -0,0 +1,153 @@ +// ReSharper disable CheckNamespace + +using System; +using System.Net.Http; +using EventStore.Client; +using Grpc.Core.Interceptors; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Logging; + +namespace Microsoft.Extensions.DependencyInjection { + /// + /// A set of extension methods for which provide support for an . + /// + public static class KurrentClientServiceCollectionExtensions { + /// + /// Adds an to the . + /// + /// + /// + /// + /// + /// + public static IServiceCollection AddKurrentClient(this IServiceCollection services, Uri address, + Func? createHttpMessageHandler = null) + => services.AddKurrentClient(options => { + options.ConnectivitySettings.Address = address; + options.CreateHttpMessageHandler = createHttpMessageHandler; + }); + + /// + /// Adds an to the . + /// + /// + /// + /// + /// + /// + public static IServiceCollection AddKurrentClient(this IServiceCollection services, + Func addressFactory, + Func? createHttpMessageHandler = null) + => services.AddKurrentClient(provider => options => { + options.ConnectivitySettings.Address = addressFactory(provider); + options.CreateHttpMessageHandler = createHttpMessageHandler; + }); + + /// + /// Adds an to the . + /// + /// + /// + /// + /// + public static IServiceCollection AddKurrentClient(this IServiceCollection services, + Action? configureSettings = null) => + services.AddKurrentClient(new KurrentClientSettings(), configureSettings); + + /// + /// Adds an to the . + /// + /// + /// + /// + /// + public static IServiceCollection AddKurrentClient(this IServiceCollection services, + Func> configureSettings) => + services.AddKurrentClient(new KurrentClientSettings(), + configureSettings); + + /// + /// Adds an to the . + /// + /// + /// + /// + /// + /// + public static IServiceCollection AddKurrentClient(this IServiceCollection services, + string connectionString, Action? configureSettings = null) { + if (services == null) { + throw new ArgumentNullException(nameof(services)); + } + + return services.AddKurrentClient(KurrentClientSettings.Create(connectionString), configureSettings); + } + + /// + /// Adds an to the . + /// + /// + /// + /// + /// + /// + public static IServiceCollection AddKurrentClient(this IServiceCollection services, + Func connectionStringFactory, + Action? configureSettings = null) { + if (services == null) { + throw new ArgumentNullException(nameof(services)); + } + + return services.AddKurrentClient(provider => KurrentClientSettings.Create(connectionStringFactory(provider)), configureSettings); + } + + private static IServiceCollection AddKurrentClient(this IServiceCollection services, + KurrentClientSettings settings, + Action? configureSettings) { + configureSettings?.Invoke(settings); + + services.TryAddSingleton(provider => { + settings.LoggerFactory ??= provider.GetService(); + settings.Interceptors ??= provider.GetServices(); + + return new KurrentClient(settings); + }); + + return services; + } + + private static IServiceCollection AddKurrentClient(this IServiceCollection services, + Func settingsFactory, + Action? configureSettings = null) { + + services.TryAddSingleton(provider => { + var settings = settingsFactory(provider); + configureSettings?.Invoke(settings); + + settings.LoggerFactory ??= provider.GetService(); + settings.Interceptors ??= provider.GetServices(); + + return new KurrentClient(settings); + }); + + return services; + } + + private static IServiceCollection AddKurrentClient(this IServiceCollection services, + KurrentClientSettings settings, + Func> configureSettingsFactory) { + + services.TryAddSingleton(provider => { + configureSettingsFactory(provider).Invoke(settings); + + settings.LoggerFactory ??= provider.GetService(); + settings.Interceptors ??= provider.GetServices(); + + return new KurrentClient(settings); + }); + + return services; + } + } +} +// ReSharper restore CheckNamespace diff --git a/src/Kurrent.Client/Streams/MaximumAppendSizeExceededException.cs b/src/Kurrent.Client/Streams/MaximumAppendSizeExceededException.cs new file mode 100644 index 000000000..7c785b951 --- /dev/null +++ b/src/Kurrent.Client/Streams/MaximumAppendSizeExceededException.cs @@ -0,0 +1,33 @@ +using System; + +namespace EventStore.Client { + /// + /// Exception thrown when an append exceeds the maximum size set by the server. + /// + public class MaximumAppendSizeExceededException : Exception { + /// + /// The configured maximum append size. + /// + public uint MaxAppendSize { get; } + + /// + /// Constructs a new . + /// + /// + /// + public MaximumAppendSizeExceededException(uint maxAppendSize, Exception? innerException = null) : + base($"Maximum Append Size of {maxAppendSize} Exceeded.", innerException) { + MaxAppendSize = maxAppendSize; + } + + /// + /// Constructs a new . + /// + /// + /// + public MaximumAppendSizeExceededException(int maxAppendSize, Exception? innerException = null) : this( + (uint)maxAppendSize, innerException) { + + } + } +} diff --git a/src/Kurrent.Client/Streams/ReadState.cs b/src/Kurrent.Client/Streams/ReadState.cs new file mode 100644 index 000000000..6f0497081 --- /dev/null +++ b/src/Kurrent.Client/Streams/ReadState.cs @@ -0,0 +1,15 @@ +namespace EventStore.Client { + /// + /// An enumeration representing the state of a read operation. + /// + public enum ReadState { + /// + /// The stream does not exist. + /// + StreamNotFound, + /// + /// The stream exists. + /// + Ok + } +} diff --git a/src/Kurrent.Client/Streams/StreamAcl.cs b/src/Kurrent.Client/Streams/StreamAcl.cs new file mode 100644 index 000000000..60f70a669 --- /dev/null +++ b/src/Kurrent.Client/Streams/StreamAcl.cs @@ -0,0 +1,107 @@ +using System; +using System.Linq; + +namespace EventStore.Client { + /// + /// Represents an access control list for a stream + /// + public sealed class StreamAcl { + /// + /// Roles and users permitted to read the stream + /// + public string[]? ReadRoles { get; } + + /// + /// Roles and users permitted to write to the stream + /// + public string[]? WriteRoles { get; } + + /// + /// Roles and users permitted to delete the stream + /// + public string[]? DeleteRoles { get; } + + /// + /// Roles and users permitted to read stream metadata + /// + public string[]? MetaReadRoles { get; } + + /// + /// Roles and users permitted to write stream metadata + /// + public string[]? MetaWriteRoles { get; } + + + /// + /// Creates a new Stream Access Control List + /// + /// Role and user permitted to read the stream + /// Role and user permitted to write to the stream + /// Role and user permitted to delete the stream + /// Role and user permitted to read stream metadata + /// Role and user permitted to write stream metadata + public StreamAcl(string? readRole = null, string? writeRole = null, string? deleteRole = null, + string? metaReadRole = null, string? metaWriteRole = null) + : this(readRole == null ? null : new[] {readRole}, + writeRole == null ? null : new[] {writeRole}, + deleteRole == null ? null : new[] {deleteRole}, + metaReadRole == null ? null : new[] {metaReadRole}, + metaWriteRole == null ? null : new[] {metaWriteRole}) { + } + + /// + /// + /// + /// Roles and users permitted to read the stream + /// Roles and users permitted to write to the stream + /// Roles and users permitted to delete the stream + /// Roles and users permitted to read stream metadata + /// Roles and users permitted to write stream metadata + public StreamAcl(string[]? readRoles = null, string[]? writeRoles = null, string[]? deleteRoles = null, + string[]? metaReadRoles = null, string[]? metaWriteRoles = null) { + ReadRoles = readRoles; + WriteRoles = writeRoles; + DeleteRoles = deleteRoles; + MetaReadRoles = metaReadRoles; + MetaWriteRoles = metaWriteRoles; + } + + private bool Equals(StreamAcl other) => + (ReadRoles ?? Array.Empty()).SequenceEqual(other.ReadRoles ?? Array.Empty()) && + (WriteRoles ?? Array.Empty()).SequenceEqual(other.WriteRoles ?? Array.Empty()) && + (DeleteRoles ?? Array.Empty()).SequenceEqual(other.DeleteRoles ?? Array.Empty()) && + (MetaReadRoles ?? Array.Empty()).SequenceEqual(other.MetaReadRoles ?? Array.Empty()) && + (MetaWriteRoles ?? Array.Empty()).SequenceEqual(other.MetaWriteRoles ?? Array.Empty()); + + /// + public override bool Equals(object? obj) => + !ReferenceEquals(null, obj) && + (ReferenceEquals(this, obj) || obj.GetType() == GetType() && Equals((StreamAcl)obj)); + + /// + /// Compares left and right for equality. + /// + /// + /// + /// True if left is equal to right. + public static bool operator ==(StreamAcl? left, StreamAcl? right) => Equals(left, right); + + /// + /// Compares left and right for inequality. + /// + /// + /// + /// True if left is not equal to right. + public static bool operator !=(StreamAcl? left, StreamAcl? right) => !Equals(left, right); + + /// + public override int GetHashCode() => + HashCode.Hash.Combine(ReadRoles).Combine(WriteRoles).Combine(DeleteRoles).Combine(MetaReadRoles) + .Combine(MetaWriteRoles); + + + /// + public override string ToString() => + $"Read: {(ReadRoles == null ? "" : "[" + string.Join(",", ReadRoles) + "]")}, Write: {(WriteRoles == null ? "" : "[" + string.Join(",", WriteRoles) + "]")}, Delete: {(DeleteRoles == null ? "" : "[" + string.Join(",", DeleteRoles) + "]")}, MetaRead: {(MetaReadRoles == null ? "" : "[" + string.Join(",", MetaReadRoles) + "]")}, MetaWrite: {(MetaWriteRoles == null ? "" : "[" + string.Join(",", MetaWriteRoles) + "]")}"; + } +} diff --git a/src/Kurrent.Client/Streams/StreamAclJsonConverter.cs b/src/Kurrent.Client/Streams/StreamAclJsonConverter.cs new file mode 100644 index 000000000..a487ddee7 --- /dev/null +++ b/src/Kurrent.Client/Streams/StreamAclJsonConverter.cs @@ -0,0 +1,111 @@ +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace EventStore.Client { + internal class StreamAclJsonConverter : JsonConverter { + public static readonly StreamAclJsonConverter Instance = new StreamAclJsonConverter(); + + public override StreamAcl Read(ref Utf8JsonReader reader, Type typeToConvert, + JsonSerializerOptions options) { + string[]? read = null, + write = default, + delete = default, + metaRead = default, + metaWrite = default; + if (reader.TokenType != JsonTokenType.StartObject) { + throw new InvalidOperationException(); + } + + while (reader.Read()) { + if (reader.TokenType == JsonTokenType.EndObject) { + break; + } + + if (reader.TokenType != JsonTokenType.PropertyName) { + throw new InvalidOperationException(); + } + + switch (reader.GetString()) { + case SystemMetadata.AclRead: + read = ReadRoles(ref reader); + break; + case SystemMetadata.AclWrite: + write = ReadRoles(ref reader); + break; + case SystemMetadata.AclDelete: + delete = ReadRoles(ref reader); + break; + case SystemMetadata.AclMetaRead: + metaRead = ReadRoles(ref reader); + break; + case SystemMetadata.AclMetaWrite: + metaWrite = ReadRoles(ref reader); + break; + } + } + + return new StreamAcl(read, write, delete, metaRead, metaWrite); + } + + private static string[]? ReadRoles(ref Utf8JsonReader reader) { + if (!reader.Read()) { + throw new InvalidOperationException(); + } + + if (reader.TokenType == JsonTokenType.Null) { + return null; + } + + if (reader.TokenType == JsonTokenType.String) { + return new[] {reader.GetString()!}; + } + + if (reader.TokenType != JsonTokenType.StartArray) { + throw new InvalidOperationException(); + } + + var roles = new List(); + + while (reader.Read()) { + if (reader.TokenType == JsonTokenType.EndArray) { + return roles.Count == 0 ? Array.Empty() : roles.ToArray(); + } + + if (reader.TokenType != JsonTokenType.String) { + throw new InvalidOperationException(); + } + + roles.Add(reader.GetString()!); + } + + return roles.ToArray(); + } + + public override void Write(Utf8JsonWriter writer, StreamAcl value, JsonSerializerOptions options) { + writer.WriteStartObject(); + + WriteRoles(writer, SystemMetadata.AclRead, value.ReadRoles); + WriteRoles(writer, SystemMetadata.AclWrite, value.WriteRoles); + WriteRoles(writer, SystemMetadata.AclDelete, value.DeleteRoles); + WriteRoles(writer, SystemMetadata.AclMetaRead, value.MetaReadRoles); + WriteRoles(writer, SystemMetadata.AclMetaWrite, value.MetaWriteRoles); + + writer.WriteEndObject(); + } + + private static void WriteRoles(Utf8JsonWriter writer, string name, string[]? roles) { + if (roles == null) { + return; + } + writer.WritePropertyName(name); + writer.WriteStartArray(); + foreach (var role in roles) { + writer.WriteStringValue(role); + } + + writer.WriteEndArray(); + } + } +} diff --git a/src/Kurrent.Client/Streams/StreamMessage.cs b/src/Kurrent.Client/Streams/StreamMessage.cs new file mode 100644 index 000000000..54aaffad9 --- /dev/null +++ b/src/Kurrent.Client/Streams/StreamMessage.cs @@ -0,0 +1,83 @@ +namespace EventStore.Client { + /// + /// The base record of all stream messages. + /// + public abstract record StreamMessage { + /// + /// A that represents a . + /// + /// The . + public record Event(ResolvedEvent ResolvedEvent) : StreamMessage; + + /// + /// A representing a stream that was not found. + /// + public record NotFound : StreamMessage { + internal static readonly NotFound Instance = new(); + } + + /// + /// A representing a successful read operation. + /// + public record Ok : StreamMessage { + internal static readonly Ok Instance = new(); + }; + + /// + /// A indicating the first position of a stream. + /// + /// The . + public record FirstStreamPosition(StreamPosition StreamPosition) : StreamMessage; + + /// + /// A indicating the last position of a stream. + /// + /// The . + public record LastStreamPosition(StreamPosition StreamPosition) : StreamMessage; + + /// + /// A indicating the last position of the $all stream. + /// + /// The . + public record LastAllStreamPosition(Position Position) : StreamMessage; + + /// + /// A indicating that the subscription is ready to send additional messages. + /// + /// The unique identifier of the subscription. + public record SubscriptionConfirmation(string SubscriptionId) : StreamMessage; + + /// + /// A indicating that a checkpoint has been reached. + /// + /// The . + public record AllStreamCheckpointReached(Position Position) : StreamMessage; + + /// + /// A indicating that a checkpoint has been reached. + /// + /// The . + public record StreamCheckpointReached(StreamPosition StreamPosition) : StreamMessage; + + /// + /// A indicating that the subscription is live. + /// + public record CaughtUp : StreamMessage { + internal static readonly CaughtUp Instance = new(); + } + + /// + /// A indicating that the subscription has switched to catch up mode. + /// + public record FellBehind : StreamMessage { + internal static readonly FellBehind Instance = new(); + } + + /// + /// A that could not be identified, usually indicating a lower client compatibility level than the server supports. + /// + public record Unknown : StreamMessage { + internal static readonly Unknown Instance = new(); + } + } +} diff --git a/src/Kurrent.Client/Streams/StreamMetadata.cs b/src/Kurrent.Client/Streams/StreamMetadata.cs new file mode 100644 index 000000000..2b53dd977 --- /dev/null +++ b/src/Kurrent.Client/Streams/StreamMetadata.cs @@ -0,0 +1,111 @@ +using System; +using System.Text.Json; + +namespace EventStore.Client { + /// + /// A structure representing a stream's custom metadata with strongly typed properties + /// for system values and a dictionary-like interface for custom values. + /// + public readonly struct StreamMetadata : IEquatable { + /// + /// The optional maximum age of events allowed in the stream. + /// + public TimeSpan? MaxAge { get; } + + /// + /// The optional from which previous events can be scavenged. + /// This is used to implement soft-deletion of streams. + /// + + public StreamPosition? TruncateBefore { get; } + + /// + /// The optional amount of time for which the stream head is cacheable. + /// + public TimeSpan? CacheControl { get; } + + /// + /// The optional for the stream. + /// + public StreamAcl? Acl { get; } + + /// + /// The optional maximum number of events allowed in the stream. + /// + public int? MaxCount { get; } + + /// + /// The optional of user provided metadata. + /// + public JsonDocument? CustomMetadata { get; } + + /// + /// Constructs a new . + /// + /// + /// + /// + /// + /// + /// + /// + public StreamMetadata( + int? maxCount = null, + TimeSpan? maxAge = null, + StreamPosition? truncateBefore = null, + TimeSpan? cacheControl = null, + StreamAcl? acl = null, + JsonDocument? customMetadata = null) : this() { + if (maxCount <= 0) { + throw new ArgumentOutOfRangeException(nameof(maxCount)); + } + + if (maxAge <= TimeSpan.Zero) { + throw new ArgumentOutOfRangeException(nameof(maxAge)); + } + + if (cacheControl <= TimeSpan.Zero) { + throw new ArgumentOutOfRangeException(nameof(cacheControl)); + } + + MaxAge = maxAge; + TruncateBefore = truncateBefore; + CacheControl = cacheControl; + Acl = acl; + MaxCount = maxCount; + CustomMetadata = customMetadata ?? JsonDocument.Parse("{}"); + } + + /// + public bool Equals(StreamMetadata other) => Nullable.Equals(MaxAge, other.MaxAge) && + Nullable.Equals(TruncateBefore, other.TruncateBefore) && + Nullable.Equals(CacheControl, other.CacheControl) && + Equals(Acl, other.Acl) && MaxCount == other.MaxCount && + string.Equals( + CustomMetadata?.RootElement.GetRawText(), + other.CustomMetadata?.RootElement.GetRawText()); + + /// + public override bool Equals(object? obj) => obj is StreamMetadata other && other.Equals(this); + + /// + public override int GetHashCode() => HashCode.Hash.Combine(MaxAge).Combine(TruncateBefore).Combine(CacheControl) + .Combine(Acl?.GetHashCode()).Combine(MaxCount); + + /// + /// Compares left and right for equality. + /// + /// + /// + /// True if left is equal to right. + public static bool operator ==(StreamMetadata left, StreamMetadata right) => Equals(left, right); + + /// + /// Compares left and right for inequality. + /// + /// + /// + /// True if left is not equal to right. + public static bool operator !=(StreamMetadata left, StreamMetadata right) => !Equals(left, right); + } +} diff --git a/src/Kurrent.Client/Streams/StreamMetadataJsonConverter.cs b/src/Kurrent.Client/Streams/StreamMetadataJsonConverter.cs new file mode 100644 index 000000000..68757a27b --- /dev/null +++ b/src/Kurrent.Client/Streams/StreamMetadataJsonConverter.cs @@ -0,0 +1,145 @@ +using System; +using System.IO; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace EventStore.Client { + internal class StreamMetadataJsonConverter : JsonConverter { + public static readonly StreamMetadataJsonConverter Instance = new StreamMetadataJsonConverter(); + + public override StreamMetadata Read(ref Utf8JsonReader reader, Type typeToConvert, + JsonSerializerOptions options) { + int? maxCount = null; + TimeSpan? maxAge = null, cacheControl = null; + StreamPosition? truncateBefore = null; + StreamAcl? acl = null; + using var stream = new MemoryStream(); + using var customMetadataWriter = new Utf8JsonWriter(stream); + + if (reader.TokenType != JsonTokenType.StartObject) { + throw new InvalidOperationException(); + } + + customMetadataWriter.WriteStartObject(); + + while (reader.Read()) { + if (reader.TokenType == JsonTokenType.EndObject) { + break; + } + + if (reader.TokenType != JsonTokenType.PropertyName) { + throw new InvalidOperationException(); + } + + switch (reader.GetString()) { + case SystemMetadata.MaxCount: + if (!reader.Read()) { + throw new InvalidOperationException(); + } + + maxCount = reader.GetInt32(); + break; + case SystemMetadata.MaxAge: + if (!reader.Read()) { + throw new InvalidOperationException(); + } + + var int64 = reader.GetInt64(); + maxAge = TimeSpan.FromSeconds(int64); + break; + case SystemMetadata.CacheControl: + if (!reader.Read()) { + throw new InvalidOperationException(); + } + + cacheControl = TimeSpan.FromSeconds(reader.GetInt64()); + break; + case SystemMetadata.TruncateBefore: + if (!reader.Read()) { + throw new InvalidOperationException(); + } + + var value = reader.GetInt64(); + truncateBefore = value == long.MaxValue + ? StreamPosition.End + : StreamPosition.FromInt64(value); + break; + case SystemMetadata.Acl: + if (!reader.Read()) { + throw new InvalidOperationException(); + } + + acl = StreamAclJsonConverter.Instance.Read(ref reader, typeof(StreamAcl), options); + break; + default: + customMetadataWriter.WritePropertyName(reader.GetString()!); + reader.Read(); + switch (reader.TokenType) { + case JsonTokenType.Comment: + customMetadataWriter.WriteCommentValue(reader.GetComment()); + break; + case JsonTokenType.String: + customMetadataWriter.WriteStringValue(reader.GetString()); + break; + case JsonTokenType.Number: + customMetadataWriter.WriteNumberValue(reader.GetDouble()); + break; + case JsonTokenType.True: + case JsonTokenType.False: + customMetadataWriter.WriteBooleanValue(reader.GetBoolean()); + break; + case JsonTokenType.Null: + reader.Read(); + customMetadataWriter.WriteNullValue(); + break; + default: + throw new ArgumentOutOfRangeException(); + } + + break; + } + } + + customMetadataWriter.WriteEndObject(); + customMetadataWriter.Flush(); + + stream.Position = 0; + + return new StreamMetadata(maxCount, maxAge, truncateBefore, cacheControl, acl, + JsonDocument.Parse(stream)); + } + + public override void Write(Utf8JsonWriter writer, StreamMetadata value, JsonSerializerOptions options) { + writer.WriteStartObject(); + + if (value.MaxCount.HasValue) { + writer.WriteNumber(SystemMetadata.MaxCount, value.MaxCount.Value); + } + + if (value.MaxAge.HasValue) { + writer.WriteNumber(SystemMetadata.MaxAge, (long)value.MaxAge.Value.TotalSeconds); + } + + if (value.TruncateBefore.HasValue) { + writer.WriteNumber(SystemMetadata.TruncateBefore, value.TruncateBefore.Value.ToInt64()); + } + + if (value.CacheControl.HasValue) { + writer.WriteNumber(SystemMetadata.CacheControl, (long)value.CacheControl.Value.TotalSeconds); + } + + if (value.Acl != null) { + writer.WritePropertyName(SystemMetadata.Acl); + StreamAclJsonConverter.Instance.Write(writer, value.Acl, options); + } + + if (value.CustomMetadata != null) { + foreach (var property in value.CustomMetadata.RootElement.EnumerateObject()) { + property.WriteTo(writer); + } + } + + writer.WriteEndObject(); + } + } +} diff --git a/src/Kurrent.Client/Streams/StreamMetadataResult.cs b/src/Kurrent.Client/Streams/StreamMetadataResult.cs new file mode 100644 index 000000000..080ba15ef --- /dev/null +++ b/src/Kurrent.Client/Streams/StreamMetadataResult.cs @@ -0,0 +1,83 @@ +using System; + +namespace EventStore.Client { + /// + /// Represents stream metadata as a series of properties for system + /// data (e.g., MaxAge) and a object for user metadata. + /// + public struct StreamMetadataResult : IEquatable { + /// + /// The name of the stream. + /// + public readonly string StreamName; + + /// + /// True if the stream is deleted. + /// + public readonly bool StreamDeleted; + + /// + /// A containing user-specified metadata. + /// + public readonly StreamMetadata Metadata; + + /// + /// A of the version of the metadata. + /// + public readonly StreamPosition? MetastreamRevision; + + /// + public override int GetHashCode() => + HashCode.Hash.Combine(StreamName).Combine(Metadata).Combine(MetastreamRevision); + + /// + public bool Equals(StreamMetadataResult other) => + StreamName == other.StreamName && StreamDeleted == other.StreamDeleted && + Equals(Metadata, other.Metadata) && Nullable.Equals(MetastreamRevision, other.MetastreamRevision); + + /// + public override bool Equals(object? obj) => obj is StreamMetadataResult other && Equals(other); + + /// + /// Compares left and right for equality. + /// + /// + /// + /// True if left is equal to right. + public static bool operator ==(StreamMetadataResult left, StreamMetadataResult right) => left.Equals(right); + + /// + /// Compares left and right for inequality. + /// + /// + /// + /// True if left is not equal to right. + public static bool operator !=(StreamMetadataResult left, StreamMetadataResult right) => !left.Equals(right); + + /// + /// A representing no metadata. + /// + /// + /// + public static StreamMetadataResult None(string streamName) => new StreamMetadataResult(streamName); + + /// + /// A factory method to create a new . + /// + /// + /// + /// + /// + /// + public static StreamMetadataResult Create(string streamName, StreamPosition revision, + StreamMetadata metadata) => new StreamMetadataResult(streamName, revision, metadata); + + private StreamMetadataResult(string streamName, StreamPosition? metastreamRevision = null, + StreamMetadata metadata = default, bool streamDeleted = false) { + StreamName = streamName; + StreamDeleted = streamDeleted; + Metadata = metadata; + MetastreamRevision = metastreamRevision; + } + } +} diff --git a/src/Kurrent.Client/Streams/StreamSubscription.cs b/src/Kurrent.Client/Streams/StreamSubscription.cs new file mode 100644 index 000000000..e7271b364 --- /dev/null +++ b/src/Kurrent.Client/Streams/StreamSubscription.cs @@ -0,0 +1,165 @@ +using Grpc.Core; +using Microsoft.Extensions.Logging; + +namespace EventStore.Client { + /// + /// A class representing a . + /// + public class StreamSubscription : IDisposable { + private readonly KurrentClient.StreamSubscriptionResult _subscription; + private readonly IAsyncEnumerator _messages; + private readonly Func _eventAppeared; + private readonly Func _checkpointReached; + private readonly Action? _subscriptionDropped; + private readonly ILogger _log; + private readonly CancellationTokenSource _cts; + private int _subscriptionDroppedInvoked; + + /// + /// The id of the set by the server. + /// + public string SubscriptionId { get; } + + internal static async Task Confirm( + KurrentClient.StreamSubscriptionResult subscription, + Func eventAppeared, + Action? subscriptionDropped, + ILogger log, + Func? checkpointReached = null, + CancellationToken cancellationToken = default + ) { + var messages = subscription.Messages; + + var enumerator = messages.GetAsyncEnumerator(cancellationToken); + if (!await enumerator.MoveNextAsync().ConfigureAwait(false) || + enumerator.Current is not StreamMessage.SubscriptionConfirmation(var subscriptionId)) { + throw new InvalidOperationException($"Subscription to {enumerator} could not be confirmed."); + } + + return new StreamSubscription( + subscription, + enumerator, + subscriptionId, + eventAppeared, + subscriptionDropped, + log, + checkpointReached, + cancellationToken + ); + } + + private StreamSubscription( + KurrentClient.StreamSubscriptionResult subscription, + IAsyncEnumerator messages, string subscriptionId, + Func eventAppeared, + Action? subscriptionDropped, + ILogger log, + Func? checkpointReached, + CancellationToken cancellationToken = default + ) { + _cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + _subscription = subscription; + _messages = messages; + _eventAppeared = eventAppeared; + _checkpointReached = checkpointReached ?? ((_, _, _) => Task.CompletedTask); + _subscriptionDropped = subscriptionDropped; + _log = log; + _subscriptionDroppedInvoked = 0; + SubscriptionId = subscriptionId; + + _log.LogDebug("Subscription {subscriptionId} confirmed.", SubscriptionId); + + Task.Run(Subscribe, cancellationToken); + } + + private async Task Subscribe() { + using var _ = _cts; + + try { + while (await _messages.MoveNextAsync().ConfigureAwait(false)) { + var message = _messages.Current; + try { + switch (message) { + case StreamMessage.Event(var resolvedEvent): + _log.LogTrace( + "Subscription {subscriptionId} received event {streamName}@{streamRevision} {position}", + SubscriptionId, + resolvedEvent.OriginalEvent.EventStreamId, + resolvedEvent.OriginalEvent.EventNumber, + resolvedEvent.OriginalEvent.Position + ); + + await _eventAppeared(this, resolvedEvent, _cts.Token).ConfigureAwait(false); + break; + + case StreamMessage.AllStreamCheckpointReached (var position): + await _checkpointReached(this, position, _cts.Token) + .ConfigureAwait(false); + + break; + } + } catch (Exception ex) when + (ex is ObjectDisposedException or OperationCanceledException) { + if (_subscriptionDroppedInvoked != 0) { + return; + } + + _log.LogWarning( + ex, + "Subscription {subscriptionId} was dropped because cancellation was requested by another caller.", + SubscriptionId + ); + + SubscriptionDropped(SubscriptionDroppedReason.Disposed); + + return; + } catch (Exception ex) { + _log.LogError( + ex, + "Subscription {subscriptionId} was dropped because the subscriber made an error.", + SubscriptionId + ); + + SubscriptionDropped(SubscriptionDroppedReason.SubscriberError, ex); + + return; + } + } + } catch (RpcException ex) when (ex.Status.StatusCode == StatusCode.Cancelled && + ex.Status.Detail.Contains("Call canceled by the client.")) { + _log.LogInformation( + "Subscription {subscriptionId} was dropped because cancellation was requested by the client.", + SubscriptionId + ); + + SubscriptionDropped(SubscriptionDroppedReason.Disposed, ex); + } catch (Exception ex) { + if (_subscriptionDroppedInvoked == 0) { + _log.LogError( + ex, + "Subscription {subscriptionId} was dropped because an error occurred on the server.", + SubscriptionId + ); + + SubscriptionDropped(SubscriptionDroppedReason.ServerError, ex); + } + } + } + + /// + public void Dispose() => SubscriptionDropped(SubscriptionDroppedReason.Disposed); + + private void SubscriptionDropped(SubscriptionDroppedReason reason, Exception? ex = null) { + if (Interlocked.CompareExchange(ref _subscriptionDroppedInvoked, 1, 0) == 1) { + return; + } + + try { + _subscriptionDropped?.Invoke(this, reason, ex); + } finally { + _subscription.Dispose(); + _cts.Dispose(); + } + } + } +} diff --git a/src/Kurrent.Client/Streams/Streams/AppendReq.cs b/src/Kurrent.Client/Streams/Streams/AppendReq.cs new file mode 100644 index 000000000..f32611530 --- /dev/null +++ b/src/Kurrent.Client/Streams/Streams/AppendReq.cs @@ -0,0 +1,15 @@ +namespace EventStore.Client.Streams { + partial class AppendReq { + public AppendReq WithAnyStreamRevision(StreamState expectedState) { + if (expectedState == StreamState.Any) { + Options.Any = new Empty(); + } else if (expectedState == StreamState.NoStream) { + Options.NoStream = new Empty(); + } else if (expectedState == StreamState.StreamExists) { + Options.StreamExists = new Empty(); + } + + return this; + } + } +} diff --git a/src/Kurrent.Client/Streams/Streams/BatchAppendReq.cs b/src/Kurrent.Client/Streams/Streams/BatchAppendReq.cs new file mode 100644 index 000000000..f9e1146c0 --- /dev/null +++ b/src/Kurrent.Client/Streams/Streams/BatchAppendReq.cs @@ -0,0 +1,34 @@ +using System; +using Google.Protobuf.WellKnownTypes; + +namespace EventStore.Client.Streams { + partial class BatchAppendReq { + partial class Types { + partial class Options { + public static Options Create(StreamIdentifier streamIdentifier, + StreamRevision expectedStreamRevision, TimeSpan? timeoutAfter) => new() { + StreamIdentifier = streamIdentifier, + StreamPosition = expectedStreamRevision.ToUInt64(), + Deadline21100 = Timestamp.FromDateTime(timeoutAfter.HasValue + ? DateTime.UtcNow + timeoutAfter.Value + : DateTime.SpecifyKind(DateTime.MaxValue, DateTimeKind.Utc)) + }; + public static Options Create(StreamIdentifier streamIdentifier, StreamState expectedState, + TimeSpan? timeoutAfter) => new() { + StreamIdentifier = streamIdentifier, + expectedStreamPositionCase_ = expectedState switch { + { } when expectedState == StreamState.Any => ExpectedStreamPositionOneofCase.Any, + { } when expectedState == StreamState.NoStream => ExpectedStreamPositionOneofCase.NoStream, + { } when expectedState == StreamState.StreamExists => ExpectedStreamPositionOneofCase + .StreamExists, + _ => ExpectedStreamPositionOneofCase.None + }, + expectedStreamPosition_ = new Google.Protobuf.WellKnownTypes.Empty(), + Deadline21100 = Timestamp.FromDateTime(timeoutAfter.HasValue + ? DateTime.UtcNow + timeoutAfter.Value + : DateTime.SpecifyKind(DateTime.MaxValue, DateTimeKind.Utc)) + }; + } + } + } +} diff --git a/src/Kurrent.Client/Streams/Streams/BatchAppendResp.cs b/src/Kurrent.Client/Streams/Streams/BatchAppendResp.cs new file mode 100644 index 000000000..4be031057 --- /dev/null +++ b/src/Kurrent.Client/Streams/Streams/BatchAppendResp.cs @@ -0,0 +1,49 @@ +using Grpc.Core; +using EventStore.Client; +using static EventStore.Client.WrongExpectedVersion.CurrentStreamRevisionOptionOneofCase; +using static EventStore.Client.WrongExpectedVersion.ExpectedStreamPositionOptionOneofCase; + +namespace EventStore.Client.Streams { + partial class BatchAppendResp { + public IWriteResult ToWriteResult() => ResultCase switch { + ResultOneofCase.Success => new SuccessResult( + Success.CurrentRevisionOptionCase switch { + Types.Success.CurrentRevisionOptionOneofCase.CurrentRevision => + new StreamRevision(Success.CurrentRevision), + _ => StreamRevision.None + }, Success.PositionOptionCase switch { + Types.Success.PositionOptionOneofCase.Position => new Position( + Success.Position.CommitPosition, + Success.Position.PreparePosition), + _ => Position.End + }), + ResultOneofCase.Error => Error.Details switch { + { } when Error.Details.Is(WrongExpectedVersion.Descriptor) => + FromWrongExpectedVersion(StreamIdentifier, Error.Details.Unpack()), + { } when Error.Details.Is(StreamDeleted.Descriptor) => + throw new StreamDeletedException(StreamIdentifier!), + { } when Error.Details.Is(AccessDenied.Descriptor) => throw new AccessDeniedException(), + { } when Error.Details.Is(Timeout.Descriptor) => throw new RpcException( + new Status(StatusCode.DeadlineExceeded, Error.Message)), + { } when Error.Details.Is(Unknown.Descriptor) => throw new InvalidOperationException(Error.Message), + { } when Error.Details.Is(MaximumAppendSizeExceeded.Descriptor) => + throw new MaximumAppendSizeExceededException( + Error.Details.Unpack().MaxAppendSize), + { } when Error.Details.Is(BadRequest.Descriptor) => throw new InvalidOperationException(Error.Details + .Unpack().Message), + _ => throw new InvalidOperationException($"Could not recognize {Error.Message}") + }, + _ => throw new InvalidOperationException() + }; + + private static WrongExpectedVersionResult FromWrongExpectedVersion(StreamIdentifier streamIdentifier, + WrongExpectedVersion wrongExpectedVersion) => new(streamIdentifier!, + wrongExpectedVersion.ExpectedStreamPositionOptionCase switch { + ExpectedStreamPosition => wrongExpectedVersion.ExpectedStreamPosition, + _ => StreamRevision.None + }, wrongExpectedVersion.CurrentStreamRevisionOptionCase switch { + CurrentStreamRevision => wrongExpectedVersion.CurrentStreamRevision, + _ => StreamRevision.None + }); + } +} diff --git a/src/Kurrent.Client/Streams/Streams/DeleteReq.cs b/src/Kurrent.Client/Streams/Streams/DeleteReq.cs new file mode 100644 index 000000000..94600138c --- /dev/null +++ b/src/Kurrent.Client/Streams/Streams/DeleteReq.cs @@ -0,0 +1,15 @@ +namespace EventStore.Client.Streams { + partial class DeleteReq { + public DeleteReq WithAnyStreamRevision(StreamState expectedState) { + if (expectedState == StreamState.Any) { + Options.Any = new Empty(); + } else if (expectedState == StreamState.NoStream) { + Options.NoStream = new Empty(); + } else if (expectedState == StreamState.StreamExists) { + Options.StreamExists = new Empty(); + } + + return this; + } + } +} diff --git a/src/Kurrent.Client/Streams/Streams/ReadReq.cs b/src/Kurrent.Client/Streams/Streams/ReadReq.cs new file mode 100644 index 000000000..ed5ca3af7 --- /dev/null +++ b/src/Kurrent.Client/Streams/Streams/ReadReq.cs @@ -0,0 +1,86 @@ +using System; + +namespace EventStore.Client.Streams { + partial class ReadReq { + partial class Types { + partial class Options { + partial class Types { + partial class StreamOptions { + public static StreamOptions FromSubscriptionPosition(string streamName, + FromStream fromStream) { + if (fromStream == FromStream.End) { + return new StreamOptions { + StreamIdentifier = streamName, + End = new Empty() + }; + } + + if (fromStream == FromStream.Start) { + return new StreamOptions { + StreamIdentifier = streamName, + Start = new Empty() + }; + } + + return new StreamOptions { + StreamIdentifier = streamName, + Revision = fromStream.ToUInt64() + }; + } + public static StreamOptions FromStreamNameAndRevision( + string streamName, + StreamPosition streamRevision) { + if (streamName == null) { + throw new ArgumentNullException(nameof(streamName)); + } + + if (streamRevision == StreamPosition.End) { + return new StreamOptions { + StreamIdentifier = streamName, + End = new Empty() + }; + } + + if (streamRevision == StreamPosition.Start) { + return new StreamOptions { + StreamIdentifier = streamName, + Start = new Empty() + }; + } + + return new StreamOptions { + StreamIdentifier = streamName, + Revision = streamRevision + }; + } + } + + partial class AllOptions { + public static AllOptions FromSubscriptionPosition(FromAll position) { + if (position == FromAll.End) { + return new AllOptions { + End = new Empty() + }; + } + + if (position == FromAll.Start) { + return new AllOptions { + Start = new Empty() + }; + } + + var (c, p) = position.ToUInt64(); + + return new AllOptions { + Position = new Position { + CommitPosition = c, + PreparePosition = p + } + }; + } + } + } + } + } + } +} diff --git a/src/Kurrent.Client/Streams/Streams/TombstoneReq.cs b/src/Kurrent.Client/Streams/Streams/TombstoneReq.cs new file mode 100644 index 000000000..cbe1ef0a0 --- /dev/null +++ b/src/Kurrent.Client/Streams/Streams/TombstoneReq.cs @@ -0,0 +1,15 @@ +namespace EventStore.Client.Streams { + partial class TombstoneReq { + public TombstoneReq WithAnyStreamRevision(StreamState expectedState) { + if (expectedState == StreamState.Any) { + Options.Any = new Empty(); + } else if (expectedState == StreamState.NoStream) { + Options.NoStream = new Empty(); + } else if (expectedState == StreamState.StreamExists) { + Options.StreamExists = new Empty(); + } + + return this; + } + } +} diff --git a/src/Kurrent.Client/Streams/SubscriptionFilterOptions.cs b/src/Kurrent.Client/Streams/SubscriptionFilterOptions.cs new file mode 100644 index 000000000..b6eb22dd0 --- /dev/null +++ b/src/Kurrent.Client/Streams/SubscriptionFilterOptions.cs @@ -0,0 +1,54 @@ +namespace EventStore.Client { + /// + /// A class representing the options to use when filtering read operations. + /// + public class SubscriptionFilterOptions { + /// + /// The to apply. + /// + public IEventFilter Filter { get; } + + /// + /// Sets how often the checkpointReached callback is called. + /// + public uint CheckpointInterval { get; } + + /// + /// A Task invoked and await when a checkpoint is reached. + /// Set the checkpointInterval to define how often this method is called. + /// + public Func CheckpointReached { get; } = null!; + + /// + /// + /// + /// The to apply. + /// Sets how often the checkpointReached callback is called. + /// + /// A Task invoked and await when a checkpoint is reached. + /// Set the checkpointInterval to define how often this method is called. + /// + /// + public SubscriptionFilterOptions(IEventFilter filter, uint checkpointInterval, + Func? checkpointReached) + : this(filter, checkpointInterval) { + CheckpointReached = checkpointReached ?? ((_, __, ct) => Task.CompletedTask); + } + + /// + /// + /// + /// The to apply. + /// Sets how often the checkpointReached callback is called. + /// + public SubscriptionFilterOptions(IEventFilter filter, uint checkpointInterval = 1) { + if (checkpointInterval == 0) { + throw new ArgumentOutOfRangeException(nameof(checkpointInterval), + checkpointInterval, $"{nameof(checkpointInterval)} must be greater than 0."); + } + + Filter = filter; + CheckpointInterval = checkpointInterval; + } + } +} diff --git a/src/Kurrent.Client/Streams/SuccessResult.cs b/src/Kurrent.Client/Streams/SuccessResult.cs new file mode 100644 index 000000000..0624d6fcc --- /dev/null +++ b/src/Kurrent.Client/Streams/SuccessResult.cs @@ -0,0 +1,57 @@ +using System; + +namespace EventStore.Client { + /// + /// An that indicates a successful append to a stream. + /// + public readonly struct SuccessResult : IWriteResult, IEquatable { + /// + public long NextExpectedVersion { get; } + + /// + public Position LogPosition { get; } + + /// + public StreamRevision NextExpectedStreamRevision { get; } + + /// + /// Constructs a new . + /// + /// + /// + public SuccessResult(StreamRevision nextExpectedStreamRevision, Position logPosition) { + NextExpectedStreamRevision = nextExpectedStreamRevision; + LogPosition = logPosition; + NextExpectedVersion = nextExpectedStreamRevision.ToInt64(); + } + + /// + public bool Equals(SuccessResult other) => + NextExpectedStreamRevision == other.NextExpectedStreamRevision && LogPosition.Equals(other.LogPosition); + + /// + public override bool Equals(object? obj) => obj is SuccessResult other && Equals(other); + + /// + /// Compares left and right for equality. + /// + /// + /// + /// True if left is equal to right. + public static bool operator ==(SuccessResult left, SuccessResult right) => left.Equals(right); + + /// + /// Compares left and right for inequality. + /// + /// + /// + /// True if left is equal not to right. + public static bool operator !=(SuccessResult left, SuccessResult right) => !left.Equals(right); + + /// + public override int GetHashCode() => HashCode.Hash.Combine(NextExpectedVersion).Combine(LogPosition); + + /// + public override string ToString() => $"{NextExpectedStreamRevision}:{LogPosition}"; + } +} diff --git a/src/Kurrent.Client/Streams/SystemEventTypes.cs b/src/Kurrent.Client/Streams/SystemEventTypes.cs new file mode 100644 index 000000000..48e585160 --- /dev/null +++ b/src/Kurrent.Client/Streams/SystemEventTypes.cs @@ -0,0 +1,31 @@ +namespace EventStore.Client { + /// + ///Constants for System event types + /// + public static class SystemEventTypes { + /// + /// event type for stream deleted + /// + public const string StreamDeleted = "$streamDeleted"; + + /// + /// event type for statistics + /// + public const string StatsCollection = "$statsCollected"; + + /// + /// event type for linkTo + /// + public const string LinkTo = "$>"; + + /// + /// event type for stream metadata + /// + public const string StreamMetadata = "$metadata"; + + /// + /// event type for the system settings + /// + public const string Settings = "$settings"; + } +} diff --git a/src/Kurrent.Client/Streams/SystemMetadata.cs b/src/Kurrent.Client/Streams/SystemMetadata.cs new file mode 100644 index 000000000..7cce81c90 --- /dev/null +++ b/src/Kurrent.Client/Streams/SystemMetadata.cs @@ -0,0 +1,71 @@ +namespace EventStore.Client { + /// + ///Constants for information in stream metadata + /// + internal static class SystemMetadata { + /// + ///The definition of the MaxAge value assigned to stream metadata + ///Setting this allows all events older than the limit to be deleted + /// + public const string MaxAge = "$maxAge"; + + /// + ///The definition of the MaxCount value assigned to stream metadata + ///setting this allows all events with a sequence less than current -maxcount to be deleted + /// + public const string MaxCount = "$maxCount"; + + /// + ///The definition of the Truncate Before value assigned to stream metadata + ///setting this allows all events prior to the integer value to be deleted + /// + public const string TruncateBefore = "$tb"; + + /// + /// Sets the cache control in seconds for the head of the stream. + /// + public const string CacheControl = "$cacheControl"; + + + /// + /// The acl definition in metadata + /// + public const string Acl = "$acl"; + + /// + /// to read from a stream + /// + public const string AclRead = "$r"; + + /// + /// to write to a stream + /// + public const string AclWrite = "$w"; + + /// + /// to delete a stream + /// + public const string AclDelete = "$d"; + + /// + /// to read metadata + /// + public const string AclMetaRead = "$mr"; + + /// + /// to write metadata + /// + public const string AclMetaWrite = "$mw"; + + + /// + /// The user default acl stream + /// + public const string UserStreamAcl = "$userStreamAcl"; + + /// + /// the system stream defaults acl stream + /// + public const string SystemStreamAcl = "$systemStreamAcl"; + } +} diff --git a/src/Kurrent.Client/Streams/SystemSettings.cs b/src/Kurrent.Client/Streams/SystemSettings.cs new file mode 100644 index 000000000..37ccd661e --- /dev/null +++ b/src/Kurrent.Client/Streams/SystemSettings.cs @@ -0,0 +1,53 @@ +namespace EventStore.Client { + /// + /// A class representing default access control lists. + /// + public sealed class SystemSettings { + /// + /// Default access control list for new user streams. + /// + public StreamAcl? UserStreamAcl { get; } + + /// + /// Default access control list for new system streams. + /// + public StreamAcl? SystemStreamAcl { get; } + + /// + /// Constructs a new . + /// + /// + /// + public SystemSettings(StreamAcl? userStreamAcl = null, StreamAcl? systemStreamAcl = null) { + UserStreamAcl = userStreamAcl; + SystemStreamAcl = systemStreamAcl; + } + + private bool Equals(SystemSettings other) + => Equals(UserStreamAcl, other.UserStreamAcl) && Equals(SystemStreamAcl, other.SystemStreamAcl); + + /// + public override bool Equals(object? obj) + => ReferenceEquals(this, obj) || obj is SystemSettings other && Equals(other); + + /// + /// Compares left and right for equality. + /// + /// + /// + /// True if left is equal to right. + public static bool operator ==(SystemSettings? left, SystemSettings? right) => Equals(left, right); + + /// + /// Compares left and right for inequality. + /// + /// + /// + /// True if left is not equal to right. + public static bool operator !=(SystemSettings? left, SystemSettings? right) => !Equals(left, right); + + /// + public override int GetHashCode() => HashCode.Hash.Combine(UserStreamAcl?.GetHashCode()) + .Combine(SystemStreamAcl?.GetHashCode()); + } +} diff --git a/src/Kurrent.Client/Streams/SystemSettingsJsonConverter.cs b/src/Kurrent.Client/Streams/SystemSettingsJsonConverter.cs new file mode 100644 index 000000000..03d7e7b9c --- /dev/null +++ b/src/Kurrent.Client/Streams/SystemSettingsJsonConverter.cs @@ -0,0 +1,62 @@ +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace EventStore.Client { + internal class SystemSettingsJsonConverter : JsonConverter { + public static readonly SystemSettingsJsonConverter Instance = new SystemSettingsJsonConverter(); + + public override SystemSettings Read(ref Utf8JsonReader reader, Type typeToConvert, + JsonSerializerOptions options) { + if (reader.TokenType != JsonTokenType.StartObject) { + throw new InvalidOperationException(); + } + + StreamAcl? system = null, user = null; + + while (reader.Read()) { + if (reader.TokenType == JsonTokenType.EndObject) { + break; + } + + if (reader.TokenType != JsonTokenType.PropertyName) { + throw new InvalidOperationException(); + } + + switch (reader.GetString()) { + case SystemMetadata.SystemStreamAcl: + if (!reader.Read()) { + throw new InvalidOperationException(); + } + + system = StreamAclJsonConverter.Instance.Read(ref reader, typeof(StreamAcl), options); + break; + case SystemMetadata.UserStreamAcl: + if (!reader.Read()) { + throw new InvalidOperationException(); + } + + user = StreamAclJsonConverter.Instance.Read(ref reader, typeof(StreamAcl), options); + break; + } + } + + return new SystemSettings(user, system); + } + + public override void Write(Utf8JsonWriter writer, SystemSettings value, JsonSerializerOptions options) { + writer.WriteStartObject(); + if (value.UserStreamAcl != null) { + writer.WritePropertyName(SystemMetadata.UserStreamAcl); + StreamAclJsonConverter.Instance.Write(writer, value.UserStreamAcl, options); + } + + if (value.SystemStreamAcl != null) { + writer.WritePropertyName(SystemMetadata.SystemStreamAcl); + StreamAclJsonConverter.Instance.Write(writer, value.SystemStreamAcl, options); + } + + writer.WriteEndObject(); + } + } +} diff --git a/src/Kurrent.Client/Streams/WriteResultExtensions.cs b/src/Kurrent.Client/Streams/WriteResultExtensions.cs new file mode 100644 index 000000000..7b5005e02 --- /dev/null +++ b/src/Kurrent.Client/Streams/WriteResultExtensions.cs @@ -0,0 +1,12 @@ +namespace EventStore.Client { + internal static class WriteResultExtensions { + public static IWriteResult OptionallyThrowWrongExpectedVersionException(this IWriteResult writeResult, + KurrentClientOperationOptions options) => + (options.ThrowOnAppendFailure, writeResult) switch { + (true, WrongExpectedVersionResult wrongExpectedVersionResult) + => throw new WrongExpectedVersionException(wrongExpectedVersionResult.StreamName, + writeResult.NextExpectedStreamRevision, wrongExpectedVersionResult.ActualStreamRevision), + _ => writeResult + }; + } +} diff --git a/src/Kurrent.Client/Streams/WrongExpectedVersionResult.cs b/src/Kurrent.Client/Streams/WrongExpectedVersionResult.cs new file mode 100644 index 000000000..7dd4887ca --- /dev/null +++ b/src/Kurrent.Client/Streams/WrongExpectedVersionResult.cs @@ -0,0 +1,58 @@ +namespace EventStore.Client { + /// + /// An that indicates a failed append to a stream. + /// + public readonly struct WrongExpectedVersionResult : IWriteResult { + /// + /// The name of the stream. + /// + public string StreamName { get; } + + /// + public long NextExpectedVersion { get; } + + /// + /// The version the stream is at. + /// + public long ActualVersion { get; } + + /// + /// The the stream is at. + /// + public StreamRevision ActualStreamRevision { get; } + + /// + public Position LogPosition { get; } + + /// + public StreamRevision NextExpectedStreamRevision { get; } + + /// + /// Construct a new . + /// + /// + /// + public WrongExpectedVersionResult(string streamName, StreamRevision nextExpectedStreamRevision) { + StreamName = streamName; + ActualVersion = NextExpectedVersion = nextExpectedStreamRevision.ToInt64(); + ActualStreamRevision = NextExpectedStreamRevision = nextExpectedStreamRevision; + LogPosition = default; + } + + /// + /// Construct a new . + /// + /// + /// + /// + public WrongExpectedVersionResult(string streamName, StreamRevision nextExpectedStreamRevision, + StreamRevision actualStreamRevision) { + StreamName = streamName; + ActualVersion = actualStreamRevision.ToInt64(); + ActualStreamRevision = actualStreamRevision; + NextExpectedVersion = nextExpectedStreamRevision.ToInt64(); + NextExpectedStreamRevision = nextExpectedStreamRevision; + LogPosition = default; + } + } +} diff --git a/src/Kurrent.Client/UserManagement/KurrentUserManagementClient.cs b/src/Kurrent.Client/UserManagement/KurrentUserManagementClient.cs new file mode 100644 index 000000000..ce18d40a0 --- /dev/null +++ b/src/Kurrent.Client/UserManagement/KurrentUserManagementClient.cs @@ -0,0 +1,279 @@ +using System.Runtime.CompilerServices; +using EventStore.Client.Users; +using Grpc.Core; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; + +namespace EventStore.Client { + /// + /// The client used for operations on internal users. + /// + public sealed class KurrentUserManagementClient : KurrentClientBase { + private readonly ILogger _log; + + /// + /// Constructs a new . + /// + /// + public KurrentUserManagementClient(KurrentClientSettings? settings = null) : + base(settings, ExceptionMap) { + _log = Settings.LoggerFactory?.CreateLogger() ?? + new NullLogger(); + } + + /// + /// Creates an internal user. + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public async Task CreateUserAsync(string loginName, string fullName, string[] groups, string password, + TimeSpan? deadline = null, UserCredentials? userCredentials = null, + CancellationToken cancellationToken = default) { + if (loginName == null) throw new ArgumentNullException(nameof(loginName)); + if (fullName == null) throw new ArgumentNullException(nameof(fullName)); + if (groups == null) throw new ArgumentNullException(nameof(groups)); + if (password == null) throw new ArgumentNullException(nameof(password)); + if (loginName == string.Empty) throw new ArgumentOutOfRangeException(nameof(loginName)); + if (fullName == string.Empty) throw new ArgumentOutOfRangeException(nameof(fullName)); + if (password == string.Empty) throw new ArgumentOutOfRangeException(nameof(password)); + + var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); + using var call = new Users.Users.UsersClient( + channelInfo.CallInvoker).CreateAsync(new CreateReq { + Options = new CreateReq.Types.Options { + LoginName = loginName, + FullName = fullName, + Password = password, + Groups = {groups} + } + }, KurrentCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); + await call.ResponseAsync.ConfigureAwait(false); + } + + /// + /// Gets the of an internal user. + /// + /// + /// + /// + /// + /// + /// + /// + public async Task GetUserAsync(string loginName, TimeSpan? deadline = null, + UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) { + if (loginName == null) { + throw new ArgumentNullException(nameof(loginName)); + } + + if (loginName == string.Empty) { + throw new ArgumentOutOfRangeException(nameof(loginName)); + } + + var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); + using var call = new Users.Users.UsersClient( + channelInfo.CallInvoker).Details(new DetailsReq { + Options = new DetailsReq.Types.Options { + LoginName = loginName + } + }, KurrentCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); + + await call.ResponseStream.MoveNext().ConfigureAwait(false); + var userDetails = call.ResponseStream.Current.UserDetails; + return ConvertUserDetails(userDetails); + } + + private static UserDetails ConvertUserDetails(DetailsResp.Types.UserDetails userDetails) => + new UserDetails(userDetails.LoginName, userDetails.FullName, userDetails.Groups.ToArray(), + userDetails.Disabled, userDetails.LastUpdated?.TicksSinceEpoch.FromTicksSinceEpoch()); + + /// + /// Deletes an internal user. + /// + /// + /// + /// + /// + /// + /// + /// + public async Task DeleteUserAsync(string loginName, TimeSpan? deadline = null, + UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) { + if (loginName == null) { + throw new ArgumentNullException(nameof(loginName)); + } + + if (loginName == string.Empty) { + throw new ArgumentOutOfRangeException(nameof(loginName)); + } + + var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); + var call = new Users.Users.UsersClient( + channelInfo.CallInvoker).DeleteAsync(new DeleteReq { + Options = new DeleteReq.Types.Options { + LoginName = loginName + } + }, KurrentCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); + await call.ResponseAsync.ConfigureAwait(false); + } + + /// + /// Enables a previously disabled internal user. + /// + /// + /// + /// + /// + /// + /// + /// + public async Task EnableUserAsync(string loginName, TimeSpan? deadline = null, + UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) { + if (loginName == null) { + throw new ArgumentNullException(nameof(loginName)); + } + + if (loginName == string.Empty) { + throw new ArgumentOutOfRangeException(nameof(loginName)); + } + + var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); + using var call = new Users.Users.UsersClient( + channelInfo.CallInvoker).EnableAsync(new EnableReq { + Options = new EnableReq.Types.Options { + LoginName = loginName + } + }, KurrentCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); + await call.ResponseAsync.ConfigureAwait(false); + } + + /// + /// Disables an internal user. + /// + /// + /// + /// + /// + /// + /// + public async Task DisableUserAsync(string loginName, TimeSpan? deadline = null, + UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) { + if (loginName == string.Empty) throw new ArgumentOutOfRangeException(nameof(loginName)); + + var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); + var call = new Users.Users.UsersClient( + channelInfo.CallInvoker).DisableAsync(new DisableReq { + Options = new DisableReq.Types.Options { + LoginName = loginName + } + }, KurrentCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); + await call.ResponseAsync.ConfigureAwait(false); + } + + /// + /// Lists the of all internal users. + /// + /// + /// + /// + /// + public async IAsyncEnumerable ListAllAsync(TimeSpan? deadline = null, + UserCredentials? userCredentials = null, + [EnumeratorCancellation] CancellationToken cancellationToken = default) { + var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); + using var call = new Users.Users.UsersClient( + channelInfo.CallInvoker).Details(new DetailsReq(), + KurrentCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); + + await foreach (var userDetail in call.ResponseStream + .ReadAllAsync(cancellationToken) + .Select(x => ConvertUserDetails(x.UserDetails)) + .WithCancellation(cancellationToken) + .ConfigureAwait(false)) { + yield return userDetail; + } + } + + /// + /// Changes the password of an internal user. + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public async Task ChangePasswordAsync(string loginName, string currentPassword, string newPassword, + TimeSpan? deadline = null, UserCredentials? userCredentials = null, + CancellationToken cancellationToken = default) { + if (loginName == null) throw new ArgumentNullException(nameof(loginName)); + if (currentPassword == null) throw new ArgumentNullException(nameof(currentPassword)); + if (newPassword == null) throw new ArgumentNullException(nameof(newPassword)); + if (loginName == string.Empty) throw new ArgumentOutOfRangeException(nameof(loginName)); + if (currentPassword == string.Empty) throw new ArgumentOutOfRangeException(nameof(currentPassword)); + if (newPassword == string.Empty) throw new ArgumentOutOfRangeException(nameof(newPassword)); + + var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); + using var call = new Users.Users.UsersClient( + channelInfo.CallInvoker).ChangePasswordAsync( + new ChangePasswordReq { + Options = new ChangePasswordReq.Types.Options { + CurrentPassword = currentPassword, + NewPassword = newPassword, + LoginName = loginName + } + }, + KurrentCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); + await call.ResponseAsync.ConfigureAwait(false); + } + + /// + /// Resets the password of an internal user. + /// + /// + /// + /// + /// + /// + /// + /// + /// + public async Task ResetPasswordAsync(string loginName, string newPassword, + TimeSpan? deadline = null, UserCredentials? userCredentials = null, + CancellationToken cancellationToken = default) { + if (loginName == null) throw new ArgumentNullException(nameof(loginName)); + if (newPassword == null) throw new ArgumentNullException(nameof(newPassword)); + if (loginName == string.Empty) throw new ArgumentOutOfRangeException(nameof(loginName)); + if (newPassword == string.Empty) throw new ArgumentOutOfRangeException(nameof(newPassword)); + + var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); + var call = new Users.Users.UsersClient( + channelInfo.CallInvoker).ResetPasswordAsync( + new ResetPasswordReq { + Options = new ResetPasswordReq.Types.Options { + NewPassword = newPassword, + LoginName = loginName + } + }, + KurrentCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); + await call.ResponseAsync.ConfigureAwait(false); + } + + private static readonly Dictionary> ExceptionMap = + new Dictionary> { + [Constants.Exceptions.UserNotFound] = ex => new UserNotFoundException( + ex.Trailers.First(x => x.Key == Constants.Exceptions.LoginName).Value), + }; + } +} diff --git a/src/Kurrent.Client/UserManagement/KurrentUserManagementClientCollectionExtensions.cs b/src/Kurrent.Client/UserManagement/KurrentUserManagementClientCollectionExtensions.cs new file mode 100644 index 000000000..220f99740 --- /dev/null +++ b/src/Kurrent.Client/UserManagement/KurrentUserManagementClientCollectionExtensions.cs @@ -0,0 +1,72 @@ +// ReSharper disable CheckNamespace + +using System.Net.Http; +using EventStore.Client; +using Grpc.Core.Interceptors; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Logging; + +namespace Microsoft.Extensions.DependencyInjection { + /// + /// A set of extension methods for which provide support for an . + /// + public static class KurrentUserManagementClientCollectionExtensions { + /// + /// Adds an to the . + /// + /// + /// + /// + /// + /// + public static IServiceCollection AddKurrentUserManagementClient(this IServiceCollection services, + Uri address, Func? createHttpMessageHandler = null) + => services.AddKurrentUserManagementClient(options => { + options.ConnectivitySettings.Address = address; + options.CreateHttpMessageHandler = createHttpMessageHandler; + }); + + /// + /// Adds an to the . + /// + /// + /// + /// + /// + /// + public static IServiceCollection AddKurrentUserManagementClient(this IServiceCollection services, + string connectionString, Action? configureSettings = null) + => services.AddKurrentUserManagementClient(KurrentClientSettings.Create(connectionString), + configureSettings); + + + /// + /// Adds an to the . + /// + /// + /// + /// + /// + public static IServiceCollection AddKurrentUserManagementClient(this IServiceCollection services, + Action? configureSettings = null) => + services.AddKurrentUserManagementClient(new KurrentClientSettings(), configureSettings); + + private static IServiceCollection AddKurrentUserManagementClient(this IServiceCollection services, + KurrentClientSettings settings, Action? configureSettings = null) { + configureSettings?.Invoke(settings); + if (services == null) { + throw new ArgumentNullException(nameof(services)); + } + + services.TryAddSingleton(provider => { + settings.LoggerFactory ??= provider.GetService(); + settings.Interceptors ??= provider.GetServices(); + + return new KurrentUserManagementClient(settings); + }); + + return services; + } + } +} +// ReSharper restore CheckNamespace diff --git a/src/Kurrent.Client/UserManagement/KurrentUserManagerClientExtensions.cs b/src/Kurrent.Client/UserManagement/KurrentUserManagerClientExtensions.cs new file mode 100644 index 000000000..be365fdd2 --- /dev/null +++ b/src/Kurrent.Client/UserManagement/KurrentUserManagerClientExtensions.cs @@ -0,0 +1,23 @@ +namespace EventStore.Client; + +/// +/// A set of extension methods for an . +/// +public static class KurrentUserManagerClientExtensions { + /// + /// Gets the of the internal user specified by the supplied . + /// + /// + /// + /// + /// + /// + public static Task GetCurrentUserAsync( + this KurrentUserManagementClient users, + UserCredentials userCredentials, TimeSpan? deadline = null, CancellationToken cancellationToken = default + ) => + users.GetUserAsync( + userCredentials.Username!, deadline, userCredentials, + cancellationToken + ); +} diff --git a/src/Kurrent.Client/UserManagement/UserDetails.cs b/src/Kurrent.Client/UserManagement/UserDetails.cs new file mode 100644 index 000000000..b7414dd36 --- /dev/null +++ b/src/Kurrent.Client/UserManagement/UserDetails.cs @@ -0,0 +1,97 @@ +namespace EventStore.Client; + +/// +/// Provides the details for a user. +/// +public readonly struct UserDetails : IEquatable { + /// + /// The users login name. + /// + public readonly string LoginName; + + /// + /// The full name of the user. + /// + public readonly string FullName; + + /// + /// The groups the user is a member of. + /// + public readonly string[] Groups; + + /// + /// The date/time the user was updated in UTC format. + /// + public readonly DateTimeOffset? DateLastUpdated; + + /// + /// Whether the user disable or not. + /// + public readonly bool Disabled; + + /// + /// create a new class. + /// + /// The login name of the user. + /// The users full name. + /// The groups this user is a member if. + /// Is this user disabled or not. + /// The datt/time this user was last updated in UTC format. + public UserDetails( + string loginName, string fullName, string[] groups, bool disabled, DateTimeOffset? dateLastUpdated) { + if (loginName == null) { + throw new ArgumentNullException(nameof(loginName)); + } + + if (fullName == null) { + throw new ArgumentNullException(nameof(fullName)); + } + + if (groups == null) { + throw new ArgumentNullException(nameof(groups)); + } + + LoginName = loginName; + FullName = fullName; + Groups = groups; + Disabled = disabled; + DateLastUpdated = dateLastUpdated; + } + + /// + public bool Equals(UserDetails other) => + LoginName == other.LoginName && FullName == other.FullName && Groups.SequenceEqual(other.Groups) && + Nullable.Equals(DateLastUpdated, other.DateLastUpdated) && Disabled == other.Disabled; + + /// + public override bool Equals(object? obj) => obj is UserDetails other && Equals(other); + + /// + public override int GetHashCode() => HashCode.Hash.Combine(LoginName).Combine(FullName).Combine(Groups) + .Combine(Disabled).Combine(DateLastUpdated); + + /// + /// Compares left and right for equality. + /// + /// + /// + /// True if left is equal to right. + public static bool operator ==(UserDetails left, UserDetails right) => left.Equals(right); + + /// + /// Compares left and right for inequality. + /// + /// + /// + /// True if left is not equal to right. + public static bool operator !=(UserDetails left, UserDetails right) => !left.Equals(right); + + /// + public override string ToString() => + new { + Disabled, + FullName, + LoginName, + Groups = string.Join(",", Groups) + }?.ToString()!; +} diff --git a/test/Directory.Build.props b/test/Directory.Build.props index 8c770cca2..6630ffb1b 100644 --- a/test/Directory.Build.props +++ b/test/Directory.Build.props @@ -30,7 +30,7 @@ - + @@ -39,6 +39,6 @@ - + diff --git a/test/EventStore.Client.Tests.Common/EventStore.Client.Tests.Common.csproj.DotSettings b/test/EventStore.Client.Tests.Common/EventStore.Client.Tests.Common.csproj.DotSettings deleted file mode 100644 index 6ba62e1cd..000000000 --- a/test/EventStore.Client.Tests.Common/EventStore.Client.Tests.Common.csproj.DotSettings +++ /dev/null @@ -1,5 +0,0 @@ - - True - True - True - True \ No newline at end of file diff --git a/test/EventStore.Client.Tests.Common/Extensions/ShouldThrowAsyncExtensions.cs b/test/EventStore.Client.Tests.Common/Extensions/ShouldThrowAsyncExtensions.cs deleted file mode 100644 index edba53a25..000000000 --- a/test/EventStore.Client.Tests.Common/Extensions/ShouldThrowAsyncExtensions.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace EventStore.Client.Tests; - -public static class ShouldThrowAsyncExtensions { - public static Task ShouldThrowAsync(this EventStoreClient.ReadStreamResult source) where TException : Exception => - source - .ToArrayAsync() - .AsTask() - .ShouldThrowAsync(); - - public static async Task ShouldThrowAsync(this EventStoreClient.ReadStreamResult source, Action handler) where TException : Exception { - var ex = await source.ShouldThrowAsync(); - handler(ex); - } -} diff --git a/test/EventStore.Client.Tests/EventStoreClientOperationsTests.cs b/test/EventStore.Client.Tests/EventStoreClientOperationsTests.cs deleted file mode 100644 index 031a4bfd6..000000000 --- a/test/EventStore.Client.Tests/EventStoreClientOperationsTests.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace EventStore.Client.Tests; - -public class EventStoreClientOperationOptionsTests { - [RetryFact] - public void setting_options_on_clone_should_not_modify_original() { - var options = EventStoreClientOperationOptions.Default; - - var clonedOptions = options.Clone(); - clonedOptions.BatchAppendSize = int.MaxValue; - - Assert.Equal(options.BatchAppendSize, EventStoreClientOperationOptions.Default.BatchAppendSize); - Assert.Equal(int.MaxValue, clonedOptions.BatchAppendSize); - } -} diff --git a/test/EventStore.Client.Tests.Common/.env b/test/Kurrent.Client.Tests.Common/.env similarity index 100% rename from test/EventStore.Client.Tests.Common/.env rename to test/Kurrent.Client.Tests.Common/.env diff --git a/test/EventStore.Client.Tests.Common/ApplicationInfo.cs b/test/Kurrent.Client.Tests.Common/ApplicationInfo.cs similarity index 98% rename from test/EventStore.Client.Tests.Common/ApplicationInfo.cs rename to test/Kurrent.Client.Tests.Common/ApplicationInfo.cs index 0120c21b4..26e78938d 100644 --- a/test/EventStore.Client.Tests.Common/ApplicationInfo.cs +++ b/test/Kurrent.Client.Tests.Common/ApplicationInfo.cs @@ -9,7 +9,7 @@ using static System.Environment; using static System.StringComparison; -namespace EventStore.Client; +namespace Kurrent.Client; /// /// Loads configuration and provides information about the application environment. diff --git a/test/EventStore.Client.Tests.Common/AssertEx.cs b/test/Kurrent.Client.Tests.Common/AssertEx.cs similarity index 98% rename from test/EventStore.Client.Tests.Common/AssertEx.cs rename to test/Kurrent.Client.Tests.Common/AssertEx.cs index db2386374..6750e66b5 100644 --- a/test/EventStore.Client.Tests.Common/AssertEx.cs +++ b/test/Kurrent.Client.Tests.Common/AssertEx.cs @@ -1,7 +1,7 @@ using System.Runtime.CompilerServices; using Xunit.Sdk; -namespace EventStore.Client.Tests; +namespace Kurrent.Client.Tests; public static class AssertEx { /// diff --git a/test/EventStore.Client.Tests.Common/Certificates.cs b/test/Kurrent.Client.Tests.Common/Certificates.cs similarity index 97% rename from test/EventStore.Client.Tests.Common/Certificates.cs rename to test/Kurrent.Client.Tests.Common/Certificates.cs index efd167d67..3b8671d9b 100644 --- a/test/EventStore.Client.Tests.Common/Certificates.cs +++ b/test/Kurrent.Client.Tests.Common/Certificates.cs @@ -1,6 +1,6 @@ // ReSharper disable InconsistentNaming -namespace EventStore.Client.Tests; +namespace Kurrent.Client.Tests; public static class Certificates { static readonly string BaseDirectory = AppDomain.CurrentDomain.BaseDirectory; diff --git a/test/EventStore.Client.Tests.Common/Extensions/ConfigurationExtensions.cs b/test/Kurrent.Client.Tests.Common/Extensions/ConfigurationExtensions.cs similarity index 90% rename from test/EventStore.Client.Tests.Common/Extensions/ConfigurationExtensions.cs rename to test/Kurrent.Client.Tests.Common/Extensions/ConfigurationExtensions.cs index 621758f36..87f06cd45 100644 --- a/test/EventStore.Client.Tests.Common/Extensions/ConfigurationExtensions.cs +++ b/test/Kurrent.Client.Tests.Common/Extensions/ConfigurationExtensions.cs @@ -1,6 +1,6 @@ using Microsoft.Extensions.Configuration; -namespace EventStore.Client.Tests; +namespace Kurrent.Client.Tests; public static class ConfigurationExtensions { public static void EnsureValue(this IConfiguration configuration, string key, string defaultValue) { diff --git a/test/EventStore.Client.Tests.Common/Extensions/EventStoreClientExtensions.cs b/test/Kurrent.Client.Tests.Common/Extensions/KurrentClientExtensions.cs similarity index 69% rename from test/EventStore.Client.Tests.Common/Extensions/EventStoreClientExtensions.cs rename to test/Kurrent.Client.Tests.Common/Extensions/KurrentClientExtensions.cs index cd7b808bd..0b0861cf3 100644 --- a/test/EventStore.Client.Tests.Common/Extensions/EventStoreClientExtensions.cs +++ b/test/Kurrent.Client.Tests.Common/Extensions/KurrentClientExtensions.cs @@ -1,11 +1,12 @@ +using EventStore.Client; using Polly; using static System.TimeSpan; -namespace EventStore.Client.Tests; +namespace Kurrent.Client.Tests; -public static class EventStoreClientExtensions { +public static class KurrentClientExtensions { public static Task CreateUserWithRetry( - this EventStoreUserManagementClient client, string loginName, string fullName, string[] groups, string password, + this KurrentUserManagementClient client, string loginName, string fullName, string[] groups, string password, UserCredentials? userCredentials = null, CancellationToken cancellationToken = default ) => Policy.Handle() diff --git a/test/EventStore.Client.Tests.Common/Extensions/EventStoreClientWarmupExtensions.cs b/test/Kurrent.Client.Tests.Common/Extensions/KurrentClientWarmupExtensions.cs similarity index 79% rename from test/EventStore.Client.Tests.Common/Extensions/EventStoreClientWarmupExtensions.cs rename to test/Kurrent.Client.Tests.Common/Extensions/KurrentClientWarmupExtensions.cs index aff15195c..89045a44b 100644 --- a/test/EventStore.Client.Tests.Common/Extensions/EventStoreClientWarmupExtensions.cs +++ b/test/Kurrent.Client.Tests.Common/Extensions/KurrentClientWarmupExtensions.cs @@ -1,11 +1,12 @@ +using EventStore.Client; using Grpc.Core; using Polly; using Polly.Contrib.WaitAndRetry; using static System.TimeSpan; -namespace EventStore.Client.Tests; +namespace Kurrent.Client.Tests; -public static class EventStoreClientWarmupExtensions { +public static class KurrentClientWarmupExtensions { static readonly TimeSpan RediscoverTimeout = FromSeconds(5); /// @@ -14,7 +15,7 @@ public static class EventStoreClientWarmupExtensions { static readonly IEnumerable DefaultBackoffDelay = Backoff.ConstantBackoff(FromMilliseconds(100), 300); static async Task TryWarmUp(T client, Func action, CancellationToken cancellationToken = default) - where T : EventStoreClientBase { + where T : KurrentClientBase { await Policy .Handle(ex => ex.StatusCode != StatusCode.Unimplemented) .Or() @@ -39,7 +40,7 @@ await Policy return client; } - public static Task WarmUp(this EventStoreClient client, CancellationToken cancellationToken = default) => + public static Task WarmUp(this KurrentClient client, CancellationToken cancellationToken = default) => TryWarmUp( client, async ct => { @@ -72,7 +73,7 @@ public static Task WarmUp(this EventStoreClient client, Cancel cancellationToken ); - public static Task WarmUp(this EventStoreOperationsClient client, CancellationToken cancellationToken = default) => + public static Task WarmUp(this KurrentOperationsClient client, CancellationToken cancellationToken = default) => TryWarmUp( client, async ct => { @@ -84,8 +85,8 @@ await client.RestartPersistentSubscriptions( cancellationToken ); - public static Task WarmUp( - this EventStorePersistentSubscriptionsClient client, CancellationToken cancellationToken = default + public static Task WarmUp( + this KurrentPersistentSubscriptionsClient client, CancellationToken cancellationToken = default ) => TryWarmUp( client, @@ -102,8 +103,8 @@ await client.CreateToStreamAsync( cancellationToken ); - public static Task WarmUp( - this EventStoreProjectionManagementClient client, CancellationToken cancellationToken = default + public static Task WarmUp( + this KurrentProjectionManagementClient client, CancellationToken cancellationToken = default ) => TryWarmUp( client, @@ -118,7 +119,7 @@ public static Task WarmUp( cancellationToken ); - public static Task WarmUp(this EventStoreUserManagementClient client, CancellationToken cancellationToken = default) => + public static Task WarmUp(this KurrentUserManagementClient client, CancellationToken cancellationToken = default) => TryWarmUp( client, async ct => { diff --git a/test/EventStore.Client.Tests.Common/Extensions/OperatingSystemExtensions.cs b/test/Kurrent.Client.Tests.Common/Extensions/OperatingSystemExtensions.cs similarity index 88% rename from test/EventStore.Client.Tests.Common/Extensions/OperatingSystemExtensions.cs rename to test/Kurrent.Client.Tests.Common/Extensions/OperatingSystemExtensions.cs index 5899f625c..fbd53f8cc 100644 --- a/test/EventStore.Client.Tests.Common/Extensions/OperatingSystemExtensions.cs +++ b/test/Kurrent.Client.Tests.Common/Extensions/OperatingSystemExtensions.cs @@ -1,4 +1,4 @@ -namespace EventStore.Client; +namespace Kurrent.Client; public static class OperatingSystemExtensions { public static bool IsWindows(this OperatingSystem operatingSystem) => diff --git a/test/EventStore.Client.Tests.Common/Extensions/ReadOnlyMemoryExtensions.cs b/test/Kurrent.Client.Tests.Common/Extensions/ReadOnlyMemoryExtensions.cs similarity index 94% rename from test/EventStore.Client.Tests.Common/Extensions/ReadOnlyMemoryExtensions.cs rename to test/Kurrent.Client.Tests.Common/Extensions/ReadOnlyMemoryExtensions.cs index 63b6694a3..403665653 100644 --- a/test/EventStore.Client.Tests.Common/Extensions/ReadOnlyMemoryExtensions.cs +++ b/test/Kurrent.Client.Tests.Common/Extensions/ReadOnlyMemoryExtensions.cs @@ -1,6 +1,7 @@ using System.Text.Json; +using EventStore.Client; -namespace EventStore.Client; +namespace Kurrent.Client; public static class ReadOnlyMemoryExtensions { public static Position ParsePosition(this ReadOnlyMemory json) { diff --git a/test/Kurrent.Client.Tests.Common/Extensions/ShouldThrowAsyncExtensions.cs b/test/Kurrent.Client.Tests.Common/Extensions/ShouldThrowAsyncExtensions.cs new file mode 100644 index 000000000..827133337 --- /dev/null +++ b/test/Kurrent.Client.Tests.Common/Extensions/ShouldThrowAsyncExtensions.cs @@ -0,0 +1,16 @@ +using EventStore.Client; + +namespace Kurrent.Client.Tests; + +public static class ShouldThrowAsyncExtensions { + public static Task ShouldThrowAsync(this KurrentClient.ReadStreamResult source) where TException : Exception => + source + .ToArrayAsync() + .AsTask() + .ShouldThrowAsync(); + + public static async Task ShouldThrowAsync(this KurrentClient.ReadStreamResult source, Action handler) where TException : Exception { + var ex = await source.ShouldThrowAsync(); + handler(ex); + } +} diff --git a/test/EventStore.Client.Tests.Common/Extensions/TaskExtensions.cs b/test/Kurrent.Client.Tests.Common/Extensions/TaskExtensions.cs similarity index 97% rename from test/EventStore.Client.Tests.Common/Extensions/TaskExtensions.cs rename to test/Kurrent.Client.Tests.Common/Extensions/TaskExtensions.cs index d3775b700..9fb022726 100644 --- a/test/EventStore.Client.Tests.Common/Extensions/TaskExtensions.cs +++ b/test/Kurrent.Client.Tests.Common/Extensions/TaskExtensions.cs @@ -1,6 +1,6 @@ using System.Diagnostics; -namespace EventStore.Client; +namespace Kurrent.Client; public static class TaskExtensions { public static Task WithTimeout(this Task task, TimeSpan timeout) diff --git a/test/EventStore.Client.Tests.Common/Extensions/TypeExtensions.cs b/test/Kurrent.Client.Tests.Common/Extensions/TypeExtensions.cs similarity index 98% rename from test/EventStore.Client.Tests.Common/Extensions/TypeExtensions.cs rename to test/Kurrent.Client.Tests.Common/Extensions/TypeExtensions.cs index 465b04f92..355544e88 100644 --- a/test/EventStore.Client.Tests.Common/Extensions/TypeExtensions.cs +++ b/test/Kurrent.Client.Tests.Common/Extensions/TypeExtensions.cs @@ -1,6 +1,6 @@ using System.Reflection; -namespace EventStore.Client.Tests; +namespace Kurrent.Client.Tests; public static class TypeExtensions { public static bool InvokeEqualityOperator(this Type type, object? left, object? right) => type.InvokeOperator("Equality", left, right); diff --git a/test/EventStore.Client.Tests.Common/Extensions/WithExtension.cs b/test/Kurrent.Client.Tests.Common/Extensions/WithExtension.cs similarity index 97% rename from test/EventStore.Client.Tests.Common/Extensions/WithExtension.cs rename to test/Kurrent.Client.Tests.Common/Extensions/WithExtension.cs index 35f731de3..b0c577e26 100644 --- a/test/EventStore.Client.Tests.Common/Extensions/WithExtension.cs +++ b/test/Kurrent.Client.Tests.Common/Extensions/WithExtension.cs @@ -1,6 +1,6 @@ using System.Diagnostics; -namespace EventStore.Client.Tests; +namespace Kurrent.Client.Tests; public static class WithExtension { [DebuggerStepThrough] diff --git a/test/EventStore.Client.Tests.Common/Facts/AnonymousAccess.cs b/test/Kurrent.Client.Tests.Common/Facts/AnonymousAccess.cs similarity index 90% rename from test/EventStore.Client.Tests.Common/Facts/AnonymousAccess.cs rename to test/Kurrent.Client.Tests.Common/Facts/AnonymousAccess.cs index 0e8a3c248..101a67df0 100644 --- a/test/EventStore.Client.Tests.Common/Facts/AnonymousAccess.cs +++ b/test/Kurrent.Client.Tests.Common/Facts/AnonymousAccess.cs @@ -1,4 +1,4 @@ -namespace EventStore.Client.Tests; +namespace Kurrent.Client.Tests; [PublicAPI] public class AnonymousAccess { diff --git a/test/EventStore.Client.Tests.Common/Facts/Deprecation.cs b/test/Kurrent.Client.Tests.Common/Facts/Deprecation.cs similarity index 94% rename from test/EventStore.Client.Tests.Common/Facts/Deprecation.cs rename to test/Kurrent.Client.Tests.Common/Facts/Deprecation.cs index 98e9fe671..3b374ee91 100644 --- a/test/EventStore.Client.Tests.Common/Facts/Deprecation.cs +++ b/test/Kurrent.Client.Tests.Common/Facts/Deprecation.cs @@ -1,4 +1,4 @@ -namespace EventStore.Client.Tests; +namespace Kurrent.Client.Tests; [PublicAPI] public class Deprecation { diff --git a/test/EventStore.Client.Tests.Common/Facts/Regression.cs b/test/Kurrent.Client.Tests.Common/Facts/Regression.cs similarity index 94% rename from test/EventStore.Client.Tests.Common/Facts/Regression.cs rename to test/Kurrent.Client.Tests.Common/Facts/Regression.cs index 5abdfbff6..09e944518 100644 --- a/test/EventStore.Client.Tests.Common/Facts/Regression.cs +++ b/test/Kurrent.Client.Tests.Common/Facts/Regression.cs @@ -1,4 +1,4 @@ -namespace EventStore.Client.Tests; +namespace Kurrent.Client.Tests; [PublicAPI] public class Regression { diff --git a/test/EventStore.Client.Tests.Common/Fakers/TestUserFaker.cs b/test/Kurrent.Client.Tests.Common/Fakers/TestUserFaker.cs similarity index 96% rename from test/EventStore.Client.Tests.Common/Fakers/TestUserFaker.cs rename to test/Kurrent.Client.Tests.Common/Fakers/TestUserFaker.cs index 9cb3c3fbb..472aa1b15 100644 --- a/test/EventStore.Client.Tests.Common/Fakers/TestUserFaker.cs +++ b/test/Kurrent.Client.Tests.Common/Fakers/TestUserFaker.cs @@ -1,4 +1,6 @@ -namespace EventStore.Client.Tests; +using EventStore.Client; + +namespace Kurrent.Client.Tests; public class TestUser { public UserDetails Details { get; set; } = default!; diff --git a/test/EventStore.Client.Tests.Common/Fixtures/BaseTestNode.cs b/test/Kurrent.Client.Tests.Common/Fixtures/BaseTestNode.cs similarity index 97% rename from test/EventStore.Client.Tests.Common/Fixtures/BaseTestNode.cs rename to test/Kurrent.Client.Tests.Common/Fixtures/BaseTestNode.cs index 789384e9d..5ace2a507 100644 --- a/test/EventStore.Client.Tests.Common/Fixtures/BaseTestNode.cs +++ b/test/Kurrent.Client.Tests.Common/Fixtures/BaseTestNode.cs @@ -6,18 +6,18 @@ // using Ductus.FluentDocker.Builders; // using Ductus.FluentDocker.Extensions; // using Ductus.FluentDocker.Services.Extensions; -// using EventStore.Client.Tests.FluentDocker; +// using Kurrent.Client.Tests.FluentDocker; // using Humanizer; // using Serilog; // using Serilog.Extensions.Logging; // using static System.TimeSpan; // -// namespace EventStore.Client.Tests; +// namespace Kurrent.Client.Tests; // // public abstract class BaseTestNode(EventStoreFixtureOptions? options = null) : TestContainerService { // static readonly NetworkPortProvider NetworkPortProvider = new(NetworkPortProvider.DefaultEsdbPort); // -// public EventStoreFixtureOptions Options { get; } = options ?? DefaultOptions(); +// public KurrentFixtureOptions Options { get; } = options ?? DefaultOptions(); // // static Version? _version; // diff --git a/test/EventStore.Client.Tests.Common/Fixtures/CertificatesManager.cs b/test/Kurrent.Client.Tests.Common/Fixtures/CertificatesManager.cs similarity index 98% rename from test/EventStore.Client.Tests.Common/Fixtures/CertificatesManager.cs rename to test/Kurrent.Client.Tests.Common/Fixtures/CertificatesManager.cs index 487bfd340..f01d9c246 100644 --- a/test/EventStore.Client.Tests.Common/Fixtures/CertificatesManager.cs +++ b/test/Kurrent.Client.Tests.Common/Fixtures/CertificatesManager.cs @@ -1,6 +1,6 @@ using Ductus.FluentDocker.Builders; -namespace EventStore.Client.Tests; +namespace Kurrent.Client.Tests; static class CertificatesManager { static readonly DirectoryInfo CertificateDirectory; diff --git a/test/EventStore.Client.Tests.Common/Fixtures/KurrentFixtureOptions.cs b/test/Kurrent.Client.Tests.Common/Fixtures/KurrentFixtureOptions.cs similarity index 86% rename from test/EventStore.Client.Tests.Common/Fixtures/KurrentFixtureOptions.cs rename to test/Kurrent.Client.Tests.Common/Fixtures/KurrentFixtureOptions.cs index 9e8496d27..51e1e4f78 100644 --- a/test/EventStore.Client.Tests.Common/Fixtures/KurrentFixtureOptions.cs +++ b/test/Kurrent.Client.Tests.Common/Fixtures/KurrentFixtureOptions.cs @@ -1,7 +1,9 @@ -namespace EventStore.Client.Tests; +using EventStore.Client; + +namespace Kurrent.Client.Tests; public record KurrentFixtureOptions( - EventStoreClientSettings ClientSettings, + KurrentClientSettings ClientSettings, IDictionary Environment ) { public KurrentFixtureOptions WithoutDefaultCredentials() => this with { ClientSettings = ClientSettings.With(x => x.DefaultCredentials = null) }; diff --git a/test/EventStore.Client.Tests.Common/Fixtures/KurrentPermanentFixture.Helpers.cs b/test/Kurrent.Client.Tests.Common/Fixtures/KurrentPermanentFixture.Helpers.cs similarity index 95% rename from test/EventStore.Client.Tests.Common/Fixtures/KurrentPermanentFixture.Helpers.cs rename to test/Kurrent.Client.Tests.Common/Fixtures/KurrentPermanentFixture.Helpers.cs index 41457d3b4..7eb5d9749 100644 --- a/test/EventStore.Client.Tests.Common/Fixtures/KurrentPermanentFixture.Helpers.cs +++ b/test/Kurrent.Client.Tests.Common/Fixtures/KurrentPermanentFixture.Helpers.cs @@ -1,14 +1,15 @@ using System.Runtime.CompilerServices; using System.Text; +using EventStore.Client; -namespace EventStore.Client.Tests; +namespace Kurrent.Client.Tests; public partial class KurrentPermanentFixture { public const string TestEventType = "test-event-type"; public const string AnotherTestEventTypePrefix = "another"; public const string AnotherTestEventType = $"{AnotherTestEventTypePrefix}-test-event-type"; - public T NewClient(Action configure) where T : EventStoreClientBase, new() => + public T NewClient(Action configure) where T : KurrentClientBase, new() => (T)Activator.CreateInstance(typeof(T), [ClientSettings.With(configure)])!; public string GetStreamName([CallerMemberName] string? testMethod = null) => diff --git a/test/EventStore.Client.Tests.Common/Fixtures/KurrentPermanentFixture.cs b/test/Kurrent.Client.Tests.Common/Fixtures/KurrentPermanentFixture.cs similarity index 78% rename from test/EventStore.Client.Tests.Common/Fixtures/KurrentPermanentFixture.cs rename to test/Kurrent.Client.Tests.Common/Fixtures/KurrentPermanentFixture.cs index e3c8e426f..2530126e0 100644 --- a/test/EventStore.Client.Tests.Common/Fixtures/KurrentPermanentFixture.cs +++ b/test/Kurrent.Client.Tests.Common/Fixtures/KurrentPermanentFixture.cs @@ -2,11 +2,13 @@ using Ductus.FluentDocker.Builders; using Ductus.FluentDocker.Extensions; using Ductus.FluentDocker.Services.Extensions; -using EventStore.Client.Tests.FluentDocker; +using EventStore.Client; +using Kurrent.Client.Tests.FluentDocker; using Serilog; using static System.TimeSpan; +using KurrentClient = EventStore.Client.KurrentClient; -namespace EventStore.Client.Tests; +namespace Kurrent.Client.Tests; [PublicAPI] public partial class KurrentPermanentFixture : IAsyncLifetime, IAsyncDisposable { @@ -42,11 +44,11 @@ protected KurrentPermanentFixture(ConfigureFixture configure) { public Version EventStoreVersion { get; private set; } = null!; public bool EventStoreHasLastStreamPosition { get; private set; } - public EventStoreClient Streams { get; private set; } = null!; - public EventStoreUserManagementClient Users { get; private set; } = null!; - public EventStoreProjectionManagementClient Projections { get; private set; } = null!; - public EventStorePersistentSubscriptionsClient Subscriptions { get; private set; } = null!; - public EventStoreOperationsClient Operations { get; private set; } = null!; + public KurrentClient Streams { get; private set; } = null!; + public KurrentUserManagementClient Users { get; private set; } = null!; + public KurrentProjectionManagementClient Projections { get; private set; } = null!; + public KurrentPersistentSubscriptionsClient Subscriptions { get; private set; } = null!; + public KurrentOperationsClient Operations { get; private set; } = null!; public bool SkipPsWarmUp { get; set; } @@ -56,7 +58,7 @@ protected KurrentPermanentFixture(ConfigureFixture configure) { /// /// must test this /// - public EventStoreClientSettings ClientSettings => + public KurrentClientSettings ClientSettings => new() { Interceptors = Options.ClientSettings.Interceptors, ConnectionName = Options.ClientSettings.ConnectionName, @@ -99,14 +101,14 @@ public async Task InitializeAsync() { Logger.Warning("*** Warmup started ***"); await Task.WhenAll( - InitClient(async x => Users = await x.WarmUp()), - InitClient(async x => Streams = await x.WarmUp()), - InitClient( + InitClient(async x => Users = await x.WarmUp()), + InitClient(async x => Streams = await x.WarmUp()), + InitClient( async x => Projections = await x.WarmUp(), Options.Environment["EVENTSTORE_RUN_PROJECTIONS"] != "None" ), - InitClient(async x => Subscriptions = SkipPsWarmUp ? x : await x.WarmUp()), - InitClient(async x => Operations = await x.WarmUp()) + InitClient(async x => Subscriptions = SkipPsWarmUp ? x : await x.WarmUp()), + InitClient(async x => Operations = await x.WarmUp()) ); WarmUpCompleted.EnsureCalledOnce(); @@ -123,7 +125,7 @@ await Task.WhenAll( return; - async Task InitClient(Func action, bool execute = true) where T : EventStoreClientBase { + async Task InitClient(Func action, bool execute = true) where T : KurrentClientBase { if (!execute) return default(T)!; var client = (Activator.CreateInstance(typeof(T), ClientSettings) as T)!; @@ -179,8 +181,8 @@ public async Task DisposeAsync() { async ValueTask IAsyncDisposable.DisposeAsync() => await DisposeAsync(); } -public abstract class EventStorePermanentTests : IClassFixture where TFixture : KurrentPermanentFixture { - protected EventStorePermanentTests(ITestOutputHelper output, TFixture fixture) => Fixture = fixture.With(x => x.CaptureTestRun(output)); +public abstract class KurrentPermanentTests : IClassFixture where TFixture : KurrentPermanentFixture { + protected KurrentPermanentTests(ITestOutputHelper output, TFixture fixture) => Fixture = fixture.With(x => x.CaptureTestRun(output)); protected TFixture Fixture { get; } } diff --git a/test/EventStore.Client.Tests.Common/Fixtures/KurrentPermanentTestNode.cs b/test/Kurrent.Client.Tests.Common/Fixtures/KurrentPermanentTestNode.cs similarity index 97% rename from test/EventStore.Client.Tests.Common/Fixtures/KurrentPermanentTestNode.cs rename to test/Kurrent.Client.Tests.Common/Fixtures/KurrentPermanentTestNode.cs index d2c4549ab..e8c554f02 100644 --- a/test/EventStore.Client.Tests.Common/Fixtures/KurrentPermanentTestNode.cs +++ b/test/Kurrent.Client.Tests.Common/Fixtures/KurrentPermanentTestNode.cs @@ -2,9 +2,9 @@ // using Ductus.FluentDocker.Builders; // using Ductus.FluentDocker.Model.Builders; -// using EventStore.Client.Tests.FluentDocker; +// using Kurrent.Client.Tests.FluentDocker; // -// namespace EventStore.Client.Tests; +// namespace Kurrent.Client.Tests; // // public class EventStorePermanentTestNode(EventStoreFixtureOptions? options = null) : BaseTestNode(options) { // protected override ContainerBuilder ConfigureContainer(ContainerBuilder builder) { @@ -40,8 +40,10 @@ using Ductus.FluentDocker.Model.Builders; using Ductus.FluentDocker.Services.Extensions; using EventStore.Client; -using EventStore.Client.Tests.FluentDocker; +using Kurrent.Client; +using Kurrent.Client.Tests.FluentDocker; using Humanizer; +using Kurrent.Client.Tests; using Serilog; using Serilog.Extensions.Logging; using static System.TimeSpan; @@ -60,7 +62,7 @@ public static KurrentFixtureOptions DefaultOptions() { var port = NetworkPortProvider.NextAvailablePort; - var defaultSettings = EventStoreClientSettings + var defaultSettings = KurrentClientSettings .Create(connString.Replace("{port}", $"{port}")) .With(x => x.LoggerFactory = new SerilogLoggerFactory(Log.Logger)) .With(x => x.DefaultDeadline = Application.DebuggerIsAttached ? new TimeSpan?() : FromSeconds(30)) diff --git a/test/EventStore.Client.Tests.Common/Fixtures/KurrentTemporaryFixture.Helpers.cs b/test/Kurrent.Client.Tests.Common/Fixtures/KurrentTemporaryFixture.Helpers.cs similarity index 95% rename from test/EventStore.Client.Tests.Common/Fixtures/KurrentTemporaryFixture.Helpers.cs rename to test/Kurrent.Client.Tests.Common/Fixtures/KurrentTemporaryFixture.Helpers.cs index 3090742b9..31da9f154 100644 --- a/test/EventStore.Client.Tests.Common/Fixtures/KurrentTemporaryFixture.Helpers.cs +++ b/test/Kurrent.Client.Tests.Common/Fixtures/KurrentTemporaryFixture.Helpers.cs @@ -1,14 +1,15 @@ using System.Runtime.CompilerServices; using System.Text; +using EventStore.Client; -namespace EventStore.Client.Tests.TestNode; +namespace Kurrent.Client.Tests.TestNode; public partial class KurrentTemporaryFixture { public const string TestEventType = "test-event-type"; public const string AnotherTestEventTypePrefix = "another"; public const string AnotherTestEventType = $"{AnotherTestEventTypePrefix}-test-event-type"; - public T NewClient(Action configure) where T : EventStoreClientBase, new() => + public T NewClient(Action configure) where T : KurrentClientBase, new() => (T)Activator.CreateInstance(typeof(T), [ClientSettings.With(configure)])!; public string GetStreamName([CallerMemberName] string? testMethod = null) => diff --git a/test/EventStore.Client.Tests.Common/Fixtures/KurrentTemporaryFixture.cs b/test/Kurrent.Client.Tests.Common/Fixtures/KurrentTemporaryFixture.cs similarity index 82% rename from test/EventStore.Client.Tests.Common/Fixtures/KurrentTemporaryFixture.cs rename to test/Kurrent.Client.Tests.Common/Fixtures/KurrentTemporaryFixture.cs index e7e6bb889..c0c054e1c 100644 --- a/test/EventStore.Client.Tests.Common/Fixtures/KurrentTemporaryFixture.cs +++ b/test/Kurrent.Client.Tests.Common/Fixtures/KurrentTemporaryFixture.cs @@ -4,11 +4,13 @@ using Ductus.FluentDocker.Builders; using Ductus.FluentDocker.Extensions; using Ductus.FluentDocker.Services.Extensions; -using EventStore.Client.Tests.FluentDocker; +using EventStore.Client; +using Kurrent.Client.Tests.FluentDocker; using Serilog; using static System.TimeSpan; +using KurrentClient = EventStore.Client.KurrentClient; -namespace EventStore.Client.Tests.TestNode; +namespace Kurrent.Client.Tests.TestNode; [PublicAPI] public partial class KurrentTemporaryFixture : IAsyncLifetime, IAsyncDisposable { @@ -45,11 +47,11 @@ protected KurrentTemporaryFixture(ConfigureFixture configure) { public Version EventStoreVersion { get; private set; } = null!; public bool EventStoreHasLastStreamPosition { get; private set; } - public EventStoreClient Streams { get; private set; } = null!; - public EventStoreUserManagementClient Users { get; private set; } = null!; - public EventStoreProjectionManagementClient Projections { get; private set; } = null!; - public EventStorePersistentSubscriptionsClient Subscriptions { get; private set; } = null!; - public EventStoreOperationsClient Operations { get; private set; } = null!; + public KurrentClient Streams { get; private set; } = null!; + public KurrentUserManagementClient Users { get; private set; } = null!; + public KurrentProjectionManagementClient Projections { get; private set; } = null!; + public KurrentPersistentSubscriptionsClient Subscriptions { get; private set; } = null!; + public KurrentOperationsClient Operations { get; private set; } = null!; public bool SkipPsWarmUp { get; set; } @@ -59,7 +61,7 @@ protected KurrentTemporaryFixture(ConfigureFixture configure) { /// /// must test this /// - public EventStoreClientSettings ClientSettings => + public KurrentClientSettings ClientSettings => new() { Interceptors = Options.ClientSettings.Interceptors, ConnectionName = Options.ClientSettings.ConnectionName, @@ -101,14 +103,14 @@ public async Task InitializeAsync() { Logger.Warning("*** Warmup started ***"); await Task.WhenAll( - InitClient(async x => Users = await x.WarmUp()), - InitClient(async x => Streams = await x.WarmUp()), - InitClient( + InitClient(async x => Users = await x.WarmUp()), + InitClient(async x => Streams = await x.WarmUp()), + InitClient( async x => Projections = await x.WarmUp(), Options.Environment["EVENTSTORE_RUN_PROJECTIONS"] != "None" ), - InitClient(async x => Subscriptions = SkipPsWarmUp ? x : await x.WarmUp()), - InitClient(async x => Operations = await x.WarmUp()) + InitClient(async x => Subscriptions = SkipPsWarmUp ? x : await x.WarmUp()), + InitClient(async x => Operations = await x.WarmUp()) ); WarmUpCompleted.EnsureCalledOnce(); @@ -125,7 +127,7 @@ await Task.WhenAll( return; - async Task InitClient(Func action, bool execute = true) where T : EventStoreClientBase { + async Task InitClient(Func action, bool execute = true) where T : KurrentClientBase { if (!execute) return default(T)!; var client = (Activator.CreateInstance(typeof(T), ClientSettings) as T)!; diff --git a/test/EventStore.Client.Tests.Common/Fixtures/KurrentTemporaryTestNode.cs b/test/Kurrent.Client.Tests.Common/Fixtures/KurrentTemporaryTestNode.cs similarity index 96% rename from test/EventStore.Client.Tests.Common/Fixtures/KurrentTemporaryTestNode.cs rename to test/Kurrent.Client.Tests.Common/Fixtures/KurrentTemporaryTestNode.cs index ca7ef8cc0..06b3663eb 100644 --- a/test/EventStore.Client.Tests.Common/Fixtures/KurrentTemporaryTestNode.cs +++ b/test/Kurrent.Client.Tests.Common/Fixtures/KurrentTemporaryTestNode.cs @@ -2,9 +2,9 @@ // using Ductus.FluentDocker.Builders; // using Ductus.FluentDocker.Model.Builders; -// using EventStore.Client.Tests.FluentDocker; +// using Kurrent.Client.Tests.FluentDocker; // -// namespace EventStore.Client.Tests.TestNode; +// namespace Kurrent.Client.Tests.TestNode; // // public class EventStoreTemporaryTestNode(EventStoreFixtureOptions? options = null) : BaseTestNode(options) { // protected override ContainerBuilder ConfigureContainer(ContainerBuilder builder) { @@ -38,13 +38,14 @@ using Ductus.FluentDocker.Extensions; using Ductus.FluentDocker.Model.Builders; using Ductus.FluentDocker.Services.Extensions; -using EventStore.Client.Tests.FluentDocker; +using EventStore.Client; +using Kurrent.Client.Tests.FluentDocker; using Humanizer; using Serilog; using Serilog.Extensions.Logging; using static System.TimeSpan; -namespace EventStore.Client.Tests.TestNode; +namespace Kurrent.Client.Tests.TestNode; public class KurrentTemporaryTestNode(KurrentFixtureOptions? options = null) : TestContainerService { static readonly NetworkPortProvider NetworkPortProvider = new(NetworkPortProvider.DefaultEsdbPort); @@ -60,7 +61,7 @@ public static KurrentFixtureOptions DefaultOptions() { var port = NetworkPortProvider.NextAvailablePort; - var defaultSettings = EventStoreClientSettings + var defaultSettings = KurrentClientSettings .Create(connString.Replace("{port}", $"{port}")) .With(x => x.LoggerFactory = new SerilogLoggerFactory(Log.Logger)) .With(x => x.DefaultDeadline = Application.DebuggerIsAttached ? new TimeSpan?() : FromSeconds(30)) diff --git a/test/EventStore.Client.Tests.Common/FluentDocker/FluentDockerBuilderExtensions.cs b/test/Kurrent.Client.Tests.Common/FluentDocker/FluentDockerBuilderExtensions.cs similarity index 98% rename from test/EventStore.Client.Tests.Common/FluentDocker/FluentDockerBuilderExtensions.cs rename to test/Kurrent.Client.Tests.Common/FluentDocker/FluentDockerBuilderExtensions.cs index 3dd450676..506b4e98f 100644 --- a/test/EventStore.Client.Tests.Common/FluentDocker/FluentDockerBuilderExtensions.cs +++ b/test/Kurrent.Client.Tests.Common/FluentDocker/FluentDockerBuilderExtensions.cs @@ -8,7 +8,7 @@ using Polly.Contrib.WaitAndRetry; using static System.TimeSpan; -namespace EventStore.Client.Tests.FluentDocker; +namespace Kurrent.Client.Tests.FluentDocker; public static class FluentDockerBuilderExtensions { public static CompositeBuilder OverrideConfiguration(this CompositeBuilder compositeBuilder, Action configure) { diff --git a/test/EventStore.Client.Tests.Common/FluentDocker/FluentDockerServiceExtensions.cs b/test/Kurrent.Client.Tests.Common/FluentDocker/FluentDockerServiceExtensions.cs similarity index 98% rename from test/EventStore.Client.Tests.Common/FluentDocker/FluentDockerServiceExtensions.cs rename to test/Kurrent.Client.Tests.Common/FluentDocker/FluentDockerServiceExtensions.cs index 0546c4e04..b14bb7a43 100644 --- a/test/EventStore.Client.Tests.Common/FluentDocker/FluentDockerServiceExtensions.cs +++ b/test/Kurrent.Client.Tests.Common/FluentDocker/FluentDockerServiceExtensions.cs @@ -8,7 +8,7 @@ using Ductus.FluentDocker.Services; using Ductus.FluentDocker.Services.Extensions; -namespace EventStore.Client.Tests.FluentDocker; +namespace Kurrent.Client.Tests.FluentDocker; public static class FluentDockerServiceExtensions { static readonly TimeSpan DefaultRetryDelay = TimeSpan.FromMilliseconds(100); diff --git a/test/EventStore.Client.Tests.Common/FluentDocker/TestBypassService.cs b/test/Kurrent.Client.Tests.Common/FluentDocker/TestBypassService.cs similarity index 97% rename from test/EventStore.Client.Tests.Common/FluentDocker/TestBypassService.cs rename to test/Kurrent.Client.Tests.Common/FluentDocker/TestBypassService.cs index 3505eb9af..d09069d48 100644 --- a/test/EventStore.Client.Tests.Common/FluentDocker/TestBypassService.cs +++ b/test/Kurrent.Client.Tests.Common/FluentDocker/TestBypassService.cs @@ -2,7 +2,7 @@ using Ductus.FluentDocker.Common; using Ductus.FluentDocker.Services; -namespace EventStore.Client.Tests.FluentDocker; +namespace Kurrent.Client.Tests.FluentDocker; public class TestBypassService : TestService { protected override BypassBuilder Configure() => throw new NotImplementedException(); diff --git a/test/EventStore.Client.Tests.Common/FluentDocker/TestCompositeService.cs b/test/Kurrent.Client.Tests.Common/FluentDocker/TestCompositeService.cs similarity index 77% rename from test/EventStore.Client.Tests.Common/FluentDocker/TestCompositeService.cs rename to test/Kurrent.Client.Tests.Common/FluentDocker/TestCompositeService.cs index 262133e9f..a321ad77d 100644 --- a/test/EventStore.Client.Tests.Common/FluentDocker/TestCompositeService.cs +++ b/test/Kurrent.Client.Tests.Common/FluentDocker/TestCompositeService.cs @@ -1,6 +1,6 @@ using Ductus.FluentDocker.Builders; using Ductus.FluentDocker.Services; -namespace EventStore.Client.Tests.FluentDocker; +namespace Kurrent.Client.Tests.FluentDocker; public abstract class TestCompositeService : TestService; diff --git a/test/EventStore.Client.Tests.Common/FluentDocker/TestContainerService.cs b/test/Kurrent.Client.Tests.Common/FluentDocker/TestContainerService.cs similarity index 77% rename from test/EventStore.Client.Tests.Common/FluentDocker/TestContainerService.cs rename to test/Kurrent.Client.Tests.Common/FluentDocker/TestContainerService.cs index ae37c353d..fb135276e 100644 --- a/test/EventStore.Client.Tests.Common/FluentDocker/TestContainerService.cs +++ b/test/Kurrent.Client.Tests.Common/FluentDocker/TestContainerService.cs @@ -1,6 +1,6 @@ using Ductus.FluentDocker.Builders; using Ductus.FluentDocker.Services; -namespace EventStore.Client.Tests.FluentDocker; +namespace Kurrent.Client.Tests.FluentDocker; public abstract class TestContainerService : TestService; diff --git a/test/EventStore.Client.Tests.Common/FluentDocker/TestService.cs b/test/Kurrent.Client.Tests.Common/FluentDocker/TestService.cs similarity index 98% rename from test/EventStore.Client.Tests.Common/FluentDocker/TestService.cs rename to test/Kurrent.Client.Tests.Common/FluentDocker/TestService.cs index 890f4e9b6..4bb942517 100644 --- a/test/EventStore.Client.Tests.Common/FluentDocker/TestService.cs +++ b/test/Kurrent.Client.Tests.Common/FluentDocker/TestService.cs @@ -5,7 +5,7 @@ using Serilog; using static Serilog.Core.Constants; -namespace EventStore.Client.Tests.FluentDocker; +namespace Kurrent.Client.Tests.FluentDocker; public interface ITestService : IAsyncDisposable { Task Start(); diff --git a/test/EventStore.Client.Tests.Common/GlobalEnvironment.cs b/test/Kurrent.Client.Tests.Common/GlobalEnvironment.cs similarity index 98% rename from test/EventStore.Client.Tests.Common/GlobalEnvironment.cs rename to test/Kurrent.Client.Tests.Common/GlobalEnvironment.cs index 1fd1ee020..5444a6be4 100644 --- a/test/EventStore.Client.Tests.Common/GlobalEnvironment.cs +++ b/test/Kurrent.Client.Tests.Common/GlobalEnvironment.cs @@ -1,7 +1,7 @@ using System.Collections.Immutable; using Microsoft.Extensions.Configuration; -namespace EventStore.Client.Tests; +namespace Kurrent.Client.Tests; public static class GlobalEnvironment { static GlobalEnvironment() { diff --git a/test/EventStore.Client.Tests.Common/InterlockedBoolean.cs b/test/Kurrent.Client.Tests.Common/InterlockedBoolean.cs similarity index 100% rename from test/EventStore.Client.Tests.Common/InterlockedBoolean.cs rename to test/Kurrent.Client.Tests.Common/InterlockedBoolean.cs diff --git a/test/EventStore.Client.Tests.Common/EventStore.Client.Tests.Common.csproj b/test/Kurrent.Client.Tests.Common/Kurrent.Client.Tests.Common.csproj similarity index 77% rename from test/EventStore.Client.Tests.Common/EventStore.Client.Tests.Common.csproj rename to test/Kurrent.Client.Tests.Common/Kurrent.Client.Tests.Common.csproj index 6ebee89cb..95d43818b 100644 --- a/test/EventStore.Client.Tests.Common/EventStore.Client.Tests.Common.csproj +++ b/test/Kurrent.Client.Tests.Common/Kurrent.Client.Tests.Common.csproj @@ -1,10 +1,10 @@ - + - EventStore.Client.Tests + Kurrent.Client.Tests - + @@ -60,9 +60,24 @@ Always + + Always + + + Always + + + Always + + + Always + + + Always + - + diff --git a/test/EventStore.Client.Tests.Common/Logging.cs b/test/Kurrent.Client.Tests.Common/Logging.cs similarity index 98% rename from test/EventStore.Client.Tests.Common/Logging.cs rename to test/Kurrent.Client.Tests.Common/Logging.cs index 5742e1134..ee991d064 100644 --- a/test/EventStore.Client.Tests.Common/Logging.cs +++ b/test/Kurrent.Client.Tests.Common/Logging.cs @@ -6,7 +6,7 @@ using Serilog.Formatting.Display; using Xunit.Sdk; -namespace EventStore.Client.Tests; +namespace Kurrent.Client.Tests; static class Logging { static readonly Subject LogEventSubject = new(); diff --git a/test/EventStore.Client.Tests.Common/PasswordGenerator.cs b/test/Kurrent.Client.Tests.Common/PasswordGenerator.cs similarity index 98% rename from test/EventStore.Client.Tests.Common/PasswordGenerator.cs rename to test/Kurrent.Client.Tests.Common/PasswordGenerator.cs index f8990b18d..bbf019ce4 100644 --- a/test/EventStore.Client.Tests.Common/PasswordGenerator.cs +++ b/test/Kurrent.Client.Tests.Common/PasswordGenerator.cs @@ -1,6 +1,6 @@ using System.Text; -namespace EventStore.Client.Tests; +namespace Kurrent.Client.Tests; static class PasswordGenerator { static PasswordGenerator() { diff --git a/test/EventStore.Client.Tests.Common/Shouldly/ShouldThrowAsyncExtensions.cs b/test/Kurrent.Client.Tests.Common/Shouldly/ShouldThrowAsyncExtensions.cs similarity index 80% rename from test/EventStore.Client.Tests.Common/Shouldly/ShouldThrowAsyncExtensions.cs rename to test/Kurrent.Client.Tests.Common/Shouldly/ShouldThrowAsyncExtensions.cs index f5ec36636..3411cd4b9 100644 --- a/test/EventStore.Client.Tests.Common/Shouldly/ShouldThrowAsyncExtensions.cs +++ b/test/Kurrent.Client.Tests.Common/Shouldly/ShouldThrowAsyncExtensions.cs @@ -7,6 +7,6 @@ namespace Shouldly; [DebuggerStepThrough] public static class ShouldThrowAsyncExtensions { - public static Task ShouldThrowAsync(this EventStoreClient.ReadStreamResult source) where TException : Exception => + public static Task ShouldThrowAsync(this KurrentClient.ReadStreamResult source) where TException : Exception => source.ToArrayAsync().AsTask().ShouldThrowAsync(); } diff --git a/test/EventStore.Client.Tests.Common/TestCaseGenerator.cs b/test/Kurrent.Client.Tests.Common/TestCaseGenerator.cs similarity index 95% rename from test/EventStore.Client.Tests.Common/TestCaseGenerator.cs rename to test/Kurrent.Client.Tests.Common/TestCaseGenerator.cs index 781d19063..b56c95794 100644 --- a/test/EventStore.Client.Tests.Common/TestCaseGenerator.cs +++ b/test/Kurrent.Client.Tests.Common/TestCaseGenerator.cs @@ -1,4 +1,4 @@ -namespace EventStore.Client.Tests; +namespace Kurrent.Client.Tests; using System.Collections; using Bogus; diff --git a/test/EventStore.Client.Tests.Common/TestCredentials.cs b/test/Kurrent.Client.Tests.Common/TestCredentials.cs similarity index 88% rename from test/EventStore.Client.Tests.Common/TestCredentials.cs rename to test/Kurrent.Client.Tests.Common/TestCredentials.cs index b3d075ba4..9f68ea68b 100644 --- a/test/EventStore.Client.Tests.Common/TestCredentials.cs +++ b/test/Kurrent.Client.Tests.Common/TestCredentials.cs @@ -1,4 +1,6 @@ -namespace EventStore.Client.Tests; +using EventStore.Client; + +namespace Kurrent.Client.Tests; public static class TestCredentials { public static readonly UserCredentials Root = new("admin", "changeit"); diff --git a/test/EventStore.Client.Tests.Common/appsettings.Development.json b/test/Kurrent.Client.Tests.Common/appsettings.Development.json similarity index 100% rename from test/EventStore.Client.Tests.Common/appsettings.Development.json rename to test/Kurrent.Client.Tests.Common/appsettings.Development.json diff --git a/test/EventStore.Client.Tests.Common/appsettings.json b/test/Kurrent.Client.Tests.Common/appsettings.json similarity index 100% rename from test/EventStore.Client.Tests.Common/appsettings.json rename to test/Kurrent.Client.Tests.Common/appsettings.json diff --git a/test/EventStore.Client.Tests.Common/docker-compose.certs.yml b/test/Kurrent.Client.Tests.Common/docker-compose.certs.yml similarity index 100% rename from test/EventStore.Client.Tests.Common/docker-compose.certs.yml rename to test/Kurrent.Client.Tests.Common/docker-compose.certs.yml diff --git a/test/EventStore.Client.Tests.Common/docker-compose.cluster.yml b/test/Kurrent.Client.Tests.Common/docker-compose.cluster.yml similarity index 100% rename from test/EventStore.Client.Tests.Common/docker-compose.cluster.yml rename to test/Kurrent.Client.Tests.Common/docker-compose.cluster.yml diff --git a/test/EventStore.Client.Tests.Common/docker-compose.node.yml b/test/Kurrent.Client.Tests.Common/docker-compose.node.yml similarity index 100% rename from test/EventStore.Client.Tests.Common/docker-compose.node.yml rename to test/Kurrent.Client.Tests.Common/docker-compose.node.yml diff --git a/test/EventStore.Client.Tests.Common/docker-compose.yml b/test/Kurrent.Client.Tests.Common/docker-compose.yml similarity index 100% rename from test/EventStore.Client.Tests.Common/docker-compose.yml rename to test/Kurrent.Client.Tests.Common/docker-compose.yml diff --git a/test/EventStore.Client.Tests.Common/shared.env b/test/Kurrent.Client.Tests.Common/shared.env similarity index 100% rename from test/EventStore.Client.Tests.Common/shared.env rename to test/Kurrent.Client.Tests.Common/shared.env diff --git a/test/EventStore.Client.Tests/Assertions/ComparableAssertion.cs b/test/Kurrent.Client.Tests/Assertions/ComparableAssertion.cs similarity index 99% rename from test/EventStore.Client.Tests/Assertions/ComparableAssertion.cs rename to test/Kurrent.Client.Tests/Assertions/ComparableAssertion.cs index a41bd9fa9..cecfe6599 100644 --- a/test/EventStore.Client.Tests/Assertions/ComparableAssertion.cs +++ b/test/Kurrent.Client.Tests/Assertions/ComparableAssertion.cs @@ -3,7 +3,7 @@ using AutoFixture.Kernel; // ReSharper disable once CheckNamespace -namespace EventStore.Client; +namespace Kurrent.Client; class ComparableAssertion : CompositeIdiomaticAssertion { public ComparableAssertion(ISpecimenBuilder builder) : base(CreateChildrenAssertions(builder)) { } diff --git a/test/EventStore.Client.Tests/Assertions/EqualityAssertion.cs b/test/Kurrent.Client.Tests/Assertions/EqualityAssertion.cs similarity index 98% rename from test/EventStore.Client.Tests/Assertions/EqualityAssertion.cs rename to test/Kurrent.Client.Tests/Assertions/EqualityAssertion.cs index 69ab7aed6..6c172af7f 100644 --- a/test/EventStore.Client.Tests/Assertions/EqualityAssertion.cs +++ b/test/Kurrent.Client.Tests/Assertions/EqualityAssertion.cs @@ -2,7 +2,7 @@ using AutoFixture.Kernel; // ReSharper disable once CheckNamespace -namespace EventStore.Client; +namespace Kurrent.Client; class EqualityAssertion : CompositeIdiomaticAssertion { public EqualityAssertion(ISpecimenBuilder builder) : base(CreateChildrenAssertions(builder)) { } diff --git a/test/EventStore.Client.Tests/Assertions/NullArgumentAssertion.cs b/test/Kurrent.Client.Tests/Assertions/NullArgumentAssertion.cs similarity index 97% rename from test/EventStore.Client.Tests/Assertions/NullArgumentAssertion.cs rename to test/Kurrent.Client.Tests/Assertions/NullArgumentAssertion.cs index 866ce8a47..4efb616d5 100644 --- a/test/EventStore.Client.Tests/Assertions/NullArgumentAssertion.cs +++ b/test/Kurrent.Client.Tests/Assertions/NullArgumentAssertion.cs @@ -3,7 +3,7 @@ using AutoFixture.Kernel; // ReSharper disable once CheckNamespace -namespace EventStore.Client; +namespace Kurrent.Client; class NullArgumentAssertion : IdiomaticAssertion { readonly ISpecimenBuilder _builder; diff --git a/test/EventStore.Client.Tests/Assertions/StringConversionAssertion.cs b/test/Kurrent.Client.Tests/Assertions/StringConversionAssertion.cs similarity index 97% rename from test/EventStore.Client.Tests/Assertions/StringConversionAssertion.cs rename to test/Kurrent.Client.Tests/Assertions/StringConversionAssertion.cs index 302803c51..fa4da4970 100644 --- a/test/EventStore.Client.Tests/Assertions/StringConversionAssertion.cs +++ b/test/Kurrent.Client.Tests/Assertions/StringConversionAssertion.cs @@ -3,7 +3,7 @@ using AutoFixture.Kernel; // ReSharper disable once CheckNamespace -namespace EventStore.Client; +namespace Kurrent.Client; class StringConversionAssertion : IdiomaticAssertion { readonly ISpecimenBuilder _builder; diff --git a/test/EventStore.Client.Tests/Assertions/ValueObjectAssertion.cs b/test/Kurrent.Client.Tests/Assertions/ValueObjectAssertion.cs similarity index 95% rename from test/EventStore.Client.Tests/Assertions/ValueObjectAssertion.cs rename to test/Kurrent.Client.Tests/Assertions/ValueObjectAssertion.cs index 3ff92b7c9..ee3fce80d 100644 --- a/test/EventStore.Client.Tests/Assertions/ValueObjectAssertion.cs +++ b/test/Kurrent.Client.Tests/Assertions/ValueObjectAssertion.cs @@ -2,7 +2,7 @@ using AutoFixture.Kernel; // ReSharper disable once CheckNamespace -namespace EventStore.Client; +namespace Kurrent.Client; class ValueObjectAssertion : CompositeIdiomaticAssertion { public ValueObjectAssertion(ISpecimenBuilder builder) : base(CreateChildrenAssertions(builder)) { } diff --git a/test/EventStore.Client.Tests/AutoScenarioDataAttribute.cs b/test/Kurrent.Client.Tests/AutoScenarioDataAttribute.cs similarity index 95% rename from test/EventStore.Client.Tests/AutoScenarioDataAttribute.cs rename to test/Kurrent.Client.Tests/AutoScenarioDataAttribute.cs index 1bad5bce5..166e5b7ef 100644 --- a/test/EventStore.Client.Tests/AutoScenarioDataAttribute.cs +++ b/test/Kurrent.Client.Tests/AutoScenarioDataAttribute.cs @@ -3,7 +3,7 @@ using AutoFixture.Xunit2; using Xunit.Sdk; -namespace EventStore.Client.Tests; +namespace Kurrent.Client.Tests; [DataDiscoverer("AutoFixture.Xunit2.NoPreDiscoveryDataDiscoverer", "AutoFixture.Xunit2")] public class AutoScenarioDataAttribute : DataAttribute { diff --git a/test/EventStore.Client.Tests/ClientCertificatesTests.cs b/test/Kurrent.Client.Tests/ClientCertificatesTests.cs similarity index 88% rename from test/EventStore.Client.Tests/ClientCertificatesTests.cs rename to test/Kurrent.Client.Tests/ClientCertificatesTests.cs index e2e1a248c..61ec9dcd4 100644 --- a/test/EventStore.Client.Tests/ClientCertificatesTests.cs +++ b/test/Kurrent.Client.Tests/ClientCertificatesTests.cs @@ -1,21 +1,22 @@ +using EventStore.Client; using Humanizer; -namespace EventStore.Client.Tests; +namespace Kurrent.Client.Tests; [Trait("Category", "Target:Plugins")] [Trait("Category", "Type:UserCertificate")] public class ClientCertificateTests(ITestOutputHelper output, KurrentPermanentFixture fixture) - : EventStorePermanentTests(output, fixture) { + : KurrentPermanentTests(output, fixture) { [SupportsPlugins.Theory(EventStoreRepository.Commercial, "This server version does not support plugins"), BadClientCertificatesTestCases] async Task bad_certificates_combinations_should_return_authentication_error(string userCertFile, string userKeyFile, string tlsCaFile) { var stream = Fixture.GetStreamName(); var seedEvents = Fixture.CreateTestEvents(); var connectionString = $"esdb://localhost:2113/?tls=true&userCertFile={userCertFile}&userKeyFile={userKeyFile}&tlsCaFile={tlsCaFile}"; - var settings = EventStoreClientSettings.Create(connectionString); + var settings = KurrentClientSettings.Create(connectionString); settings.ConnectivitySettings.TlsVerifyCert.ShouldBeTrue(); - await using var client = new EventStoreClient(settings); + await using var client = new KurrentClient(settings); await client.AppendToStreamAsync(stream, StreamState.NoStream, seedEvents).ShouldThrowAsync(); } @@ -26,10 +27,10 @@ async Task valid_certificates_combinations_should_write_to_stream(string userCer var seedEvents = Fixture.CreateTestEvents(); var connectionString = $"esdb://localhost:2113/?userCertFile={userCertFile}&userKeyFile={userKeyFile}&tlsCaFile={tlsCaFile}"; - var settings = EventStoreClientSettings.Create(connectionString); + var settings = KurrentClientSettings.Create(connectionString); settings.ConnectivitySettings.TlsVerifyCert.ShouldBeTrue(); - await using var client = new EventStoreClient(settings); + await using var client = new KurrentClient(settings); var result = await client.AppendToStreamAsync(stream, StreamState.NoStream, seedEvents); result.ShouldNotBeNull(); @@ -41,10 +42,10 @@ async Task basic_authentication_should_take_precedence(string userCertFile, stri var seedEvents = Fixture.CreateTestEvents(); var connectionString = $"esdb://admin:changeit@localhost:2113/?userCertFile={userCertFile}&userKeyFile={userKeyFile}&tlsCaFile={tlsCaFile}"; - var settings = EventStoreClientSettings.Create(connectionString); + var settings = KurrentClientSettings.Create(connectionString); settings.ConnectivitySettings.TlsVerifyCert.ShouldBeTrue(); - await using var client = new EventStoreClient(settings); + await using var client = new KurrentClient(settings); var result = await client.AppendToStreamAsync(stream, StreamState.NoStream, seedEvents); result.ShouldNotBeNull(); diff --git a/test/EventStore.Client.Tests/ConnectionStringTests.cs b/test/Kurrent.Client.Tests/ConnectionStringTests.cs similarity index 75% rename from test/EventStore.Client.Tests/ConnectionStringTests.cs rename to test/Kurrent.Client.Tests/ConnectionStringTests.cs index 15b86703c..2d3d09ab2 100644 --- a/test/EventStore.Client.Tests/ConnectionStringTests.cs +++ b/test/Kurrent.Client.Tests/ConnectionStringTests.cs @@ -1,13 +1,12 @@ using System.Net; +using System.Net.Http; using System.Reflection; using System.Security.Cryptography.X509Certificates; using AutoFixture; +using EventStore.Client; +using HashCode = EventStore.Client.HashCode; -#if NET48 -using System.Net.Http; -#endif - -namespace EventStore.Client.Tests; +namespace Kurrent.Client.Tests; public class ConnectionStringTests { public static IEnumerable ValidCases() { @@ -27,14 +26,14 @@ public class ConnectionStringTests { return Enumerable.Range(0, 3).SelectMany(GetTestCases); IEnumerable GetTestCases(int _) { - var settings = new EventStoreClientSettings { + var settings = new KurrentClientSettings { ConnectionName = fixture.Create(), - ConnectivitySettings = fixture.Create(), - OperationOptions = fixture.Create() + ConnectivitySettings = fixture.Create(), + OperationOptions = fixture.Create() }; settings.ConnectivitySettings.Address = - new UriBuilder(EventStoreClientConnectivitySettings.Default.ResolvedAddressOrDefault) { + new UriBuilder(KurrentClientConnectivitySettings.Default.ResolvedAddressOrDefault) { Scheme = settings.ConnectivitySettings.ResolvedAddressOrDefault.Scheme }.Uri; @@ -48,14 +47,14 @@ public class ConnectionStringTests { settings }; - var ipGossipSettings = new EventStoreClientSettings { + var ipGossipSettings = new KurrentClientSettings { ConnectionName = fixture.Create(), - ConnectivitySettings = fixture.Create(), - OperationOptions = fixture.Create() + ConnectivitySettings = fixture.Create(), + OperationOptions = fixture.Create() }; ipGossipSettings.ConnectivitySettings.Address = - new UriBuilder(EventStoreClientConnectivitySettings.Default.ResolvedAddressOrDefault) { + new UriBuilder(KurrentClientConnectivitySettings.Default.ResolvedAddressOrDefault) { Scheme = ipGossipSettings.ConnectivitySettings.ResolvedAddressOrDefault.Scheme }.Uri; @@ -71,10 +70,10 @@ public class ConnectionStringTests { ipGossipSettings }; - var singleNodeSettings = new EventStoreClientSettings { + var singleNodeSettings = new KurrentClientSettings { ConnectionName = fixture.Create(), - ConnectivitySettings = fixture.Create(), - OperationOptions = fixture.Create() + ConnectivitySettings = fixture.Create(), + OperationOptions = fixture.Create() }; singleNodeSettings.ConnectivitySettings.DnsGossipSeeds = null; @@ -99,18 +98,18 @@ public class ConnectionStringTests { [Theory] [MemberData(nameof(ValidCases))] - public void valid_connection_string(string connectionString, EventStoreClientSettings expected) { - var result = EventStoreClientSettings.Create(connectionString); + public void valid_connection_string(string connectionString, KurrentClientSettings expected) { + var result = KurrentClientSettings.Create(connectionString); - Assert.Equal(expected, result, EventStoreClientSettingsEqualityComparer.Instance); + Assert.Equal(expected, result, KurrentClientSettingsEqualityComparer.Instance); } [Theory] [MemberData(nameof(ValidCases))] - public void valid_connection_string_with_empty_path(string connectionString, EventStoreClientSettings expected) { - var result = EventStoreClientSettings.Create(connectionString.Replace("?", "/?")); + public void valid_connection_string_with_empty_path(string connectionString, KurrentClientSettings expected) { + var result = KurrentClientSettings.Create(connectionString.Replace("?", "/?")); - Assert.Equal(expected, result, EventStoreClientSettingsEqualityComparer.Instance); + Assert.Equal(expected, result, KurrentClientSettingsEqualityComparer.Instance); } #if !GRPC_CORE @@ -119,7 +118,7 @@ public void valid_connection_string_with_empty_path(string connectionString, Eve [InlineData(true)] public void tls_verify_cert(bool tlsVerifyCert) { var connectionString = $"esdb://localhost:2113/?tlsVerifyCert={tlsVerifyCert}"; - var result = EventStoreClientSettings.Create(connectionString); + var result = KurrentClientSettings.Create(connectionString); using var handler = result.CreateHttpMessageHandler?.Invoke(); #if NET var socketsHandler = Assert.IsType(handler); @@ -159,7 +158,7 @@ public void tls_verify_cert(bool tlsVerifyCert) { [MemberData(nameof(InvalidTlsCertificates))] public void connection_string_with_invalid_tls_certificate_should_throw(string clientCertificatePath) { Assert.Throws( - () => EventStoreClientSettings.Create($"esdb://admin:changeit@localhost:2113/?tls=true&tlsVerifyCert=true&tlsCAFile={clientCertificatePath}") + () => KurrentClientSettings.Create($"esdb://admin:changeit@localhost:2113/?tls=true&tlsVerifyCert=true&tlsCAFile={clientCertificatePath}") ); } @@ -174,7 +173,7 @@ public void connection_string_with_invalid_tls_certificate_should_throw(string c [MemberData(nameof(InvalidClientCertificates))] public void connection_string_with_invalid_client_certificate_should_throw(string userCertFile, string userKeyFile) { Assert.Throws( - () => EventStoreClientSettings.Create( + () => KurrentClientSettings.Create( $"esdb://admin:changeit@localhost:2113/?tls=true&tlsVerifyCert=true&userCertFile={userCertFile}&userKeyFile={userKeyFile}" ) ); @@ -182,7 +181,7 @@ public void connection_string_with_invalid_client_certificate_should_throw(strin [RetryFact] public void infinite_grpc_timeouts() { - var result = EventStoreClientSettings.Create("esdb://localhost:2113?keepAliveInterval=-1&keepAliveTimeout=-1"); + var result = KurrentClientSettings.Create("esdb://localhost:2113?keepAliveInterval=-1&keepAliveTimeout=-1"); Assert.Equal(System.Threading.Timeout.InfiniteTimeSpan, result.ConnectivitySettings.KeepAliveInterval); Assert.Equal(System.Threading.Timeout.InfiniteTimeSpan, result.ConnectivitySettings.KeepAliveTimeout); @@ -201,21 +200,21 @@ public void infinite_grpc_timeouts() { } [RetryFact] - public void connection_string_with_no_schema() => Assert.Throws(() => EventStoreClientSettings.Create(":so/mething/random")); + public void connection_string_with_no_schema() => Assert.Throws(() => KurrentClientSettings.Create(":so/mething/random")); [Theory] [InlineData("esdbwrong://")] [InlineData("wrong://")] [InlineData("badesdb://")] public void connection_string_with_invalid_scheme_should_throw(string connectionString) => - Assert.Throws(() => EventStoreClientSettings.Create(connectionString)); + Assert.Throws(() => KurrentClientSettings.Create(connectionString)); [Theory] [InlineData("esdb://userpass@127.0.0.1/")] [InlineData("esdb://user:pa:ss@127.0.0.1/")] [InlineData("esdb://us:er:pa:ss@127.0.0.1/")] public void connection_string_with_invalid_userinfo_should_throw(string connectionString) => - Assert.Throws(() => EventStoreClientSettings.Create(connectionString)); + Assert.Throws(() => KurrentClientSettings.Create(connectionString)); [Theory] [InlineData("esdb://user:pass@127.0.0.1:abc")] @@ -229,14 +228,14 @@ public void connection_string_with_invalid_userinfo_should_throw(string connecti [InlineData("esdb://user:pass@localhost:1234,,127.0.0.3:4321")] [InlineData("esdb://user:pass@localhost:1234,,127.0.0.3:4321/")] public void connection_string_with_invalid_host_should_throw(string connectionString) => - Assert.Throws(() => EventStoreClientSettings.Create(connectionString)); + Assert.Throws(() => KurrentClientSettings.Create(connectionString)); [Theory] [InlineData("esdb://user:pass@127.0.0.1/test")] [InlineData("esdb://user:pass@127.0.0.1/maxDiscoverAttempts=10")] [InlineData("esdb://user:pass@127.0.0.1/hello?maxDiscoverAttempts=10")] public void connection_string_with_non_empty_path_should_throw(string connectionString) => - Assert.Throws(() => EventStoreClientSettings.Create(connectionString)); + Assert.Throws(() => KurrentClientSettings.Create(connectionString)); [Theory] [InlineData("esdb://user:pass@127.0.0.1")] @@ -244,19 +243,19 @@ public void connection_string_with_non_empty_path_should_throw(string connection [InlineData("esdb+discover://user:pass@127.0.0.1")] [InlineData("esdb+discover://user:pass@127.0.0.1/")] public void connection_string_with_no_key_value_pairs_specified_should_not_throw(string connectionString) => - EventStoreClientSettings.Create(connectionString); + KurrentClientSettings.Create(connectionString); [Theory] [InlineData("esdb://user:pass@127.0.0.1/?maxDiscoverAttempts=12=34")] [InlineData("esdb://user:pass@127.0.0.1/?maxDiscoverAttempts1234")] public void connection_string_with_invalid_key_value_pair_should_throw(string connectionString) => - Assert.Throws(() => EventStoreClientSettings.Create(connectionString)); + Assert.Throws(() => KurrentClientSettings.Create(connectionString)); [Theory] [InlineData("esdb://user:pass@127.0.0.1/?maxDiscoverAttempts=1234&MaxDiscoverAttempts=10")] [InlineData("esdb://user:pass@127.0.0.1/?gossipTimeout=10&gossipTimeout=30")] public void connection_string_with_duplicate_key_should_throw(string connectionString) => - Assert.Throws(() => EventStoreClientSettings.Create(connectionString)); + Assert.Throws(() => KurrentClientSettings.Create(connectionString)); [Theory] [InlineData("esdb://user:pass@127.0.0.1/?unknown=1234")] @@ -269,59 +268,59 @@ public void connection_string_with_duplicate_key_should_throw(string connectionS [InlineData("esdb://user:pass@127.0.0.1/?keepAliveInterval=-2")] [InlineData("esdb://user:pass@127.0.0.1/?keepAliveTimeout=-2")] public void connection_string_with_invalid_settings_should_throw(string connectionString) => - Assert.Throws(() => EventStoreClientSettings.Create(connectionString)); + Assert.Throws(() => KurrentClientSettings.Create(connectionString)); [RetryFact] public void with_default_settings() { - var settings = EventStoreClientSettings.Create("esdb://hostname:4321/"); + var settings = KurrentClientSettings.Create("esdb://hostname:4321/"); Assert.Null(settings.ConnectionName); Assert.Equal( - EventStoreClientConnectivitySettings.Default.ResolvedAddressOrDefault.Scheme, + KurrentClientConnectivitySettings.Default.ResolvedAddressOrDefault.Scheme, settings.ConnectivitySettings.ResolvedAddressOrDefault.Scheme ); Assert.Equal( - EventStoreClientConnectivitySettings.Default.DiscoveryInterval.TotalMilliseconds, + KurrentClientConnectivitySettings.Default.DiscoveryInterval.TotalMilliseconds, settings.ConnectivitySettings.DiscoveryInterval.TotalMilliseconds ); - Assert.Null(EventStoreClientConnectivitySettings.Default.DnsGossipSeeds); - Assert.Empty(EventStoreClientConnectivitySettings.Default.GossipSeeds); + Assert.Null(KurrentClientConnectivitySettings.Default.DnsGossipSeeds); + Assert.Empty(KurrentClientConnectivitySettings.Default.GossipSeeds); Assert.Equal( - EventStoreClientConnectivitySettings.Default.GossipTimeout.TotalMilliseconds, + KurrentClientConnectivitySettings.Default.GossipTimeout.TotalMilliseconds, settings.ConnectivitySettings.GossipTimeout.TotalMilliseconds ); - Assert.Null(EventStoreClientConnectivitySettings.Default.IpGossipSeeds); + Assert.Null(KurrentClientConnectivitySettings.Default.IpGossipSeeds); Assert.Equal( - EventStoreClientConnectivitySettings.Default.MaxDiscoverAttempts, + KurrentClientConnectivitySettings.Default.MaxDiscoverAttempts, settings.ConnectivitySettings.MaxDiscoverAttempts ); Assert.Equal( - EventStoreClientConnectivitySettings.Default.NodePreference, + KurrentClientConnectivitySettings.Default.NodePreference, settings.ConnectivitySettings.NodePreference ); Assert.Equal( - EventStoreClientConnectivitySettings.Default.Insecure, + KurrentClientConnectivitySettings.Default.Insecure, settings.ConnectivitySettings.Insecure ); Assert.Equal(TimeSpan.FromSeconds(10), settings.DefaultDeadline); Assert.Equal( - EventStoreClientOperationOptions.Default.ThrowOnAppendFailure, + KurrentClientOperationOptions.Default.ThrowOnAppendFailure, settings.OperationOptions.ThrowOnAppendFailure ); Assert.Equal( - EventStoreClientConnectivitySettings.Default.KeepAliveInterval, + KurrentClientConnectivitySettings.Default.KeepAliveInterval, settings.ConnectivitySettings.KeepAliveInterval ); Assert.Equal( - EventStoreClientConnectivitySettings.Default.KeepAliveTimeout, + KurrentClientConnectivitySettings.Default.KeepAliveTimeout, settings.ConnectivitySettings.KeepAliveTimeout ); } @@ -334,7 +333,7 @@ public void with_default_settings() { [InlineData("esdb://localhost1,localhost2,localhost3/?tls=false", false)] [InlineData("esdb://localhost1,localhost2,localhost3/?tls=true", true)] public void use_tls(string connectionString, bool expectedUseTls) { - var result = EventStoreClientSettings.Create(connectionString); + var result = KurrentClientSettings.Create(connectionString); var expectedScheme = expectedUseTls ? "https" : "http"; Assert.NotEqual(expectedUseTls, result.ConnectivitySettings.Insecure); Assert.Equal(expectedScheme, result.ConnectivitySettings.ResolvedAddressOrDefault.Scheme); @@ -360,7 +359,7 @@ public void use_tls(string connectionString, bool expectedUseTls) { [InlineData("esdb://localhost1,localhost2,localhost3/?tls=false", true, false)] [InlineData("esdb://localhost1,localhost2,localhost3/?tls=false", false, false)] public void allow_tls_override_for_single_node(string connectionString, bool? insecureOverride, bool expectedUseTls) { - var result = EventStoreClientSettings.Create(connectionString); + var result = KurrentClientSettings.Create(connectionString); var settings = result.ConnectivitySettings; if (insecureOverride.HasValue) @@ -379,7 +378,7 @@ public void allow_tls_override_for_single_node(string connectionString, bool? in [InlineData("esdb+discover://localhost:1234", null, null)] [InlineData("esdb+discover://localhost:1234,localhost:4567", null, null)] public void connection_string_with_custom_ports(string connectionString, string? expectedHost, int? expectedPort) { - var result = EventStoreClientSettings.Create(connectionString); + var result = KurrentClientSettings.Create(connectionString); var connectivitySettings = result.ConnectivitySettings; Assert.Equal(expectedHost, connectivitySettings.Address?.Host); @@ -387,17 +386,17 @@ public void connection_string_with_custom_ports(string connectionString, string? } static string GetConnectionString( - EventStoreClientSettings settings, + KurrentClientSettings settings, Func? getKey = default ) => $"{GetScheme(settings)}{GetAuthority(settings)}?{GetKeyValuePairs(settings, getKey)}"; - static string GetScheme(EventStoreClientSettings settings) => + static string GetScheme(KurrentClientSettings settings) => settings.ConnectivitySettings.IsSingleNode ? "esdb://" : "esdb+discover://"; - static string GetAuthority(EventStoreClientSettings settings) => + static string GetAuthority(KurrentClientSettings settings) => settings.ConnectivitySettings.IsSingleNode ? $"{settings.ConnectivitySettings.ResolvedAddressOrDefault.Host}:{settings.ConnectivitySettings.ResolvedAddressOrDefault.Port}" : string.Join( @@ -406,7 +405,7 @@ static string GetAuthority(EventStoreClientSettings settings) => ); static string GetKeyValuePairs( - EventStoreClientSettings settings, + KurrentClientSettings settings, Func? getKey = default ) { var pairs = new Dictionary { @@ -444,10 +443,10 @@ static string GetKeyValuePairs( return string.Join("&", pairs.Select(pair => $"{getKey?.Invoke(pair.Key) ?? pair.Key}={pair.Value}")); } - class EventStoreClientSettingsEqualityComparer : IEqualityComparer { - public static readonly EventStoreClientSettingsEqualityComparer Instance = new(); + class KurrentClientSettingsEqualityComparer : IEqualityComparer { + public static readonly KurrentClientSettingsEqualityComparer Instance = new(); - public bool Equals(EventStoreClientSettings? x, EventStoreClientSettings? y) { + public bool Equals(KurrentClientSettings? x, KurrentClientSettings? y) { if (ReferenceEquals(x, y)) return true; @@ -461,29 +460,29 @@ public bool Equals(EventStoreClientSettings? x, EventStoreClientSettings? y) { return false; return x.ConnectionName == y.ConnectionName && - EventStoreClientConnectivitySettingsEqualityComparer.Instance.Equals( + KurrentClientConnectivitySettingsEqualityComparer.Instance.Equals( x.ConnectivitySettings, y.ConnectivitySettings ) && - EventStoreClientOperationOptionsEqualityComparer.Instance.Equals( + KurrentClientOperationOptionsEqualityComparer.Instance.Equals( x.OperationOptions, y.OperationOptions ) && Equals(x.DefaultCredentials?.ToString(), y.DefaultCredentials?.ToString()); } - public int GetHashCode(EventStoreClientSettings obj) => + public int GetHashCode(KurrentClientSettings obj) => HashCode.Hash .Combine(obj.ConnectionName) - .Combine(EventStoreClientConnectivitySettingsEqualityComparer.Instance.GetHashCode(obj.ConnectivitySettings)) - .Combine(EventStoreClientOperationOptionsEqualityComparer.Instance.GetHashCode(obj.OperationOptions)); + .Combine(KurrentClientConnectivitySettingsEqualityComparer.Instance.GetHashCode(obj.ConnectivitySettings)) + .Combine(KurrentClientOperationOptionsEqualityComparer.Instance.GetHashCode(obj.OperationOptions)); } - class EventStoreClientConnectivitySettingsEqualityComparer - : IEqualityComparer { - public static readonly EventStoreClientConnectivitySettingsEqualityComparer Instance = new(); + class KurrentClientConnectivitySettingsEqualityComparer + : IEqualityComparer { + public static readonly KurrentClientConnectivitySettingsEqualityComparer Instance = new(); - public bool Equals(EventStoreClientConnectivitySettings? x, EventStoreClientConnectivitySettings? y) { + public bool Equals(KurrentClientConnectivitySettings? x, KurrentClientConnectivitySettings? y) { if (ReferenceEquals(x, y)) return true; @@ -507,7 +506,7 @@ public bool Equals(EventStoreClientConnectivitySettings? x, EventStoreClientConn x.Insecure == y.Insecure; } - public int GetHashCode(EventStoreClientConnectivitySettings obj) => + public int GetHashCode(KurrentClientConnectivitySettings obj) => obj.GossipSeeds.Aggregate( HashCode.Hash .Combine(obj.ResolvedAddressOrDefault.GetHashCode()) @@ -522,11 +521,11 @@ public int GetHashCode(EventStoreClientConnectivitySettings obj) => ); } - class EventStoreClientOperationOptionsEqualityComparer - : IEqualityComparer { - public static readonly EventStoreClientOperationOptionsEqualityComparer Instance = new(); + class KurrentClientOperationOptionsEqualityComparer + : IEqualityComparer { + public static readonly KurrentClientOperationOptionsEqualityComparer Instance = new(); - public bool Equals(EventStoreClientOperationOptions? x, EventStoreClientOperationOptions? y) { + public bool Equals(KurrentClientOperationOptions? x, KurrentClientOperationOptions? y) { if (ReferenceEquals(x, y)) return true; @@ -539,7 +538,7 @@ public bool Equals(EventStoreClientOperationOptions? x, EventStoreClientOperatio return x.GetType() == y.GetType(); } - public int GetHashCode(EventStoreClientOperationOptions obj) => + public int GetHashCode(KurrentClientOperationOptions obj) => System.HashCode.Combine(obj.ThrowOnAppendFailure); } } diff --git a/test/EventStore.Client.Tests/FromAllTests.cs b/test/Kurrent.Client.Tests/FromAllTests.cs similarity index 96% rename from test/EventStore.Client.Tests/FromAllTests.cs rename to test/Kurrent.Client.Tests/FromAllTests.cs index ae00af5dc..93adcf189 100644 --- a/test/EventStore.Client.Tests/FromAllTests.cs +++ b/test/Kurrent.Client.Tests/FromAllTests.cs @@ -1,6 +1,7 @@ using AutoFixture; +using EventStore.Client; -namespace EventStore.Client.Tests; +namespace Kurrent.Client.Tests; public class FromAllTests : ValueObjectTests { public FromAllTests() : base(new ScenarioFixture()) { } diff --git a/test/EventStore.Client.Tests/FromStreamTests.cs b/test/Kurrent.Client.Tests/FromStreamTests.cs similarity index 96% rename from test/EventStore.Client.Tests/FromStreamTests.cs rename to test/Kurrent.Client.Tests/FromStreamTests.cs index e6e1c08f8..2b0df8dae 100644 --- a/test/EventStore.Client.Tests/FromStreamTests.cs +++ b/test/Kurrent.Client.Tests/FromStreamTests.cs @@ -1,6 +1,7 @@ using AutoFixture; +using EventStore.Client; -namespace EventStore.Client.Tests; +namespace Kurrent.Client.Tests; public class FromStreamTests : ValueObjectTests { public FromStreamTests() : base(new ScenarioFixture()) { } diff --git a/test/EventStore.Client.Tests/GossipChannelSelectorTests.cs b/test/Kurrent.Client.Tests/GossipChannelSelectorTests.cs similarity index 94% rename from test/EventStore.Client.Tests/GossipChannelSelectorTests.cs rename to test/Kurrent.Client.Tests/GossipChannelSelectorTests.cs index 00cdc0e0f..142a7dc04 100644 --- a/test/EventStore.Client.Tests/GossipChannelSelectorTests.cs +++ b/test/Kurrent.Client.Tests/GossipChannelSelectorTests.cs @@ -1,7 +1,8 @@ using System.Net; +using EventStore.Client; using Grpc.Core; -namespace EventStore.Client.Tests; +namespace Kurrent.Client.Tests; public class GossipChannelSelectorTests { [RetryFact] @@ -12,7 +13,7 @@ public async Task ExplicitlySettingEndPointChangesChannels() { var firstSelection = new DnsEndPoint(firstId.ToString(), 2113); var secondSelection = new DnsEndPoint(secondId.ToString(), 2113); - var settings = new EventStoreClientSettings { + var settings = new KurrentClientSettings { ConnectivitySettings = { DnsGossipSeeds = new[] { firstSelection, @@ -55,7 +56,7 @@ public async Task ExplicitlySettingEndPointChangesChannels() { [RetryFact] public async Task ThrowsWhenDiscoveryFails() { - var settings = new EventStoreClientSettings { + var settings = new KurrentClientSettings { ConnectivitySettings = { IpGossipSeeds = new[] { new IPEndPoint(IPAddress.Loopback, 2113) diff --git a/test/EventStore.Client.Tests/GrpcServerCapabilitiesClientTests.cs b/test/Kurrent.Client.Tests/GrpcServerCapabilitiesClientTests.cs similarity index 97% rename from test/EventStore.Client.Tests/GrpcServerCapabilitiesClientTests.cs rename to test/Kurrent.Client.Tests/GrpcServerCapabilitiesClientTests.cs index b1b5014df..1bb44dc60 100644 --- a/test/EventStore.Client.Tests/GrpcServerCapabilitiesClientTests.cs +++ b/test/Kurrent.Client.Tests/GrpcServerCapabilitiesClientTests.cs @@ -1,13 +1,13 @@ // #if NET // using System.Net; -// using EventStore.Client.ServerFeatures; +// using Kurrent.Client.ServerFeatures; // using Grpc.Core; // using Microsoft.AspNetCore.Builder; // using Microsoft.AspNetCore.Hosting; // using Microsoft.AspNetCore.TestHost; // using Microsoft.Extensions.DependencyInjection; // -// namespace EventStore.Client.Tests; +// namespace Kurrent.Client.Tests; // // public class GrpcServerCapabilitiesClientTests { // public static IEnumerable ExpectedResultsCases() { diff --git a/test/EventStore.Client.Tests/EventStore.Client.Tests.csproj b/test/Kurrent.Client.Tests/Kurrent.Client.Tests.csproj similarity index 70% rename from test/EventStore.Client.Tests/EventStore.Client.Tests.csproj rename to test/Kurrent.Client.Tests/Kurrent.Client.Tests.csproj index c4456169e..ffca50910 100644 --- a/test/EventStore.Client.Tests/EventStore.Client.Tests.csproj +++ b/test/Kurrent.Client.Tests/Kurrent.Client.Tests.csproj @@ -1,10 +1,7 @@  - - - - + @@ -23,4 +20,4 @@ - \ No newline at end of file + diff --git a/test/Kurrent.Client.Tests/KurrentClientOperationsTests.cs b/test/Kurrent.Client.Tests/KurrentClientOperationsTests.cs new file mode 100644 index 000000000..07f5ffb46 --- /dev/null +++ b/test/Kurrent.Client.Tests/KurrentClientOperationsTests.cs @@ -0,0 +1,16 @@ +using EventStore.Client; + +namespace Kurrent.Client.Tests; + +public class KurrentClientOperationOptionsTests { + [RetryFact] + public void setting_options_on_clone_should_not_modify_original() { + var options = KurrentClientOperationOptions.Default; + + var clonedOptions = options.Clone(); + clonedOptions.BatchAppendSize = int.MaxValue; + + Assert.Equal(options.BatchAppendSize, KurrentClientOperationOptions.Default.BatchAppendSize); + Assert.Equal(int.MaxValue, clonedOptions.BatchAppendSize); + } +} diff --git a/test/EventStore.Client.Tests/NodePreferenceComparerTests.cs b/test/Kurrent.Client.Tests/NodePreferenceComparerTests.cs similarity index 97% rename from test/EventStore.Client.Tests/NodePreferenceComparerTests.cs rename to test/Kurrent.Client.Tests/NodePreferenceComparerTests.cs index abbec81a1..58300f00f 100644 --- a/test/EventStore.Client.Tests/NodePreferenceComparerTests.cs +++ b/test/Kurrent.Client.Tests/NodePreferenceComparerTests.cs @@ -1,6 +1,7 @@ +using EventStore.Client; using static EventStore.Client.ClusterMessages.VNodeState; -namespace EventStore.Client.Tests; +namespace Kurrent.Client.Tests; public class NodePreferenceComparerTests { static ClusterMessages.VNodeState RunTest(IComparer sut, params ClusterMessages.VNodeState[] states) => diff --git a/test/EventStore.Client.Tests/NodeSelectorTests.cs b/test/Kurrent.Client.Tests/NodeSelectorTests.cs similarity index 94% rename from test/EventStore.Client.Tests/NodeSelectorTests.cs rename to test/Kurrent.Client.Tests/NodeSelectorTests.cs index 19855b362..5f0eb51ba 100644 --- a/test/EventStore.Client.Tests/NodeSelectorTests.cs +++ b/test/Kurrent.Client.Tests/NodeSelectorTests.cs @@ -1,6 +1,7 @@ using System.Net; +using EventStore.Client; -namespace EventStore.Client.Tests; +namespace Kurrent.Client.Tests; public class NodeSelectorTests { static readonly ClusterMessages.VNodeState[] NotAllowedStates = { @@ -26,7 +27,7 @@ public class NodeSelectorTests { var notAllowedNodeId = Uuid.NewUuid(); var notAllowedNode = new DnsEndPoint(notAllowedNodeId.ToString(), 2114); - var settings = new EventStoreClientSettings { + var settings = new KurrentClientSettings { ConnectivitySettings = { DnsGossipSeeds = new[] { allowedNode, notAllowedNode }, Insecure = true @@ -50,7 +51,7 @@ public class NodeSelectorTests { [MemberData(nameof(InvalidStatesCases))] internal void InvalidStatesAreNotConsidered( ClusterMessages.ClusterInfo clusterInfo, - EventStoreClientSettings settings, + KurrentClientSettings settings, DnsEndPoint allowedNode ) { var sut = new NodeSelector(settings); @@ -68,7 +69,7 @@ public void DeadNodesAreNotConsidered() { var notAllowedNodeId = Uuid.NewUuid(); var notAllowedNode = new DnsEndPoint(notAllowedNodeId.ToString(), 2114); - var settings = new EventStoreClientSettings { + var settings = new KurrentClientSettings { ConnectivitySettings = { DnsGossipSeeds = new[] { allowedNode, notAllowedNode }, Insecure = true @@ -96,7 +97,7 @@ public void DeadNodesAreNotConsidered() { [InlineData(NodePreference.ReadOnlyReplica, "readOnlyReplica")] [InlineData(NodePreference.Random, "any")] public void CanPrefer(NodePreference nodePreference, string expectedHost) { - var settings = new EventStoreClientSettings { + var settings = new KurrentClientSettings { ConnectivitySettings = { NodePreference = nodePreference } diff --git a/test/EventStore.Client.Tests/Operations/AuthenticationTests.cs b/test/Kurrent.Client.Tests/Operations/AuthenticationTests.cs similarity index 92% rename from test/EventStore.Client.Tests/Operations/AuthenticationTests.cs rename to test/Kurrent.Client.Tests/Operations/AuthenticationTests.cs index b05bd46ba..61d31701a 100644 --- a/test/EventStore.Client.Tests/Operations/AuthenticationTests.cs +++ b/test/Kurrent.Client.Tests/Operations/AuthenticationTests.cs @@ -1,7 +1,9 @@ -namespace EventStore.Client.Tests; +using EventStore.Client; + +namespace Kurrent.Client.Tests; public class AuthenticationTests(ITestOutputHelper output, AuthenticationTests.CustomFixture fixture) - : EventStorePermanentTests(output, fixture) { + : KurrentPermanentTests(output, fixture) { public enum CredentialsCase { None, TestUser, RootUser } public static IEnumerable InvalidAuthenticationCases() { @@ -40,7 +42,7 @@ async Task ExecuteTest(int caseNr, CredentialsCase defaultCredentials, Credentia settings.DefaultCredentials = defaultUserCredentials; settings.ConnectionName = $"Authentication case #{caseNr} {defaultCredentials}"; - await using var operations = new EventStoreOperationsClient(settings); + await using var operations = new KurrentOperationsClient(settings); if (shouldThrow) await operations diff --git a/test/EventStore.Client.Tests/Operations/MergeIndexTests.cs b/test/Kurrent.Client.Tests/Operations/MergeIndexTests.cs similarity index 80% rename from test/EventStore.Client.Tests/Operations/MergeIndexTests.cs rename to test/Kurrent.Client.Tests/Operations/MergeIndexTests.cs index 9da4fc434..629924a45 100644 --- a/test/EventStore.Client.Tests/Operations/MergeIndexTests.cs +++ b/test/Kurrent.Client.Tests/Operations/MergeIndexTests.cs @@ -1,7 +1,9 @@ -namespace EventStore.Client.Tests; +using EventStore.Client; + +namespace Kurrent.Client.Tests; public class MergeIndexTests(ITestOutputHelper output, MergeIndexTests.CustomFixture fixture) - : EventStorePermanentTests(output, fixture) { + : KurrentPermanentTests(output, fixture) { [RetryFact] public async Task merge_indexes_does_not_throw() => await Fixture.Operations diff --git a/test/EventStore.Client.Tests/Operations/ResignNodeTests.cs b/test/Kurrent.Client.Tests/Operations/ResignNodeTests.cs similarity index 82% rename from test/EventStore.Client.Tests/Operations/ResignNodeTests.cs rename to test/Kurrent.Client.Tests/Operations/ResignNodeTests.cs index b707a41b5..4961873ad 100644 --- a/test/EventStore.Client.Tests/Operations/ResignNodeTests.cs +++ b/test/Kurrent.Client.Tests/Operations/ResignNodeTests.cs @@ -1,7 +1,8 @@ -using EventStore.Client.Tests.TestNode; -using EventStore.Client.Tests; +using EventStore.Client; +using Kurrent.Client.Tests.TestNode; +using Kurrent.Client.Tests; -namespace EventStore.Client.Tests.Operations; +namespace Kurrent.Client.Tests.Operations; public class ResignNodeTests(ITestOutputHelper output, ResignNodeTests.CustomFixture fixture) : KurrentTemporaryTests(output, fixture) { diff --git a/test/EventStore.Client.Tests/Operations/RestartPersistentSubscriptionsTests.cs b/test/Kurrent.Client.Tests/Operations/RestartPersistentSubscriptionsTests.cs similarity index 55% rename from test/EventStore.Client.Tests/Operations/RestartPersistentSubscriptionsTests.cs rename to test/Kurrent.Client.Tests/Operations/RestartPersistentSubscriptionsTests.cs index b14242f80..a0683c6b4 100644 --- a/test/EventStore.Client.Tests/Operations/RestartPersistentSubscriptionsTests.cs +++ b/test/Kurrent.Client.Tests/Operations/RestartPersistentSubscriptionsTests.cs @@ -1,9 +1,10 @@ -using EventStore.Client.Tests.TestNode; +using EventStore.Client; +using Kurrent.Client.Tests.TestNode; -namespace EventStore.Client.Tests.Operations; +namespace Kurrent.Client.Tests.Operations; -public class RestartPersistentSubscriptionsTests(ITestOutputHelper output, RestartPersistentSubscriptionsTests.NoDefaultCredentialsFixture fixture) - : KurrentTemporaryTests(output, fixture) { +public class RestartPersistentSubscriptionsTests(ITestOutputHelper output, RestartPersistentSubscriptionsTests.CustomFixture fixture) + : KurrentTemporaryTests(output, fixture) { [RetryFact] public async Task restart_persistent_subscriptions_does_not_throw() => await Fixture.Operations @@ -16,5 +17,5 @@ await Fixture.Operations .RestartPersistentSubscriptions() .ShouldThrowAsync(); - public class NoDefaultCredentialsFixture() : KurrentTemporaryFixture(x => x.WithoutDefaultCredentials()); + public class CustomFixture() : KurrentTemporaryFixture(x => x.WithoutDefaultCredentials()); } diff --git a/test/EventStore.Client.Tests/Operations/ScavengeTests.cs b/test/Kurrent.Client.Tests/Operations/ScavengeTests.cs similarity index 94% rename from test/EventStore.Client.Tests/Operations/ScavengeTests.cs rename to test/Kurrent.Client.Tests/Operations/ScavengeTests.cs index 39090938b..7dc6912e4 100644 --- a/test/EventStore.Client.Tests/Operations/ScavengeTests.cs +++ b/test/Kurrent.Client.Tests/Operations/ScavengeTests.cs @@ -1,7 +1,8 @@ -using EventStore.Client.Tests.TestNode; -using EventStore.Client.Tests; +using EventStore.Client; +using Kurrent.Client.Tests.TestNode; +using Kurrent.Client.Tests; -namespace EventStore.Client.Tests; +namespace Kurrent.Client.Tests; public class ScavengeTests(ITestOutputHelper output, ScavengeTests.CustomFixture fixture) : KurrentTemporaryTests(output, fixture) { diff --git a/test/EventStore.Client.Tests/Operations/ShutdownNodeAuthenticationTests.cs b/test/Kurrent.Client.Tests/Operations/ShutdownNodeAuthenticationTests.cs similarity index 79% rename from test/EventStore.Client.Tests/Operations/ShutdownNodeAuthenticationTests.cs rename to test/Kurrent.Client.Tests/Operations/ShutdownNodeAuthenticationTests.cs index da575c581..a1294f878 100644 --- a/test/EventStore.Client.Tests/Operations/ShutdownNodeAuthenticationTests.cs +++ b/test/Kurrent.Client.Tests/Operations/ShutdownNodeAuthenticationTests.cs @@ -1,7 +1,8 @@ -using EventStore.Client.Tests.TestNode; -using EventStore.Client.Tests; +using EventStore.Client; +using Kurrent.Client.Tests.TestNode; +using Kurrent.Client.Tests; -namespace EventStore.Client.Tests; +namespace Kurrent.Client.Tests; public class ShutdownNodeAuthenticationTests(ITestOutputHelper output, ShutdownNodeAuthenticationTests.CustomFixture fixture) : KurrentTemporaryTests(output, fixture) { diff --git a/test/EventStore.Client.Tests/Operations/ShutdownNodeTests.cs b/test/Kurrent.Client.Tests/Operations/ShutdownNodeTests.cs similarity index 80% rename from test/EventStore.Client.Tests/Operations/ShutdownNodeTests.cs rename to test/Kurrent.Client.Tests/Operations/ShutdownNodeTests.cs index 8680865d2..e668dfe72 100644 --- a/test/EventStore.Client.Tests/Operations/ShutdownNodeTests.cs +++ b/test/Kurrent.Client.Tests/Operations/ShutdownNodeTests.cs @@ -1,7 +1,7 @@ -using EventStore.Client.Tests.TestNode; -using EventStore.Client.Tests; +using Kurrent.Client.Tests.TestNode; +using Kurrent.Client.Tests; -namespace EventStore.Client.Tests.Operations; +namespace Kurrent.Client.Tests.Operations; public class ShutdownNodeTests(ITestOutputHelper output, ShutdownNodeTests.NoDefaultCredentialsFixture fixture) : KurrentTemporaryTests(output, fixture) { diff --git a/test/EventStore.Client.Tests/PersistentSubscriptions/FilterTestCases.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/FilterTestCases.cs similarity index 94% rename from test/EventStore.Client.Tests/PersistentSubscriptions/FilterTestCases.cs rename to test/Kurrent.Client.Tests/PersistentSubscriptions/FilterTestCases.cs index e7a2355e8..5dc4c098b 100644 --- a/test/EventStore.Client.Tests/PersistentSubscriptions/FilterTestCases.cs +++ b/test/Kurrent.Client.Tests/PersistentSubscriptions/FilterTestCases.cs @@ -1,6 +1,7 @@ using System.Reflection; +using EventStore.Client; -namespace EventStore.Client.Tests.PersistentSubscriptions; +namespace Kurrent.Client.Tests.PersistentSubscriptions; public static class Filters { const string StreamNamePrefix = nameof(StreamNamePrefix); diff --git a/test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToAllConnectWithoutReadPermissionsTests.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllConnectWithoutReadPermissionsTests.cs similarity index 86% rename from test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToAllConnectWithoutReadPermissionsTests.cs rename to test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllConnectWithoutReadPermissionsTests.cs index d31a6d3f3..7a3a1e0e2 100644 --- a/test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToAllConnectWithoutReadPermissionsTests.cs +++ b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllConnectWithoutReadPermissionsTests.cs @@ -1,7 +1,7 @@ -using EventStore.Client.Tests.TestNode; -using EventStore.Client.Tests; +using EventStore.Client; +using Kurrent.Client.Tests.TestNode; -namespace EventStore.Client.Tests.PersistentSubscriptions; +namespace Kurrent.Client.Tests.PersistentSubscriptions; public class SubscribeToAllConnectWithoutReadPermissionsTests(ITestOutputHelper output, KurrentTemporaryFixture fixture) : KurrentTemporaryTests(output, fixture) { diff --git a/test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToAllFilterTests.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllFilterTests.cs similarity index 96% rename from test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToAllFilterTests.cs rename to test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllFilterTests.cs index b4607e03f..52fa03ef1 100644 --- a/test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToAllFilterTests.cs +++ b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllFilterTests.cs @@ -1,7 +1,8 @@ -using EventStore.Client.Tests.TestNode; -using EventStore.Client.Tests; +using EventStore.Client; +using Kurrent.Client.Tests.TestNode; +using Kurrent.Client.Tests; -namespace EventStore.Client.Tests.PersistentSubscriptions; +namespace Kurrent.Client.Tests.PersistentSubscriptions; public class SubscribeToAllFilterTests(ITestOutputHelper output, KurrentTemporaryFixture fixture) : KurrentTemporaryTests(output, fixture) { diff --git a/test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToAllGetInfoTests.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllGetInfoTests.cs similarity index 95% rename from test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToAllGetInfoTests.cs rename to test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllGetInfoTests.cs index c9f11f171..0dddd0b8b 100644 --- a/test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToAllGetInfoTests.cs +++ b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllGetInfoTests.cs @@ -1,9 +1,9 @@ // ReSharper disable InconsistentNaming -using EventStore.Client.Tests.TestNode; -using EventStore.Client.Tests; +using EventStore.Client; +using Kurrent.Client.Tests.TestNode; -namespace EventStore.Client.Tests.PersistentSubscriptions; +namespace Kurrent.Client.Tests.PersistentSubscriptions; public class SubscribeToAllGetInfoTests(SubscribeToAllGetInfoTests.CustomFixture fixture) : IClassFixture { diff --git a/test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToAllListWithIncorrectCredentialsTests.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllListWithIncorrectCredentialsTests.cs similarity index 93% rename from test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToAllListWithIncorrectCredentialsTests.cs rename to test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllListWithIncorrectCredentialsTests.cs index 6df8b9d7b..47fd98fa9 100644 --- a/test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToAllListWithIncorrectCredentialsTests.cs +++ b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllListWithIncorrectCredentialsTests.cs @@ -1,7 +1,7 @@ -using EventStore.Client.Tests.TestNode; -using EventStore.Client.Tests; +using EventStore.Client; +using Kurrent.Client.Tests.TestNode; -namespace EventStore.Client.Tests.PersistentSubscriptions; +namespace Kurrent.Client.Tests.PersistentSubscriptions; public class SubscribeToAllListWithIncorrectCredentialsTests(ITestOutputHelper output, SubscribeToAllListWithIncorrectCredentialsTests.CustomFixture fixture) : KurrentTemporaryTests(output, fixture) { diff --git a/test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToAllNoDefaultCredentialsTests.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllNoDefaultCredentialsTests.cs similarity index 93% rename from test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToAllNoDefaultCredentialsTests.cs rename to test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllNoDefaultCredentialsTests.cs index d7143d1d2..8f2ee60cd 100644 --- a/test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToAllNoDefaultCredentialsTests.cs +++ b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllNoDefaultCredentialsTests.cs @@ -1,7 +1,9 @@ -namespace EventStore.Client.Tests.PersistentSubscriptions; +using EventStore.Client; + +namespace Kurrent.Client.Tests.PersistentSubscriptions; public class SubscribeToAllNoDefaultCredentialsTests(ITestOutputHelper output, SubscribeToAllNoDefaultCredentialsTests.CustomFixture fixture) - : EventStorePermanentTests(output, fixture) { + : KurrentPermanentTests(output, fixture) { [RetryFact] public async Task connect_to_existing_without_permissions() { var group = Fixture.GetGroupName(); diff --git a/test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToAllReplayParkedTests.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllReplayParkedTests.cs similarity index 93% rename from test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToAllReplayParkedTests.cs rename to test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllReplayParkedTests.cs index ca608d092..802501317 100644 --- a/test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToAllReplayParkedTests.cs +++ b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllReplayParkedTests.cs @@ -1,7 +1,9 @@ -namespace EventStore.Client.Tests.PersistentSubscriptions; +using EventStore.Client; + +namespace Kurrent.Client.Tests.PersistentSubscriptions; public class SubscribeToAllReplayParkedTests(ITestOutputHelper output, SubscribeToAllReplayParkedTests.CustomFixture fixture) - : EventStorePermanentTests(output, fixture) { + : KurrentPermanentTests(output, fixture) { [RetryFact] public async Task does_not_throw() { var group = Fixture.GetGroupName(); diff --git a/test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToAllResultWithNormalUserCredentialsTests.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllResultWithNormalUserCredentialsTests.cs similarity index 88% rename from test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToAllResultWithNormalUserCredentialsTests.cs rename to test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllResultWithNormalUserCredentialsTests.cs index 906bbde8f..10a52d40b 100644 --- a/test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToAllResultWithNormalUserCredentialsTests.cs +++ b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllResultWithNormalUserCredentialsTests.cs @@ -1,7 +1,7 @@ -using EventStore.Client.Tests.TestNode; -using EventStore.Client.Tests; +using Kurrent.Client.Tests.TestNode; +using Kurrent.Client.Tests; -namespace EventStore.Client.Tests.PersistentSubscriptions; +namespace Kurrent.Client.Tests.PersistentSubscriptions; public class SubscribeToAllResultWithNormalUserCredentialsTests(ITestOutputHelper output, KurrentTemporaryFixture fixture) : KurrentTemporaryTests(output, fixture) { diff --git a/test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToAllReturnsAllSubscriptions.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllReturnsAllSubscriptions.cs similarity index 90% rename from test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToAllReturnsAllSubscriptions.cs rename to test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllReturnsAllSubscriptions.cs index e7b542da9..2fcc75d6f 100644 --- a/test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToAllReturnsAllSubscriptions.cs +++ b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllReturnsAllSubscriptions.cs @@ -1,7 +1,7 @@ -using EventStore.Client.Tests.TestNode; -using EventStore.Client.Tests; +using Kurrent.Client.Tests.TestNode; +using Kurrent.Client.Tests; -namespace EventStore.Client.Tests.PersistentSubscriptions; +namespace Kurrent.Client.Tests.PersistentSubscriptions; public class SubscribeToAllReturnsAllSubscriptions(ITestOutputHelper output, SubscribeToAllReturnsAllSubscriptions.CustomFixture fixture) : KurrentTemporaryTests(output, fixture) { diff --git a/test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToAllReturnsSubscriptionsToAllStreamTests.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllReturnsSubscriptionsToAllStreamTests.cs similarity index 88% rename from test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToAllReturnsSubscriptionsToAllStreamTests.cs rename to test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllReturnsSubscriptionsToAllStreamTests.cs index 9a5e9c2a1..4ab3bd508 100644 --- a/test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToAllReturnsSubscriptionsToAllStreamTests.cs +++ b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllReturnsSubscriptionsToAllStreamTests.cs @@ -1,7 +1,7 @@ -using EventStore.Client.Tests.TestNode; -using EventStore.Client.Tests; +using Kurrent.Client.Tests.TestNode; +using Kurrent.Client.Tests; -namespace EventStore.Client.Tests.PersistentSubscriptions; +namespace Kurrent.Client.Tests.PersistentSubscriptions; public class SubscribeToAllReturnsSubscriptionsToAllStreamTests(ITestOutputHelper output, KurrentTemporaryFixture fixture) : KurrentTemporaryTests(output, fixture) { diff --git a/test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToAllTests.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllTests.cs similarity index 99% rename from test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToAllTests.cs rename to test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllTests.cs index 1b6e55834..f8fc927b2 100644 --- a/test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToAllTests.cs +++ b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllTests.cs @@ -1,10 +1,11 @@ using System.Text; +using EventStore.Client; using Grpc.Core; -namespace EventStore.Client.Tests.PersistentSubscriptions; +namespace Kurrent.Client.Tests.PersistentSubscriptions; public class SubscribeToAllTests(ITestOutputHelper output, KurrentPermanentFixture fixture) - : EventStorePermanentTests(output, fixture) { + : KurrentPermanentTests(output, fixture) { [RetryFact] public async Task can_create_duplicate_name_on_different_streams() { // Arrange @@ -100,7 +101,7 @@ await Fixture.Streams.AppendToStreamAsync( var subscription = Fixture.Subscriptions.SubscribeToAll(group, userCredentials: TestCredentials.Root); await Assert.ThrowsAsync( - () => subscription!.Messages + () => subscription.Messages .OfType() .Where(e => !SystemStreams.IsSystemStream(e.ResolvedEvent.OriginalStreamId)) .AnyAsync() diff --git a/test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToAllUpdateExistingWithCheckpointTest.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllUpdateExistingWithCheckpointTest.cs similarity index 94% rename from test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToAllUpdateExistingWithCheckpointTest.cs rename to test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllUpdateExistingWithCheckpointTest.cs index 091d55583..54e9f64ff 100644 --- a/test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToAllUpdateExistingWithCheckpointTest.cs +++ b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllUpdateExistingWithCheckpointTest.cs @@ -1,7 +1,8 @@ -using EventStore.Client.Tests.TestNode; -using EventStore.Client.Tests; +using EventStore.Client; +using Kurrent.Client.Tests.TestNode; +using Kurrent.Client.Tests; -namespace EventStore.Client.Tests.PersistentSubscriptions; +namespace Kurrent.Client.Tests.PersistentSubscriptions; public class SubscribeToAllUpdateExistingWithCheckpointTest(ITestOutputHelper output, KurrentTemporaryFixture fixture) : KurrentTemporaryTests(output, fixture) { diff --git a/test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToAllWithoutPSTests.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllWithoutPSTests.cs similarity index 74% rename from test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToAllWithoutPSTests.cs rename to test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllWithoutPSTests.cs index 4afb8744e..059c28606 100644 --- a/test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToAllWithoutPSTests.cs +++ b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllWithoutPSTests.cs @@ -1,7 +1,8 @@ -using EventStore.Client.Tests.TestNode; -using EventStore.Client.Tests; +using EventStore.Client; +using Kurrent.Client.Tests.TestNode; +using Kurrent.Client.Tests; -namespace EventStore.Client.Tests.PersistentSubscriptions; +namespace Kurrent.Client.Tests.PersistentSubscriptions; public class SubscribeToAllWithoutPsTests(ITestOutputHelper output, KurrentTemporaryFixture fixture) : KurrentTemporaryTests(output, fixture) { diff --git a/test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToStreamGetInfoTests.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToStreamGetInfoTests.cs similarity index 97% rename from test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToStreamGetInfoTests.cs rename to test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToStreamGetInfoTests.cs index 941ae6a9a..4614c691d 100644 --- a/test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToStreamGetInfoTests.cs +++ b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToStreamGetInfoTests.cs @@ -1,9 +1,9 @@ // ReSharper disable InconsistentNaming -using EventStore.Client.Tests.TestNode; -using EventStore.Client.Tests; +using EventStore.Client; +using Kurrent.Client.Tests.TestNode; -namespace EventStore.Client.Tests.PersistentSubscriptions; +namespace Kurrent.Client.Tests.PersistentSubscriptions; public class SubscribeToStreamGetInfoTests(SubscribeToStreamGetInfoTests.NoDefaultCredentialsFixture fixture) : IClassFixture { @@ -151,7 +151,7 @@ public class NoDefaultCredentialsFixture : KurrentTemporaryFixture { public string Group { get; set; } public string Stream { get; set; } - EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult? Subscription; + KurrentPersistentSubscriptionsClient.PersistentSubscriptionResult? Subscription; IAsyncEnumerator? Enumerator; public NoDefaultCredentialsFixture() : base(x => x.WithoutDefaultCredentials()) { diff --git a/test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToStreamListTests.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToStreamListTests.cs similarity index 81% rename from test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToStreamListTests.cs rename to test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToStreamListTests.cs index bea57c9ab..dd93d9e2d 100644 --- a/test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToStreamListTests.cs +++ b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToStreamListTests.cs @@ -1,7 +1,10 @@ -namespace EventStore.Client.Tests.PersistentSubscriptions; +using EventStore.Client; +using Kurrent.Client.Tests.TestNode; + +namespace Kurrent.Client.Tests.PersistentSubscriptions; public class SubscribeToStreamListTests(ITestOutputHelper output, SubscribeToStreamListTests.CustomFixture fixture) - : EventStorePermanentTests(output, fixture) { + : KurrentTemporaryTests(output, fixture) { [RetryFact] public async Task throws_with_no_credentials() { var stream = Fixture.GetStreamName(); @@ -40,5 +43,5 @@ await Assert.ThrowsAsync( ); } - public class CustomFixture() : KurrentPermanentFixture(x => x.WithoutDefaultCredentials()); + public class CustomFixture() : KurrentTemporaryFixture(x => x.WithoutDefaultCredentials()); } diff --git a/test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToStreamNoDefaultCredentialsTests.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToStreamNoDefaultCredentialsTests.cs similarity index 91% rename from test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToStreamNoDefaultCredentialsTests.cs rename to test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToStreamNoDefaultCredentialsTests.cs index 3a473b58e..858140147 100644 --- a/test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToStreamNoDefaultCredentialsTests.cs +++ b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToStreamNoDefaultCredentialsTests.cs @@ -1,7 +1,9 @@ -namespace EventStore.Client.Tests.PersistentSubscriptions; +using EventStore.Client; + +namespace Kurrent.Client.Tests.PersistentSubscriptions; public class SubscribeToStreamNoDefaultCredentialsTests(ITestOutputHelper output, SubscribeToStreamNoDefaultCredentialsTests.CustomFixture fixture) - : EventStorePermanentTests(output, fixture) { + : KurrentPermanentTests(output, fixture) { [RetryFact] public async Task connect_to_existing_without_permissions() { var group = Fixture.GetGroupName(); diff --git a/test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToStreamReplayParkedTests.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToStreamReplayParkedTests.cs similarity index 92% rename from test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToStreamReplayParkedTests.cs rename to test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToStreamReplayParkedTests.cs index 384cc8dfb..8aab4c46c 100644 --- a/test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToStreamReplayParkedTests.cs +++ b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToStreamReplayParkedTests.cs @@ -1,7 +1,9 @@ -namespace EventStore.Client.Tests.PersistentSubscriptions; +using EventStore.Client; + +namespace Kurrent.Client.Tests.PersistentSubscriptions; public class SubscribeToStreamReplayParkedTests(ITestOutputHelper output, SubscribeToStreamReplayParkedTests.CustomFixture fixture) - : EventStorePermanentTests(output, fixture) { + : KurrentPermanentTests(output, fixture) { [RetryFact] public async Task does_not_throw() { var stream = Fixture.GetStreamName(); diff --git a/test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToStreamTests.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToStreamTests.cs similarity index 99% rename from test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToStreamTests.cs rename to test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToStreamTests.cs index f9cdb68d4..d5ddbc82a 100644 --- a/test/EventStore.Client.Tests/PersistentSubscriptions/SubscribeToStreamTests.cs +++ b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToStreamTests.cs @@ -1,10 +1,11 @@ using System.Text; +using EventStore.Client; using Grpc.Core; -namespace EventStore.Client.Tests.PersistentSubscriptions; +namespace Kurrent.Client.Tests.PersistentSubscriptions; public class SubscribeToStreamTests(ITestOutputHelper output, KurrentPermanentFixture fixture) - : EventStorePermanentTests(output, fixture) { + : KurrentPermanentTests(output, fixture) { [RetryFact] public async Task can_create_duplicate_name_on_different_streams() { var stream = Fixture.GetStreamName(); diff --git a/test/EventStore.Client.Tests/PositionTests.cs b/test/Kurrent.Client.Tests/PositionTests.cs similarity index 97% rename from test/EventStore.Client.Tests/PositionTests.cs rename to test/Kurrent.Client.Tests/PositionTests.cs index b9657a2c5..92a478008 100644 --- a/test/EventStore.Client.Tests/PositionTests.cs +++ b/test/Kurrent.Client.Tests/PositionTests.cs @@ -1,6 +1,7 @@ using AutoFixture; +using EventStore.Client; -namespace EventStore.Client.Tests; +namespace Kurrent.Client.Tests; public class PositionTests : ValueObjectTests { public PositionTests() : base(new ScenarioFixture()) { } diff --git a/test/EventStore.Client.Tests/ListProjectionTests.cs b/test/Kurrent.Client.Tests/Projections/ListAllProjectionsTests.cs similarity index 62% rename from test/EventStore.Client.Tests/ListProjectionTests.cs rename to test/Kurrent.Client.Tests/Projections/ListAllProjectionsTests.cs index d5d673657..127552eab 100644 --- a/test/EventStore.Client.Tests/ListProjectionTests.cs +++ b/test/Kurrent.Client.Tests/Projections/ListAllProjectionsTests.cs @@ -1,19 +1,11 @@ // ReSharper disable InconsistentNaming -using EventStore.Client.Tests.TestNode; +using Kurrent.Client.Tests.TestNode; -namespace EventStore.Client.Tests; - -public class ListProjectionTests(ITestOutputHelper output, ListProjectionTests.CustomFixture fixture) - : KurrentTemporaryTests(output, fixture) { - [Fact] - public async Task list_all_projections() { - var result = await Fixture.Projections.ListAllAsync(userCredentials: TestCredentials.Root) - .ToArrayAsync(); - - Assert.Equal(result.Select(x => x.Name).OrderBy(x => x), Names.OrderBy(x => x)); - } +namespace Kurrent.Client.Tests; +public class ListAllProjectionsTests(ITestOutputHelper output, ListAllProjectionsTests.CustomFixture fixture) + : KurrentTemporaryTests(output, fixture) { [Fact] public async Task list_continuous_projections() { var name = Fixture.GetProjectionName(); diff --git a/test/Kurrent.Client.Tests/Projections/ListContinuousProjectionsTests.cs b/test/Kurrent.Client.Tests/Projections/ListContinuousProjectionsTests.cs new file mode 100644 index 000000000..1f3bbd76c --- /dev/null +++ b/test/Kurrent.Client.Tests/Projections/ListContinuousProjectionsTests.cs @@ -0,0 +1,35 @@ +// ReSharper disable InconsistentNaming + +using Kurrent.Client.Tests.TestNode; + +namespace Kurrent.Client.Tests; + +public class ListContinuousProjectionsTests(ITestOutputHelper output, ListContinuousProjectionsTests.CustomFixture fixture) + : KurrentTemporaryTests(output, fixture) { + [Fact] + public async Task list_continuous_projections() { + var name = Fixture.GetProjectionName(); + + await Fixture.Projections.CreateContinuousAsync( + name, + "fromAll().when({$init: function (state, ev) {return {};}});", + userCredentials: TestCredentials.Root + ); + + var result = await Fixture.Projections.ListContinuousAsync(userCredentials: TestCredentials.Root) + .ToArrayAsync(); + + Assert.Equal( + result.Select(x => x.Name).OrderBy(x => x), + Names.Concat([name]).OrderBy(x => x) + ); + + Assert.True(result.All(x => x.Mode == "Continuous")); + } + + static readonly string[] Names = ["$streams", "$stream_by_category", "$by_category", "$by_event_type", "$by_correlation_id"]; + + public class CustomFixture : KurrentTemporaryFixture { + public CustomFixture() : base(x => x.RunProjections()) { } + } +} diff --git a/test/Kurrent.Client.Tests/Projections/ListOneTimeProjectionsTests.cs b/test/Kurrent.Client.Tests/Projections/ListOneTimeProjectionsTests.cs new file mode 100644 index 000000000..b2b0d3899 --- /dev/null +++ b/test/Kurrent.Client.Tests/Projections/ListOneTimeProjectionsTests.cs @@ -0,0 +1,21 @@ +using Kurrent.Client.Tests.TestNode; + +namespace Kurrent.Client.Tests.Projections; + +public class ListOneTimeProjectionsTests(ITestOutputHelper output, ListOneTimeProjectionsTests.CustomFixture fixture) + : KurrentTemporaryTests(output, fixture) { + [Fact] + public async Task list_one_time_projections() { + await Fixture.Projections.CreateOneTimeAsync("fromAll().when({$init: function (state, ev) {return {};}});", userCredentials: TestCredentials.Root); + + var result = await Fixture.Projections.ListOneTimeAsync(userCredentials: TestCredentials.Root) + .ToArrayAsync(); + + var details = Assert.Single(result); + Assert.Equal("OneTime", details.Mode); + } + + public class CustomFixture : KurrentTemporaryFixture { + public CustomFixture() : base(x => x.RunProjections()) { } + } +} diff --git a/test/EventStore.Client.Tests/ProjectionManagementTests.cs b/test/Kurrent.Client.Tests/Projections/ProjectionManagementTests.cs similarity index 86% rename from test/EventStore.Client.Tests/ProjectionManagementTests.cs rename to test/Kurrent.Client.Tests/Projections/ProjectionManagementTests.cs index e119ff3cd..83b1a35cc 100644 --- a/test/EventStore.Client.Tests/ProjectionManagementTests.cs +++ b/test/Kurrent.Client.Tests/Projections/ProjectionManagementTests.cs @@ -1,10 +1,10 @@ // ReSharper disable InconsistentNaming // ReSharper disable ClassNeverInstantiated.Local -using EventStore.Client.Tests.TestNode; -using EventStore.Client.Tests; +using EventStore.Client; +using Kurrent.Client.Tests.TestNode; -namespace EventStore.Client.Tests; +namespace Kurrent.Client.Tests; public class ProjectionManagementTests(ITestOutputHelper output, ProjectionManagementTests.CustomFixture fixture) : KurrentTemporaryTests(output, fixture) { @@ -172,27 +172,6 @@ await Fixture.Projections.UpdateAsync( ); } - [Fact] - public async Task list_one_time_projections() { - await Fixture.Projections.CreateOneTimeAsync("fromAll().when({$init: function (state, ev) {return {};}});", userCredentials: TestCredentials.Root); - - var result = await Fixture.Projections.ListOneTimeAsync(userCredentials: TestCredentials.Root) - .ToArrayAsync(); - - var details = Assert.Single(result); - Assert.Equal("OneTime", details.Mode); - } - - [Fact] - public async Task reset_projection() { - var name = Names.First(); - await Fixture.Projections.ResetAsync(name, userCredentials: TestCredentials.Root); - var result = await Fixture.Projections.GetStatusAsync(name, userCredentials: TestCredentials.Root); - - Assert.NotNull(result); - Assert.Equal("Running", result.Status); - } - static readonly string[] Names = ["$streams", "$stream_by_category", "$by_category", "$by_event_type", "$by_correlation_id"]; record Result { diff --git a/test/Kurrent.Client.Tests/Projections/ResetProjectionTests.cs b/test/Kurrent.Client.Tests/Projections/ResetProjectionTests.cs new file mode 100644 index 000000000..44e596667 --- /dev/null +++ b/test/Kurrent.Client.Tests/Projections/ResetProjectionTests.cs @@ -0,0 +1,22 @@ +using Kurrent.Client.Tests.TestNode; + +namespace Kurrent.Client.Tests.Projections; + +public class ResetProjectionTests(ITestOutputHelper output, ResetProjectionTests.CustomFixture fixture) + : KurrentTemporaryTests(output, fixture) { + [Fact] + public async Task reset_projection() { + var name = Names.First(); + await Fixture.Projections.ResetAsync(name, userCredentials: TestCredentials.Root); + var result = await Fixture.Projections.GetStatusAsync(name, userCredentials: TestCredentials.Root); + + Assert.NotNull(result); + Assert.Equal("Running", result.Status); + } + + static readonly string[] Names = ["$streams", "$stream_by_category", "$by_category", "$by_event_type", "$by_correlation_id"]; + + public class CustomFixture : KurrentTemporaryFixture { + public CustomFixture() : base(x => x.RunProjections()) { } + } +} diff --git a/test/EventStore.Client.Tests/RegularFilterExpressionTests.cs b/test/Kurrent.Client.Tests/RegularFilterExpressionTests.cs similarity index 87% rename from test/EventStore.Client.Tests/RegularFilterExpressionTests.cs rename to test/Kurrent.Client.Tests/RegularFilterExpressionTests.cs index 67bd40039..de0a4e7b9 100644 --- a/test/EventStore.Client.Tests/RegularFilterExpressionTests.cs +++ b/test/Kurrent.Client.Tests/RegularFilterExpressionTests.cs @@ -1,7 +1,8 @@ using System.Text.RegularExpressions; using AutoFixture; +using EventStore.Client; -namespace EventStore.Client.Tests; +namespace Kurrent.Client.Tests; public class RegularFilterExpressionTests : ValueObjectTests { public RegularFilterExpressionTests() : base(new ScenarioFixture()) { } diff --git a/test/EventStore.Client.Tests/Security/AllStreamWithNoAclSecurityTests.cs b/test/Kurrent.Client.Tests/Security/AllStreamWithNoAclSecurityTests.cs similarity index 96% rename from test/EventStore.Client.Tests/Security/AllStreamWithNoAclSecurityTests.cs rename to test/Kurrent.Client.Tests/Security/AllStreamWithNoAclSecurityTests.cs index b404c7357..1e6da34e4 100644 --- a/test/EventStore.Client.Tests/Security/AllStreamWithNoAclSecurityTests.cs +++ b/test/Kurrent.Client.Tests/Security/AllStreamWithNoAclSecurityTests.cs @@ -1,7 +1,8 @@ -using EventStore.Client.Tests.TestNode; -using EventStore.Client.Tests; +using EventStore.Client; +using Kurrent.Client.Tests.TestNode; +using Kurrent.Client.Tests; -namespace EventStore.Client.Tests; +namespace Kurrent.Client.Tests; [Trait("Category", "Security")] public class AllStreamWithNoAclSecurityTests(ITestOutputHelper output, AllStreamWithNoAclSecurityTests.CustomFixture fixture) diff --git a/test/EventStore.Client.Tests/Security/DeleteStreamSecurityTests.cs b/test/Kurrent.Client.Tests/Security/DeleteStreamSecurityTests.cs similarity index 98% rename from test/EventStore.Client.Tests/Security/DeleteStreamSecurityTests.cs rename to test/Kurrent.Client.Tests/Security/DeleteStreamSecurityTests.cs index 7e661bf9f..feea4452a 100644 --- a/test/EventStore.Client.Tests/Security/DeleteStreamSecurityTests.cs +++ b/test/Kurrent.Client.Tests/Security/DeleteStreamSecurityTests.cs @@ -1,7 +1,8 @@ -using EventStore.Client.Tests.TestNode; -using EventStore.Client.Tests; +using EventStore.Client; +using Kurrent.Client.Tests.TestNode; +using Kurrent.Client.Tests; -namespace EventStore.Client.Tests; +namespace Kurrent.Client.Tests; [Trait("Category", "Security")] public class DeleteStreamSecurityTests(ITestOutputHelper output, SecurityFixture fixture) : KurrentTemporaryTests(output, fixture) { diff --git a/test/EventStore.Client.Tests/Security/MultipleRoleSecurityTests.cs b/test/Kurrent.Client.Tests/Security/MultipleRoleSecurityTests.cs similarity index 93% rename from test/EventStore.Client.Tests/Security/MultipleRoleSecurityTests.cs rename to test/Kurrent.Client.Tests/Security/MultipleRoleSecurityTests.cs index 3a5a0c78a..fd36ae2d3 100644 --- a/test/EventStore.Client.Tests/Security/MultipleRoleSecurityTests.cs +++ b/test/Kurrent.Client.Tests/Security/MultipleRoleSecurityTests.cs @@ -1,7 +1,8 @@ -using EventStore.Client.Tests.TestNode; -using EventStore.Client.Tests; +using EventStore.Client; +using Kurrent.Client.Tests.TestNode; +using Kurrent.Client.Tests; -namespace EventStore.Client.Tests; +namespace Kurrent.Client.Tests; [Trait("Category", "Security")] public class MultipleRoleSecurityTests(ITestOutputHelper output, MultipleRoleSecurityTests.CustomFixture fixture) diff --git a/test/EventStore.Client.Tests/Security/OverridenSystemStreamSecurityForAllTests.cs b/test/Kurrent.Client.Tests/Security/OverridenSystemStreamSecurityForAllTests.cs similarity index 95% rename from test/EventStore.Client.Tests/Security/OverridenSystemStreamSecurityForAllTests.cs rename to test/Kurrent.Client.Tests/Security/OverridenSystemStreamSecurityForAllTests.cs index dd4057df0..14f3e1b93 100644 --- a/test/EventStore.Client.Tests/Security/OverridenSystemStreamSecurityForAllTests.cs +++ b/test/Kurrent.Client.Tests/Security/OverridenSystemStreamSecurityForAllTests.cs @@ -1,7 +1,8 @@ -using EventStore.Client.Tests.TestNode; -using EventStore.Client.Tests; +using EventStore.Client; +using Kurrent.Client.Tests.TestNode; +using Kurrent.Client.Tests; -namespace EventStore.Client.Tests; +namespace Kurrent.Client.Tests; [Trait("Category", "Security")] public class OverridenSystemStreamSecurityForAllTests(ITestOutputHelper output, OverridenSystemStreamSecurityForAllTests.CustomFixture fixture) diff --git a/test/EventStore.Client.Tests/Security/OverridenSystemStreamSecurityTests.cs b/test/Kurrent.Client.Tests/Security/OverridenSystemStreamSecurityTests.cs similarity index 97% rename from test/EventStore.Client.Tests/Security/OverridenSystemStreamSecurityTests.cs rename to test/Kurrent.Client.Tests/Security/OverridenSystemStreamSecurityTests.cs index deedbb33d..af9e4ddd8 100644 --- a/test/EventStore.Client.Tests/Security/OverridenSystemStreamSecurityTests.cs +++ b/test/Kurrent.Client.Tests/Security/OverridenSystemStreamSecurityTests.cs @@ -1,7 +1,7 @@ -using EventStore.Client.Tests.TestNode; -using EventStore.Client.Tests; +using EventStore.Client; +using Kurrent.Client.Tests.TestNode; -namespace EventStore.Client.Tests; +namespace Kurrent.Client.Tests; [Trait("Category", "Security")] public class OverridenSystemStreamSecurityTests(ITestOutputHelper output, OverridenSystemStreamSecurityTests.CustomFixture fixture) diff --git a/test/EventStore.Client.Tests/Security/OverridenUserStreamSecurityTests.cs b/test/Kurrent.Client.Tests/Security/OverridenUserStreamSecurityTests.cs similarity index 97% rename from test/EventStore.Client.Tests/Security/OverridenUserStreamSecurityTests.cs rename to test/Kurrent.Client.Tests/Security/OverridenUserStreamSecurityTests.cs index f5da17028..78cbe52ec 100644 --- a/test/EventStore.Client.Tests/Security/OverridenUserStreamSecurityTests.cs +++ b/test/Kurrent.Client.Tests/Security/OverridenUserStreamSecurityTests.cs @@ -1,7 +1,7 @@ -using EventStore.Client.Tests.TestNode; -using EventStore.Client.Tests; +using EventStore.Client; +using Kurrent.Client.Tests.TestNode; -namespace EventStore.Client.Tests; +namespace Kurrent.Client.Tests; [Trait("Category", "Security")] public class OverridenUserStreamSecurityTests(ITestOutputHelper output, OverridenUserStreamSecurityTests.CustomFixture fixture) diff --git a/test/EventStore.Client.Tests/Security/ReadAllSecurityTests.cs b/test/Kurrent.Client.Tests/Security/ReadAllSecurityTests.cs similarity index 92% rename from test/EventStore.Client.Tests/Security/ReadAllSecurityTests.cs rename to test/Kurrent.Client.Tests/Security/ReadAllSecurityTests.cs index b1a88b7a4..e87c12762 100644 --- a/test/EventStore.Client.Tests/Security/ReadAllSecurityTests.cs +++ b/test/Kurrent.Client.Tests/Security/ReadAllSecurityTests.cs @@ -1,7 +1,8 @@ -using EventStore.Client.Tests.TestNode; -using EventStore.Client.Tests; +using EventStore.Client; +using Kurrent.Client.Tests.TestNode; +using Kurrent.Client.Tests; -namespace EventStore.Client.Tests; +namespace Kurrent.Client.Tests; [Trait("Category", "Security")] public class ReadAllSecurityTests(ITestOutputHelper output, SecurityFixture fixture) : KurrentTemporaryTests(output, fixture) { diff --git a/test/EventStore.Client.Tests/Security/ReadStreamMetaSecurityTests.cs b/test/Kurrent.Client.Tests/Security/ReadStreamMetaSecurityTests.cs similarity index 96% rename from test/EventStore.Client.Tests/Security/ReadStreamMetaSecurityTests.cs rename to test/Kurrent.Client.Tests/Security/ReadStreamMetaSecurityTests.cs index b3c966977..8d6e56c29 100644 --- a/test/EventStore.Client.Tests/Security/ReadStreamMetaSecurityTests.cs +++ b/test/Kurrent.Client.Tests/Security/ReadStreamMetaSecurityTests.cs @@ -1,7 +1,8 @@ -using EventStore.Client.Tests.TestNode; -using EventStore.Client.Tests; +using EventStore.Client; +using Kurrent.Client.Tests.TestNode; +using Kurrent.Client.Tests; -namespace EventStore.Client.Tests; +namespace Kurrent.Client.Tests; [Trait("Category", "Security")] public class ReadStreamMetaSecurityTests(ITestOutputHelper output, SecurityFixture fixture) : KurrentTemporaryTests(output, fixture) { diff --git a/test/EventStore.Client.Tests/Security/ReadStreamSecurityTests.cs b/test/Kurrent.Client.Tests/Security/ReadStreamSecurityTests.cs similarity index 98% rename from test/EventStore.Client.Tests/Security/ReadStreamSecurityTests.cs rename to test/Kurrent.Client.Tests/Security/ReadStreamSecurityTests.cs index a26d7e797..9422ff671 100644 --- a/test/EventStore.Client.Tests/Security/ReadStreamSecurityTests.cs +++ b/test/Kurrent.Client.Tests/Security/ReadStreamSecurityTests.cs @@ -1,7 +1,8 @@ -using EventStore.Client.Tests.TestNode; -using EventStore.Client.Tests; +using EventStore.Client; +using Kurrent.Client.Tests.TestNode; +using Kurrent.Client.Tests; -namespace EventStore.Client.Tests; +namespace Kurrent.Client.Tests; [Trait("Category", "Security")] public class ReadStreamSecurityTests(ITestOutputHelper output, SecurityFixture fixture) : KurrentTemporaryTests(output, fixture) { diff --git a/test/EventStore.Client.Tests/Security/SecurityFixture.cs b/test/Kurrent.Client.Tests/Security/SecurityFixture.cs similarity index 99% rename from test/EventStore.Client.Tests/Security/SecurityFixture.cs rename to test/Kurrent.Client.Tests/Security/SecurityFixture.cs index 43edb97b6..366284a11 100644 --- a/test/EventStore.Client.Tests/Security/SecurityFixture.cs +++ b/test/Kurrent.Client.Tests/Security/SecurityFixture.cs @@ -1,7 +1,7 @@ using System.Runtime.CompilerServices; using EventStore.Client; -using EventStore.Client.Tests.TestNode; -using EventStore.Client.Tests; +using Kurrent.Client; +using Kurrent.Client.Tests.TestNode; public class SecurityFixture : KurrentTemporaryFixture { public const string NoAclStream = nameof(NoAclStream); diff --git a/test/EventStore.Client.Tests/Security/StreamSecurityInheritanceTests.cs b/test/Kurrent.Client.Tests/Security/StreamSecurityInheritanceTests.cs similarity index 98% rename from test/EventStore.Client.Tests/Security/StreamSecurityInheritanceTests.cs rename to test/Kurrent.Client.Tests/Security/StreamSecurityInheritanceTests.cs index 247da1a3e..dbe25c65e 100644 --- a/test/EventStore.Client.Tests/Security/StreamSecurityInheritanceTests.cs +++ b/test/Kurrent.Client.Tests/Security/StreamSecurityInheritanceTests.cs @@ -1,7 +1,8 @@ -using EventStore.Client.Tests.TestNode; -using EventStore.Client.Tests; +using EventStore.Client; +using Kurrent.Client.Tests.TestNode; +using Kurrent.Client.Tests; -namespace EventStore.Client.Tests; +namespace Kurrent.Client.Tests; [Trait("Category", "Security")] public class StreamSecurityInheritanceTests(ITestOutputHelper output, StreamSecurityInheritanceTests.CustomFixture fixture) diff --git a/test/EventStore.Client.Tests/Security/SubscribeToAllSecurityTests.cs b/test/Kurrent.Client.Tests/Security/SubscribeToAllSecurityTests.cs similarity index 89% rename from test/EventStore.Client.Tests/Security/SubscribeToAllSecurityTests.cs rename to test/Kurrent.Client.Tests/Security/SubscribeToAllSecurityTests.cs index c42e552f0..62f35497a 100644 --- a/test/EventStore.Client.Tests/Security/SubscribeToAllSecurityTests.cs +++ b/test/Kurrent.Client.Tests/Security/SubscribeToAllSecurityTests.cs @@ -1,7 +1,8 @@ -using EventStore.Client.Tests.TestNode; -using EventStore.Client.Tests; +using EventStore.Client; +using Kurrent.Client.Tests.TestNode; +using Kurrent.Client.Tests; -namespace EventStore.Client.Tests; +namespace Kurrent.Client.Tests; [Trait("Category", "Security")] public class SubscribeToAllSecurityTests(ITestOutputHelper output, SecurityFixture fixture) : KurrentTemporaryTests(output, fixture) { diff --git a/test/EventStore.Client.Tests/Security/SubscribeToStreamSecurityTests.cs b/test/Kurrent.Client.Tests/Security/SubscribeToStreamSecurityTests.cs similarity index 96% rename from test/EventStore.Client.Tests/Security/SubscribeToStreamSecurityTests.cs rename to test/Kurrent.Client.Tests/Security/SubscribeToStreamSecurityTests.cs index 6e34a6144..8d9625990 100644 --- a/test/EventStore.Client.Tests/Security/SubscribeToStreamSecurityTests.cs +++ b/test/Kurrent.Client.Tests/Security/SubscribeToStreamSecurityTests.cs @@ -1,7 +1,8 @@ -using EventStore.Client.Tests.TestNode; -using EventStore.Client.Tests; +using EventStore.Client; +using Kurrent.Client.Tests.TestNode; +using Kurrent.Client.Tests; -namespace EventStore.Client.Tests; +namespace Kurrent.Client.Tests; [Trait("Category", "Security")] public class SubscribeToStreamSecurityTests(ITestOutputHelper output, SecurityFixture fixture) diff --git a/test/EventStore.Client.Tests/Security/SystemStreamSecurityTests.cs b/test/Kurrent.Client.Tests/Security/SystemStreamSecurityTests.cs similarity index 98% rename from test/EventStore.Client.Tests/Security/SystemStreamSecurityTests.cs rename to test/Kurrent.Client.Tests/Security/SystemStreamSecurityTests.cs index f5ed68732..4ed213611 100644 --- a/test/EventStore.Client.Tests/Security/SystemStreamSecurityTests.cs +++ b/test/Kurrent.Client.Tests/Security/SystemStreamSecurityTests.cs @@ -1,7 +1,8 @@ -using EventStore.Client.Tests.TestNode; -using EventStore.Client.Tests; +using EventStore.Client; +using Kurrent.Client.Tests.TestNode; +using Kurrent.Client.Tests; -namespace EventStore.Client.Tests; +namespace Kurrent.Client.Tests; [Trait("Category", "Security")] public class SystemStreamSecurityTests(ITestOutputHelper output, SecurityFixture fixture) : KurrentTemporaryTests(output, fixture) { diff --git a/test/EventStore.Client.Tests/Security/WriteStreamMetaSecurityTests.cs b/test/Kurrent.Client.Tests/Security/WriteStreamMetaSecurityTests.cs similarity index 96% rename from test/EventStore.Client.Tests/Security/WriteStreamMetaSecurityTests.cs rename to test/Kurrent.Client.Tests/Security/WriteStreamMetaSecurityTests.cs index d2ace8af4..a2346b124 100644 --- a/test/EventStore.Client.Tests/Security/WriteStreamMetaSecurityTests.cs +++ b/test/Kurrent.Client.Tests/Security/WriteStreamMetaSecurityTests.cs @@ -1,7 +1,8 @@ -using EventStore.Client.Tests.TestNode; -using EventStore.Client.Tests; +using EventStore.Client; +using Kurrent.Client.Tests.TestNode; +using Kurrent.Client.Tests; -namespace EventStore.Client.Tests; +namespace Kurrent.Client.Tests; [Trait("Category", "Security")] public class WriteStreamMetaSecurityTests(ITestOutputHelper output, SecurityFixture fixture) : KurrentTemporaryTests(output, fixture) { diff --git a/test/EventStore.Client.Tests/Security/WriteStreamSecurityTests.cs b/test/Kurrent.Client.Tests/Security/WriteStreamSecurityTests.cs similarity index 98% rename from test/EventStore.Client.Tests/Security/WriteStreamSecurityTests.cs rename to test/Kurrent.Client.Tests/Security/WriteStreamSecurityTests.cs index aec66866a..0bb37a94d 100644 --- a/test/EventStore.Client.Tests/Security/WriteStreamSecurityTests.cs +++ b/test/Kurrent.Client.Tests/Security/WriteStreamSecurityTests.cs @@ -1,4 +1,6 @@ -namespace EventStore.Client.Tests; +using EventStore.Client; + +namespace Kurrent.Client.Tests; [Trait("Category", "Security")] public class WriteStreamSecurityTests : IClassFixture { diff --git a/test/EventStore.Client.Tests/SharingProviderTests.cs b/test/Kurrent.Client.Tests/SharingProviderTests.cs similarity index 97% rename from test/EventStore.Client.Tests/SharingProviderTests.cs rename to test/Kurrent.Client.Tests/SharingProviderTests.cs index efebe5b09..fc1f31abc 100644 --- a/test/EventStore.Client.Tests/SharingProviderTests.cs +++ b/test/Kurrent.Client.Tests/SharingProviderTests.cs @@ -1,6 +1,8 @@ -#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously +using EventStore.Client; -namespace EventStore.Client.Tests; +#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously + +namespace Kurrent.Client.Tests; public class SharingProviderTests { [RetryFact] diff --git a/test/EventStore.Client.Tests/StreamPositionTests.cs b/test/Kurrent.Client.Tests/StreamPositionTests.cs similarity index 98% rename from test/EventStore.Client.Tests/StreamPositionTests.cs rename to test/Kurrent.Client.Tests/StreamPositionTests.cs index d70475dd0..591ea617e 100644 --- a/test/EventStore.Client.Tests/StreamPositionTests.cs +++ b/test/Kurrent.Client.Tests/StreamPositionTests.cs @@ -1,6 +1,7 @@ using AutoFixture; +using EventStore.Client; -namespace EventStore.Client.Tests; +namespace Kurrent.Client.Tests; public class StreamPositionTests : ValueObjectTests { public StreamPositionTests() : base(new ScenarioFixture()) { } diff --git a/test/EventStore.Client.Tests/StreamRevisionTests.cs b/test/Kurrent.Client.Tests/StreamRevisionTests.cs similarity index 98% rename from test/EventStore.Client.Tests/StreamRevisionTests.cs rename to test/Kurrent.Client.Tests/StreamRevisionTests.cs index ddc479251..4b77f81d0 100644 --- a/test/EventStore.Client.Tests/StreamRevisionTests.cs +++ b/test/Kurrent.Client.Tests/StreamRevisionTests.cs @@ -1,6 +1,7 @@ using AutoFixture; +using EventStore.Client; -namespace EventStore.Client.Tests; +namespace Kurrent.Client.Tests; public class StreamRevisionTests : ValueObjectTests { public StreamRevisionTests() : base(new ScenarioFixture()) { } diff --git a/test/EventStore.Client.Tests/StreamStateTests.cs b/test/Kurrent.Client.Tests/StreamStateTests.cs similarity index 96% rename from test/EventStore.Client.Tests/StreamStateTests.cs rename to test/Kurrent.Client.Tests/StreamStateTests.cs index 7bd086099..b9c68df38 100644 --- a/test/EventStore.Client.Tests/StreamStateTests.cs +++ b/test/Kurrent.Client.Tests/StreamStateTests.cs @@ -1,7 +1,8 @@ using System.Reflection; using AutoFixture; +using EventStore.Client; -namespace EventStore.Client.Tests; +namespace Kurrent.Client.Tests; public class StreamStateTests : ValueObjectTests { public StreamStateTests() : base(new ScenarioFixture()) { } diff --git a/test/EventStore.Client.Tests/Streams/AppendTests.cs b/test/Kurrent.Client.Tests/Streams/AppendTests.cs similarity index 99% rename from test/EventStore.Client.Tests/Streams/AppendTests.cs rename to test/Kurrent.Client.Tests/Streams/AppendTests.cs index 3b2792b8f..0dab95225 100644 --- a/test/EventStore.Client.Tests/Streams/AppendTests.cs +++ b/test/Kurrent.Client.Tests/Streams/AppendTests.cs @@ -1,11 +1,12 @@ +using EventStore.Client; using Grpc.Core; using Humanizer; -namespace EventStore.Client.Tests.Streams; +namespace Kurrent.Client.Tests.Streams; [Trait("Category", "Target:Stream")] [Trait("Category", "Operation:Append")] -public class AppendTests(ITestOutputHelper output, KurrentPermanentFixture fixture) : EventStorePermanentTests(output, fixture) { +public class AppendTests(ITestOutputHelper output, KurrentPermanentFixture fixture) : KurrentPermanentTests(output, fixture) { [Theory, ExpectedVersionCreateStreamTestCases] public async Task appending_zero_events(StreamState expectedStreamState) { var stream = $"{Fixture.GetStreamName()}_{expectedStreamState}"; diff --git a/test/EventStore.Client.Tests/Streams/Bugs/Obsolete/Issue104.cs b/test/Kurrent.Client.Tests/Streams/Bugs/Obsolete/Issue104.cs similarity index 91% rename from test/EventStore.Client.Tests/Streams/Bugs/Obsolete/Issue104.cs rename to test/Kurrent.Client.Tests/Streams/Bugs/Obsolete/Issue104.cs index 47386e105..9eb0124b4 100644 --- a/test/EventStore.Client.Tests/Streams/Bugs/Obsolete/Issue104.cs +++ b/test/Kurrent.Client.Tests/Streams/Bugs/Obsolete/Issue104.cs @@ -1,8 +1,10 @@ -namespace EventStore.Client.Tests.Bugs.Obsolete; +using EventStore.Client; + +namespace Kurrent.Client.Tests.Bugs.Obsolete; [Trait("Category", "Bug")] [Obsolete("Tests will be removed in future release when older subscriptions APIs are removed from the client")] -public class Issue104(ITestOutputHelper output, KurrentPermanentFixture fixture) : EventStorePermanentTests(output, fixture) { +public class Issue104(ITestOutputHelper output, KurrentPermanentFixture fixture) : KurrentPermanentTests(output, fixture) { [Fact] public async Task subscription_does_not_send_checkpoint_reached_after_disposal() { var streamName = Fixture.GetStreamName(); diff --git a/test/EventStore.Client.Tests/Streams/Bugs/Obsolete/Issue2544.cs b/test/Kurrent.Client.Tests/Streams/Bugs/Obsolete/Issue2544.cs similarity index 98% rename from test/EventStore.Client.Tests/Streams/Bugs/Obsolete/Issue2544.cs rename to test/Kurrent.Client.Tests/Streams/Bugs/Obsolete/Issue2544.cs index e2353090e..027a70500 100644 --- a/test/EventStore.Client.Tests/Streams/Bugs/Obsolete/Issue2544.cs +++ b/test/Kurrent.Client.Tests/Streams/Bugs/Obsolete/Issue2544.cs @@ -1,6 +1,8 @@ // ReSharper disable InconsistentNaming -namespace EventStore.Client.Tests.Bugs.Obsolete; +using EventStore.Client; + +namespace Kurrent.Client.Tests.Bugs.Obsolete; [Trait("Category", "Bug")] [Obsolete("Tests will be removed in future release when older subscriptions APIs are removed from the client")] diff --git a/test/EventStore.Client.Tests/Streams/DeleteTests.cs b/test/Kurrent.Client.Tests/Streams/DeleteTests.cs similarity index 96% rename from test/EventStore.Client.Tests/Streams/DeleteTests.cs rename to test/Kurrent.Client.Tests/Streams/DeleteTests.cs index 69bca6b56..07b28f098 100644 --- a/test/EventStore.Client.Tests/Streams/DeleteTests.cs +++ b/test/Kurrent.Client.Tests/Streams/DeleteTests.cs @@ -1,10 +1,11 @@ +using EventStore.Client; using Grpc.Core; -namespace EventStore.Client.Tests.Streams; +namespace Kurrent.Client.Tests.Streams; [Trait("Category", "Target:Stream")] [Trait("Category", "Operation:Delete")] -public class DeleteTests(ITestOutputHelper output, KurrentPermanentFixture fixture) : EventStorePermanentTests(output, fixture) { +public class DeleteTests(ITestOutputHelper output, KurrentPermanentFixture fixture) : KurrentPermanentTests(output, fixture) { [Theory, ExpectedStreamStateCases] public async Task hard_deleting_a_stream_that_does_not_exist_with_expected_version_does_not_throw(StreamState expectedVersion, string name) { var stream = $"{Fixture.GetStreamName()}_{name}"; diff --git a/test/EventStore.Client.Tests/Streams/Read/EventBinaryData.cs b/test/Kurrent.Client.Tests/Streams/Read/EventBinaryData.cs similarity index 95% rename from test/EventStore.Client.Tests/Streams/Read/EventBinaryData.cs rename to test/Kurrent.Client.Tests/Streams/Read/EventBinaryData.cs index 53e221b5d..09f8ce6ae 100644 --- a/test/EventStore.Client.Tests/Streams/Read/EventBinaryData.cs +++ b/test/Kurrent.Client.Tests/Streams/Read/EventBinaryData.cs @@ -1,4 +1,6 @@ -namespace EventStore.Client.Tests; +using EventStore.Client; + +namespace Kurrent.Client.Tests; public readonly record struct EventBinaryData(Uuid Id, byte[] Data, byte[] Metadata) { public bool Equals(EventBinaryData other) => diff --git a/test/EventStore.Client.Tests/Streams/Read/EventDataComparer.cs b/test/Kurrent.Client.Tests/Streams/Read/EventDataComparer.cs similarity index 90% rename from test/EventStore.Client.Tests/Streams/Read/EventDataComparer.cs rename to test/Kurrent.Client.Tests/Streams/Read/EventDataComparer.cs index 9ffb2c2bc..86113e285 100644 --- a/test/EventStore.Client.Tests/Streams/Read/EventDataComparer.cs +++ b/test/Kurrent.Client.Tests/Streams/Read/EventDataComparer.cs @@ -1,4 +1,6 @@ -namespace EventStore.Client.Tests; +using EventStore.Client; + +namespace Kurrent.Client.Tests; static class EventDataComparer { public static bool Equal(EventData expected, EventRecord actual) { diff --git a/test/EventStore.Client.Tests/Streams/Read/ReadAllEventsBackwardTests.cs b/test/Kurrent.Client.Tests/Streams/Read/ReadAllEventsBackwardTests.cs similarity index 97% rename from test/EventStore.Client.Tests/Streams/Read/ReadAllEventsBackwardTests.cs rename to test/Kurrent.Client.Tests/Streams/Read/ReadAllEventsBackwardTests.cs index 1620842f9..3de2fd591 100644 --- a/test/EventStore.Client.Tests/Streams/Read/ReadAllEventsBackwardTests.cs +++ b/test/Kurrent.Client.Tests/Streams/Read/ReadAllEventsBackwardTests.cs @@ -1,7 +1,8 @@ -using EventStore.Client.Tests.TestNode; +using EventStore.Client; +using Kurrent.Client.Tests.TestNode; using Grpc.Core; -namespace EventStore.Client.Tests; +namespace Kurrent.Client.Tests; [Trait("Category", "Target:All")] [Trait("Category", "Operation:Read")] diff --git a/test/EventStore.Client.Tests/Streams/Read/ReadAllEventsFixture.cs b/test/Kurrent.Client.Tests/Streams/Read/ReadAllEventsFixture.cs similarity index 93% rename from test/EventStore.Client.Tests/Streams/Read/ReadAllEventsFixture.cs rename to test/Kurrent.Client.Tests/Streams/Read/ReadAllEventsFixture.cs index 979bcccc0..a6119c874 100644 --- a/test/EventStore.Client.Tests/Streams/Read/ReadAllEventsFixture.cs +++ b/test/Kurrent.Client.Tests/Streams/Read/ReadAllEventsFixture.cs @@ -1,7 +1,7 @@ -using EventStore.Client.Tests.TestNode; -using EventStore.Client.Tests; +using EventStore.Client; +using Kurrent.Client.Tests.TestNode; -namespace EventStore.Client.Tests; +namespace Kurrent.Client.Tests; [Trait("Category", "Target:All")] [Trait("Category", "Operation:Read")] diff --git a/test/EventStore.Client.Tests/Streams/Read/ReadAllEventsForwardTests.cs b/test/Kurrent.Client.Tests/Streams/Read/ReadAllEventsForwardTests.cs similarity index 98% rename from test/EventStore.Client.Tests/Streams/Read/ReadAllEventsForwardTests.cs rename to test/Kurrent.Client.Tests/Streams/Read/ReadAllEventsForwardTests.cs index fc927ef11..a8086faf6 100644 --- a/test/EventStore.Client.Tests/Streams/Read/ReadAllEventsForwardTests.cs +++ b/test/Kurrent.Client.Tests/Streams/Read/ReadAllEventsForwardTests.cs @@ -1,7 +1,8 @@ using System.Text; -using EventStore.Client.Tests.TestNode; +using EventStore.Client; +using Kurrent.Client.Tests.TestNode; -namespace EventStore.Client.Tests; +namespace Kurrent.Client.Tests; [Trait("Category", "Target:All")] [Trait("Category", "Operation:Read")] diff --git a/test/EventStore.Client.Tests/Streams/Read/ReadStreamBackwardTests.cs b/test/Kurrent.Client.Tests/Streams/Read/ReadStreamBackwardTests.cs similarity index 98% rename from test/EventStore.Client.Tests/Streams/Read/ReadStreamBackwardTests.cs rename to test/Kurrent.Client.Tests/Streams/Read/ReadStreamBackwardTests.cs index 01be08eb7..190a33e11 100644 --- a/test/EventStore.Client.Tests/Streams/Read/ReadStreamBackwardTests.cs +++ b/test/Kurrent.Client.Tests/Streams/Read/ReadStreamBackwardTests.cs @@ -1,7 +1,8 @@ -using EventStore.Client.Tests.TestNode; +using EventStore.Client; +using Kurrent.Client.Tests.TestNode; using Grpc.Core; -namespace EventStore.Client.Tests; +namespace Kurrent.Client.Tests; [Trait("Category", "Target:Stream")] [Trait("Category", "Operation:Read")] diff --git a/test/EventStore.Client.Tests/Streams/Read/ReadStreamEventsLinkedToDeletedStreamTests.cs b/test/Kurrent.Client.Tests/Streams/Read/ReadStreamEventsLinkedToDeletedStreamTests.cs similarity index 95% rename from test/EventStore.Client.Tests/Streams/Read/ReadStreamEventsLinkedToDeletedStreamTests.cs rename to test/Kurrent.Client.Tests/Streams/Read/ReadStreamEventsLinkedToDeletedStreamTests.cs index a96e82ab5..c5c151868 100644 --- a/test/EventStore.Client.Tests/Streams/Read/ReadStreamEventsLinkedToDeletedStreamTests.cs +++ b/test/Kurrent.Client.Tests/Streams/Read/ReadStreamEventsLinkedToDeletedStreamTests.cs @@ -1,10 +1,11 @@ using System.Text; -using EventStore.Client.Tests.TestNode; -using EventStore.Client.Tests; +using EventStore.Client; +using Kurrent.Client.Tests.TestNode; +using Kurrent.Client.Tests; // ReSharper disable ClassNeverInstantiated.Global -namespace EventStore.Client.Tests; +namespace Kurrent.Client.Tests; [Trait("Category", "Target:Stream")] public abstract class ReadStreamEventsLinkedToDeletedStreamTests(ReadEventsLinkedToDeletedStreamFixture fixture) { diff --git a/test/EventStore.Client.Tests/Streams/Read/ReadStreamForwardTests.cs b/test/Kurrent.Client.Tests/Streams/Read/ReadStreamForwardTests.cs similarity index 98% rename from test/EventStore.Client.Tests/Streams/Read/ReadStreamForwardTests.cs rename to test/Kurrent.Client.Tests/Streams/Read/ReadStreamForwardTests.cs index 4100cf010..d9d2a35d3 100644 --- a/test/EventStore.Client.Tests/Streams/Read/ReadStreamForwardTests.cs +++ b/test/Kurrent.Client.Tests/Streams/Read/ReadStreamForwardTests.cs @@ -1,9 +1,11 @@ -namespace EventStore.Client.Tests; +using EventStore.Client; + +namespace Kurrent.Client.Tests; [Trait("Category", "Target:Stream")] [Trait("Category", "Operation:Read")] [Trait("Category", "Operation:Read:Forwards")] -public class ReadStreamForwardTests(ITestOutputHelper output, KurrentPermanentFixture fixture) : EventStorePermanentTests(output, fixture) { +public class ReadStreamForwardTests(ITestOutputHelper output, KurrentPermanentFixture fixture) : KurrentPermanentTests(output, fixture) { [Theory] [InlineData(0)] public async Task count_le_equal_zero_throws(long maxCount) { diff --git a/test/EventStore.Client.Tests/Streams/Read/ReadStreamWhenHavingMaxCountSetForStreamTests.cs b/test/Kurrent.Client.Tests/Streams/Read/ReadStreamWhenHavingMaxCountSetForStreamTests.cs similarity index 97% rename from test/EventStore.Client.Tests/Streams/Read/ReadStreamWhenHavingMaxCountSetForStreamTests.cs rename to test/Kurrent.Client.Tests/Streams/Read/ReadStreamWhenHavingMaxCountSetForStreamTests.cs index fbd2696f1..10a3cce28 100644 --- a/test/EventStore.Client.Tests/Streams/Read/ReadStreamWhenHavingMaxCountSetForStreamTests.cs +++ b/test/Kurrent.Client.Tests/Streams/Read/ReadStreamWhenHavingMaxCountSetForStreamTests.cs @@ -1,7 +1,8 @@ -using EventStore.Client.Tests.TestNode; -using EventStore.Client.Tests; +using EventStore.Client; +using Kurrent.Client.Tests.TestNode; +using Kurrent.Client.Tests; -namespace EventStore.Client.Tests; +namespace Kurrent.Client.Tests; [Trait("Category", "Target:Stream")] [Trait("Category", "Operation:Read")] diff --git a/test/EventStore.Client.Tests/Streams/SoftDeleteTests.cs b/test/Kurrent.Client.Tests/Streams/SoftDeleteTests.cs similarity index 98% rename from test/EventStore.Client.Tests/Streams/SoftDeleteTests.cs rename to test/Kurrent.Client.Tests/Streams/SoftDeleteTests.cs index 7560167b8..84e4b8176 100644 --- a/test/EventStore.Client.Tests/Streams/SoftDeleteTests.cs +++ b/test/Kurrent.Client.Tests/Streams/SoftDeleteTests.cs @@ -1,10 +1,11 @@ using System.Text.Json; +using EventStore.Client; -namespace EventStore.Client.Tests.Streams; +namespace Kurrent.Client.Tests.Streams; [Trait("Category", "Target:Stream")] [Trait("Category", "Operation:Delete")] -public class SoftDeleteTests(ITestOutputHelper output, KurrentPermanentFixture fixture) : EventStorePermanentTests(output, fixture) { +public class SoftDeleteTests(ITestOutputHelper output, KurrentPermanentFixture fixture) : KurrentPermanentTests(output, fixture) { static JsonDocument CustomMetadata { get; } static SoftDeleteTests() { diff --git a/test/EventStore.Client.Tests/Streams/StreamMetadataTests.cs b/test/Kurrent.Client.Tests/Streams/StreamMetadataTests.cs similarity index 97% rename from test/EventStore.Client.Tests/Streams/StreamMetadataTests.cs rename to test/Kurrent.Client.Tests/Streams/StreamMetadataTests.cs index 5ba987732..23320b6b2 100644 --- a/test/EventStore.Client.Tests/Streams/StreamMetadataTests.cs +++ b/test/Kurrent.Client.Tests/Streams/StreamMetadataTests.cs @@ -1,11 +1,12 @@ using System.Text.Json; +using EventStore.Client; using Grpc.Core; -namespace EventStore.Client.Tests.Streams; +namespace Kurrent.Client.Tests.Streams; [Trait("Category", "Target:Stream")] [Trait("Category", "Operation:Metadata")] -public class StreamMetadataTests(ITestOutputHelper output, KurrentPermanentFixture fixture) : EventStorePermanentTests(output, fixture) { +public class StreamMetadataTests(ITestOutputHelper output, KurrentPermanentFixture fixture) : KurrentPermanentTests(output, fixture) { [Fact] public async Task getting_for_an_existing_stream_and_no_metadata_exists() { var stream = Fixture.GetStreamName(); diff --git a/test/EventStore.Client.Tests/Streams/SubscribeToStreamTests.cs b/test/Kurrent.Client.Tests/Streams/SubscribeToStreamTests.cs similarity index 97% rename from test/EventStore.Client.Tests/Streams/SubscribeToStreamTests.cs rename to test/Kurrent.Client.Tests/Streams/SubscribeToStreamTests.cs index 00d38e1e5..61ca29ebb 100644 --- a/test/EventStore.Client.Tests/Streams/SubscribeToStreamTests.cs +++ b/test/Kurrent.Client.Tests/Streams/SubscribeToStreamTests.cs @@ -1,9 +1,11 @@ -namespace EventStore.Client.Tests.Streams; +using EventStore.Client; + +namespace Kurrent.Client.Tests.Streams; [Trait("Category", "Subscriptions")] [Trait("Category", "Target:Stream")] public class SubscribeToStreamTests(ITestOutputHelper output, SubscribeToStreamTests.CustomFixture fixture) - : EventStorePermanentTests(output, fixture) { + : KurrentPermanentTests(output, fixture) { [RetryFact] public async Task receives_all_events_from_start() { var streamName = Fixture.GetStreamName(); diff --git a/test/EventStore.Client.Tests/Streams/SubscriptionFilter.cs b/test/Kurrent.Client.Tests/Streams/SubscriptionFilter.cs similarity index 94% rename from test/EventStore.Client.Tests/Streams/SubscriptionFilter.cs rename to test/Kurrent.Client.Tests/Streams/SubscriptionFilter.cs index 937f7d1a2..91621aba9 100644 --- a/test/EventStore.Client.Tests/Streams/SubscriptionFilter.cs +++ b/test/Kurrent.Client.Tests/Streams/SubscriptionFilter.cs @@ -1,5 +1,8 @@ // ReSharper disable InconsistentNaming -namespace EventStore.Client.Tests.Streams; + +using EventStore.Client; + +namespace Kurrent.Client.Tests.Streams; public record SubscriptionFilter(string Name, Func Create, Func PrepareEvent) { public override string ToString() => Name; diff --git a/test/EventStore.Client.Tests/UuidTests.cs b/test/Kurrent.Client.Tests/UuidTests.cs similarity index 96% rename from test/EventStore.Client.Tests/UuidTests.cs rename to test/Kurrent.Client.Tests/UuidTests.cs index 4533e5416..feeadb4e7 100644 --- a/test/EventStore.Client.Tests/UuidTests.cs +++ b/test/Kurrent.Client.Tests/UuidTests.cs @@ -1,6 +1,7 @@ using AutoFixture; +using EventStore.Client; -namespace EventStore.Client.Tests; +namespace Kurrent.Client.Tests; public class UuidTests : ValueObjectTests { public UuidTests() : base(new ScenarioFixture()) { } diff --git a/test/EventStore.Client.Tests/ValueObjectTests.cs b/test/Kurrent.Client.Tests/ValueObjectTests.cs similarity index 92% rename from test/EventStore.Client.Tests/ValueObjectTests.cs rename to test/Kurrent.Client.Tests/ValueObjectTests.cs index c70b5f7d6..dc4e9f2b0 100644 --- a/test/EventStore.Client.Tests/ValueObjectTests.cs +++ b/test/Kurrent.Client.Tests/ValueObjectTests.cs @@ -1,6 +1,6 @@ using AutoFixture; -namespace EventStore.Client.Tests; +namespace Kurrent.Client.Tests; public abstract class ValueObjectTests { protected readonly Fixture _fixture; diff --git a/test/EventStore.Client.Tests/X509CertificatesTests.cs b/test/Kurrent.Client.Tests/X509CertificatesTests.cs similarity index 95% rename from test/EventStore.Client.Tests/X509CertificatesTests.cs rename to test/Kurrent.Client.Tests/X509CertificatesTests.cs index 5d70fdb89..71d4bc752 100644 --- a/test/EventStore.Client.Tests/X509CertificatesTests.cs +++ b/test/Kurrent.Client.Tests/X509CertificatesTests.cs @@ -1,6 +1,7 @@ using System.Security.Cryptography.X509Certificates; +using EventStore.Client; -namespace EventStore.Client.Tests; +namespace Kurrent.Client.Tests; public class X509CertificatesTests { [RetryFact] From 740872b0d00de2ca3bd419589856a8df3e684276 Mon Sep 17 00:00:00 2001 From: William Chong Date: Tue, 7 Jan 2025 12:51:25 +0400 Subject: [PATCH 06/15] Test workflow --- .github/workflows/base.yml | 1 + .../Fixtures/KurrentPermanentFixture.cs | 72 +++++++++---------- .../Fixtures/KurrentTemporaryFixture.cs | 72 +++++++++---------- 3 files changed, 73 insertions(+), 72 deletions(-) diff --git a/.github/workflows/base.yml b/.github/workflows/base.yml index c57a9ce3d..43d5ce4c5 100644 --- a/.github/workflows/base.yml +++ b/.github/workflows/base.yml @@ -53,6 +53,7 @@ jobs: uses: actions/setup-dotnet@v3 with: dotnet-version: | + 8.0.x 9.0.x - name: Run Tests shell: bash diff --git a/test/Kurrent.Client.Tests.Common/Fixtures/KurrentPermanentFixture.cs b/test/Kurrent.Client.Tests.Common/Fixtures/KurrentPermanentFixture.cs index 2530126e0..ddf9e8c93 100644 --- a/test/Kurrent.Client.Tests.Common/Fixtures/KurrentPermanentFixture.cs +++ b/test/Kurrent.Client.Tests.Common/Fixtures/KurrentPermanentFixture.cs @@ -37,11 +37,11 @@ protected KurrentPermanentFixture(ConfigureFixture configure) { public ILogger Log => Logger; - public ITestService Service { get; } + public ITestService Service { get; } public KurrentFixtureOptions Options { get; } - public Faker Faker { get; } = new Faker(); + public Faker Faker { get; } = new Faker(); - public Version EventStoreVersion { get; private set; } = null!; + // public Version EventStoreVersion { get; private set; } = null!; public bool EventStoreHasLastStreamPosition { get; private set; } public KurrentClient Streams { get; private set; } = null!; @@ -87,13 +87,13 @@ public async Task InitializeAsync() { await ContainerSemaphore.WaitAsync(); try { await Service.Start(); + // EventStoreVersion = GetEventStoreVersion(); + // EventStoreHasLastStreamPosition = (EventStoreVersion?.Major ?? int.MaxValue) >= 21; + EventStoreHasLastStreamPosition = true; } finally { ContainerSemaphore.Release(); } - EventStoreVersion = GetEventStoreVersion(); - EventStoreHasLastStreamPosition = (EventStoreVersion?.Major ?? int.MaxValue) >= 21; - await WarmUpGatekeeper.WaitAsync(); try { @@ -133,36 +133,36 @@ async Task InitClient(Func action, bool execute = true) where T : return client; } - static Version GetEventStoreVersion() { - const string versionPrefix = "EventStoreDB version"; - - using var cancellator = new CancellationTokenSource(FromSeconds(30)); - using var eventstore = new Builder() - .UseContainer() - .UseImage(GlobalEnvironment.DockerImage) - .Command("--version") - .Build() - .Start(); - - using var log = eventstore.Logs(true, cancellator.Token); - foreach (var line in log.ReadToEnd()) { - if (line.StartsWith(versionPrefix) && - Version.TryParse( - new string(ReadVersion(line[(versionPrefix.Length + 1)..]).ToArray()), - out var version - )) { - return version; - } - } - - throw new InvalidOperationException("Could not determine server version."); - - IEnumerable ReadVersion(string s) { - foreach (var c in s.TakeWhile(c => c == '.' || char.IsDigit(c))) { - yield return c; - } - } - } + // static Version GetEventStoreVersion() { + // const string versionPrefix = "EventStoreDB version"; + // + // using var cancellator = new CancellationTokenSource(FromSeconds(30)); + // using var eventstore = new Builder() + // .UseContainer() + // .UseImage(GlobalEnvironment.DockerImage) + // .Command("--version") + // .Build() + // .Start(); + // + // using var log = eventstore.Logs(true, cancellator.Token); + // foreach (var line in log.ReadToEnd()) { + // if (line.StartsWith(versionPrefix) && + // Version.TryParse( + // new string(ReadVersion(line[(versionPrefix.Length + 1)..]).ToArray()), + // out var version + // )) { + // return version; + // } + // } + // + // throw new InvalidOperationException("Could not determine server version."); + // + // IEnumerable ReadVersion(string s) { + // foreach (var c in s.TakeWhile(c => c == '.' || char.IsDigit(c))) { + // yield return c; + // } + // } + // } } public async Task DisposeAsync() { diff --git a/test/Kurrent.Client.Tests.Common/Fixtures/KurrentTemporaryFixture.cs b/test/Kurrent.Client.Tests.Common/Fixtures/KurrentTemporaryFixture.cs index c0c054e1c..7b5ccd6bb 100644 --- a/test/Kurrent.Client.Tests.Common/Fixtures/KurrentTemporaryFixture.cs +++ b/test/Kurrent.Client.Tests.Common/Fixtures/KurrentTemporaryFixture.cs @@ -40,11 +40,11 @@ protected KurrentTemporaryFixture(ConfigureFixture configure) { public ILogger Log => Logger; - public ITestService Service { get; } + public ITestService Service { get; } public KurrentFixtureOptions Options { get; } - public Faker Faker { get; } = new Faker(); + public Faker Faker { get; } = new Faker(); - public Version EventStoreVersion { get; private set; } = null!; + // public Version EventStoreVersion { get; private set; } = null!; public bool EventStoreHasLastStreamPosition { get; private set; } public KurrentClient Streams { get; private set; } = null!; @@ -89,13 +89,13 @@ public async Task InitializeAsync() { await ContainerSemaphore.WaitAsync(); try { await Service.Start(); + // EventStoreVersion = GetEventStoreVersion(); + // EventStoreHasLastStreamPosition = (EventStoreVersion?.Major ?? int.MaxValue) >= 21; + EventStoreHasLastStreamPosition = true; } finally { ContainerSemaphore.Release(); } - EventStoreVersion = GetEventStoreVersion(); - EventStoreHasLastStreamPosition = (EventStoreVersion?.Major ?? int.MaxValue) >= 21; - await WarmUpGatekeeper.WaitAsync(); try { @@ -135,36 +135,36 @@ async Task InitClient(Func action, bool execute = true) where T : return client; } - static Version GetEventStoreVersion() { - const string versionPrefix = "EventStoreDB version"; - - using var cancellator = new CancellationTokenSource(FromSeconds(30)); - using var eventstore = new Builder() - .UseContainer() - .UseImage(GlobalEnvironment.DockerImage) - .Command("--version") - .Build() - .Start(); - - using var log = eventstore.Logs(true, cancellator.Token); - foreach (var line in log.ReadToEnd()) { - if (line.StartsWith(versionPrefix) && - Version.TryParse( - new string(ReadVersion(line[(versionPrefix.Length + 1)..]).ToArray()), - out var version - )) { - return version; - } - } - - throw new InvalidOperationException("Could not determine server version."); - - IEnumerable ReadVersion(string s) { - foreach (var c in s.TakeWhile(c => c == '.' || char.IsDigit(c))) { - yield return c; - } - } - } + // static Version GetEventStoreVersion() { + // const string versionPrefix = "EventStoreDB version"; + // + // using var cancellator = new CancellationTokenSource(FromSeconds(30)); + // using var eventstore = new Builder() + // .UseContainer() + // .UseImage(GlobalEnvironment.DockerImage) + // .Command("--version") + // .Build() + // .Start(); + // + // using var log = eventstore.Logs(true, cancellator.Token); + // foreach (var line in log.ReadToEnd()) { + // if (line.StartsWith(versionPrefix) && + // Version.TryParse( + // new string(ReadVersion(line[(versionPrefix.Length + 1)..]).ToArray()), + // out var version + // )) { + // return version; + // } + // } + // + // throw new InvalidOperationException("Could not determine server version."); + // + // IEnumerable ReadVersion(string s) { + // foreach (var c in s.TakeWhile(c => c == '.' || char.IsDigit(c))) { + // yield return c; + // } + // } + // } } public async Task DisposeAsync() { From f68cec043df2e870dbb8b4a0ff3b9141316fba12 Mon Sep 17 00:00:00 2001 From: William Chong Date: Tue, 7 Jan 2025 16:53:25 +0400 Subject: [PATCH 07/15] Testing something --- .../Fixtures/KurrentPermanentFixture.cs | 70 ++++++++++--------- .../Fixtures/KurrentTemporaryFixture.cs | 70 ++++++++++--------- 2 files changed, 72 insertions(+), 68 deletions(-) diff --git a/test/Kurrent.Client.Tests.Common/Fixtures/KurrentPermanentFixture.cs b/test/Kurrent.Client.Tests.Common/Fixtures/KurrentPermanentFixture.cs index ddf9e8c93..c5da8c280 100644 --- a/test/Kurrent.Client.Tests.Common/Fixtures/KurrentPermanentFixture.cs +++ b/test/Kurrent.Client.Tests.Common/Fixtures/KurrentPermanentFixture.cs @@ -41,7 +41,7 @@ protected KurrentPermanentFixture(ConfigureFixture configure) { public KurrentFixtureOptions Options { get; } public Faker Faker { get; } = new Faker(); - // public Version EventStoreVersion { get; private set; } = null!; + public Version EventStoreVersion { get; private set; } = null!; public bool EventStoreHasLastStreamPosition { get; private set; } public KurrentClient Streams { get; private set; } = null!; @@ -87,9 +87,9 @@ public async Task InitializeAsync() { await ContainerSemaphore.WaitAsync(); try { await Service.Start(); - // EventStoreVersion = GetEventStoreVersion(); - // EventStoreHasLastStreamPosition = (EventStoreVersion?.Major ?? int.MaxValue) >= 21; - EventStoreHasLastStreamPosition = true; + EventStoreVersion = GetEventStoreVersion(); + EventStoreHasLastStreamPosition = (EventStoreVersion?.Major ?? int.MaxValue) >= 21; + // EventStoreHasLastStreamPosition = true; } finally { ContainerSemaphore.Release(); } @@ -133,36 +133,38 @@ async Task InitClient(Func action, bool execute = true) where T : return client; } - // static Version GetEventStoreVersion() { - // const string versionPrefix = "EventStoreDB version"; - // - // using var cancellator = new CancellationTokenSource(FromSeconds(30)); - // using var eventstore = new Builder() - // .UseContainer() - // .UseImage(GlobalEnvironment.DockerImage) - // .Command("--version") - // .Build() - // .Start(); - // - // using var log = eventstore.Logs(true, cancellator.Token); - // foreach (var line in log.ReadToEnd()) { - // if (line.StartsWith(versionPrefix) && - // Version.TryParse( - // new string(ReadVersion(line[(versionPrefix.Length + 1)..]).ToArray()), - // out var version - // )) { - // return version; - // } - // } - // - // throw new InvalidOperationException("Could not determine server version."); - // - // IEnumerable ReadVersion(string s) { - // foreach (var c in s.TakeWhile(c => c == '.' || char.IsDigit(c))) { - // yield return c; - // } - // } - // } + static Version GetEventStoreVersion() { + const string versionPrefix = "EventStoreDB version"; + + using var cancellator = new CancellationTokenSource(FromSeconds(30)); + using var eventstore = new Builder() + .UseContainer() + .UseImage(GlobalEnvironment.DockerImage) + .Command("--version") + .Build() + .Start(); + + using var log = eventstore.Logs(true, cancellator.Token); + foreach (var line in log.ReadToEnd()) { + Logger.Warning("line---> {line}", line); + + if (line.StartsWith(versionPrefix) && + Version.TryParse( + new string(ReadVersion(line[(versionPrefix.Length + 1)..]).ToArray()), + out var version + )) { + return version; + } + } + + throw new InvalidOperationException("Could not determine server version."); + + IEnumerable ReadVersion(string s) { + foreach (var c in s.TakeWhile(c => c == '.' || char.IsDigit(c))) { + yield return c; + } + } + } } public async Task DisposeAsync() { diff --git a/test/Kurrent.Client.Tests.Common/Fixtures/KurrentTemporaryFixture.cs b/test/Kurrent.Client.Tests.Common/Fixtures/KurrentTemporaryFixture.cs index 7b5ccd6bb..146808d20 100644 --- a/test/Kurrent.Client.Tests.Common/Fixtures/KurrentTemporaryFixture.cs +++ b/test/Kurrent.Client.Tests.Common/Fixtures/KurrentTemporaryFixture.cs @@ -44,7 +44,7 @@ protected KurrentTemporaryFixture(ConfigureFixture configure) { public KurrentFixtureOptions Options { get; } public Faker Faker { get; } = new Faker(); - // public Version EventStoreVersion { get; private set; } = null!; + public Version EventStoreVersion { get; private set; } = null!; public bool EventStoreHasLastStreamPosition { get; private set; } public KurrentClient Streams { get; private set; } = null!; @@ -89,9 +89,9 @@ public async Task InitializeAsync() { await ContainerSemaphore.WaitAsync(); try { await Service.Start(); - // EventStoreVersion = GetEventStoreVersion(); - // EventStoreHasLastStreamPosition = (EventStoreVersion?.Major ?? int.MaxValue) >= 21; - EventStoreHasLastStreamPosition = true; + EventStoreVersion = GetEventStoreVersion(); + EventStoreHasLastStreamPosition = (EventStoreVersion?.Major ?? int.MaxValue) >= 21; + // EventStoreHasLastStreamPosition = true; } finally { ContainerSemaphore.Release(); } @@ -135,36 +135,38 @@ async Task InitClient(Func action, bool execute = true) where T : return client; } - // static Version GetEventStoreVersion() { - // const string versionPrefix = "EventStoreDB version"; - // - // using var cancellator = new CancellationTokenSource(FromSeconds(30)); - // using var eventstore = new Builder() - // .UseContainer() - // .UseImage(GlobalEnvironment.DockerImage) - // .Command("--version") - // .Build() - // .Start(); - // - // using var log = eventstore.Logs(true, cancellator.Token); - // foreach (var line in log.ReadToEnd()) { - // if (line.StartsWith(versionPrefix) && - // Version.TryParse( - // new string(ReadVersion(line[(versionPrefix.Length + 1)..]).ToArray()), - // out var version - // )) { - // return version; - // } - // } - // - // throw new InvalidOperationException("Could not determine server version."); - // - // IEnumerable ReadVersion(string s) { - // foreach (var c in s.TakeWhile(c => c == '.' || char.IsDigit(c))) { - // yield return c; - // } - // } - // } + static Version GetEventStoreVersion() { + const string versionPrefix = "EventStoreDB version"; + + using var cancellator = new CancellationTokenSource(FromSeconds(30)); + using var eventstore = new Builder() + .UseContainer() + .UseImage(GlobalEnvironment.DockerImage) + .Command("--version") + .Build() + .Start(); + + using var log = eventstore.Logs(true, cancellator.Token); + foreach (var line in log.ReadToEnd()) { + Logger.Warning("line---> {line}", line); + + if (line.StartsWith(versionPrefix) && + Version.TryParse( + new string(ReadVersion(line[(versionPrefix.Length + 1)..]).ToArray()), + out var version + )) { + return version; + } + } + + throw new InvalidOperationException("Could not determine server version."); + + IEnumerable ReadVersion(string s) { + foreach (var c in s.TakeWhile(c => c == '.' || char.IsDigit(c))) { + yield return c; + } + } + } } public async Task DisposeAsync() { From 769915207e17e085bbcbb5e48df47f6cfc5e2a1e Mon Sep 17 00:00:00 2001 From: William Chong Date: Tue, 7 Jan 2025 17:26:07 +0400 Subject: [PATCH 08/15] More tests --- .github/workflows/base.yml | 13 +- .github/workflows/ci.yml | 5 +- .../Fixtures/KurrentPermanentFixture.cs | 7 +- .../Fixtures/KurrentPermanentTestNode.cs | 2 +- .../Fixtures/KurrentTemporaryFixture.cs | 7 +- .../Projections/DisableProjectionTests.cs | 21 +++ .../Projections/EnableProjectionTests.cs | 21 +++ .../Projections/GetProjectionResultTests.cs | 50 +++++++ .../Projections/GetProjectionStateTests.cs | 51 +++++++ .../Projections/GetProjectionStatusTests.cs | 21 +++ .../Projections/ProjectionManagementTests.cs | 131 ------------------ .../Projections/RestartSubsystemTests.cs | 19 +++ .../Projections/UpdateProjectionTests.cs | 30 ++++ 13 files changed, 222 insertions(+), 156 deletions(-) create mode 100644 test/Kurrent.Client.Tests/Projections/DisableProjectionTests.cs create mode 100644 test/Kurrent.Client.Tests/Projections/EnableProjectionTests.cs create mode 100644 test/Kurrent.Client.Tests/Projections/GetProjectionResultTests.cs create mode 100644 test/Kurrent.Client.Tests/Projections/GetProjectionStateTests.cs create mode 100644 test/Kurrent.Client.Tests/Projections/GetProjectionStatusTests.cs create mode 100644 test/Kurrent.Client.Tests/Projections/RestartSubsystemTests.cs create mode 100644 test/Kurrent.Client.Tests/Projections/UpdateProjectionTests.cs diff --git a/.github/workflows/base.yml b/.github/workflows/base.yml index 43d5ce4c5..27a82d347 100644 --- a/.github/workflows/base.yml +++ b/.github/workflows/base.yml @@ -15,9 +15,6 @@ on: required: false type: string default: eventstore-ce/eventstoredb-ce -# test: -# required: true -# type: string jobs: test: @@ -25,11 +22,10 @@ jobs: strategy: fail-fast: false matrix: - framework: [ net9.0 ] + framework: [ net8.0, net9.0 ] os: [ ubuntu-latest ] configuration: [ release ] runs-on: ${{ matrix.os }} -# name: ${{ inputs.test }} (${{ matrix.os }}, ${{ matrix.framework }}) name: (${{ matrix.os }}, ${{ matrix.framework }}) env: CLOUDSMITH_CICD_USER: ${{ secrets.CLOUDSMITH_CICD_USER }} @@ -66,10 +62,3 @@ jobs: --logger:"GitHubActions;report-warnings=false" --logger:"console;verbosity=normal" \ --framework ${{ matrix.framework }} \ test/Kurrent.Client.Tests - -# run: | -# sudo ./gencert.sh -# dotnet test --configuration ${{ matrix.configuration }} --blame \ -# --logger:"GitHubActions;report-warnings=false" --logger:"console;verbosity=normal" \ -# --framework ${{ matrix.framework }} \ -# test/EventStore.Client.${{ inputs.test }}.Tests diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1f408ae3c..5b659d1fe 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,14 +14,11 @@ jobs: strategy: fail-fast: false matrix: -# docker-tag: [ ci, lts, previous-lts ] - docker-tag: [ ci ] -# test: [ Streams, PersistentSubscriptions, Operations, UserManagement, ProjectionManagement ] + docker-tag: [ ci, lts, previous-lts ] name: Test CE (${{ matrix.docker-tag }}) with: docker-tag: ${{ matrix.docker-tag }} docker-image: eventstore-ce/eventstoredb-ce -# test: ${{ matrix.test }} # ee: # uses: ./.github/workflows/base.yml # if: ${{ github.repository_owner == 'EventStore' }} diff --git a/test/Kurrent.Client.Tests.Common/Fixtures/KurrentPermanentFixture.cs b/test/Kurrent.Client.Tests.Common/Fixtures/KurrentPermanentFixture.cs index c5da8c280..8e9f5bf56 100644 --- a/test/Kurrent.Client.Tests.Common/Fixtures/KurrentPermanentFixture.cs +++ b/test/Kurrent.Client.Tests.Common/Fixtures/KurrentPermanentFixture.cs @@ -87,7 +87,7 @@ public async Task InitializeAsync() { await ContainerSemaphore.WaitAsync(); try { await Service.Start(); - EventStoreVersion = GetEventStoreVersion(); + EventStoreVersion = GetKurrentVersion(); EventStoreHasLastStreamPosition = (EventStoreVersion?.Major ?? int.MaxValue) >= 21; // EventStoreHasLastStreamPosition = true; } finally { @@ -133,7 +133,7 @@ async Task InitClient(Func action, bool execute = true) where T : return client; } - static Version GetEventStoreVersion() { + static Version GetKurrentVersion() { const string versionPrefix = "EventStoreDB version"; using var cancellator = new CancellationTokenSource(FromSeconds(30)); @@ -146,8 +146,7 @@ static Version GetEventStoreVersion() { using var log = eventstore.Logs(true, cancellator.Token); foreach (var line in log.ReadToEnd()) { - Logger.Warning("line---> {line}", line); - + Logger.Information("EventStoreDB: {Line}", line); if (line.StartsWith(versionPrefix) && Version.TryParse( new string(ReadVersion(line[(versionPrefix.Length + 1)..]).ToArray()), diff --git a/test/Kurrent.Client.Tests.Common/Fixtures/KurrentPermanentTestNode.cs b/test/Kurrent.Client.Tests.Common/Fixtures/KurrentPermanentTestNode.cs index e8c554f02..b93bc9f89 100644 --- a/test/Kurrent.Client.Tests.Common/Fixtures/KurrentPermanentTestNode.cs +++ b/test/Kurrent.Client.Tests.Common/Fixtures/KurrentPermanentTestNode.cs @@ -189,7 +189,7 @@ async Task GetNextAvailablePort(TimeSpan delay = default) { #if NET if (socket.Connected) await socket.DisconnectAsync(true); #else - if (socket.Connected) socket.Disconnect(true); + if (socket.Connected) socket.Disconnect(true); #endif } } diff --git a/test/Kurrent.Client.Tests.Common/Fixtures/KurrentTemporaryFixture.cs b/test/Kurrent.Client.Tests.Common/Fixtures/KurrentTemporaryFixture.cs index 146808d20..3be5f5229 100644 --- a/test/Kurrent.Client.Tests.Common/Fixtures/KurrentTemporaryFixture.cs +++ b/test/Kurrent.Client.Tests.Common/Fixtures/KurrentTemporaryFixture.cs @@ -89,7 +89,7 @@ public async Task InitializeAsync() { await ContainerSemaphore.WaitAsync(); try { await Service.Start(); - EventStoreVersion = GetEventStoreVersion(); + EventStoreVersion = GetKurrentVersion(); EventStoreHasLastStreamPosition = (EventStoreVersion?.Major ?? int.MaxValue) >= 21; // EventStoreHasLastStreamPosition = true; } finally { @@ -135,7 +135,7 @@ async Task InitClient(Func action, bool execute = true) where T : return client; } - static Version GetEventStoreVersion() { + static Version GetKurrentVersion() { const string versionPrefix = "EventStoreDB version"; using var cancellator = new CancellationTokenSource(FromSeconds(30)); @@ -148,8 +148,7 @@ static Version GetEventStoreVersion() { using var log = eventstore.Logs(true, cancellator.Token); foreach (var line in log.ReadToEnd()) { - Logger.Warning("line---> {line}", line); - + Logger.Information("EventStoreDB: {Line}", line); if (line.StartsWith(versionPrefix) && Version.TryParse( new string(ReadVersion(line[(versionPrefix.Length + 1)..]).ToArray()), diff --git a/test/Kurrent.Client.Tests/Projections/DisableProjectionTests.cs b/test/Kurrent.Client.Tests/Projections/DisableProjectionTests.cs new file mode 100644 index 000000000..debe5d3db --- /dev/null +++ b/test/Kurrent.Client.Tests/Projections/DisableProjectionTests.cs @@ -0,0 +1,21 @@ +using Kurrent.Client.Tests.TestNode; + +namespace Kurrent.Client.Tests.Projections; + +public class DisableProjectionTests(ITestOutputHelper output, DisableProjectionTests.CustomFixture fixture) + : KurrentTemporaryTests(output, fixture) { + [Fact] + public async Task disable_projection() { + var name = Names.First(); + await Fixture.Projections.DisableAsync(name, userCredentials: TestCredentials.Root); + var result = await Fixture.Projections.GetStatusAsync(name, userCredentials: TestCredentials.Root); + Assert.NotNull(result); + Assert.Contains(["Aborted/Stopped", "Stopped"], x => x == result!.Status); + } + + static readonly string[] Names = ["$streams", "$stream_by_category", "$by_category", "$by_event_type", "$by_correlation_id"]; + + public class CustomFixture : KurrentTemporaryFixture { + public CustomFixture() : base(x => x.RunProjections()) { } + } +} diff --git a/test/Kurrent.Client.Tests/Projections/EnableProjectionTests.cs b/test/Kurrent.Client.Tests/Projections/EnableProjectionTests.cs new file mode 100644 index 000000000..26e49b972 --- /dev/null +++ b/test/Kurrent.Client.Tests/Projections/EnableProjectionTests.cs @@ -0,0 +1,21 @@ +using Kurrent.Client.Tests.TestNode; + +namespace Kurrent.Client.Tests.Projections; + +public class EnableProjectionTests(ITestOutputHelper output, EnableProjectionTests.CustomFixture fixture) + : KurrentTemporaryTests(output, fixture) { + [Fact] + public async Task enable_projection() { + var name = Names.First(); + await Fixture.Projections.EnableAsync(name, userCredentials: TestCredentials.Root); + var result = await Fixture.Projections.GetStatusAsync(name, userCredentials: TestCredentials.Root); + Assert.NotNull(result); + Assert.Equal("Running", result.Status); + } + + static readonly string[] Names = ["$streams", "$stream_by_category", "$by_category", "$by_event_type", "$by_correlation_id"]; + + public class CustomFixture : KurrentTemporaryFixture { + public CustomFixture() : base(x => x.RunProjections()) { } + } +} diff --git a/test/Kurrent.Client.Tests/Projections/GetProjectionResultTests.cs b/test/Kurrent.Client.Tests/Projections/GetProjectionResultTests.cs new file mode 100644 index 000000000..bf6ee80d9 --- /dev/null +++ b/test/Kurrent.Client.Tests/Projections/GetProjectionResultTests.cs @@ -0,0 +1,50 @@ +using EventStore.Client; +using Kurrent.Client.Tests.TestNode; + +namespace Kurrent.Client.Tests.Projections; + +public class GetProjectionResultTests(ITestOutputHelper output, GetProjectionResultTests.CustomFixture fixture) + : KurrentTemporaryTests(output, fixture) { + [Fact] + public async Task get_result() { + var name = Fixture.GetProjectionName(); + Result? result = null; + + var projection = $$""" + fromStream('{{name}}').when({ + "$init": function() { return { Count: 0 }; }, + "$any": function(s, e) { s.Count++; return s; } + }); + """; + + await Fixture.Projections.CreateContinuousAsync( + name, + projection, + userCredentials: TestCredentials.Root + ); + + await Fixture.Streams.AppendToStreamAsync( + name, + StreamState.NoStream, + Fixture.CreateTestEvents() + ); + + await AssertEx.IsOrBecomesTrue( + async () => { + result = await Fixture.Projections.GetResultAsync(name, userCredentials: TestCredentials.Root); + return result.Count > 0; + } + ); + + Assert.NotNull(result); + Assert.Equal(1, result!.Count); + } + + record Result { + public int Count { get; set; } + } + + public class CustomFixture : KurrentTemporaryFixture { + public CustomFixture() : base(x => x.RunProjections()) { } + } +} diff --git a/test/Kurrent.Client.Tests/Projections/GetProjectionStateTests.cs b/test/Kurrent.Client.Tests/Projections/GetProjectionStateTests.cs new file mode 100644 index 000000000..a762d97fd --- /dev/null +++ b/test/Kurrent.Client.Tests/Projections/GetProjectionStateTests.cs @@ -0,0 +1,51 @@ +using EventStore.Client; +using Kurrent.Client.Tests.TestNode; + +namespace Kurrent.Client.Tests.Projections; + +public class GetProjectionStateTests(ITestOutputHelper output, GetProjectionStateTests.CustomFixture fixture) + : KurrentTemporaryTests(output, fixture) { + [Fact] + public async Task get_state() { + var name = Fixture.GetProjectionName(); + + var projection = $$""" + fromStream('{{name}}').when({ + "$init": function() { return { Count: 0 }; }, + "$any": function(s, e) { s.Count++; return s; } + }); + """; + + Result? result = null; + + await Fixture.Projections.CreateContinuousAsync( + name, + projection, + userCredentials: TestCredentials.Root + ); + + await Fixture.Streams.AppendToStreamAsync( + name, + StreamState.NoStream, + Fixture.CreateTestEvents() + ); + + await AssertEx.IsOrBecomesTrue( + async () => { + result = await Fixture.Projections.GetStateAsync(name, userCredentials: TestCredentials.Root); + return result.Count > 0; + } + ); + + Assert.NotNull(result); + Assert.Equal(1, result!.Count); + } + + record Result { + public int Count { get; set; } + } + + public class CustomFixture : KurrentTemporaryFixture { + public CustomFixture() : base(x => x.RunProjections()) { } + } +} diff --git a/test/Kurrent.Client.Tests/Projections/GetProjectionStatusTests.cs b/test/Kurrent.Client.Tests/Projections/GetProjectionStatusTests.cs new file mode 100644 index 000000000..27d978eb0 --- /dev/null +++ b/test/Kurrent.Client.Tests/Projections/GetProjectionStatusTests.cs @@ -0,0 +1,21 @@ +using Kurrent.Client.Tests.TestNode; + +namespace Kurrent.Client.Tests.Projections; + +public class GetProjectionStatusTests(ITestOutputHelper output, GetProjectionStatusTests.CustomFixture fixture) + : KurrentTemporaryTests(output, fixture) { + [Fact] + public async Task get_status() { + var name = Names.First(); + var result = await Fixture.Projections.GetStatusAsync(name, userCredentials: TestCredentials.Root); + + Assert.NotNull(result); + Assert.Equal(name, result.Name); + } + + static readonly string[] Names = ["$streams", "$stream_by_category", "$by_category", "$by_event_type", "$by_correlation_id"]; + + public class CustomFixture : KurrentTemporaryFixture { + public CustomFixture() : base(x => x.RunProjections()) { } + } +} diff --git a/test/Kurrent.Client.Tests/Projections/ProjectionManagementTests.cs b/test/Kurrent.Client.Tests/Projections/ProjectionManagementTests.cs index 83b1a35cc..59c3f6b7a 100644 --- a/test/Kurrent.Client.Tests/Projections/ProjectionManagementTests.cs +++ b/test/Kurrent.Client.Tests/Projections/ProjectionManagementTests.cs @@ -1,7 +1,6 @@ // ReSharper disable InconsistentNaming // ReSharper disable ClassNeverInstantiated.Local -using EventStore.Client; using Kurrent.Client.Tests.TestNode; namespace Kurrent.Client.Tests; @@ -46,138 +45,8 @@ await Fixture.Projections.CreateTransientAsync( ); } - [Fact] - public async Task disable_projection() { - var name = Names.First(); - await Fixture.Projections.DisableAsync(name, userCredentials: TestCredentials.Root); - var result = await Fixture.Projections.GetStatusAsync(name, userCredentials: TestCredentials.Root); - Assert.NotNull(result); - Assert.Contains(["Aborted/Stopped", "Stopped"], x => x == result!.Status); - } - - [Fact] - public async Task enable_projection() { - var name = Names.First(); - await Fixture.Projections.EnableAsync(name, userCredentials: TestCredentials.Root); - var result = await Fixture.Projections.GetStatusAsync(name, userCredentials: TestCredentials.Root); - Assert.NotNull(result); - Assert.Equal("Running", result.Status); - } - - [Fact] - public async Task get_result() { - var name = Fixture.GetProjectionName(); - Result? result = null; - - var projection = $$""" - fromStream('{{name}}').when({ - "$init": function() { return { Count: 0 }; }, - "$any": function(s, e) { s.Count++; return s; } - }); - """; - - await Fixture.Projections.CreateContinuousAsync( - name, - projection, - userCredentials: TestCredentials.Root - ); - - await Fixture.Streams.AppendToStreamAsync( - name, - StreamState.NoStream, - Fixture.CreateTestEvents() - ); - - await AssertEx.IsOrBecomesTrue( - async () => { - result = await Fixture.Projections.GetResultAsync(name, userCredentials: TestCredentials.Root); - return result.Count > 0; - } - ); - - Assert.NotNull(result); - Assert.Equal(1, result!.Count); - } - - [Fact] - public async Task get_state() { - var name = Fixture.GetProjectionName(); - - var projection = $$""" - fromStream('{{name}}').when({ - "$init": function() { return { Count: 0 }; }, - "$any": function(s, e) { s.Count++; return s; } - }); - """; - - Result? result = null; - - await Fixture.Projections.CreateContinuousAsync( - name, - projection, - userCredentials: TestCredentials.Root - ); - - await Fixture.Streams.AppendToStreamAsync( - name, - StreamState.NoStream, - Fixture.CreateTestEvents() - ); - - await AssertEx.IsOrBecomesTrue( - async () => { - result = await Fixture.Projections.GetStateAsync(name, userCredentials: TestCredentials.Root); - return result.Count > 0; - } - ); - - Assert.NotNull(result); - Assert.Equal(1, result!.Count); - } - - [Fact] - public async Task get_status() { - var name = Names.First(); - var result = await Fixture.Projections.GetStatusAsync(name, userCredentials: TestCredentials.Root); - - Assert.NotNull(result); - Assert.Equal(name, result.Name); - } - - [Fact] - public async Task restart_subsystem_does_not_throw() => - await Fixture.Projections.RestartSubsystemAsync(userCredentials: TestCredentials.Root); - - [Fact] - public async Task restart_subsystem_throws_when_given_no_credentials() => - await Assert.ThrowsAsync(() => Fixture.Projections.RestartSubsystemAsync(userCredentials: TestCredentials.TestUser1)); - - [Theory] - [InlineData(true)] - [InlineData(false)] - [InlineData(null)] - public async Task update_projection(bool? emitEnabled) { - var name = Fixture.GetProjectionName(); - await Fixture.Projections.CreateContinuousAsync( - name, - "fromAll().when({$init: function (state, ev) {return {};}});", - userCredentials: TestCredentials.Root - ); - - await Fixture.Projections.UpdateAsync( - name, - "fromAll().when({$init: function (s, e) {return {};}});", - emitEnabled, - userCredentials: TestCredentials.Root - ); - } - static readonly string[] Names = ["$streams", "$stream_by_category", "$by_category", "$by_event_type", "$by_correlation_id"]; - record Result { - public int Count { get; set; } - } - public class CustomFixture : KurrentTemporaryFixture { public CustomFixture() : base(x => x.RunProjections()) { } } diff --git a/test/Kurrent.Client.Tests/Projections/RestartSubsystemTests.cs b/test/Kurrent.Client.Tests/Projections/RestartSubsystemTests.cs new file mode 100644 index 000000000..bd5584a50 --- /dev/null +++ b/test/Kurrent.Client.Tests/Projections/RestartSubsystemTests.cs @@ -0,0 +1,19 @@ +using EventStore.Client; +using Kurrent.Client.Tests.TestNode; + +namespace Kurrent.Client.Tests.Projections; + +public class RestartSubsystemTests(ITestOutputHelper output, RestartSubsystemTests.CustomFixture fixture) + : KurrentTemporaryTests(output, fixture) { + [Fact] + public async Task restart_subsystem_does_not_throw() => + await Fixture.Projections.RestartSubsystemAsync(userCredentials: TestCredentials.Root); + + [Fact] + public async Task restart_subsystem_throws_when_given_no_credentials() => + await Assert.ThrowsAsync(() => Fixture.Projections.RestartSubsystemAsync(userCredentials: TestCredentials.TestUser1)); + + public class CustomFixture : KurrentTemporaryFixture { + public CustomFixture() : base(x => x.RunProjections()) { } + } +} diff --git a/test/Kurrent.Client.Tests/Projections/UpdateProjectionTests.cs b/test/Kurrent.Client.Tests/Projections/UpdateProjectionTests.cs new file mode 100644 index 000000000..a64eb6319 --- /dev/null +++ b/test/Kurrent.Client.Tests/Projections/UpdateProjectionTests.cs @@ -0,0 +1,30 @@ +using Kurrent.Client.Tests.TestNode; + +namespace Kurrent.Client.Tests.Projections; + +public class UpdateProjectionTests(ITestOutputHelper output, UpdateProjectionTests.CustomFixture fixture) + : KurrentTemporaryTests(output, fixture) { + [Theory] + [InlineData(true)] + [InlineData(false)] + [InlineData(null)] + public async Task update_projection(bool? emitEnabled) { + var name = Fixture.GetProjectionName(); + await Fixture.Projections.CreateContinuousAsync( + name, + "fromAll().when({$init: function (state, ev) {return {};}});", + userCredentials: TestCredentials.Root + ); + + await Fixture.Projections.UpdateAsync( + name, + "fromAll().when({$init: function (s, e) {return {};}});", + emitEnabled, + userCredentials: TestCredentials.Root + ); + } + + public class CustomFixture : KurrentTemporaryFixture { + public CustomFixture() : base(x => x.RunProjections()) { } + } +} From 4c238c774cbafc83961f9d3fa40f821c65c9fed6 Mon Sep 17 00:00:00 2001 From: William Chong Date: Wed, 8 Jan 2025 10:06:42 +0400 Subject: [PATCH 09/15] Improve CI --- .github/workflows/base.yml | 74 ++++++++++--------- .github/workflows/ci.yml | 33 +++++---- .github/workflows/dispatch-ce.yml | 8 +- .github/workflows/dispatch-ee.yml | 58 +++++++-------- .../ClientCertificatesTests.cs | 1 + .../ConnectionStringTests.cs | 1 + test/Kurrent.Client.Tests/FromAllTests.cs | 1 + test/Kurrent.Client.Tests/FromStreamTests.cs | 1 + .../GossipChannelSelectorTests.cs | 1 + .../KurrentClientOperationsTests.cs | 1 + .../NodePreferenceComparerTests.cs | 1 + .../Kurrent.Client.Tests/NodeSelectorTests.cs | 1 + .../Operations/AuthenticationTests.cs | 1 + .../Operations/MergeIndexTests.cs | 1 + .../Operations/ResignNodeTests.cs | 1 + .../RestartPersistentSubscriptionsTests.cs | 1 + .../Operations/ScavengeTests.cs | 1 + .../ShutdownNodeAuthenticationTests.cs | 1 + .../Operations/ShutdownNodeTests.cs | 1 + ...ToAllConnectWithoutReadPermissionsTests.cs | 1 + .../SubscribeToAllFilterTests.cs | 1 + .../SubscribeToAllGetInfoTests.cs | 1 + ...eToAllListWithIncorrectCredentialsTests.cs | 1 + ...SubscribeToAllNoDefaultCredentialsTests.cs | 1 + .../SubscribeToAllReplayParkedTests.cs | 1 + ...AllResultWithNormalUserCredentialsTests.cs | 1 + .../SubscribeToAllReturnsAllSubscriptions.cs | 1 + ...AllReturnsSubscriptionsToAllStreamTests.cs | 1 + .../SubscribeToAllTests.cs | 1 + ...beToAllUpdateExistingWithCheckpointTest.cs | 1 + .../SubscribeToAllWithoutPSTests.cs | 1 + .../SubscribeToStreamGetInfoTests.cs | 1 + .../SubscribeToStreamListTests.cs | 1 + ...scribeToStreamNoDefaultCredentialsTests.cs | 1 + .../SubscribeToStreamReplayParkedTests.cs | 1 + .../SubscribeToStreamTests.cs | 1 + test/Kurrent.Client.Tests/PositionTests.cs | 1 + .../Projections/DisableProjectionTests.cs | 1 + .../Projections/EnableProjectionTests.cs | 1 + .../Projections/GetProjectionResultTests.cs | 1 + .../Projections/GetProjectionStateTests.cs | 1 + .../Projections/GetProjectionStatusTests.cs | 1 + .../Projections/ListAllProjectionsTests.cs | 1 + .../ListContinuousProjectionsTests.cs | 1 + .../ListOneTimeProjectionsTests.cs | 1 + .../Projections/ProjectionManagementTests.cs | 1 + .../Projections/ResetProjectionTests.cs | 1 + .../Projections/RestartSubsystemTests.cs | 1 + .../Projections/UpdateProjectionTests.cs | 1 + .../RegularFilterExpressionTests.cs | 1 + .../AllStreamWithNoAclSecurityTests.cs | 2 +- .../Security/DeleteStreamSecurityTests.cs | 2 +- .../Security/MultipleRoleSecurityTests.cs | 2 +- ...verridenSystemStreamSecurityForAllTests.cs | 2 +- .../OverridenSystemStreamSecurityTests.cs | 2 +- .../OverridenUserStreamSecurityTests.cs | 2 +- .../Security/ReadAllSecurityTests.cs | 2 +- .../Security/ReadStreamMetaSecurityTests.cs | 2 +- .../Security/ReadStreamSecurityTests.cs | 2 +- .../StreamSecurityInheritanceTests.cs | 2 +- .../Security/SubscribeToAllSecurityTests.cs | 2 +- .../SubscribeToStreamSecurityTests.cs | 2 +- .../Security/SystemStreamSecurityTests.cs | 2 +- .../Security/WriteStreamMetaSecurityTests.cs | 2 +- .../Security/WriteStreamSecurityTests.cs | 2 +- .../SharingProviderTests.cs | 1 + .../StreamPositionTests.cs | 1 + .../StreamRevisionTests.cs | 1 + test/Kurrent.Client.Tests/StreamStateTests.cs | 1 + .../Streams/AppendTests.cs | 2 +- .../Streams/Bugs/Obsolete/Issue104.cs | 1 + .../Streams/Bugs/Obsolete/Issue2544.cs | 1 + .../Streams/DeleteTests.cs | 2 +- .../Read/ReadAllEventsBackwardTests.cs | 1 + .../Streams/Read/ReadAllEventsFixture.cs | 1 + .../Streams/Read/ReadAllEventsForwardTests.cs | 1 + .../Streams/Read/ReadStreamBackwardTests.cs | 3 +- ...dStreamEventsLinkedToDeletedStreamTests.cs | 3 +- .../Streams/Read/ReadStreamForwardTests.cs | 3 +- ...reamWhenHavingMaxCountSetForStreamTests.cs | 3 +- .../Streams/SoftDeleteTests.cs | 2 +- .../Streams/StreamMetadataTests.cs | 2 +- .../Streams/SubscribeToStreamTests.cs | 2 +- test/Kurrent.Client.Tests/UuidTests.cs | 1 + test/Kurrent.Client.Tests/ValueObjectTests.cs | 1 + .../X509CertificatesTests.cs | 1 + 86 files changed, 175 insertions(+), 108 deletions(-) diff --git a/.github/workflows/base.yml b/.github/workflows/base.yml index 27a82d347..c4efd3227 100644 --- a/.github/workflows/base.yml +++ b/.github/workflows/base.yml @@ -15,50 +15,54 @@ on: required: false type: string default: eventstore-ce/eventstoredb-ce + test: + required: true + type: string jobs: test: timeout-minutes: 20 - strategy: + strategy: fail-fast: false matrix: - framework: [ net8.0, net9.0 ] + framework: [ net9.0 ] os: [ ubuntu-latest ] configuration: [ release ] runs-on: ${{ matrix.os }} - name: (${{ matrix.os }}, ${{ matrix.framework }}) + name: ${{ inputs.test }} (${{ matrix.os }}, ${{ matrix.framework }}) env: CLOUDSMITH_CICD_USER: ${{ secrets.CLOUDSMITH_CICD_USER }} steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - name: Login to Cloudsmith - if: ${{ env.CLOUDSMITH_CICD_USER != '' }} - uses: docker/login-action@v3 - with: - registry: docker.eventstore.com - username: ${{ secrets.CLOUDSMITH_CICD_USER }} - password: ${{ secrets.CLOUDSMITH_CICD_TOKEN }} - - name: Pull EventStore Image - shell: bash - run: | - docker pull docker.eventstore.com/${{ inputs.docker-image }}:${{ inputs.docker-tag }} - - name: Install dotnet SDKs - uses: actions/setup-dotnet@v3 - with: - dotnet-version: | - 8.0.x - 9.0.x - - name: Run Tests - shell: bash - env: - ES_DOCKER_TAG: ${{ inputs.docker-tag }} - ES_DOCKER_REGISTRY: docker.eventstore.com/${{ inputs.docker-image }} - run: | - sudo ./gencert.sh - dotnet test --configuration ${{ matrix.configuration }} --blame \ - --logger:"GitHubActions;report-warnings=false" --logger:"console;verbosity=normal" \ - --framework ${{ matrix.framework }} \ - test/Kurrent.Client.Tests + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Login to Cloudsmith + if: ${{ env.CLOUDSMITH_CICD_USER != '' }} + uses: docker/login-action@v3 + with: + registry: docker.eventstore.com + username: ${{ secrets.CLOUDSMITH_CICD_USER }} + password: ${{ secrets.CLOUDSMITH_CICD_TOKEN }} + - name: Pull EventStore Image + shell: bash + run: | + docker pull docker.eventstore.com/${{ inputs.docker-image }}:${{ inputs.docker-tag }} + - name: Install dotnet SDKs + uses: actions/setup-dotnet@v3 + with: + dotnet-version: | + 8.0.x + 9.0.x + - name: Run Tests + shell: bash + env: + ES_DOCKER_TAG: ${{ inputs.docker-tag }} + ES_DOCKER_REGISTRY: docker.eventstore.com/${{ inputs.docker-image }} + run: | + sudo ./gencert.sh + dotnet test --configuration ${{ matrix.configuration }} --blame \ + --logger:"GitHubActions;report-warnings=false" --logger:"console;verbosity=normal" \ + --framework ${{ matrix.framework }} \ + --filter "Category=Target:${{ inputs.test }}" \ + test/Kurrent.Client.Tests diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5b659d1fe..b2daa69ce 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,23 +15,24 @@ jobs: fail-fast: false matrix: docker-tag: [ ci, lts, previous-lts ] + test: [ Streams, PersistentSubscriptions, Operations, Projections, Security, Misc ] name: Test CE (${{ matrix.docker-tag }}) with: docker-tag: ${{ matrix.docker-tag }} docker-image: eventstore-ce/eventstoredb-ce -# ee: -# uses: ./.github/workflows/base.yml -# if: ${{ github.repository_owner == 'EventStore' }} -# strategy: -# fail-fast: false -# matrix: -# docker-tag: [ 24.2.0-jammy ] -# test: [ Plugins ] -# name: Test EE (${{ matrix.docker-tag }}) -# with: -# docker-tag: ${{ matrix.docker-tag }} -# docker-image: eventstore-ee/eventstoredb-commercial -# test: ${{ matrix.test }} -# secrets: -# CLOUDSMITH_CICD_USER: ${{ secrets.CLOUDSMITH_CICD_USER }} -# CLOUDSMITH_CICD_TOKEN: ${{ secrets.CLOUDSMITH_CICD_TOKEN }} + ee: + uses: ./.github/workflows/base.yml + if: ${{ github.repository_owner == 'EventStore' }} + strategy: + fail-fast: false + matrix: + docker-tag: [ 24.2.0-jammy ] + test: [ Plugins ] + name: Test EE (${{ matrix.docker-tag }}) + with: + docker-tag: ${{ matrix.docker-tag }} + docker-image: eventstore-ee/eventstoredb-commercial + test: ${{ matrix.test }} + secrets: + CLOUDSMITH_CICD_USER: ${{ secrets.CLOUDSMITH_CICD_USER }} + CLOUDSMITH_CICD_TOKEN: ${{ secrets.CLOUDSMITH_CICD_TOKEN }} diff --git a/.github/workflows/dispatch-ce.yml b/.github/workflows/dispatch-ce.yml index 31e2d8966..2611dfbd2 100644 --- a/.github/workflows/dispatch-ce.yml +++ b/.github/workflows/dispatch-ce.yml @@ -15,10 +15,10 @@ on: jobs: test: uses: ./.github/workflows/base.yml -# strategy: -# fail-fast: false -# matrix: -# test: [ Streams, PersistentSubscriptions, Operations, UserManagement, ProjectionManagement ] + strategy: + fail-fast: false + matrix: + test: [ Streams, PersistentSubscriptions, Operations, Projections, Security, Misc ] name: Test CE (${{ inputs.docker-tag }}) with: docker-tag: ${{ inputs.docker-tag }} diff --git a/.github/workflows/dispatch-ee.yml b/.github/workflows/dispatch-ee.yml index 75bde07de..13f0b3398 100644 --- a/.github/workflows/dispatch-ee.yml +++ b/.github/workflows/dispatch-ee.yml @@ -1,29 +1,29 @@ -#name: Dispatch EE -# -#on: -# workflow_dispatch: -# inputs: -# docker-tag: -# description: "Docker tag" -# required: true -# type: string -# docker-image: -# description: "Docker image" -# required: true -# type: string -# -#jobs: -# test: -# uses: ./.github/workflows/base.yml -## strategy: -## fail-fast: false -## matrix: -## test: [ Plugins ] -# name: Test EE (${{ inputs.docker-tag }}) -# with: -# docker-tag: ${{ inputs.docker-tag }} -# docker-image: ${{ inputs.docker-image }} -# test: ${{ matrix.test }} -# secrets: -# CLOUDSMITH_CICD_USER: ${{ secrets.CLOUDSMITH_CICD_USER }} -# CLOUDSMITH_CICD_TOKEN: ${{ secrets.CLOUDSMITH_CICD_TOKEN }} +name: Dispatch EE + +on: + workflow_dispatch: + inputs: + docker-tag: + description: "Docker tag" + required: true + type: string + docker-image: + description: "Docker image" + required: true + type: string + +jobs: + test: + uses: ./.github/workflows/base.yml + strategy: + fail-fast: false + matrix: + test: [ Plugins ] + name: Test EE (${{ inputs.docker-tag }}) + with: + docker-tag: ${{ inputs.docker-tag }} + docker-image: ${{ inputs.docker-image }} + test: ${{ matrix.test }} + secrets: + CLOUDSMITH_CICD_USER: ${{ secrets.CLOUDSMITH_CICD_USER }} + CLOUDSMITH_CICD_TOKEN: ${{ secrets.CLOUDSMITH_CICD_TOKEN }} diff --git a/test/Kurrent.Client.Tests/ClientCertificatesTests.cs b/test/Kurrent.Client.Tests/ClientCertificatesTests.cs index 61ec9dcd4..2903d8fb5 100644 --- a/test/Kurrent.Client.Tests/ClientCertificatesTests.cs +++ b/test/Kurrent.Client.Tests/ClientCertificatesTests.cs @@ -3,6 +3,7 @@ namespace Kurrent.Client.Tests; +[Trait("Category", "Target:Misc")] [Trait("Category", "Target:Plugins")] [Trait("Category", "Type:UserCertificate")] public class ClientCertificateTests(ITestOutputHelper output, KurrentPermanentFixture fixture) diff --git a/test/Kurrent.Client.Tests/ConnectionStringTests.cs b/test/Kurrent.Client.Tests/ConnectionStringTests.cs index 2d3d09ab2..7ef507b6c 100644 --- a/test/Kurrent.Client.Tests/ConnectionStringTests.cs +++ b/test/Kurrent.Client.Tests/ConnectionStringTests.cs @@ -8,6 +8,7 @@ namespace Kurrent.Client.Tests; +[Trait("Category", "Target:Misc")] public class ConnectionStringTests { public static IEnumerable ValidCases() { var fixture = new Fixture(); diff --git a/test/Kurrent.Client.Tests/FromAllTests.cs b/test/Kurrent.Client.Tests/FromAllTests.cs index 93adcf189..5be40e9bf 100644 --- a/test/Kurrent.Client.Tests/FromAllTests.cs +++ b/test/Kurrent.Client.Tests/FromAllTests.cs @@ -3,6 +3,7 @@ namespace Kurrent.Client.Tests; +[Trait("Category", "Target:Misc")] public class FromAllTests : ValueObjectTests { public FromAllTests() : base(new ScenarioFixture()) { } diff --git a/test/Kurrent.Client.Tests/FromStreamTests.cs b/test/Kurrent.Client.Tests/FromStreamTests.cs index 2b0df8dae..9593c8bea 100644 --- a/test/Kurrent.Client.Tests/FromStreamTests.cs +++ b/test/Kurrent.Client.Tests/FromStreamTests.cs @@ -3,6 +3,7 @@ namespace Kurrent.Client.Tests; +[Trait("Category", "Target:Misc")] public class FromStreamTests : ValueObjectTests { public FromStreamTests() : base(new ScenarioFixture()) { } diff --git a/test/Kurrent.Client.Tests/GossipChannelSelectorTests.cs b/test/Kurrent.Client.Tests/GossipChannelSelectorTests.cs index 142a7dc04..1e5afe076 100644 --- a/test/Kurrent.Client.Tests/GossipChannelSelectorTests.cs +++ b/test/Kurrent.Client.Tests/GossipChannelSelectorTests.cs @@ -4,6 +4,7 @@ namespace Kurrent.Client.Tests; +[Trait("Category", "Target:Misc")] public class GossipChannelSelectorTests { [RetryFact] public async Task ExplicitlySettingEndPointChangesChannels() { diff --git a/test/Kurrent.Client.Tests/KurrentClientOperationsTests.cs b/test/Kurrent.Client.Tests/KurrentClientOperationsTests.cs index 07f5ffb46..c8b70bdce 100644 --- a/test/Kurrent.Client.Tests/KurrentClientOperationsTests.cs +++ b/test/Kurrent.Client.Tests/KurrentClientOperationsTests.cs @@ -2,6 +2,7 @@ namespace Kurrent.Client.Tests; +[Trait("Category", "Target:Misc")] public class KurrentClientOperationOptionsTests { [RetryFact] public void setting_options_on_clone_should_not_modify_original() { diff --git a/test/Kurrent.Client.Tests/NodePreferenceComparerTests.cs b/test/Kurrent.Client.Tests/NodePreferenceComparerTests.cs index 58300f00f..cf42ded5e 100644 --- a/test/Kurrent.Client.Tests/NodePreferenceComparerTests.cs +++ b/test/Kurrent.Client.Tests/NodePreferenceComparerTests.cs @@ -3,6 +3,7 @@ namespace Kurrent.Client.Tests; +[Trait("Category", "Target:Misc")] public class NodePreferenceComparerTests { static ClusterMessages.VNodeState RunTest(IComparer sut, params ClusterMessages.VNodeState[] states) => states diff --git a/test/Kurrent.Client.Tests/NodeSelectorTests.cs b/test/Kurrent.Client.Tests/NodeSelectorTests.cs index 5f0eb51ba..b2b43ea2b 100644 --- a/test/Kurrent.Client.Tests/NodeSelectorTests.cs +++ b/test/Kurrent.Client.Tests/NodeSelectorTests.cs @@ -3,6 +3,7 @@ namespace Kurrent.Client.Tests; +[Trait("Category", "Target:Misc")] public class NodeSelectorTests { static readonly ClusterMessages.VNodeState[] NotAllowedStates = { ClusterMessages.VNodeState.Manager, diff --git a/test/Kurrent.Client.Tests/Operations/AuthenticationTests.cs b/test/Kurrent.Client.Tests/Operations/AuthenticationTests.cs index 61d31701a..9a1d4dee2 100644 --- a/test/Kurrent.Client.Tests/Operations/AuthenticationTests.cs +++ b/test/Kurrent.Client.Tests/Operations/AuthenticationTests.cs @@ -2,6 +2,7 @@ namespace Kurrent.Client.Tests; +[Trait("Category", "Target:Operations")] public class AuthenticationTests(ITestOutputHelper output, AuthenticationTests.CustomFixture fixture) : KurrentPermanentTests(output, fixture) { public enum CredentialsCase { None, TestUser, RootUser } diff --git a/test/Kurrent.Client.Tests/Operations/MergeIndexTests.cs b/test/Kurrent.Client.Tests/Operations/MergeIndexTests.cs index 629924a45..35112ddee 100644 --- a/test/Kurrent.Client.Tests/Operations/MergeIndexTests.cs +++ b/test/Kurrent.Client.Tests/Operations/MergeIndexTests.cs @@ -2,6 +2,7 @@ namespace Kurrent.Client.Tests; +[Trait("Category", "Target:Operations")] public class MergeIndexTests(ITestOutputHelper output, MergeIndexTests.CustomFixture fixture) : KurrentPermanentTests(output, fixture) { [RetryFact] diff --git a/test/Kurrent.Client.Tests/Operations/ResignNodeTests.cs b/test/Kurrent.Client.Tests/Operations/ResignNodeTests.cs index 4961873ad..82bd88252 100644 --- a/test/Kurrent.Client.Tests/Operations/ResignNodeTests.cs +++ b/test/Kurrent.Client.Tests/Operations/ResignNodeTests.cs @@ -4,6 +4,7 @@ namespace Kurrent.Client.Tests.Operations; +[Trait("Category", "Target:Operations")] public class ResignNodeTests(ITestOutputHelper output, ResignNodeTests.CustomFixture fixture) : KurrentTemporaryTests(output, fixture) { [RetryFact] diff --git a/test/Kurrent.Client.Tests/Operations/RestartPersistentSubscriptionsTests.cs b/test/Kurrent.Client.Tests/Operations/RestartPersistentSubscriptionsTests.cs index a0683c6b4..f414a3529 100644 --- a/test/Kurrent.Client.Tests/Operations/RestartPersistentSubscriptionsTests.cs +++ b/test/Kurrent.Client.Tests/Operations/RestartPersistentSubscriptionsTests.cs @@ -3,6 +3,7 @@ namespace Kurrent.Client.Tests.Operations; +[Trait("Category", "Target:Operations")] public class RestartPersistentSubscriptionsTests(ITestOutputHelper output, RestartPersistentSubscriptionsTests.CustomFixture fixture) : KurrentTemporaryTests(output, fixture) { [RetryFact] diff --git a/test/Kurrent.Client.Tests/Operations/ScavengeTests.cs b/test/Kurrent.Client.Tests/Operations/ScavengeTests.cs index 7dc6912e4..039a3b22b 100644 --- a/test/Kurrent.Client.Tests/Operations/ScavengeTests.cs +++ b/test/Kurrent.Client.Tests/Operations/ScavengeTests.cs @@ -4,6 +4,7 @@ namespace Kurrent.Client.Tests; +[Trait("Category", "Target:Operations")] public class ScavengeTests(ITestOutputHelper output, ScavengeTests.CustomFixture fixture) : KurrentTemporaryTests(output, fixture) { [RetryFact] diff --git a/test/Kurrent.Client.Tests/Operations/ShutdownNodeAuthenticationTests.cs b/test/Kurrent.Client.Tests/Operations/ShutdownNodeAuthenticationTests.cs index a1294f878..31d2a0600 100644 --- a/test/Kurrent.Client.Tests/Operations/ShutdownNodeAuthenticationTests.cs +++ b/test/Kurrent.Client.Tests/Operations/ShutdownNodeAuthenticationTests.cs @@ -4,6 +4,7 @@ namespace Kurrent.Client.Tests; +[Trait("Category", "Target:Operations")] public class ShutdownNodeAuthenticationTests(ITestOutputHelper output, ShutdownNodeAuthenticationTests.CustomFixture fixture) : KurrentTemporaryTests(output, fixture) { [RetryFact] diff --git a/test/Kurrent.Client.Tests/Operations/ShutdownNodeTests.cs b/test/Kurrent.Client.Tests/Operations/ShutdownNodeTests.cs index e668dfe72..4e685c37d 100644 --- a/test/Kurrent.Client.Tests/Operations/ShutdownNodeTests.cs +++ b/test/Kurrent.Client.Tests/Operations/ShutdownNodeTests.cs @@ -3,6 +3,7 @@ namespace Kurrent.Client.Tests.Operations; +[Trait("Category", "Target:Operations")] public class ShutdownNodeTests(ITestOutputHelper output, ShutdownNodeTests.NoDefaultCredentialsFixture fixture) : KurrentTemporaryTests(output, fixture) { [RetryFact] diff --git a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllConnectWithoutReadPermissionsTests.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllConnectWithoutReadPermissionsTests.cs index 7a3a1e0e2..99fa039e5 100644 --- a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllConnectWithoutReadPermissionsTests.cs +++ b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllConnectWithoutReadPermissionsTests.cs @@ -3,6 +3,7 @@ namespace Kurrent.Client.Tests.PersistentSubscriptions; +[Trait("Category", "Target:PersistentSubscriptions")] public class SubscribeToAllConnectWithoutReadPermissionsTests(ITestOutputHelper output, KurrentTemporaryFixture fixture) : KurrentTemporaryTests(output, fixture) { [RetryFact] diff --git a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllFilterTests.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllFilterTests.cs index 52fa03ef1..3021fb696 100644 --- a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllFilterTests.cs +++ b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllFilterTests.cs @@ -4,6 +4,7 @@ namespace Kurrent.Client.Tests.PersistentSubscriptions; +[Trait("Category", "Target:PersistentSubscriptions")] public class SubscribeToAllFilterTests(ITestOutputHelper output, KurrentTemporaryFixture fixture) : KurrentTemporaryTests(output, fixture) { [RetryTheory] diff --git a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllGetInfoTests.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllGetInfoTests.cs index 0dddd0b8b..3b4beb81e 100644 --- a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllGetInfoTests.cs +++ b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllGetInfoTests.cs @@ -5,6 +5,7 @@ namespace Kurrent.Client.Tests.PersistentSubscriptions; +[Trait("Category", "Target:PersistentSubscriptions")] public class SubscribeToAllGetInfoTests(SubscribeToAllGetInfoTests.CustomFixture fixture) : IClassFixture { static readonly PersistentSubscriptionSettings Settings = new( diff --git a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllListWithIncorrectCredentialsTests.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllListWithIncorrectCredentialsTests.cs index 47fd98fa9..aa3957ed0 100644 --- a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllListWithIncorrectCredentialsTests.cs +++ b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllListWithIncorrectCredentialsTests.cs @@ -3,6 +3,7 @@ namespace Kurrent.Client.Tests.PersistentSubscriptions; +[Trait("Category", "Target:PersistentSubscriptions")] public class SubscribeToAllListWithIncorrectCredentialsTests(ITestOutputHelper output, SubscribeToAllListWithIncorrectCredentialsTests.CustomFixture fixture) : KurrentTemporaryTests(output, fixture) { [RetryFact] diff --git a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllNoDefaultCredentialsTests.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllNoDefaultCredentialsTests.cs index 8f2ee60cd..49e8cfa30 100644 --- a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllNoDefaultCredentialsTests.cs +++ b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllNoDefaultCredentialsTests.cs @@ -2,6 +2,7 @@ namespace Kurrent.Client.Tests.PersistentSubscriptions; +[Trait("Category", "Target:PersistentSubscriptions")] public class SubscribeToAllNoDefaultCredentialsTests(ITestOutputHelper output, SubscribeToAllNoDefaultCredentialsTests.CustomFixture fixture) : KurrentPermanentTests(output, fixture) { [RetryFact] diff --git a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllReplayParkedTests.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllReplayParkedTests.cs index 802501317..f2eacb3e7 100644 --- a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllReplayParkedTests.cs +++ b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllReplayParkedTests.cs @@ -2,6 +2,7 @@ namespace Kurrent.Client.Tests.PersistentSubscriptions; +[Trait("Category", "Target:PersistentSubscriptions")] public class SubscribeToAllReplayParkedTests(ITestOutputHelper output, SubscribeToAllReplayParkedTests.CustomFixture fixture) : KurrentPermanentTests(output, fixture) { [RetryFact] diff --git a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllResultWithNormalUserCredentialsTests.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllResultWithNormalUserCredentialsTests.cs index 10a52d40b..627a3bc19 100644 --- a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllResultWithNormalUserCredentialsTests.cs +++ b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllResultWithNormalUserCredentialsTests.cs @@ -3,6 +3,7 @@ namespace Kurrent.Client.Tests.PersistentSubscriptions; +[Trait("Category", "Target:PersistentSubscriptions")] public class SubscribeToAllResultWithNormalUserCredentialsTests(ITestOutputHelper output, KurrentTemporaryFixture fixture) : KurrentTemporaryTests(output, fixture) { [RetryFact] diff --git a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllReturnsAllSubscriptions.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllReturnsAllSubscriptions.cs index 2fcc75d6f..148607b83 100644 --- a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllReturnsAllSubscriptions.cs +++ b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllReturnsAllSubscriptions.cs @@ -3,6 +3,7 @@ namespace Kurrent.Client.Tests.PersistentSubscriptions; +[Trait("Category", "Target:PersistentSubscriptions")] public class SubscribeToAllReturnsAllSubscriptions(ITestOutputHelper output, SubscribeToAllReturnsAllSubscriptions.CustomFixture fixture) : KurrentTemporaryTests(output, fixture) { [RetryFact] diff --git a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllReturnsSubscriptionsToAllStreamTests.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllReturnsSubscriptionsToAllStreamTests.cs index 4ab3bd508..07498fc28 100644 --- a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllReturnsSubscriptionsToAllStreamTests.cs +++ b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllReturnsSubscriptionsToAllStreamTests.cs @@ -3,6 +3,7 @@ namespace Kurrent.Client.Tests.PersistentSubscriptions; +[Trait("Category", "Target:PersistentSubscriptions")] public class SubscribeToAllReturnsSubscriptionsToAllStreamTests(ITestOutputHelper output, KurrentTemporaryFixture fixture) : KurrentTemporaryTests(output, fixture) { [RetryFact] diff --git a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllTests.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllTests.cs index f8fc927b2..b699cec08 100644 --- a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllTests.cs +++ b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllTests.cs @@ -4,6 +4,7 @@ namespace Kurrent.Client.Tests.PersistentSubscriptions; +[Trait("Category", "Target:PersistentSubscriptions")] public class SubscribeToAllTests(ITestOutputHelper output, KurrentPermanentFixture fixture) : KurrentPermanentTests(output, fixture) { [RetryFact] diff --git a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllUpdateExistingWithCheckpointTest.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllUpdateExistingWithCheckpointTest.cs index 54e9f64ff..0a0ae75f4 100644 --- a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllUpdateExistingWithCheckpointTest.cs +++ b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllUpdateExistingWithCheckpointTest.cs @@ -4,6 +4,7 @@ namespace Kurrent.Client.Tests.PersistentSubscriptions; +[Trait("Category", "Target:PersistentSubscriptions")] public class SubscribeToAllUpdateExistingWithCheckpointTest(ITestOutputHelper output, KurrentTemporaryFixture fixture) : KurrentTemporaryTests(output, fixture) { [RetryFact] diff --git a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllWithoutPSTests.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllWithoutPSTests.cs index 059c28606..237ca1f2b 100644 --- a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllWithoutPSTests.cs +++ b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllWithoutPSTests.cs @@ -4,6 +4,7 @@ namespace Kurrent.Client.Tests.PersistentSubscriptions; +[Trait("Category", "Target:PersistentSubscriptions")] public class SubscribeToAllWithoutPsTests(ITestOutputHelper output, KurrentTemporaryFixture fixture) : KurrentTemporaryTests(output, fixture) { [RetryFact] diff --git a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToStreamGetInfoTests.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToStreamGetInfoTests.cs index 4614c691d..52efa2647 100644 --- a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToStreamGetInfoTests.cs +++ b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToStreamGetInfoTests.cs @@ -5,6 +5,7 @@ namespace Kurrent.Client.Tests.PersistentSubscriptions; +[Trait("Category", "Target:PersistentSubscriptions")] public class SubscribeToStreamGetInfoTests(SubscribeToStreamGetInfoTests.NoDefaultCredentialsFixture fixture) : IClassFixture { static readonly PersistentSubscriptionSettings Settings = new( diff --git a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToStreamListTests.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToStreamListTests.cs index dd93d9e2d..e27f4eee2 100644 --- a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToStreamListTests.cs +++ b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToStreamListTests.cs @@ -3,6 +3,7 @@ namespace Kurrent.Client.Tests.PersistentSubscriptions; +[Trait("Category", "Target:PersistentSubscriptions")] public class SubscribeToStreamListTests(ITestOutputHelper output, SubscribeToStreamListTests.CustomFixture fixture) : KurrentTemporaryTests(output, fixture) { [RetryFact] diff --git a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToStreamNoDefaultCredentialsTests.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToStreamNoDefaultCredentialsTests.cs index 858140147..b1889475c 100644 --- a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToStreamNoDefaultCredentialsTests.cs +++ b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToStreamNoDefaultCredentialsTests.cs @@ -2,6 +2,7 @@ namespace Kurrent.Client.Tests.PersistentSubscriptions; +[Trait("Category", "Target:PersistentSubscriptions")] public class SubscribeToStreamNoDefaultCredentialsTests(ITestOutputHelper output, SubscribeToStreamNoDefaultCredentialsTests.CustomFixture fixture) : KurrentPermanentTests(output, fixture) { [RetryFact] diff --git a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToStreamReplayParkedTests.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToStreamReplayParkedTests.cs index 8aab4c46c..fffe7b154 100644 --- a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToStreamReplayParkedTests.cs +++ b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToStreamReplayParkedTests.cs @@ -2,6 +2,7 @@ namespace Kurrent.Client.Tests.PersistentSubscriptions; +[Trait("Category", "Target:PersistentSubscriptions")] public class SubscribeToStreamReplayParkedTests(ITestOutputHelper output, SubscribeToStreamReplayParkedTests.CustomFixture fixture) : KurrentPermanentTests(output, fixture) { [RetryFact] diff --git a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToStreamTests.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToStreamTests.cs index d5ddbc82a..08533d1c3 100644 --- a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToStreamTests.cs +++ b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToStreamTests.cs @@ -4,6 +4,7 @@ namespace Kurrent.Client.Tests.PersistentSubscriptions; +[Trait("Category", "Target:PersistentSubscriptions")] public class SubscribeToStreamTests(ITestOutputHelper output, KurrentPermanentFixture fixture) : KurrentPermanentTests(output, fixture) { [RetryFact] diff --git a/test/Kurrent.Client.Tests/PositionTests.cs b/test/Kurrent.Client.Tests/PositionTests.cs index 92a478008..fc4401267 100644 --- a/test/Kurrent.Client.Tests/PositionTests.cs +++ b/test/Kurrent.Client.Tests/PositionTests.cs @@ -3,6 +3,7 @@ namespace Kurrent.Client.Tests; +[Trait("Category", "Target:Misc")] public class PositionTests : ValueObjectTests { public PositionTests() : base(new ScenarioFixture()) { } diff --git a/test/Kurrent.Client.Tests/Projections/DisableProjectionTests.cs b/test/Kurrent.Client.Tests/Projections/DisableProjectionTests.cs index debe5d3db..8db2eb293 100644 --- a/test/Kurrent.Client.Tests/Projections/DisableProjectionTests.cs +++ b/test/Kurrent.Client.Tests/Projections/DisableProjectionTests.cs @@ -2,6 +2,7 @@ namespace Kurrent.Client.Tests.Projections; +[Trait("Category", "Target:Projections")] public class DisableProjectionTests(ITestOutputHelper output, DisableProjectionTests.CustomFixture fixture) : KurrentTemporaryTests(output, fixture) { [Fact] diff --git a/test/Kurrent.Client.Tests/Projections/EnableProjectionTests.cs b/test/Kurrent.Client.Tests/Projections/EnableProjectionTests.cs index 26e49b972..a321305b2 100644 --- a/test/Kurrent.Client.Tests/Projections/EnableProjectionTests.cs +++ b/test/Kurrent.Client.Tests/Projections/EnableProjectionTests.cs @@ -2,6 +2,7 @@ namespace Kurrent.Client.Tests.Projections; +[Trait("Category", "Target:Projections")] public class EnableProjectionTests(ITestOutputHelper output, EnableProjectionTests.CustomFixture fixture) : KurrentTemporaryTests(output, fixture) { [Fact] diff --git a/test/Kurrent.Client.Tests/Projections/GetProjectionResultTests.cs b/test/Kurrent.Client.Tests/Projections/GetProjectionResultTests.cs index bf6ee80d9..c4bf0441c 100644 --- a/test/Kurrent.Client.Tests/Projections/GetProjectionResultTests.cs +++ b/test/Kurrent.Client.Tests/Projections/GetProjectionResultTests.cs @@ -3,6 +3,7 @@ namespace Kurrent.Client.Tests.Projections; +[Trait("Category", "Target:Projections")] public class GetProjectionResultTests(ITestOutputHelper output, GetProjectionResultTests.CustomFixture fixture) : KurrentTemporaryTests(output, fixture) { [Fact] diff --git a/test/Kurrent.Client.Tests/Projections/GetProjectionStateTests.cs b/test/Kurrent.Client.Tests/Projections/GetProjectionStateTests.cs index a762d97fd..56476f7e3 100644 --- a/test/Kurrent.Client.Tests/Projections/GetProjectionStateTests.cs +++ b/test/Kurrent.Client.Tests/Projections/GetProjectionStateTests.cs @@ -3,6 +3,7 @@ namespace Kurrent.Client.Tests.Projections; +[Trait("Category", "Target:Projections")] public class GetProjectionStateTests(ITestOutputHelper output, GetProjectionStateTests.CustomFixture fixture) : KurrentTemporaryTests(output, fixture) { [Fact] diff --git a/test/Kurrent.Client.Tests/Projections/GetProjectionStatusTests.cs b/test/Kurrent.Client.Tests/Projections/GetProjectionStatusTests.cs index 27d978eb0..8fea4bce2 100644 --- a/test/Kurrent.Client.Tests/Projections/GetProjectionStatusTests.cs +++ b/test/Kurrent.Client.Tests/Projections/GetProjectionStatusTests.cs @@ -2,6 +2,7 @@ namespace Kurrent.Client.Tests.Projections; +[Trait("Category", "Target:Projections")] public class GetProjectionStatusTests(ITestOutputHelper output, GetProjectionStatusTests.CustomFixture fixture) : KurrentTemporaryTests(output, fixture) { [Fact] diff --git a/test/Kurrent.Client.Tests/Projections/ListAllProjectionsTests.cs b/test/Kurrent.Client.Tests/Projections/ListAllProjectionsTests.cs index 127552eab..10ff83c67 100644 --- a/test/Kurrent.Client.Tests/Projections/ListAllProjectionsTests.cs +++ b/test/Kurrent.Client.Tests/Projections/ListAllProjectionsTests.cs @@ -4,6 +4,7 @@ namespace Kurrent.Client.Tests; +[Trait("Category", "Target:Projections")] public class ListAllProjectionsTests(ITestOutputHelper output, ListAllProjectionsTests.CustomFixture fixture) : KurrentTemporaryTests(output, fixture) { [Fact] diff --git a/test/Kurrent.Client.Tests/Projections/ListContinuousProjectionsTests.cs b/test/Kurrent.Client.Tests/Projections/ListContinuousProjectionsTests.cs index 1f3bbd76c..f6b0fbf81 100644 --- a/test/Kurrent.Client.Tests/Projections/ListContinuousProjectionsTests.cs +++ b/test/Kurrent.Client.Tests/Projections/ListContinuousProjectionsTests.cs @@ -4,6 +4,7 @@ namespace Kurrent.Client.Tests; +[Trait("Category", "Target:Projections")] public class ListContinuousProjectionsTests(ITestOutputHelper output, ListContinuousProjectionsTests.CustomFixture fixture) : KurrentTemporaryTests(output, fixture) { [Fact] diff --git a/test/Kurrent.Client.Tests/Projections/ListOneTimeProjectionsTests.cs b/test/Kurrent.Client.Tests/Projections/ListOneTimeProjectionsTests.cs index b2b0d3899..fb1e537b9 100644 --- a/test/Kurrent.Client.Tests/Projections/ListOneTimeProjectionsTests.cs +++ b/test/Kurrent.Client.Tests/Projections/ListOneTimeProjectionsTests.cs @@ -2,6 +2,7 @@ namespace Kurrent.Client.Tests.Projections; +[Trait("Category", "Target:Projections")] public class ListOneTimeProjectionsTests(ITestOutputHelper output, ListOneTimeProjectionsTests.CustomFixture fixture) : KurrentTemporaryTests(output, fixture) { [Fact] diff --git a/test/Kurrent.Client.Tests/Projections/ProjectionManagementTests.cs b/test/Kurrent.Client.Tests/Projections/ProjectionManagementTests.cs index 59c3f6b7a..fe45326e6 100644 --- a/test/Kurrent.Client.Tests/Projections/ProjectionManagementTests.cs +++ b/test/Kurrent.Client.Tests/Projections/ProjectionManagementTests.cs @@ -5,6 +5,7 @@ namespace Kurrent.Client.Tests; +[Trait("Category", "Target:Projections")] public class ProjectionManagementTests(ITestOutputHelper output, ProjectionManagementTests.CustomFixture fixture) : KurrentTemporaryTests(output, fixture) { [Fact] diff --git a/test/Kurrent.Client.Tests/Projections/ResetProjectionTests.cs b/test/Kurrent.Client.Tests/Projections/ResetProjectionTests.cs index 44e596667..4c8ef7c77 100644 --- a/test/Kurrent.Client.Tests/Projections/ResetProjectionTests.cs +++ b/test/Kurrent.Client.Tests/Projections/ResetProjectionTests.cs @@ -2,6 +2,7 @@ namespace Kurrent.Client.Tests.Projections; +[Trait("Category", "Target:Projections")] public class ResetProjectionTests(ITestOutputHelper output, ResetProjectionTests.CustomFixture fixture) : KurrentTemporaryTests(output, fixture) { [Fact] diff --git a/test/Kurrent.Client.Tests/Projections/RestartSubsystemTests.cs b/test/Kurrent.Client.Tests/Projections/RestartSubsystemTests.cs index bd5584a50..066d28d61 100644 --- a/test/Kurrent.Client.Tests/Projections/RestartSubsystemTests.cs +++ b/test/Kurrent.Client.Tests/Projections/RestartSubsystemTests.cs @@ -3,6 +3,7 @@ namespace Kurrent.Client.Tests.Projections; +[Trait("Category", "Target:Projections")] public class RestartSubsystemTests(ITestOutputHelper output, RestartSubsystemTests.CustomFixture fixture) : KurrentTemporaryTests(output, fixture) { [Fact] diff --git a/test/Kurrent.Client.Tests/Projections/UpdateProjectionTests.cs b/test/Kurrent.Client.Tests/Projections/UpdateProjectionTests.cs index a64eb6319..17c4ba896 100644 --- a/test/Kurrent.Client.Tests/Projections/UpdateProjectionTests.cs +++ b/test/Kurrent.Client.Tests/Projections/UpdateProjectionTests.cs @@ -2,6 +2,7 @@ namespace Kurrent.Client.Tests.Projections; +[Trait("Category", "Target:Projections")] public class UpdateProjectionTests(ITestOutputHelper output, UpdateProjectionTests.CustomFixture fixture) : KurrentTemporaryTests(output, fixture) { [Theory] diff --git a/test/Kurrent.Client.Tests/RegularFilterExpressionTests.cs b/test/Kurrent.Client.Tests/RegularFilterExpressionTests.cs index de0a4e7b9..4fe4f6650 100644 --- a/test/Kurrent.Client.Tests/RegularFilterExpressionTests.cs +++ b/test/Kurrent.Client.Tests/RegularFilterExpressionTests.cs @@ -4,6 +4,7 @@ namespace Kurrent.Client.Tests; +[Trait("Category", "Target:Misc")] public class RegularFilterExpressionTests : ValueObjectTests { public RegularFilterExpressionTests() : base(new ScenarioFixture()) { } diff --git a/test/Kurrent.Client.Tests/Security/AllStreamWithNoAclSecurityTests.cs b/test/Kurrent.Client.Tests/Security/AllStreamWithNoAclSecurityTests.cs index 1e6da34e4..e33f707ac 100644 --- a/test/Kurrent.Client.Tests/Security/AllStreamWithNoAclSecurityTests.cs +++ b/test/Kurrent.Client.Tests/Security/AllStreamWithNoAclSecurityTests.cs @@ -4,7 +4,7 @@ namespace Kurrent.Client.Tests; -[Trait("Category", "Security")] +[Trait("Category", "Target:Security")] public class AllStreamWithNoAclSecurityTests(ITestOutputHelper output, AllStreamWithNoAclSecurityTests.CustomFixture fixture) : KurrentTemporaryTests(output, fixture) { [Fact] diff --git a/test/Kurrent.Client.Tests/Security/DeleteStreamSecurityTests.cs b/test/Kurrent.Client.Tests/Security/DeleteStreamSecurityTests.cs index feea4452a..69b31609c 100644 --- a/test/Kurrent.Client.Tests/Security/DeleteStreamSecurityTests.cs +++ b/test/Kurrent.Client.Tests/Security/DeleteStreamSecurityTests.cs @@ -4,7 +4,7 @@ namespace Kurrent.Client.Tests; -[Trait("Category", "Security")] +[Trait("Category", "Target:Security")] public class DeleteStreamSecurityTests(ITestOutputHelper output, SecurityFixture fixture) : KurrentTemporaryTests(output, fixture) { [Fact] public async Task delete_of_all_is_never_allowed() { diff --git a/test/Kurrent.Client.Tests/Security/MultipleRoleSecurityTests.cs b/test/Kurrent.Client.Tests/Security/MultipleRoleSecurityTests.cs index fd36ae2d3..f38d3fd27 100644 --- a/test/Kurrent.Client.Tests/Security/MultipleRoleSecurityTests.cs +++ b/test/Kurrent.Client.Tests/Security/MultipleRoleSecurityTests.cs @@ -4,7 +4,7 @@ namespace Kurrent.Client.Tests; -[Trait("Category", "Security")] +[Trait("Category", "Target:Security")] public class MultipleRoleSecurityTests(ITestOutputHelper output, MultipleRoleSecurityTests.CustomFixture fixture) : KurrentTemporaryTests(output, fixture) { [Fact] diff --git a/test/Kurrent.Client.Tests/Security/OverridenSystemStreamSecurityForAllTests.cs b/test/Kurrent.Client.Tests/Security/OverridenSystemStreamSecurityForAllTests.cs index 14f3e1b93..ebc66f3c9 100644 --- a/test/Kurrent.Client.Tests/Security/OverridenSystemStreamSecurityForAllTests.cs +++ b/test/Kurrent.Client.Tests/Security/OverridenSystemStreamSecurityForAllTests.cs @@ -4,7 +4,7 @@ namespace Kurrent.Client.Tests; -[Trait("Category", "Security")] +[Trait("Category", "Target:Security")] public class OverridenSystemStreamSecurityForAllTests(ITestOutputHelper output, OverridenSystemStreamSecurityForAllTests.CustomFixture fixture) : KurrentTemporaryTests(output, fixture) { [Fact] diff --git a/test/Kurrent.Client.Tests/Security/OverridenSystemStreamSecurityTests.cs b/test/Kurrent.Client.Tests/Security/OverridenSystemStreamSecurityTests.cs index af9e4ddd8..eded40e7e 100644 --- a/test/Kurrent.Client.Tests/Security/OverridenSystemStreamSecurityTests.cs +++ b/test/Kurrent.Client.Tests/Security/OverridenSystemStreamSecurityTests.cs @@ -3,7 +3,7 @@ namespace Kurrent.Client.Tests; -[Trait("Category", "Security")] +[Trait("Category", "Target:Security")] public class OverridenSystemStreamSecurityTests(ITestOutputHelper output, OverridenSystemStreamSecurityTests.CustomFixture fixture) : KurrentTemporaryTests(output, fixture) { [Fact] diff --git a/test/Kurrent.Client.Tests/Security/OverridenUserStreamSecurityTests.cs b/test/Kurrent.Client.Tests/Security/OverridenUserStreamSecurityTests.cs index 78cbe52ec..445bf564b 100644 --- a/test/Kurrent.Client.Tests/Security/OverridenUserStreamSecurityTests.cs +++ b/test/Kurrent.Client.Tests/Security/OverridenUserStreamSecurityTests.cs @@ -3,7 +3,7 @@ namespace Kurrent.Client.Tests; -[Trait("Category", "Security")] +[Trait("Category", "Target:Security")] public class OverridenUserStreamSecurityTests(ITestOutputHelper output, OverridenUserStreamSecurityTests.CustomFixture fixture) : KurrentTemporaryTests(output, fixture) { [Fact] diff --git a/test/Kurrent.Client.Tests/Security/ReadAllSecurityTests.cs b/test/Kurrent.Client.Tests/Security/ReadAllSecurityTests.cs index e87c12762..fcbf0980f 100644 --- a/test/Kurrent.Client.Tests/Security/ReadAllSecurityTests.cs +++ b/test/Kurrent.Client.Tests/Security/ReadAllSecurityTests.cs @@ -4,7 +4,7 @@ namespace Kurrent.Client.Tests; -[Trait("Category", "Security")] +[Trait("Category", "Target:Security")] public class ReadAllSecurityTests(ITestOutputHelper output, SecurityFixture fixture) : KurrentTemporaryTests(output, fixture) { [Fact] public async Task reading_all_with_not_existing_credentials_is_not_authenticated() { diff --git a/test/Kurrent.Client.Tests/Security/ReadStreamMetaSecurityTests.cs b/test/Kurrent.Client.Tests/Security/ReadStreamMetaSecurityTests.cs index 8d6e56c29..4bdb20689 100644 --- a/test/Kurrent.Client.Tests/Security/ReadStreamMetaSecurityTests.cs +++ b/test/Kurrent.Client.Tests/Security/ReadStreamMetaSecurityTests.cs @@ -4,7 +4,7 @@ namespace Kurrent.Client.Tests; -[Trait("Category", "Security")] +[Trait("Category", "Target:Security")] public class ReadStreamMetaSecurityTests(ITestOutputHelper output, SecurityFixture fixture) : KurrentTemporaryTests(output, fixture) { [Fact] public async Task reading_stream_meta_with_not_existing_credentials_is_not_authenticated() => diff --git a/test/Kurrent.Client.Tests/Security/ReadStreamSecurityTests.cs b/test/Kurrent.Client.Tests/Security/ReadStreamSecurityTests.cs index 9422ff671..7c3795a44 100644 --- a/test/Kurrent.Client.Tests/Security/ReadStreamSecurityTests.cs +++ b/test/Kurrent.Client.Tests/Security/ReadStreamSecurityTests.cs @@ -4,7 +4,7 @@ namespace Kurrent.Client.Tests; -[Trait("Category", "Security")] +[Trait("Category", "Target:Security")] public class ReadStreamSecurityTests(ITestOutputHelper output, SecurityFixture fixture) : KurrentTemporaryTests(output, fixture) { [Fact] public async Task reading_stream_with_not_existing_credentials_is_not_authenticated() { diff --git a/test/Kurrent.Client.Tests/Security/StreamSecurityInheritanceTests.cs b/test/Kurrent.Client.Tests/Security/StreamSecurityInheritanceTests.cs index dbe25c65e..cb6d0b177 100644 --- a/test/Kurrent.Client.Tests/Security/StreamSecurityInheritanceTests.cs +++ b/test/Kurrent.Client.Tests/Security/StreamSecurityInheritanceTests.cs @@ -4,7 +4,7 @@ namespace Kurrent.Client.Tests; -[Trait("Category", "Security")] +[Trait("Category", "Target:Security")] public class StreamSecurityInheritanceTests(ITestOutputHelper output, StreamSecurityInheritanceTests.CustomFixture fixture) : KurrentTemporaryTests(output, fixture) { [RetryFact] diff --git a/test/Kurrent.Client.Tests/Security/SubscribeToAllSecurityTests.cs b/test/Kurrent.Client.Tests/Security/SubscribeToAllSecurityTests.cs index 62f35497a..ba3cea9bc 100644 --- a/test/Kurrent.Client.Tests/Security/SubscribeToAllSecurityTests.cs +++ b/test/Kurrent.Client.Tests/Security/SubscribeToAllSecurityTests.cs @@ -4,7 +4,7 @@ namespace Kurrent.Client.Tests; -[Trait("Category", "Security")] +[Trait("Category", "Target:Security")] public class SubscribeToAllSecurityTests(ITestOutputHelper output, SecurityFixture fixture) : KurrentTemporaryTests(output, fixture) { [Fact] public async Task subscribing_to_all_with_not_existing_credentials_is_not_authenticated() => diff --git a/test/Kurrent.Client.Tests/Security/SubscribeToStreamSecurityTests.cs b/test/Kurrent.Client.Tests/Security/SubscribeToStreamSecurityTests.cs index 8d9625990..e1226122b 100644 --- a/test/Kurrent.Client.Tests/Security/SubscribeToStreamSecurityTests.cs +++ b/test/Kurrent.Client.Tests/Security/SubscribeToStreamSecurityTests.cs @@ -4,7 +4,7 @@ namespace Kurrent.Client.Tests; -[Trait("Category", "Security")] +[Trait("Category", "Target:Security")] public class SubscribeToStreamSecurityTests(ITestOutputHelper output, SecurityFixture fixture) : KurrentTemporaryTests(output, fixture) { [Fact] diff --git a/test/Kurrent.Client.Tests/Security/SystemStreamSecurityTests.cs b/test/Kurrent.Client.Tests/Security/SystemStreamSecurityTests.cs index 4ed213611..720ae8ec1 100644 --- a/test/Kurrent.Client.Tests/Security/SystemStreamSecurityTests.cs +++ b/test/Kurrent.Client.Tests/Security/SystemStreamSecurityTests.cs @@ -4,7 +4,7 @@ namespace Kurrent.Client.Tests; -[Trait("Category", "Security")] +[Trait("Category", "Target:Security")] public class SystemStreamSecurityTests(ITestOutputHelper output, SecurityFixture fixture) : KurrentTemporaryTests(output, fixture) { [Fact] public async Task operations_on_system_stream_with_no_acl_set_fail_for_non_admin() { diff --git a/test/Kurrent.Client.Tests/Security/WriteStreamMetaSecurityTests.cs b/test/Kurrent.Client.Tests/Security/WriteStreamMetaSecurityTests.cs index a2346b124..942a155cc 100644 --- a/test/Kurrent.Client.Tests/Security/WriteStreamMetaSecurityTests.cs +++ b/test/Kurrent.Client.Tests/Security/WriteStreamMetaSecurityTests.cs @@ -4,7 +4,7 @@ namespace Kurrent.Client.Tests; -[Trait("Category", "Security")] +[Trait("Category", "Target:Security")] public class WriteStreamMetaSecurityTests(ITestOutputHelper output, SecurityFixture fixture) : KurrentTemporaryTests(output, fixture) { [Fact] public async Task writing_meta_with_not_existing_credentials_is_not_authenticated() => diff --git a/test/Kurrent.Client.Tests/Security/WriteStreamSecurityTests.cs b/test/Kurrent.Client.Tests/Security/WriteStreamSecurityTests.cs index 0bb37a94d..67ef35bc5 100644 --- a/test/Kurrent.Client.Tests/Security/WriteStreamSecurityTests.cs +++ b/test/Kurrent.Client.Tests/Security/WriteStreamSecurityTests.cs @@ -2,7 +2,7 @@ namespace Kurrent.Client.Tests; -[Trait("Category", "Security")] +[Trait("Category", "Target:Security")] public class WriteStreamSecurityTests : IClassFixture { public WriteStreamSecurityTests(ITestOutputHelper output, SecurityFixture fixture) => Fixture = fixture.With(x => x.CaptureTestRun(output)); diff --git a/test/Kurrent.Client.Tests/SharingProviderTests.cs b/test/Kurrent.Client.Tests/SharingProviderTests.cs index fc1f31abc..3c55cfdda 100644 --- a/test/Kurrent.Client.Tests/SharingProviderTests.cs +++ b/test/Kurrent.Client.Tests/SharingProviderTests.cs @@ -4,6 +4,7 @@ namespace Kurrent.Client.Tests; +[Trait("Category", "Target:Misc")] public class SharingProviderTests { [RetryFact] public async Task CanGetCurrent() { diff --git a/test/Kurrent.Client.Tests/StreamPositionTests.cs b/test/Kurrent.Client.Tests/StreamPositionTests.cs index 591ea617e..06a4ad0ce 100644 --- a/test/Kurrent.Client.Tests/StreamPositionTests.cs +++ b/test/Kurrent.Client.Tests/StreamPositionTests.cs @@ -3,6 +3,7 @@ namespace Kurrent.Client.Tests; +[Trait("Category", "Target:Misc")] public class StreamPositionTests : ValueObjectTests { public StreamPositionTests() : base(new ScenarioFixture()) { } diff --git a/test/Kurrent.Client.Tests/StreamRevisionTests.cs b/test/Kurrent.Client.Tests/StreamRevisionTests.cs index 4b77f81d0..eb1d5c534 100644 --- a/test/Kurrent.Client.Tests/StreamRevisionTests.cs +++ b/test/Kurrent.Client.Tests/StreamRevisionTests.cs @@ -3,6 +3,7 @@ namespace Kurrent.Client.Tests; +[Trait("Category", "Target:Misc")] public class StreamRevisionTests : ValueObjectTests { public StreamRevisionTests() : base(new ScenarioFixture()) { } diff --git a/test/Kurrent.Client.Tests/StreamStateTests.cs b/test/Kurrent.Client.Tests/StreamStateTests.cs index b9c68df38..1faf56214 100644 --- a/test/Kurrent.Client.Tests/StreamStateTests.cs +++ b/test/Kurrent.Client.Tests/StreamStateTests.cs @@ -4,6 +4,7 @@ namespace Kurrent.Client.Tests; +[Trait("Category", "Target:Misc")] public class StreamStateTests : ValueObjectTests { public StreamStateTests() : base(new ScenarioFixture()) { } diff --git a/test/Kurrent.Client.Tests/Streams/AppendTests.cs b/test/Kurrent.Client.Tests/Streams/AppendTests.cs index 0dab95225..f6442219e 100644 --- a/test/Kurrent.Client.Tests/Streams/AppendTests.cs +++ b/test/Kurrent.Client.Tests/Streams/AppendTests.cs @@ -4,7 +4,7 @@ namespace Kurrent.Client.Tests.Streams; -[Trait("Category", "Target:Stream")] +[Trait("Category", "Target:Streams")] [Trait("Category", "Operation:Append")] public class AppendTests(ITestOutputHelper output, KurrentPermanentFixture fixture) : KurrentPermanentTests(output, fixture) { [Theory, ExpectedVersionCreateStreamTestCases] diff --git a/test/Kurrent.Client.Tests/Streams/Bugs/Obsolete/Issue104.cs b/test/Kurrent.Client.Tests/Streams/Bugs/Obsolete/Issue104.cs index 9eb0124b4..4fe12ecbe 100644 --- a/test/Kurrent.Client.Tests/Streams/Bugs/Obsolete/Issue104.cs +++ b/test/Kurrent.Client.Tests/Streams/Bugs/Obsolete/Issue104.cs @@ -2,6 +2,7 @@ namespace Kurrent.Client.Tests.Bugs.Obsolete; +[Trait("Category", "Target:Streams")] [Trait("Category", "Bug")] [Obsolete("Tests will be removed in future release when older subscriptions APIs are removed from the client")] public class Issue104(ITestOutputHelper output, KurrentPermanentFixture fixture) : KurrentPermanentTests(output, fixture) { diff --git a/test/Kurrent.Client.Tests/Streams/Bugs/Obsolete/Issue2544.cs b/test/Kurrent.Client.Tests/Streams/Bugs/Obsolete/Issue2544.cs index 027a70500..a1add87e2 100644 --- a/test/Kurrent.Client.Tests/Streams/Bugs/Obsolete/Issue2544.cs +++ b/test/Kurrent.Client.Tests/Streams/Bugs/Obsolete/Issue2544.cs @@ -4,6 +4,7 @@ namespace Kurrent.Client.Tests.Bugs.Obsolete; +[Trait("Category", "Target:Streams")] [Trait("Category", "Bug")] [Obsolete("Tests will be removed in future release when older subscriptions APIs are removed from the client")] public class Issue2544 : IClassFixture { diff --git a/test/Kurrent.Client.Tests/Streams/DeleteTests.cs b/test/Kurrent.Client.Tests/Streams/DeleteTests.cs index 07b28f098..c09e3fdbe 100644 --- a/test/Kurrent.Client.Tests/Streams/DeleteTests.cs +++ b/test/Kurrent.Client.Tests/Streams/DeleteTests.cs @@ -3,7 +3,7 @@ namespace Kurrent.Client.Tests.Streams; -[Trait("Category", "Target:Stream")] +[Trait("Category", "Target:Streams")] [Trait("Category", "Operation:Delete")] public class DeleteTests(ITestOutputHelper output, KurrentPermanentFixture fixture) : KurrentPermanentTests(output, fixture) { [Theory, ExpectedStreamStateCases] diff --git a/test/Kurrent.Client.Tests/Streams/Read/ReadAllEventsBackwardTests.cs b/test/Kurrent.Client.Tests/Streams/Read/ReadAllEventsBackwardTests.cs index 3de2fd591..a5850e906 100644 --- a/test/Kurrent.Client.Tests/Streams/Read/ReadAllEventsBackwardTests.cs +++ b/test/Kurrent.Client.Tests/Streams/Read/ReadAllEventsBackwardTests.cs @@ -4,6 +4,7 @@ namespace Kurrent.Client.Tests; +[Trait("Category", "Target:Streams")] [Trait("Category", "Target:All")] [Trait("Category", "Operation:Read")] [Trait("Category", "Operation:Read:Backwards")] diff --git a/test/Kurrent.Client.Tests/Streams/Read/ReadAllEventsFixture.cs b/test/Kurrent.Client.Tests/Streams/Read/ReadAllEventsFixture.cs index a6119c874..f3d2562e1 100644 --- a/test/Kurrent.Client.Tests/Streams/Read/ReadAllEventsFixture.cs +++ b/test/Kurrent.Client.Tests/Streams/Read/ReadAllEventsFixture.cs @@ -3,6 +3,7 @@ namespace Kurrent.Client.Tests; +[Trait("Category", "Target:Streams")] [Trait("Category", "Target:All")] [Trait("Category", "Operation:Read")] [Trait("Category", "Database:Dedicated")] diff --git a/test/Kurrent.Client.Tests/Streams/Read/ReadAllEventsForwardTests.cs b/test/Kurrent.Client.Tests/Streams/Read/ReadAllEventsForwardTests.cs index a8086faf6..f1c4ec2bd 100644 --- a/test/Kurrent.Client.Tests/Streams/Read/ReadAllEventsForwardTests.cs +++ b/test/Kurrent.Client.Tests/Streams/Read/ReadAllEventsForwardTests.cs @@ -4,6 +4,7 @@ namespace Kurrent.Client.Tests; +[Trait("Category", "Target:Streams")] [Trait("Category", "Target:All")] [Trait("Category", "Operation:Read")] [Trait("Category", "Operation:Read:Forwards")] diff --git a/test/Kurrent.Client.Tests/Streams/Read/ReadStreamBackwardTests.cs b/test/Kurrent.Client.Tests/Streams/Read/ReadStreamBackwardTests.cs index 190a33e11..04e8c441b 100644 --- a/test/Kurrent.Client.Tests/Streams/Read/ReadStreamBackwardTests.cs +++ b/test/Kurrent.Client.Tests/Streams/Read/ReadStreamBackwardTests.cs @@ -4,7 +4,8 @@ namespace Kurrent.Client.Tests; -[Trait("Category", "Target:Stream")] +[Trait("Category", "Target:Streams")] +[Trait("Category", "Target:Streams")] [Trait("Category", "Operation:Read")] [Trait("Category", "Operation:Read:Backwards")] public class ReadStreamBackwardTests(ITestOutputHelper output, KurrentTemporaryFixture fixture) diff --git a/test/Kurrent.Client.Tests/Streams/Read/ReadStreamEventsLinkedToDeletedStreamTests.cs b/test/Kurrent.Client.Tests/Streams/Read/ReadStreamEventsLinkedToDeletedStreamTests.cs index c5c151868..b1a803c71 100644 --- a/test/Kurrent.Client.Tests/Streams/Read/ReadStreamEventsLinkedToDeletedStreamTests.cs +++ b/test/Kurrent.Client.Tests/Streams/Read/ReadStreamEventsLinkedToDeletedStreamTests.cs @@ -7,7 +7,8 @@ namespace Kurrent.Client.Tests; -[Trait("Category", "Target:Stream")] +[Trait("Category", "Target:Streams")] +[Trait("Category", "Target:Streams")] public abstract class ReadStreamEventsLinkedToDeletedStreamTests(ReadEventsLinkedToDeletedStreamFixture fixture) { ReadEventsLinkedToDeletedStreamFixture Fixture { get; } = fixture; diff --git a/test/Kurrent.Client.Tests/Streams/Read/ReadStreamForwardTests.cs b/test/Kurrent.Client.Tests/Streams/Read/ReadStreamForwardTests.cs index d9d2a35d3..1e8d5db5b 100644 --- a/test/Kurrent.Client.Tests/Streams/Read/ReadStreamForwardTests.cs +++ b/test/Kurrent.Client.Tests/Streams/Read/ReadStreamForwardTests.cs @@ -2,7 +2,8 @@ namespace Kurrent.Client.Tests; -[Trait("Category", "Target:Stream")] +[Trait("Category", "Target:Streams")] +[Trait("Category", "Target:Streams")] [Trait("Category", "Operation:Read")] [Trait("Category", "Operation:Read:Forwards")] public class ReadStreamForwardTests(ITestOutputHelper output, KurrentPermanentFixture fixture) : KurrentPermanentTests(output, fixture) { diff --git a/test/Kurrent.Client.Tests/Streams/Read/ReadStreamWhenHavingMaxCountSetForStreamTests.cs b/test/Kurrent.Client.Tests/Streams/Read/ReadStreamWhenHavingMaxCountSetForStreamTests.cs index 10a3cce28..1e22b5eb1 100644 --- a/test/Kurrent.Client.Tests/Streams/Read/ReadStreamWhenHavingMaxCountSetForStreamTests.cs +++ b/test/Kurrent.Client.Tests/Streams/Read/ReadStreamWhenHavingMaxCountSetForStreamTests.cs @@ -4,7 +4,8 @@ namespace Kurrent.Client.Tests; -[Trait("Category", "Target:Stream")] +[Trait("Category", "Target:Streams")] +[Trait("Category", "Target:Streams")] [Trait("Category", "Operation:Read")] public class ReadStreamWhenHavingMaxCountSetForStreamTests(ITestOutputHelper output, KurrentTemporaryFixture fixture) : KurrentTemporaryTests(output, fixture) { diff --git a/test/Kurrent.Client.Tests/Streams/SoftDeleteTests.cs b/test/Kurrent.Client.Tests/Streams/SoftDeleteTests.cs index 84e4b8176..3e8cf2617 100644 --- a/test/Kurrent.Client.Tests/Streams/SoftDeleteTests.cs +++ b/test/Kurrent.Client.Tests/Streams/SoftDeleteTests.cs @@ -3,7 +3,7 @@ namespace Kurrent.Client.Tests.Streams; -[Trait("Category", "Target:Stream")] +[Trait("Category", "Target:Streams")] [Trait("Category", "Operation:Delete")] public class SoftDeleteTests(ITestOutputHelper output, KurrentPermanentFixture fixture) : KurrentPermanentTests(output, fixture) { static JsonDocument CustomMetadata { get; } diff --git a/test/Kurrent.Client.Tests/Streams/StreamMetadataTests.cs b/test/Kurrent.Client.Tests/Streams/StreamMetadataTests.cs index 23320b6b2..a02083f6c 100644 --- a/test/Kurrent.Client.Tests/Streams/StreamMetadataTests.cs +++ b/test/Kurrent.Client.Tests/Streams/StreamMetadataTests.cs @@ -4,7 +4,7 @@ namespace Kurrent.Client.Tests.Streams; -[Trait("Category", "Target:Stream")] +[Trait("Category", "Target:Streams")] [Trait("Category", "Operation:Metadata")] public class StreamMetadataTests(ITestOutputHelper output, KurrentPermanentFixture fixture) : KurrentPermanentTests(output, fixture) { [Fact] diff --git a/test/Kurrent.Client.Tests/Streams/SubscribeToStreamTests.cs b/test/Kurrent.Client.Tests/Streams/SubscribeToStreamTests.cs index 61ca29ebb..304144e9b 100644 --- a/test/Kurrent.Client.Tests/Streams/SubscribeToStreamTests.cs +++ b/test/Kurrent.Client.Tests/Streams/SubscribeToStreamTests.cs @@ -3,7 +3,7 @@ namespace Kurrent.Client.Tests.Streams; [Trait("Category", "Subscriptions")] -[Trait("Category", "Target:Stream")] +[Trait("Category", "Target:Streams")] public class SubscribeToStreamTests(ITestOutputHelper output, SubscribeToStreamTests.CustomFixture fixture) : KurrentPermanentTests(output, fixture) { [RetryFact] diff --git a/test/Kurrent.Client.Tests/UuidTests.cs b/test/Kurrent.Client.Tests/UuidTests.cs index feeadb4e7..96550b734 100644 --- a/test/Kurrent.Client.Tests/UuidTests.cs +++ b/test/Kurrent.Client.Tests/UuidTests.cs @@ -3,6 +3,7 @@ namespace Kurrent.Client.Tests; +[Trait("Category", "Target:Misc")] public class UuidTests : ValueObjectTests { public UuidTests() : base(new ScenarioFixture()) { } diff --git a/test/Kurrent.Client.Tests/ValueObjectTests.cs b/test/Kurrent.Client.Tests/ValueObjectTests.cs index dc4e9f2b0..08d09606d 100644 --- a/test/Kurrent.Client.Tests/ValueObjectTests.cs +++ b/test/Kurrent.Client.Tests/ValueObjectTests.cs @@ -2,6 +2,7 @@ namespace Kurrent.Client.Tests; +[Trait("Category", "Target:Misc")] public abstract class ValueObjectTests { protected readonly Fixture _fixture; diff --git a/test/Kurrent.Client.Tests/X509CertificatesTests.cs b/test/Kurrent.Client.Tests/X509CertificatesTests.cs index 71d4bc752..04dce9b7a 100644 --- a/test/Kurrent.Client.Tests/X509CertificatesTests.cs +++ b/test/Kurrent.Client.Tests/X509CertificatesTests.cs @@ -3,6 +3,7 @@ namespace Kurrent.Client.Tests; +[Trait("Category", "Target:Misc")] public class X509CertificatesTests { [RetryFact] public void create_from_pem_file() { From b278f7d22a64a08fb024f8dfdf8c0c57cd3ab662 Mon Sep 17 00:00:00 2001 From: William Chong Date: Wed, 8 Jan 2025 10:10:28 +0400 Subject: [PATCH 10/15] Improve CI --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b2daa69ce..ed2ccb823 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,6 +20,7 @@ jobs: with: docker-tag: ${{ matrix.docker-tag }} docker-image: eventstore-ce/eventstoredb-ce + test: ${{ matrix.test }} ee: uses: ./.github/workflows/base.yml if: ${{ github.repository_owner == 'EventStore' }} From fd0b009160f9e67baf3514ed4c0797a9e354be63 Mon Sep 17 00:00:00 2001 From: William Chong Date: Wed, 8 Jan 2025 12:09:23 +0400 Subject: [PATCH 11/15] Fixup --- .github/workflows/ci.yml | 2 +- .github/workflows/dispatch-ce.yml | 2 +- .../Core/Certificates/X509Certificates.cs | 28 ++---- .../KurrentClientSettings.ConnectionString.cs | 3 +- .../Fixtures/KurrentPermanentTestNode.cs | 5 + .../Fixtures/KurrentTemporaryTestNode.cs | 7 +- .../ClientCertificatesTests.cs | 29 +++--- .../InvalidCredentialsTestCases.cs | 28 ++++++ ...nnectToExistingWithStartFromNotSetTests.cs | 33 +++++++ ...stingWithStartFromSetToEndPositionTests.cs | 35 +++++++ .../SubscribeToAllTests.cs | 52 ----------- ...ctToExistingWithStartFromBeginningTests.cs | 33 +++++++ .../SubscribeToStreamTests.cs | 25 ----- .../DisableProjectionTests.cs | 2 +- .../EnableProjectionTests.cs | 2 +- .../GetProjectionResultTests.cs | 2 +- .../GetProjectionStateTests.cs | 2 +- .../GetProjectionStatusTests.cs | 2 +- .../ListAllProjectionsTests.cs | 2 +- .../ListContinuousProjectionsTests.cs | 2 +- .../ListOneTimeProjectionsTests.cs | 2 +- .../ProjectionManagementTests.cs | 2 +- .../ResetProjectionTests.cs | 2 +- .../RestartSubsystemTests.cs | 2 +- .../UpdateProjectionTests.cs | 2 +- .../UserManagement/ChangePasswordTests.cs | 69 ++++++++++++++ .../UserManagement/CreateUserTests.cs | 93 +++++++++++++++++++ .../UserManagement/DeleteUserTests.cs | 69 ++++++++++++++ .../UserManagement/DisableUserTests.cs | 64 +++++++++++++ .../UserManagement/EnableUserTests.cs | 61 ++++++++++++ .../UserManagement/GetCurrentUserTests.cs | 12 +++ .../UserManagement/ListUserTests.cs | 40 ++++++++ .../ResettingUserPasswordTests.cs | 81 ++++++++++++++++ .../UserManagement/UserCredentialsTests.cs | 44 +++++++++ 34 files changed, 713 insertions(+), 126 deletions(-) create mode 100644 test/Kurrent.Client.Tests/InvalidCredentialsTestCases.cs create mode 100644 test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllConnectToExistingWithStartFromNotSetTests.cs create mode 100644 test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllConnectToExistingWithStartFromSetToEndPositionTests.cs create mode 100644 test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToStreamConnectToExistingWithStartFromBeginningTests.cs rename test/Kurrent.Client.Tests/{Projections => ProjectionManagement}/DisableProjectionTests.cs (94%) rename test/Kurrent.Client.Tests/{Projections => ProjectionManagement}/EnableProjectionTests.cs (94%) rename test/Kurrent.Client.Tests/{Projections => ProjectionManagement}/GetProjectionResultTests.cs (96%) rename test/Kurrent.Client.Tests/{Projections => ProjectionManagement}/GetProjectionStateTests.cs (96%) rename test/Kurrent.Client.Tests/{Projections => ProjectionManagement}/GetProjectionStatusTests.cs (93%) rename test/Kurrent.Client.Tests/{Projections => ProjectionManagement}/ListAllProjectionsTests.cs (95%) rename test/Kurrent.Client.Tests/{Projections => ProjectionManagement}/ListContinuousProjectionsTests.cs (95%) rename test/Kurrent.Client.Tests/{Projections => ProjectionManagement}/ListOneTimeProjectionsTests.cs (94%) rename test/Kurrent.Client.Tests/{Projections => ProjectionManagement}/ProjectionManagementTests.cs (97%) rename test/Kurrent.Client.Tests/{Projections => ProjectionManagement}/ResetProjectionTests.cs (94%) rename test/Kurrent.Client.Tests/{Projections => ProjectionManagement}/RestartSubsystemTests.cs (94%) rename test/Kurrent.Client.Tests/{Projections => ProjectionManagement}/UpdateProjectionTests.cs (94%) create mode 100644 test/Kurrent.Client.Tests/UserManagement/ChangePasswordTests.cs create mode 100644 test/Kurrent.Client.Tests/UserManagement/CreateUserTests.cs create mode 100644 test/Kurrent.Client.Tests/UserManagement/DeleteUserTests.cs create mode 100644 test/Kurrent.Client.Tests/UserManagement/DisableUserTests.cs create mode 100644 test/Kurrent.Client.Tests/UserManagement/EnableUserTests.cs create mode 100644 test/Kurrent.Client.Tests/UserManagement/GetCurrentUserTests.cs create mode 100644 test/Kurrent.Client.Tests/UserManagement/ListUserTests.cs create mode 100644 test/Kurrent.Client.Tests/UserManagement/ResettingUserPasswordTests.cs create mode 100644 test/Kurrent.Client.Tests/UserManagement/UserCredentialsTests.cs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ed2ccb823..ec0af3697 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,7 +15,7 @@ jobs: fail-fast: false matrix: docker-tag: [ ci, lts, previous-lts ] - test: [ Streams, PersistentSubscriptions, Operations, Projections, Security, Misc ] + test: [ Streams, PersistentSubscriptions, Operations, ProjectionManagement, UserManagement, Security, Misc ] name: Test CE (${{ matrix.docker-tag }}) with: docker-tag: ${{ matrix.docker-tag }} diff --git a/.github/workflows/dispatch-ce.yml b/.github/workflows/dispatch-ce.yml index 2611dfbd2..c05c2402f 100644 --- a/.github/workflows/dispatch-ce.yml +++ b/.github/workflows/dispatch-ce.yml @@ -18,7 +18,7 @@ jobs: strategy: fail-fast: false matrix: - test: [ Streams, PersistentSubscriptions, Operations, Projections, Security, Misc ] + test: [ Streams, PersistentSubscriptions, Operations, ProjectionManagement, UserManagement, Security, Misc ] name: Test CE (${{ inputs.docker-tag }}) with: docker-tag: ${{ inputs.docker-tag }} diff --git a/src/Kurrent.Client/Core/Certificates/X509Certificates.cs b/src/Kurrent.Client/Core/Certificates/X509Certificates.cs index 3fe1006f5..9cda47a08 100644 --- a/src/Kurrent.Client/Core/Certificates/X509Certificates.cs +++ b/src/Kurrent.Client/Core/Certificates/X509Certificates.cs @@ -2,6 +2,7 @@ using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; +#pragma warning disable SYSLIB0057 #if NET48 using Org.BouncyCastle.Crypto; @@ -13,35 +14,20 @@ namespace EventStore.Client; static class X509Certificates { - // TODO SS: Use .NET 8 X509Certificate2.CreateFromPemFile(certPemFilePath, keyPemFilePath) once the Windows32Exception issue is resolved public static X509Certificate2 CreateFromPemFile(string certPemFilePath, string keyPemFilePath) { try { -#if NET9_0_OR_GREATER - using var publicCert = X509CertificateLoader.LoadCertificateFromFile(certPemFilePath); +#if NET8_0_OR_GREATER + using var certificate = X509Certificate2.CreateFromPemFile(certPemFilePath, keyPemFilePath); #else using var publicCert = new X509Certificate2(certPemFilePath); -#endif - using var privateKey = RSA.Create().ImportPrivateKeyFromFile(keyPemFilePath); + using var privateKey = RSA.Create().ImportPrivateKeyFromFile(keyPemFilePath); using var certificate = publicCert.CopyWithPrivateKey(privateKey); - -#if NET48 - return new(certificate.Export(X509ContentType.Pfx)); -#else - return X509Certificate2.CreateFromPemFile(certPemFilePath, keyPemFilePath); #endif + + return new X509Certificate2(certificate.Export(X509ContentType.Pfx)); } catch (Exception ex) { throw new CryptographicException($"Failed to load private key: {ex.Message}"); } - - // Notes: - // using X509Certificate2.CreateFromPemFile(certPemFilePath, keyPemFilePath) would be the ideal choice here, - // but it's currently causing a Win32Exception specifically on Windows. Alternative implementation is used until the issue is resolved. - // - // Error: The SSL connection could not be established, see inner exception. AuthenticationException: Authentication failed because the platform - // does not support ephemeral keys. Win32Exception: No credentials are available in the security package - // - // public static X509Certificate2 CreateFromPemFile(string certPemFilePath, string keyPemFilePath) => - // X509Certificate2.CreateFromPemFile(certPemFilePath, keyPemFilePath); } } @@ -66,7 +52,7 @@ public static RSA ImportPrivateKeyFromFile(this RSA rsa, string privateKeyPath) public static RSA ImportPrivateKeyFromFile(this RSA rsa, string privateKeyPath) { var (content, label) = LoadPemKeyFile(privateKeyPath); - var privateKey = string.Join(string.Empty, content[1..^1]); + var privateKey = string.Join(string.Empty, content[1..^1]); var privateKeyBytes = Convert.FromBase64String(privateKey); if (label == RsaPemLabels.Pkcs8PrivateKey) diff --git a/src/Kurrent.Client/Core/KurrentClientSettings.ConnectionString.cs b/src/Kurrent.Client/Core/KurrentClientSettings.ConnectionString.cs index 307794019..c730b7b5a 100644 --- a/src/Kurrent.Client/Core/KurrentClientSettings.ConnectionString.cs +++ b/src/Kurrent.Client/Core/KurrentClientSettings.ConnectionString.cs @@ -317,8 +317,7 @@ static void ConfigureClientCertificate(KurrentClientSettings settings, IReadOnly ); try { - settings.ConnectivitySettings.ClientCertificate = - X509Certificates.CreateFromPemFile(certPemFilePath, keyPemFilePath); + settings.ConnectivitySettings.ClientCertificate = X509Certificates.CreateFromPemFile(certPemFilePath, keyPemFilePath); } catch (Exception ex) { throw new InvalidClientCertificateException("Failed to create client certificate.", ex); } diff --git a/test/Kurrent.Client.Tests.Common/Fixtures/KurrentPermanentTestNode.cs b/test/Kurrent.Client.Tests.Common/Fixtures/KurrentPermanentTestNode.cs index b93bc9f89..c39895cf1 100644 --- a/test/Kurrent.Client.Tests.Common/Fixtures/KurrentPermanentTestNode.cs +++ b/test/Kurrent.Client.Tests.Common/Fixtures/KurrentPermanentTestNode.cs @@ -85,6 +85,11 @@ public static KurrentFixtureOptions DefaultOptions() { ["EVENTSTORE_ADVERTISE_HTTP_PORT_TO_CLIENT_AS"] = $"{NetworkPortProvider.DefaultEsdbPort}" }; + if (GlobalEnvironment.DockerImage.Contains("commercial")) { + defaultEnvironment["EVENTSTORE_TRUSTED_ROOT_CERTIFICATES_PATH"] = "/etc/eventstore/certs/ca"; + defaultEnvironment["EventStore__Plugins__UserCertificates__Enabled"] = "true"; + } + if (port != NetworkPortProvider.DefaultEsdbPort) { if (GlobalEnvironment.Variables.TryGetValue("ES_DOCKER_TAG", out var tag) && tag == "ci") defaultEnvironment["EVENTSTORE_ADVERTISE_NODE_PORT_TO_CLIENT_AS"] = $"{port}"; diff --git a/test/Kurrent.Client.Tests.Common/Fixtures/KurrentTemporaryTestNode.cs b/test/Kurrent.Client.Tests.Common/Fixtures/KurrentTemporaryTestNode.cs index 06b3663eb..7a6a98965 100644 --- a/test/Kurrent.Client.Tests.Common/Fixtures/KurrentTemporaryTestNode.cs +++ b/test/Kurrent.Client.Tests.Common/Fixtures/KurrentTemporaryTestNode.cs @@ -82,6 +82,11 @@ public static KurrentFixtureOptions DefaultOptions() { ["EVENTSTORE_ADVERTISE_HTTP_PORT_TO_CLIENT_AS"] = $"{NetworkPortProvider.DefaultEsdbPort}" }; + if (GlobalEnvironment.DockerImage.Contains("commercial")) { + defaultEnvironment["EVENTSTORE_TRUSTED_ROOT_CERTIFICATES_PATH"] = "/etc/eventstore/certs/ca"; + defaultEnvironment["EventStore__Plugins__UserCertificates__Enabled"] = "true"; + } + if (port != NetworkPortProvider.DefaultEsdbPort) { if (GlobalEnvironment.Variables.TryGetValue("ES_DOCKER_TAG", out var tag) && tag == "ci") defaultEnvironment["EVENTSTORE_ADVERTISE_NODE_PORT_TO_CLIENT_AS"] = $"{port}"; @@ -181,7 +186,7 @@ async Task GetNextAvailablePort(TimeSpan delay = default) { #if NET if (socket.Connected) await socket.DisconnectAsync(true); #else - if (socket.Connected) socket.Disconnect(true); + if (socket.Connected) socket.Disconnect(true); #endif } } diff --git a/test/Kurrent.Client.Tests/ClientCertificatesTests.cs b/test/Kurrent.Client.Tests/ClientCertificatesTests.cs index 2903d8fb5..92d08fa51 100644 --- a/test/Kurrent.Client.Tests/ClientCertificatesTests.cs +++ b/test/Kurrent.Client.Tests/ClientCertificatesTests.cs @@ -1,18 +1,21 @@ using EventStore.Client; using Humanizer; +using Kurrent.Client.Tests.TestNode; namespace Kurrent.Client.Tests; [Trait("Category", "Target:Misc")] [Trait("Category", "Target:Plugins")] [Trait("Category", "Type:UserCertificate")] -public class ClientCertificateTests(ITestOutputHelper output, KurrentPermanentFixture fixture) - : KurrentPermanentTests(output, fixture) { +public class ClientCertificateTests(ITestOutputHelper output, KurrentTemporaryFixture fixture) + : KurrentTemporaryTests(output, fixture) { [SupportsPlugins.Theory(EventStoreRepository.Commercial, "This server version does not support plugins"), BadClientCertificatesTestCases] async Task bad_certificates_combinations_should_return_authentication_error(string userCertFile, string userKeyFile, string tlsCaFile) { - var stream = Fixture.GetStreamName(); - var seedEvents = Fixture.CreateTestEvents(); - var connectionString = $"esdb://localhost:2113/?tls=true&userCertFile={userCertFile}&userKeyFile={userKeyFile}&tlsCaFile={tlsCaFile}"; + var stream = Fixture.GetStreamName(); + var seedEvents = Fixture.CreateTestEvents(); + var port = Fixture.Options.ClientSettings.ConnectivitySettings.ResolvedAddressOrDefault.Port; + + var connectionString = $"esdb://localhost:{port}/?tls=true&userCertFile={userCertFile}&userKeyFile={userKeyFile}&tlsCaFile={tlsCaFile}"; var settings = KurrentClientSettings.Create(connectionString); settings.ConnectivitySettings.TlsVerifyCert.ShouldBeTrue(); @@ -24,9 +27,11 @@ async Task bad_certificates_combinations_should_return_authentication_error(stri [SupportsPlugins.Theory(EventStoreRepository.Commercial, "This server version does not support plugins"), ValidClientCertificatesTestCases] async Task valid_certificates_combinations_should_write_to_stream(string userCertFile, string userKeyFile, string tlsCaFile) { - var stream = Fixture.GetStreamName(); - var seedEvents = Fixture.CreateTestEvents(); - var connectionString = $"esdb://localhost:2113/?userCertFile={userCertFile}&userKeyFile={userKeyFile}&tlsCaFile={tlsCaFile}"; + var stream = Fixture.GetStreamName(); + var seedEvents = Fixture.CreateTestEvents(); + var port = Fixture.Options.ClientSettings.ConnectivitySettings.ResolvedAddressOrDefault.Port; + + var connectionString = $"esdb://localhost:{port}/?userCertFile={userCertFile}&userKeyFile={userKeyFile}&tlsCaFile={tlsCaFile}"; var settings = KurrentClientSettings.Create(connectionString); settings.ConnectivitySettings.TlsVerifyCert.ShouldBeTrue(); @@ -39,9 +44,11 @@ async Task valid_certificates_combinations_should_write_to_stream(string userCer [SupportsPlugins.Theory(EventStoreRepository.Commercial, "This server version does not support plugins"), BadClientCertificatesTestCases] async Task basic_authentication_should_take_precedence(string userCertFile, string userKeyFile, string tlsCaFile) { - var stream = Fixture.GetStreamName(); - var seedEvents = Fixture.CreateTestEvents(); - var connectionString = $"esdb://admin:changeit@localhost:2113/?userCertFile={userCertFile}&userKeyFile={userKeyFile}&tlsCaFile={tlsCaFile}"; + var stream = Fixture.GetStreamName(); + var seedEvents = Fixture.CreateTestEvents(); + var port = Fixture.Options.ClientSettings.ConnectivitySettings.ResolvedAddressOrDefault.Port; + + var connectionString = $"esdb://admin:changeit@localhost:{port}/?userCertFile={userCertFile}&userKeyFile={userKeyFile}&tlsCaFile={tlsCaFile}"; var settings = KurrentClientSettings.Create(connectionString); settings.ConnectivitySettings.TlsVerifyCert.ShouldBeTrue(); diff --git a/test/Kurrent.Client.Tests/InvalidCredentialsTestCases.cs b/test/Kurrent.Client.Tests/InvalidCredentialsTestCases.cs new file mode 100644 index 000000000..b60499693 --- /dev/null +++ b/test/Kurrent.Client.Tests/InvalidCredentialsTestCases.cs @@ -0,0 +1,28 @@ +using System.Collections; +using EventStore.Client; + +namespace Kurrent.Client.Tests; + +public abstract record InvalidCredentialsTestCase(TestUser User, Type ExpectedException); + +public class InvalidCredentialsTestCases : IEnumerable { + public IEnumerator GetEnumerator() { + yield return new object?[] { new MissingCredentials() }; + yield return new object?[] { new WrongUsername() }; + yield return new object?[] { new WrongPassword() }; + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public record MissingCredentials() : InvalidCredentialsTestCase(Fakers.Users.WithNoCredentials(), typeof(AccessDeniedException)) { + public override string ToString() => nameof(MissingCredentials); + } + + public record WrongUsername() : InvalidCredentialsTestCase(Fakers.Users.WithInvalidCredentials(false), typeof(NotAuthenticatedException)) { + public override string ToString() => nameof(WrongUsername); + } + + public record WrongPassword() : InvalidCredentialsTestCase(Fakers.Users.WithInvalidCredentials(wrongPassword: false), typeof(NotAuthenticatedException)) { + public override string ToString() => nameof(WrongPassword); + } +} diff --git a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllConnectToExistingWithStartFromNotSetTests.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllConnectToExistingWithStartFromNotSetTests.cs new file mode 100644 index 000000000..b6a05c5cc --- /dev/null +++ b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllConnectToExistingWithStartFromNotSetTests.cs @@ -0,0 +1,33 @@ +using EventStore.Client; +using Kurrent.Client.Tests.TestNode; + +namespace Kurrent.Client.Tests.PersistentSubscriptions; + +[Trait("Category", "Target:PersistentSubscriptions")] +public class SubscribeToAllConnectToExistingWithStartFromNotSetTests(ITestOutputHelper output, KurrentTemporaryFixture fixture) + : KurrentTemporaryTests(output, fixture) { + [RetryFact] + public async Task connect_to_existing_with_start_from_not_set() { + var group = Fixture.GetGroupName(); + var stream = Fixture.GetStreamName(); + + foreach (var @event in Fixture.CreateTestEvents(10)) + await Fixture.Streams.AppendToStreamAsync( + stream, + StreamState.Any, + [@event] + ); + + await Fixture.Subscriptions.CreateToAllAsync(group, new(), userCredentials: TestCredentials.Root); + var subscription = Fixture.Subscriptions.SubscribeToAll(group, userCredentials: TestCredentials.Root); + + await Assert.ThrowsAsync( + () => subscription.Messages + .OfType() + .Where(e => !SystemStreams.IsSystemStream(e.ResolvedEvent.OriginalStreamId)) + .AnyAsync() + .AsTask() + .WithTimeout() + ); + } +} diff --git a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllConnectToExistingWithStartFromSetToEndPositionTests.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllConnectToExistingWithStartFromSetToEndPositionTests.cs new file mode 100644 index 000000000..effbd67c2 --- /dev/null +++ b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllConnectToExistingWithStartFromSetToEndPositionTests.cs @@ -0,0 +1,35 @@ +using EventStore.Client; +using Kurrent.Client.Tests.TestNode; + +namespace Kurrent.Client.Tests.PersistentSubscriptions; + +[Trait("Category", "Target:PersistentSubscriptions")] +public class SubscribeToAllConnectToExistingWithStartFromSetToEndPositionTests(ITestOutputHelper output, KurrentTemporaryFixture fixture) + : KurrentTemporaryTests(output, fixture) { + [RetryFact] + public async Task connect_to_existing_with_start_from_set_to_end_position() { + var group = Fixture.GetGroupName(); + var stream = Fixture.GetStreamName(); + + foreach (var @event in Fixture.CreateTestEvents(10)) { + await Fixture.Streams.AppendToStreamAsync( + stream, + StreamState.Any, + [@event] + ); + } + + await Fixture.Subscriptions.CreateToAllAsync(group, new(startFrom: Position.End), userCredentials: TestCredentials.Root); + + var subscription = Fixture.Subscriptions.SubscribeToAll(group, userCredentials: TestCredentials.Root); + + await Assert.ThrowsAsync( + () => subscription.Messages + .OfType() + .Where(e => !SystemStreams.IsSystemStream(e.ResolvedEvent.OriginalStreamId)) + .AnyAsync() + .AsTask() + .WithTimeout() + ); + } +} diff --git a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllTests.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllTests.cs index b699cec08..e7e67f51f 100644 --- a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllTests.cs +++ b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllTests.cs @@ -86,31 +86,6 @@ await Fixture.Streams.AppendToStreamAsync( Assert.Equal(events[0].Event.EventId, resolvedEvent.Event.EventId); } - [RetryFact] - public async Task connect_to_existing_with_start_from_not_set() { - var group = Fixture.GetGroupName(); - var stream = Fixture.GetStreamName(); - - foreach (var @event in Fixture.CreateTestEvents(10)) - await Fixture.Streams.AppendToStreamAsync( - stream, - StreamState.Any, - [@event] - ); - - await Fixture.Subscriptions.CreateToAllAsync(group, new(), userCredentials: TestCredentials.Root); - var subscription = Fixture.Subscriptions.SubscribeToAll(group, userCredentials: TestCredentials.Root); - - await Assert.ThrowsAsync( - () => subscription.Messages - .OfType() - .Where(e => !SystemStreams.IsSystemStream(e.ResolvedEvent.OriginalStreamId)) - .AnyAsync() - .AsTask() - .WithTimeout(TimeSpan.FromMilliseconds(250)) - ); - } - [RetryFact] public async Task connect_to_existing_with_start_from_not_set_then_event_written() { var group = Fixture.GetGroupName(); @@ -141,33 +116,6 @@ await Fixture.Streams.AppendToStreamAsync( Assert.Equal(expectedStreamId, resolvedEvent.Event.EventStreamId); } - [RetryFact] - public async Task connect_to_existing_with_start_from_set_to_end_position() { - var group = Fixture.GetGroupName(); - var stream = Fixture.GetStreamName(); - - foreach (var @event in Fixture.CreateTestEvents(10)) { - await Fixture.Streams.AppendToStreamAsync( - stream, - StreamState.Any, - [@event] - ); - } - - await Fixture.Subscriptions.CreateToAllAsync(group, new(startFrom: Position.End), userCredentials: TestCredentials.Root); - - var subscription = Fixture.Subscriptions.SubscribeToAll(group, userCredentials: TestCredentials.Root); - - await Assert.ThrowsAsync( - () => subscription.Messages - .OfType() - .Where(e => !SystemStreams.IsSystemStream(e.ResolvedEvent.OriginalStreamId)) - .AnyAsync() - .AsTask() - .WithTimeout(TimeSpan.FromMilliseconds(250)) - ); - } - [RetryFact] public async Task connect_to_existing_with_start_from_set_to_end_position_then_event_written() { var stream = Fixture.GetStreamName(); diff --git a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToStreamConnectToExistingWithStartFromBeginningTests.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToStreamConnectToExistingWithStartFromBeginningTests.cs new file mode 100644 index 000000000..61eb46eb0 --- /dev/null +++ b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToStreamConnectToExistingWithStartFromBeginningTests.cs @@ -0,0 +1,33 @@ +using EventStore.Client; +using Kurrent.Client.Tests.TestNode; + +namespace Kurrent.Client.Tests.PersistentSubscriptions; + +[Trait("Category", "Target:PersistentSubscriptions")] +public class SubscribeToStreamConnectToExistingWithStartFromBeginningTests(ITestOutputHelper output, KurrentTemporaryFixture fixture) + : KurrentTemporaryTests(output, fixture) { + [RetryFact] + public async Task connect_to_existing_with_start_from_beginning_and_no_streamconnect_to_existing_with_start_from_not_set_and_events_in_it() { + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + var events = Fixture.CreateTestEvents(10).ToArray(); + + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events); + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group, + new(), + userCredentials: TestCredentials.Root + ); + + await using var subscription = Fixture.Subscriptions.SubscribeToStream( + stream, + group, + userCredentials: TestCredentials.Root + ); + + await Assert.ThrowsAsync( + () => subscription.Messages.AnyAsync(message => message is PersistentSubscriptionMessage.Event).AsTask().WithTimeout() + ); + } +} diff --git a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToStreamTests.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToStreamTests.cs index 08533d1c3..7fc9779f5 100644 --- a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToStreamTests.cs +++ b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToStreamTests.cs @@ -112,31 +112,6 @@ await Fixture.Subscriptions.CreateToStreamAsync( Assert.Equal(eventId, resolvedEvent.Event.EventId); } - [RetryFact] - public async Task connect_to_existing_with_start_from_beginning_and_no_streamconnect_to_existing_with_start_from_not_set_and_events_in_it() { - var stream = Fixture.GetStreamName(); - var group = Fixture.GetGroupName(); - var events = Fixture.CreateTestEvents(10).ToArray(); - - await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events); - await Fixture.Subscriptions.CreateToStreamAsync( - stream, - group, - new(), - userCredentials: TestCredentials.Root - ); - - await using var subscription = Fixture.Subscriptions.SubscribeToStream( - stream, - group, - userCredentials: TestCredentials.Root - ); - - await Assert.ThrowsAsync( - () => subscription.Messages.AnyAsync(message => message is PersistentSubscriptionMessage.Event).AsTask().WithTimeout(TimeSpan.FromMilliseconds(250)) - ); - } - [RetryFact] public async Task connect_to_existing_with_start_from_not_set_and_events_in_it_then_event_written() { var stream = Fixture.GetStreamName(); diff --git a/test/Kurrent.Client.Tests/Projections/DisableProjectionTests.cs b/test/Kurrent.Client.Tests/ProjectionManagement/DisableProjectionTests.cs similarity index 94% rename from test/Kurrent.Client.Tests/Projections/DisableProjectionTests.cs rename to test/Kurrent.Client.Tests/ProjectionManagement/DisableProjectionTests.cs index 8db2eb293..15405cdcc 100644 --- a/test/Kurrent.Client.Tests/Projections/DisableProjectionTests.cs +++ b/test/Kurrent.Client.Tests/ProjectionManagement/DisableProjectionTests.cs @@ -2,7 +2,7 @@ namespace Kurrent.Client.Tests.Projections; -[Trait("Category", "Target:Projections")] +[Trait("Category", "Target:ProjectionManagement")] public class DisableProjectionTests(ITestOutputHelper output, DisableProjectionTests.CustomFixture fixture) : KurrentTemporaryTests(output, fixture) { [Fact] diff --git a/test/Kurrent.Client.Tests/Projections/EnableProjectionTests.cs b/test/Kurrent.Client.Tests/ProjectionManagement/EnableProjectionTests.cs similarity index 94% rename from test/Kurrent.Client.Tests/Projections/EnableProjectionTests.cs rename to test/Kurrent.Client.Tests/ProjectionManagement/EnableProjectionTests.cs index a321305b2..7261bfb9f 100644 --- a/test/Kurrent.Client.Tests/Projections/EnableProjectionTests.cs +++ b/test/Kurrent.Client.Tests/ProjectionManagement/EnableProjectionTests.cs @@ -2,7 +2,7 @@ namespace Kurrent.Client.Tests.Projections; -[Trait("Category", "Target:Projections")] +[Trait("Category", "Target:ProjectionManagement")] public class EnableProjectionTests(ITestOutputHelper output, EnableProjectionTests.CustomFixture fixture) : KurrentTemporaryTests(output, fixture) { [Fact] diff --git a/test/Kurrent.Client.Tests/Projections/GetProjectionResultTests.cs b/test/Kurrent.Client.Tests/ProjectionManagement/GetProjectionResultTests.cs similarity index 96% rename from test/Kurrent.Client.Tests/Projections/GetProjectionResultTests.cs rename to test/Kurrent.Client.Tests/ProjectionManagement/GetProjectionResultTests.cs index c4bf0441c..2b2c8ef08 100644 --- a/test/Kurrent.Client.Tests/Projections/GetProjectionResultTests.cs +++ b/test/Kurrent.Client.Tests/ProjectionManagement/GetProjectionResultTests.cs @@ -3,7 +3,7 @@ namespace Kurrent.Client.Tests.Projections; -[Trait("Category", "Target:Projections")] +[Trait("Category", "Target:ProjectionManagement")] public class GetProjectionResultTests(ITestOutputHelper output, GetProjectionResultTests.CustomFixture fixture) : KurrentTemporaryTests(output, fixture) { [Fact] diff --git a/test/Kurrent.Client.Tests/Projections/GetProjectionStateTests.cs b/test/Kurrent.Client.Tests/ProjectionManagement/GetProjectionStateTests.cs similarity index 96% rename from test/Kurrent.Client.Tests/Projections/GetProjectionStateTests.cs rename to test/Kurrent.Client.Tests/ProjectionManagement/GetProjectionStateTests.cs index 56476f7e3..f180813ff 100644 --- a/test/Kurrent.Client.Tests/Projections/GetProjectionStateTests.cs +++ b/test/Kurrent.Client.Tests/ProjectionManagement/GetProjectionStateTests.cs @@ -3,7 +3,7 @@ namespace Kurrent.Client.Tests.Projections; -[Trait("Category", "Target:Projections")] +[Trait("Category", "Target:ProjectionManagement")] public class GetProjectionStateTests(ITestOutputHelper output, GetProjectionStateTests.CustomFixture fixture) : KurrentTemporaryTests(output, fixture) { [Fact] diff --git a/test/Kurrent.Client.Tests/Projections/GetProjectionStatusTests.cs b/test/Kurrent.Client.Tests/ProjectionManagement/GetProjectionStatusTests.cs similarity index 93% rename from test/Kurrent.Client.Tests/Projections/GetProjectionStatusTests.cs rename to test/Kurrent.Client.Tests/ProjectionManagement/GetProjectionStatusTests.cs index 8fea4bce2..41ed02932 100644 --- a/test/Kurrent.Client.Tests/Projections/GetProjectionStatusTests.cs +++ b/test/Kurrent.Client.Tests/ProjectionManagement/GetProjectionStatusTests.cs @@ -2,7 +2,7 @@ namespace Kurrent.Client.Tests.Projections; -[Trait("Category", "Target:Projections")] +[Trait("Category", "Target:ProjectionManagement")] public class GetProjectionStatusTests(ITestOutputHelper output, GetProjectionStatusTests.CustomFixture fixture) : KurrentTemporaryTests(output, fixture) { [Fact] diff --git a/test/Kurrent.Client.Tests/Projections/ListAllProjectionsTests.cs b/test/Kurrent.Client.Tests/ProjectionManagement/ListAllProjectionsTests.cs similarity index 95% rename from test/Kurrent.Client.Tests/Projections/ListAllProjectionsTests.cs rename to test/Kurrent.Client.Tests/ProjectionManagement/ListAllProjectionsTests.cs index 10ff83c67..18f717efc 100644 --- a/test/Kurrent.Client.Tests/Projections/ListAllProjectionsTests.cs +++ b/test/Kurrent.Client.Tests/ProjectionManagement/ListAllProjectionsTests.cs @@ -4,7 +4,7 @@ namespace Kurrent.Client.Tests; -[Trait("Category", "Target:Projections")] +[Trait("Category", "Target:ProjectionManagement")] public class ListAllProjectionsTests(ITestOutputHelper output, ListAllProjectionsTests.CustomFixture fixture) : KurrentTemporaryTests(output, fixture) { [Fact] diff --git a/test/Kurrent.Client.Tests/Projections/ListContinuousProjectionsTests.cs b/test/Kurrent.Client.Tests/ProjectionManagement/ListContinuousProjectionsTests.cs similarity index 95% rename from test/Kurrent.Client.Tests/Projections/ListContinuousProjectionsTests.cs rename to test/Kurrent.Client.Tests/ProjectionManagement/ListContinuousProjectionsTests.cs index f6b0fbf81..774ad5a0f 100644 --- a/test/Kurrent.Client.Tests/Projections/ListContinuousProjectionsTests.cs +++ b/test/Kurrent.Client.Tests/ProjectionManagement/ListContinuousProjectionsTests.cs @@ -4,7 +4,7 @@ namespace Kurrent.Client.Tests; -[Trait("Category", "Target:Projections")] +[Trait("Category", "Target:ProjectionManagement")] public class ListContinuousProjectionsTests(ITestOutputHelper output, ListContinuousProjectionsTests.CustomFixture fixture) : KurrentTemporaryTests(output, fixture) { [Fact] diff --git a/test/Kurrent.Client.Tests/Projections/ListOneTimeProjectionsTests.cs b/test/Kurrent.Client.Tests/ProjectionManagement/ListOneTimeProjectionsTests.cs similarity index 94% rename from test/Kurrent.Client.Tests/Projections/ListOneTimeProjectionsTests.cs rename to test/Kurrent.Client.Tests/ProjectionManagement/ListOneTimeProjectionsTests.cs index fb1e537b9..d664d24fa 100644 --- a/test/Kurrent.Client.Tests/Projections/ListOneTimeProjectionsTests.cs +++ b/test/Kurrent.Client.Tests/ProjectionManagement/ListOneTimeProjectionsTests.cs @@ -2,7 +2,7 @@ namespace Kurrent.Client.Tests.Projections; -[Trait("Category", "Target:Projections")] +[Trait("Category", "Target:ProjectionManagement")] public class ListOneTimeProjectionsTests(ITestOutputHelper output, ListOneTimeProjectionsTests.CustomFixture fixture) : KurrentTemporaryTests(output, fixture) { [Fact] diff --git a/test/Kurrent.Client.Tests/Projections/ProjectionManagementTests.cs b/test/Kurrent.Client.Tests/ProjectionManagement/ProjectionManagementTests.cs similarity index 97% rename from test/Kurrent.Client.Tests/Projections/ProjectionManagementTests.cs rename to test/Kurrent.Client.Tests/ProjectionManagement/ProjectionManagementTests.cs index fe45326e6..fd8d9b42a 100644 --- a/test/Kurrent.Client.Tests/Projections/ProjectionManagementTests.cs +++ b/test/Kurrent.Client.Tests/ProjectionManagement/ProjectionManagementTests.cs @@ -5,7 +5,7 @@ namespace Kurrent.Client.Tests; -[Trait("Category", "Target:Projections")] +[Trait("Category", "Target:ProjectionManagement")] public class ProjectionManagementTests(ITestOutputHelper output, ProjectionManagementTests.CustomFixture fixture) : KurrentTemporaryTests(output, fixture) { [Fact] diff --git a/test/Kurrent.Client.Tests/Projections/ResetProjectionTests.cs b/test/Kurrent.Client.Tests/ProjectionManagement/ResetProjectionTests.cs similarity index 94% rename from test/Kurrent.Client.Tests/Projections/ResetProjectionTests.cs rename to test/Kurrent.Client.Tests/ProjectionManagement/ResetProjectionTests.cs index 4c8ef7c77..71d5f8fcc 100644 --- a/test/Kurrent.Client.Tests/Projections/ResetProjectionTests.cs +++ b/test/Kurrent.Client.Tests/ProjectionManagement/ResetProjectionTests.cs @@ -2,7 +2,7 @@ namespace Kurrent.Client.Tests.Projections; -[Trait("Category", "Target:Projections")] +[Trait("Category", "Target:ProjectionManagement")] public class ResetProjectionTests(ITestOutputHelper output, ResetProjectionTests.CustomFixture fixture) : KurrentTemporaryTests(output, fixture) { [Fact] diff --git a/test/Kurrent.Client.Tests/Projections/RestartSubsystemTests.cs b/test/Kurrent.Client.Tests/ProjectionManagement/RestartSubsystemTests.cs similarity index 94% rename from test/Kurrent.Client.Tests/Projections/RestartSubsystemTests.cs rename to test/Kurrent.Client.Tests/ProjectionManagement/RestartSubsystemTests.cs index 066d28d61..6431fbee5 100644 --- a/test/Kurrent.Client.Tests/Projections/RestartSubsystemTests.cs +++ b/test/Kurrent.Client.Tests/ProjectionManagement/RestartSubsystemTests.cs @@ -3,7 +3,7 @@ namespace Kurrent.Client.Tests.Projections; -[Trait("Category", "Target:Projections")] +[Trait("Category", "Target:ProjectionManagement")] public class RestartSubsystemTests(ITestOutputHelper output, RestartSubsystemTests.CustomFixture fixture) : KurrentTemporaryTests(output, fixture) { [Fact] diff --git a/test/Kurrent.Client.Tests/Projections/UpdateProjectionTests.cs b/test/Kurrent.Client.Tests/ProjectionManagement/UpdateProjectionTests.cs similarity index 94% rename from test/Kurrent.Client.Tests/Projections/UpdateProjectionTests.cs rename to test/Kurrent.Client.Tests/ProjectionManagement/UpdateProjectionTests.cs index 17c4ba896..1af8e880b 100644 --- a/test/Kurrent.Client.Tests/Projections/UpdateProjectionTests.cs +++ b/test/Kurrent.Client.Tests/ProjectionManagement/UpdateProjectionTests.cs @@ -2,7 +2,7 @@ namespace Kurrent.Client.Tests.Projections; -[Trait("Category", "Target:Projections")] +[Trait("Category", "Target:ProjectionManagement")] public class UpdateProjectionTests(ITestOutputHelper output, UpdateProjectionTests.CustomFixture fixture) : KurrentTemporaryTests(output, fixture) { [Theory] diff --git a/test/Kurrent.Client.Tests/UserManagement/ChangePasswordTests.cs b/test/Kurrent.Client.Tests/UserManagement/ChangePasswordTests.cs new file mode 100644 index 000000000..f393e24da --- /dev/null +++ b/test/Kurrent.Client.Tests/UserManagement/ChangePasswordTests.cs @@ -0,0 +1,69 @@ +using EventStore.Client; + +namespace Kurrent.Client.Tests; + +[Trait("Category", "Target:UserManagement")] +public class ChangePasswordTests(ITestOutputHelper output, KurrentPermanentFixture fixture) + : KurrentPermanentTests(output, fixture) { + [Theory, ChangePasswordNullInputCases] + public async Task changing_user_password_with_null_input_throws(string loginName, string currentPassword, string newPassword, string paramName) { + var ex = await Fixture.Users + .ChangePasswordAsync(loginName, currentPassword, newPassword, userCredentials: TestCredentials.Root) + .ShouldThrowAsync(); + + ex.ParamName.ShouldBe(paramName); + } + + [Theory, ChangePasswordEmptyInputCases] + public async Task changing_user_password_with_empty_input_throws(string loginName, string currentPassword, string newPassword, string paramName) { + var ex = await Fixture.Users + .ChangePasswordAsync(loginName, currentPassword, newPassword, userCredentials: TestCredentials.Root) + .ShouldThrowAsync(); + + ex.ParamName.ShouldBe(paramName); + } + + [Theory(Skip = "This can't be right")] + [ClassData(typeof(InvalidCredentialsTestCases))] + public async Task changing_user_password_with_user_with_insufficient_credentials_throws(string loginName, UserCredentials userCredentials) { + await Fixture.Users.CreateUserAsync(loginName, "Full Name", Array.Empty(), "password", userCredentials: TestCredentials.Root); + + await Fixture.Users + .ChangePasswordAsync(loginName, "password", "newPassword", userCredentials: userCredentials) + .ShouldThrowAsync(); + } + + [Fact] + public async Task changing_user_password_when_the_current_password_is_wrong_throws() { + var user = await Fixture.CreateTestUser(); + + await Fixture.Users + .ChangePasswordAsync(user.LoginName, "wrong-password", "new-password", userCredentials: TestCredentials.Root) + .ShouldThrowAsync(); + } + + [Fact] + public async Task changing_user_password_with_correct_credentials() { + var user = await Fixture.CreateTestUser(); + + await Fixture.Users + .ChangePasswordAsync(user.LoginName, user.Password, "new-password", userCredentials: TestCredentials.Root) + .ShouldNotThrowAsync(); + } + + class ChangePasswordNullInputCases : TestCaseGenerator { + protected override IEnumerable Data() { + yield return [null!, Faker.Internet.Password(), Faker.Internet.Password(), "loginName"]; + yield return [Faker.Person.UserName, null!, Faker.Internet.Password(), "currentPassword"]; + yield return [Faker.Person.UserName, Faker.Internet.Password(), null!, "newPassword"]; + } + } + + class ChangePasswordEmptyInputCases : TestCaseGenerator { + protected override IEnumerable Data() { + yield return [string.Empty, Faker.Internet.Password(), Faker.Internet.Password(), "loginName"]; + yield return [Faker.Person.UserName, string.Empty, Faker.Internet.Password(), "currentPassword"]; + yield return [Faker.Person.UserName, Faker.Internet.Password(), string.Empty, "newPassword"]; + } + } +} diff --git a/test/Kurrent.Client.Tests/UserManagement/CreateUserTests.cs b/test/Kurrent.Client.Tests/UserManagement/CreateUserTests.cs new file mode 100644 index 000000000..3096e58e5 --- /dev/null +++ b/test/Kurrent.Client.Tests/UserManagement/CreateUserTests.cs @@ -0,0 +1,93 @@ +using EventStore.Client; + +namespace Kurrent.Client.Tests; + +[Trait("Category", "Target:UserManagement")] +public class CreateUserTests(ITestOutputHelper output, CreateUserTests.CustomFixture fixture) + : KurrentPermanentTests(output, fixture) { + [Theory, CreateUserNullInputCases] + public async Task creating_user_with_null_input_throws(string loginName, string fullName, string[] groups, string password, string paramName) { + var ex = await Fixture.Users + .CreateUserAsync(loginName, fullName, groups, password, userCredentials: TestCredentials.Root) + .ShouldThrowAsync(); + + ex.ParamName.ShouldBe(paramName); + } + + [Theory, CreateUserEmptyInputCases] + public async Task creating_user_with_empty_input_throws(string loginName, string fullName, string[] groups, string password, string paramName) { + var ex = await Fixture.Users + .CreateUserAsync(loginName, fullName, groups, password, userCredentials: TestCredentials.Root) + .ShouldThrowAsync(); + + ex.ParamName.ShouldBe(paramName); + } + + [Fact] + public async Task creating_user_with_password_containing_ascii_chars() { + var user = Fakers.Users.Generate(); + + await Fixture.Users + .CreateUserAsync(user.LoginName, user.FullName, user.Groups, user.Password, userCredentials: TestCredentials.Root) + .ShouldNotThrowAsync(); + } + + [Theory] + [ClassData(typeof(InvalidCredentialsTestCases))] + public async Task creating_user_with_insufficient_credentials_throws(InvalidCredentialsTestCase testCase) => + await Fixture.Users + .CreateUserAsync( + testCase.User.LoginName, + testCase.User.FullName, + testCase.User.Groups, + testCase.User.Password, + userCredentials: testCase.User.Credentials + ) + .ShouldThrowAsync(testCase.ExpectedException); + + [Fact] + public async Task creating_user_can_be_read() { + var user = Fakers.Users.Generate(); + + await Fixture.Users + .CreateUserAsync( + user.LoginName, + user.FullName, + user.Groups, + user.Password, + userCredentials: TestCredentials.Root + ) + .ShouldNotThrowAsync(); + + var actual = await Fixture.Users.GetUserAsync(user.LoginName, userCredentials: TestCredentials.Root); + + var expected = new UserDetails( + user.Details.LoginName, + user.Details.FullName, + user.Details.Groups, + user.Details.Disabled, + actual.DateLastUpdated + ); + + actual.ShouldBeEquivalentTo(expected); + } + + class CreateUserNullInputCases : TestCaseGenerator { + protected override IEnumerable Data() { + yield return [null!, Faker.Person.UserName, Faker.Lorem.Words(), Faker.Internet.Password(), "loginName"]; + yield return [Faker.Person.UserName, null!, Faker.Lorem.Words(), Faker.Internet.Password(), "fullName"]; + yield return [Faker.Person.UserName, Faker.Person.FullName, null!, Faker.Internet.Password(), "groups"]; + yield return [Faker.Person.UserName, Faker.Person.FullName, Faker.Lorem.Words(), null!, "password"]; + } + } + + class CreateUserEmptyInputCases : TestCaseGenerator { + protected override IEnumerable Data() { + yield return [string.Empty, Faker.Person.UserName, Faker.Lorem.Words(), Faker.Internet.Password(), "loginName"]; + yield return [Faker.Person.UserName, string.Empty, Faker.Lorem.Words(), Faker.Internet.Password(), "fullName"]; + yield return [Faker.Person.UserName, Faker.Person.FullName, Faker.Lorem.Words(), string.Empty, "password"]; + } + } + + public class CustomFixture() : KurrentPermanentFixture(x => x.WithoutDefaultCredentials()); +} diff --git a/test/Kurrent.Client.Tests/UserManagement/DeleteUserTests.cs b/test/Kurrent.Client.Tests/UserManagement/DeleteUserTests.cs new file mode 100644 index 000000000..e8e4e4a9d --- /dev/null +++ b/test/Kurrent.Client.Tests/UserManagement/DeleteUserTests.cs @@ -0,0 +1,69 @@ +using EventStore.Client; + +namespace Kurrent.Client.Tests; + +[Trait("Category", "Target:UserManagement")] +public class DeleteUserTests(ITestOutputHelper output, DeleteUserTests.CustomFixture fixture) + : KurrentPermanentTests(output, fixture) { + [Fact] + public async Task with_null_input_throws() { + var ex = await Fixture.Users + .DeleteUserAsync(null!, userCredentials: TestCredentials.Root) + .ShouldThrowAsync(); + + ex.ParamName.ShouldBe("loginName"); + } + + [Fact] + public async Task with_empty_input_throws() { + var ex = await Fixture.Users + .DeleteUserAsync(string.Empty, userCredentials: TestCredentials.Root) + .ShouldThrowAsync(); + + ex.ParamName.ShouldBe("loginName"); + } + + [Theory] + [ClassData(typeof(InvalidCredentialsTestCases))] + public async Task with_user_with_insufficient_credentials_throws(InvalidCredentialsTestCase testCase) { + await Fixture.Users.CreateUserAsync( + testCase.User.LoginName, + testCase.User.FullName, + testCase.User.Groups, + testCase.User.Password, + userCredentials: TestCredentials.Root + ); + + await Fixture.Users + .DeleteUserAsync(testCase.User.LoginName, userCredentials: testCase.User.Credentials) + .ShouldThrowAsync(testCase.ExpectedException); + } + + [Fact] + public async Task cannot_be_read() { + var user = await Fixture.CreateTestUser(); + + await Fixture.Users.DeleteUserAsync(user.LoginName, userCredentials: TestCredentials.Root); + + var ex = await Fixture.Users + .GetUserAsync(user.LoginName, userCredentials: TestCredentials.Root) + .ShouldThrowAsync(); + + ex.LoginName.ShouldBe(user.LoginName); + } + + [Fact] + public async Task a_second_time_throws() { + var user = await Fixture.CreateTestUser(); + + await Fixture.Users.DeleteUserAsync(user.LoginName, userCredentials: TestCredentials.Root); + + var ex = await Fixture.Users + .DeleteUserAsync(user.LoginName, userCredentials: TestCredentials.Root) + .ShouldThrowAsync(); + + ex.LoginName.ShouldBe(user.LoginName); + } + + public class CustomFixture() : KurrentPermanentFixture(x => x.WithoutDefaultCredentials()); +} diff --git a/test/Kurrent.Client.Tests/UserManagement/DisableUserTests.cs b/test/Kurrent.Client.Tests/UserManagement/DisableUserTests.cs new file mode 100644 index 000000000..d4be66f86 --- /dev/null +++ b/test/Kurrent.Client.Tests/UserManagement/DisableUserTests.cs @@ -0,0 +1,64 @@ +namespace Kurrent.Client.Tests; + +[Trait("Category", "Target:UserManagement")] +public class DisableUserTests(ITestOutputHelper output, DisableUserTests.CustomFixture fixture) + : KurrentPermanentTests(output, fixture) { + [Fact] + public async Task with_null_input_throws() { + var ex = await Fixture.Users + .DisableUserAsync(null!, userCredentials: TestCredentials.Root) + .ShouldThrowAsync(); + + // must fix since it is returning value instead of param name + //ex.ParamName.ShouldBe("loginName"); + } + + [Fact] + public async Task with_empty_input_throws() { + var ex = await Fixture.Users + .DisableUserAsync(string.Empty, userCredentials: TestCredentials.Root) + .ShouldThrowAsync(); + + ex.ParamName.ShouldBe("loginName"); + } + + [Theory] + [ClassData(typeof(InvalidCredentialsTestCases))] + public async Task with_user_with_insufficient_credentials_throws(InvalidCredentialsTestCase testCase) { + await Fixture.Users.CreateUserAsync( + testCase.User.LoginName, + testCase.User.FullName, + testCase.User.Groups, + testCase.User.Password, + userCredentials: TestCredentials.Root + ); + + await Fixture.Users + .DisableUserAsync(testCase.User.LoginName, userCredentials: testCase.User.Credentials) + .ShouldThrowAsync(testCase.ExpectedException); + } + + [Fact] + public async Task that_was_disabled() { + var user = await Fixture.CreateTestUser(); + + await Fixture.Users + .DisableUserAsync(user.LoginName, userCredentials: TestCredentials.Root) + .ShouldNotThrowAsync(); + + await Fixture.Users + .DisableUserAsync(user.LoginName, userCredentials: TestCredentials.Root) + .ShouldNotThrowAsync(); + } + + [Fact] + public async Task that_is_enabled() { + var user = await Fixture.CreateTestUser(); + + await Fixture.Users + .DisableUserAsync(user.LoginName, userCredentials: TestCredentials.Root) + .ShouldNotThrowAsync(); + } + + public class CustomFixture() : KurrentPermanentFixture(x => x.WithoutDefaultCredentials()); +} diff --git a/test/Kurrent.Client.Tests/UserManagement/EnableUserTests.cs b/test/Kurrent.Client.Tests/UserManagement/EnableUserTests.cs new file mode 100644 index 000000000..7392a0dbe --- /dev/null +++ b/test/Kurrent.Client.Tests/UserManagement/EnableUserTests.cs @@ -0,0 +1,61 @@ +namespace Kurrent.Client.Tests; + +[Trait("Category", "Target:UserManagement")] +public class EnableUserTests(ITestOutputHelper output, EnableUserTests.CustomFixture fixture) + : KurrentPermanentTests(output, fixture) { + [Fact] + public async Task with_null_input_throws() { + var ex = await Fixture.Users + .EnableUserAsync(null!, userCredentials: TestCredentials.Root) + .ShouldThrowAsync(); + + ex.ParamName.ShouldBe("loginName"); + } + + [Fact] + public async Task with_empty_input_throws() { + var ex = await Fixture.Users + .EnableUserAsync(string.Empty, userCredentials: TestCredentials.Root) + .ShouldThrowAsync(); + + ex.ParamName.ShouldBe("loginName"); + } + + [Theory] + [ClassData(typeof(InvalidCredentialsTestCases))] + public async Task with_user_with_insufficient_credentials_throws(InvalidCredentialsTestCase testCase) { + await Fixture.Users.CreateUserAsync( + testCase.User.LoginName, + testCase.User.FullName, + testCase.User.Groups, + testCase.User.Password, + userCredentials: TestCredentials.Root + ); + + await Fixture.Users + .EnableUserAsync(testCase.User.LoginName, userCredentials: testCase.User.Credentials) + .ShouldThrowAsync(testCase.ExpectedException); + } + + [Fact] + public async Task that_was_disabled() { + var user = await Fixture.CreateTestUser(); + + await Fixture.Users.DisableUserAsync(user.LoginName, userCredentials: TestCredentials.Root); + + await Fixture.Users + .EnableUserAsync(user.LoginName, userCredentials: TestCredentials.Root) + .ShouldNotThrowAsync(); + } + + [Fact] + public async Task that_is_enabled() { + var user = await Fixture.CreateTestUser(); + + await Fixture.Users + .EnableUserAsync(user.LoginName, userCredentials: TestCredentials.Root) + .ShouldNotThrowAsync(); + } + + public class CustomFixture() : KurrentPermanentFixture(x => x.WithoutDefaultCredentials()); +} diff --git a/test/Kurrent.Client.Tests/UserManagement/GetCurrentUserTests.cs b/test/Kurrent.Client.Tests/UserManagement/GetCurrentUserTests.cs new file mode 100644 index 000000000..bb91c1a64 --- /dev/null +++ b/test/Kurrent.Client.Tests/UserManagement/GetCurrentUserTests.cs @@ -0,0 +1,12 @@ +using EventStore.Client; + +namespace Kurrent.Client.Tests; + +[Trait("Category", "Target:UserManagement")] +public class GetCurrentUserTests(ITestOutputHelper output, KurrentPermanentFixture fixture) : KurrentPermanentTests(output, fixture) { + [Fact] + public async Task returns_the_current_user() { + var user = await Fixture.Users.GetCurrentUserAsync(TestCredentials.Root); + user.LoginName.ShouldBe(TestCredentials.Root.Username); + } +} diff --git a/test/Kurrent.Client.Tests/UserManagement/ListUserTests.cs b/test/Kurrent.Client.Tests/UserManagement/ListUserTests.cs new file mode 100644 index 000000000..8cf4d1ad1 --- /dev/null +++ b/test/Kurrent.Client.Tests/UserManagement/ListUserTests.cs @@ -0,0 +1,40 @@ +using EventStore.Client; + +namespace Kurrent.Client.Tests; + +[Trait("Category", "Target:UserManagement")] +public class ListUserTests(ITestOutputHelper output, KurrentPermanentFixture fixture) : KurrentPermanentTests(output, fixture) { + [Fact] + public async Task returns_all_created_users() { + var seed = await Fixture.CreateTestUsers(); + + var admin = new UserDetails("admin", "Event Store Administrator", new[] { "$admins" }, false, default); + var ops = new UserDetails("ops", "Event Store Operations", new[] { "$ops" }, false, default); + + var expected = new[] { admin, ops } + .Concat(seed.Select(user => user.Details)) + .ToArray(); + + var actual = await Fixture.Users + .ListAllAsync(userCredentials: TestCredentials.Root) + .Select(user => new UserDetails(user.LoginName, user.FullName, user.Groups, user.Disabled, default)) + .ToArrayAsync(); + + expected.ShouldBeSubsetOf(actual); + } + + [Fact] + public async Task returns_all_system_users() { + var admin = new UserDetails("admin", "Event Store Administrator", new[] { "$admins" }, false, default); + var ops = new UserDetails("ops", "Event Store Operations", new[] { "$ops" }, false, default); + + var expected = new[] { admin, ops }; + + var actual = await Fixture.Users + .ListAllAsync(userCredentials: TestCredentials.Root) + .Select(user => new UserDetails(user.LoginName, user.FullName, user.Groups, user.Disabled, default)) + .ToArrayAsync(); + + expected.ShouldBeSubsetOf(actual); + } +} diff --git a/test/Kurrent.Client.Tests/UserManagement/ResettingUserPasswordTests.cs b/test/Kurrent.Client.Tests/UserManagement/ResettingUserPasswordTests.cs new file mode 100644 index 000000000..39b0bf718 --- /dev/null +++ b/test/Kurrent.Client.Tests/UserManagement/ResettingUserPasswordTests.cs @@ -0,0 +1,81 @@ +using EventStore.Client; + +namespace Kurrent.Client.Tests; + +[Trait("Category", "Target:UserManagement")] +public class ResettingUserPasswordTests(ITestOutputHelper output, ResettingUserPasswordTests.CustomFixture fixture) + : KurrentPermanentTests(output, fixture) { + public static IEnumerable NullInputCases() { + yield return Fakers.Users.Generate().WithResult(x => new object?[] { null, x.Password, "loginName" }); + yield return Fakers.Users.Generate().WithResult(x => new object?[] { x.LoginName, null, "newPassword" }); + } + + [Theory] + [MemberData(nameof(NullInputCases))] + public async Task with_null_input_throws(string loginName, string newPassword, string paramName) { + var ex = await Fixture.Users + .ResetPasswordAsync(loginName, newPassword, userCredentials: TestCredentials.Root) + .ShouldThrowAsync(); + + ex.ParamName.ShouldBe(paramName); + } + + public static IEnumerable EmptyInputCases() { + yield return Fakers.Users.Generate().WithResult(x => new object?[] { string.Empty, x.Password, "loginName" }); + yield return Fakers.Users.Generate().WithResult(x => new object?[] { x.LoginName, string.Empty, "newPassword" }); + } + + [Theory] + [MemberData(nameof(EmptyInputCases))] + public async Task with_empty_input_throws(string loginName, string newPassword, string paramName) { + var ex = await Fixture.Users + .ResetPasswordAsync(loginName, newPassword, userCredentials: TestCredentials.Root) + .ShouldThrowAsync(); + + ex.ParamName.ShouldBe(paramName); + } + + [Theory] + [ClassData(typeof(InvalidCredentialsTestCases))] + public async Task with_user_with_insufficient_credentials_throws(InvalidCredentialsTestCase testCase) { + await Fixture.Users.CreateUserAsync( + testCase.User.LoginName, + testCase.User.FullName, + testCase.User.Groups, + testCase.User.Password, + userCredentials: TestCredentials.Root + ); + + await Fixture.Users + .ResetPasswordAsync(testCase.User.LoginName, "newPassword", userCredentials: testCase.User.Credentials) + .ShouldThrowAsync(testCase.ExpectedException); + } + + [Fact] + public async Task with_correct_credentials() { + var user = Fakers.Users.Generate(); + + await Fixture.Users.CreateUserAsync( + user.LoginName, + user.FullName, + user.Groups, + user.Password, + userCredentials: TestCredentials.Root + ); + + await Fixture.Users + .ResetPasswordAsync(user.LoginName, "new-password", userCredentials: TestCredentials.Root) + .ShouldNotThrowAsync(); + } + + [Fact] + public async Task with_own_credentials_throws() { + var user = await Fixture.CreateTestUser(); + + await Fixture.Users + .ResetPasswordAsync(user.LoginName, "new-password", userCredentials: user.Credentials) + .ShouldThrowAsync(); + } + + public class CustomFixture() : KurrentPermanentFixture(x => x.WithoutDefaultCredentials()); +} diff --git a/test/Kurrent.Client.Tests/UserManagement/UserCredentialsTests.cs b/test/Kurrent.Client.Tests/UserManagement/UserCredentialsTests.cs new file mode 100644 index 000000000..e4ffbe456 --- /dev/null +++ b/test/Kurrent.Client.Tests/UserManagement/UserCredentialsTests.cs @@ -0,0 +1,44 @@ +using System.Net.Http.Headers; +using System.Text; +using EventStore.Client; +using static System.Convert; + +namespace Kurrent.Client.Tests; + +[Trait("Category", "Target:UserManagement")] +public class UserCredentialsTests { + const string JwtToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." + + "eyJzdWIiOiI5OSIsIm5hbWUiOiJKb2huIFdpY2siLCJpYXQiOjE1MTYyMzkwMjJ9." + + "MEdv44JIdlLh-GgqxOTZD7DHq28xJowhQFmDnT3NDIE"; + + static readonly UTF8Encoding Utf8NoBom = new(false); + + static string EncodeCredentials(string username, string password) => ToBase64String(Utf8NoBom.GetBytes($"{username}:{password}")); + + [Fact] + public void from_username_and_password() { + var user = Fakers.Users.WithNonAsciiPassword(); + + var value = new AuthenticationHeaderValue( + Constants.Headers.BasicScheme, + EncodeCredentials(user.LoginName, user.Password) + ); + + var basicAuthInfo = value.ToString(); + + var credentials = new UserCredentials(user.LoginName, user.Password); + + credentials.Username.ShouldBe(user.LoginName); + credentials.Password.ShouldBe(user.Password); + credentials.ToString().ShouldBe(basicAuthInfo); + } + + [Fact] + public void from_bearer_token() { + var credentials = new UserCredentials(JwtToken); + + credentials.Username.ShouldBeNull(); + credentials.Password.ShouldBeNull(); + credentials.ToString().ShouldBe($"Bearer {JwtToken}"); + } +} From d613ba3d2ad17bedd1d65bfc486f00707b847070 Mon Sep 17 00:00:00 2001 From: William Chong Date: Wed, 8 Jan 2025 16:52:57 +0400 Subject: [PATCH 12/15] Publish actions --- .github/workflows/base.yml | 2 +- .github/workflows/publish.yml | 393 ++++---- samples/Directory.Build.props | 2 +- samples/Samples.sln | 19 +- samples/appending-events/Program.cs | 14 +- .../appending-events/appending-events.csproj | 3 +- samples/connecting-to-a-cluster/Program.cs | 18 +- .../connecting-to-a-cluster.csproj | 3 +- .../connecting-to-a-single-node/Program.cs | 27 +- .../connecting-to-a-single-node.csproj | 3 +- samples/diagnostics/Program.cs | 9 +- samples/diagnostics/diagnostics.csproj | 5 +- samples/persistent-subscriptions/Program.cs | 36 +- .../persistent-subscriptions.csproj | 3 +- samples/projection-management/Program.cs | 52 +- .../projection-management.csproj | 3 +- samples/quick-start/Program.cs | 6 +- samples/quick-start/quick-start.csproj | 3 +- samples/reading-events/Program.cs | 34 +- samples/reading-events/reading-events.csproj | 3 +- samples/secure-with-tls/Program.cs | 4 +- .../secure-with-tls/secure-with-tls.csproj | 2 +- samples/server-side-filtering/Program.cs | 16 +- .../server-side-filtering.csproj | 3 +- .../Controllers/EventStoreController.cs | 8 +- .../Startup.cs | 4 +- .../setting-up-dependency-injection.csproj | 2 +- samples/subscribing-to-streams/Program.cs | 24 +- .../subscribing-to-streams.csproj | 3 +- samples/user-certificates/Program.cs | 4 +- .../user-certificates.csproj | 2 +- ...ore.Client.Extensions.OpenTelemetry.csproj | 20 - .../TracerProviderBuilderExtensions.cs | 19 - .../Core/Certificates/X509Certificates.cs | 114 --- .../Core/ChannelBaseExtensions.cs | 10 - src/EventStore.Client/Core/ChannelCache.cs | 146 --- src/EventStore.Client/Core/ChannelFactory.cs | 106 -- src/EventStore.Client/Core/ChannelInfo.cs | 9 - src/EventStore.Client/Core/ChannelSelector.cs | 24 - src/EventStore.Client/Core/ClusterMessage.cs | 28 - .../Common/AsyncStreamReaderExtensions.cs | 29 - .../Core/Common/Constants.cs | 62 -- .../Diagnostics/ActivitySourceExtensions.cs | 90 -- .../ActivityTagsCollectionExtensions.cs | 37 - .../Diagnostics/Core/ActivityExtensions.cs | 52 - .../Common/Diagnostics/Core/ActivityStatus.cs | 13 - .../Core/ActivityStatusCodeHelper.cs | 24 - .../Core/ActivityTagsCollectionExtensions.cs | 25 - .../Diagnostics/Core/ExceptionExtensions.cs | 25 - .../Core/Telemetry/TelemetryTags.cs | 35 - .../Core/Tracing/TracingConstants.cs | 10 - .../Core/Tracing/TracingMetadata.cs | 32 - .../Diagnostics/EventMetadataExtensions.cs | 84 -- .../EventStoreClientDiagnostics.cs | 8 - .../Diagnostics/Telemetry/TelemetryTags.cs | 12 - .../Diagnostics/Tracing/TracingConstants.cs | 10 - .../Core/Common/EnumerableTaskExtensions.cs | 11 - .../Core/Common/EpochExtensions.cs | 25 - .../Core/Common/EventStoreCallOptions.cs | 74 -- .../Core/Common/MetadataExtensions.cs | 29 - .../Core/Common/Shims/Index.cs | 110 -- .../Core/Common/Shims/IsExternalInit.cs | 10 - .../Core/Common/Shims/Range.cs | 75 -- .../Core/Common/Shims/TaskCompletionSource.cs | 11 - .../Core/DefaultRequestVersionHandler.cs | 14 - .../Core/EndPointExtensions.cs | 41 - src/EventStore.Client/Core/EventData.cs | 65 -- src/EventStore.Client/Core/EventRecord.cs | 83 -- .../Core/EventStoreClientBase.cs | 151 --- .../EventStoreClientConnectivitySettings.cs | 128 --- .../Core/EventStoreClientOperationOptions.cs | 46 - ...entStoreClientSettings.ConnectionString.cs | 410 -------- .../Core/EventStoreClientSettings.cs | 61 -- src/EventStore.Client/Core/EventTypeFilter.cs | 145 --- .../Core/Exceptions/AccessDeniedException.cs | 22 - .../ConnectionStringParseException.cs | 15 - .../ConnectionString/DuplicateKeyException.cs | 13 - .../InvalidClientCertificateException.cs | 14 - .../ConnectionString/InvalidHostException.cs | 13 - .../InvalidKeyValuePairException.cs | 13 - .../InvalidSchemeException.cs | 14 - .../InvalidSettingException.cs | 12 - .../InvalidUserCredentialsException.cs | 13 - .../ConnectionString/NoSchemeException.cs | 12 - .../Core/Exceptions/DiscoveryException.cs | 33 - .../Exceptions/NotAuthenticatedException.cs | 16 - .../Core/Exceptions/NotLeaderException.cs | 26 - ...equiredMetadataPropertyMissingException.cs | 18 - .../Exceptions/ScavengeNotFoundException.cs | 23 - .../Core/Exceptions/StreamDeletedException.cs | 24 - .../Exceptions/StreamNotFoundException.cs | 23 - .../Core/Exceptions/UserNotFoundException.cs | 23 - .../WrongExpectedVersionException.cs | 66 -- src/EventStore.Client/Core/FromAll.cs | 100 -- src/EventStore.Client/Core/FromStream.cs | 100 -- .../Core/GossipChannelSelector.cs | 99 -- .../Core/GrpcGossipClient.cs | 30 - .../Core/GrpcServerCapabilitiesClient.cs | 75 -- src/EventStore.Client/Core/HashCode.cs | 37 - src/EventStore.Client/Core/HttpFallback.cs | 143 --- .../Core/IChannelSelector.cs | 14 - src/EventStore.Client/Core/IEventFilter.cs | 21 - src/EventStore.Client/Core/IGossipClient.cs | 10 - src/EventStore.Client/Core/IPosition.cs | 7 - .../Core/IServerCapabilitiesClient.cs | 9 - .../Interceptors/ConnectionNameInterceptor.cs | 45 - .../Interceptors/ReportLeaderInterceptor.cs | 126 --- .../Interceptors/TypedExceptionInterceptor.cs | 165 --- src/EventStore.Client/Core/NodePreference.cs | 26 - .../Core/NodePreferenceComparers.cs | 49 - src/EventStore.Client/Core/NodeSelector.cs | 63 -- src/EventStore.Client/Core/Position.cs | 200 ---- .../Core/PrefixFilterExpression.cs | 64 -- .../Core/ReconnectionRequired.cs | 15 - .../Core/RegularFilterExpression.cs | 86 -- src/EventStore.Client/Core/ResolvedEvent.cs | 60 -- .../Core/ServerCapabilities.cs | 10 - src/EventStore.Client/Core/SharingProvider.cs | 111 -- .../Core/SingleNodeChannelSelector.cs | 36 - .../Core/SingleNodeHttpHandler.cs | 22 - src/EventStore.Client/Core/StreamFilter.cs | 134 --- .../Core/StreamIdentifier.cs | 28 - src/EventStore.Client/Core/StreamPosition.cs | 201 ---- src/EventStore.Client/Core/StreamRevision.cs | 196 ---- src/EventStore.Client/Core/StreamState.cs | 88 -- .../Core/SubscriptionDroppedReason.cs | 19 - src/EventStore.Client/Core/SystemRoles.cs | 21 - src/EventStore.Client/Core/SystemStreams.cs | 54 - src/EventStore.Client/Core/TaskExtensions.cs | 22 - src/EventStore.Client/Core/UserCredentials.cs | 54 - src/EventStore.Client/Core/Uuid.cs | 203 ---- src/EventStore.Client/Core/protos/code.proto | 186 ---- .../Core/protos/gossip.proto | 44 - .../Core/protos/operations.proto | 45 - .../Core/protos/persistentsubscriptions.proto | 370 ------- .../Core/protos/projectionmanagement.proto | 174 ---- .../Core/protos/serverfeatures.proto | 19 - .../Core/protos/shared.proto | 61 -- .../Core/protos/status.proto | 48 - .../Core/protos/streams.proto | 316 ------ .../Core/protos/usermanagement.proto | 119 --- .../EventStore.Client.csproj | 57 -- .../EventStore.Client.csproj.DotSettings | 5 - .../Operations/DatabaseScavengeResult.cs | 81 -- .../EventStoreOperationsClient.Admin.cs | 103 -- .../EventStoreOperationsClient.Scavenge.cs | 82 -- .../Operations/EventStoreOperationsClient.cs | 33 - ...ationsClientServiceCollectionExtensions.cs | 74 -- .../Operations/ScavengeResult.cs | 25 - ...orePersistentSubscriptionsClient.Create.cs | 252 ----- ...orePersistentSubscriptionsClient.Delete.cs | 55 - ...StorePersistentSubscriptionsClient.Info.cs | 77 -- ...StorePersistentSubscriptionsClient.List.cs | 111 -- ...StorePersistentSubscriptionsClient.Read.cs | 478 --------- ...sistentSubscriptionsClient.ReplayParked.cs | 94 -- ...entSubscriptionsClient.RestartSubsystem.cs | 32 - ...orePersistentSubscriptionsClient.Update.cs | 165 --- ...EventStorePersistentSubscriptionsClient.cs | 46 - ...SubscriptionsClientCollectionExtensions.cs | 61 -- .../MaximumSubscribersReachedException.cs | 30 - .../PersistentSubscription.cs | 205 ---- ...entSubscriptionDroppedByServerException.cs | 31 - .../PersistentSubscriptionExtraStatistic.cs | 23 - .../PersistentSubscriptionInfo.cs | 224 ----- .../PersistentSubscriptionMessage.cs | 33 - .../PersistentSubscriptionNakEventAction.cs | 31 - ...PersistentSubscriptionNotFoundException.cs | 29 - .../PersistentSubscriptionSettings.cs | 112 --- .../SystemConsumerStrategies.cs | 22 - ...StoreProjectionManagementClient.Control.cs | 102 -- ...tStoreProjectionManagementClient.Create.cs | 80 -- ...ntStoreProjectionManagementClient.State.cs | 205 ---- ...reProjectionManagementClient.Statistics.cs | 104 -- ...tStoreProjectionManagementClient.Update.cs | 40 - .../EventStoreProjectionManagementClient.cs | 32 - ...ionManagementClientCollectionExtensions.cs | 74 -- .../ProjectionManagement/ProjectionDetails.cs | 164 --- .../Streams/ConditionalWriteResult.cs | 87 -- .../Streams/ConditionalWriteStatus.cs | 21 - src/EventStore.Client/Streams/DeadLine.cs | 12 - src/EventStore.Client/Streams/DeleteResult.cs | 46 - src/EventStore.Client/Streams/Direction.cs | 16 - .../Streams/EventStoreClient.Append.cs | 430 -------- .../Streams/EventStoreClient.Delete.cs | 65 -- .../Streams/EventStoreClient.Metadata.cs | 106 -- .../Streams/EventStoreClient.Read.cs | 468 --------- .../Streams/EventStoreClient.Subscriptions.cs | 302 ------ .../Streams/EventStoreClient.Tombstone.cs | 63 -- .../Streams/EventStoreClient.cs | 166 --- .../Streams/EventStoreClientExtensions.cs | 109 -- ...tStoreClientServiceCollectionExtensions.cs | 153 --- src/EventStore.Client/Streams/IWriteResult.cs | 23 - .../Streams/InvalidTransactionException.cs | 31 - .../MaximumAppendSizeExceededException.cs | 33 - src/EventStore.Client/Streams/ReadState.cs | 15 - src/EventStore.Client/Streams/StreamAcl.cs | 107 -- .../Streams/StreamAclJsonConverter.cs | 111 -- .../Streams/StreamMessage.cs | 83 -- .../Streams/StreamMetadata.cs | 111 -- .../Streams/StreamMetadataJsonConverter.cs | 145 --- .../Streams/StreamMetadataResult.cs | 83 -- .../Streams/StreamSubscription.cs | 165 --- .../Streams/Streams/AppendReq.cs | 15 - .../Streams/Streams/BatchAppendReq.cs | 34 - .../Streams/Streams/BatchAppendResp.cs | 49 - .../Streams/Streams/DeleteReq.cs | 15 - .../Streams/Streams/ReadReq.cs | 86 -- .../Streams/Streams/TombstoneReq.cs | 15 - .../Streams/SubscriptionFilterOptions.cs | 54 - .../Streams/SuccessResult.cs | 57 -- .../Streams/SystemEventTypes.cs | 31 - .../Streams/SystemMetadata.cs | 71 -- .../Streams/SystemSettings.cs | 53 - .../Streams/SystemSettingsJsonConverter.cs | 62 -- .../Streams/WriteResultExtensions.cs | 12 - .../Streams/WrongExpectedVersionResult.cs | 58 -- .../EventStoreUserManagementClient.cs | 279 ------ ...serManagementClientCollectionExtensions.cs | 72 -- .../EventStoreUserManagerClientExtensions.cs | 23 - .../UserManagement/UserDetails.cs | 97 -- .../TracerProviderBuilderExtensions.cs | 2 +- .../Fixtures/KurrentFixtureOptions.cs | 3 + ...xistingWithStartFromNotSetObsoleteTests.cs | 51 + ...hStartFromSetToEndPositionObsoleteTests.cs | 46 + ...nectWithoutReadPermissionsObsoleteTests.cs | 34 + .../SubscribeToAllFilterObsoleteTests.cs | 131 +++ .../SubscribeToAllGetInfoObsoleteTests.cs | 97 ++ ...stWithIncorrectCredentialsObsoleteTests.cs | 64 ++ ...eToAllNoDefaultCredentialsObsoleteTests.cs | 103 ++ .../Obsolete/SubscribeToAllObsoleteTests.cs | 786 +++++++++++++++ ...SubscribeToAllReplayParkedObsoleteTests.cs | 87 ++ ...tWithNormalUserCredentialsObsoleteTests.cs | 35 + ...AllReturnsAllSubscriptionsObsoleteTests.cs | 42 + ...nsSubscriptionsToAllStreamObsoleteTests.cs | 36 + ...dateExistingWithCheckpointObsoleteTests.cs | 88 ++ .../SubscribeToAllWithoutPSObsoleteTests.cs | 17 + ...nnectToExistingWithStartFromNotSetTests.cs | 2 +- ...stingWithStartFromSetToEndPositionTests.cs | 0 ...ToAllConnectWithoutReadPermissionsTests.cs | 0 .../SubscribeToAllFilterTests.cs | 0 .../SubscribeToAllGetInfoTests.cs | 0 ...eToAllListWithIncorrectCredentialsTests.cs | 0 ...SubscribeToAllNoDefaultCredentialsTests.cs | 0 .../SubscribeToAllReplayParkedTests.cs | 0 ...AllResultWithNormalUserCredentialsTests.cs | 0 .../SubscribeToAllReturnsAllSubscriptions.cs | 0 ...AllReturnsSubscriptionsToAllStreamTests.cs | 0 .../SubscribeToAllTests.cs | 0 ...beToAllUpdateExistingWithCheckpointTest.cs | 0 .../SubscribeToAllWithoutPSTests.cs | 0 ...oExistingWithoutPermissionObsoleteTests.cs | 36 + .../SubscribeToStreamGetInfoObsoleteTests.cs | 201 ++++ .../SubscribeToStreamObsoleteTests.cs | 948 ++++++++++++++++++ ...ctToExistingWithStartFromBeginningTests.cs | 0 .../SubscribeToStreamGetInfoTests.cs | 10 +- .../SubscribeToStreamListTests.cs | 0 ...scribeToStreamNoDefaultCredentialsTests.cs | 0 .../SubscribeToStreamReplayParkedTests.cs | 0 .../SubscribeToStreamTests.cs | 0 .../Obsolete/SubscribeToAllObsoleteTests.cs | 616 ++++++++++++ .../SubscribeToStreamObsoleteTests.cs | 322 ++++++ .../Subscriptions/SubscribeToAllTests.cs | 497 +++++++++ .../SubscribeToStreamTests.cs | 0 .../SubscriptionDroppedResult.cs | 18 + 264 files changed, 4616 insertions(+), 14803 deletions(-) delete mode 100644 src/EventStore.Client.Extensions.OpenTelemetry/EventStore.Client.Extensions.OpenTelemetry.csproj delete mode 100644 src/EventStore.Client.Extensions.OpenTelemetry/TracerProviderBuilderExtensions.cs delete mode 100644 src/EventStore.Client/Core/Certificates/X509Certificates.cs delete mode 100644 src/EventStore.Client/Core/ChannelBaseExtensions.cs delete mode 100644 src/EventStore.Client/Core/ChannelCache.cs delete mode 100644 src/EventStore.Client/Core/ChannelFactory.cs delete mode 100644 src/EventStore.Client/Core/ChannelInfo.cs delete mode 100644 src/EventStore.Client/Core/ChannelSelector.cs delete mode 100644 src/EventStore.Client/Core/ClusterMessage.cs delete mode 100644 src/EventStore.Client/Core/Common/AsyncStreamReaderExtensions.cs delete mode 100644 src/EventStore.Client/Core/Common/Constants.cs delete mode 100644 src/EventStore.Client/Core/Common/Diagnostics/ActivitySourceExtensions.cs delete mode 100644 src/EventStore.Client/Core/Common/Diagnostics/ActivityTagsCollectionExtensions.cs delete mode 100644 src/EventStore.Client/Core/Common/Diagnostics/Core/ActivityExtensions.cs delete mode 100644 src/EventStore.Client/Core/Common/Diagnostics/Core/ActivityStatus.cs delete mode 100644 src/EventStore.Client/Core/Common/Diagnostics/Core/ActivityStatusCodeHelper.cs delete mode 100644 src/EventStore.Client/Core/Common/Diagnostics/Core/ActivityTagsCollectionExtensions.cs delete mode 100644 src/EventStore.Client/Core/Common/Diagnostics/Core/ExceptionExtensions.cs delete mode 100644 src/EventStore.Client/Core/Common/Diagnostics/Core/Telemetry/TelemetryTags.cs delete mode 100644 src/EventStore.Client/Core/Common/Diagnostics/Core/Tracing/TracingConstants.cs delete mode 100644 src/EventStore.Client/Core/Common/Diagnostics/Core/Tracing/TracingMetadata.cs delete mode 100644 src/EventStore.Client/Core/Common/Diagnostics/EventMetadataExtensions.cs delete mode 100644 src/EventStore.Client/Core/Common/Diagnostics/EventStoreClientDiagnostics.cs delete mode 100644 src/EventStore.Client/Core/Common/Diagnostics/Telemetry/TelemetryTags.cs delete mode 100644 src/EventStore.Client/Core/Common/Diagnostics/Tracing/TracingConstants.cs delete mode 100644 src/EventStore.Client/Core/Common/EnumerableTaskExtensions.cs delete mode 100644 src/EventStore.Client/Core/Common/EpochExtensions.cs delete mode 100644 src/EventStore.Client/Core/Common/EventStoreCallOptions.cs delete mode 100644 src/EventStore.Client/Core/Common/MetadataExtensions.cs delete mode 100644 src/EventStore.Client/Core/Common/Shims/Index.cs delete mode 100644 src/EventStore.Client/Core/Common/Shims/IsExternalInit.cs delete mode 100644 src/EventStore.Client/Core/Common/Shims/Range.cs delete mode 100644 src/EventStore.Client/Core/Common/Shims/TaskCompletionSource.cs delete mode 100644 src/EventStore.Client/Core/DefaultRequestVersionHandler.cs delete mode 100644 src/EventStore.Client/Core/EndPointExtensions.cs delete mode 100644 src/EventStore.Client/Core/EventData.cs delete mode 100644 src/EventStore.Client/Core/EventRecord.cs delete mode 100644 src/EventStore.Client/Core/EventStoreClientBase.cs delete mode 100644 src/EventStore.Client/Core/EventStoreClientConnectivitySettings.cs delete mode 100644 src/EventStore.Client/Core/EventStoreClientOperationOptions.cs delete mode 100644 src/EventStore.Client/Core/EventStoreClientSettings.ConnectionString.cs delete mode 100644 src/EventStore.Client/Core/EventStoreClientSettings.cs delete mode 100644 src/EventStore.Client/Core/EventTypeFilter.cs delete mode 100644 src/EventStore.Client/Core/Exceptions/AccessDeniedException.cs delete mode 100644 src/EventStore.Client/Core/Exceptions/ConnectionString/ConnectionStringParseException.cs delete mode 100644 src/EventStore.Client/Core/Exceptions/ConnectionString/DuplicateKeyException.cs delete mode 100644 src/EventStore.Client/Core/Exceptions/ConnectionString/InvalidClientCertificateException.cs delete mode 100644 src/EventStore.Client/Core/Exceptions/ConnectionString/InvalidHostException.cs delete mode 100644 src/EventStore.Client/Core/Exceptions/ConnectionString/InvalidKeyValuePairException.cs delete mode 100644 src/EventStore.Client/Core/Exceptions/ConnectionString/InvalidSchemeException.cs delete mode 100644 src/EventStore.Client/Core/Exceptions/ConnectionString/InvalidSettingException.cs delete mode 100644 src/EventStore.Client/Core/Exceptions/ConnectionString/InvalidUserCredentialsException.cs delete mode 100644 src/EventStore.Client/Core/Exceptions/ConnectionString/NoSchemeException.cs delete mode 100644 src/EventStore.Client/Core/Exceptions/DiscoveryException.cs delete mode 100644 src/EventStore.Client/Core/Exceptions/NotAuthenticatedException.cs delete mode 100644 src/EventStore.Client/Core/Exceptions/NotLeaderException.cs delete mode 100644 src/EventStore.Client/Core/Exceptions/RequiredMetadataPropertyMissingException.cs delete mode 100644 src/EventStore.Client/Core/Exceptions/ScavengeNotFoundException.cs delete mode 100644 src/EventStore.Client/Core/Exceptions/StreamDeletedException.cs delete mode 100644 src/EventStore.Client/Core/Exceptions/StreamNotFoundException.cs delete mode 100644 src/EventStore.Client/Core/Exceptions/UserNotFoundException.cs delete mode 100644 src/EventStore.Client/Core/Exceptions/WrongExpectedVersionException.cs delete mode 100644 src/EventStore.Client/Core/FromAll.cs delete mode 100644 src/EventStore.Client/Core/FromStream.cs delete mode 100644 src/EventStore.Client/Core/GossipChannelSelector.cs delete mode 100644 src/EventStore.Client/Core/GrpcGossipClient.cs delete mode 100644 src/EventStore.Client/Core/GrpcServerCapabilitiesClient.cs delete mode 100644 src/EventStore.Client/Core/HashCode.cs delete mode 100644 src/EventStore.Client/Core/HttpFallback.cs delete mode 100644 src/EventStore.Client/Core/IChannelSelector.cs delete mode 100644 src/EventStore.Client/Core/IEventFilter.cs delete mode 100644 src/EventStore.Client/Core/IGossipClient.cs delete mode 100644 src/EventStore.Client/Core/IPosition.cs delete mode 100644 src/EventStore.Client/Core/IServerCapabilitiesClient.cs delete mode 100644 src/EventStore.Client/Core/Interceptors/ConnectionNameInterceptor.cs delete mode 100644 src/EventStore.Client/Core/Interceptors/ReportLeaderInterceptor.cs delete mode 100644 src/EventStore.Client/Core/Interceptors/TypedExceptionInterceptor.cs delete mode 100644 src/EventStore.Client/Core/NodePreference.cs delete mode 100644 src/EventStore.Client/Core/NodePreferenceComparers.cs delete mode 100644 src/EventStore.Client/Core/NodeSelector.cs delete mode 100644 src/EventStore.Client/Core/Position.cs delete mode 100644 src/EventStore.Client/Core/PrefixFilterExpression.cs delete mode 100644 src/EventStore.Client/Core/ReconnectionRequired.cs delete mode 100644 src/EventStore.Client/Core/RegularFilterExpression.cs delete mode 100644 src/EventStore.Client/Core/ResolvedEvent.cs delete mode 100644 src/EventStore.Client/Core/ServerCapabilities.cs delete mode 100644 src/EventStore.Client/Core/SharingProvider.cs delete mode 100644 src/EventStore.Client/Core/SingleNodeChannelSelector.cs delete mode 100644 src/EventStore.Client/Core/SingleNodeHttpHandler.cs delete mode 100644 src/EventStore.Client/Core/StreamFilter.cs delete mode 100644 src/EventStore.Client/Core/StreamIdentifier.cs delete mode 100644 src/EventStore.Client/Core/StreamPosition.cs delete mode 100644 src/EventStore.Client/Core/StreamRevision.cs delete mode 100644 src/EventStore.Client/Core/StreamState.cs delete mode 100644 src/EventStore.Client/Core/SubscriptionDroppedReason.cs delete mode 100644 src/EventStore.Client/Core/SystemRoles.cs delete mode 100644 src/EventStore.Client/Core/SystemStreams.cs delete mode 100644 src/EventStore.Client/Core/TaskExtensions.cs delete mode 100644 src/EventStore.Client/Core/UserCredentials.cs delete mode 100644 src/EventStore.Client/Core/Uuid.cs delete mode 100644 src/EventStore.Client/Core/protos/code.proto delete mode 100644 src/EventStore.Client/Core/protos/gossip.proto delete mode 100644 src/EventStore.Client/Core/protos/operations.proto delete mode 100644 src/EventStore.Client/Core/protos/persistentsubscriptions.proto delete mode 100644 src/EventStore.Client/Core/protos/projectionmanagement.proto delete mode 100644 src/EventStore.Client/Core/protos/serverfeatures.proto delete mode 100644 src/EventStore.Client/Core/protos/shared.proto delete mode 100644 src/EventStore.Client/Core/protos/status.proto delete mode 100644 src/EventStore.Client/Core/protos/streams.proto delete mode 100644 src/EventStore.Client/Core/protos/usermanagement.proto delete mode 100644 src/EventStore.Client/EventStore.Client.csproj delete mode 100644 src/EventStore.Client/EventStore.Client.csproj.DotSettings delete mode 100644 src/EventStore.Client/Operations/DatabaseScavengeResult.cs delete mode 100644 src/EventStore.Client/Operations/EventStoreOperationsClient.Admin.cs delete mode 100644 src/EventStore.Client/Operations/EventStoreOperationsClient.Scavenge.cs delete mode 100644 src/EventStore.Client/Operations/EventStoreOperationsClient.cs delete mode 100644 src/EventStore.Client/Operations/EventStoreOperationsClientServiceCollectionExtensions.cs delete mode 100644 src/EventStore.Client/Operations/ScavengeResult.cs delete mode 100644 src/EventStore.Client/PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Create.cs delete mode 100644 src/EventStore.Client/PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Delete.cs delete mode 100644 src/EventStore.Client/PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Info.cs delete mode 100644 src/EventStore.Client/PersistentSubscriptions/EventStorePersistentSubscriptionsClient.List.cs delete mode 100644 src/EventStore.Client/PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Read.cs delete mode 100644 src/EventStore.Client/PersistentSubscriptions/EventStorePersistentSubscriptionsClient.ReplayParked.cs delete mode 100644 src/EventStore.Client/PersistentSubscriptions/EventStorePersistentSubscriptionsClient.RestartSubsystem.cs delete mode 100644 src/EventStore.Client/PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Update.cs delete mode 100644 src/EventStore.Client/PersistentSubscriptions/EventStorePersistentSubscriptionsClient.cs delete mode 100644 src/EventStore.Client/PersistentSubscriptions/EventStorePersistentSubscriptionsClientCollectionExtensions.cs delete mode 100644 src/EventStore.Client/PersistentSubscriptions/MaximumSubscribersReachedException.cs delete mode 100644 src/EventStore.Client/PersistentSubscriptions/PersistentSubscription.cs delete mode 100644 src/EventStore.Client/PersistentSubscriptions/PersistentSubscriptionDroppedByServerException.cs delete mode 100644 src/EventStore.Client/PersistentSubscriptions/PersistentSubscriptionExtraStatistic.cs delete mode 100644 src/EventStore.Client/PersistentSubscriptions/PersistentSubscriptionInfo.cs delete mode 100644 src/EventStore.Client/PersistentSubscriptions/PersistentSubscriptionMessage.cs delete mode 100644 src/EventStore.Client/PersistentSubscriptions/PersistentSubscriptionNakEventAction.cs delete mode 100644 src/EventStore.Client/PersistentSubscriptions/PersistentSubscriptionNotFoundException.cs delete mode 100644 src/EventStore.Client/PersistentSubscriptions/PersistentSubscriptionSettings.cs delete mode 100644 src/EventStore.Client/PersistentSubscriptions/SystemConsumerStrategies.cs delete mode 100644 src/EventStore.Client/ProjectionManagement/EventStoreProjectionManagementClient.Control.cs delete mode 100644 src/EventStore.Client/ProjectionManagement/EventStoreProjectionManagementClient.Create.cs delete mode 100644 src/EventStore.Client/ProjectionManagement/EventStoreProjectionManagementClient.State.cs delete mode 100644 src/EventStore.Client/ProjectionManagement/EventStoreProjectionManagementClient.Statistics.cs delete mode 100644 src/EventStore.Client/ProjectionManagement/EventStoreProjectionManagementClient.Update.cs delete mode 100644 src/EventStore.Client/ProjectionManagement/EventStoreProjectionManagementClient.cs delete mode 100644 src/EventStore.Client/ProjectionManagement/EventStoreProjectionManagementClientCollectionExtensions.cs delete mode 100644 src/EventStore.Client/ProjectionManagement/ProjectionDetails.cs delete mode 100644 src/EventStore.Client/Streams/ConditionalWriteResult.cs delete mode 100644 src/EventStore.Client/Streams/ConditionalWriteStatus.cs delete mode 100644 src/EventStore.Client/Streams/DeadLine.cs delete mode 100644 src/EventStore.Client/Streams/DeleteResult.cs delete mode 100644 src/EventStore.Client/Streams/Direction.cs delete mode 100644 src/EventStore.Client/Streams/EventStoreClient.Append.cs delete mode 100644 src/EventStore.Client/Streams/EventStoreClient.Delete.cs delete mode 100644 src/EventStore.Client/Streams/EventStoreClient.Metadata.cs delete mode 100644 src/EventStore.Client/Streams/EventStoreClient.Read.cs delete mode 100644 src/EventStore.Client/Streams/EventStoreClient.Subscriptions.cs delete mode 100644 src/EventStore.Client/Streams/EventStoreClient.Tombstone.cs delete mode 100644 src/EventStore.Client/Streams/EventStoreClient.cs delete mode 100644 src/EventStore.Client/Streams/EventStoreClientExtensions.cs delete mode 100644 src/EventStore.Client/Streams/EventStoreClientServiceCollectionExtensions.cs delete mode 100644 src/EventStore.Client/Streams/IWriteResult.cs delete mode 100644 src/EventStore.Client/Streams/InvalidTransactionException.cs delete mode 100644 src/EventStore.Client/Streams/MaximumAppendSizeExceededException.cs delete mode 100644 src/EventStore.Client/Streams/ReadState.cs delete mode 100644 src/EventStore.Client/Streams/StreamAcl.cs delete mode 100644 src/EventStore.Client/Streams/StreamAclJsonConverter.cs delete mode 100644 src/EventStore.Client/Streams/StreamMessage.cs delete mode 100644 src/EventStore.Client/Streams/StreamMetadata.cs delete mode 100644 src/EventStore.Client/Streams/StreamMetadataJsonConverter.cs delete mode 100644 src/EventStore.Client/Streams/StreamMetadataResult.cs delete mode 100644 src/EventStore.Client/Streams/StreamSubscription.cs delete mode 100644 src/EventStore.Client/Streams/Streams/AppendReq.cs delete mode 100644 src/EventStore.Client/Streams/Streams/BatchAppendReq.cs delete mode 100644 src/EventStore.Client/Streams/Streams/BatchAppendResp.cs delete mode 100644 src/EventStore.Client/Streams/Streams/DeleteReq.cs delete mode 100644 src/EventStore.Client/Streams/Streams/ReadReq.cs delete mode 100644 src/EventStore.Client/Streams/Streams/TombstoneReq.cs delete mode 100644 src/EventStore.Client/Streams/SubscriptionFilterOptions.cs delete mode 100644 src/EventStore.Client/Streams/SuccessResult.cs delete mode 100644 src/EventStore.Client/Streams/SystemEventTypes.cs delete mode 100644 src/EventStore.Client/Streams/SystemMetadata.cs delete mode 100644 src/EventStore.Client/Streams/SystemSettings.cs delete mode 100644 src/EventStore.Client/Streams/SystemSettingsJsonConverter.cs delete mode 100644 src/EventStore.Client/Streams/WriteResultExtensions.cs delete mode 100644 src/EventStore.Client/Streams/WrongExpectedVersionResult.cs delete mode 100644 src/EventStore.Client/UserManagement/EventStoreUserManagementClient.cs delete mode 100644 src/EventStore.Client/UserManagement/EventStoreUserManagementClientCollectionExtensions.cs delete mode 100644 src/EventStore.Client/UserManagement/EventStoreUserManagerClientExtensions.cs delete mode 100644 src/EventStore.Client/UserManagement/UserDetails.cs create mode 100644 test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/Obsolete/SubscribeToAllConnectToExistingWithStartFromNotSetObsoleteTests.cs create mode 100644 test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/Obsolete/SubscribeToAllConnectToExistingWithStartFromSetToEndPositionObsoleteTests.cs create mode 100644 test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/Obsolete/SubscribeToAllConnectWithoutReadPermissionsObsoleteTests.cs create mode 100644 test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/Obsolete/SubscribeToAllFilterObsoleteTests.cs create mode 100644 test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/Obsolete/SubscribeToAllGetInfoObsoleteTests.cs create mode 100644 test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/Obsolete/SubscribeToAllListWithIncorrectCredentialsObsoleteTests.cs create mode 100644 test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/Obsolete/SubscribeToAllNoDefaultCredentialsObsoleteTests.cs create mode 100644 test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/Obsolete/SubscribeToAllObsoleteTests.cs create mode 100644 test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/Obsolete/SubscribeToAllReplayParkedObsoleteTests.cs create mode 100644 test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/Obsolete/SubscribeToAllResultWithNormalUserCredentialsObsoleteTests.cs create mode 100644 test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/Obsolete/SubscribeToAllReturnsAllSubscriptionsObsoleteTests.cs create mode 100644 test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/Obsolete/SubscribeToAllReturnsSubscriptionsToAllStreamObsoleteTests.cs create mode 100644 test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/Obsolete/SubscribeToAllUpdateExistingWithCheckpointObsoleteTests.cs create mode 100644 test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/Obsolete/SubscribeToAllWithoutPSObsoleteTests.cs rename test/Kurrent.Client.Tests/PersistentSubscriptions/{ => SubscribeToAll}/SubscribeToAllConnectToExistingWithStartFromNotSetTests.cs (90%) rename test/Kurrent.Client.Tests/PersistentSubscriptions/{ => SubscribeToAll}/SubscribeToAllConnectToExistingWithStartFromSetToEndPositionTests.cs (100%) rename test/Kurrent.Client.Tests/PersistentSubscriptions/{ => SubscribeToAll}/SubscribeToAllConnectWithoutReadPermissionsTests.cs (100%) rename test/Kurrent.Client.Tests/PersistentSubscriptions/{ => SubscribeToAll}/SubscribeToAllFilterTests.cs (100%) rename test/Kurrent.Client.Tests/PersistentSubscriptions/{ => SubscribeToAll}/SubscribeToAllGetInfoTests.cs (100%) rename test/Kurrent.Client.Tests/PersistentSubscriptions/{ => SubscribeToAll}/SubscribeToAllListWithIncorrectCredentialsTests.cs (100%) rename test/Kurrent.Client.Tests/PersistentSubscriptions/{ => SubscribeToAll}/SubscribeToAllNoDefaultCredentialsTests.cs (100%) rename test/Kurrent.Client.Tests/PersistentSubscriptions/{ => SubscribeToAll}/SubscribeToAllReplayParkedTests.cs (100%) rename test/Kurrent.Client.Tests/PersistentSubscriptions/{ => SubscribeToAll}/SubscribeToAllResultWithNormalUserCredentialsTests.cs (100%) rename test/Kurrent.Client.Tests/PersistentSubscriptions/{ => SubscribeToAll}/SubscribeToAllReturnsAllSubscriptions.cs (100%) rename test/Kurrent.Client.Tests/PersistentSubscriptions/{ => SubscribeToAll}/SubscribeToAllReturnsSubscriptionsToAllStreamTests.cs (100%) rename test/Kurrent.Client.Tests/PersistentSubscriptions/{ => SubscribeToAll}/SubscribeToAllTests.cs (100%) rename test/Kurrent.Client.Tests/PersistentSubscriptions/{ => SubscribeToAll}/SubscribeToAllUpdateExistingWithCheckpointTest.cs (100%) rename test/Kurrent.Client.Tests/PersistentSubscriptions/{ => SubscribeToAll}/SubscribeToAllWithoutPSTests.cs (100%) create mode 100644 test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToStream/Obsolete/SubscribeToStreamConnectToExistingWithoutPermissionObsoleteTests.cs create mode 100644 test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToStream/Obsolete/SubscribeToStreamGetInfoObsoleteTests.cs create mode 100644 test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToStream/Obsolete/SubscribeToStreamObsoleteTests.cs rename test/Kurrent.Client.Tests/PersistentSubscriptions/{ => SubscribeToStream}/SubscribeToStreamConnectToExistingWithStartFromBeginningTests.cs (100%) rename test/Kurrent.Client.Tests/PersistentSubscriptions/{ => SubscribeToStream}/SubscribeToStreamGetInfoTests.cs (96%) rename test/Kurrent.Client.Tests/PersistentSubscriptions/{ => SubscribeToStream}/SubscribeToStreamListTests.cs (100%) rename test/Kurrent.Client.Tests/PersistentSubscriptions/{ => SubscribeToStream}/SubscribeToStreamNoDefaultCredentialsTests.cs (100%) rename test/Kurrent.Client.Tests/PersistentSubscriptions/{ => SubscribeToStream}/SubscribeToStreamReplayParkedTests.cs (100%) rename test/Kurrent.Client.Tests/PersistentSubscriptions/{ => SubscribeToStream}/SubscribeToStreamTests.cs (100%) create mode 100644 test/Kurrent.Client.Tests/Streams/Subscriptions/Obsolete/SubscribeToAllObsoleteTests.cs create mode 100644 test/Kurrent.Client.Tests/Streams/Subscriptions/Obsolete/SubscribeToStreamObsoleteTests.cs create mode 100644 test/Kurrent.Client.Tests/Streams/Subscriptions/SubscribeToAllTests.cs rename test/Kurrent.Client.Tests/Streams/{ => Subscriptions}/SubscribeToStreamTests.cs (100%) create mode 100644 test/Kurrent.Client.Tests/Streams/Subscriptions/SubscriptionDroppedResult.cs diff --git a/.github/workflows/base.yml b/.github/workflows/base.yml index c4efd3227..4023569e8 100644 --- a/.github/workflows/base.yml +++ b/.github/workflows/base.yml @@ -25,7 +25,7 @@ jobs: strategy: fail-fast: false matrix: - framework: [ net9.0 ] + framework: [ net8.0, net9.0 ] os: [ ubuntu-latest ] configuration: [ release ] runs-on: ${{ matrix.os }} diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index ebe348e3e..0aabc306f 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,193 +1,200 @@ -#name: Publish -# -#on: -# pull_request: -# push: -# branches: -# - master -# tags: -# - v* -# -#jobs: -# vulnerability-scan: -# timeout-minutes: 10 -# strategy: -# fail-fast: false -# matrix: -# framework: [ net8.0 ] -# os: [ ubuntu-latest, windows-latest ] -# runs-on: ${{ matrix.os }} -# name: scan-vulnerabilities/${{ matrix.os }}/${{ matrix.framework }} -# steps: -# - name: Checkout -# uses: actions/checkout@v3 -# - name: Install dotnet SDKs -# uses: actions/setup-dotnet@v3 -# with: -# dotnet-version: | -# 8.0.x -# - name: Scan for Vulnerabilities -# shell: bash -# run: | -# dotnet nuget list source -# dotnet restore -# dotnet list package --vulnerable --include-transitive --framework ${{ matrix.framework }} | tee vulnerabilities.txt -# ! cat vulnerabilities.txt | grep -q "has the following vulnerable packages" -# -# build-samples: -# timeout-minutes: 5 -# name: build-samples/${{ matrix.framework }} -# runs-on: ubuntu-latest -# strategy: -# fail-fast: false -# matrix: -# framework: [ net8.0 ] -# services: -# esdb: -# image: docker.eventstore.com/eventstore-ce/eventstoredb-ce:lts -# env: -# EVENTSTORE_INSECURE: true -# EVENTSTORE_MEM_DB: false -# EVENTSTORE_RUN_PROJECTIONS: all -# EVENTSTORE_START_STANDARD_PROJECTIONS: true -# ports: -# - 2113:2113 -# options: --health-cmd "exit 0" -# steps: -# - name: Checkout -# uses: actions/checkout@v3 -# - name: Install dotnet SDKs -# uses: actions/setup-dotnet@v3 -# with: -# dotnet-version: | -# 8.0.x -# - name: Compile -# shell: bash -# run: | -# dotnet build samples -# - name: Run -# shell: bash -# run: | -# find samples/ -type f -iname "*.csproj" -print0 | xargs -0L1 dotnet run --framework ${{ matrix.framework }} --project -# -# generate-certificates: -# runs-on: ubuntu-latest -# steps: -# - name: Checkout code -# uses: actions/checkout@v4 -# - name: Generate certificates -# run: | -# mkdir -p certs -# docker run --rm --user root --volume "$PWD/certs:/tmp" docker.eventstore.com/eventstore-utils/es-gencert-cli:latest create-ca -out /tmp/ca -# docker run --rm --user root --volume "$PWD/certs:/tmp" docker.eventstore.com/eventstore-utils/es-gencert-cli:latest create-node -ca-certificate /tmp/ca/ca.crt -ca-key /tmp/ca/ca.key -out /tmp/node -ip-addresses 127.0.0.1 -dns-names localhost -# docker run --rm --user root --volume "$PWD/certs:/tmp" docker.eventstore.com/eventstore-utils/es-gencert-cli:latest create-user -username admin -ca-certificate /tmp/ca/ca.crt -ca-key /tmp/ca/ca.key -out /tmp/user-admin -# docker run --rm --user root --volume "$PWD/certs:/tmp" docker.eventstore.com/eventstore-utils/es-gencert-cli:latest create-user -username invalid -ca-certificate /tmp/ca/ca.crt -ca-key /tmp/ca/ca.key -out /tmp/user-invalid -# - name: Set permissions on certificates -# run: | -# sudo chown -R $USER:$USER certs -# sudo chmod -R 755 certs -# - name: Upload certificates -# uses: actions/upload-artifact@v4 -# with: -# name: certs -# path: certs -# -# test: -# needs: generate-certificates -# timeout-minutes: 20 -# strategy: -# fail-fast: false -# matrix: -# framework: [ net8.0 ] -# os: [ ubuntu-latest, windows-latest ] -# configuration: [ release ] -# runs-on: ${{ matrix.os }} -# name: test/EventStore.Client/${{ matrix.os }}/${{ matrix.framework }} -# steps: -# - name: Checkout -# uses: actions/checkout@v3 -# - shell: bash -# run: | -# git fetch --prune --unshallow -# - name: Install dotnet SDKs -# uses: actions/setup-dotnet@v3 -# with: -# dotnet-version: | -# 8.0.x -# - name: Compile -# shell: bash -# run: | -# dotnet build --configuration ${{ matrix.configuration }} --framework ${{ matrix.framework }} src/EventStore.Client -# - name: Download certificates -# uses: actions/download-artifact@v4 -# with: -# name: certs -# path: certs -# - name: Run Tests (Linux) -# if: runner.os == 'Linux' -# shell: bash -# run: | -# dotnet test --configuration ${{ matrix.configuration }} --blame \ -# --logger:"GitHubActions;report-warnings=false" --logger:"console;verbosity=normal" \ -# --framework ${{ matrix.framework }} \ -# test/EventStore.Client.Tests -# - name: Run Tests (Windows) -# if: runner.os == 'Windows' -# shell: pwsh -# run: | -# dotnet test --configuration ${{ matrix.configuration }} --blame ` -# --logger:"GitHubActions;report-warnings=false" --logger:"console;verbosity=normal" ` -# --framework ${{ matrix.framework }} ` -# test/EventStore.Client.Tests -# -# publish: -# timeout-minutes: 5 -# needs: [ vulnerability-scan, test, build-samples ] -# runs-on: ubuntu-latest -# name: publish -# steps: -# - name: Checkout -# uses: actions/checkout@v3 -# - name: Get Version -# id: get_version -# run: | -# echo "branch=${GITHUB_REF:10}" >> $GITHUB_OUTPUT -# dotnet nuget list source -# dotnet tool restore -# version=$(dotnet tool run minver -- --tag-prefix=v) -# echo "version=${version}" >> $GITHUB_OUTPUT -# - shell: bash -# run: | -# git fetch --prune --unshallow -# - name: Install dotnet SDKs -# uses: actions/setup-dotnet@v3 -# with: -# dotnet-version: | -# 8.0.x -# - name: Dotnet Pack -# shell: bash -# run: | -# mkdir -p packages -# dotnet pack /p:Version=${{ steps.get_version.outputs.version }} --configuration=Release \ -# /p:PublishDir=./packages \ -# /p:NoWarn=NU5105 \ -# /p:RepositoryUrl=https://github.com/EventStore/EventStore-Client-Dotnet \ -# /p:RepositoryType=git -# - name: Publish Artifacts -# uses: actions/upload-artifact@v4 -# with: -# path: packages -# name: nuget-packages -# - name: Dotnet Push to Github Packages -# shell: bash -# if: github.event_name == 'push' -# run: | -# dotnet tool restore -# find . -name "*.nupkg" | xargs -n1 dotnet nuget push --api-key=${{ secrets.github_token }} --source https://nuget.pkg.github.com/EventStore/index.json --skip-duplicate -# - name: Dotnet Push to Nuget.org -# shell: bash -# if: contains(steps.get_version.outputs.branch, 'v') -# run: | -# dotnet nuget list source -# dotnet tool restore -# find . -name "*.nupkg" | xargs -n1 dotnet nuget push --api-key=${{ secrets.nuget_key }} --source https://api.nuget.org/v3/index.json --skip-duplicate +name: Publish + +on: + pull_request: + push: + branches: + - master + tags: + - v* + +jobs: + vulnerability-scan: + timeout-minutes: 10 + strategy: + fail-fast: false + matrix: + framework: [ net8.0, net9.0 ] + os: [ ubuntu-latest, windows-latest ] + runs-on: ${{ matrix.os }} + name: scan-vulnerabilities/${{ matrix.os }}/${{ matrix.framework }} + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Install dotnet SDKs + uses: actions/setup-dotnet@v3 + with: + dotnet-version: | + 8.0.x + 9.0.x + - name: Scan for Vulnerabilities + shell: bash + run: | + dotnet nuget list source + dotnet restore + dotnet list package --vulnerable --include-transitive --framework ${{ matrix.framework }} | tee vulnerabilities.txt + ! cat vulnerabilities.txt | grep -q "has the following vulnerable packages" + + build-samples: + timeout-minutes: 5 + name: build-samples/${{ matrix.framework }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + framework: [ net8.0, net9.0 ] + services: + esdb: + image: docker.eventstore.com/eventstore-ce/eventstoredb-ce:lts + env: + EVENTSTORE_INSECURE: true + EVENTSTORE_MEM_DB: false + EVENTSTORE_RUN_PROJECTIONS: all + EVENTSTORE_START_STANDARD_PROJECTIONS: true + ports: + - 2113:2113 + options: --health-cmd "exit 0" + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Install dotnet SDKs + uses: actions/setup-dotnet@v3 + with: + dotnet-version: | + 8.0.x + 9.0.x + - name: Compile + shell: bash + run: | + dotnet build samples + - name: Run + shell: bash + run: | + find samples/ -type f -iname "*.csproj" -print0 | xargs -0L1 dotnet run --framework ${{ matrix.framework }} --project + + generate-certificates: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + - name: Generate certificates + run: | + mkdir -p certs + docker run --rm --user root --volume "$PWD/certs:/tmp" docker.eventstore.com/eventstore-utils/es-gencert-cli:latest create-ca -out /tmp/ca + docker run --rm --user root --volume "$PWD/certs:/tmp" docker.eventstore.com/eventstore-utils/es-gencert-cli:latest create-node -ca-certificate /tmp/ca/ca.crt -ca-key /tmp/ca/ca.key -out /tmp/node -ip-addresses 127.0.0.1 -dns-names localhost + docker run --rm --user root --volume "$PWD/certs:/tmp" docker.eventstore.com/eventstore-utils/es-gencert-cli:latest create-user -username admin -ca-certificate /tmp/ca/ca.crt -ca-key /tmp/ca/ca.key -out /tmp/user-admin + docker run --rm --user root --volume "$PWD/certs:/tmp" docker.eventstore.com/eventstore-utils/es-gencert-cli:latest create-user -username invalid -ca-certificate /tmp/ca/ca.crt -ca-key /tmp/ca/ca.key -out /tmp/user-invalid + - name: Set permissions on certificates + run: | + sudo chown -R $USER:$USER certs + sudo chmod -R 755 certs + - name: Upload certificates + uses: actions/upload-artifact@v4 + with: + name: certs + path: certs + + test: + needs: generate-certificates + timeout-minutes: 10 + strategy: + fail-fast: false + matrix: + framework: [ net8.0, 9.0 ] + os: [ ubuntu-latest, windows-latest ] + configuration: [ release ] + test: [ Streams, PersistentSubscriptions, Operations, ProjectionManagement, UserManagement, Security, Misc ] + runs-on: ${{ matrix.os }} + name: ${{ inputs.test }} (${{ matrix.os }}, ${{ matrix.framework }}) + steps: + - name: Checkout + uses: actions/checkout@v3 + - shell: bash + run: | + git fetch --prune --unshallow + - name: Install dotnet SDKs + uses: actions/setup-dotnet@v3 + with: + dotnet-version: | + 8.0.x + 9.0.x + - name: Compile + shell: bash + run: | + dotnet build --configuration ${{ matrix.configuration }} --framework ${{ matrix.framework }} src/Kurrent.Client + - name: Download certificates + uses: actions/download-artifact@v4 + with: + name: certs + path: certs + - name: Run Tests (Linux) + if: runner.os == 'Linux' + shell: bash + run: | + dotnet test --configuration ${{ matrix.configuration }} --blame \ + --logger:"GitHubActions;report-warnings=false" --logger:"console;verbosity=normal" \ + --filter "Category=Target:${{ inputs.test }}" \ + --framework ${{ matrix.framework }} \ + test/Kurrent.Client.Tests + - name: Run Tests (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: | + dotnet test --configuration ${{ matrix.configuration }} --blame ` + --logger:"GitHubActions;report-warnings=false" --logger:"console;verbosity=normal" ` + --framework ${{ matrix.framework }} ` + --filter "Category=Target:${{ inputs.test }}" ` + test/Kurrent.Client.Tests + + publish: + timeout-minutes: 5 + needs: [ vulnerability-scan, test, build-samples ] + runs-on: ubuntu-latest + name: publish + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Get Version + id: get_version + run: | + echo "branch=${GITHUB_REF:10}" >> $GITHUB_OUTPUT + dotnet nuget list source + dotnet tool restore + version=$(dotnet tool run minver -- --tag-prefix=v) + echo "version=${version}" >> $GITHUB_OUTPUT + - shell: bash + run: | + git fetch --prune --unshallow + - name: Install dotnet SDKs + uses: actions/setup-dotnet@v3 + with: + dotnet-version: | + 8.0.x + 9.0.x + - name: Dotnet Pack + shell: bash + run: | + mkdir -p packages + dotnet pack /p:Version=${{ steps.get_version.outputs.version }} --configuration=Release \ + /p:PublishDir=./packages \ + /p:NoWarn=NU5105 \ + /p:RepositoryUrl=https://github.com/EventStore/EventStore-Client-Dotnet \ + /p:RepositoryType=git + - name: Publish Artifacts + uses: actions/upload-artifact@v4 + with: + path: packages + name: nuget-packages + - name: Dotnet Push to Github Packages + shell: bash + if: github.event_name == 'push' + run: | + dotnet tool restore + find . -name "*.nupkg" | xargs -n1 dotnet nuget push --api-key=${{ secrets.github_token }} --source https://nuget.pkg.github.com/EventStore/index.json --skip-duplicate + - name: Dotnet Push to Nuget.org + shell: bash + if: contains(steps.get_version.outputs.branch, 'v') + run: | + dotnet nuget list source + dotnet tool restore + find . -name "*.nupkg" | xargs -n1 dotnet nuget push --api-key=${{ secrets.nuget_key }} --source https://api.nuget.org/v3/index.json --skip-duplicate diff --git a/samples/Directory.Build.props b/samples/Directory.Build.props index 54497a508..4e510486d 100644 --- a/samples/Directory.Build.props +++ b/samples/Directory.Build.props @@ -12,4 +12,4 @@ - \ No newline at end of file + diff --git a/samples/Samples.sln b/samples/Samples.sln index d3c46d188..ec2611d7b 100644 --- a/samples/Samples.sln +++ b/samples/Samples.sln @@ -7,8 +7,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "quick-start", "quick-start\ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "client", "client", "{EBB93BBC-42A7-48E4-B1EA-0EA3953D347C}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EventStore.Client", "..\src\EventStore.Client\EventStore.Client.csproj", "{A71A13F7-8480-4E48-B88D-A2364F7C95B6}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "connecting-to-a-cluster", "connecting-to-a-cluster\connecting-to-a-cluster.csproj", "{C4CA324A-450D-4621-82F1-B4ECD18216B6}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "appending-events", "appending-events\appending-events.csproj", "{496D9886-AF65-4579-AECE-2C147B9AF3C1}" @@ -33,7 +31,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "diagnostics", "diagnostics\ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "user-certificates", "user-certificates\user-certificates.csproj", "{28112410-D02D-427A-9D36-3FE3A6DC6B0D}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventStore.Client.Extensions.OpenTelemetry", "..\src\EventStore.Client.Extensions.OpenTelemetry\EventStore.Client.Extensions.OpenTelemetry.csproj", "{29E3F07A-6676-45C1-805C-46BDF6CF325B}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kurrent.Client", "..\src\Kurrent.Client\Kurrent.Client.csproj", "{16F61817-6E46-4F52-879F-E4B92A6A90C6}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -45,10 +43,6 @@ Global {521987E5-4394-4EE0-B217-E3DC9DB0D327}.Debug|Any CPU.Build.0 = Debug|Any CPU {521987E5-4394-4EE0-B217-E3DC9DB0D327}.Release|Any CPU.ActiveCfg = Release|Any CPU {521987E5-4394-4EE0-B217-E3DC9DB0D327}.Release|Any CPU.Build.0 = Release|Any CPU - {A71A13F7-8480-4E48-B88D-A2364F7C95B6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A71A13F7-8480-4E48-B88D-A2364F7C95B6}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A71A13F7-8480-4E48-B88D-A2364F7C95B6}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A71A13F7-8480-4E48-B88D-A2364F7C95B6}.Release|Any CPU.Build.0 = Release|Any CPU {C4CA324A-450D-4621-82F1-B4ECD18216B6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C4CA324A-450D-4621-82F1-B4ECD18216B6}.Debug|Any CPU.Build.0 = Debug|Any CPU {C4CA324A-450D-4621-82F1-B4ECD18216B6}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -97,17 +91,16 @@ Global {28112410-D02D-427A-9D36-3FE3A6DC6B0D}.Debug|Any CPU.Build.0 = Debug|Any CPU {28112410-D02D-427A-9D36-3FE3A6DC6B0D}.Release|Any CPU.ActiveCfg = Release|Any CPU {28112410-D02D-427A-9D36-3FE3A6DC6B0D}.Release|Any CPU.Build.0 = Release|Any CPU - {29E3F07A-6676-45C1-805C-46BDF6CF325B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {29E3F07A-6676-45C1-805C-46BDF6CF325B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {29E3F07A-6676-45C1-805C-46BDF6CF325B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {29E3F07A-6676-45C1-805C-46BDF6CF325B}.Release|Any CPU.Build.0 = Release|Any CPU + {16F61817-6E46-4F52-879F-E4B92A6A90C6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {16F61817-6E46-4F52-879F-E4B92A6A90C6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {16F61817-6E46-4F52-879F-E4B92A6A90C6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {16F61817-6E46-4F52-879F-E4B92A6A90C6}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution - {A71A13F7-8480-4E48-B88D-A2364F7C95B6} = {EBB93BBC-42A7-48E4-B1EA-0EA3953D347C} - {29E3F07A-6676-45C1-805C-46BDF6CF325B} = {EBB93BBC-42A7-48E4-B1EA-0EA3953D347C} + {16F61817-6E46-4F52-879F-E4B92A6A90C6} = {EBB93BBC-42A7-48E4-B1EA-0EA3953D347C} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {2E7E3A6C-21DA-4DBD-9EB3-68DFC5CDE48F} diff --git a/samples/appending-events/Program.cs b/samples/appending-events/Program.cs index 2e1bb758f..98f12fb1a 100644 --- a/samples/appending-events/Program.cs +++ b/samples/appending-events/Program.cs @@ -1,10 +1,10 @@ #pragma warning disable CS8321 // Local function is declared but never used -var settings = EventStoreClientSettings.Create("esdb://localhost:2113?tls=false"); +var settings = KurrentClientSettings.Create("esdb://localhost:2113?tls=false"); settings.OperationOptions.ThrowOnAppendFailure = false; -await using var client = new EventStoreClient(settings); +await using var client = new KurrentClient(settings); await AppendToStream(client); await AppendWithConcurrencyCheck(client); @@ -13,7 +13,7 @@ return; -static async Task AppendToStream(EventStoreClient client) { +static async Task AppendToStream(KurrentClient client) { #region append-to-stream var eventData = new EventData( @@ -33,7 +33,7 @@ await client.AppendToStreamAsync( #endregion append-to-stream } -static async Task AppendWithSameId(EventStoreClient client) { +static async Task AppendWithSameId(KurrentClient client) { #region append-duplicate-event var eventData = new EventData( @@ -62,7 +62,7 @@ await client.AppendToStreamAsync( #endregion append-duplicate-event } -static async Task AppendWithNoStream(EventStoreClient client) { +static async Task AppendWithNoStream(KurrentClient client) { #region append-with-no-stream var eventDataOne = new EventData( @@ -97,7 +97,7 @@ await client.AppendToStreamAsync( #endregion append-with-no-stream } -static async Task AppendWithConcurrencyCheck(EventStoreClient client) { +static async Task AppendWithConcurrencyCheck(KurrentClient client) { await client.AppendToStreamAsync( "concurrency-stream", StreamRevision.None, @@ -148,7 +148,7 @@ await client.AppendToStreamAsync( #endregion append-with-concurrency-check } -static async Task AppendOverridingUserCredentials(EventStoreClient client, CancellationToken cancellationToken) { +static async Task AppendOverridingUserCredentials(KurrentClient client, CancellationToken cancellationToken) { var eventData = new EventData( Uuid.NewUuid(), "TestEvent", diff --git a/samples/appending-events/appending-events.csproj b/samples/appending-events/appending-events.csproj index 3dbe997bc..c74c70ae9 100644 --- a/samples/appending-events/appending-events.csproj +++ b/samples/appending-events/appending-events.csproj @@ -3,8 +3,7 @@ Exe appending_events - - + diff --git a/samples/connecting-to-a-cluster/Program.cs b/samples/connecting-to-a-cluster/Program.cs index a31666fc6..01b033c6c 100644 --- a/samples/connecting-to-a-cluster/Program.cs +++ b/samples/connecting-to-a-cluster/Program.cs @@ -1,10 +1,12 @@ -#pragma warning disable CS8321 // Local function is declared but never used +using EventStore.Client; + +#pragma warning disable CS8321 // Local function is declared but never used static void ConnectingToACluster() { #region connecting-to-a-cluster - using var client = new EventStoreClient( - EventStoreClientSettings.Create("esdb://localhost:1114,localhost:2114,localhost:3114") + using var client = new KurrentClient( + KurrentClientSettings.Create("esdb://localhost:1114,localhost:2114,localhost:3114") ); #endregion connecting-to-a-cluster @@ -13,8 +15,8 @@ static void ConnectingToACluster() { static void ProvidingDefaultCredentials() { #region providing-default-credentials - using var client = new EventStoreClient( - EventStoreClientSettings.Create("esdb://admin:changeit@localhost:1114,localhost:2114,localhost:3114") + using var client = new KurrentClient( + KurrentClientSettings.Create("esdb://admin:changeit@localhost:1114,localhost:2114,localhost:3114") ); #endregion providing-default-credentials @@ -23,11 +25,11 @@ static void ProvidingDefaultCredentials() { static void ConnectingToAClusterComplex() { #region connecting-to-a-cluster-complex - using var client = new EventStoreClient( - EventStoreClientSettings.Create( + using var client = new KurrentClient( + KurrentClientSettings.Create( "esdb://admin:changeit@localhost:1114,localhost:2114,localhost:3114?DiscoveryInterval=30000;GossipTimeout=10000;NodePreference=leader;MaxDiscoverAttempts=5" ) ); #endregion connecting-to-a-cluster-complex -} \ No newline at end of file +} diff --git a/samples/connecting-to-a-cluster/connecting-to-a-cluster.csproj b/samples/connecting-to-a-cluster/connecting-to-a-cluster.csproj index af05183b9..f7304d3f3 100644 --- a/samples/connecting-to-a-cluster/connecting-to-a-cluster.csproj +++ b/samples/connecting-to-a-cluster/connecting-to-a-cluster.csproj @@ -3,8 +3,7 @@ Exe connecting_to_a_cluster - - + diff --git a/samples/connecting-to-a-single-node/Program.cs b/samples/connecting-to-a-single-node/Program.cs index f50e1689f..63d0b3e98 100644 --- a/samples/connecting-to-a-single-node/Program.cs +++ b/samples/connecting-to-a-single-node/Program.cs @@ -1,11 +1,12 @@ using connecting_to_a_single_node; +using EventStore.Client; #pragma warning disable CS8321 // Local function is declared but never used static void SimpleConnection() { #region creating-simple-connection - using var client = new EventStoreClient(EventStoreClientSettings.Create("esdb://localhost:2113")); + using var client = new KurrentClient(KurrentClientSettings.Create("esdb://localhost:2113")); #endregion creating-simple-connection } @@ -13,7 +14,7 @@ static void SimpleConnection() { static void ProvidingDefaultCredentials() { #region providing-default-credentials - using var client = new EventStoreClient(EventStoreClientSettings.Create("esdb://admin:changeit@localhost:2113")); + using var client = new KurrentClient(KurrentClientSettings.Create("esdb://admin:changeit@localhost:2113")); #endregion providing-default-credentials } @@ -21,8 +22,8 @@ static void ProvidingDefaultCredentials() { static void SpecifyingAConnectionName() { #region setting-the-connection-name - using var client = new EventStoreClient( - EventStoreClientSettings.Create("esdb://admin:changeit@localhost:2113?ConnectionName=SomeConnection") + using var client = new KurrentClient( + KurrentClientSettings.Create("esdb://admin:changeit@localhost:2113?ConnectionName=SomeConnection") ); #endregion setting-the-connection-name @@ -31,8 +32,8 @@ static void SpecifyingAConnectionName() { static void OverridingTheTimeout() { #region overriding-timeout - using var client = new EventStoreClient( - EventStoreClientSettings.Create($"esdb://admin:changeit@localhost:2113?OperationTimeout=30000") + using var client = new KurrentClient( + KurrentClientSettings.Create($"esdb://admin:changeit@localhost:2113?OperationTimeout=30000") ); #endregion overriding-timeout @@ -41,8 +42,8 @@ static void OverridingTheTimeout() { static void CombiningSettings() { #region overriding-timeout - using var client = new EventStoreClient( - EventStoreClientSettings.Create( + using var client = new KurrentClient( + KurrentClientSettings.Create( $"esdb://admin:changeit@localhost:2113?ConnectionName=SomeConnection&OperationTimeout=30000" ) ); @@ -53,7 +54,7 @@ static void CombiningSettings() { static void CreatingAnInterceptor() { #region adding-an-interceptor - var settings = new EventStoreClientSettings { + var settings = new KurrentClientSettings { Interceptors = new[] { new DemoInterceptor() }, ConnectivitySettings = { Address = new Uri("https://localhost:2113") @@ -62,13 +63,13 @@ static void CreatingAnInterceptor() { #endregion adding-an-interceptor - var client = new EventStoreClient(settings); + var client = new KurrentClient(settings); } static void CustomHttpMessageHandler() { #region adding-an-custom-http-message-handler - var settings = new EventStoreClientSettings { + var settings = new KurrentClientSettings { CreateHttpMessageHandler = () => new HttpClientHandler { ServerCertificateCustomValidationCallback = @@ -81,5 +82,5 @@ static void CustomHttpMessageHandler() { #endregion adding-an-custom-http-message-handler - var client = new EventStoreClient(settings); -} \ No newline at end of file + var client = new KurrentClient(settings); +} diff --git a/samples/connecting-to-a-single-node/connecting-to-a-single-node.csproj b/samples/connecting-to-a-single-node/connecting-to-a-single-node.csproj index 7700f4667..97609c80a 100644 --- a/samples/connecting-to-a-single-node/connecting-to-a-single-node.csproj +++ b/samples/connecting-to-a-single-node/connecting-to-a-single-node.csproj @@ -2,8 +2,7 @@ Exe - - + diff --git a/samples/diagnostics/Program.cs b/samples/diagnostics/Program.cs index 5804be7a4..86f9a6e3a 100644 --- a/samples/diagnostics/Program.cs +++ b/samples/diagnostics/Program.cs @@ -1,4 +1,5 @@ using System.Diagnostics; +using EventStore.Client; using EventStore.Client.Extensions.OpenTelemetry; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; @@ -23,17 +24,17 @@ dotnet add package OpenTelemetry.Extensions.Hosting # endregion import-required-packages **/ -var settings = EventStoreClientSettings.Create("esdb://localhost:2113?tls=false"); +var settings = KurrentClientSettings.Create("esdb://localhost:2113?tls=false"); settings.OperationOptions.ThrowOnAppendFailure = false; -await using var client = new EventStoreClient(settings); +await using var client = new KurrentClient(settings); await TraceAppendToStream(client); return; -static async Task TraceAppendToStream(EventStoreClient client) { +static async Task TraceAppendToStream(KurrentClient client) { const string serviceName = "sample"; var host = Host.CreateDefaultBuilder() @@ -76,7 +77,7 @@ static void ConfigureTracerProviderBuilder(TracerProviderBuilder tracerProviderB #region register-instrumentation tracerProviderBuilder - .AddEventStoreClientInstrumentation(); + .AddKurrentClientInstrumentation(); #endregion register-instrumentation diff --git a/samples/diagnostics/diagnostics.csproj b/samples/diagnostics/diagnostics.csproj index d0ca46e87..56bd7c49c 100644 --- a/samples/diagnostics/diagnostics.csproj +++ b/samples/diagnostics/diagnostics.csproj @@ -13,9 +13,6 @@ - - - - + diff --git a/samples/persistent-subscriptions/Program.cs b/samples/persistent-subscriptions/Program.cs index e1e9387c6..6d07b7947 100644 --- a/samples/persistent-subscriptions/Program.cs +++ b/samples/persistent-subscriptions/Program.cs @@ -1,5 +1,5 @@ -await using var client = new EventStorePersistentSubscriptionsClient( - EventStoreClientSettings.Create("esdb://localhost:2113?tls=false&tlsVerifyCert=false") +await using var client = new KurrentPersistentSubscriptionsClient( + KurrentClientSettings.Create("esdb://localhost:2113?tls=false&tlsVerifyCert=false") ); await DeletePersistentSubscription(client); @@ -36,7 +36,7 @@ return; -static async Task CreatePersistentSubscription(EventStorePersistentSubscriptionsClient client) { +static async Task CreatePersistentSubscription(KurrentPersistentSubscriptionsClient client) { #region create-persistent-subscription-to-stream var userCredentials = new UserCredentials("admin", "changeit"); @@ -54,7 +54,7 @@ await client.CreateToStreamAsync( #endregion create-persistent-subscription-to-stream } -static async Task ConnectToPersistentSubscriptionToStream(EventStorePersistentSubscriptionsClient client, +static async Task ConnectToPersistentSubscriptionToStream(KurrentPersistentSubscriptionsClient client, CancellationToken ct) { #region subscribe-to-persistent-subscription-to-stream @@ -77,7 +77,7 @@ static async Task ConnectToPersistentSubscriptionToStream(EventStorePersistentSu #endregion subscribe-to-persistent-subscription-to-stream } -static async Task CreatePersistentSubscriptionToAll(EventStorePersistentSubscriptionsClient client) { +static async Task CreatePersistentSubscriptionToAll(KurrentPersistentSubscriptionsClient client) { #region create-persistent-subscription-to-all var userCredentials = new UserCredentials("admin", "changeit"); @@ -96,7 +96,7 @@ await client.CreateToAllAsync( #endregion create-persistent-subscription-to-all } -static async Task ConnectToPersistentSubscriptionToAll(EventStorePersistentSubscriptionsClient client, +static async Task ConnectToPersistentSubscriptionToAll(KurrentPersistentSubscriptionsClient client, CancellationToken ct) { #region subscribe-to-persistent-subscription-to-all @@ -118,7 +118,7 @@ static async Task ConnectToPersistentSubscriptionToAll(EventStorePersistentSubsc #endregion subscribe-to-persistent-subscription-to-all } -static async Task ConnectToPersistentSubscriptionWithManualAcks(EventStorePersistentSubscriptionsClient client, +static async Task ConnectToPersistentSubscriptionWithManualAcks(KurrentPersistentSubscriptionsClient client, CancellationToken ct) { #region subscribe-to-persistent-subscription-with-manual-acks @@ -145,7 +145,7 @@ static async Task ConnectToPersistentSubscriptionWithManualAcks(EventStorePersis #endregion subscribe-to-persistent-subscription-with-manual-acks } -static async Task UpdatePersistentSubscription(EventStorePersistentSubscriptionsClient client) { +static async Task UpdatePersistentSubscription(KurrentPersistentSubscriptionsClient client) { #region update-persistent-subscription var userCredentials = new UserCredentials("admin", "changeit"); @@ -163,7 +163,7 @@ await client.UpdateToStreamAsync( #endregion update-persistent-subscription } -static async Task DeletePersistentSubscription(EventStorePersistentSubscriptionsClient client) { +static async Task DeletePersistentSubscription(KurrentPersistentSubscriptionsClient client) { #region delete-persistent-subscription try { @@ -184,7 +184,7 @@ await client.DeleteToStreamAsync( #endregion delete-persistent-subscription } -static async Task DeletePersistentSubscriptionToAll(EventStorePersistentSubscriptionsClient client) { +static async Task DeletePersistentSubscriptionToAll(KurrentPersistentSubscriptionsClient client) { #region delete-persistent-subscription-to-all try { @@ -204,7 +204,7 @@ await client.DeleteToAllAsync( #endregion delete-persistent-subscription-to-all } -static async Task GetPersistentSubscriptionToStreamInfo(EventStorePersistentSubscriptionsClient client) { +static async Task GetPersistentSubscriptionToStreamInfo(KurrentPersistentSubscriptionsClient client) { #region get-persistent-subscription-to-stream-info var userCredentials = new UserCredentials("admin", "changeit"); @@ -219,7 +219,7 @@ static async Task GetPersistentSubscriptionToStreamInfo(EventStorePersistentSubs #endregion get-persistent-subscription-to-stream-info } -static async Task GetPersistentSubscriptionToAllInfo(EventStorePersistentSubscriptionsClient client) { +static async Task GetPersistentSubscriptionToAllInfo(KurrentPersistentSubscriptionsClient client) { #region get-persistent-subscription-to-all-info var userCredentials = new UserCredentials("admin", "changeit"); @@ -233,7 +233,7 @@ static async Task GetPersistentSubscriptionToAllInfo(EventStorePersistentSubscri #endregion get-persistent-subscription-to-all-info } -static async Task ReplayParkedToStream(EventStorePersistentSubscriptionsClient client) { +static async Task ReplayParkedToStream(KurrentPersistentSubscriptionsClient client) { #region replay-parked-of-persistent-subscription-to-stream var userCredentials = new UserCredentials("admin", "changeit"); @@ -249,7 +249,7 @@ await client.ReplayParkedMessagesToStreamAsync( #endregion persistent-subscription-replay-parked-to-stream } -static async Task ReplayParkedToAll(EventStorePersistentSubscriptionsClient client) { +static async Task ReplayParkedToAll(KurrentPersistentSubscriptionsClient client) { #region replay-parked-of-persistent-subscription-to-all var userCredentials = new UserCredentials("admin", "changeit"); @@ -264,7 +264,7 @@ await client.ReplayParkedMessagesToAllAsync( #endregion replay-parked-of-persistent-subscription-to-all } -static async Task ListPersistentSubscriptionsToStream(EventStorePersistentSubscriptionsClient client) { +static async Task ListPersistentSubscriptionsToStream(KurrentPersistentSubscriptionsClient client) { #region list-persistent-subscriptions-to-stream var userCredentials = new UserCredentials("admin", "changeit"); @@ -281,7 +281,7 @@ static async Task ListPersistentSubscriptionsToStream(EventStorePersistentSubscr #endregion list-persistent-subscriptions-to-stream } -static async Task ListPersistentSubscriptionsToAll(EventStorePersistentSubscriptionsClient client) { +static async Task ListPersistentSubscriptionsToAll(KurrentPersistentSubscriptionsClient client) { #region list-persistent-subscriptions-to-all var userCredentials = new UserCredentials("admin", "changeit"); @@ -295,7 +295,7 @@ static async Task ListPersistentSubscriptionsToAll(EventStorePersistentSubscript #endregion list-persistent-subscriptions-to-all } -static async Task ListAllPersistentSubscriptions(EventStorePersistentSubscriptionsClient client) { +static async Task ListAllPersistentSubscriptions(KurrentPersistentSubscriptionsClient client) { #region list-persistent-subscriptions var userCredentials = new UserCredentials("admin", "changeit"); @@ -309,7 +309,7 @@ static async Task ListAllPersistentSubscriptions(EventStorePersistentSubscriptio #endregion list-persistent-subscriptions } -static async Task RestartPersistentSubscriptionSubsystem(EventStorePersistentSubscriptionsClient client) { +static async Task RestartPersistentSubscriptionSubsystem(KurrentPersistentSubscriptionsClient client) { #region restart-persistent-subscription-subsystem var userCredentials = new UserCredentials("admin", "changeit"); diff --git a/samples/persistent-subscriptions/persistent-subscriptions.csproj b/samples/persistent-subscriptions/persistent-subscriptions.csproj index 4204ed387..e296fca91 100644 --- a/samples/persistent-subscriptions/persistent-subscriptions.csproj +++ b/samples/persistent-subscriptions/persistent-subscriptions.csproj @@ -3,8 +3,7 @@ Exe persistent_subscriptions - - + diff --git a/samples/projection-management/Program.cs b/samples/projection-management/Program.cs index 0f3d66b70..478b88b10 100644 --- a/samples/projection-management/Program.cs +++ b/samples/projection-management/Program.cs @@ -64,20 +64,20 @@ return; -static EventStoreProjectionManagementClient ManagementClient(string connection) { +static KurrentProjectionManagementClient ManagementClient(string connection) { #region createClient - var settings = EventStoreClientSettings.Create(connection); + var settings = KurrentClientSettings.Create(connection); settings.ConnectionName = "Projection management client"; settings.DefaultCredentials = new UserCredentials("admin", "changeit"); - var managementClient = new EventStoreProjectionManagementClient(settings); + var managementClient = new KurrentProjectionManagementClient(settings); #endregion createClient return managementClient; } -static async Task RestartSubSystem(EventStoreProjectionManagementClient managementClient) { +static async Task RestartSubSystem(KurrentProjectionManagementClient managementClient) { #region RestartSubSystem await managementClient.RestartSubsystemAsync(); @@ -85,7 +85,7 @@ static async Task RestartSubSystem(EventStoreProjectionManagementClient manageme #endregion RestartSubSystem } -static async Task Disable(EventStoreProjectionManagementClient managementClient) { +static async Task Disable(KurrentProjectionManagementClient managementClient) { #region Disable await managementClient.DisableAsync("$by_category"); @@ -93,7 +93,7 @@ static async Task Disable(EventStoreProjectionManagementClient managementClient) #endregion Disable } -static async Task DisableNotFound(EventStoreProjectionManagementClient managementClient) { +static async Task DisableNotFound(KurrentProjectionManagementClient managementClient) { #region DisableNotFound try { @@ -109,7 +109,7 @@ static async Task DisableNotFound(EventStoreProjectionManagementClient managemen #endregion DisableNotFound } -static async Task Enable(EventStoreProjectionManagementClient managementClient) { +static async Task Enable(KurrentProjectionManagementClient managementClient) { #region Enable await managementClient.EnableAsync("$by_category"); @@ -117,7 +117,7 @@ static async Task Enable(EventStoreProjectionManagementClient managementClient) #endregion Enable } -static async Task EnableNotFound(EventStoreProjectionManagementClient managementClient) { +static async Task EnableNotFound(KurrentProjectionManagementClient managementClient) { #region EnableNotFound try { @@ -133,7 +133,7 @@ static async Task EnableNotFound(EventStoreProjectionManagementClient management #endregion EnableNotFound } -static Task Delete(EventStoreProjectionManagementClient managementClient) { +static Task Delete(KurrentProjectionManagementClient managementClient) { #region Delete // this is not yet available in the .net grpc client @@ -143,7 +143,7 @@ static Task Delete(EventStoreProjectionManagementClient managementClient) { return Task.CompletedTask; } -static async Task Abort(EventStoreProjectionManagementClient managementClient) { +static async Task Abort(KurrentProjectionManagementClient managementClient) { try { var js = "fromAll() .when({$init:function(){return {count:0};},$any:function(s, e){s.count += 1;}}).outputState();"; @@ -165,7 +165,7 @@ static async Task Abort(EventStoreProjectionManagementClient managementClient) { #endregion Abort } -static async Task Abort_NotFound(EventStoreProjectionManagementClient managementClient) { +static async Task Abort_NotFound(KurrentProjectionManagementClient managementClient) { #region Abort_NotFound try { @@ -181,7 +181,7 @@ static async Task Abort_NotFound(EventStoreProjectionManagementClient management #endregion Abort_NotFound } -static async Task Reset(EventStoreProjectionManagementClient managementClient) { +static async Task Reset(KurrentProjectionManagementClient managementClient) { try { var js = "fromAll() .when({$init:function(){return {count:0};},$any:function(s, e){s.count += 1;}}).outputState();"; @@ -203,7 +203,7 @@ static async Task Reset(EventStoreProjectionManagementClient managementClient) { #endregion Reset } -static async Task Reset_NotFound(EventStoreProjectionManagementClient managementClient) { +static async Task Reset_NotFound(KurrentProjectionManagementClient managementClient) { #region Reset_NotFound try { @@ -219,14 +219,14 @@ static async Task Reset_NotFound(EventStoreProjectionManagementClient management #endregion Reset_NotFound } -static async Task CreateOneTime(EventStoreProjectionManagementClient managementClient) { +static async Task CreateOneTime(KurrentProjectionManagementClient managementClient) { const string js = "fromAll() .when({$init:function(){return {count:0};},$any:function(s, e){s.count += 1;}}).outputState();"; await managementClient.CreateOneTimeAsync(js); } -static async Task CreateContinuous(EventStoreProjectionManagementClient managementClient) { +static async Task CreateContinuous(KurrentProjectionManagementClient managementClient) { #region CreateContinuous const string js = @"fromAll() @@ -248,7 +248,7 @@ static async Task CreateContinuous(EventStoreProjectionManagementClient manageme #endregion CreateContinuous } -static async Task CreateContinuous_Conflict(EventStoreProjectionManagementClient managementClient) { +static async Task CreateContinuous_Conflict(KurrentProjectionManagementClient managementClient) { const string js = @"fromAll() .when({ $init: function() { @@ -281,7 +281,7 @@ static async Task CreateContinuous_Conflict(EventStoreProjectionManagementClient #endregion CreateContinuous_Conflict } -static async Task Update(EventStoreProjectionManagementClient managementClient) { +static async Task Update(KurrentProjectionManagementClient managementClient) { #region Update const string js = @"fromAll() @@ -305,7 +305,7 @@ static async Task Update(EventStoreProjectionManagementClient managementClient) #endregion Update } -static async Task Update_NotFound(EventStoreProjectionManagementClient managementClient) { +static async Task Update_NotFound(KurrentProjectionManagementClient managementClient) { #region Update_NotFound try { @@ -321,7 +321,7 @@ static async Task Update_NotFound(EventStoreProjectionManagementClient managemen #endregion Update_NotFound } -static async Task ListAll(EventStoreProjectionManagementClient managementClient) { +static async Task ListAll(KurrentProjectionManagementClient managementClient) { #region ListAll var details = managementClient.ListAllAsync(); @@ -333,7 +333,7 @@ static async Task ListAll(EventStoreProjectionManagementClient managementClient) #endregion ListAll } -static async Task ListContinuous(EventStoreProjectionManagementClient managementClient) { +static async Task ListContinuous(KurrentProjectionManagementClient managementClient) { #region ListContinuous var details = managementClient.ListContinuousAsync(); @@ -345,7 +345,7 @@ static async Task ListContinuous(EventStoreProjectionManagementClient management #endregion ListContinuous } -static async Task GetStatus(EventStoreProjectionManagementClient managementClient) { +static async Task GetStatus(KurrentProjectionManagementClient managementClient) { const string js = "fromAll().when({$init:function(){return {count:0};},$any:function(s, e){s.count += 1;}}).outputState();"; @@ -362,7 +362,7 @@ static async Task GetStatus(EventStoreProjectionManagementClient managementClien #endregion GetStatus } -static async Task GetState(EventStoreProjectionManagementClient managementClient) { +static async Task GetState(KurrentProjectionManagementClient managementClient) { // will have to wait for the client to be fixed before we import in the doc #region GetState @@ -393,7 +393,7 @@ static async Task DocToString(JsonDocument d) { #endregion GetState } -static async Task GetResult(EventStoreProjectionManagementClient managementClient) { +static async Task GetResult(KurrentProjectionManagementClient managementClient) { #region GetResult const string js = @"fromAll() @@ -433,9 +433,9 @@ static string DocToString(JsonDocument d) { } static async Task Populate(string connection, int numberOfEvents) { - var settings = EventStoreClientSettings.Create(connection); + var settings = KurrentClientSettings.Create(connection); settings.DefaultCredentials = new UserCredentials("admin", "changeit"); - var client = new EventStoreClient(settings); + var client = new KurrentClient(settings); var messages = Enumerable.Range(0, numberOfEvents).Select( number => new EventData( @@ -452,4 +452,4 @@ public class Result { public int count { get; set; } public override string ToString() => $"count= {count}"; -}; \ No newline at end of file +}; diff --git a/samples/projection-management/projection-management.csproj b/samples/projection-management/projection-management.csproj index 80fe56f78..f6a6206aa 100644 --- a/samples/projection-management/projection-management.csproj +++ b/samples/projection-management/projection-management.csproj @@ -2,8 +2,7 @@ projection_management - - + diff --git a/samples/quick-start/Program.cs b/samples/quick-start/Program.cs index 0cd070137..dd763ae42 100644 --- a/samples/quick-start/Program.cs +++ b/samples/quick-start/Program.cs @@ -7,9 +7,9 @@ const string connectionString = "esdb://admin:changeit@localhost:2113?tls=false&tlsVerifyCert=false"; -var settings = EventStoreClientSettings.Create(connectionString); +var settings = KurrentClientSettings.Create(connectionString); -var client = new EventStoreClient(settings); +var client = new KurrentClient(settings); #endregion createClient @@ -67,4 +67,4 @@ await client.AppendToStreamAsync( public class TestEvent { public string? EntityId { get; set; } public string? ImportantData { get; set; } -} \ No newline at end of file +} diff --git a/samples/quick-start/quick-start.csproj b/samples/quick-start/quick-start.csproj index 74eea2001..5fe35463c 100644 --- a/samples/quick-start/quick-start.csproj +++ b/samples/quick-start/quick-start.csproj @@ -2,8 +2,7 @@ quick_start - - + diff --git a/samples/reading-events/Program.cs b/samples/reading-events/Program.cs index 854634da8..751086d5c 100644 --- a/samples/reading-events/Program.cs +++ b/samples/reading-events/Program.cs @@ -1,6 +1,6 @@ #pragma warning disable CS8321 // Local function is declared but never used -await using var client = new EventStoreClient(EventStoreClientSettings.Create("esdb://localhost:2113?tls=false")); +await using var client = new KurrentClient(KurrentClientSettings.Create("esdb://localhost:2113?tls=false")); var events = Enumerable.Range(0, 20) .Select( @@ -21,7 +21,7 @@ await client.AppendToStreamAsync( return; -static async Task ReadFromStream(EventStoreClient client) { +static async Task ReadFromStream(KurrentClient client) { #region read-from-stream var events = client.ReadStreamAsync( @@ -46,7 +46,7 @@ static async Task ReadFromStream(EventStoreClient client) { #endregion } -static async Task ReadFromStreamMessages(EventStoreClient client) { +static async Task ReadFromStreamMessages(KurrentClient client) { #region read-from-stream-messages var streamPosition = StreamPosition.Start; @@ -90,7 +90,7 @@ static async Task ReadFromStreamMessages(EventStoreClient client) { #endregion iterate-stream-messages } -static async Task ReadFromStreamPosition(EventStoreClient client) { +static async Task ReadFromStreamPosition(KurrentClient client) { #region read-from-stream-position var events = client.ReadStreamAsync( @@ -109,7 +109,7 @@ static async Task ReadFromStreamPosition(EventStoreClient client) { #endregion iterate-stream } -static async Task ReadFromStreamPositionCheck(EventStoreClient client) { +static async Task ReadFromStreamPositionCheck(KurrentClient client) { #region checking-for-stream-presence var result = client.ReadStreamAsync( @@ -126,7 +126,7 @@ static async Task ReadFromStreamPositionCheck(EventStoreClient client) { #endregion checking-for-stream-presence } -static async Task ReadFromStreamBackwards(EventStoreClient client) { +static async Task ReadFromStreamBackwards(KurrentClient client) { #region reading-backwards var events = client.ReadStreamAsync( @@ -140,7 +140,7 @@ static async Task ReadFromStreamBackwards(EventStoreClient client) { #endregion reading-backwards } -static async Task ReadFromStreamMessagesBackwards(EventStoreClient client) { +static async Task ReadFromStreamMessagesBackwards(KurrentClient client) { #region read-from-stream-messages-backwards var results = client.ReadStreamAsync( @@ -175,7 +175,7 @@ static async Task ReadFromStreamMessagesBackwards(EventStoreClient client) { #endregion iterate-stream-messages-backwards } -static async Task ReadFromAllStream(EventStoreClient client) { +static async Task ReadFromAllStream(KurrentClient client) { #region read-from-all-stream var events = client.ReadAllAsync(Direction.Forwards, Position.Start); @@ -189,7 +189,7 @@ static async Task ReadFromAllStream(EventStoreClient client) { #endregion read-from-all-stream-iterate } -static async Task ReadFromAllStreamMessages(EventStoreClient client) { +static async Task ReadFromAllStreamMessages(KurrentClient client) { #region read-from-all-stream-messages var position = Position.Start; @@ -216,7 +216,7 @@ static async Task ReadFromAllStreamMessages(EventStoreClient client) { #endregion iterate-all-stream-messages } -static async Task IgnoreSystemEvents(EventStoreClient client) { +static async Task IgnoreSystemEvents(KurrentClient client) { #region ignore-system-events var events = client.ReadAllAsync(Direction.Forwards, Position.Start); @@ -230,7 +230,7 @@ static async Task IgnoreSystemEvents(EventStoreClient client) { #endregion ignore-system-events } -static async Task ReadFromAllStreamBackwards(EventStoreClient client) { +static async Task ReadFromAllStreamBackwards(KurrentClient client) { #region read-from-all-stream-backwards var events = client.ReadAllAsync(Direction.Backwards, Position.End); @@ -244,7 +244,7 @@ static async Task ReadFromAllStreamBackwards(EventStoreClient client) { #endregion read-from-all-stream-iterate } -static async Task ReadFromAllStreamBackwardsMessages(EventStoreClient client) { +static async Task ReadFromAllStreamBackwardsMessages(KurrentClient client) { #region read-from-all-stream-messages-backwards var position = Position.End; @@ -272,7 +272,7 @@ static async Task ReadFromAllStreamBackwardsMessages(EventStoreClient client) { #endregion iterate-all-stream-messages-backwards } -static async Task FilteringOutSystemEvents(EventStoreClient client) { +static async Task FilteringOutSystemEvents(KurrentClient client) { var events = client.ReadAllAsync(Direction.Forwards, Position.Start); await foreach (var e in events) @@ -280,7 +280,7 @@ static async Task FilteringOutSystemEvents(EventStoreClient client) { Console.WriteLine(Encoding.UTF8.GetString(e.Event.Data.ToArray())); } -static void ReadStreamOverridingUserCredentials(EventStoreClient client, CancellationToken cancellationToken) { +static void ReadStreamOverridingUserCredentials(KurrentClient client, CancellationToken cancellationToken) { #region overriding-user-credentials var result = client.ReadStreamAsync( @@ -294,7 +294,7 @@ static void ReadStreamOverridingUserCredentials(EventStoreClient client, Cancell #endregion overriding-user-credentials } -static void ReadAllOverridingUserCredentials(EventStoreClient client, CancellationToken cancellationToken) { +static void ReadAllOverridingUserCredentials(KurrentClient client, CancellationToken cancellationToken) { #region read-all-overriding-user-credentials var result = client.ReadAllAsync( @@ -307,7 +307,7 @@ static void ReadAllOverridingUserCredentials(EventStoreClient client, Cancellati #endregion read-all-overriding-user-credentials } -static void ReadAllResolvingLinkTos(EventStoreClient client, CancellationToken cancellationToken) { +static void ReadAllResolvingLinkTos(KurrentClient client, CancellationToken cancellationToken) { #region read-from-all-stream-resolving-link-Tos var result = client.ReadAllAsync( @@ -318,4 +318,4 @@ static void ReadAllResolvingLinkTos(EventStoreClient client, CancellationToken c ); #endregion read-from-all-stream-resolving-link-Tos -} \ No newline at end of file +} diff --git a/samples/reading-events/reading-events.csproj b/samples/reading-events/reading-events.csproj index d29b531fb..d79ede9fc 100644 --- a/samples/reading-events/reading-events.csproj +++ b/samples/reading-events/reading-events.csproj @@ -2,8 +2,7 @@ reading_events - - + diff --git a/samples/secure-with-tls/Program.cs b/samples/secure-with-tls/Program.cs index 621ee46b1..b2dcd05bb 100644 --- a/samples/secure-with-tls/Program.cs +++ b/samples/secure-with-tls/Program.cs @@ -6,7 +6,7 @@ Console.WriteLine($"Connecting to EventStoreDB at: {connectionString}"); -await using var client = new EventStoreClient(EventStoreClientSettings.Create(connectionString)); +await using var client = new KurrentClient(KurrentClientSettings.Create(connectionString)); var eventData = new EventData(Uuid.NewUuid(), "some-event", "{\"id\": \"1\" \"value\": \"some value\"}"u8.ToArray()); @@ -48,4 +48,4 @@ } Console.WriteLine($"FAILED! {exception}"); -} \ No newline at end of file +} diff --git a/samples/secure-with-tls/secure-with-tls.csproj b/samples/secure-with-tls/secure-with-tls.csproj index 9d29b3972..a11ffdb55 100644 --- a/samples/secure-with-tls/secure-with-tls.csproj +++ b/samples/secure-with-tls/secure-with-tls.csproj @@ -14,6 +14,6 @@ - + diff --git a/samples/server-side-filtering/Program.cs b/samples/server-side-filtering/Program.cs index b9e5de8c8..b8641905a 100644 --- a/samples/server-side-filtering/Program.cs +++ b/samples/server-side-filtering/Program.cs @@ -6,7 +6,7 @@ var semaphore = new SemaphoreSlim(eventCount); -await using var client = new EventStoreClient(EventStoreClientSettings.Create("esdb://localhost:2113?tls=false")); +await using var client = new KurrentClient(KurrentClientSettings.Create("esdb://localhost:2113?tls=false")); _ = Task.Run(async () => { await using var subscription = client.SubscribeToAll( @@ -46,7 +46,7 @@ await client.AppendToStreamAsync( return; -static async Task ExcludeSystemEvents(EventStoreClient client) { +static async Task ExcludeSystemEvents(KurrentClient client) { #region exclude-system await using var subscription = client.SubscribeToAll( @@ -63,7 +63,7 @@ static async Task ExcludeSystemEvents(EventStoreClient client) { #endregion exclude-system } -static async Task EventTypePrefix(EventStoreClient client) { +static async Task EventTypePrefix(KurrentClient client) { #region event-type-prefix var filterOptions = new SubscriptionFilterOptions(EventTypeFilter.Prefix("customer-")); @@ -80,7 +80,7 @@ static async Task EventTypePrefix(EventStoreClient client) { } } -static async Task EventTypeRegex(EventStoreClient client) { +static async Task EventTypeRegex(KurrentClient client) { #region event-type-regex var filterOptions = new SubscriptionFilterOptions(EventTypeFilter.RegularExpression("^user|^company")); @@ -97,7 +97,7 @@ static async Task EventTypeRegex(EventStoreClient client) { } } -static async Task StreamPrefix(EventStoreClient client) { +static async Task StreamPrefix(KurrentClient client) { #region stream-prefix var filterOptions = new SubscriptionFilterOptions(StreamFilter.Prefix("user-")); @@ -114,7 +114,7 @@ static async Task StreamPrefix(EventStoreClient client) { } } -static async Task StreamRegex(EventStoreClient client) { +static async Task StreamRegex(KurrentClient client) { #region stream-regex var filterOptions = new SubscriptionFilterOptions(StreamFilter.RegularExpression("^account|^savings")); @@ -131,7 +131,7 @@ static async Task StreamRegex(EventStoreClient client) { } } -static async Task CheckpointCallback(EventStoreClient client) { +static async Task CheckpointCallback(KurrentClient client) { #region checkpoint var filterOptions = new SubscriptionFilterOptions(EventTypeFilter.ExcludeSystemEvents()); @@ -151,7 +151,7 @@ static async Task CheckpointCallback(EventStoreClient client) { #endregion checkpoint } -static async Task CheckpointCallbackWithInterval(EventStoreClient client) { +static async Task CheckpointCallbackWithInterval(KurrentClient client) { #region checkpoint-with-interval var filterOptions = new SubscriptionFilterOptions(EventTypeFilter.ExcludeSystemEvents(), 1000); diff --git a/samples/server-side-filtering/server-side-filtering.csproj b/samples/server-side-filtering/server-side-filtering.csproj index 0c13c9497..f3a5ffc58 100644 --- a/samples/server-side-filtering/server-side-filtering.csproj +++ b/samples/server-side-filtering/server-side-filtering.csproj @@ -2,8 +2,7 @@ server_side_filtering - - + diff --git a/samples/setting-up-dependency-injection/Controllers/EventStoreController.cs b/samples/setting-up-dependency-injection/Controllers/EventStoreController.cs index d4c3f4d59..f66c237ce 100644 --- a/samples/setting-up-dependency-injection/Controllers/EventStoreController.cs +++ b/samples/setting-up-dependency-injection/Controllers/EventStoreController.cs @@ -5,15 +5,15 @@ namespace setting_up_dependency_injection.Controllers { [Route("[controller]")] public class EventStoreController : ControllerBase { #region using-dependency - private readonly EventStoreClient _eventStoreClient; + private readonly KurrentClient _KurrentClient; - public EventStoreController(EventStoreClient eventStoreClient) { - _eventStoreClient = eventStoreClient; + public EventStoreController(KurrentClient KurrentClient) { + _KurrentClient = KurrentClient; } [HttpGet] public IAsyncEnumerable Get() { - return _eventStoreClient.ReadAllAsync(Direction.Forwards, Position.Start); + return _KurrentClient.ReadAllAsync(Direction.Forwards, Position.Start); } #endregion using-dependency } diff --git a/samples/setting-up-dependency-injection/Startup.cs b/samples/setting-up-dependency-injection/Startup.cs index a1d618aff..cb8c9fb94 100644 --- a/samples/setting-up-dependency-injection/Startup.cs +++ b/samples/setting-up-dependency-injection/Startup.cs @@ -10,7 +10,7 @@ public void ConfigureServices(IServiceCollection services) { #region setting-up-dependency - services.AddEventStoreClient("esdb://admin:changeit@localhost:2113?tls=false"); + services.AddKurrentClient("esdb://admin:changeit@localhost:2113?tls=false"); #endregion setting-up-dependency } @@ -22,4 +22,4 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { app.UseRouting(); app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); } -} \ No newline at end of file +} diff --git a/samples/setting-up-dependency-injection/setting-up-dependency-injection.csproj b/samples/setting-up-dependency-injection/setting-up-dependency-injection.csproj index 3a57950a1..e4f581a89 100644 --- a/samples/setting-up-dependency-injection/setting-up-dependency-injection.csproj +++ b/samples/setting-up-dependency-injection/setting-up-dependency-injection.csproj @@ -4,6 +4,6 @@ - + diff --git a/samples/subscribing-to-streams/Program.cs b/samples/subscribing-to-streams/Program.cs index 90fa29737..01990a17e 100644 --- a/samples/subscribing-to-streams/Program.cs +++ b/samples/subscribing-to-streams/Program.cs @@ -3,7 +3,7 @@ // ReSharper disable UnusedParameter.Local // ReSharper disable UnusedVariable -await using var client = new EventStoreClient(EventStoreClientSettings.Create("esdb://localhost:2113?tls=false")); +await using var client = new KurrentClient(KurrentClientSettings.Create("esdb://localhost:2113?tls=false")); await Task.WhenAll(YieldSamples().Select(async sample => { try { @@ -27,7 +27,7 @@ IEnumerable YieldSamples() { yield return OverridingUserCredentials(client, GetCT()); } -static async Task SubscribeToStreamFromPosition(EventStoreClient client, CancellationToken ct) { +static async Task SubscribeToStreamFromPosition(KurrentClient client, CancellationToken ct) { #region subscribe-to-stream-from-position await using var subscription = client.SubscribeToStream( @@ -46,7 +46,7 @@ static async Task SubscribeToStreamFromPosition(EventStoreClient client, Cancell #endregion subscribe-to-stream-from-position } -static async Task SubscribeToStreamLive(EventStoreClient client, CancellationToken ct) { +static async Task SubscribeToStreamLive(KurrentClient client, CancellationToken ct) { #region subscribe-to-stream-live await using var subscription = client.SubscribeToStream( @@ -65,7 +65,7 @@ static async Task SubscribeToStreamLive(EventStoreClient client, CancellationTok #endregion subscribe-to-stream-live } -static async Task SubscribeToStreamResolvingLinkTos(EventStoreClient client, CancellationToken ct) { +static async Task SubscribeToStreamResolvingLinkTos(KurrentClient client, CancellationToken ct) { #region subscribe-to-stream-resolving-linktos await using var subscription = client.SubscribeToStream( @@ -85,7 +85,7 @@ static async Task SubscribeToStreamResolvingLinkTos(EventStoreClient client, Can #endregion subscribe-to-stream-resolving-linktos } -static async Task SubscribeToStreamSubscriptionDropped(EventStoreClient client, CancellationToken ct) { +static async Task SubscribeToStreamSubscriptionDropped(KurrentClient client, CancellationToken ct) { #region subscribe-to-stream-subscription-dropped var checkpoint = await ReadStreamCheckpointAsync() switch { @@ -120,7 +120,7 @@ static async Task SubscribeToStreamSubscriptionDropped(EventStoreClient client, #endregion subscribe-to-stream-subscription-dropped } -static async Task SubscribeToStream(EventStoreClient client, CancellationToken ct) { +static async Task SubscribeToStream(KurrentClient client, CancellationToken ct) { #region subscribe-to-stream await using var subscription = client.SubscribeToStream( @@ -139,7 +139,7 @@ static async Task SubscribeToStream(EventStoreClient client, CancellationToken c #endregion subscribe-to-stream } -static async Task SubscribeToAll(EventStoreClient client, CancellationToken ct) { +static async Task SubscribeToAll(KurrentClient client, CancellationToken ct) { #region subscribe-to-all await using var subscription = client.SubscribeToAll( @@ -157,7 +157,7 @@ static async Task SubscribeToAll(EventStoreClient client, CancellationToken ct) #endregion subscribe-to-all } -static async Task SubscribeToAllFromPosition(EventStoreClient client, CancellationToken ct) { +static async Task SubscribeToAllFromPosition(KurrentClient client, CancellationToken ct) { #region subscribe-to-all-from-position var result = await client.AppendToStreamAsync( @@ -182,7 +182,7 @@ static async Task SubscribeToAllFromPosition(EventStoreClient client, Cancellati #endregion subscribe-to-all-from-position } -static async Task SubscribeToAllLive(EventStoreClient client, CancellationToken ct) { +static async Task SubscribeToAllLive(KurrentClient client, CancellationToken ct) { #region subscribe-to-all-live var subscription = client.SubscribeToAll( @@ -200,7 +200,7 @@ static async Task SubscribeToAllLive(EventStoreClient client, CancellationToken #endregion subscribe-to-all-live } -static async Task SubscribeToAllSubscriptionDropped(EventStoreClient client, CancellationToken ct) { +static async Task SubscribeToAllSubscriptionDropped(KurrentClient client, CancellationToken ct) { #region subscribe-to-all-subscription-dropped var checkpoint = await ReadCheckpointAsync() switch { @@ -237,7 +237,7 @@ static async Task SubscribeToAllSubscriptionDropped(EventStoreClient client, Can #endregion subscribe-to-all-subscription-dropped } -static async Task SubscribeToFiltered(EventStoreClient client, CancellationToken ct) { +static async Task SubscribeToFiltered(KurrentClient client, CancellationToken ct) { #region stream-prefix-filtered-subscription var prefixStreamFilter = new SubscriptionFilterOptions(StreamFilter.Prefix("test-", "other-")); @@ -267,7 +267,7 @@ static async Task SubscribeToFiltered(EventStoreClient client, CancellationToken #endregion stream-regex-filtered-subscription } -static async Task OverridingUserCredentials(EventStoreClient client, CancellationToken ct) { +static async Task OverridingUserCredentials(KurrentClient client, CancellationToken ct) { #region overriding-user-credentials await using var subscription = client.SubscribeToAll( diff --git a/samples/subscribing-to-streams/subscribing-to-streams.csproj b/samples/subscribing-to-streams/subscribing-to-streams.csproj index 92424b533..83a740e45 100644 --- a/samples/subscribing-to-streams/subscribing-to-streams.csproj +++ b/samples/subscribing-to-streams/subscribing-to-streams.csproj @@ -2,8 +2,7 @@ subscribing_to_streams - - + diff --git a/samples/user-certificates/Program.cs b/samples/user-certificates/Program.cs index dddbb1c7f..98f9e882e 100644 --- a/samples/user-certificates/Program.cs +++ b/samples/user-certificates/Program.cs @@ -9,11 +9,11 @@ static async Task ClientWithUserCertificates() { const string userCertFile = "/path/to/user.crt"; const string userKeyFile = "/path/to/user.key"; - var settings = EventStoreClientSettings.Create( + var settings = KurrentClientSettings.Create( $"esdb://localhost:2113/?tls=true&tlsVerifyCert=true&userCertFile={userCertFile}&userKeyFile={userKeyFile}" ); - await using var client = new EventStoreClient(settings); + await using var client = new KurrentClient(settings); # endregion client-with-user-certificates } catch (InvalidClientCertificateException) { diff --git a/samples/user-certificates/user-certificates.csproj b/samples/user-certificates/user-certificates.csproj index 8cb9d2294..14de7d848 100644 --- a/samples/user-certificates/user-certificates.csproj +++ b/samples/user-certificates/user-certificates.csproj @@ -9,7 +9,7 @@ - + diff --git a/src/EventStore.Client.Extensions.OpenTelemetry/EventStore.Client.Extensions.OpenTelemetry.csproj b/src/EventStore.Client.Extensions.OpenTelemetry/EventStore.Client.Extensions.OpenTelemetry.csproj deleted file mode 100644 index 81f4d38ea..000000000 --- a/src/EventStore.Client.Extensions.OpenTelemetry/EventStore.Client.Extensions.OpenTelemetry.csproj +++ /dev/null @@ -1,20 +0,0 @@ - - - - EventStore.Client.Extensions.OpenTelemetry - - - - EventStore.Client.Extensions.OpenTelemetry - Extensions used to facilitate instrumentation of the EventStore Client. - - - - - - - - - - - diff --git a/src/EventStore.Client.Extensions.OpenTelemetry/TracerProviderBuilderExtensions.cs b/src/EventStore.Client.Extensions.OpenTelemetry/TracerProviderBuilderExtensions.cs deleted file mode 100644 index 6a8b42b96..000000000 --- a/src/EventStore.Client.Extensions.OpenTelemetry/TracerProviderBuilderExtensions.cs +++ /dev/null @@ -1,19 +0,0 @@ -using EventStore.Client.Diagnostics; -using JetBrains.Annotations; -using OpenTelemetry.Trace; - -namespace EventStore.Client.Extensions.OpenTelemetry; - -/// -/// Extension methods used to facilitate tracing instrumentation of the EventStore Client. -/// -[PublicAPI] -public static class TracerProviderBuilderExtensions { - /// - /// Adds the EventStore client ActivitySource name to the list of subscribed sources on the - /// - /// being configured. - /// The instance of to chain configuration. - public static TracerProviderBuilder AddEventStoreClientInstrumentation(this TracerProviderBuilder builder) => - builder.AddSource(EventStoreClientDiagnostics.InstrumentationName); -} \ No newline at end of file diff --git a/src/EventStore.Client/Core/Certificates/X509Certificates.cs b/src/EventStore.Client/Core/Certificates/X509Certificates.cs deleted file mode 100644 index 3fe1006f5..000000000 --- a/src/EventStore.Client/Core/Certificates/X509Certificates.cs +++ /dev/null @@ -1,114 +0,0 @@ -#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member - -using System.Security.Cryptography; -using System.Security.Cryptography.X509Certificates; - -#if NET48 -using Org.BouncyCastle.Crypto; -using Org.BouncyCastle.Crypto.Parameters; -using Org.BouncyCastle.OpenSsl; -using Org.BouncyCastle.Security; -#endif - -namespace EventStore.Client; - -static class X509Certificates { - // TODO SS: Use .NET 8 X509Certificate2.CreateFromPemFile(certPemFilePath, keyPemFilePath) once the Windows32Exception issue is resolved - public static X509Certificate2 CreateFromPemFile(string certPemFilePath, string keyPemFilePath) { - try { -#if NET9_0_OR_GREATER - using var publicCert = X509CertificateLoader.LoadCertificateFromFile(certPemFilePath); -#else - using var publicCert = new X509Certificate2(certPemFilePath); -#endif - using var privateKey = RSA.Create().ImportPrivateKeyFromFile(keyPemFilePath); - using var certificate = publicCert.CopyWithPrivateKey(privateKey); - -#if NET48 - return new(certificate.Export(X509ContentType.Pfx)); -#else - return X509Certificate2.CreateFromPemFile(certPemFilePath, keyPemFilePath); -#endif - } catch (Exception ex) { - throw new CryptographicException($"Failed to load private key: {ex.Message}"); - } - - // Notes: - // using X509Certificate2.CreateFromPemFile(certPemFilePath, keyPemFilePath) would be the ideal choice here, - // but it's currently causing a Win32Exception specifically on Windows. Alternative implementation is used until the issue is resolved. - // - // Error: The SSL connection could not be established, see inner exception. AuthenticationException: Authentication failed because the platform - // does not support ephemeral keys. Win32Exception: No credentials are available in the security package - // - // public static X509Certificate2 CreateFromPemFile(string certPemFilePath, string keyPemFilePath) => - // X509Certificate2.CreateFromPemFile(certPemFilePath, keyPemFilePath); - } -} - -public static class RsaExtensions { -#if NET48 - public static RSA ImportPrivateKeyFromFile(this RSA rsa, string privateKeyPath) { - var (content, label) = LoadPemKeyFile(privateKeyPath); - - using var reader = new PemReader(new StringReader(string.Join(Environment.NewLine, content))); - - var keyParameters = reader.ReadObject() switch { - RsaPrivateCrtKeyParameters parameters => parameters, - AsymmetricCipherKeyPair keyPair => keyPair.Private as RsaPrivateCrtKeyParameters, - _ => throw new NotSupportedException($"Invalid private key format: {label}") - }; - - rsa.ImportParameters(DotNetUtilities.ToRSAParameters(keyParameters)); - - return rsa; - } -#else - public static RSA ImportPrivateKeyFromFile(this RSA rsa, string privateKeyPath) { - var (content, label) = LoadPemKeyFile(privateKeyPath); - - var privateKey = string.Join(string.Empty, content[1..^1]); - var privateKeyBytes = Convert.FromBase64String(privateKey); - - if (label == RsaPemLabels.Pkcs8PrivateKey) - rsa.ImportPkcs8PrivateKey(privateKeyBytes, out _); - else if (label == RsaPemLabels.RSAPrivateKey) - rsa.ImportRSAPrivateKey(privateKeyBytes, out _); - - return rsa; - } -#endif - - static (string[] Content, string Label) LoadPemKeyFile(string privateKeyPath) { - var content = File.ReadAllLines(privateKeyPath); - var label = RsaPemLabels.ParseKeyLabel(content[0]); - - if (RsaPemLabels.IsEncryptedPrivateKey(label)) - throw new NotSupportedException("Encrypted private keys are not supported"); - - return (content, label); - } -} - -static class RsaPemLabels { - public const string RSAPrivateKey = "RSA PRIVATE KEY"; - public const string Pkcs8PrivateKey = "PRIVATE KEY"; - public const string EncryptedPkcs8PrivateKey = "ENCRYPTED PRIVATE KEY"; - - public static readonly string[] PrivateKeyLabels = [RSAPrivateKey, Pkcs8PrivateKey, EncryptedPkcs8PrivateKey]; - - public static bool IsPrivateKey(string label) => Array.IndexOf(PrivateKeyLabels, label) != -1; - - public static bool IsEncryptedPrivateKey(string label) => label == EncryptedPkcs8PrivateKey; - - const string LabelPrefix = "-----BEGIN "; - const string LabelSuffix = "-----"; - - public static string ParseKeyLabel(string pemFileHeader) { - var label = pemFileHeader.Replace(LabelPrefix, string.Empty).Replace(LabelSuffix, string.Empty); - - if (!IsPrivateKey(label)) - throw new CryptographicException($"Unknown private key label: {label}"); - - return label; - } -} diff --git a/src/EventStore.Client/Core/ChannelBaseExtensions.cs b/src/EventStore.Client/Core/ChannelBaseExtensions.cs deleted file mode 100644 index 3edbf59fd..000000000 --- a/src/EventStore.Client/Core/ChannelBaseExtensions.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Grpc.Core; - -namespace EventStore.Client; - -static class ChannelBaseExtensions { - public static async ValueTask DisposeAsync(this ChannelBase channel) { - await channel.ShutdownAsync().ConfigureAwait(false); - (channel as IDisposable)?.Dispose(); - } -} \ No newline at end of file diff --git a/src/EventStore.Client/Core/ChannelCache.cs b/src/EventStore.Client/Core/ChannelCache.cs deleted file mode 100644 index a3369e25f..000000000 --- a/src/EventStore.Client/Core/ChannelCache.cs +++ /dev/null @@ -1,146 +0,0 @@ -using System.Net; -using TChannel = Grpc.Net.Client.GrpcChannel; - -namespace EventStore.Client { - // Maintains Channels keyed by DnsEndPoint so the channels can be reused. - // Deals with the disposal difference between grpc.net and grpc.core - // Thread safe. - internal class ChannelCache : - IAsyncDisposable { - - private readonly EventStoreClientSettings _settings; - private readonly Random _random; - private readonly Dictionary _channels; - private readonly object _lock = new(); - private bool _disposed; - - public ChannelCache(EventStoreClientSettings settings) { - _settings = settings; - _random = new Random(0); - _channels = new Dictionary( - DnsEndPointEqualityComparer.Instance); - } - - public TChannel GetChannelInfo(DnsEndPoint endPoint) { - lock (_lock) { - ThrowIfDisposed(); - - if (!_channels.TryGetValue(endPoint, out var channel)) { - channel = ChannelFactory.CreateChannel( - settings: _settings, - endPoint: endPoint); - _channels[endPoint] = channel; - } - - return channel; - } - } - - public KeyValuePair[] GetRandomOrderSnapshot() { - lock (_lock) { - ThrowIfDisposed(); - - return _channels - .OrderBy(_ => _random.Next()) - .ToArray(); - } - } - - // Update the cache to contain channels for exactly these endpoints - public void UpdateCache(IEnumerable endPoints) { - lock (_lock) { - ThrowIfDisposed(); - - // remove - var endPointsToDiscard = _channels.Keys - .Except(endPoints, DnsEndPointEqualityComparer.Instance) - .ToArray(); - - var channelsToDispose = new List(endPointsToDiscard.Length); - - foreach (var endPoint in endPointsToDiscard) { - if (!_channels.TryGetValue(endPoint, out var channel)) - continue; - - _channels.Remove(endPoint); - channelsToDispose.Add(channel); - } - - _ = DisposeChannelsAsync(channelsToDispose); - - // add - foreach (var endPoint in endPoints) { - GetChannelInfo(endPoint); - } - } - } - - public void Dispose() { - lock (_lock) { - if (_disposed) - return; - - _disposed = true; - - foreach (var channel in _channels.Values) { - channel.Dispose(); - } - - _channels.Clear(); - } - } - - public async ValueTask DisposeAsync() { - var channelsToDispose = Array.Empty(); - - lock (_lock) { - if (_disposed) - return; - _disposed = true; - - channelsToDispose = _channels.Values.ToArray(); - _channels.Clear(); - } - - await DisposeChannelsAsync(channelsToDispose).ConfigureAwait(false); - } - - private void ThrowIfDisposed() { - lock (_lock) { - if (_disposed) { - throw new ObjectDisposedException(GetType().ToString()); - } - } - } - - private static async Task DisposeChannelsAsync(IEnumerable channels) { - foreach (var channel in channels) - await channel.DisposeAsync().ConfigureAwait(false); - } - - private class DnsEndPointEqualityComparer : IEqualityComparer { - public static readonly DnsEndPointEqualityComparer Instance = new(); - - public bool Equals(DnsEndPoint? x, DnsEndPoint? y) { - if (ReferenceEquals(x, y)) - return true; - if (x is null) - return false; - if (y is null) - return false; - if (x.GetType() != y.GetType()) - return false; - return - string.Equals(x.Host, y.Host, StringComparison.OrdinalIgnoreCase) && - x.Port == y.Port; - } - - public int GetHashCode(DnsEndPoint obj) { - unchecked { - return (StringComparer.OrdinalIgnoreCase.GetHashCode(obj.Host) * 397) ^ - obj.Port; - } - } - } - } -} diff --git a/src/EventStore.Client/Core/ChannelFactory.cs b/src/EventStore.Client/Core/ChannelFactory.cs deleted file mode 100644 index 0dc28ee8e..000000000 --- a/src/EventStore.Client/Core/ChannelFactory.cs +++ /dev/null @@ -1,106 +0,0 @@ -using System.Net.Http; -using System.Security.Cryptography.X509Certificates; -using Grpc.Net.Client; -using EndPoint = System.Net.EndPoint; -using TChannel = Grpc.Net.Client.GrpcChannel; - -namespace EventStore.Client { - - internal static class ChannelFactory { - private const int MaxReceiveMessageLength = 17 * 1024 * 1024; - - public static TChannel CreateChannel(EventStoreClientSettings settings, EndPoint endPoint) { - var address = endPoint.ToUri(!settings.ConnectivitySettings.Insecure); - - if (settings.ConnectivitySettings.Insecure) { - //this must be switched on before creation of the HttpMessageHandler - AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true); - } - - return TChannel.ForAddress( - address, - new GrpcChannelOptions { -#if NET48 - HttpHandler = CreateHandler(settings), -#else - HttpClient = new HttpClient(CreateHandler(settings), true) { - Timeout = System.Threading.Timeout.InfiniteTimeSpan, - DefaultRequestVersion = new Version(2, 0) - }, -#endif - LoggerFactory = settings.LoggerFactory, - Credentials = settings.ChannelCredentials, - DisposeHttpClient = true, - MaxReceiveMessageSize = MaxReceiveMessageLength - } - ); - - -#if NET48 - static HttpMessageHandler CreateHandler(EventStoreClientSettings settings) { - if (settings.CreateHttpMessageHandler is not null) - return settings.CreateHttpMessageHandler.Invoke(); - - var handler = new WinHttpHandler { - TcpKeepAliveEnabled = true, - TcpKeepAliveTime = settings.ConnectivitySettings.KeepAliveTimeout, - TcpKeepAliveInterval = settings.ConnectivitySettings.KeepAliveInterval, - EnableMultipleHttp2Connections = true - }; - - if (settings.ConnectivitySettings.Insecure) return handler; - - if (settings.ConnectivitySettings.ClientCertificate is not null) - handler.ClientCertificates.Add(settings.ConnectivitySettings.ClientCertificate); - - handler.ServerCertificateValidationCallback = settings.ConnectivitySettings.TlsVerifyCert switch { - false => delegate { return true; }, - true when settings.ConnectivitySettings.TlsCaFile is not null => (sender, certificate, chain, errors) => { - if (chain is null) return false; - - chain.ChainPolicy.ExtraStore.Add(settings.ConnectivitySettings.TlsCaFile); - return chain.Build(certificate); - }, - _ => null - }; - - return handler; - } -#else - static HttpMessageHandler CreateHandler(EventStoreClientSettings settings) { - if (settings.CreateHttpMessageHandler is not null) - return settings.CreateHttpMessageHandler.Invoke(); - - var handler = new SocketsHttpHandler { - KeepAlivePingDelay = settings.ConnectivitySettings.KeepAliveInterval, - KeepAlivePingTimeout = settings.ConnectivitySettings.KeepAliveTimeout, - EnableMultipleHttp2Connections = true - }; - - if (settings.ConnectivitySettings.Insecure) - return handler; - - if (settings.ConnectivitySettings.ClientCertificate is not null) { - handler.SslOptions.ClientCertificates = new X509CertificateCollection { - settings.ConnectivitySettings.ClientCertificate - }; - } - - handler.SslOptions.RemoteCertificateValidationCallback = settings.ConnectivitySettings.TlsVerifyCert switch { - false => delegate { return true; }, - true when settings.ConnectivitySettings.TlsCaFile is not null => (sender, certificate, chain, errors) => { - if (certificate is not X509Certificate2 peerCertificate || chain is null) return false; - - chain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust; - chain.ChainPolicy.CustomTrustStore.Add(settings.ConnectivitySettings.TlsCaFile); - return chain.Build(peerCertificate); - }, - _ => null - }; - - return handler; - } -#endif - } - } -} diff --git a/src/EventStore.Client/Core/ChannelInfo.cs b/src/EventStore.Client/Core/ChannelInfo.cs deleted file mode 100644 index 87c80b644..000000000 --- a/src/EventStore.Client/Core/ChannelInfo.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Grpc.Core; - -namespace EventStore.Client { -#pragma warning disable 1591 - public record ChannelInfo( - ChannelBase Channel, - ServerCapabilities ServerCapabilities, - CallInvoker CallInvoker); -} diff --git a/src/EventStore.Client/Core/ChannelSelector.cs b/src/EventStore.Client/Core/ChannelSelector.cs deleted file mode 100644 index 354c0a9f5..000000000 --- a/src/EventStore.Client/Core/ChannelSelector.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System.Net; -using System.Threading; -using System.Threading.Tasks; -using Grpc.Core; - -namespace EventStore.Client { - internal class ChannelSelector : IChannelSelector { - private readonly IChannelSelector _inner; - - public ChannelSelector( - EventStoreClientSettings settings, - ChannelCache channelCache) { - _inner = settings.ConnectivitySettings.IsSingleNode - ? new SingleNodeChannelSelector(settings, channelCache) - : new GossipChannelSelector(settings, channelCache, new GrpcGossipClient(settings)); - } - - public Task SelectChannelAsync(CancellationToken cancellationToken) => - _inner.SelectChannelAsync(cancellationToken); - - public ChannelBase SelectChannel(DnsEndPoint endPoint) => - _inner.SelectChannel(endPoint); - } -} diff --git a/src/EventStore.Client/Core/ClusterMessage.cs b/src/EventStore.Client/Core/ClusterMessage.cs deleted file mode 100644 index 64701781e..000000000 --- a/src/EventStore.Client/Core/ClusterMessage.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System.Net; - -namespace EventStore.Client { - internal static class ClusterMessages { - public record ClusterInfo(MemberInfo[] Members); - - public record MemberInfo(Uuid InstanceId, VNodeState State, bool IsAlive, DnsEndPoint EndPoint); - - public enum VNodeState { - Initializing = 0, - DiscoverLeader = 1, - Unknown = 2, - PreReplica = 3, - CatchingUp = 4, - Clone = 5, - Follower = 6, - PreLeader = 7, - Leader = 8, - Manager = 9, - ShuttingDown = 10, - Shutdown = 11, - ReadOnlyLeaderless = 12, - PreReadOnlyReplica = 13, - ReadOnlyReplica = 14, - ResigningLeader = 15 - } - } -} diff --git a/src/EventStore.Client/Core/Common/AsyncStreamReaderExtensions.cs b/src/EventStore.Client/Core/Common/AsyncStreamReaderExtensions.cs deleted file mode 100644 index 98f9de54d..000000000 --- a/src/EventStore.Client/Core/Common/AsyncStreamReaderExtensions.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System.Threading.Channels; -using System.Runtime.CompilerServices; -using Grpc.Core; - -namespace EventStore.Client; - -static class AsyncStreamReaderExtensions { - public static async IAsyncEnumerable ReadAllAsync( - this IAsyncStreamReader reader, - [EnumeratorCancellation] - CancellationToken cancellationToken = default - ) { - while (await reader.MoveNext(cancellationToken).ConfigureAwait(false)) - yield return reader.Current; - } - - public static async IAsyncEnumerable ReadAllAsync(this ChannelReader reader, [EnumeratorCancellation] CancellationToken cancellationToken = default) { -#if NET - await foreach (var item in reader.ReadAllAsync(cancellationToken)) - yield return item; -#else - while (await reader.WaitToReadAsync(cancellationToken).ConfigureAwait(false)) { - while (reader.TryRead(out T? item)) { - yield return item; - } - } -#endif - } -} diff --git a/src/EventStore.Client/Core/Common/Constants.cs b/src/EventStore.Client/Core/Common/Constants.cs deleted file mode 100644 index 2ed9d7c82..000000000 --- a/src/EventStore.Client/Core/Common/Constants.cs +++ /dev/null @@ -1,62 +0,0 @@ -namespace EventStore.Client; - -static class Constants { - public static class Exceptions { - public const string ExceptionKey = "exception"; - - public const string AccessDenied = "access-denied"; - public const string InvalidTransaction = "invalid-transaction"; - public const string StreamDeleted = "stream-deleted"; - public const string WrongExpectedVersion = "wrong-expected-version"; - public const string StreamNotFound = "stream-not-found"; - public const string MaximumAppendSizeExceeded = "maximum-append-size-exceeded"; - public const string MissingRequiredMetadataProperty = "missing-required-metadata-property"; - public const string NotLeader = "not-leader"; - - public const string PersistentSubscriptionFailed = "persistent-subscription-failed"; - public const string PersistentSubscriptionDoesNotExist = "persistent-subscription-does-not-exist"; - public const string PersistentSubscriptionExists = "persistent-subscription-exists"; - public const string MaximumSubscribersReached = "maximum-subscribers-reached"; - public const string PersistentSubscriptionDropped = "persistent-subscription-dropped"; - - public const string UserNotFound = "user-not-found"; - public const string UserConflict = "user-conflict"; - - public const string ScavengeNotFound = "scavenge-not-found"; - - public const string ExpectedVersion = "expected-version"; - public const string ActualVersion = "actual-version"; - public const string StreamName = "stream-name"; - public const string GroupName = "group-name"; - public const string Reason = "reason"; - public const string MaximumAppendSize = "maximum-append-size"; - public const string RequiredMetadataProperties = "required-metadata-properties"; - public const string ScavengeId = "scavenge-id"; - public const string LeaderEndpointHost = "leader-endpoint-host"; - public const string LeaderEndpointPort = "leader-endpoint-port"; - - public const string LoginName = "login-name"; - } - - public static class Metadata { - public const string Type = "type"; - public const string Created = "created"; - public const string ContentType = "content-type"; - - public static readonly string[] RequiredMetadata = [Type, ContentType]; - - public static class ContentTypes { - public const string ApplicationJson = "application/json"; - public const string ApplicationOctetStream = "application/octet-stream"; - } - } - - public static class Headers { - public const string Authorization = "authorization"; - public const string BasicScheme = "Basic"; - public const string BearerScheme = "Bearer"; - - public const string ConnectionName = "connection-name"; - public const string RequiresLeader = "requires-leader"; - } -} diff --git a/src/EventStore.Client/Core/Common/Diagnostics/ActivitySourceExtensions.cs b/src/EventStore.Client/Core/Common/Diagnostics/ActivitySourceExtensions.cs deleted file mode 100644 index 71f4372da..000000000 --- a/src/EventStore.Client/Core/Common/Diagnostics/ActivitySourceExtensions.cs +++ /dev/null @@ -1,90 +0,0 @@ -// ReSharper disable ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract - -using System.Diagnostics; -using EventStore.Diagnostics; -using EventStore.Diagnostics.Telemetry; -using EventStore.Diagnostics.Tracing; - -namespace EventStore.Client.Diagnostics; - -static class ActivitySourceExtensions { - public static async ValueTask TraceClientOperation( - this ActivitySource source, - Func> tracedOperation, - string operationName, - Func? tagsFactory = null - ) { - if (source.HasNoActiveListeners()) - return await tracedOperation().ConfigureAwait(false); - - var tags = tagsFactory?.Invoke(); - - using var activity = StartActivity(source, operationName, ActivityKind.Client, tags, Activity.Current?.Context); - - try { - var res = await tracedOperation().ConfigureAwait(false); - activity?.StatusOk(); - return res; - } catch (Exception ex) { - activity?.StatusError(ex); - throw; - } - } - - public static void TraceSubscriptionEvent( - this ActivitySource source, - string? subscriptionId, - ResolvedEvent resolvedEvent, - ChannelInfo channelInfo, - EventStoreClientSettings settings, - UserCredentials? userCredentials - ) { - if (source.HasNoActiveListeners() || resolvedEvent.Event is null) - return; - - var parentContext = resolvedEvent.Event.Metadata.ExtractPropagationContext(); - - if (parentContext == default(ActivityContext)) return; - - var tags = new ActivityTagsCollection() - .WithRequiredTag(TelemetryTags.EventStore.Stream, resolvedEvent.OriginalEvent.EventStreamId) - .WithOptionalTag(TelemetryTags.EventStore.SubscriptionId, subscriptionId) - .WithRequiredTag(TelemetryTags.EventStore.EventId, resolvedEvent.OriginalEvent.EventId.ToString()) - .WithRequiredTag(TelemetryTags.EventStore.EventType, resolvedEvent.OriginalEvent.EventType) - // Ensure consistent server.address attribute when connecting to cluster via dns discovery - .WithGrpcChannelServerTags(settings, channelInfo) - .WithClientSettingsServerTags(settings) - .WithOptionalTag( - TelemetryTags.Database.User, - userCredentials?.Username ?? settings.DefaultCredentials?.Username - ); - - StartActivity(source, TracingConstants.Operations.Subscribe, ActivityKind.Consumer, tags, parentContext) - ?.Dispose(); - } - - static Activity? StartActivity( - this ActivitySource source, - string operationName, ActivityKind activityKind, ActivityTagsCollection? tags = null, - ActivityContext? parentContext = null - ) { - if (source.HasNoActiveListeners()) - return null; - - (tags ??= new ActivityTagsCollection()) - .WithRequiredTag(TelemetryTags.Database.System, "eventstoredb") - .WithRequiredTag(TelemetryTags.Database.Operation, operationName); - - return source - .CreateActivity( - operationName, - activityKind, - parentContext ?? default, - tags, - idFormat: ActivityIdFormat.W3C - ) - ?.Start(); - } - - static bool HasNoActiveListeners(this ActivitySource source) => !source.HasListeners(); -} diff --git a/src/EventStore.Client/Core/Common/Diagnostics/ActivityTagsCollectionExtensions.cs b/src/EventStore.Client/Core/Common/Diagnostics/ActivityTagsCollectionExtensions.cs deleted file mode 100644 index bc3c2aa75..000000000 --- a/src/EventStore.Client/Core/Common/Diagnostics/ActivityTagsCollectionExtensions.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System.Diagnostics; -using System.Runtime.CompilerServices; -using EventStore.Diagnostics; -using EventStore.Diagnostics.Telemetry; - -namespace EventStore.Client.Diagnostics; - -static class ActivityTagsCollectionExtensions { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static ActivityTagsCollection WithGrpcChannelServerTags(this ActivityTagsCollection tags, EventStoreClientSettings settings, ChannelInfo? channelInfo) { - if (channelInfo is null) - return tags; - - var authorityParts = channelInfo.Channel.Target.Split([':'], StringSplitOptions.RemoveEmptyEntries); - - string host = authorityParts[0]; - int port = authorityParts.Length == 1 - ? settings.ConnectivitySettings.Insecure ? 80 : 443 - : int.Parse(authorityParts[1]); - - return tags - .WithRequiredTag(TelemetryTags.Server.Address, host) - .WithRequiredTag(TelemetryTags.Server.Port, port); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static ActivityTagsCollection WithClientSettingsServerTags(this ActivityTagsCollection source, EventStoreClientSettings settings) { - if (settings.ConnectivitySettings.DnsGossipSeeds?.Length != 1) - return source; - - var gossipSeed = settings.ConnectivitySettings.DnsGossipSeeds[0]; - - return source - .WithRequiredTag(TelemetryTags.Server.Address, gossipSeed.Host) - .WithRequiredTag(TelemetryTags.Server.Port, gossipSeed.Port); - } -} diff --git a/src/EventStore.Client/Core/Common/Diagnostics/Core/ActivityExtensions.cs b/src/EventStore.Client/Core/Common/Diagnostics/Core/ActivityExtensions.cs deleted file mode 100644 index 4594d1000..000000000 --- a/src/EventStore.Client/Core/Common/Diagnostics/Core/ActivityExtensions.cs +++ /dev/null @@ -1,52 +0,0 @@ -// ReSharper disable CheckNamespace - -using System.Diagnostics; -using System.Runtime.CompilerServices; -using EventStore.Diagnostics.Telemetry; -using EventStore.Diagnostics.Tracing; - -namespace EventStore.Diagnostics; - -static class ActivityExtensions { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static TracingMetadata GetTracingMetadata(this Activity activity) => - new(activity.TraceId.ToString(), activity.SpanId.ToString()); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Activity StatusOk(this Activity activity, string? description = null) => - activity.SetActivityStatus(ActivityStatus.Ok(description)); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Activity StatusError(this Activity activity, Exception exception) => - activity.SetActivityStatus(ActivityStatus.Error(exception)); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - static Activity RecordException(this Activity activity, Exception? exception) { - if (exception is null) return activity; - - var ex = exception is AggregateException aex ? aex.Flatten() : exception; - - var tags = new ActivityTagsCollection { - { TelemetryTags.Exception.Type, ex.GetType().FullName }, - { TelemetryTags.Exception.Stacktrace, ex.ToInvariantString() } - }; - - if (!string.IsNullOrWhiteSpace(exception.Message)) - tags.Add(TelemetryTags.Exception.Message, ex.Message); - - activity.AddEvent(new ActivityEvent(TelemetryTags.Exception.EventName, default, tags)); - - return activity; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - static Activity SetActivityStatus(this Activity activity, ActivityStatus status) { - var statusCode = ActivityStatusCodeHelper.GetTagValueForStatusCode(status.StatusCode); - - activity.SetStatus(status.StatusCode, status.Description); - activity.SetTag(TelemetryTags.Otel.StatusCode, statusCode); - activity.SetTag(TelemetryTags.Otel.StatusDescription, status.Description); - - return activity.IsAllDataRequested ? activity.RecordException(status.Exception) : activity; - } -} \ No newline at end of file diff --git a/src/EventStore.Client/Core/Common/Diagnostics/Core/ActivityStatus.cs b/src/EventStore.Client/Core/Common/Diagnostics/Core/ActivityStatus.cs deleted file mode 100644 index a25b7326f..000000000 --- a/src/EventStore.Client/Core/Common/Diagnostics/Core/ActivityStatus.cs +++ /dev/null @@ -1,13 +0,0 @@ -// ReSharper disable CheckNamespace - -using System.Diagnostics; - -namespace EventStore.Diagnostics; - -record ActivityStatus(ActivityStatusCode StatusCode, string? Description, Exception? Exception) { - public static ActivityStatus Ok(string? description = null) => - new(ActivityStatusCode.Ok, description, null); - - public static ActivityStatus Error(Exception exception, string? description = null) => - new(ActivityStatusCode.Error, description ?? exception.Message, exception); -} diff --git a/src/EventStore.Client/Core/Common/Diagnostics/Core/ActivityStatusCodeHelper.cs b/src/EventStore.Client/Core/Common/Diagnostics/Core/ActivityStatusCodeHelper.cs deleted file mode 100644 index 77810aa6c..000000000 --- a/src/EventStore.Client/Core/Common/Diagnostics/Core/ActivityStatusCodeHelper.cs +++ /dev/null @@ -1,24 +0,0 @@ -// ReSharper disable CheckNamespace - -using System.Diagnostics; -using System.Runtime.CompilerServices; - -using static System.Diagnostics.ActivityStatusCode; -using static System.StringComparison; - -namespace EventStore.Diagnostics; - -static class ActivityStatusCodeHelper { - public const string UnsetStatusCodeTagValue = "UNSET"; - public const string OkStatusCodeTagValue = "OK"; - public const string ErrorStatusCodeTagValue = "ERROR"; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static string? GetTagValueForStatusCode(ActivityStatusCode statusCode) => - statusCode switch { - Unset => UnsetStatusCodeTagValue, - Error => ErrorStatusCodeTagValue, - Ok => OkStatusCodeTagValue, - _ => null - }; -} \ No newline at end of file diff --git a/src/EventStore.Client/Core/Common/Diagnostics/Core/ActivityTagsCollectionExtensions.cs b/src/EventStore.Client/Core/Common/Diagnostics/Core/ActivityTagsCollectionExtensions.cs deleted file mode 100644 index 15aa0662e..000000000 --- a/src/EventStore.Client/Core/Common/Diagnostics/Core/ActivityTagsCollectionExtensions.cs +++ /dev/null @@ -1,25 +0,0 @@ -// ReSharper disable CheckNamespace - -using System.Diagnostics; -using System.Runtime.CompilerServices; - -namespace EventStore.Diagnostics; - -static class ActivityTagsCollectionExtensions { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static ActivityTagsCollection WithRequiredTag(this ActivityTagsCollection source, string key, object? value) { - source[key] = value ?? throw new ArgumentNullException(key); - return source; - } - - /// - /// - If the key previously existed in the collection and the value is , the collection item matching the key will get removed from the collection. - /// - If the key previously existed in the collection and the value is not , the value will replace the old value stored in the collection. - /// - Otherwise, a new item will get added to the collection. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static ActivityTagsCollection WithOptionalTag(this ActivityTagsCollection source, string key, object? value) { - source[key] = value; - return source; - } -} diff --git a/src/EventStore.Client/Core/Common/Diagnostics/Core/ExceptionExtensions.cs b/src/EventStore.Client/Core/Common/Diagnostics/Core/ExceptionExtensions.cs deleted file mode 100644 index 8403c3c01..000000000 --- a/src/EventStore.Client/Core/Common/Diagnostics/Core/ExceptionExtensions.cs +++ /dev/null @@ -1,25 +0,0 @@ -// ReSharper disable CheckNamespace - -using System.Globalization; - -namespace EventStore.Diagnostics; - -static class ExceptionExtensions { - /// - /// Returns a culture-independent string representation of the given object, - /// appropriate for diagnostics tracing. - /// - /// Exception to convert to string. - /// Exception as string with no culture. - public static string ToInvariantString(this Exception exception) { - var originalUiCulture = Thread.CurrentThread.CurrentUICulture; - - try { - Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture; - return exception.ToString(); - } - finally { - Thread.CurrentThread.CurrentUICulture = originalUiCulture; - } - } -} \ No newline at end of file diff --git a/src/EventStore.Client/Core/Common/Diagnostics/Core/Telemetry/TelemetryTags.cs b/src/EventStore.Client/Core/Common/Diagnostics/Core/Telemetry/TelemetryTags.cs deleted file mode 100644 index f7d5e4b17..000000000 --- a/src/EventStore.Client/Core/Common/Diagnostics/Core/Telemetry/TelemetryTags.cs +++ /dev/null @@ -1,35 +0,0 @@ -// ReSharper disable CheckNamespace - -namespace EventStore.Diagnostics.Telemetry; - -// The attributes below match the specification of v1.24.0 of the Open Telemetry semantic conventions. -// Some attributes are ignored where not required or relevant. -// https://github.com/open-telemetry/semantic-conventions/blob/v1.24.0/docs/general/trace.md -// https://github.com/open-telemetry/semantic-conventions/blob/v1.24.0/docs/database/database-spans.md -// https://github.com/open-telemetry/semantic-conventions/blob/v1.24.0/docs/exceptions/exceptions-spans.md - -static partial class TelemetryTags { - public static class Database { - public const string User = "db.user"; - public const string System = "db.system"; - public const string Operation = "db.operation"; - } - - public static class Server { - public const string Address = "server.address"; - public const string Port = "server.port"; - public const string SocketAddress = "server.socket.address"; // replaces: "net.peer.ip" (AttributeNetPeerIp) - } - - public static class Exception { - public const string EventName = "exception"; - public const string Type = "exception.type"; - public const string Message = "exception.message"; - public const string Stacktrace = "exception.stacktrace"; - } - - public static class Otel { - public const string StatusCode = "otel.status_code"; - public const string StatusDescription = "otel.status_description"; - } -} \ No newline at end of file diff --git a/src/EventStore.Client/Core/Common/Diagnostics/Core/Tracing/TracingConstants.cs b/src/EventStore.Client/Core/Common/Diagnostics/Core/Tracing/TracingConstants.cs deleted file mode 100644 index 26aa2be21..000000000 --- a/src/EventStore.Client/Core/Common/Diagnostics/Core/Tracing/TracingConstants.cs +++ /dev/null @@ -1,10 +0,0 @@ -// ReSharper disable CheckNamespace - -namespace EventStore.Diagnostics.Tracing; - -static partial class TracingConstants { - public static class Metadata { - public const string TraceId = "$traceId"; - public const string SpanId = "$spanId"; - } -} \ No newline at end of file diff --git a/src/EventStore.Client/Core/Common/Diagnostics/Core/Tracing/TracingMetadata.cs b/src/EventStore.Client/Core/Common/Diagnostics/Core/Tracing/TracingMetadata.cs deleted file mode 100644 index ecb9c68c6..000000000 --- a/src/EventStore.Client/Core/Common/Diagnostics/Core/Tracing/TracingMetadata.cs +++ /dev/null @@ -1,32 +0,0 @@ -// ReSharper disable CheckNamespace - -using System.Diagnostics; -using System.Text.Json.Serialization; - -namespace EventStore.Diagnostics.Tracing; - -readonly record struct TracingMetadata( - [property: JsonPropertyName(TracingConstants.Metadata.TraceId)] - string? TraceId, - [property: JsonPropertyName(TracingConstants.Metadata.SpanId)] - string? SpanId -) { - public static readonly TracingMetadata None = new(null, null); - - [JsonIgnore] public bool IsValid => TraceId != null && SpanId != null; - - public ActivityContext? ToActivityContext(bool isRemote = true) { - try { - return IsValid - ? new ActivityContext( - ActivityTraceId.CreateFromString(new ReadOnlySpan(TraceId!.ToCharArray())), - ActivitySpanId.CreateFromString(new ReadOnlySpan(SpanId!.ToCharArray())), - ActivityTraceFlags.Recorded, - isRemote: isRemote - ) - : default; - } catch (Exception) { - return default; - } - } -} diff --git a/src/EventStore.Client/Core/Common/Diagnostics/EventMetadataExtensions.cs b/src/EventStore.Client/Core/Common/Diagnostics/EventMetadataExtensions.cs deleted file mode 100644 index 4245d5bb7..000000000 --- a/src/EventStore.Client/Core/Common/Diagnostics/EventMetadataExtensions.cs +++ /dev/null @@ -1,84 +0,0 @@ -using System.Diagnostics; -using System.Runtime.CompilerServices; -using System.Text.Json; -using EventStore.Diagnostics; -using EventStore.Diagnostics.Tracing; - -namespace EventStore.Client.Diagnostics; - -static class EventMetadataExtensions { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static ReadOnlySpan InjectTracingContext( - this ReadOnlyMemory eventMetadata, Activity? activity - ) => - eventMetadata.InjectTracingMetadata(activity?.GetTracingMetadata() ?? TracingMetadata.None); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static ActivityContext? ExtractPropagationContext(this ReadOnlyMemory eventMetadata) => - eventMetadata.ExtractTracingMetadata().ToActivityContext(isRemote: true); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static TracingMetadata ExtractTracingMetadata(this ReadOnlyMemory eventMetadata) { - if (eventMetadata.IsEmpty) - return TracingMetadata.None; - - var reader = new Utf8JsonReader(eventMetadata.Span); - try { - if (!JsonDocument.TryParseValue(ref reader, out var doc)) - return TracingMetadata.None; - - using (doc) { - if (!doc.RootElement.TryGetProperty(TracingConstants.Metadata.TraceId, out var traceId) - || !doc.RootElement.TryGetProperty(TracingConstants.Metadata.SpanId, out var spanId)) - return TracingMetadata.None; - - return new TracingMetadata(traceId.GetString(), spanId.GetString()); - } - } catch (Exception) { - return TracingMetadata.None; - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - static ReadOnlySpan InjectTracingMetadata( - this ReadOnlyMemory eventMetadata, TracingMetadata tracingMetadata - ) { - if (tracingMetadata == TracingMetadata.None || !tracingMetadata.IsValid) - return eventMetadata.Span; - - return eventMetadata.IsEmpty - ? JsonSerializer.SerializeToUtf8Bytes(tracingMetadata) - : TryInjectTracingMetadata(eventMetadata, tracingMetadata).ToArray(); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - static ReadOnlyMemory TryInjectTracingMetadata( - this ReadOnlyMemory utf8Json, TracingMetadata tracingMetadata - ) { - try { - using var doc = JsonDocument.Parse(utf8Json); - using var stream = new MemoryStream(); - using var writer = new Utf8JsonWriter(stream); - - writer.WriteStartObject(); - - if (doc.RootElement.ValueKind != JsonValueKind.Object) - return utf8Json; - - foreach (var prop in doc.RootElement.EnumerateObject()) - prop.WriteTo(writer); - - writer.WritePropertyName(TracingConstants.Metadata.TraceId); - writer.WriteStringValue(tracingMetadata.TraceId); - writer.WritePropertyName(TracingConstants.Metadata.SpanId); - writer.WriteStringValue(tracingMetadata.SpanId); - - writer.WriteEndObject(); - writer.Flush(); - - return stream.ToArray(); - } catch (Exception) { - return utf8Json; - } - } -} diff --git a/src/EventStore.Client/Core/Common/Diagnostics/EventStoreClientDiagnostics.cs b/src/EventStore.Client/Core/Common/Diagnostics/EventStoreClientDiagnostics.cs deleted file mode 100644 index 6328387ab..000000000 --- a/src/EventStore.Client/Core/Common/Diagnostics/EventStoreClientDiagnostics.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System.Diagnostics; - -namespace EventStore.Client.Diagnostics; - -public static class EventStoreClientDiagnostics { - public const string InstrumentationName = "eventstoredb"; - public static readonly ActivitySource ActivitySource = new ActivitySource(InstrumentationName); -} \ No newline at end of file diff --git a/src/EventStore.Client/Core/Common/Diagnostics/Telemetry/TelemetryTags.cs b/src/EventStore.Client/Core/Common/Diagnostics/Telemetry/TelemetryTags.cs deleted file mode 100644 index ea05d04fa..000000000 --- a/src/EventStore.Client/Core/Common/Diagnostics/Telemetry/TelemetryTags.cs +++ /dev/null @@ -1,12 +0,0 @@ -// ReSharper disable CheckNamespace - -namespace EventStore.Diagnostics.Telemetry; - -static partial class TelemetryTags { - public static class EventStore { - public const string Stream = "db.eventstoredb.stream"; - public const string SubscriptionId = "db.eventstoredb.subscription.id"; - public const string EventId = "db.eventstoredb.event.id"; - public const string EventType = "db.eventstoredb.event.type"; - } -} \ No newline at end of file diff --git a/src/EventStore.Client/Core/Common/Diagnostics/Tracing/TracingConstants.cs b/src/EventStore.Client/Core/Common/Diagnostics/Tracing/TracingConstants.cs deleted file mode 100644 index e43102ebe..000000000 --- a/src/EventStore.Client/Core/Common/Diagnostics/Tracing/TracingConstants.cs +++ /dev/null @@ -1,10 +0,0 @@ -// ReSharper disable CheckNamespace - -namespace EventStore.Diagnostics.Tracing; - -static partial class TracingConstants { - public static class Operations { - public const string Append = "streams.append"; - public const string Subscribe = "streams.subscribe"; - } -} \ No newline at end of file diff --git a/src/EventStore.Client/Core/Common/EnumerableTaskExtensions.cs b/src/EventStore.Client/Core/Common/EnumerableTaskExtensions.cs deleted file mode 100644 index eb4517006..000000000 --- a/src/EventStore.Client/Core/Common/EnumerableTaskExtensions.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System.Diagnostics; - -namespace EventStore.Client; - -static class EnumerableTaskExtensions { - [DebuggerStepThrough] - public static Task WhenAll(this IEnumerable source) => Task.WhenAll(source); - - [DebuggerStepThrough] - public static Task WhenAll(this IEnumerable> source) => Task.WhenAll(source); -} \ No newline at end of file diff --git a/src/EventStore.Client/Core/Common/EpochExtensions.cs b/src/EventStore.Client/Core/Common/EpochExtensions.cs deleted file mode 100644 index db59e620d..000000000 --- a/src/EventStore.Client/Core/Common/EpochExtensions.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace EventStore.Client; - -static class EpochExtensions { -#if NET - static readonly DateTime UnixEpoch = DateTime.UnixEpoch; -#else - const long TicksPerMillisecond = 10000; - const long TicksPerSecond = TicksPerMillisecond * 1000; - const long TicksPerMinute = TicksPerSecond * 60; - const long TicksPerHour = TicksPerMinute * 60; - const long TicksPerDay = TicksPerHour * 24; - const int DaysPerYear = 365; - const int DaysPer4Years = DaysPerYear * 4 + 1; - const int DaysPer100Years = DaysPer4Years * 25 - 1; - const int DaysPer400Years = DaysPer100Years * 4 + 1; - const int DaysTo1970 = DaysPer400Years * 4 + DaysPer100Years * 3 + DaysPer4Years * 17 + DaysPerYear; - const long UnixEpochTicks = DaysTo1970 * TicksPerDay; - - static readonly DateTime UnixEpoch = new(UnixEpochTicks, DateTimeKind.Utc); -#endif - - public static DateTime FromTicksSinceEpoch(this long value) => new(UnixEpoch.Ticks + value, DateTimeKind.Utc); - - public static long ToTicksSinceEpoch(this DateTime value) => (value - UnixEpoch).Ticks; -} \ No newline at end of file diff --git a/src/EventStore.Client/Core/Common/EventStoreCallOptions.cs b/src/EventStore.Client/Core/Common/EventStoreCallOptions.cs deleted file mode 100644 index e6058a170..000000000 --- a/src/EventStore.Client/Core/Common/EventStoreCallOptions.cs +++ /dev/null @@ -1,74 +0,0 @@ -using Grpc.Core; -using static System.Threading.Timeout; - -namespace EventStore.Client; - -static class EventStoreCallOptions { - // deadline falls back to infinity - public static CallOptions CreateStreaming( - EventStoreClientSettings settings, - TimeSpan? deadline = null, UserCredentials? userCredentials = null, - CancellationToken cancellationToken = default - ) => - Create(settings, deadline, userCredentials, cancellationToken); - - // deadline falls back to connection DefaultDeadline - public static CallOptions CreateNonStreaming( - EventStoreClientSettings settings, - CancellationToken cancellationToken - ) => - Create( - settings, - settings.DefaultDeadline, - settings.DefaultCredentials, - cancellationToken - ); - - public static CallOptions CreateNonStreaming( - EventStoreClientSettings settings, TimeSpan? deadline, - UserCredentials? userCredentials, CancellationToken cancellationToken - ) => - Create( - settings, - deadline ?? settings.DefaultDeadline, - userCredentials, - cancellationToken - ); - - static CallOptions Create( - EventStoreClientSettings settings, TimeSpan? deadline, - UserCredentials? userCredentials, CancellationToken cancellationToken - ) => - new( - cancellationToken: cancellationToken, - deadline: DeadlineAfter(deadline), - headers: new() { - { - Constants.Headers.RequiresLeader, - settings.ConnectivitySettings.NodePreference == NodePreference.Leader - ? bool.TrueString - : bool.FalseString - } - }, - credentials: (userCredentials ?? settings.DefaultCredentials) == null - ? null - : CallCredentials.FromInterceptor( - async (_, metadata) => { - var credentials = userCredentials ?? settings.DefaultCredentials; - - var authorizationHeader = await settings.OperationOptions - .GetAuthenticationHeaderValue(credentials!, CancellationToken.None) - .ConfigureAwait(false); - - metadata.Add(Constants.Headers.Authorization, authorizationHeader); - } - ) - ); - - static DateTime? DeadlineAfter(TimeSpan? timeoutAfter) => - !timeoutAfter.HasValue - ? new DateTime?() - : timeoutAfter.Value == TimeSpan.MaxValue || timeoutAfter.Value == InfiniteTimeSpan - ? DateTime.MaxValue - : DateTime.UtcNow.Add(timeoutAfter.Value); -} \ No newline at end of file diff --git a/src/EventStore.Client/Core/Common/MetadataExtensions.cs b/src/EventStore.Client/Core/Common/MetadataExtensions.cs deleted file mode 100644 index e547970fd..000000000 --- a/src/EventStore.Client/Core/Common/MetadataExtensions.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Grpc.Core; - -namespace EventStore.Client; - -static class MetadataExtensions { - public static bool TryGetValue(this Metadata metadata, string key, out string? value) { - value = default; - - foreach (var entry in metadata) { - if (entry.Key != key) - continue; - - value = entry.Value; - return true; - } - - return false; - } - - public static StreamRevision GetStreamRevision(this Metadata metadata, string key) => - metadata.TryGetValue(key, out var s) && ulong.TryParse(s, out var value) - ? new(value) - : StreamRevision.None; - - public static int GetIntValueOrDefault(this Metadata metadata, string key) => - metadata.TryGetValue(key, out var s) && int.TryParse(s, out var value) - ? value - : default; -} \ No newline at end of file diff --git a/src/EventStore.Client/Core/Common/Shims/Index.cs b/src/EventStore.Client/Core/Common/Shims/Index.cs deleted file mode 100644 index 357bbd34d..000000000 --- a/src/EventStore.Client/Core/Common/Shims/Index.cs +++ /dev/null @@ -1,110 +0,0 @@ -#if !NET -using System.Runtime.CompilerServices; - -namespace System; - -/// Represent a type can be used to index a collection either from the start or the end. -/// -/// Index is used by the C# compiler to support the new index syntax -/// -/// int[] someArray = new int[5] { 1, 2, 3, 4, 5 } ; -/// int lastElement = someArray[^1]; // lastElement = 5 -/// -/// -readonly struct Index : IEquatable { - readonly int _value; - - /// Construct an Index using a value and indicating if the index is from the start or from the end. - /// The index value. it has to be zero or positive number. - /// Indicating if the index is from the start or from the end. - /// - /// If the Index constructed from the end, index value 1 means pointing at the last element and index value 0 means pointing at beyond last element. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public Index(int value, bool fromEnd = false) { - if (value < 0) throw new ArgumentOutOfRangeException(nameof(value), "value must be non-negative"); - - if (fromEnd) - _value = ~value; - else - _value = value; - } - - // The following private constructors mainly created for perf reason to avoid the checks - Index(int value) => _value = value; - - /// Create an Index pointing at first element. - public static Index Start => new(0); - - /// Create an Index pointing at beyond last element. - public static Index End => new(~0); - - /// Create an Index from the start at the position indicated by the value. - /// The index value from the start. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Index FromStart(int value) { - if (value < 0) throw new ArgumentOutOfRangeException(nameof(value), "value must be non-negative"); - - return new Index(value); - } - - /// Create an Index from the end at the position indicated by the value. - /// The index value from the end. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Index FromEnd(int value) { - if (value < 0) throw new ArgumentOutOfRangeException(nameof(value), "value must be non-negative"); - - return new Index(~value); - } - - /// Returns the index value. - public int Value { - get { - if (_value < 0) - return ~_value; - else - return _value; - } - } - - /// Indicates whether the index is from the start or the end. - public bool IsFromEnd => _value < 0; - - /// Calculate the offset from the start using the giving collection length. - /// The length of the collection that the Index will be used with. length has to be a positive value - /// - /// For performance reason, we don't validate the input length parameter and the returned offset value against negative values. - /// we don't validate either the returned offset is greater than the input length. - /// It is expected Index will be used with collections which always have non negative length/count. If the returned offset is negative and - /// then used to index a collection will get out of range exception which will be same affect as the validation. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public int GetOffset(int length) { - var offset = _value; - if (IsFromEnd) - // offset = length - (~value) - // offset = length + (~(~value) + 1) - // offset = length + value + 1 - offset += length + 1; - - return offset; - } - - /// Indicates whether the current Index object is equal to another object of the same type. - /// An object to compare with this object - public override bool Equals(object? value) => value is Index && _value == ((Index)value)._value; - - /// Indicates whether the current Index object is equal to another Index object. - /// An object to compare with this object - public bool Equals(Index other) => _value == other._value; - - /// Returns the hash code for this instance. - public override int GetHashCode() => _value; - - /// Converts integer number to an Index. - public static implicit operator Index(int value) => FromStart(value); - - /// Converts the value of the current Index object to its equivalent string representation. - public override string ToString() => IsFromEnd ? $"^{(uint)Value}" : ((uint)Value).ToString(); -} -#endif \ No newline at end of file diff --git a/src/EventStore.Client/Core/Common/Shims/IsExternalInit.cs b/src/EventStore.Client/Core/Common/Shims/IsExternalInit.cs deleted file mode 100644 index 7dc4fea3d..000000000 --- a/src/EventStore.Client/Core/Common/Shims/IsExternalInit.cs +++ /dev/null @@ -1,10 +0,0 @@ -#if !NET - -using System.ComponentModel; - -// ReSharper disable once CheckNamespace -namespace System.Runtime.CompilerServices; - -[EditorBrowsable(EditorBrowsableState.Never)] -class IsExternalInit{} -#endif diff --git a/src/EventStore.Client/Core/Common/Shims/Range.cs b/src/EventStore.Client/Core/Common/Shims/Range.cs deleted file mode 100644 index 3a0b34fde..000000000 --- a/src/EventStore.Client/Core/Common/Shims/Range.cs +++ /dev/null @@ -1,75 +0,0 @@ -#if !NET -// ReSharper disable CheckNamespace - -using System.Runtime.CompilerServices; - -namespace System; - -/// Represent a range has start and end indexes. -/// -/// Range is used by the C# compiler to support the range syntax. -/// -/// int[] someArray = new int[5] { 1, 2, 3, 4, 5 }; -/// int[] subArray1 = someArray[0..2]; // { 1, 2 } -/// int[] subArray2 = someArray[1..^0]; // { 2, 3, 4, 5 } -/// -/// -readonly struct Range : IEquatable { - /// Represent the inclusive start index of the Range. - public Index Start { get; } - - /// Represent the exclusive end index of the Range. - public Index End { get; } - - /// Construct a Range object using the start and end indexes. - /// Represent the inclusive start index of the range. - /// Represent the exclusive end index of the range. - public Range(Index start, Index end) { - Start = start; - End = end; - } - - /// Indicates whether the current Range object is equal to another object of the same type. - /// An object to compare with this object - public override bool Equals(object? value) => - value is Range r && - r.Start.Equals(Start) && - r.End.Equals(End); - - /// Indicates whether the current Range object is equal to another Range object. - /// An object to compare with this object - public bool Equals(Range other) => other.Start.Equals(Start) && other.End.Equals(End); - - /// Returns the hash code for this instance. - public override int GetHashCode() => Start.GetHashCode() * 31 + End.GetHashCode(); - - /// Converts the value of the current Range object to its equivalent string representation. - public override string ToString() => $"{Start}..{End}"; - - /// Create a Range object starting from start index to the end of the collection. - public static Range StartAt(Index start) => new(start, Index.End); - - /// Create a Range object starting from first element in the collection to the end Index. - public static Range EndAt(Index end) => new(Index.Start, end); - - /// Create a Range object starting from first element to the end. - public static Range All => new(Index.Start, Index.End); - - /// Calculate the start offset and length of range object using a collection length. - /// The length of the collection that the range will be used with. length has to be a positive value. - /// - /// For performance reason, we don't validate the input length parameter against negative values. - /// It is expected Range will be used with collections which always have non negative length/count. - /// We validate the range is inside the length scope though. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public (int Offset, int Length) GetOffsetAndLength(int length) { - var start = Start.GetOffset(length); - var end = End.GetOffset(length); - - if ((uint)end > (uint)length || (uint)start > (uint)end) throw new ArgumentOutOfRangeException(nameof(length)); - - return (start, end - start); - } -} -#endif \ No newline at end of file diff --git a/src/EventStore.Client/Core/Common/Shims/TaskCompletionSource.cs b/src/EventStore.Client/Core/Common/Shims/TaskCompletionSource.cs deleted file mode 100644 index ad6573c4a..000000000 --- a/src/EventStore.Client/Core/Common/Shims/TaskCompletionSource.cs +++ /dev/null @@ -1,11 +0,0 @@ -#if !NET -// ReSharper disable CheckNamespace - -namespace System.Threading.Tasks; - -class TaskCompletionSource : TaskCompletionSource { - public void SetResult() => base.SetResult(null); - public bool TrySetResult() => base.TrySetResult(null); -} - -#endif \ No newline at end of file diff --git a/src/EventStore.Client/Core/DefaultRequestVersionHandler.cs b/src/EventStore.Client/Core/DefaultRequestVersionHandler.cs deleted file mode 100644 index b6cda6a7f..000000000 --- a/src/EventStore.Client/Core/DefaultRequestVersionHandler.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; - -namespace EventStore.Client { - internal class DefaultRequestVersionHandler : DelegatingHandler { - public DefaultRequestVersionHandler(HttpMessageHandler innerHandler) : base(innerHandler) { } - protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { - request.Version = new Version(2, 0); - return base.SendAsync(request, cancellationToken); - } - } -} diff --git a/src/EventStore.Client/Core/EndPointExtensions.cs b/src/EventStore.Client/Core/EndPointExtensions.cs deleted file mode 100644 index 8ebad38e0..000000000 --- a/src/EventStore.Client/Core/EndPointExtensions.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System; -using System.Net; - -namespace EventStore.Client { - internal static class EndPointExtensions { - public static string GetHost(this EndPoint endpoint) => - endpoint switch { - IPEndPoint ip => ip.Address.ToString(), - DnsEndPoint dns => dns.Host, - _ => throw new ArgumentOutOfRangeException(nameof(endpoint), endpoint?.GetType(), - "An invalid endpoint has been provided") - }; - - public static int GetPort(this EndPoint endpoint) => - endpoint switch { - IPEndPoint ip => ip.Port, - DnsEndPoint dns => dns.Port, - _ => throw new ArgumentOutOfRangeException(nameof(endpoint), endpoint?.GetType(), - "An invalid endpoint has been provided") - }; - - public static Uri ToUri(this EndPoint endPoint, bool https) => new UriBuilder { - Scheme = https ? Uri.UriSchemeHttps : Uri.UriSchemeHttp, - Host = endPoint.GetHost(), - Port = endPoint.GetPort() - }.Uri; - - public static string? ToHttpUrl(this EndPoint endPoint, string schema, string? rawUrl = null) => - endPoint switch { - IPEndPoint ipEndPoint => CreateHttpUrl(schema, ipEndPoint.Address.ToString(), ipEndPoint.Port, - rawUrl != null ? rawUrl.TrimStart('/') : string.Empty), - DnsEndPoint dnsEndpoint => CreateHttpUrl(schema, dnsEndpoint.Host, dnsEndpoint.Port, - rawUrl != null ? rawUrl.TrimStart('/') : string.Empty), - _ => null - }; - - private static string CreateHttpUrl(string schema, string host, int port, string path) { - return $"{schema}://{host}:{port}/{path}"; - } - } -} diff --git a/src/EventStore.Client/Core/EventData.cs b/src/EventStore.Client/Core/EventData.cs deleted file mode 100644 index 3849ff364..000000000 --- a/src/EventStore.Client/Core/EventData.cs +++ /dev/null @@ -1,65 +0,0 @@ -using System; -using System.Net.Http.Headers; - -namespace EventStore.Client { - /// - /// Represents an event to be written. - /// - public sealed class EventData { - /// - /// The raw bytes of the event data. - /// - public readonly ReadOnlyMemory Data; - - /// - /// The of the event, used as part of the idempotent write check. - /// - public readonly Uuid EventId; - - /// - /// The raw bytes of the event metadata. - /// - public readonly ReadOnlyMemory Metadata; - - /// - /// The name of the event type. It is strongly recommended that these - /// use lowerCamelCase if projections are to be used. - /// - public readonly string Type; - - /// - /// The Content-Type of the . Valid values are 'application/json' and 'application/octet-stream'. - /// - public readonly string ContentType; - - /// - /// Constructs a new . - /// - /// The of the event, used as part of the idempotent write check. - /// The name of the event type. It is strongly recommended that these use lowerCamelCase if projections are to be used. - /// The raw bytes of the event data. - /// The raw bytes of the event metadata. - /// The Content-Type of the . Valid values are 'application/json' and 'application/octet-stream'. - /// - public EventData(Uuid eventId, string type, ReadOnlyMemory data, ReadOnlyMemory? metadata = null, - string contentType = Constants.Metadata.ContentTypes.ApplicationJson) { - if (eventId == Uuid.Empty) { - throw new ArgumentOutOfRangeException(nameof(eventId)); - } - - MediaTypeHeaderValue.Parse(contentType); - - if (contentType != Constants.Metadata.ContentTypes.ApplicationJson && - contentType != Constants.Metadata.ContentTypes.ApplicationOctetStream) { - throw new ArgumentOutOfRangeException(nameof(contentType), contentType, - $"Only {Constants.Metadata.ContentTypes.ApplicationJson} or {Constants.Metadata.ContentTypes.ApplicationOctetStream} are acceptable values."); - } - - EventId = eventId; - Type = type; - Data = data; - Metadata = metadata ?? Array.Empty(); - ContentType = contentType; - } - } -} diff --git a/src/EventStore.Client/Core/EventRecord.cs b/src/EventStore.Client/Core/EventRecord.cs deleted file mode 100644 index 67a7b04cf..000000000 --- a/src/EventStore.Client/Core/EventRecord.cs +++ /dev/null @@ -1,83 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace EventStore.Client { - /// - /// Represents a previously written event. - /// - public class EventRecord { - /// - /// The stream that this event belongs to. - /// - public readonly string EventStreamId; - - /// - /// The representing this event. - /// - public readonly Uuid EventId; - - /// - /// The of this event in the stream. - /// - public readonly StreamPosition EventNumber; - - /// - /// The type of event this is. - /// - public readonly string EventType; - - /// - /// The raw bytes representing the data of this event. - /// - public readonly ReadOnlyMemory Data; - - /// - /// The raw bytes representing the metadata of this event. - /// - public readonly ReadOnlyMemory Metadata; - - /// - /// A UTC representing when this event was created in the system. - /// - public readonly DateTime Created; - - /// - /// The of this event in the $all stream. - /// - public readonly Position Position; - - /// - /// The Content-Type of the event's data. - /// - public readonly string ContentType; - - /// - /// Constructs a new . - /// - /// - /// - /// - /// - /// - /// - /// - public EventRecord( - string eventStreamId, - Uuid eventId, - StreamPosition eventNumber, - Position position, - IDictionary metadata, - ReadOnlyMemory data, - ReadOnlyMemory customMetadata) { - EventStreamId = eventStreamId; - EventId = eventId; - EventNumber = eventNumber; - Position = position; - Data = data; - Metadata = customMetadata; - EventType = metadata[Constants.Metadata.Type]; - Created = Convert.ToInt64(metadata[Constants.Metadata.Created]).FromTicksSinceEpoch(); - ContentType = metadata[Constants.Metadata.ContentType]; - } - } -} diff --git a/src/EventStore.Client/Core/EventStoreClientBase.cs b/src/EventStore.Client/Core/EventStoreClientBase.cs deleted file mode 100644 index 2b4a5b2f6..000000000 --- a/src/EventStore.Client/Core/EventStoreClientBase.cs +++ /dev/null @@ -1,151 +0,0 @@ -using EventStore.Client.Interceptors; -using Grpc.Core; -using Grpc.Core.Interceptors; -using Enum = System.Enum; - -namespace EventStore.Client { - /// - /// The base class used by clients used to communicate with the EventStoreDB. - /// - public abstract class EventStoreClientBase : - IDisposable, // for grpc.net we can dispose synchronously, but not for grpc.core - IAsyncDisposable { - private readonly Dictionary> _exceptionMap; - private readonly CancellationTokenSource _cts; - private readonly ChannelCache _channelCache; - private readonly SharingProvider _channelInfoProvider; - private readonly Lazy _httpFallback; - - /// The name of the connection. - public string ConnectionName { get; } - - /// The . - protected EventStoreClientSettings Settings { get; } - - /// Constructs a new . - protected EventStoreClientBase( - EventStoreClientSettings? settings, - Dictionary> exceptionMap - ) { - Settings = settings ?? new EventStoreClientSettings(); - _exceptionMap = exceptionMap; - _cts = new CancellationTokenSource(); - _channelCache = new(Settings); - _httpFallback = new Lazy(() => new HttpFallback(Settings)); - - ConnectionName = Settings.ConnectionName ?? $"ES-{Guid.NewGuid()}"; - - var channelSelector = new ChannelSelector(Settings, _channelCache); - _channelInfoProvider = new SharingProvider( - factory: (endPoint, onBroken) => - GetChannelInfoExpensive(endPoint, onBroken, channelSelector, _cts.Token), - factoryRetryDelay: Settings.ConnectivitySettings.DiscoveryInterval, - initialInput: ReconnectionRequired.Rediscover.Instance, - loggerFactory: Settings.LoggerFactory - ); - } - - // Select a channel and query its capabilities. This is an expensive call that - // we don't want to do often. - private async Task GetChannelInfoExpensive( - ReconnectionRequired reconnectionRequired, - Action onReconnectionRequired, - IChannelSelector channelSelector, - CancellationToken cancellationToken - ) { - var channel = reconnectionRequired switch { - ReconnectionRequired.Rediscover => await channelSelector.SelectChannelAsync(cancellationToken) - .ConfigureAwait(false), - ReconnectionRequired.NewLeader (var endPoint) => channelSelector.SelectChannel(endPoint), - _ => throw new ArgumentException(null, nameof(reconnectionRequired)) - }; - - var invoker = channel.CreateCallInvoker() - .Intercept(new TypedExceptionInterceptor(_exceptionMap)) - .Intercept(new ConnectionNameInterceptor(ConnectionName)) - .Intercept(new ReportLeaderInterceptor(onReconnectionRequired)); - - if (Settings.Interceptors is not null) { - foreach (var interceptor in Settings.Interceptors) { - invoker = invoker.Intercept(interceptor); - } - } - - var caps = await new GrpcServerCapabilitiesClient(Settings) - .GetAsync(invoker, cancellationToken) - .ConfigureAwait(false); - - return new(channel, caps, invoker); - } - - /// Gets the current channel info. - protected async ValueTask GetChannelInfo(CancellationToken cancellationToken) => - await _channelInfoProvider.CurrentAsync.WithCancellation(cancellationToken).ConfigureAwait(false); - - /// - /// Only exists so that we can manually trigger rediscovery in the tests - /// in cases where the server doesn't yet let the client know that it needs to. - /// note if rediscovery is already in progress it will continue, not restart. - /// - internal Task RediscoverAsync() { - _channelInfoProvider.Reset(); - return Task.CompletedTask; - } - - /// Returns the result of an HTTP Get request based on the client settings. - protected async Task HttpGet( - string path, Action onNotFound, ChannelInfo channelInfo, - TimeSpan? deadline, UserCredentials? userCredentials, CancellationToken cancellationToken - ) { - return await _httpFallback.Value - .HttpGetAsync(path, channelInfo, deadline, userCredentials, onNotFound, cancellationToken) - .ConfigureAwait(false); - } - - /// Executes an HTTP Post request based on the client settings. - protected async Task HttpPost( - string path, string query, Action onNotFound, ChannelInfo channelInfo, - TimeSpan? deadline, UserCredentials? userCredentials, CancellationToken cancellationToken - ) { - await _httpFallback.Value - .HttpPostAsync( - path, - query, - channelInfo, - deadline, - userCredentials, - onNotFound, - cancellationToken - ) - .ConfigureAwait(false); - } - - /// - public virtual void Dispose() { - _channelInfoProvider.Dispose(); - _cts.Cancel(); - _cts.Dispose(); - _channelCache.Dispose(); - - if (_httpFallback.IsValueCreated) { - _httpFallback.Value.Dispose(); - } - } - - /// - public virtual async ValueTask DisposeAsync() { - _channelInfoProvider.Dispose(); - _cts.Cancel(); - _cts.Dispose(); - await _channelCache.DisposeAsync().ConfigureAwait(false); - - if (_httpFallback.IsValueCreated) { - _httpFallback.Value.Dispose(); - } - } - - /// Returns an InvalidOperation exception. - protected Exception InvalidOption(T option) where T : Enum => - new InvalidOperationException($"The {typeof(T).Name} {option:x} was not valid."); - } -} diff --git a/src/EventStore.Client/Core/EventStoreClientConnectivitySettings.cs b/src/EventStore.Client/Core/EventStoreClientConnectivitySettings.cs deleted file mode 100644 index 92890d2f2..000000000 --- a/src/EventStore.Client/Core/EventStoreClientConnectivitySettings.cs +++ /dev/null @@ -1,128 +0,0 @@ -using System; -using System.Net; -using System.Security.Cryptography.X509Certificates; - -namespace EventStore.Client { - /// - /// A class used to describe how to connect to an instance of EventStoreDB. - /// - public class EventStoreClientConnectivitySettings { - private const int DefaultPort = 2113; - private bool _insecure; - private Uri? _address; - - /// - /// The of the EventStoreDB. Use this when connecting to a single node. - /// - public Uri? Address { - get => IsSingleNode ? _address : null; - set => _address = value; - } - - internal Uri ResolvedAddressOrDefault => Address ?? DefaultAddress; - - private Uri DefaultAddress => - new UriBuilder { - Scheme = _insecure ? Uri.UriSchemeHttp : Uri.UriSchemeHttps, - Port = DefaultPort - }.Uri; - - /// - /// The maximum number of times to attempt discovery. - /// - public int MaxDiscoverAttempts { get; set; } - - /// - /// An array of s used to seed gossip. - /// - public EndPoint[] GossipSeeds => - ((object?)DnsGossipSeeds ?? IpGossipSeeds) switch { - DnsEndPoint[] dns => Array.ConvertAll(dns, x => x), - IPEndPoint[] ip => Array.ConvertAll(ip, x => x), - _ => Array.Empty() - }; - - /// - /// An array of s to use for seeding gossip. This will be checked before . - /// - public DnsEndPoint[]? DnsGossipSeeds { get; set; } - - /// - /// An array of s to use for seeding gossip. This will be checked after . - /// - public IPEndPoint[]? IpGossipSeeds { get; set; } - - /// - /// The after which an attempt to discover gossip will fail. - /// - public TimeSpan GossipTimeout { get; set; } - - /// - /// Whether or not to use HTTPS when communicating via gossip. - /// - [Obsolete] - public bool GossipOverHttps { get; set; } = true; - - /// - /// The polling interval used to discover the . - /// - public TimeSpan DiscoveryInterval { get; set; } - - /// - /// The to use when connecting. - /// - public NodePreference NodePreference { get; set; } - - /// - /// The optional amount of time to wait after which a keepalive ping is sent on the transport. - /// - public TimeSpan KeepAliveInterval { get; set; } = TimeSpan.FromSeconds(10); - - /// - /// The optional amount of time to wait after which a sent keepalive ping is considered timed out. - /// - public TimeSpan KeepAliveTimeout { get; set; } = TimeSpan.FromSeconds(10); - - /// - /// True if pointing to a single EventStoreDB node. - /// - public bool IsSingleNode => GossipSeeds.Length == 0; - - /// - /// True if communicating over an insecure channel; otherwise false. - /// - public bool Insecure { - get => IsSingleNode ? string.Equals(Address?.Scheme, Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase) : _insecure; - set => _insecure = value; - } - - /// - /// True if certificates will be validated; otherwise false. - /// - public bool TlsVerifyCert { get; set; } = true; - - /// - /// Path to a certificate file for secure connection. Not required for enabling secure connection. Useful for self-signed certificate - /// that are not installed on the system trust store. - /// - public X509Certificate2? TlsCaFile { get; set; } - - /// - /// Client certificate used for user authentication. - /// - public X509Certificate2? ClientCertificate { get; set; } - - /// - /// The default . - /// - public static EventStoreClientConnectivitySettings Default => new EventStoreClientConnectivitySettings { - MaxDiscoverAttempts = 10, - GossipTimeout = TimeSpan.FromSeconds(5), - DiscoveryInterval = TimeSpan.FromMilliseconds(100), - NodePreference = NodePreference.Leader, - KeepAliveInterval = TimeSpan.FromSeconds(10), - KeepAliveTimeout = TimeSpan.FromSeconds(10), - TlsVerifyCert = true, - }; - } -} diff --git a/src/EventStore.Client/Core/EventStoreClientOperationOptions.cs b/src/EventStore.Client/Core/EventStoreClientOperationOptions.cs deleted file mode 100644 index 94561e213..000000000 --- a/src/EventStore.Client/Core/EventStoreClientOperationOptions.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System; -using System.Threading; -using System.Threading.Tasks; - -namespace EventStore.Client { - /// - /// A class representing the options to apply to an individual operation. - /// - public class EventStoreClientOperationOptions { - /// - /// Whether or not to immediately throw a when an append fails. - /// - public bool ThrowOnAppendFailure { get; set; } - - /// - /// The batch size, in bytes. - /// - public int BatchAppendSize { get; set; } - - /// - /// A callback function to extract the authorize header value from the used in the operation. - /// - public Func> GetAuthenticationHeaderValue { get; set; } = - null!; - - /// - /// The default . - /// - public static EventStoreClientOperationOptions Default => new() { - ThrowOnAppendFailure = true, - GetAuthenticationHeaderValue = (userCredentials, _) => new ValueTask(userCredentials.ToString()), - BatchAppendSize = 3 * 1024 * 1024 - }; - - - /// - /// Clones a copy of the current . - /// - /// - public EventStoreClientOperationOptions Clone() => new() { - ThrowOnAppendFailure = ThrowOnAppendFailure, - GetAuthenticationHeaderValue = GetAuthenticationHeaderValue, - BatchAppendSize = BatchAppendSize - }; - } -} diff --git a/src/EventStore.Client/Core/EventStoreClientSettings.ConnectionString.cs b/src/EventStore.Client/Core/EventStoreClientSettings.ConnectionString.cs deleted file mode 100644 index 2aadebf64..000000000 --- a/src/EventStore.Client/Core/EventStoreClientSettings.ConnectionString.cs +++ /dev/null @@ -1,410 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Net.Security; -using System.Security.Authentication; -using System.Security.Cryptography; -using System.Security.Cryptography.X509Certificates; -using Timeout_ = System.Threading.Timeout; - -namespace EventStore.Client { - public partial class EventStoreClientSettings { - /// - /// Creates client settings from a connection string - /// - /// - /// - public static EventStoreClientSettings Create(string connectionString) => - ConnectionStringParser.Parse(connectionString); - - private static class ConnectionStringParser { - private const string SchemeSeparator = "://"; - private const string UserInfoSeparator = "@"; - private const string Colon = ":"; - private const string Slash = "/"; - private const string Comma = ","; - private const string Ampersand = "&"; - private const string Equal = "="; - private const string QuestionMark = "?"; - - private const string Tls = nameof(Tls); - private const string ConnectionName = nameof(ConnectionName); - private const string MaxDiscoverAttempts = nameof(MaxDiscoverAttempts); - private const string DiscoveryInterval = nameof(DiscoveryInterval); - private const string GossipTimeout = nameof(GossipTimeout); - private const string NodePreference = nameof(NodePreference); - private const string TlsVerifyCert = nameof(TlsVerifyCert); - private const string TlsCaFile = nameof(TlsCaFile); - private const string DefaultDeadline = nameof(DefaultDeadline); - private const string ThrowOnAppendFailure = nameof(ThrowOnAppendFailure); - private const string KeepAliveInterval = nameof(KeepAliveInterval); - private const string KeepAliveTimeout = nameof(KeepAliveTimeout); - private const string UserCertFile = nameof(UserCertFile); - private const string UserKeyFile = nameof(UserKeyFile); - - private const string UriSchemeDiscover = "esdb+discover"; - - private static readonly string[] Schemes = { "esdb", UriSchemeDiscover }; - private static readonly int DefaultPort = EventStoreClientConnectivitySettings.Default.ResolvedAddressOrDefault.Port; - private static readonly bool DefaultUseTls = true; - - private static readonly Dictionary SettingsType = - new(StringComparer.InvariantCultureIgnoreCase) { - { ConnectionName, typeof(string) }, - { MaxDiscoverAttempts, typeof(int) }, - { DiscoveryInterval, typeof(int) }, - { GossipTimeout, typeof(int) }, - { NodePreference, typeof(string) }, - { Tls, typeof(bool) }, - { TlsVerifyCert, typeof(bool) }, - { TlsCaFile, typeof(string) }, - { DefaultDeadline, typeof(int) }, - { ThrowOnAppendFailure, typeof(bool) }, - { KeepAliveInterval, typeof(int) }, - { KeepAliveTimeout, typeof(int) }, - { UserCertFile, typeof(string) }, - { UserKeyFile, typeof(string) }, - }; - - public static EventStoreClientSettings Parse(string connectionString) { - var currentIndex = 0; - var schemeIndex = connectionString.IndexOf(SchemeSeparator, currentIndex, StringComparison.Ordinal); - if (schemeIndex == -1) - throw new NoSchemeException(); - - var scheme = ParseScheme(connectionString.Substring(0, schemeIndex)); - - currentIndex = schemeIndex + SchemeSeparator.Length; - var userInfoIndex = connectionString.IndexOf(UserInfoSeparator, currentIndex, StringComparison.Ordinal); - (string user, string pass)? userInfo = null; - if (userInfoIndex != -1) { - userInfo = ParseUserInfo(connectionString.Substring(currentIndex, userInfoIndex - currentIndex)); - currentIndex = userInfoIndex + UserInfoSeparator.Length; - } - - var slashIndex = connectionString.IndexOf(Slash, currentIndex, StringComparison.Ordinal); - var questionMarkIndex = connectionString.IndexOf(QuestionMark, currentIndex, StringComparison.Ordinal); - var endIndex = connectionString.Length; - - //for simpler substring operations: - if (slashIndex == -1) slashIndex = int.MaxValue; - if (questionMarkIndex == -1) questionMarkIndex = int.MaxValue; - - var hostSeparatorIndex = Math.Min(Math.Min(slashIndex, questionMarkIndex), endIndex); - var hosts = ParseHosts(connectionString.Substring(currentIndex, hostSeparatorIndex - currentIndex)); - currentIndex = hostSeparatorIndex; - - string path = ""; - if (slashIndex != int.MaxValue) - path = connectionString.Substring( - currentIndex, - Math.Min(questionMarkIndex, endIndex) - currentIndex - ); - - if (path != "" && path != "/") - throw new ConnectionStringParseException( - $"The specified path must be either an empty string or a forward slash (/) but the following path was found instead: '{path}'" - ); - - var options = new Dictionary(); - if (questionMarkIndex != int.MaxValue) { - currentIndex = questionMarkIndex + QuestionMark.Length; - options = ParseKeyValuePairs(connectionString.Substring(currentIndex)); - } - - return CreateSettings(scheme, userInfo, hosts, options); - } - - private static EventStoreClientSettings CreateSettings( - string scheme, (string user, string pass)? userInfo, - EndPoint[] hosts, Dictionary options - ) { - var settings = new EventStoreClientSettings { - ConnectivitySettings = EventStoreClientConnectivitySettings.Default, - OperationOptions = EventStoreClientOperationOptions.Default - }; - - if (userInfo.HasValue) - settings.DefaultCredentials = new UserCredentials(userInfo.Value.user, userInfo.Value.pass); - - var typedOptions = new Dictionary(StringComparer.InvariantCultureIgnoreCase); - foreach (var kv in options) { - if (!SettingsType.TryGetValue(kv.Key, out var type)) - throw new InvalidSettingException($"Unknown option: {kv.Key}"); - - if (type == typeof(int)) { - if (!int.TryParse(kv.Value, out var intValue)) - throw new InvalidSettingException($"{kv.Key} must be an integer value"); - - typedOptions.Add(kv.Key, intValue); - } else if (type == typeof(bool)) { - if (!bool.TryParse(kv.Value, out var boolValue)) - throw new InvalidSettingException($"{kv.Key} must be either true or false"); - - typedOptions.Add(kv.Key, boolValue); - } else if (type == typeof(string)) { - typedOptions.Add(kv.Key, kv.Value); - } - } - - if (typedOptions.TryGetValue(ConnectionName, out object? connectionName)) - settings.ConnectionName = (string)connectionName; - - if (typedOptions.TryGetValue(MaxDiscoverAttempts, out object? maxDiscoverAttempts)) - settings.ConnectivitySettings.MaxDiscoverAttempts = (int)maxDiscoverAttempts; - - if (typedOptions.TryGetValue(DiscoveryInterval, out object? discoveryInterval)) - settings.ConnectivitySettings.DiscoveryInterval = TimeSpan.FromMilliseconds((int)discoveryInterval); - - if (typedOptions.TryGetValue(GossipTimeout, out object? gossipTimeout)) - settings.ConnectivitySettings.GossipTimeout = TimeSpan.FromMilliseconds((int)gossipTimeout); - - if (typedOptions.TryGetValue(NodePreference, out object? nodePreference)) { - settings.ConnectivitySettings.NodePreference = ((string)nodePreference).ToLowerInvariant() switch { - "leader" => EventStore.Client.NodePreference.Leader, - "follower" => EventStore.Client.NodePreference.Follower, - "random" => EventStore.Client.NodePreference.Random, - "readonlyreplica" => EventStore.Client.NodePreference.ReadOnlyReplica, - _ => throw new InvalidSettingException($"Invalid NodePreference: {nodePreference}") - }; - } - - var useTls = DefaultUseTls; - if (typedOptions.TryGetValue(Tls, out object? tls)) { - useTls = (bool)tls; - } - - if (typedOptions.TryGetValue(DefaultDeadline, out object? operationTimeout)) - settings.DefaultDeadline = TimeSpan.FromMilliseconds((int)operationTimeout); - - if (typedOptions.TryGetValue(ThrowOnAppendFailure, out object? throwOnAppendFailure)) - settings.OperationOptions.ThrowOnAppendFailure = (bool)throwOnAppendFailure; - - if (typedOptions.TryGetValue(KeepAliveInterval, out var keepAliveIntervalMs)) { - settings.ConnectivitySettings.KeepAliveInterval = keepAliveIntervalMs switch { - -1 => Timeout_.InfiniteTimeSpan, - int value and >= 0 => TimeSpan.FromMilliseconds(value), - _ => throw new InvalidSettingException($"Invalid KeepAliveInterval: {keepAliveIntervalMs}") - }; - } - - if (typedOptions.TryGetValue(KeepAliveTimeout, out var keepAliveTimeoutMs)) { - settings.ConnectivitySettings.KeepAliveTimeout = keepAliveTimeoutMs switch { - -1 => Timeout_.InfiniteTimeSpan, - int value and >= 0 => TimeSpan.FromMilliseconds(value), - _ => throw new InvalidSettingException($"Invalid KeepAliveTimeout: {keepAliveTimeoutMs}") - }; - } - - settings.ConnectivitySettings.Insecure = !useTls; - - if (hosts.Length == 1 && scheme != UriSchemeDiscover) { - settings.ConnectivitySettings.Address = hosts[0].ToUri(useTls); - } else { - if (hosts.Any(x => x is DnsEndPoint)) - settings.ConnectivitySettings.DnsGossipSeeds = - Array.ConvertAll(hosts, x => new DnsEndPoint(x.GetHost(), x.GetPort())); - else - settings.ConnectivitySettings.IpGossipSeeds = Array.ConvertAll(hosts, x => (IPEndPoint)x); - } - - if (typedOptions.TryGetValue(TlsVerifyCert, out var tlsVerifyCert)) { - settings.ConnectivitySettings.TlsVerifyCert = (bool)tlsVerifyCert; - } - - if (typedOptions.TryGetValue(TlsCaFile, out var tlsCaFile)) { - var tlsCaFilePath = Path.GetFullPath((string)tlsCaFile); - if (!string.IsNullOrEmpty(tlsCaFilePath) && !File.Exists(tlsCaFilePath)) { - throw new InvalidClientCertificateException($"Failed to load certificate. File was not found."); - } - - try { -#if NET9_0_OR_GREATER - settings.ConnectivitySettings.TlsCaFile = X509CertificateLoader.LoadCertificateFromFile(tlsCaFilePath); -#else - settings.ConnectivitySettings.TlsCaFile = new X509Certificate2(tlsCaFilePath); -#endif - } catch (CryptographicException) { - throw new InvalidClientCertificateException("Failed to load certificate. Invalid file format."); - } - } - - ConfigureClientCertificate(settings, typedOptions); - - settings.CreateHttpMessageHandler = CreateDefaultHandler; - - return settings; - -#if NET48 - HttpMessageHandler CreateDefaultHandler() { - if (settings.CreateHttpMessageHandler is not null) - return settings.CreateHttpMessageHandler.Invoke(); - - var handler = new WinHttpHandler { - TcpKeepAliveEnabled = true, - TcpKeepAliveTime = settings.ConnectivitySettings.KeepAliveTimeout, - TcpKeepAliveInterval = settings.ConnectivitySettings.KeepAliveInterval, - EnableMultipleHttp2Connections = true - }; - - if (settings.ConnectivitySettings.Insecure) return handler; - - if (settings.ConnectivitySettings.ClientCertificate is not null) - handler.ClientCertificates.Add(settings.ConnectivitySettings.ClientCertificate); - - handler.ServerCertificateValidationCallback = settings.ConnectivitySettings.TlsVerifyCert switch { - false => delegate { return true; }, - true when settings.ConnectivitySettings.TlsCaFile is not null => (sender, certificate, chain, errors) => { - if (chain is null) return false; - - chain.ChainPolicy.ExtraStore.Add(settings.ConnectivitySettings.TlsCaFile); - return chain.Build(certificate); - }, - _ => null - }; - - return handler; - } -#else - HttpMessageHandler CreateDefaultHandler() { - var handler = new SocketsHttpHandler { - KeepAlivePingDelay = settings.ConnectivitySettings.KeepAliveInterval, - KeepAlivePingTimeout = settings.ConnectivitySettings.KeepAliveTimeout, - EnableMultipleHttp2Connections = true - }; - - if (settings.ConnectivitySettings.Insecure) - return handler; - - if (settings.ConnectivitySettings.ClientCertificate is not null) { - handler.SslOptions.ClientCertificates = new X509CertificateCollection { - settings.ConnectivitySettings.ClientCertificate - }; - } - - handler.SslOptions.RemoteCertificateValidationCallback = settings.ConnectivitySettings.TlsVerifyCert switch { - false => delegate { return true; }, - true when settings.ConnectivitySettings.TlsCaFile is not null => (sender, certificate, chain, errors) => { - if (certificate is not X509Certificate2 peerCertificate || chain is null) return false; - - chain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust; - chain.ChainPolicy.CustomTrustStore.Add(settings.ConnectivitySettings.TlsCaFile); - return chain.Build(peerCertificate); - }, - _ => null - }; - - return handler; - } -#endif - } - - static void ConfigureClientCertificate(EventStoreClientSettings settings, IReadOnlyDictionary options) { - var certPemFilePath = GetOptionValueAsString(UserCertFile); - var keyPemFilePath = GetOptionValueAsString(UserKeyFile); - - if (string.IsNullOrEmpty(certPemFilePath) && string.IsNullOrEmpty(keyPemFilePath)) - return; - - if (string.IsNullOrEmpty(certPemFilePath) || string.IsNullOrEmpty(keyPemFilePath)) - throw new InvalidClientCertificateException("Invalid client certificate settings. Both UserCertFile and UserKeyFile must be set."); - - if (!File.Exists(certPemFilePath)) - throw new InvalidClientCertificateException( - $"Invalid client certificate settings. The specified UserCertFile does not exist: {certPemFilePath}" - ); - - if (!File.Exists(keyPemFilePath)) - throw new InvalidClientCertificateException( - $"Invalid client certificate settings. The specified UserKeyFile does not exist: {keyPemFilePath}" - ); - - try { - settings.ConnectivitySettings.ClientCertificate = - X509Certificates.CreateFromPemFile(certPemFilePath, keyPemFilePath); - } catch (Exception ex) { - throw new InvalidClientCertificateException("Failed to create client certificate.", ex); - } - - return; - - string GetOptionValueAsString(string key) => options.TryGetValue(key, out var value) ? (string)value : ""; - } - - private static string ParseScheme(string s) => - !Schemes.Contains(s) ? throw new InvalidSchemeException(s, Schemes) : s; - - private static (string, string) ParseUserInfo(string s) { - var tokens = s.Split(Colon[0]); - if (tokens.Length != 2) throw new InvalidUserCredentialsException(s); - - return (tokens[0], tokens[1]); - } - - private static EndPoint[] ParseHosts(string s) { - var hostsTokens = s.Split(Comma[0]); - var hosts = new List(); - foreach (var hostToken in hostsTokens) { - var hostPortToken = hostToken.Split(Colon[0]); - string host; - int port; - switch (hostPortToken.Length) { - case 1: - host = hostPortToken[0]; - port = DefaultPort; - break; - - case 2: { - host = hostPortToken[0]; - if (!int.TryParse(hostPortToken[1], out port)) - throw new InvalidHostException(hostToken); - - break; - } - - default: - throw new InvalidHostException(hostToken); - } - - if (host.Length == 0) { - throw new InvalidHostException(hostToken); - } - - if (IPAddress.TryParse(host, out IPAddress? ip)) { - hosts.Add(new IPEndPoint(ip, port)); - } else { - hosts.Add(new DnsEndPoint(host, port)); - } - } - - return hosts.ToArray(); - } - - private static Dictionary ParseKeyValuePairs(string s) { - var options = new Dictionary(StringComparer.InvariantCultureIgnoreCase); - var optionsTokens = s.Split(Ampersand[0]); - foreach (var optionToken in optionsTokens) { - var (key, val) = ParseKeyValuePair(optionToken); - try { - options.Add(key, val); - } catch (ArgumentException) { - throw new DuplicateKeyException(key); - } - } - - return options; - } - - private static (string, string) ParseKeyValuePair(string s) { - var keyValueToken = s.Split(Equal[0]); - if (keyValueToken.Length != 2) { - throw new InvalidKeyValuePairException(s); - } - - return (keyValueToken[0], keyValueToken[1]); - } - } - } -} diff --git a/src/EventStore.Client/Core/EventStoreClientSettings.cs b/src/EventStore.Client/Core/EventStoreClientSettings.cs deleted file mode 100644 index f6a65219e..000000000 --- a/src/EventStore.Client/Core/EventStoreClientSettings.cs +++ /dev/null @@ -1,61 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Net.Http; -using Grpc.Core; -using Grpc.Core.Interceptors; - -using Microsoft.Extensions.Logging; - -namespace EventStore.Client { - /// - /// A class that represents the settings to use for operations made from an implementation of . - /// - public partial class EventStoreClientSettings { - /// - /// An optional list of s to use. - /// - public IEnumerable? Interceptors { get; set; } - - /// - /// The name of the connection. - /// - public string? ConnectionName { get; set; } - - /// - /// An optional factory. - /// - public Func? CreateHttpMessageHandler { get; set; } - - /// - /// An optional to use. - /// - public ILoggerFactory? LoggerFactory { get; set; } - - /// - /// The optional to use when creating the . - /// - public ChannelCredentials? ChannelCredentials { get; set; } - - /// - /// The default to use. - /// - public EventStoreClientOperationOptions OperationOptions { get; set; } = - EventStoreClientOperationOptions.Default; - - /// - /// The to use. - /// - public EventStoreClientConnectivitySettings ConnectivitySettings { get; set; } = - EventStoreClientConnectivitySettings.Default; - - /// - /// The optional to use if none have been supplied to the operation. - /// - public UserCredentials? DefaultCredentials { get; set; } - - /// - /// The default deadline for calls. Will not be applied to reads or subscriptions. - /// - public TimeSpan? DefaultDeadline { get; set; } = TimeSpan.FromSeconds(10); - } -} diff --git a/src/EventStore.Client/Core/EventTypeFilter.cs b/src/EventStore.Client/Core/EventTypeFilter.cs deleted file mode 100644 index 1e53e84e9..000000000 --- a/src/EventStore.Client/Core/EventTypeFilter.cs +++ /dev/null @@ -1,145 +0,0 @@ -using System; -using System.Linq; -using System.Text.RegularExpressions; - -namespace EventStore.Client { - /// - /// A structure representing a filter on event types for read operations. - /// - public readonly struct EventTypeFilter : IEquatable, IEventFilter { - /// - /// An empty . - /// - public static readonly EventTypeFilter None = default; - - readonly PrefixFilterExpression[] _prefixes; - - - /// - public PrefixFilterExpression[] Prefixes => _prefixes ?? Array.Empty(); - - /// - public RegularFilterExpression Regex { get; } - - /// - public uint? MaxSearchWindow { get; } - - /// - /// An that excludes system events (i.e., those whose types start with $). - /// - /// - /// - public static EventTypeFilter ExcludeSystemEvents(uint maxSearchWindow = 32) => - new EventTypeFilter(maxSearchWindow, RegularFilterExpression.ExcludeSystemEvents); - - /// - /// Creates an from a single prefix. - /// - /// - /// - public static IEventFilter Prefix(string prefix) - => new EventTypeFilter(new PrefixFilterExpression(prefix)); - - /// - /// Creates an from multiple prefixes. - /// - /// - /// - public static IEventFilter Prefix(params string[] prefixes) - => new EventTypeFilter(Array.ConvertAll(prefixes, prefix => new PrefixFilterExpression(prefix))); - - /// - /// Creates an from a search window and multiple prefixes. - /// - /// - /// - /// - public static IEventFilter Prefix(uint maxSearchWindow, params string[] prefixes) - => new EventTypeFilter(maxSearchWindow, - Array.ConvertAll(prefixes, prefix => new PrefixFilterExpression(prefix))); - - /// - /// Creates an from a regular expression and a search window. - /// - /// - /// - /// - public static IEventFilter RegularExpression(string regex, uint maxSearchWindow = 32) - => new EventTypeFilter(maxSearchWindow, new RegularFilterExpression(regex)); - - /// - /// Creates an from a regular expression and a search window. - /// - /// - /// - /// - public static IEventFilter RegularExpression(Regex regex, uint maxSearchWindow = 32) - => new EventTypeFilter(maxSearchWindow, new RegularFilterExpression(regex)); - - EventTypeFilter(uint maxSearchWindow, RegularFilterExpression regex) { - if (maxSearchWindow == 0) { - throw new ArgumentOutOfRangeException(nameof(maxSearchWindow), - maxSearchWindow, $"{nameof(maxSearchWindow)} must be greater than 0."); - } - - Regex = regex; - _prefixes = Array.Empty(); - MaxSearchWindow = maxSearchWindow; - } - - EventTypeFilter(params PrefixFilterExpression[] prefixes) : this(32, prefixes) { } - - EventTypeFilter(uint maxSearchWindow, params PrefixFilterExpression[] prefixes) { - if (prefixes.Length == 0) { - throw new ArgumentException(); - } - - if (maxSearchWindow == 0) { - throw new ArgumentOutOfRangeException(nameof(maxSearchWindow), - maxSearchWindow, $"{nameof(maxSearchWindow)} must be greater than 0."); - } - - _prefixes = prefixes; - Regex = RegularFilterExpression.None; - MaxSearchWindow = maxSearchWindow; - } - - /// - public bool Equals(EventTypeFilter other) => - Prefixes == null || other.Prefixes == null - ? Prefixes == other.Prefixes && - Regex.Equals(other.Regex) && - MaxSearchWindow.Equals(other.MaxSearchWindow) - : Prefixes.SequenceEqual(other.Prefixes) && - Regex.Equals(other.Regex) && - MaxSearchWindow.Equals(other.MaxSearchWindow); - - /// - public override bool Equals(object? obj) => obj is EventTypeFilter other && Equals(other); - - /// - public override int GetHashCode() => HashCode.Hash.Combine(Prefixes).Combine(Regex).Combine(MaxSearchWindow); - - /// - /// Compares left and right for equality. - /// - /// - /// - /// True if left is equal to right. - public static bool operator ==(EventTypeFilter left, EventTypeFilter right) => left.Equals(right); - - /// - /// Compares left and right for inequality. - /// - /// - /// - /// True if left is not equal to right. - public static bool operator !=(EventTypeFilter left, EventTypeFilter right) => !left.Equals(right); - - /// - public override string ToString() => - this == None - ? "(none)" - : $"{nameof(EventTypeFilter)} {(Prefixes.Length == 0 ? Regex.ToString() : $"[{string.Join(", ", Prefixes)}]")}"; - } -} diff --git a/src/EventStore.Client/Core/Exceptions/AccessDeniedException.cs b/src/EventStore.Client/Core/Exceptions/AccessDeniedException.cs deleted file mode 100644 index cfd69ee6d..000000000 --- a/src/EventStore.Client/Core/Exceptions/AccessDeniedException.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; - -namespace EventStore.Client { - /// - /// Exception thrown when a user is not authorised to carry out - /// an operation. - /// - public class AccessDeniedException : Exception { - /// - /// Constructs a new . - /// - public AccessDeniedException(string message, Exception innerException) : base(message, innerException) { - } - - /// - /// Constructs a new . - /// - public AccessDeniedException() : base("Access denied.") { - - } - } -} diff --git a/src/EventStore.Client/Core/Exceptions/ConnectionString/ConnectionStringParseException.cs b/src/EventStore.Client/Core/Exceptions/ConnectionString/ConnectionStringParseException.cs deleted file mode 100644 index cb79f161d..000000000 --- a/src/EventStore.Client/Core/Exceptions/ConnectionString/ConnectionStringParseException.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; - -namespace EventStore.Client { - /// - /// The base exception that is thrown when an EventStoreDB connection string could not be parsed. - /// - public class ConnectionStringParseException : Exception { - /// - /// Constructs a new . - /// - /// - /// The exception that is the cause of the current exception, or a null reference ( in Visual Basic) if no inner exception is specified. - public ConnectionStringParseException(string message, Exception? innerException = null) : base(message, innerException) { } - } -} diff --git a/src/EventStore.Client/Core/Exceptions/ConnectionString/DuplicateKeyException.cs b/src/EventStore.Client/Core/Exceptions/ConnectionString/DuplicateKeyException.cs deleted file mode 100644 index 4027d08e8..000000000 --- a/src/EventStore.Client/Core/Exceptions/ConnectionString/DuplicateKeyException.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace EventStore.Client { - /// - /// The exception that is thrown when a key in the EventStoreDB connection string is duplicated. - /// - public class DuplicateKeyException : ConnectionStringParseException { - /// - /// Constructs a new . - /// - /// - public DuplicateKeyException(string key) - : base($"Duplicate key: '{key}'") { } - } -} diff --git a/src/EventStore.Client/Core/Exceptions/ConnectionString/InvalidClientCertificateException.cs b/src/EventStore.Client/Core/Exceptions/ConnectionString/InvalidClientCertificateException.cs deleted file mode 100644 index 8415742f8..000000000 --- a/src/EventStore.Client/Core/Exceptions/ConnectionString/InvalidClientCertificateException.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace EventStore.Client { - /// - /// The exception that is thrown when a certificate is invalid or not found in the EventStoreDB connection string. - /// - public class InvalidClientCertificateException : ConnectionStringParseException { - /// - /// Constructs a new . - /// - /// - /// - public InvalidClientCertificateException(string message, Exception? innerException = null) - : base(message, innerException) { } - } -} \ No newline at end of file diff --git a/src/EventStore.Client/Core/Exceptions/ConnectionString/InvalidHostException.cs b/src/EventStore.Client/Core/Exceptions/ConnectionString/InvalidHostException.cs deleted file mode 100644 index 27ba8f615..000000000 --- a/src/EventStore.Client/Core/Exceptions/ConnectionString/InvalidHostException.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace EventStore.Client { - /// - /// The exception that is thrown when there is an invalid host in the EventStoreDB connection string. - /// - public class InvalidHostException : ConnectionStringParseException { - /// - /// Constructs a new . - /// - /// - public InvalidHostException(string host) - : base($"Invalid host: '{host}'") { } - } -} diff --git a/src/EventStore.Client/Core/Exceptions/ConnectionString/InvalidKeyValuePairException.cs b/src/EventStore.Client/Core/Exceptions/ConnectionString/InvalidKeyValuePairException.cs deleted file mode 100644 index 2e6fc58df..000000000 --- a/src/EventStore.Client/Core/Exceptions/ConnectionString/InvalidKeyValuePairException.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace EventStore.Client { - /// - /// The exception that is thrown when an invalid key value pair is found in an EventStoreDB connection string. - /// - public class InvalidKeyValuePairException : ConnectionStringParseException { - /// - /// Constructs a new . - /// - /// - public InvalidKeyValuePairException(string keyValuePair) - : base($"Invalid key/value pair: '{keyValuePair}'") { } - } -} diff --git a/src/EventStore.Client/Core/Exceptions/ConnectionString/InvalidSchemeException.cs b/src/EventStore.Client/Core/Exceptions/ConnectionString/InvalidSchemeException.cs deleted file mode 100644 index 766c13f37..000000000 --- a/src/EventStore.Client/Core/Exceptions/ConnectionString/InvalidSchemeException.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace EventStore.Client { - /// - /// The exception that is thrown when an invalid scheme is defined in the EventStoreDB connection string. - /// - public class InvalidSchemeException : ConnectionStringParseException { - /// - /// Constructs a new . - /// - /// - /// - public InvalidSchemeException(string scheme, string[] supportedSchemes) - : base($"Invalid scheme: '{scheme}'. Supported values are: {string.Join(",", supportedSchemes)}") { } - } -} diff --git a/src/EventStore.Client/Core/Exceptions/ConnectionString/InvalidSettingException.cs b/src/EventStore.Client/Core/Exceptions/ConnectionString/InvalidSettingException.cs deleted file mode 100644 index 0bb5da667..000000000 --- a/src/EventStore.Client/Core/Exceptions/ConnectionString/InvalidSettingException.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace EventStore.Client { - /// - /// The exception that is thrown when an invalid setting is found in an EventStoreDB connection string. - /// - public class InvalidSettingException : ConnectionStringParseException { - /// - /// Constructs a new . - /// - /// - public InvalidSettingException(string message) : base(message) { } - } -} diff --git a/src/EventStore.Client/Core/Exceptions/ConnectionString/InvalidUserCredentialsException.cs b/src/EventStore.Client/Core/Exceptions/ConnectionString/InvalidUserCredentialsException.cs deleted file mode 100644 index bd515fee4..000000000 --- a/src/EventStore.Client/Core/Exceptions/ConnectionString/InvalidUserCredentialsException.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace EventStore.Client { - /// - /// The exception that is thrown when an invalid is specified in the EventStoreDB connection string. - /// - public class InvalidUserCredentialsException : ConnectionStringParseException { - /// - /// - /// - /// - public InvalidUserCredentialsException(string userInfo) - : base($"Invalid user credentials: '{userInfo}'. Username & password must be delimited by a colon") { } - } -} diff --git a/src/EventStore.Client/Core/Exceptions/ConnectionString/NoSchemeException.cs b/src/EventStore.Client/Core/Exceptions/ConnectionString/NoSchemeException.cs deleted file mode 100644 index 7551ad467..000000000 --- a/src/EventStore.Client/Core/Exceptions/ConnectionString/NoSchemeException.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace EventStore.Client { - /// - /// The exception that is thrown when no scheme was specified in the EventStoreDB connection string. - /// - public class NoSchemeException : ConnectionStringParseException { - /// - /// Constructs a new . - /// - public NoSchemeException() - : base("Could not parse scheme from connection string") { } - } -} diff --git a/src/EventStore.Client/Core/Exceptions/DiscoveryException.cs b/src/EventStore.Client/Core/Exceptions/DiscoveryException.cs deleted file mode 100644 index f3f490adb..000000000 --- a/src/EventStore.Client/Core/Exceptions/DiscoveryException.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System; - -namespace EventStore.Client { - /// - /// The exception that is thrown when discovery fails. - /// - public class DiscoveryException : Exception { - /// - /// The configured number of discovery attempts. - /// - public int MaxDiscoverAttempts { get; } - - /// - /// Constructs a new . - /// - /// - /// - [Obsolete] - public DiscoveryException(string message, Exception? innerException = null) - : base(message, innerException) { - MaxDiscoverAttempts = 0; - } - - /// - /// Constructs a new . - /// - /// The configured number of discovery attempts. - public DiscoveryException(int maxDiscoverAttempts) : base( - $"Failed to discover candidate in {maxDiscoverAttempts} attempts.") { - MaxDiscoverAttempts = maxDiscoverAttempts; - } - } -} diff --git a/src/EventStore.Client/Core/Exceptions/NotAuthenticatedException.cs b/src/EventStore.Client/Core/Exceptions/NotAuthenticatedException.cs deleted file mode 100644 index 289cfa230..000000000 --- a/src/EventStore.Client/Core/Exceptions/NotAuthenticatedException.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; - -namespace EventStore.Client { - /// - /// The exception that is thrown when a user is not authenticated. - /// - public class NotAuthenticatedException : Exception { - /// - /// Constructs a new . - /// - /// - /// - public NotAuthenticatedException(string message, Exception? innerException = null) : base(message, innerException) { - } - } -} diff --git a/src/EventStore.Client/Core/Exceptions/NotLeaderException.cs b/src/EventStore.Client/Core/Exceptions/NotLeaderException.cs deleted file mode 100644 index 4ad5ca32a..000000000 --- a/src/EventStore.Client/Core/Exceptions/NotLeaderException.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System; -using System.Net; - -namespace EventStore.Client { - /// - /// The exception that is thrown when an operation requiring a leader node is made on a follower node. - /// - public class NotLeaderException : Exception { - - /// - /// The of the current leader node. - /// - public DnsEndPoint LeaderEndpoint { get; } - - /// - /// Constructs a new - /// - /// - /// - /// - public NotLeaderException(string host, int port, Exception? exception = null) : base( - $"Not leader. New leader at {host}:{port}.", exception) { - LeaderEndpoint = new DnsEndPoint(host, port); - } - } -} diff --git a/src/EventStore.Client/Core/Exceptions/RequiredMetadataPropertyMissingException.cs b/src/EventStore.Client/Core/Exceptions/RequiredMetadataPropertyMissingException.cs deleted file mode 100644 index 079eb0444..000000000 --- a/src/EventStore.Client/Core/Exceptions/RequiredMetadataPropertyMissingException.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; - -namespace EventStore.Client { - /// - /// Exception thrown when a required metadata property is missing. - /// - public class RequiredMetadataPropertyMissingException : Exception { - /// - /// Constructs a new . - /// - /// - /// - public RequiredMetadataPropertyMissingException(string missingMetadataProperty, - Exception? innerException = null) : - base($"Required metadata property {missingMetadataProperty} is missing", innerException) { - } - } -} diff --git a/src/EventStore.Client/Core/Exceptions/ScavengeNotFoundException.cs b/src/EventStore.Client/Core/Exceptions/ScavengeNotFoundException.cs deleted file mode 100644 index 06a629661..000000000 --- a/src/EventStore.Client/Core/Exceptions/ScavengeNotFoundException.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System; - -namespace EventStore.Client { - /// - /// The exception that is thrown when attempting to see the status of a scavenge operation that does not exist. - /// - public class ScavengeNotFoundException : Exception { - /// - /// The id of the scavenge operation. - /// - public string? ScavengeId { get; } - - /// - /// Constructs a new . - /// - /// - /// - public ScavengeNotFoundException(string? scavengeId, Exception? exception = null) : base( - $"Scavenge not found. The currently running scavenge is {scavengeId ?? ""}.", exception) { - ScavengeId = scavengeId; - } - } -} diff --git a/src/EventStore.Client/Core/Exceptions/StreamDeletedException.cs b/src/EventStore.Client/Core/Exceptions/StreamDeletedException.cs deleted file mode 100644 index 0b892578f..000000000 --- a/src/EventStore.Client/Core/Exceptions/StreamDeletedException.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System; - -namespace EventStore.Client { - /// - /// Exception thrown if an operation is attempted on a stream which - /// has been deleted. - /// - public class StreamDeletedException : Exception { - /// - /// The name of the deleted stream. - /// - public readonly string Stream; - - /// - /// Constructs a new instance of . - /// - /// The name of the deleted stream. - /// - public StreamDeletedException(string stream, Exception? exception = null) - : base($"Event stream '{stream}' is deleted.", exception) { - Stream = stream; - } - } -} diff --git a/src/EventStore.Client/Core/Exceptions/StreamNotFoundException.cs b/src/EventStore.Client/Core/Exceptions/StreamNotFoundException.cs deleted file mode 100644 index dc844df36..000000000 --- a/src/EventStore.Client/Core/Exceptions/StreamNotFoundException.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System; - -namespace EventStore.Client { - /// - /// The exception that is thrown when an attempt is made to read or write to a stream that does not exist. - /// - public class StreamNotFoundException : Exception { - /// - /// The name of the stream. - /// - public readonly string Stream; - - /// - /// Constructs a new instance of . - /// - /// The name of the stream. - /// - public StreamNotFoundException(string stream, Exception? exception = null) - : base($"Event stream '{stream}' was not found.", exception) { - Stream = stream; - } - } -} diff --git a/src/EventStore.Client/Core/Exceptions/UserNotFoundException.cs b/src/EventStore.Client/Core/Exceptions/UserNotFoundException.cs deleted file mode 100644 index 3caa6cb68..000000000 --- a/src/EventStore.Client/Core/Exceptions/UserNotFoundException.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System; - -namespace EventStore.Client { - /// - /// The exception that is thrown when an operation is performed on an internal user that does not exist. - /// - public class UserNotFoundException : Exception { - /// - /// The login name of the user. - /// - public string LoginName { get; } - - /// - /// Constructs a new . - /// - /// - /// - public UserNotFoundException(string loginName, Exception? exception = null) - : base($"User '{loginName}' was not found.", exception) { - LoginName = loginName; - } - } -} diff --git a/src/EventStore.Client/Core/Exceptions/WrongExpectedVersionException.cs b/src/EventStore.Client/Core/Exceptions/WrongExpectedVersionException.cs deleted file mode 100644 index ea2489e63..000000000 --- a/src/EventStore.Client/Core/Exceptions/WrongExpectedVersionException.cs +++ /dev/null @@ -1,66 +0,0 @@ -using System; - -namespace EventStore.Client { - /// - /// Exception thrown if the expected version specified on an operation - /// does not match the version of the stream when the operation was attempted. - /// - public class WrongExpectedVersionException : Exception { - /// - /// The stream identifier. - /// - public string StreamName { get; } - - /// - /// If available, the expected version specified for the operation that failed. - /// - public long? ExpectedVersion { get; } - - /// - /// If available, the current version of the stream that the operation was attempted on. - /// - public long? ActualVersion { get; } - - /// - /// The current of the stream that the operation was attempted on. - /// - public StreamRevision ActualStreamRevision { get; } - - /// - /// If available, the expected version specified for the operation that failed. - /// - public StreamRevision ExpectedStreamRevision { get; } - - /// - /// Constructs a new instance of with the expected and actual versions if available. - /// - public WrongExpectedVersionException(string streamName, StreamRevision expectedStreamRevision, - StreamRevision actualStreamRevision, Exception? exception = null, string? message = null) : - base( - message ?? $"Append failed due to WrongExpectedVersion. Stream: {streamName}, Expected version: {expectedStreamRevision}, Actual version: {actualStreamRevision}", - exception) { - StreamName = streamName; - ActualStreamRevision = actualStreamRevision; - ExpectedStreamRevision = expectedStreamRevision; - ExpectedVersion = expectedStreamRevision == StreamRevision.None ? new long?() : expectedStreamRevision.ToInt64(); - ActualVersion = actualStreamRevision == StreamRevision.None ? new long?() : actualStreamRevision.ToInt64(); - } - - /// - /// Constructs a new instance of with the expected and actual versions if available. - /// - /// - /// - /// - /// - public WrongExpectedVersionException(string streamName, StreamState expectedStreamState, - StreamRevision actualStreamRevision, Exception? exception = null) : base( - $"Append failed due to WrongExpectedVersion. Stream: {streamName}, Expected state: {expectedStreamState}, Actual version: {actualStreamRevision}", - exception) { - StreamName = streamName; - ActualStreamRevision = actualStreamRevision; - ActualVersion = actualStreamRevision == StreamRevision.None ? new long?() : actualStreamRevision.ToInt64(); - ExpectedStreamRevision = StreamRevision.None; - } - } -} diff --git a/src/EventStore.Client/Core/FromAll.cs b/src/EventStore.Client/Core/FromAll.cs deleted file mode 100644 index 3be460659..000000000 --- a/src/EventStore.Client/Core/FromAll.cs +++ /dev/null @@ -1,100 +0,0 @@ -using System; - -namespace EventStore.Client { - /// - /// A structure representing the logical position of a subscription to all. /> - /// - public readonly struct FromAll : IEquatable, IComparable, IComparable { - /// - /// Represents a when no events have been seen (i.e., the beginning). - /// - public static readonly FromAll Start = new(null); - - /// - /// Represents a to receive events written after the subscription is confirmed. - /// - public static readonly FromAll End = new(Position.End); - - /// - /// Returns a for the given . - /// - /// The . - /// - /// - public static FromAll After(Position position) => position == Position.End - ? throw new ArgumentException($"Use '{nameof(FromAll)}.{nameof(End)}.'", nameof(position)) - : new(position); - - private readonly Position? _value; - - private FromAll(Position? value) => _value = value; - - /// - /// Converts the to a . - /// It is not meant to be used directly from your code. - /// - /// - /// - public (ulong commitPosition, ulong preparePosition) ToUInt64() => this == Start - ? throw new InvalidOperationException( - $"{nameof(FromAll)}.{nameof(Start)} may not be converted.") - : (_value!.Value.CommitPosition, _value!.Value.PreparePosition); - - /// - public bool Equals(FromAll other) => Nullable.Equals(_value, other._value); - - /// - public override bool Equals(object? obj) => obj is FromAll other && Equals(other); - - /// - public override int GetHashCode() => _value.GetHashCode(); - -#pragma warning disable CS1591 - public static bool operator ==(FromAll left, FromAll right) => - Nullable.Equals(left, right); - - public static bool operator !=(FromAll left, FromAll right) => - !Nullable.Equals(left, right); - - public static bool operator >(FromAll left, FromAll right) => - left.CompareTo(right) > 0; - - public static bool operator <(FromAll left, FromAll right) => - left.CompareTo(right) < 0; - - public static bool operator >=(FromAll left, FromAll right) => - left.CompareTo(right) >= 0; - - public static bool operator <=(FromAll left, FromAll right) => - left.CompareTo(right) <= 0; -#pragma warning restore CS1591 - - /// - public int CompareTo(FromAll other) => (_value, other._value) switch { - (null, null) => 0, - (null, _) => -1, - (_, null) => 1, - _ => _value.Value.CompareTo(other._value.Value) - }; - - /// - public int CompareTo(object? obj) => obj switch { - null => 1, - FromAll other => CompareTo(other), - _ => throw new ArgumentException($"Object is not a {nameof(FromAll)}"), - }; - - /// - public override string ToString() { - if (_value is null) { - return "Start"; - } - - if (_value == Position.End) { - return "Live"; - } - - return _value.Value.ToString(); - } - } -} diff --git a/src/EventStore.Client/Core/FromStream.cs b/src/EventStore.Client/Core/FromStream.cs deleted file mode 100644 index 7cf99e081..000000000 --- a/src/EventStore.Client/Core/FromStream.cs +++ /dev/null @@ -1,100 +0,0 @@ -using System; - -namespace EventStore.Client { - /// - /// A structure representing the logical position of a subscription. /> - /// - public readonly struct FromStream : IEquatable, IComparable, IComparable { - /// - /// Represents a when no events have been seen (i.e., the beginning). - /// - public static readonly FromStream Start = new(null); - - /// - /// Represents a to receive events written after the subscription is confirmed. - /// - public static readonly FromStream End = new(StreamPosition.End); - - private readonly StreamPosition? _value; - - /// - /// Returns a for the given . - /// - /// The . - /// - /// - public static FromStream After(StreamPosition streamPosition) => - streamPosition == StreamPosition.End - ? throw new ArgumentException($"Use '{nameof(FromStream)}.{nameof(End)}.'", nameof(streamPosition)) - : new(streamPosition); - - private FromStream(StreamPosition? value) => _value = value; - - /// - /// Converts the to a . It is not meant to be used directly from your code. - /// - /// - /// - public ulong ToUInt64() => this == Start - ? throw new InvalidOperationException( - $"{nameof(FromStream)}.{nameof(Start)} may not be converted.") - : _value!.Value.ToUInt64(); - - /// - public bool Equals(FromStream other) => Nullable.Equals(_value, other._value); - - /// - public override bool Equals(object? obj) => obj is FromStream other && Equals(other); - - /// - public override int GetHashCode() => _value.GetHashCode(); - -#pragma warning disable CS1591 - public static bool operator ==(FromStream left, FromStream right) => - left.Equals(right); - - public static bool operator !=(FromStream left, FromStream right) => - !left.Equals(right); - - public static bool operator <(FromStream left, FromStream right) => - left.CompareTo(right) < 0; - - public static bool operator >(FromStream left, FromStream right) => - left.CompareTo(right) > 0; - - public static bool operator <=(FromStream left, FromStream right) => - left.CompareTo(right) <= 0; - - public static bool operator >=(FromStream left, FromStream right) => - left.CompareTo(right) >= 0; -#pragma warning restore CS1591 - - /// - public int CompareTo(FromStream other) => (_value, other._value) switch { - (null, null) => 0, - (null, _) => -1, - (_, null) => 1, - _ => _value.Value.CompareTo(other._value.Value) - }; - - /// - public int CompareTo(object? obj) => obj switch { - null => 1, - FromStream other => CompareTo(other), - _ => throw new ArgumentException($"Object is not a {nameof(FromStream)}"), - }; - - /// - public override string ToString() { - if (_value is null) { - return "Start"; - } - - if (_value == StreamPosition.End) { - return "Live"; - } - - return _value.Value.ToString(); - } - } -} diff --git a/src/EventStore.Client/Core/GossipChannelSelector.cs b/src/EventStore.Client/Core/GossipChannelSelector.cs deleted file mode 100644 index 633ab0d84..000000000 --- a/src/EventStore.Client/Core/GossipChannelSelector.cs +++ /dev/null @@ -1,99 +0,0 @@ -using System; -using System.Linq; -using System.Net; -using System.Threading; -using System.Threading.Tasks; -using Grpc.Core; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; - -namespace EventStore.Client { - // Thread safe - internal class GossipChannelSelector : IChannelSelector { - private readonly EventStoreClientSettings _settings; - private readonly ChannelCache _channels; - private readonly IGossipClient _gossipClient; - private readonly ILogger _log; - private readonly NodeSelector _nodeSelector; - - public GossipChannelSelector( - EventStoreClientSettings settings, - ChannelCache channelCache, - IGossipClient gossipClient) { - - _settings = settings; - _channels = channelCache; - _gossipClient = gossipClient; - _log = settings.LoggerFactory?.CreateLogger() ?? - new NullLogger(); - _nodeSelector = new(_settings); - } - - public ChannelBase SelectChannel(DnsEndPoint endPoint) { - return _channels.GetChannelInfo(endPoint); - } - - public async Task SelectChannelAsync(CancellationToken cancellationToken) { - var endPoint = await DiscoverAsync(cancellationToken).ConfigureAwait(false); - - _log.LogInformation("Successfully discovered candidate at {endPoint}.", endPoint); - - return _channels.GetChannelInfo(endPoint); - } - - private async Task DiscoverAsync(CancellationToken cancellationToken) { - for (var attempt = 1; attempt <= _settings.ConnectivitySettings.MaxDiscoverAttempts; attempt++) { - foreach (var kvp in _channels.GetRandomOrderSnapshot()) { - var endPointToGetGossip = kvp.Key; - var channelToGetGossip = kvp.Value; - - try { - var clusterInfo = await _gossipClient - .GetAsync(channelToGetGossip, cancellationToken) - .ConfigureAwait(false); - - var selectedEndpoint = _nodeSelector.SelectNode(clusterInfo); - - // Successfully selected an endpoint using this gossip! - // We want _channels to contain exactly the nodes in ClusterInfo. - // nodes no longer in the cluster can be forgotten. - // new nodes are added so we can use them to get gossip. - _channels.UpdateCache(clusterInfo.Members.Select(x => x.EndPoint)); - - return selectedEndpoint; - - } catch (Exception ex) { - _log.Log( - GetLogLevelForDiscoveryAttempt(attempt), - ex, - "Could not discover candidate from {endPoint}. Attempts remaining: {remainingAttempts}", - endPointToGetGossip, - _settings.ConnectivitySettings.MaxDiscoverAttempts - attempt); - } - } - - // couldn't select a node from any _channel. reseed the channels. - _channels.UpdateCache(_settings.ConnectivitySettings.GossipSeeds.Select(endPoint => - endPoint as DnsEndPoint ?? new DnsEndPoint(endPoint.GetHost(), endPoint.GetPort()))); - - await Task - .Delay(_settings.ConnectivitySettings.DiscoveryInterval, cancellationToken) - .ConfigureAwait(false); - } - - _log.LogError("Failed to discover candidate in {maxDiscoverAttempts} attempts.", - _settings.ConnectivitySettings.MaxDiscoverAttempts); - - throw new DiscoveryException(_settings.ConnectivitySettings.MaxDiscoverAttempts); - } - - private LogLevel GetLogLevelForDiscoveryAttempt(int attempt) => attempt switch { - _ when attempt == _settings.ConnectivitySettings.MaxDiscoverAttempts => - LogLevel.Error, - 1 => - LogLevel.Warning, - _ => - LogLevel.Debug - }; - } -} diff --git a/src/EventStore.Client/Core/GrpcGossipClient.cs b/src/EventStore.Client/Core/GrpcGossipClient.cs deleted file mode 100644 index 655d05b5f..000000000 --- a/src/EventStore.Client/Core/GrpcGossipClient.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System.Linq; -using System.Net; -using System.Threading; -using System.Threading.Tasks; -using Grpc.Core; - -namespace EventStore.Client { - internal class GrpcGossipClient : IGossipClient { - private readonly EventStoreClientSettings _settings; - - public GrpcGossipClient(EventStoreClientSettings settings) { - _settings = settings; - } - - public async ValueTask GetAsync(ChannelBase channel, CancellationToken ct) { - var client = new Gossip.Gossip.GossipClient(channel); - using var call = client.ReadAsync( - new Empty(), - EventStoreCallOptions.CreateNonStreaming(_settings, ct)); - var result = await call.ResponseAsync.ConfigureAwait(false); - - return new(result.Members.Select(x => - new ClusterMessages.MemberInfo( - Uuid.FromDto(x.InstanceId), - (ClusterMessages.VNodeState)x.State, - x.IsAlive, - new DnsEndPoint(x.HttpEndPoint.Address, (int)x.HttpEndPoint.Port))).ToArray()); - } - } -} diff --git a/src/EventStore.Client/Core/GrpcServerCapabilitiesClient.cs b/src/EventStore.Client/Core/GrpcServerCapabilitiesClient.cs deleted file mode 100644 index 02cd41222..000000000 --- a/src/EventStore.Client/Core/GrpcServerCapabilitiesClient.cs +++ /dev/null @@ -1,75 +0,0 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using Grpc.Core; - -namespace EventStore.Client { - internal class GrpcServerCapabilitiesClient : IServerCapabilitiesClient { - private readonly EventStoreClientSettings _settings; - - public GrpcServerCapabilitiesClient(EventStoreClientSettings settings) { - _settings = settings; - } - - public async Task GetAsync( - CallInvoker callInvoker, - CancellationToken cancellationToken) { - - var client = new ServerFeatures.ServerFeatures.ServerFeaturesClient(callInvoker); - using var call = client.GetSupportedMethodsAsync( - new(), - EventStoreCallOptions.CreateNonStreaming( - _settings, - _settings.ConnectivitySettings.GossipTimeout, - null, - cancellationToken)); - - try { - var supportsBatchAppend = false; - var supportsPersistentSubscriptionsToAll = false; - var supportsPersistentSubscriptionsGetInfo = false; - var supportsPersistentSubscriptionsRestartSubsystem = false; - var supportsPersistentSubscriptionsReplayParked = false; - var supportsPersistentSubscriptionsList = false; - - var response = await call.ResponseAsync.ConfigureAwait(false); - - foreach (var supportedMethod in response.Methods) { - switch (supportedMethod.ServiceName, supportedMethod.MethodName) { - case ("event_store.client.streams.streams", "batchappend"): - supportsBatchAppend = true; - continue; - case ("event_store.client.persistent_subscriptions.persistentsubscriptions", "read"): - supportsPersistentSubscriptionsToAll = supportedMethod.Features.Contains("all"); - continue; - case ("event_store.client.persistent_subscriptions.persistentsubscriptions", "getinfo"): - supportsPersistentSubscriptionsGetInfo = true; - continue; - case ("event_store.client.persistent_subscriptions.persistentsubscriptions", "restartsubsystem"): - supportsPersistentSubscriptionsRestartSubsystem = true; - continue; - case ("event_store.client.persistent_subscriptions.persistentsubscriptions", "replayparked"): - supportsPersistentSubscriptionsReplayParked = true; - continue; - case ("event_store.client.persistent_subscriptions.persistentsubscriptions", "list"): - supportsPersistentSubscriptionsList = true; - continue; - } - } - - return new( - SupportsBatchAppend: supportsBatchAppend, - SupportsPersistentSubscriptionsToAll: supportsPersistentSubscriptionsToAll, - SupportsPersistentSubscriptionsGetInfo: supportsPersistentSubscriptionsGetInfo, - SupportsPersistentSubscriptionsRestartSubsystem: supportsPersistentSubscriptionsRestartSubsystem, - SupportsPersistentSubscriptionsReplayParked: supportsPersistentSubscriptionsReplayParked, - SupportsPersistentSubscriptionsList: supportsPersistentSubscriptionsList); - - } catch (Exception ex) when (ex.GetBaseException() is RpcException rpcException && - rpcException.StatusCode == StatusCode.Unimplemented) { - - return new(); - } - } - } -} diff --git a/src/EventStore.Client/Core/HashCode.cs b/src/EventStore.Client/Core/HashCode.cs deleted file mode 100644 index 31f9c2514..000000000 --- a/src/EventStore.Client/Core/HashCode.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System.Collections.Generic; -using System.Linq; - -namespace EventStore.Client { - #pragma warning disable 1591 - public readonly struct HashCode { - private readonly int _value; - - private HashCode(int value) { - _value = value; - } - - public static readonly HashCode Hash = default; - - public HashCode Combine(T? value) where T : struct => Combine(value ?? default); - - public HashCode Combine(T value) where T: struct { - unchecked { - return new HashCode((_value * 397) ^ value.GetHashCode()); - } - } - - public HashCode Combine(string? value){ - unchecked { - return new HashCode((_value * 397) ^ (value?.GetHashCode() ?? 0)); - } - } - - public HashCode Combine(IEnumerable? values) where T: struct => - (values ?? Enumerable.Empty()).Aggregate(Hash, (previous, value) => previous.Combine(value)); - - public HashCode Combine(IEnumerable? values) => - (values ?? Enumerable.Empty()).Aggregate(Hash, (previous, value) => previous.Combine(value)); - - public static implicit operator int(HashCode value) => value._value; - } -} diff --git a/src/EventStore.Client/Core/HttpFallback.cs b/src/EventStore.Client/Core/HttpFallback.cs deleted file mode 100644 index 3e5420e9a..000000000 --- a/src/EventStore.Client/Core/HttpFallback.cs +++ /dev/null @@ -1,143 +0,0 @@ -using System; -using System.Net; -using System.Net.Http; -using System.Security.Cryptography.X509Certificates; -using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; - -namespace EventStore.Client { - internal class HttpFallback : IDisposable { - private readonly HttpClient _httpClient; - private readonly JsonSerializerOptions _jsonSettings; - private readonly UserCredentials? _defaultCredentials; - private readonly string _addressScheme; - - internal HttpFallback(EventStoreClientSettings settings) { - _addressScheme = settings.ConnectivitySettings.ResolvedAddressOrDefault.Scheme; - _defaultCredentials = settings.DefaultCredentials; - - var handler = new HttpClientHandler(); - if (!settings.ConnectivitySettings.Insecure) { - handler.ClientCertificateOptions = ClientCertificateOption.Manual; - - if (settings.ConnectivitySettings.ClientCertificate is not null) - handler.ClientCertificates.Add(settings.ConnectivitySettings.ClientCertificate); - - handler.ServerCertificateCustomValidationCallback = settings.ConnectivitySettings.TlsVerifyCert switch { - false => delegate { return true; }, - true when settings.ConnectivitySettings.TlsCaFile is not null => (sender, certificate, chain, errors) => { - if (certificate is null || chain is null) return false; - - chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck; - -#if NET48 - chain.ChainPolicy.ExtraStore.Add(settings.ConnectivitySettings.TlsCaFile); -#else - chain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust; - chain.ChainPolicy.CustomTrustStore.Add(settings.ConnectivitySettings.TlsCaFile); -#endif - - return chain.Build(certificate); - }, - _ => null - }; - } - - _httpClient = new HttpClient(handler); - if (settings.DefaultDeadline.HasValue) { - _httpClient.Timeout = settings.DefaultDeadline.Value; - } - - _jsonSettings = new JsonSerializerOptions { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase - }; - } - - internal async Task HttpGetAsync(string path, ChannelInfo channelInfo, TimeSpan? deadline, - UserCredentials? userCredentials, Action onNotFound, CancellationToken cancellationToken) { - - var request = CreateRequest(path, HttpMethod.Get, channelInfo, userCredentials); - - var httpResult = await HttpSendAsync(request, onNotFound, deadline, cancellationToken).ConfigureAwait(false); - -#if NET - var json = await httpResult.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); -#else - var json = await httpResult.Content.ReadAsStringAsync().ConfigureAwait(false); -#endif - - var result = JsonSerializer.Deserialize(json, _jsonSettings); - if (result == null) { - throw new InvalidOperationException("Unable to deserialize response into object of type " + typeof(T)); - } - - return result; - } - - internal async Task HttpPostAsync(string path, string query, ChannelInfo channelInfo, TimeSpan? deadline, - UserCredentials? userCredentials, Action onNotFound, CancellationToken cancellationToken) { - - var request = CreateRequest(path, query, HttpMethod.Post, channelInfo, userCredentials); - - await HttpSendAsync(request, onNotFound, deadline, cancellationToken).ConfigureAwait(false); - } - - private async Task HttpSendAsync(HttpRequestMessage request, Action onNotFound, - TimeSpan? deadline, CancellationToken cancellationToken) { - - if (!deadline.HasValue) { - return await HttpSendAsync(request, onNotFound, cancellationToken).ConfigureAwait(false); - } - - using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); - cts.CancelAfter(deadline.Value); - - return await HttpSendAsync(request, onNotFound, cts.Token).ConfigureAwait(false); - } - - async Task HttpSendAsync(HttpRequestMessage request, Action onNotFound, - CancellationToken cancellationToken) { - - var httpResult = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false); - if (httpResult.IsSuccessStatusCode) { - return httpResult; - } - - if (httpResult.StatusCode == HttpStatusCode.Unauthorized) { - throw new AccessDeniedException(); - } - - if (httpResult.StatusCode == HttpStatusCode.NotFound) { - onNotFound(); - } - - throw new Exception($"The HTTP request failed with status code: {httpResult.StatusCode}"); - } - - private HttpRequestMessage CreateRequest(string path, HttpMethod method, ChannelInfo channelInfo, - UserCredentials? credentials) => CreateRequest(path, query: "", method, channelInfo, credentials); - - private HttpRequestMessage CreateRequest(string path, string query, HttpMethod method, ChannelInfo channelInfo, - UserCredentials? credentials) { - - var uriBuilder = new UriBuilder($"{_addressScheme}://{channelInfo.Channel.Target}") { - Path = path, - Query = query - }; - - var httpRequest = new HttpRequestMessage(method, uriBuilder.Uri); - httpRequest.Headers.Add("accept", "application/json"); - credentials ??= _defaultCredentials; - if (credentials != null) { - httpRequest.Headers.Add(Constants.Headers.Authorization, credentials.ToString()); - } - - return httpRequest; - } - - public void Dispose() { - _httpClient.Dispose(); - } - } -} diff --git a/src/EventStore.Client/Core/IChannelSelector.cs b/src/EventStore.Client/Core/IChannelSelector.cs deleted file mode 100644 index 1765ccebc..000000000 --- a/src/EventStore.Client/Core/IChannelSelector.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System.Net; -using System.Threading; -using System.Threading.Tasks; -using Grpc.Core; - -namespace EventStore.Client { - internal interface IChannelSelector { - // Let the channel selector pick an endpoint. - Task SelectChannelAsync(CancellationToken cancellationToken); - - // Get a channel for the specified endpoint - ChannelBase SelectChannel(DnsEndPoint endPoint); - } -} diff --git a/src/EventStore.Client/Core/IEventFilter.cs b/src/EventStore.Client/Core/IEventFilter.cs deleted file mode 100644 index eb290685e..000000000 --- a/src/EventStore.Client/Core/IEventFilter.cs +++ /dev/null @@ -1,21 +0,0 @@ -namespace EventStore.Client { - /// - /// An interface that represents a search filter, used for read operations. - /// - public interface IEventFilter { - /// - /// The s associated with this . - /// - PrefixFilterExpression[] Prefixes { get; } - - /// - /// The associated with this . - /// - RegularFilterExpression Regex { get; } - - /// - /// The maximum number of events to read that do not match the filter before the operation returns. - /// - uint? MaxSearchWindow { get; } - } -} diff --git a/src/EventStore.Client/Core/IGossipClient.cs b/src/EventStore.Client/Core/IGossipClient.cs deleted file mode 100644 index 80c0e9395..000000000 --- a/src/EventStore.Client/Core/IGossipClient.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Threading; -using System.Threading.Tasks; -using Grpc.Core; - -namespace EventStore.Client { - internal interface IGossipClient { - public ValueTask GetAsync(ChannelBase channel, - CancellationToken cancellationToken); - } -} diff --git a/src/EventStore.Client/Core/IPosition.cs b/src/EventStore.Client/Core/IPosition.cs deleted file mode 100644 index e3c5da9bc..000000000 --- a/src/EventStore.Client/Core/IPosition.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace EventStore.Client { - /// - /// Represents the position in a stream or transaction file - /// - public interface IPosition { - } -} diff --git a/src/EventStore.Client/Core/IServerCapabilitiesClient.cs b/src/EventStore.Client/Core/IServerCapabilitiesClient.cs deleted file mode 100644 index 20943805d..000000000 --- a/src/EventStore.Client/Core/IServerCapabilitiesClient.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.Threading; -using System.Threading.Tasks; -using Grpc.Core; - -namespace EventStore.Client { - internal interface IServerCapabilitiesClient { - public Task GetAsync(CallInvoker callInvoker, CancellationToken cancellationToken); - } -} diff --git a/src/EventStore.Client/Core/Interceptors/ConnectionNameInterceptor.cs b/src/EventStore.Client/Core/Interceptors/ConnectionNameInterceptor.cs deleted file mode 100644 index d709a2a3b..000000000 --- a/src/EventStore.Client/Core/Interceptors/ConnectionNameInterceptor.cs +++ /dev/null @@ -1,45 +0,0 @@ -using Grpc.Core; -using Grpc.Core.Interceptors; - -namespace EventStore.Client.Interceptors { - internal class ConnectionNameInterceptor : Interceptor { - private readonly string _connectionName; - - public ConnectionNameInterceptor(string connectionName) { - _connectionName = connectionName; - } - - public override AsyncUnaryCall AsyncUnaryCall(TRequest request, - ClientInterceptorContext context, - AsyncUnaryCallContinuation continuation) { - AddConnectionName(context); - return continuation(request, context); - } - - public override AsyncClientStreamingCall AsyncClientStreamingCall( - ClientInterceptorContext context, - AsyncClientStreamingCallContinuation continuation) { - AddConnectionName(context); - return continuation(context); - } - - public override AsyncServerStreamingCall AsyncServerStreamingCall( - TRequest request, - ClientInterceptorContext context, - AsyncServerStreamingCallContinuation continuation) { - AddConnectionName(context); - return continuation(request, context); - } - - public override AsyncDuplexStreamingCall AsyncDuplexStreamingCall( - ClientInterceptorContext context, - AsyncDuplexStreamingCallContinuation continuation) { - AddConnectionName(context); - return continuation(context); - } - - private void AddConnectionName(ClientInterceptorContext context) - where TRequest : class where TResponse : class => - context.Options.Headers?.Add(Constants.Headers.ConnectionName, _connectionName); - } -} diff --git a/src/EventStore.Client/Core/Interceptors/ReportLeaderInterceptor.cs b/src/EventStore.Client/Core/Interceptors/ReportLeaderInterceptor.cs deleted file mode 100644 index 6d9327858..000000000 --- a/src/EventStore.Client/Core/Interceptors/ReportLeaderInterceptor.cs +++ /dev/null @@ -1,126 +0,0 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using Grpc.Core; -using Grpc.Core.Interceptors; - -namespace EventStore.Client.Interceptors { - // this has become more general than just detecting leader changes. - // triggers the action on any rpc exception with StatusCode.Unavailable - internal class ReportLeaderInterceptor : Interceptor { - private readonly Action _onReconnectionRequired; - - private const TaskContinuationOptions ContinuationOptions = - TaskContinuationOptions.ExecuteSynchronously | TaskContinuationOptions.OnlyOnFaulted; - - internal ReportLeaderInterceptor(Action onReconnectionRequired) { - _onReconnectionRequired = onReconnectionRequired; - } - - public override AsyncUnaryCall AsyncUnaryCall(TRequest request, - ClientInterceptorContext context, - AsyncUnaryCallContinuation continuation) { - var response = continuation(request, context); - - response.ResponseAsync.ContinueWith(OnReconnectionRequired, ContinuationOptions); - - return new AsyncUnaryCall(response.ResponseAsync, response.ResponseHeadersAsync, - response.GetStatus, response.GetTrailers, response.Dispose); - } - - public override AsyncClientStreamingCall AsyncClientStreamingCall( - ClientInterceptorContext context, - AsyncClientStreamingCallContinuation continuation) { - var response = continuation(context); - - response.ResponseAsync.ContinueWith(OnReconnectionRequired, ContinuationOptions); - - return new AsyncClientStreamingCall( - new StreamWriter(response.RequestStream, OnReconnectionRequired), - response.ResponseAsync, - response.ResponseHeadersAsync, response.GetStatus, response.GetTrailers, response.Dispose); - } - - public override AsyncDuplexStreamingCall AsyncDuplexStreamingCall( - ClientInterceptorContext context, - AsyncDuplexStreamingCallContinuation continuation) { - var response = continuation(context); - - return new AsyncDuplexStreamingCall( - new StreamWriter(response.RequestStream, OnReconnectionRequired), - new StreamReader(response.ResponseStream, OnReconnectionRequired), - response.ResponseHeadersAsync, - response.GetStatus, response.GetTrailers, response.Dispose); - } - - public override AsyncServerStreamingCall AsyncServerStreamingCall( - TRequest request, ClientInterceptorContext context, - AsyncServerStreamingCallContinuation continuation) { - var response = continuation(request, context); - - return new AsyncServerStreamingCall( - new StreamReader(response.ResponseStream, OnReconnectionRequired), - response.ResponseHeadersAsync, - response.GetStatus, response.GetTrailers, response.Dispose); - } - - private void OnReconnectionRequired(Task task) { - ReconnectionRequired reconnectionRequired = task.Exception?.InnerException switch { - NotLeaderException ex => new ReconnectionRequired.NewLeader(ex.LeaderEndpoint), - RpcException { - StatusCode: StatusCode.Unavailable - // or StatusCode.Unknown or TODO: use RPC exceptions on server - } => ReconnectionRequired.Rediscover.Instance, - _ => ReconnectionRequired.None.Instance - }; - - if (reconnectionRequired is not ReconnectionRequired.None) - _onReconnectionRequired(reconnectionRequired); - } - - private class StreamWriter : IClientStreamWriter { - private readonly IClientStreamWriter _inner; - private readonly Action _reportNewLeader; - - public StreamWriter(IClientStreamWriter inner, Action reportNewLeader) { - _inner = inner; - _reportNewLeader = reportNewLeader; - } - - public WriteOptions? WriteOptions { - get => _inner.WriteOptions; - set => _inner.WriteOptions = value; - } - - public Task CompleteAsync() { - var task = _inner.CompleteAsync(); - task.ContinueWith(_reportNewLeader, ContinuationOptions); - return task; - } - - public Task WriteAsync(T message) { - var task = _inner.WriteAsync(message); - task.ContinueWith(_reportNewLeader, ContinuationOptions); - return task; - } - } - - private class StreamReader : IAsyncStreamReader { - private readonly IAsyncStreamReader _inner; - private readonly Action _reportNewLeader; - - public StreamReader(IAsyncStreamReader inner, Action reportNewLeader) { - _inner = inner; - _reportNewLeader = reportNewLeader; - } - - public Task MoveNext(CancellationToken cancellationToken) { - var task = _inner.MoveNext(cancellationToken); - task.ContinueWith(_reportNewLeader, ContinuationOptions); - return task; - } - - public T Current => _inner.Current; - } - } -} diff --git a/src/EventStore.Client/Core/Interceptors/TypedExceptionInterceptor.cs b/src/EventStore.Client/Core/Interceptors/TypedExceptionInterceptor.cs deleted file mode 100644 index 0f1368805..000000000 --- a/src/EventStore.Client/Core/Interceptors/TypedExceptionInterceptor.cs +++ /dev/null @@ -1,165 +0,0 @@ -using Grpc.Core; -using Grpc.Core.Interceptors; -using static EventStore.Client.Constants; -using static Grpc.Core.StatusCode; - -namespace EventStore.Client.Interceptors; - -class TypedExceptionInterceptor : Interceptor { - static readonly Dictionary> DefaultExceptionMap = new() { - [Exceptions.AccessDenied] = ex => ex.ToAccessDeniedException(), - [Exceptions.NotLeader] = ex => ex.ToNotLeaderException(), - }; - - public TypedExceptionInterceptor(Dictionary> customExceptionMap) { -#if NET48 - var map = new Dictionary>(DefaultExceptionMap.Concat(customExceptionMap).ToDictionary(x => x.Key, x => x.Value)); -#else - var map = new Dictionary>(DefaultExceptionMap.Concat(customExceptionMap)); -#endif - ConvertRpcException = rpcEx => { - if (rpcEx.TryMapException(map, out var ex)) - throw ex; - - throw rpcEx.StatusCode switch { - Unavailable when rpcEx.Status.Detail == "Deadline Exceeded" => rpcEx.ToDeadlineExceededRpcException(), - Unauthenticated => rpcEx.ToNotAuthenticatedException(), - _ => rpcEx - }; - }; - } - - Func ConvertRpcException { get; } - - public override AsyncServerStreamingCall AsyncServerStreamingCall( - TRequest request, - ClientInterceptorContext context, - AsyncServerStreamingCallContinuation continuation - ) { - var response = continuation(request, context); - - return new AsyncServerStreamingCall( - response.ResponseStream.Apply(ConvertRpcException), - response.ResponseHeadersAsync, - response.GetStatus, - response.GetTrailers, - response.Dispose - ); - } - - public override AsyncClientStreamingCall AsyncClientStreamingCall( - ClientInterceptorContext context, - AsyncClientStreamingCallContinuation continuation - ) { - var response = continuation(context); - - return new AsyncClientStreamingCall( - response.RequestStream.Apply(ConvertRpcException), - response.ResponseAsync.Apply(ConvertRpcException), - response.ResponseHeadersAsync, - response.GetStatus, - response.GetTrailers, - response.Dispose - ); - } - - public override AsyncUnaryCall AsyncUnaryCall( - TRequest request, - ClientInterceptorContext context, - AsyncUnaryCallContinuation continuation - ) { - var response = continuation(request, context); - - return new AsyncUnaryCall( - response.ResponseAsync.Apply(ConvertRpcException), - response.ResponseHeadersAsync, - response.GetStatus, - response.GetTrailers, - response.Dispose - ); - } - - public override AsyncDuplexStreamingCall AsyncDuplexStreamingCall( - ClientInterceptorContext context, - AsyncDuplexStreamingCallContinuation continuation - ) { - var response = continuation(context); - - return new AsyncDuplexStreamingCall( - response.RequestStream, - response.ResponseStream.Apply(ConvertRpcException), - response.ResponseHeadersAsync, - response.GetStatus, - response.GetTrailers, - response.Dispose - ); - } -} - -static class RpcExceptionConversionExtensions { - public static IAsyncStreamReader Apply(this IAsyncStreamReader reader, Func convertException) => - new ExceptionConverterStreamReader(reader, convertException); - - public static Task Apply(this Task task, Func convertException) => - task.ContinueWith(t => t.Exception?.InnerException is RpcException ex ? throw convertException(ex) : t.Result); - - public static IClientStreamWriter Apply( - this IClientStreamWriter writer, Func convertException - ) => - new ExceptionConverterStreamWriter(writer, convertException); - - public static Task Apply(this Task task, Func convertException) => - task.ContinueWith(t => t.Exception?.InnerException is RpcException ex ? throw convertException(ex) : t); - - public static AccessDeniedException ToAccessDeniedException(this RpcException exception) => - new(exception.Message, exception); - - public static NotLeaderException ToNotLeaderException(this RpcException exception) { - var host = exception.Trailers.FirstOrDefault(x => x.Key == Exceptions.LeaderEndpointHost)?.Value!; - var port = exception.Trailers.GetIntValueOrDefault(Exceptions.LeaderEndpointPort); - return new NotLeaderException(host, port, exception); - } - - public static NotAuthenticatedException ToNotAuthenticatedException(this RpcException exception) => - new(exception.Message, exception); - - public static RpcException ToDeadlineExceededRpcException(this RpcException exception) => - new(new Status(DeadlineExceeded, exception.Status.Detail, exception.Status.DebugException)); - - public static bool TryMapException(this RpcException exception, Dictionary> map, out Exception createdException) { - if (exception.Trailers.TryGetValue(Exceptions.ExceptionKey, out var key) && map.TryGetValue(key!, out var factory)) { - createdException = factory.Invoke(exception); - return true; - } - - createdException = null!; - return false; - } -} - -class ExceptionConverterStreamReader(IAsyncStreamReader reader, Func convertException) : IAsyncStreamReader { - public TResponse Current => reader.Current; - - public async Task MoveNext(CancellationToken cancellationToken) { - try { - return await reader.MoveNext(cancellationToken).ConfigureAwait(false); - } - catch (RpcException ex) { - throw convertException(ex); - } - } -} - -class ExceptionConverterStreamWriter( - IClientStreamWriter writer, - Func convertException -) - : IClientStreamWriter { - public WriteOptions? WriteOptions { - get => writer.WriteOptions; - set => writer.WriteOptions = value; - } - - public Task WriteAsync(TRequest message) => writer.WriteAsync(message).Apply(convertException); - public Task CompleteAsync() => writer.CompleteAsync().Apply(convertException); -} diff --git a/src/EventStore.Client/Core/NodePreference.cs b/src/EventStore.Client/Core/NodePreference.cs deleted file mode 100644 index a3f453eb3..000000000 --- a/src/EventStore.Client/Core/NodePreference.cs +++ /dev/null @@ -1,26 +0,0 @@ -namespace EventStore.Client { - /// - /// Indicates the preferred EventStoreDB node type to connect to. - /// - public enum NodePreference { - /// - /// When attempting connection, prefers leader node. - /// - Leader, - - /// - /// When attempting connection, prefers follower node. - /// - Follower, - - /// - /// When attempting connection, has no node preference. - /// - Random, - - /// - /// When attempting connection, prefers read only replicas. - /// - ReadOnlyReplica - } -} diff --git a/src/EventStore.Client/Core/NodePreferenceComparers.cs b/src/EventStore.Client/Core/NodePreferenceComparers.cs deleted file mode 100644 index 07e8a1cc5..000000000 --- a/src/EventStore.Client/Core/NodePreferenceComparers.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace EventStore.Client { - internal static class NodePreferenceComparers { - public static readonly IComparer Leader = new Comparer(state => - state switch { - ClusterMessages.VNodeState.Leader => 0, - ClusterMessages.VNodeState.Follower => 1, - ClusterMessages.VNodeState.ReadOnlyReplica => 2, - ClusterMessages.VNodeState.PreReadOnlyReplica => 3, - ClusterMessages.VNodeState.ReadOnlyLeaderless => 4, - _ => int.MaxValue - }); - - public static readonly IComparer Follower = new Comparer(state => - state switch { - ClusterMessages.VNodeState.Follower => 0, - ClusterMessages.VNodeState.Leader => 1, - ClusterMessages.VNodeState.ReadOnlyReplica => 2, - ClusterMessages.VNodeState.PreReadOnlyReplica => 3, - ClusterMessages.VNodeState.ReadOnlyLeaderless => 4, - _ => int.MaxValue - }); - - public static readonly IComparer ReadOnlyReplica = new Comparer(state => - state switch { - ClusterMessages.VNodeState.ReadOnlyReplica => 0, - ClusterMessages.VNodeState.PreReadOnlyReplica => 1, - ClusterMessages.VNodeState.ReadOnlyLeaderless => 2, - ClusterMessages.VNodeState.Leader => 3, - ClusterMessages.VNodeState.Follower => 4, - _ => int.MaxValue - }); - - public static readonly IComparer None = new Comparer(_ => 0); - - private class Comparer : IComparer { - private readonly Func _getPriority; - - public Comparer(Func getPriority) { - _getPriority = getPriority; - } - - public int Compare(ClusterMessages.VNodeState left, ClusterMessages.VNodeState right) => - _getPriority(left).CompareTo(_getPriority(right)); - } - } -} diff --git a/src/EventStore.Client/Core/NodeSelector.cs b/src/EventStore.Client/Core/NodeSelector.cs deleted file mode 100644 index 94c00cc56..000000000 --- a/src/EventStore.Client/Core/NodeSelector.cs +++ /dev/null @@ -1,63 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; - -namespace EventStore.Client { - // Selects a node to connect to from a ClusterInfo, based on the node preference. - // Deals with endpoints, no grpc here. - // Thread safe. - internal class NodeSelector { - private static readonly ClusterMessages.VNodeState[] _notAllowedStates = { - ClusterMessages.VNodeState.Manager, - ClusterMessages.VNodeState.ShuttingDown, - ClusterMessages.VNodeState.Shutdown, - ClusterMessages.VNodeState.Unknown, - ClusterMessages.VNodeState.Initializing, - ClusterMessages.VNodeState.CatchingUp, - ClusterMessages.VNodeState.ResigningLeader, - ClusterMessages.VNodeState.PreLeader, - ClusterMessages.VNodeState.PreReplica, - ClusterMessages.VNodeState.PreReadOnlyReplica, - ClusterMessages.VNodeState.Clone, - ClusterMessages.VNodeState.DiscoverLeader, - }; - - private readonly Random _random; - private readonly IComparer? _nodeStateComparer; - - public NodeSelector(EventStoreClientSettings settings) { - _random = new Random(0); - _nodeStateComparer = settings.ConnectivitySettings.NodePreference switch { - NodePreference.Leader => NodePreferenceComparers.Leader, - NodePreference.Follower => NodePreferenceComparers.Follower, - NodePreference.ReadOnlyReplica => NodePreferenceComparers.ReadOnlyReplica, - _ => NodePreferenceComparers.None - }; - } - - public DnsEndPoint SelectNode(ClusterMessages.ClusterInfo clusterInfo) { - if (clusterInfo.Members.Length == 0) { - throw new Exception("No nodes in cluster info."); - } - - lock (_random) { - var node = clusterInfo.Members - .Where(IsConnectable) - .OrderBy(node => node.State, _nodeStateComparer) - .ThenBy(_ => _random.Next()) - .FirstOrDefault(); - - if (node is null) { - throw new Exception("No nodes are in a connectable state."); - } - - return node.EndPoint; - } - } - - private static bool IsConnectable(ClusterMessages.MemberInfo node) => - node.IsAlive && - !_notAllowedStates.Contains(node.State); - } -} diff --git a/src/EventStore.Client/Core/Position.cs b/src/EventStore.Client/Core/Position.cs deleted file mode 100644 index 169439804..000000000 --- a/src/EventStore.Client/Core/Position.cs +++ /dev/null @@ -1,200 +0,0 @@ -using System; - -namespace EventStore.Client { - /// - /// A structure referring to a potential logical record position - /// in the Event Store transaction file. - /// - public readonly struct Position : IEquatable, IComparable, IComparable, IPosition { - /// - /// Position representing the start of the transaction file - /// - public static readonly Position Start = new Position(0, 0); - - /// - /// Position representing the end of the transaction file - /// - public static readonly Position End = new Position(ulong.MaxValue, ulong.MaxValue); - - /// - /// The commit position of the record - /// - public readonly ulong CommitPosition; - - /// - /// The prepare position of the record. - /// - public readonly ulong PreparePosition; - - /// - /// Constructs a position with the given commit and prepare positions. - /// It is not guaranteed that the position is actually the start of a - /// record in the transaction file. - /// - /// The commit position cannot be less than the prepare position. - /// - /// The commit position of the record. - /// The prepare position of the record. - public Position(ulong commitPosition, ulong preparePosition) { - if (commitPosition < preparePosition) - throw new ArgumentOutOfRangeException( - nameof(commitPosition), - "The commit position cannot be less than the prepare position"); - - if (commitPosition > long.MaxValue && commitPosition != ulong.MaxValue) { - throw new ArgumentOutOfRangeException(nameof(commitPosition)); - } - - - if (preparePosition > long.MaxValue && preparePosition != ulong.MaxValue) { - throw new ArgumentOutOfRangeException(nameof(preparePosition)); - } - - CommitPosition = commitPosition; - PreparePosition = preparePosition; - } - - /// - /// Compares whether p1 < p2. - /// - /// A . - /// A . - /// True if p1 < p2. - public static bool operator <(Position p1, Position p2) => - p1.CommitPosition < p2.CommitPosition || - p1.CommitPosition == p2.CommitPosition && p1.PreparePosition < p2.PreparePosition; - - - /// - /// Compares whether p1 > p2. - /// - /// A . - /// A . - /// True if p1 > p2. - public static bool operator >(Position p1, Position p2) => - p1.CommitPosition > p2.CommitPosition || - p1.CommitPosition == p2.CommitPosition && p1.PreparePosition > p2.PreparePosition; - - /// - /// Compares whether p1 >= p2. - /// - /// A . - /// A . - /// True if p1 >= p2. - public static bool operator >=(Position p1, Position p2) => p1 > p2 || p1 == p2; - - /// - /// Compares whether p1 <= p2. - /// - /// A . - /// A . - /// True if p1 <= p2. - public static bool operator <=(Position p1, Position p2) => p1 < p2 || p1 == p2; - - /// - /// Compares p1 and p2 for equality. - /// - /// A . - /// A . - /// True if p1 is equal to p2. - public static bool operator ==(Position p1, Position p2) => - Equals(p1, p2); - - /// - /// Compares p1 and p2 for equality. - /// - /// A . - /// A . - /// True if p1 is not equal to p2. - public static bool operator !=(Position p1, Position p2) => !(p1 == p2); - - /// - public int CompareTo(Position other) => this == other ? 0 : this > other ? 1 : -1; - - /// - public int CompareTo(object? obj) => obj switch { - null => 1, - Position other => CompareTo(other), - _ => throw new ArgumentException("Object is not a Position"), - }; - - /// - /// Indicates whether this instance and a specified object are equal. - /// - /// - /// true if and this instance are the same type and represent the same value; otherwise, false. - /// - /// Another object to compare to. 2 - public override bool Equals(object? obj) => obj is Position position && Equals(position); - - /// - /// Compares this instance of for equality - /// with another instance. - /// - /// A - /// True if this instance is equal to the other instance. - public bool Equals(Position other) => - CommitPosition == other.CommitPosition && PreparePosition == other.PreparePosition; - - /// - /// Returns the hash code for this instance. - /// - /// - /// A 32-bit signed integer that is the hash code for this instance. - /// - /// 2 - public override int GetHashCode() => HashCode.Hash.Combine(CommitPosition).Combine(PreparePosition); - - /// - /// Returns the fully qualified type name of this instance. - /// - /// - /// A containing a fully qualified type name. - /// - /// 2 - public override string ToString() => $"C:{CommitPosition}/P:{PreparePosition}"; - - /// - /// Tries to convert the string representation of a to its equivalent. - /// A return value indicates whether the conversion succeeded or failed. - /// - /// A string that represents the to convert. - /// Contains the that is equivalent to the string - /// representation, if the conversion succeeded, or null if the conversion failed. - /// true if the value was converted successfully; otherwise, false. - public static bool TryParse(string value, out Position? position) { - position = null; - var parts = value.Split('/'); - - if (parts.Length != 2) { - return false; - } - - if (!TryParsePosition("C", parts[0], out var commitPosition)) { - return false; - } - - if (!TryParsePosition("P", parts[1], out var preparePosition)) { - return false; - } - - position = new Position(commitPosition, preparePosition); - return true; - - static bool TryParsePosition(string expectedPrefix, string v, out ulong p) { - p = 0; - - var prts = v.Split(':'); - if (prts.Length != 2) { - return false; - } - - if (prts[0] != expectedPrefix) { - return false; - } - - return ulong.TryParse(prts[1], out p); - } - } - } -} diff --git a/src/EventStore.Client/Core/PrefixFilterExpression.cs b/src/EventStore.Client/Core/PrefixFilterExpression.cs deleted file mode 100644 index ea154aee3..000000000 --- a/src/EventStore.Client/Core/PrefixFilterExpression.cs +++ /dev/null @@ -1,64 +0,0 @@ -using System; - -namespace EventStore.Client { - /// - /// A structure representing a prefix (i.e., starts with) filter. - /// - public readonly struct PrefixFilterExpression : IEquatable { - /// - /// An empty . - /// - public static readonly PrefixFilterExpression None = default; - - private readonly string? _value; - - /// - /// Constructs a new . - /// - /// - /// - public PrefixFilterExpression(string value) { - if (value == null) { - throw new ArgumentNullException(nameof(value)); - } - - _value = value; - } - - /// - public bool Equals(PrefixFilterExpression other) => string.Equals(_value, other._value); - - /// - public override bool Equals(object? obj) => obj is PrefixFilterExpression other && Equals(other); - - /// - public override int GetHashCode() => _value?.GetHashCode() ?? 0; - - /// - /// Compares left and right for equality. - /// - /// - /// - /// True if left is equal to right. - public static bool operator ==(PrefixFilterExpression left, PrefixFilterExpression right) => left.Equals(right); - - /// - /// Compares left and right for inequality. - /// - /// - /// - /// True if left is not equal to right. - public static bool operator !=(PrefixFilterExpression left, PrefixFilterExpression right) => - !left.Equals(right); - - /// - /// Converts the to a . - /// - /// - /// - public static implicit operator string?(PrefixFilterExpression value) => value._value; - - /// - public override string? ToString() => _value; - } -} diff --git a/src/EventStore.Client/Core/ReconnectionRequired.cs b/src/EventStore.Client/Core/ReconnectionRequired.cs deleted file mode 100644 index bf448971d..000000000 --- a/src/EventStore.Client/Core/ReconnectionRequired.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.Net; - -namespace EventStore.Client { - internal abstract record ReconnectionRequired { - public record None : ReconnectionRequired { - public static None Instance = new(); - } - - public record Rediscover : ReconnectionRequired { - public static Rediscover Instance = new(); - } - - public record NewLeader(DnsEndPoint EndPoint) : ReconnectionRequired; - } -} diff --git a/src/EventStore.Client/Core/RegularFilterExpression.cs b/src/EventStore.Client/Core/RegularFilterExpression.cs deleted file mode 100644 index b8e6a42b8..000000000 --- a/src/EventStore.Client/Core/RegularFilterExpression.cs +++ /dev/null @@ -1,86 +0,0 @@ -using System; -using System.Text.RegularExpressions; - -namespace EventStore.Client { - /// - /// A structure representing a regular expression filter. - /// - public readonly struct RegularFilterExpression : IEquatable { - /// - /// An empty . - /// - public static readonly RegularFilterExpression None = default; - - /// - /// A that excludes system events (i.e., those whose types start with $). - /// - /// - public static readonly RegularFilterExpression ExcludeSystemEvents = - new RegularFilterExpression(new Regex(@"^[^\$].*")); - - private readonly string? _value; - - /// - /// Constructs a new . - /// - /// - /// - public RegularFilterExpression(string value) { - if (value == null) { - throw new ArgumentNullException(nameof(value)); - } - - _value = value; - } - - /// - /// Constructs a new . - /// - /// - /// - public RegularFilterExpression(Regex value) { - if (value == null) { - throw new ArgumentNullException(nameof(value)); - } - - _value = value.ToString(); - } - - /// - public bool Equals(RegularFilterExpression other) => string.Equals(_value, other._value); - - /// - public override bool Equals(object? obj) => obj is RegularFilterExpression other && Equals(other); - - /// - public override int GetHashCode() => _value?.GetHashCode() ?? 0; - - /// - /// Compares left and right for equality. - /// - /// - /// - /// True if left is equal to right. - public static bool operator ==(RegularFilterExpression left, RegularFilterExpression right) => - left.Equals(right); - - /// - /// Compares left and right for inequality. - /// - /// - /// - /// True if left is not equal to right. - public static bool operator !=(RegularFilterExpression left, RegularFilterExpression right) => - !left.Equals(right); - - /// - /// Converts a to a . - /// - /// - /// - public static implicit operator string?(RegularFilterExpression value) => value._value; - - /// - public override string? ToString() => _value; - } -} diff --git a/src/EventStore.Client/Core/ResolvedEvent.cs b/src/EventStore.Client/Core/ResolvedEvent.cs deleted file mode 100644 index 25ca13a78..000000000 --- a/src/EventStore.Client/Core/ResolvedEvent.cs +++ /dev/null @@ -1,60 +0,0 @@ -namespace EventStore.Client { - /// - /// A structure representing a single event or a resolved link event. - /// - public readonly struct ResolvedEvent { - /// - /// If this represents a link event, the - /// will be the resolved link event, otherwise it will be the single event. - /// - public readonly EventRecord Event; - - /// - /// The link event if this is a link event. - /// - public readonly EventRecord? Link; - - /// - /// Returns the event that was read or which triggered the subscription. - /// - /// If this represents a link event, the - /// will be the , otherwise it will be . - /// - public EventRecord OriginalEvent => Link ?? Event; - - /// - /// Position of the if available. - /// - public readonly Position? OriginalPosition; - - /// - /// The stream name of the . - /// - public string OriginalStreamId => OriginalEvent.EventStreamId; - - /// - /// The in the stream of the . - /// - public StreamPosition OriginalEventNumber => OriginalEvent.EventNumber; - - /// - /// Indicates whether this is a resolved link - /// event. - /// - public bool IsResolved => Link != null && Event != null; - - /// - /// Constructs a new . - /// - /// - /// - /// - public ResolvedEvent(EventRecord @event, EventRecord? link, ulong? commitPosition) { - Event = @event; - Link = link; - OriginalPosition = commitPosition.HasValue - ? new Position(commitPosition.Value, (link ?? @event).Position.PreparePosition) - : new Position?(); - } - } -} diff --git a/src/EventStore.Client/Core/ServerCapabilities.cs b/src/EventStore.Client/Core/ServerCapabilities.cs deleted file mode 100644 index aa63c4e3d..000000000 --- a/src/EventStore.Client/Core/ServerCapabilities.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace EventStore.Client { -#pragma warning disable 1591 - public record ServerCapabilities( - bool SupportsBatchAppend = false, - bool SupportsPersistentSubscriptionsToAll = false, - bool SupportsPersistentSubscriptionsGetInfo = false, - bool SupportsPersistentSubscriptionsRestartSubsystem = false, - bool SupportsPersistentSubscriptionsReplayParked = false, - bool SupportsPersistentSubscriptionsList = false); -} diff --git a/src/EventStore.Client/Core/SharingProvider.cs b/src/EventStore.Client/Core/SharingProvider.cs deleted file mode 100644 index 67911a618..000000000 --- a/src/EventStore.Client/Core/SharingProvider.cs +++ /dev/null @@ -1,111 +0,0 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; - -namespace EventStore.Client { - internal class SharingProvider { - protected ILogger Log { get; } - - public SharingProvider(ILoggerFactory? loggerFactory) { - Log = loggerFactory?.CreateLogger() ?? - new NullLogger(); - } - } - - // Given a factory for items of type TOutput, where the items: - // - are expensive to produce - // - can be shared by consumers - // - can break - // - can fail to be successfully produced by the factory to begin with. - // - // This class will make minimal use of the factory to provide items to consumers. - // The Factory can produce and return an item, or it can throw an exception. - // We pass the factory a OnBroken callback to be called later if that instance becomes broken. - // the OnBroken callback can be called multiple times, the factory will be called once. - // the argument to the OnBroken callback is the input to construct the next item. - // - // The factory will not be called multiple times concurrently so does not need to be - // thread safe, but it does need to terminate. - // - // This class is thread safe. - - internal class SharingProvider : SharingProvider, IDisposable { - private readonly Func, Task> _factory; - private readonly TimeSpan _factoryRetryDelay; - private readonly TInput _initialInput; - private TaskCompletionSource _currentBox; - private bool _disposed; - - public SharingProvider( - Func, Task> factory, - TimeSpan factoryRetryDelay, - TInput initialInput, - ILoggerFactory? loggerFactory = null) : base(loggerFactory) { - - _factory = factory; - _factoryRetryDelay = factoryRetryDelay; - _initialInput = initialInput; - _currentBox = new(TaskCreationOptions.RunContinuationsAsynchronously); - _ = FillBoxAsync(_currentBox, input: initialInput); - } - - public Task CurrentAsync => _currentBox.Task; - - public void Reset() { - OnBroken(_currentBox, _initialInput); - } - - // Call this to return a box containing a defective item, or indeed no item at all. - // A new box will be produced and filled if necessary. - private void OnBroken(TaskCompletionSource brokenBox, TInput input) { - if (!brokenBox.Task.IsCompleted) { - // factory is still working on this box. don't create a new box to fill - // or we would have to require the factory be thread safe. - Log.LogDebug("{type} returned to factory. Production already in progress.", typeof(TOutput).Name); - return; - } - - // replace _currentBox with a new one, but only if it is the broken one. - var originalBox = Interlocked.CompareExchange( - location1: ref _currentBox, - value: new(TaskCreationOptions.RunContinuationsAsynchronously), - comparand: brokenBox); - - if (originalBox == brokenBox) { - // replaced the _currentBox, call the factory to fill it. - Log.LogDebug("{type} returned to factory. Producing a new one.", typeof(TOutput).Name); - _ = FillBoxAsync(_currentBox, input); - } else { - // did not replace. a new one was created previously. do nothing. - Log.LogDebug("{type} returned to factory. Production already complete.", typeof(TOutput).Name); - } - } - - private async Task FillBoxAsync(TaskCompletionSource box, TInput input) { - if (_disposed) { - Log.LogDebug("{type} will not be produced, factory is closed!", typeof(TOutput).Name); - box.TrySetException(new ObjectDisposedException(GetType().ToString())); - return; - } - - try { - Log.LogDebug("{type} being produced...", typeof(TOutput).Name); - var item = await _factory(input, x => OnBroken(box, x)).ConfigureAwait(false); - box.TrySetResult(item); - Log.LogDebug("{type} produced!", typeof(TOutput).Name); - } catch (Exception ex) { - await Task.Yield(); // avoid risk of stack overflow - Log.LogDebug(ex, "{type} production failed. Retrying in {delay}", typeof(TOutput).Name, _factoryRetryDelay); - await Task.Delay(_factoryRetryDelay).ConfigureAwait(false); - box.TrySetException(ex); - OnBroken(box, _initialInput); - } - } - - public void Dispose() { - _disposed = true; - } - } -} diff --git a/src/EventStore.Client/Core/SingleNodeChannelSelector.cs b/src/EventStore.Client/Core/SingleNodeChannelSelector.cs deleted file mode 100644 index 79c3affb0..000000000 --- a/src/EventStore.Client/Core/SingleNodeChannelSelector.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Net; -using System.Threading; -using System.Threading.Tasks; -using Grpc.Core; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; - -namespace EventStore.Client { - internal class SingleNodeChannelSelector : IChannelSelector { - private readonly ILogger _log; - private readonly ChannelCache _channelCache; - private readonly DnsEndPoint _endPoint; - - public SingleNodeChannelSelector( - EventStoreClientSettings settings, - ChannelCache channelCache) { - - _log = settings.LoggerFactory?.CreateLogger() ?? - new NullLogger(); - - _channelCache = channelCache; - - var uri = settings.ConnectivitySettings.ResolvedAddressOrDefault; - _endPoint = new DnsEndPoint(host: uri.Host, port: uri.Port); - } - - public Task SelectChannelAsync(CancellationToken cancellationToken) => - Task.FromResult(SelectChannel(_endPoint)); - - public ChannelBase SelectChannel(DnsEndPoint endPoint) { - _log.LogInformation("Selected {endPoint}.", endPoint); - - return _channelCache.GetChannelInfo(endPoint); - } - } -} diff --git a/src/EventStore.Client/Core/SingleNodeHttpHandler.cs b/src/EventStore.Client/Core/SingleNodeHttpHandler.cs deleted file mode 100644 index b8560152e..000000000 --- a/src/EventStore.Client/Core/SingleNodeHttpHandler.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; - -namespace EventStore.Client { - internal class SingleNodeHttpHandler : DelegatingHandler { - private readonly EventStoreClientSettings _settings; - - public SingleNodeHttpHandler(EventStoreClientSettings settings) { - _settings = settings; - } - - protected override Task SendAsync(HttpRequestMessage request, - CancellationToken cancellationToken) { - request.RequestUri = new UriBuilder(request.RequestUri!) { - Scheme = _settings.ConnectivitySettings.ResolvedAddressOrDefault.Scheme - }.Uri; - return base.SendAsync(request, cancellationToken); - } - } -} diff --git a/src/EventStore.Client/Core/StreamFilter.cs b/src/EventStore.Client/Core/StreamFilter.cs deleted file mode 100644 index 8ed70e7bb..000000000 --- a/src/EventStore.Client/Core/StreamFilter.cs +++ /dev/null @@ -1,134 +0,0 @@ -using System; -using System.Linq; -using System.Text.RegularExpressions; - -namespace EventStore.Client { - /// - /// A structure representing a filter on stream names for read operations. - /// - public readonly struct StreamFilter : IEquatable, IEventFilter { - /// - /// An empty . - /// - public static readonly StreamFilter None = default; - - readonly PrefixFilterExpression[] _prefixes; - - /// - public PrefixFilterExpression[] Prefixes => _prefixes ?? Array.Empty(); - - /// - public RegularFilterExpression Regex { get; } - - /// - public uint? MaxSearchWindow { get; } - - /// - /// Creates a from a single prefix. - /// - /// - /// - public static IEventFilter Prefix(string prefix) - => new StreamFilter(new PrefixFilterExpression(prefix)); - - /// - /// Creates a from multiple prefixes. - /// - /// - /// - public static IEventFilter Prefix(params string[] prefixes) - => new StreamFilter(Array.ConvertAll(prefixes, prefix => new PrefixFilterExpression(prefix))); - - /// - /// Creates a from a search window and multiple prefixes. - /// - /// - /// - /// - public static IEventFilter Prefix(uint maxSearchWindow, params string[] prefixes) - => new StreamFilter(maxSearchWindow, - Array.ConvertAll(prefixes, prefix => new PrefixFilterExpression(prefix))); - - /// - /// Creates a from a regular expression and a search window. - /// - /// - /// - /// - public static IEventFilter RegularExpression(string regex, uint maxSearchWindow = 32) - => new StreamFilter(maxSearchWindow, new RegularFilterExpression(regex)); - - /// - /// Creates a from a regular expression and a search window. - /// - /// - /// - /// - public static IEventFilter RegularExpression(Regex regex, uint maxSearchWindow = 32) - => new StreamFilter(maxSearchWindow, new RegularFilterExpression(regex)); - - StreamFilter(RegularFilterExpression regex) : this(default, regex) { } - - StreamFilter(uint maxSearchWindow, RegularFilterExpression regex) { - if (maxSearchWindow == 0) { - throw new ArgumentOutOfRangeException(nameof(maxSearchWindow), - maxSearchWindow, $"{nameof(maxSearchWindow)} must be greater than 0."); - } - - Regex = regex; - _prefixes = Array.Empty(); - MaxSearchWindow = maxSearchWindow; - } - - StreamFilter(params PrefixFilterExpression[] prefixes) : this(32, prefixes) { } - - StreamFilter(uint maxSearchWindow, params PrefixFilterExpression[] prefixes) { - if (prefixes.Length == 0) { - throw new ArgumentException(); - } - - if (maxSearchWindow == 0) { - throw new ArgumentOutOfRangeException(nameof(maxSearchWindow), - maxSearchWindow, $"{nameof(maxSearchWindow)} must be greater than 0."); - } - - _prefixes = prefixes; - Regex = RegularFilterExpression.None; - MaxSearchWindow = maxSearchWindow; - } - - /// - public bool Equals(StreamFilter other) => - Prefixes.SequenceEqual(other.Prefixes) && - Regex.Equals(other.Regex) && - MaxSearchWindow.Equals(other.MaxSearchWindow); - - /// - public override bool Equals(object? obj) => obj is StreamFilter other && Equals(other); - - /// - public override int GetHashCode() => HashCode.Hash.Combine(Prefixes).Combine(Regex).Combine(MaxSearchWindow); - - /// - /// Compares left and right for equality. - /// - /// - /// - /// True if left is equal to right. - public static bool operator ==(StreamFilter left, StreamFilter right) => left.Equals(right); - - /// - /// Compares left and right for inequality. - /// - /// - /// - /// True if left is not equal to right. - public static bool operator !=(StreamFilter left, StreamFilter right) => !left.Equals(right); - - /// - public override string ToString() => - this == None - ? "(none)" - : $"{nameof(StreamFilter)} {(Prefixes.Length == 0 ? Regex.ToString() : $"[{string.Join(", ", Prefixes)}]")}"; - } -} diff --git a/src/EventStore.Client/Core/StreamIdentifier.cs b/src/EventStore.Client/Core/StreamIdentifier.cs deleted file mode 100644 index e77286166..000000000 --- a/src/EventStore.Client/Core/StreamIdentifier.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System.Text; -using Google.Protobuf; - -namespace EventStore.Client { -#pragma warning disable 1591 - internal partial class StreamIdentifier { - private string? _cached; - - public static implicit operator string?(StreamIdentifier? source) { - if (source == null) { - return null; - } - if (source._cached != null || source.StreamName.IsEmpty) return source._cached; - -#if NET - var tmp = Encoding.UTF8.GetString(source.StreamName.Span); -#else - var tmp = Encoding.UTF8.GetString(source.StreamName.ToByteArray()); -#endif - //this doesn't have to be thread safe, its just a cache in case the identifier is turned into a string several times - source._cached = tmp; - return source._cached; - } - - public static implicit operator StreamIdentifier(string source) => - new() {StreamName = ByteString.CopyFromUtf8(source)}; - } -} \ No newline at end of file diff --git a/src/EventStore.Client/Core/StreamPosition.cs b/src/EventStore.Client/Core/StreamPosition.cs deleted file mode 100644 index 7c196c7da..000000000 --- a/src/EventStore.Client/Core/StreamPosition.cs +++ /dev/null @@ -1,201 +0,0 @@ -using System; - -namespace EventStore.Client { - /// - /// A structure referring to an 's position within a stream. - /// - public readonly struct StreamPosition : IEquatable, IComparable, IComparable, IPosition { - private readonly ulong _value; - - /// - /// The beginning (i.e., the first event) of a stream. - /// - public static readonly StreamPosition Start = new StreamPosition(0); - - /// - /// The end of a stream. Use this when reading a stream backwards, or subscribing live to a stream. - /// - public static readonly StreamPosition End = new StreamPosition(ulong.MaxValue); - - /// - /// Converts a to a . It is not meant to be used directly from your code. - /// - /// - /// - public static StreamPosition FromInt64(long value) => - value == -1 ? End : new StreamPosition(Convert.ToUInt64(value)); - - /// - /// Creates a from a . - /// - /// - /// - public static StreamPosition FromStreamRevision(StreamRevision revision) => revision.ToUInt64() switch { - ulong.MaxValue => throw new ArgumentOutOfRangeException(nameof(revision)), - _ => new StreamPosition(revision.ToUInt64()) - }; - - /// - /// Constructs a new . - /// - /// - /// - public StreamPosition(ulong value) { - if (value > long.MaxValue && value != ulong.MaxValue) { - throw new ArgumentOutOfRangeException(nameof(value)); - } - - _value = value; - } - - /// - /// Advance to the next . - /// - /// - public StreamPosition Next() => this + 1; - - /// - public int CompareTo(StreamPosition other) => _value.CompareTo(other._value); - - /// - public int CompareTo(object? obj) => obj switch { - null => 1, - StreamPosition other => CompareTo(other), - _ => throw new ArgumentException("Object is not a StreamPosition"), - }; - - /// - public bool Equals(StreamPosition other) => _value == other._value; - - /// - public override bool Equals(object? obj) => obj is StreamPosition other && Equals(other); - - /// - public override int GetHashCode() => _value.GetHashCode(); - - /// - /// Compares left and right for equality. - /// - /// - /// - /// True if left is equal to right. - public static bool operator ==(StreamPosition left, StreamPosition right) => left.Equals(right); - - /// - /// Compares left and right for inequality. - /// - /// - /// - /// True if left is not equal to right. - public static bool operator !=(StreamPosition left, StreamPosition right) => !left.Equals(right); - - /// - /// Adds right to left. - /// - /// - /// - /// - public static StreamPosition operator +(StreamPosition left, ulong right) { - checked { - return new StreamPosition(left._value + right); - } - } - - /// - /// Adds right to left. - /// - /// - /// - /// - public static StreamPosition operator +(ulong left, StreamPosition right) { - checked { - return new StreamPosition(left + right._value); - } - } - - /// - /// Subtracts right from left. - /// - /// - /// - /// - public static StreamPosition operator -(StreamPosition left, ulong right) { - checked { - return new StreamPosition(left._value - right); - } - } - - /// - /// Subtracts right from left. - /// - /// - /// - /// - public static StreamPosition operator -(ulong left, StreamPosition right) { - checked { - return new StreamPosition(left - right._value); - } - } - - /// - /// Compares whether left > right. - /// - /// - /// - /// - public static bool operator >(StreamPosition left, StreamPosition right) => left._value > right._value; - - /// - /// Compares whether left < right. - /// - /// - /// - /// - public static bool operator <(StreamPosition left, StreamPosition right) => left._value < right._value; - - /// - /// Compares whether left >= right. - /// - /// - /// - /// - public static bool operator >=(StreamPosition left, StreamPosition right) => left._value >= right._value; - - /// - /// Compares whether left <= right. - /// - /// - /// - /// - public static bool operator <=(StreamPosition left, StreamPosition right) => left._value <= right._value; - - /// - /// Converts the to a . It is not meant to be used directly from your code. - /// - /// - public long ToInt64() => Equals(End) ? -1 : Convert.ToInt64(_value); - - /// - /// Converts a to a . - /// - /// - /// - public static implicit operator ulong(StreamPosition streamPosition) => streamPosition._value; - - /// - /// Converts a to a . - /// - /// - /// - public static implicit operator StreamPosition(ulong value) => new StreamPosition(value); - - /// - public override string ToString() => this == End ? "End" : _value.ToString(); - - /// - /// Converts the to a . - /// - /// - public ulong ToUInt64() => _value; - } -} diff --git a/src/EventStore.Client/Core/StreamRevision.cs b/src/EventStore.Client/Core/StreamRevision.cs deleted file mode 100644 index 89b553f53..000000000 --- a/src/EventStore.Client/Core/StreamRevision.cs +++ /dev/null @@ -1,196 +0,0 @@ -using System; - -namespace EventStore.Client { - /// - /// A structure referring to the expected stream revision when writing to a stream. - /// - public readonly struct StreamRevision : IEquatable, IComparable, IComparable { - private readonly ulong _value; - - /// - /// Represents no , i.e., when a stream does not exist. - /// - public static readonly StreamRevision None = new StreamRevision(ulong.MaxValue); - - /// - /// Converts a to a . It is not meant to be used directly from your code. - /// - /// - /// - public static StreamRevision FromInt64(long value) => - value == -1 ? None : new StreamRevision(Convert.ToUInt64(value)); - - /// - /// Creates a new from the given . - /// - /// - /// - public static StreamRevision FromStreamPosition(StreamPosition position) => position.ToUInt64() switch { - ulong.MaxValue => throw new ArgumentOutOfRangeException(nameof(position)), - _ => new StreamRevision(position.ToUInt64()) - }; - - /// - /// Constructs a new . - /// - /// - /// - public StreamRevision(ulong value) { - if (value > long.MaxValue && value != ulong.MaxValue) { - throw new ArgumentOutOfRangeException(nameof(value)); - } - - _value = value; - } - - /// - /// Advances the to the next revision. - /// - /// - public StreamRevision Next() => this + 1; - - /// - public int CompareTo(StreamRevision other) => _value.CompareTo(other._value); - - /// - public int CompareTo(object? obj) => obj switch { - null => 1, - StreamRevision other => CompareTo(other), - _ => throw new ArgumentException("Object is not a StreamRevision"), - }; - - /// - public bool Equals(StreamRevision other) => _value == other._value; - - /// - public override bool Equals(object? obj) => obj is StreamRevision other && Equals(other); - - /// - public override int GetHashCode() => _value.GetHashCode(); - - /// - /// Compares left and right for equality. - /// - /// - /// - /// True if left is equal to right. - public static bool operator ==(StreamRevision left, StreamRevision right) => left.Equals(right); - - /// - /// Compares left and right for inequality. - /// - /// - /// - /// True if left is not equal to right. - public static bool operator !=(StreamRevision left, StreamRevision right) => !left.Equals(right); - - /// - /// Adds right to left. - /// - /// - /// - /// - public static StreamRevision operator +(StreamRevision left, ulong right) { - checked { - return new StreamRevision(left._value + right); - } - } - - /// - /// Adds right to left. - /// - /// - /// - /// - public static StreamRevision operator +(ulong left, StreamRevision right) { - checked { - return new StreamRevision(left + right._value); - } - } - - /// - /// Subtracts right from left. - /// - /// - /// - /// - public static StreamRevision operator -(StreamRevision left, ulong right) { - checked { - return new StreamRevision(left._value - right); - } - } - - /// - /// Subtracts right from left. - /// - /// - /// - /// - public static StreamRevision operator -(ulong left, StreamRevision right) { - checked { - return new StreamRevision(left - right._value); - } - } - - /// - /// Compares whether left > right. - /// - /// - /// - /// - public static bool operator >(StreamRevision left, StreamRevision right) => left._value > right._value; - - /// - /// Compares whether left < right. - /// - /// - /// - /// - public static bool operator <(StreamRevision left, StreamRevision right) => left._value < right._value; - - /// - /// Compares whether left >= right. - /// - /// - /// - /// - public static bool operator >=(StreamRevision left, StreamRevision right) => left._value >= right._value; - - /// - /// Compares whether left <= right. - /// - /// - /// - /// - public static bool operator <=(StreamRevision left, StreamRevision right) => left._value <= right._value; - - /// - /// Converts the to a . It is not meant to be used directly from your code. - /// - /// - public long ToInt64() => Equals(None) ? -1 : Convert.ToInt64(_value); - - /// - /// Converts a to a . - /// - /// - /// - public static implicit operator ulong(StreamRevision streamRevision) => streamRevision._value; - - /// - /// Converts a to a . - /// - /// - /// - public static implicit operator StreamRevision(ulong value) => new StreamRevision(value); - - /// - public override string ToString() => this == None ? nameof(None) : _value.ToString(); - - /// - /// Converts the to a . - /// - /// - public ulong ToUInt64() => _value; - } -} diff --git a/src/EventStore.Client/Core/StreamState.cs b/src/EventStore.Client/Core/StreamState.cs deleted file mode 100644 index a0a021726..000000000 --- a/src/EventStore.Client/Core/StreamState.cs +++ /dev/null @@ -1,88 +0,0 @@ -using System; - -namespace EventStore.Client { - /// - /// A structure that represents the state the stream should be in when writing. - /// - public readonly struct StreamState : IEquatable { - /// - /// The stream should not exist. - /// - public static readonly StreamState NoStream = new StreamState(Constants.NoStream); - - /// - /// The stream may or may not exist. - /// - public static readonly StreamState Any = new StreamState(Constants.Any); - - /// - /// The stream must exist. - /// - public static readonly StreamState StreamExists = new StreamState(Constants.StreamExists); - - private readonly int _value; - - private static class Constants { - public const int NoStream = 1; - public const int Any = 2; - public const int StreamExists = 4; - } - - internal StreamState(int value) { - switch (value) { - case Constants.NoStream: - case Constants.Any: - case Constants.StreamExists: - _value = value; - return; - default: - throw new ArgumentOutOfRangeException(nameof(value)); - } - } - - /// - public bool Equals(StreamState other) => _value == other._value; - - /// - public override bool Equals(object? obj) => obj is StreamState other && Equals(other); - - /// - public override int GetHashCode() => HashCode.Hash.Combine(_value); - - /// - /// Compares left and right for equality. - /// - /// - /// - /// Returns True when left and right are equal. - public static bool operator ==(StreamState left, StreamState right) => left.Equals(right); - - /// - /// Compares left and right for inequality. - /// - /// - /// - /// Returns True when left and right are not equal. - public static bool operator !=(StreamState left, StreamState right) => !left.Equals(right); - - /// - /// Converts the to a . It is not meant to be used directly from your code. - /// - /// - public long ToInt64() => -Convert.ToInt64(_value); - - /// - /// Converts the to an . It is not meant to be used directly from your code. - /// - /// - public static implicit operator int(StreamState streamState) => streamState._value; - - /// - public override string ToString() => _value switch { - Constants.NoStream => nameof(NoStream), - Constants.Any => nameof(Any), - Constants.StreamExists => nameof(StreamExists), - _ => _value.ToString() - }; - } -} diff --git a/src/EventStore.Client/Core/SubscriptionDroppedReason.cs b/src/EventStore.Client/Core/SubscriptionDroppedReason.cs deleted file mode 100644 index 1865a5d44..000000000 --- a/src/EventStore.Client/Core/SubscriptionDroppedReason.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace EventStore.Client { - /// - /// Represents the reason subscription was dropped. - /// - public enum SubscriptionDroppedReason { - /// - /// Subscription was dropped because the subscription was disposed. - /// - Disposed, - /// - /// Subscription was dropped because of an error in user code. - /// - SubscriberError, - /// - /// Subscription was dropped because of a server error. - /// - ServerError - } -} diff --git a/src/EventStore.Client/Core/SystemRoles.cs b/src/EventStore.Client/Core/SystemRoles.cs deleted file mode 100644 index b9cb068f7..000000000 --- a/src/EventStore.Client/Core/SystemRoles.cs +++ /dev/null @@ -1,21 +0,0 @@ -namespace EventStore.Client { - /// - /// Roles used by the system. - /// - public static class SystemRoles { - /// - /// The $admins role. - /// - public const string Admins = "$admins"; - - /// - /// The $ops role. - /// - public const string Operations = "$ops"; - - /// - /// The $all role. - /// - public const string All = "$all"; - } -} diff --git a/src/EventStore.Client/Core/SystemStreams.cs b/src/EventStore.Client/Core/SystemStreams.cs deleted file mode 100644 index 67899794b..000000000 --- a/src/EventStore.Client/Core/SystemStreams.cs +++ /dev/null @@ -1,54 +0,0 @@ -namespace EventStore.Client { - /// - /// A collection of constants and methods to identify streams. - /// - public static class SystemStreams { - /// - /// A stream containing all events in the EventStoreDB transaction file. - /// - public const string AllStream = "$all"; - - /// - /// A stream containing links pointing to each stream in the EventStoreDB. - /// - public const string StreamsStream = "$streams"; - - /// - /// A stream containing system settings. - /// - public const string SettingsStream = "$settings"; - - /// - /// A stream containing statistics. - /// - public const string StatsStreamPrefix = "$stats"; - - /// - /// Returns True if the stream is a system stream. - /// - /// - /// - public static bool IsSystemStream(string streamId) => streamId.Length != 0 && streamId[0] == '$'; - - /// - /// Returns the metadata stream of the stream. - /// - /// - /// - public static string MetastreamOf(string streamId) => "$$" + streamId; - - /// - /// Returns true if the stream is a metadata stream. - /// - /// - /// - public static bool IsMetastream(string streamId) => streamId[..2] == "$$"; - - /// - /// Returns the original stream of the metadata stream. - /// - /// - /// - public static string OriginalStreamOf(string metastreamId) => metastreamId[2..]; - } -} diff --git a/src/EventStore.Client/Core/TaskExtensions.cs b/src/EventStore.Client/Core/TaskExtensions.cs deleted file mode 100644 index 5511b1c20..000000000 --- a/src/EventStore.Client/Core/TaskExtensions.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System.Threading; -using System.Threading.Tasks; - -namespace EventStore.Client { - internal static class TaskExtensions { - // To give up waiting for the task, cancel the token. - // obvs this wouldn't cancel the task itself. - public static async ValueTask WithCancellation(this Task task, CancellationToken cancellationToken) { - if (task.Status == TaskStatus.RanToCompletion) - return task.Result; - - await Task - .WhenAny( - task, - Task.Delay(-1, cancellationToken)) - .ConfigureAwait(false); - - cancellationToken.ThrowIfCancellationRequested(); - return await task.ConfigureAwait(false); - } - } -} diff --git a/src/EventStore.Client/Core/UserCredentials.cs b/src/EventStore.Client/Core/UserCredentials.cs deleted file mode 100644 index d944d90d7..000000000 --- a/src/EventStore.Client/Core/UserCredentials.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System.Net.Http.Headers; -using System.Text; -using static System.Convert; - -namespace EventStore.Client { - /// - /// Represents either a username/password pair or a JWT token used for authentication and - /// authorization to perform operations on the EventStoreDB. - /// - public class UserCredentials { - // ReSharper disable once InconsistentNaming - static readonly UTF8Encoding UTF8NoBom = new UTF8Encoding(false); - - /// - /// Constructs a new . - /// - public UserCredentials(string username, string password) { - Username = username; - Password = password; - - Authorization = new( - Constants.Headers.BasicScheme, - ToBase64String(UTF8NoBom.GetBytes($"{username}:{password}")) - ); - } - - /// - /// Constructs a new . - /// - public UserCredentials(string bearerToken) { - Authorization = new(Constants.Headers.BearerScheme, bearerToken); - } - - AuthenticationHeaderValue Authorization { get; } - - /// - /// The username - /// - public string? Username { get; } - - /// - /// The password - /// - public string? Password { get; } - - /// - public override string ToString() => Authorization.ToString(); - - /// - /// Implicitly convert a to a . - /// - public static implicit operator string(UserCredentials self) => self.ToString(); - } -} \ No newline at end of file diff --git a/src/EventStore.Client/Core/Uuid.cs b/src/EventStore.Client/Core/Uuid.cs deleted file mode 100644 index 0a38cc766..000000000 --- a/src/EventStore.Client/Core/Uuid.cs +++ /dev/null @@ -1,203 +0,0 @@ -using System; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -namespace EventStore.Client { - /// - /// An RFC-4122 compliant v4 UUID. - /// - public readonly struct Uuid : IEquatable { - /// - /// Represents the empty (00000000-0000-0000-0000-000000000000) . - /// - /// - /// This reorders the bits in System.Guid to improve interop with other languages. See: https://stackoverflow.com/a/16722909 - /// - public static readonly Uuid Empty = new Uuid(Guid.Empty); - - private readonly long _lsb; - private readonly long _msb; - - /// - /// Creates a new, randomized . - /// - /// - public static Uuid NewUuid() => new Uuid(Guid.NewGuid()); - - /// - /// Converts a to a . - /// - /// - /// - public static Uuid FromGuid(Guid value) => new Uuid(value); - - /// - /// Parses a into a . - /// - /// - /// - public static Uuid Parse(string value) => new Uuid(value); - - /// - /// Creates a from a pair of . - /// - /// The representing the most significant bits. - /// The representing the least significant bits. - /// - public static Uuid FromInt64(long msb, long lsb) => new Uuid(msb, lsb); - - /// - /// Creates a from the gRPC wire format. - /// - /// - /// - internal static Uuid FromDto(UUID dto) => - dto == null - ? throw new ArgumentNullException(nameof(dto)) - : dto.ValueCase switch { - UUID.ValueOneofCase.String => new Uuid(dto.String), - UUID.ValueOneofCase.Structured => new Uuid(dto.Structured.MostSignificantBits, - dto.Structured.LeastSignificantBits), - _ => throw new ArgumentException($"Invalid argument: {dto.ValueCase}", nameof(dto)) - }; - - private Uuid(Guid value) { - if (!BitConverter.IsLittleEndian) { - throw new NotSupportedException(); - } - - Span data = stackalloc byte[16]; - - if (!TryWriteGuidBytes(value, data)) { - throw new InvalidOperationException(); - } - - data[..8].Reverse(); - data[..2].Reverse(); - data.Slice(2, 2).Reverse(); - data.Slice(4, 4).Reverse(); - data[8..].Reverse(); - - _msb = BitConverterToInt64(data); - _lsb = BitConverterToInt64(data[8..]); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static long BitConverterToInt64(ReadOnlySpan value) - { -#if NET - return BitConverter.ToInt64(value); -#else - return Unsafe.ReadUnaligned(ref MemoryMarshal.GetReference(value)); -#endif - } - - private Uuid(string value) : this(value == null - ? throw new ArgumentNullException(nameof(value)) - : Guid.Parse(value)) { - } - - private Uuid(long msb, long lsb) { - _msb = msb; - _lsb = lsb; - } - - /// - /// Converts the to the gRPC wire format. - /// - /// - internal UUID ToDto() => - new UUID { - Structured = new UUID.Types.Structured { - LeastSignificantBits = _lsb, - MostSignificantBits = _msb - } - }; - - - /// - public bool Equals(Uuid other) => _lsb == other._lsb && _msb == other._msb; - - /// - public override bool Equals(object? obj) => obj is Uuid other && Equals(other); - - /// - public override int GetHashCode() => HashCode.Hash.Combine(_lsb).Combine(_msb); - - /// - /// Compares left and right for equality. - /// - /// A - /// A - /// True if left is equal to right. - public static bool operator ==(Uuid left, Uuid right) => left.Equals(right); - - /// - /// Compares left and right for inequality. - /// - /// A - /// A - /// True if left is not equal to right. - public static bool operator !=(Uuid left, Uuid right) => !left.Equals(right); - - /// - public override string ToString() => ToGuid().ToString(); - - /// - /// Converts the to a based on the supplied format. - /// - /// - /// - public string ToString(string format) => ToGuid().ToString(format); - - /// - /// Converts the to a . - /// - /// - public Guid ToGuid() { - if (!BitConverter.IsLittleEndian) { - throw new NotSupportedException(); - } - - Span data = stackalloc byte[16]; - if (!TryWriteBytes(data, _msb) || - !TryWriteBytes(data[8..], _lsb)) { - throw new InvalidOperationException(); - } - - data[..8].Reverse(); - data[..4].Reverse(); - data.Slice(4, 2).Reverse(); - data.Slice(6, 2).Reverse(); - data[8..].Reverse(); - -#if NET - return new Guid(data); -#else - return new Guid(data.ToArray()); -#endif - } - private static bool TryWriteBytes(Span destination, long value) - { - if (destination.Length < sizeof(long)) - return false; - - Unsafe.WriteUnaligned(ref MemoryMarshal.GetReference(destination), value); - return true; - } - - private bool TryWriteGuidBytes(Guid value, Span destination) - { -#if NET - return value.TryWriteBytes(destination); -#else - if (destination.Length < 16) - return false; - - var bytes = value.ToByteArray(); - bytes.CopyTo(destination); - return true; -#endif - } - } -} \ No newline at end of file diff --git a/src/EventStore.Client/Core/protos/code.proto b/src/EventStore.Client/Core/protos/code.proto deleted file mode 100644 index 98ae0ac18..000000000 --- a/src/EventStore.Client/Core/protos/code.proto +++ /dev/null @@ -1,186 +0,0 @@ -// Copyright 2020 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -syntax = "proto3"; - -package google.rpc; - -option go_package = "google.golang.org/genproto/googleapis/rpc/code;code"; -option java_multiple_files = true; -option java_outer_classname = "CodeProto"; -option java_package = "com.google.rpc"; -option objc_class_prefix = "RPC"; - -// The canonical error codes for gRPC APIs. -// -// -// Sometimes multiple error codes may apply. Services should return -// the most specific error code that applies. For example, prefer -// `OUT_OF_RANGE` over `FAILED_PRECONDITION` if both codes apply. -// Similarly prefer `NOT_FOUND` or `ALREADY_EXISTS` over `FAILED_PRECONDITION`. -enum Code { - // Not an error; returned on success - // - // HTTP Mapping: 200 OK - OK = 0; - - // The operation was cancelled, typically by the caller. - // - // HTTP Mapping: 499 Client Closed Request - CANCELLED = 1; - - // Unknown error. For example, this error may be returned when - // a `Status` value received from another address space belongs to - // an error space that is not known in this address space. Also - // errors raised by APIs that do not return enough error information - // may be converted to this error. - // - // HTTP Mapping: 500 Internal Server Error - UNKNOWN = 2; - - // The client specified an invalid argument. Note that this differs - // from `FAILED_PRECONDITION`. `INVALID_ARGUMENT` indicates arguments - // that are problematic regardless of the state of the system - // (e.g., a malformed file name). - // - // HTTP Mapping: 400 Bad Request - INVALID_ARGUMENT = 3; - - // The deadline expired before the operation could complete. For operations - // that change the state of the system, this error may be returned - // even if the operation has completed successfully. For example, a - // successful response from a server could have been delayed long - // enough for the deadline to expire. - // - // HTTP Mapping: 504 Gateway Timeout - DEADLINE_EXCEEDED = 4; - - // Some requested entity (e.g., file or directory) was not found. - // - // Note to server developers: if a request is denied for an entire class - // of users, such as gradual feature rollout or undocumented whitelist, - // `NOT_FOUND` may be used. If a request is denied for some users within - // a class of users, such as user-based access control, `PERMISSION_DENIED` - // must be used. - // - // HTTP Mapping: 404 Not Found - NOT_FOUND = 5; - - // The entity that a client attempted to create (e.g., file or directory) - // already exists. - // - // HTTP Mapping: 409 Conflict - ALREADY_EXISTS = 6; - - // The caller does not have permission to execute the specified - // operation. `PERMISSION_DENIED` must not be used for rejections - // caused by exhausting some resource (use `RESOURCE_EXHAUSTED` - // instead for those errors). `PERMISSION_DENIED` must not be - // used if the caller can not be identified (use `UNAUTHENTICATED` - // instead for those errors). This error code does not imply the - // request is valid or the requested entity exists or satisfies - // other pre-conditions. - // - // HTTP Mapping: 403 Forbidden - PERMISSION_DENIED = 7; - - // The request does not have valid authentication credentials for the - // operation. - // - // HTTP Mapping: 401 Unauthorized - UNAUTHENTICATED = 16; - - // Some resource has been exhausted, perhaps a per-user quota, or - // perhaps the entire file system is out of space. - // - // HTTP Mapping: 429 Too Many Requests - RESOURCE_EXHAUSTED = 8; - - // The operation was rejected because the system is not in a state - // required for the operation's execution. For example, the directory - // to be deleted is non-empty, an rmdir operation is applied to - // a non-directory, etc. - // - // Service implementors can use the following guidelines to decide - // between `FAILED_PRECONDITION`, `ABORTED`, and `UNAVAILABLE`: - // (a) Use `UNAVAILABLE` if the client can retry just the failing call. - // (b) Use `ABORTED` if the client should retry at a higher level - // (e.g., when a client-specified test-and-set fails, indicating the - // client should restart a read-modify-write sequence). - // (c) Use `FAILED_PRECONDITION` if the client should not retry until - // the system state has been explicitly fixed. E.g., if an "rmdir" - // fails because the directory is non-empty, `FAILED_PRECONDITION` - // should be returned since the client should not retry unless - // the files are deleted from the directory. - // - // HTTP Mapping: 400 Bad Request - FAILED_PRECONDITION = 9; - - // The operation was aborted, typically due to a concurrency issue such as - // a sequencer check failure or transaction abort. - // - // See the guidelines above for deciding between `FAILED_PRECONDITION`, - // `ABORTED`, and `UNAVAILABLE`. - // - // HTTP Mapping: 409 Conflict - ABORTED = 10; - - // The operation was attempted past the valid range. E.g., seeking or - // reading past end-of-file. - // - // Unlike `INVALID_ARGUMENT`, this error indicates a problem that may - // be fixed if the system state changes. For example, a 32-bit file - // system will generate `INVALID_ARGUMENT` if asked to read at an - // offset that is not in the range [0,2^32-1], but it will generate - // `OUT_OF_RANGE` if asked to read from an offset past the current - // file size. - // - // There is a fair bit of overlap between `FAILED_PRECONDITION` and - // `OUT_OF_RANGE`. We recommend using `OUT_OF_RANGE` (the more specific - // error) when it applies so that callers who are iterating through - // a space can easily look for an `OUT_OF_RANGE` error to detect when - // they are done. - // - // HTTP Mapping: 400 Bad Request - OUT_OF_RANGE = 11; - - // The operation is not implemented or is not supported/enabled in this - // service. - // - // HTTP Mapping: 501 Not Implemented - UNIMPLEMENTED = 12; - - // Internal errors. This means that some invariants expected by the - // underlying system have been broken. This error code is reserved - // for serious errors. - // - // HTTP Mapping: 500 Internal Server Error - INTERNAL = 13; - - // The service is currently unavailable. This is most likely a - // transient condition, which can be corrected by retrying with - // a backoff. Note that it is not always safe to retry - // non-idempotent operations. - // - // See the guidelines above for deciding between `FAILED_PRECONDITION`, - // `ABORTED`, and `UNAVAILABLE`. - // - // HTTP Mapping: 503 Service Unavailable - UNAVAILABLE = 14; - - // Unrecoverable data loss or corruption. - // - // HTTP Mapping: 500 Internal Server Error - DATA_LOSS = 15; -} diff --git a/src/EventStore.Client/Core/protos/gossip.proto b/src/EventStore.Client/Core/protos/gossip.proto deleted file mode 100644 index 192b3b0a4..000000000 --- a/src/EventStore.Client/Core/protos/gossip.proto +++ /dev/null @@ -1,44 +0,0 @@ -syntax = "proto3"; -package event_store.client.gossip; -option java_package = "com.eventstore.client.gossip"; - -import "shared.proto"; - -service Gossip { - rpc Read (event_store.client.Empty) returns (ClusterInfo); -} - -message ClusterInfo { - repeated MemberInfo members = 1; -} - -message EndPoint { - string address = 1; - uint32 port = 2; -} - -message MemberInfo { - enum VNodeState { - Initializing = 0; - DiscoverLeader = 1; - Unknown = 2; - PreReplica = 3; - CatchingUp = 4; - Clone = 5; - Follower = 6; - PreLeader = 7; - Leader = 8; - Manager = 9; - ShuttingDown = 10; - Shutdown = 11; - ReadOnlyLeaderless = 12; - PreReadOnlyReplica = 13; - ReadOnlyReplica = 14; - ResigningLeader = 15; - } - event_store.client.UUID instance_id = 1; - int64 time_stamp = 2; - VNodeState state = 3; - bool is_alive = 4; - EndPoint http_end_point = 5; -} diff --git a/src/EventStore.Client/Core/protos/operations.proto b/src/EventStore.Client/Core/protos/operations.proto deleted file mode 100644 index e8d5d2726..000000000 --- a/src/EventStore.Client/Core/protos/operations.proto +++ /dev/null @@ -1,45 +0,0 @@ -syntax = "proto3"; -package event_store.client.operations; -option java_package = "com.eventstore.client.operations"; - -import "shared.proto"; - -service Operations { - rpc StartScavenge (StartScavengeReq) returns (ScavengeResp); - rpc StopScavenge (StopScavengeReq) returns (ScavengeResp); - rpc Shutdown (Empty) returns (Empty); - rpc MergeIndexes (Empty) returns (Empty); - rpc ResignNode (Empty) returns (Empty); - rpc SetNodePriority (SetNodePriorityReq) returns (Empty); - rpc RestartPersistentSubscriptions (Empty) returns (Empty); -} - -message StartScavengeReq { - Options options = 1; - message Options { - int32 thread_count = 1; - int32 start_from_chunk = 2; - } -} - -message StopScavengeReq { - Options options = 1; - message Options { - string scavenge_id = 1; - } -} - -message ScavengeResp { - string scavenge_id = 1; - ScavengeResult scavenge_result = 2; - - enum ScavengeResult { - Started = 0; - InProgress = 1; - Stopped = 2; - } -} - -message SetNodePriorityReq { - int32 priority = 1; -} diff --git a/src/EventStore.Client/Core/protos/persistentsubscriptions.proto b/src/EventStore.Client/Core/protos/persistentsubscriptions.proto deleted file mode 100644 index a4109cad2..000000000 --- a/src/EventStore.Client/Core/protos/persistentsubscriptions.proto +++ /dev/null @@ -1,370 +0,0 @@ -syntax = "proto3"; -package event_store.client.persistent_subscriptions; -option java_package = "com.eventstore.dbclient.proto.persistentsubscriptions"; - -import "shared.proto"; - -service PersistentSubscriptions { - rpc Create (CreateReq) returns (CreateResp); - rpc Update (UpdateReq) returns (UpdateResp); - rpc Delete (DeleteReq) returns (DeleteResp); - rpc Read (stream ReadReq) returns (stream ReadResp); - rpc GetInfo (GetInfoReq) returns (GetInfoResp); - rpc ReplayParked (ReplayParkedReq) returns (ReplayParkedResp); - rpc List (ListReq) returns (ListResp); - rpc RestartSubsystem (event_store.client.Empty) returns (event_store.client.Empty); -} - -message ReadReq { - oneof content { - Options options = 1; - Ack ack = 2; - Nack nack = 3; - } - - message Options { - oneof stream_option { - event_store.client.StreamIdentifier stream_identifier = 1; - event_store.client.Empty all = 5; - } - - string group_name = 2; - int32 buffer_size = 3; - UUIDOption uuid_option = 4; - - message UUIDOption { - oneof content { - event_store.client.Empty structured = 1; - event_store.client.Empty string = 2; - } - } - } - - message Ack { - bytes id = 1; - repeated event_store.client.UUID ids = 2; - } - - message Nack { - bytes id = 1; - repeated event_store.client.UUID ids = 2; - Action action = 3; - string reason = 4; - - enum Action { - Unknown = 0; - Park = 1; - Retry = 2; - Skip = 3; - Stop = 4; - } - } -} - -message ReadResp { - oneof content { - ReadEvent event = 1; - SubscriptionConfirmation subscription_confirmation = 2; - } - message ReadEvent { - RecordedEvent event = 1; - RecordedEvent link = 2; - oneof position { - uint64 commit_position = 3; - event_store.client.Empty no_position = 4; - } - oneof count { - int32 retry_count = 5; - event_store.client.Empty no_retry_count = 6; - } - message RecordedEvent { - event_store.client.UUID id = 1; - event_store.client.StreamIdentifier stream_identifier = 2; - uint64 stream_revision = 3; - uint64 prepare_position = 4; - uint64 commit_position = 5; - map metadata = 6; - bytes custom_metadata = 7; - bytes data = 8; - } - } - message SubscriptionConfirmation { - string subscription_id = 1; - } -} - -message CreateReq { - Options options = 1; - - message Options { - oneof stream_option { - StreamOptions stream = 4; - AllOptions all = 5; - } - event_store.client.StreamIdentifier stream_identifier = 1 [deprecated=true]; - string group_name = 2; - Settings settings = 3; - } - - message StreamOptions { - event_store.client.StreamIdentifier stream_identifier = 1; - oneof revision_option { - uint64 revision = 2; - event_store.client.Empty start = 3; - event_store.client.Empty end = 4; - } - } - - message AllOptions { - oneof all_option { - Position position = 1; - event_store.client.Empty start = 2; - event_store.client.Empty end = 3; - } - oneof filter_option { - FilterOptions filter = 4; - event_store.client.Empty no_filter = 5; - } - message FilterOptions { - oneof filter { - Expression stream_identifier = 1; - Expression event_type = 2; - } - oneof window { - uint32 max = 3; - event_store.client.Empty count = 4; - } - uint32 checkpointIntervalMultiplier = 5; - - message Expression { - string regex = 1; - repeated string prefix = 2; - } - } - } - - message Position { - uint64 commit_position = 1; - uint64 prepare_position = 2; - } - - message Settings { - bool resolve_links = 1; - uint64 revision = 2 [deprecated = true]; - bool extra_statistics = 3; - int32 max_retry_count = 5; - int32 min_checkpoint_count = 7; - int32 max_checkpoint_count = 8; - int32 max_subscriber_count = 9; - int32 live_buffer_size = 10; - int32 read_batch_size = 11; - int32 history_buffer_size = 12; - ConsumerStrategy named_consumer_strategy = 13 [deprecated = true]; - oneof message_timeout { - int64 message_timeout_ticks = 4; - int32 message_timeout_ms = 14; - } - oneof checkpoint_after { - int64 checkpoint_after_ticks = 6; - int32 checkpoint_after_ms = 15; - } - string consumer_strategy = 16; - } - - enum ConsumerStrategy { - DispatchToSingle = 0; - RoundRobin = 1; - Pinned = 2; - } -} - -message CreateResp { -} - -message UpdateReq { - Options options = 1; - - message Options { - oneof stream_option { - StreamOptions stream = 4; - AllOptions all = 5; - } - event_store.client.StreamIdentifier stream_identifier = 1 [deprecated = true]; - string group_name = 2; - Settings settings = 3; - } - - message StreamOptions { - event_store.client.StreamIdentifier stream_identifier = 1; - oneof revision_option { - uint64 revision = 2; - event_store.client.Empty start = 3; - event_store.client.Empty end = 4; - } - } - - message AllOptions { - oneof all_option { - Position position = 1; - event_store.client.Empty start = 2; - event_store.client.Empty end = 3; - } - } - - message Position { - uint64 commit_position = 1; - uint64 prepare_position = 2; - } - - message Settings { - bool resolve_links = 1; - uint64 revision = 2 [deprecated = true]; - bool extra_statistics = 3; - int32 max_retry_count = 5; - int32 min_checkpoint_count = 7; - int32 max_checkpoint_count = 8; - int32 max_subscriber_count = 9; - int32 live_buffer_size = 10; - int32 read_batch_size = 11; - int32 history_buffer_size = 12; - ConsumerStrategy named_consumer_strategy = 13; - oneof message_timeout { - int64 message_timeout_ticks = 4; - int32 message_timeout_ms = 14; - } - oneof checkpoint_after { - int64 checkpoint_after_ticks = 6; - int32 checkpoint_after_ms = 15; - } - } - - enum ConsumerStrategy { - DispatchToSingle = 0; - RoundRobin = 1; - Pinned = 2; - } -} - -message UpdateResp { -} - -message DeleteReq { - Options options = 1; - - message Options { - oneof stream_option { - event_store.client.StreamIdentifier stream_identifier = 1; - event_store.client.Empty all = 3; - } - - string group_name = 2; - } -} - -message DeleteResp { -} - -message GetInfoReq { - Options options = 1; - - message Options { - oneof stream_option { - event_store.client.StreamIdentifier stream_identifier = 1; - event_store.client.Empty all = 2; - } - - string group_name = 3; - } -} - -message GetInfoResp { - SubscriptionInfo subscription_info = 1; -} - -message SubscriptionInfo { - string event_source = 1; - string group_name = 2; - string status = 3; - repeated ConnectionInfo connections = 4; - int32 average_per_second = 5; - int64 total_items = 6; - int64 count_since_last_measurement = 7; - string last_checkpointed_event_position = 8; - string last_known_event_position = 9; - bool resolve_link_tos = 10; - string start_from = 11; - int32 message_timeout_milliseconds = 12; - bool extra_statistics = 13; - int32 max_retry_count = 14; - int32 live_buffer_size = 15; - int32 buffer_size = 16; - int32 read_batch_size = 17; - int32 check_point_after_milliseconds = 18; - int32 min_check_point_count = 19; - int32 max_check_point_count = 20; - int32 read_buffer_count = 21; - int64 live_buffer_count = 22; - int32 retry_buffer_count = 23; - int32 total_in_flight_messages = 24; - int32 outstanding_messages_count = 25; - string named_consumer_strategy = 26; - int32 max_subscriber_count = 27; - int64 parked_message_count = 28; - - message ConnectionInfo { - string from = 1; - string username = 2; - int32 average_items_per_second = 3; - int64 total_items = 4; - int64 count_since_last_measurement = 5; - repeated Measurement observed_measurements = 6; - int32 available_slots = 7; - int32 in_flight_messages = 8; - string connection_name = 9; - } - - message Measurement { - string key = 1; - int64 value = 2; - } -} - -message ReplayParkedReq { - Options options = 1; - - message Options { - string group_name = 1; - oneof stream_option { - event_store.client.StreamIdentifier stream_identifier = 2; - event_store.client.Empty all = 3; - } - oneof stop_at_option { - int64 stop_at = 4; - event_store.client.Empty no_limit = 5; - } - } -} - -message ReplayParkedResp { -} - -message ListReq { - Options options = 1; - - message Options { - oneof list_option { - event_store.client.Empty list_all_subscriptions = 1; - StreamOption list_for_stream = 2; - } - } - message StreamOption { - oneof stream_option { - event_store.client.StreamIdentifier stream = 1; - event_store.client.Empty all = 2; - } - } -} - -message ListResp { - repeated SubscriptionInfo subscriptions = 1; -} diff --git a/src/EventStore.Client/Core/protos/projectionmanagement.proto b/src/EventStore.Client/Core/protos/projectionmanagement.proto deleted file mode 100644 index f1733b55a..000000000 --- a/src/EventStore.Client/Core/protos/projectionmanagement.proto +++ /dev/null @@ -1,174 +0,0 @@ -syntax = "proto3"; -package event_store.client.projections; -option java_package = "com.eventstore.client.projections"; - -import "google/protobuf/struct.proto"; -import "shared.proto"; - -service Projections { - rpc Create (CreateReq) returns (CreateResp); - rpc Update (UpdateReq) returns (UpdateResp); - rpc Delete (DeleteReq) returns (DeleteResp); - rpc Statistics (StatisticsReq) returns (stream StatisticsResp); - rpc Disable (DisableReq) returns (DisableResp); - rpc Enable (EnableReq) returns (EnableResp); - rpc Reset (ResetReq) returns (ResetResp); - rpc State (StateReq) returns (StateResp); - rpc Result (ResultReq) returns (ResultResp); - rpc RestartSubsystem (Empty) returns (Empty); -} - -message CreateReq { - Options options = 1; - - message Options { - oneof mode { - event_store.client.Empty one_time = 1; - Transient transient = 2; - Continuous continuous = 3; - } - string query = 4; - - message Transient { - string name = 1; - } - message Continuous { - string name = 1; - bool track_emitted_streams = 2; - } - } -} - -message CreateResp { -} - -message UpdateReq { - Options options = 1; - - message Options { - string name = 1; - string query = 2; - oneof emit_option { - bool emit_enabled = 3; - event_store.client.Empty no_emit_options = 4; - } - } -} - -message UpdateResp { -} - -message DeleteReq { - Options options = 1; - - message Options { - string name = 1; - bool delete_emitted_streams = 2; - bool delete_state_stream = 3; - bool delete_checkpoint_stream = 4; - } -} - -message DeleteResp { -} - -message StatisticsReq { - Options options = 1; - message Options { - oneof mode { - string name = 1; - event_store.client.Empty all = 2; - event_store.client.Empty transient = 3; - event_store.client.Empty continuous = 4; - event_store.client.Empty one_time = 5; - } - } -} - -message StatisticsResp { - Details details = 1; - - message Details { - int64 coreProcessingTime = 1; - int64 version = 2; - int64 epoch = 3; - string effectiveName = 4; - int32 writesInProgress = 5; - int32 readsInProgress = 6; - int32 partitionsCached = 7; - string status = 8; - string stateReason = 9; - string name = 10; - string mode = 11; - string position = 12; - float progress = 13; - string lastCheckpoint = 14; - int64 eventsProcessedAfterRestart = 15; - string checkpointStatus = 16; - int64 bufferedEvents = 17; - int32 writePendingEventsBeforeCheckpoint = 18; - int32 writePendingEventsAfterCheckpoint = 19; - } -} - -message StateReq { - Options options = 1; - - message Options { - string name = 1; - string partition = 2; - } -} - -message StateResp { - google.protobuf.Value state = 1; -} - -message ResultReq { - Options options = 1; - - message Options { - string name = 1; - string partition = 2; - } -} - -message ResultResp { - google.protobuf.Value result = 1; -} - -message ResetReq { - Options options = 1; - - message Options { - string name = 1; - bool write_checkpoint = 2; - } -} - -message ResetResp { -} - - -message EnableReq { - Options options = 1; - - message Options { - string name = 1; - } -} - -message EnableResp { -} - -message DisableReq { - Options options = 1; - - message Options { - string name = 1; - bool write_checkpoint = 2; - } -} - -message DisableResp { -} diff --git a/src/EventStore.Client/Core/protos/serverfeatures.proto b/src/EventStore.Client/Core/protos/serverfeatures.proto deleted file mode 100644 index cba04f2c6..000000000 --- a/src/EventStore.Client/Core/protos/serverfeatures.proto +++ /dev/null @@ -1,19 +0,0 @@ -syntax = "proto3"; -package event_store.client.server_features; -option java_package = "com.eventstore.dbclient.proto.serverfeatures"; -import "shared.proto"; - -service ServerFeatures { - rpc GetSupportedMethods (event_store.client.Empty) returns (SupportedMethods); -} - -message SupportedMethods { - repeated SupportedMethod methods = 1; - string event_store_server_version = 2; -} - -message SupportedMethod { - string method_name = 1; - string service_name = 2; - repeated string features = 3; -} diff --git a/src/EventStore.Client/Core/protos/shared.proto b/src/EventStore.Client/Core/protos/shared.proto deleted file mode 100644 index 2b113aed8..000000000 --- a/src/EventStore.Client/Core/protos/shared.proto +++ /dev/null @@ -1,61 +0,0 @@ -syntax = "proto3"; -package event_store.client; -option java_package = "com.eventstore.dbclient.proto.shared"; -import "google/protobuf/empty.proto"; - -message UUID { - oneof value { - Structured structured = 1; - string string = 2; - } - - message Structured { - int64 most_significant_bits = 1; - int64 least_significant_bits = 2; - } -} -message Empty { -} - -message StreamIdentifier { - reserved 1 to 2; - bytes stream_name = 3; -} - -message AllStreamPosition { - uint64 commit_position = 1; - uint64 prepare_position = 2; -} - -message WrongExpectedVersion { - oneof current_stream_revision_option { - uint64 current_stream_revision = 1; - google.protobuf.Empty current_no_stream = 2; - } - oneof expected_stream_position_option { - uint64 expected_stream_position = 3; - google.protobuf.Empty expected_any = 4; - google.protobuf.Empty expected_stream_exists = 5; - google.protobuf.Empty expected_no_stream = 6; - } -} - -message AccessDenied {} - -message StreamDeleted { - StreamIdentifier stream_identifier = 1; -} - -message Timeout {} - -message Unknown {} - -message InvalidTransaction {} - -message MaximumAppendSizeExceeded { - uint32 maxAppendSize = 1; -} - -message BadRequest { - string message = 1; -} diff --git a/src/EventStore.Client/Core/protos/status.proto b/src/EventStore.Client/Core/protos/status.proto deleted file mode 100644 index 65eced268..000000000 --- a/src/EventStore.Client/Core/protos/status.proto +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright 2020 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -syntax = "proto3"; - -package google.rpc; - -import "google/protobuf/any.proto"; -import "code.proto"; - -option cc_enable_arenas = true; -option go_package = "google.golang.org/genproto/googleapis/rpc/status;status"; -option java_multiple_files = true; -option java_outer_classname = "StatusProto"; -option java_package = "com.google.rpc"; -option objc_class_prefix = "RPC"; - -// The `Status` type defines a logical error model that is suitable for -// different programming environments, including REST APIs and RPC APIs. It is -// used by [gRPC](https://github.com/grpc). Each `Status` message contains -// three pieces of data: error code, error message, and error details. -// -// You can find out more about this error model and how to work with it in the -// [API Design Guide](https://cloud.google.com/apis/design/errors). -message Status { - // The status code, which should be an enum value of [google.rpc.Code][google.rpc.Code]. - google.rpc.Code code = 1; - - // A developer-facing error message, which should be in English. Any - // user-facing error message should be localized and sent in the - // [google.rpc.Status.details][google.rpc.Status.details] field, or localized by the client. - string message = 2; - - // A list of messages that carry the error details. There is a common set of - // message types for APIs to use. - google.protobuf.Any details = 3; -} diff --git a/src/EventStore.Client/Core/protos/streams.proto b/src/EventStore.Client/Core/protos/streams.proto deleted file mode 100644 index ac599fb82..000000000 --- a/src/EventStore.Client/Core/protos/streams.proto +++ /dev/null @@ -1,316 +0,0 @@ -syntax = "proto3"; -package event_store.client.streams; -option java_package = "com.eventstore.dbclient.proto.streams"; - -import "shared.proto"; -import "status.proto"; -import "google/protobuf/duration.proto"; -import "google/protobuf/empty.proto"; -import "google/protobuf/timestamp.proto"; - -service Streams { - rpc Read (ReadReq) returns (stream ReadResp); - rpc Append (stream AppendReq) returns (AppendResp); - rpc Delete (DeleteReq) returns (DeleteResp); - rpc Tombstone (TombstoneReq) returns (TombstoneResp); - rpc BatchAppend (stream BatchAppendReq) returns (stream BatchAppendResp); -} - -message ReadReq { - Options options = 1; - - message Options { - oneof stream_option { - StreamOptions stream = 1; - AllOptions all = 2; - } - ReadDirection read_direction = 3; - bool resolve_links = 4; - oneof count_option { - uint64 count = 5; - SubscriptionOptions subscription = 6; - } - oneof filter_option { - FilterOptions filter = 7; - event_store.client.Empty no_filter = 8; - } - UUIDOption uuid_option = 9; - ControlOption control_option = 10; - - enum ReadDirection { - Forwards = 0; - Backwards = 1; - } - message StreamOptions { - event_store.client.StreamIdentifier stream_identifier = 1; - oneof revision_option { - uint64 revision = 2; - event_store.client.Empty start = 3; - event_store.client.Empty end = 4; - } - } - message AllOptions { - oneof all_option { - Position position = 1; - event_store.client.Empty start = 2; - event_store.client.Empty end = 3; - } - } - message SubscriptionOptions { - } - message Position { - uint64 commit_position = 1; - uint64 prepare_position = 2; - } - message FilterOptions { - oneof filter { - Expression stream_identifier = 1; - Expression event_type = 2; - } - oneof window { - uint32 max = 3; - event_store.client.Empty count = 4; - } - uint32 checkpointIntervalMultiplier = 5; - - message Expression { - string regex = 1; - repeated string prefix = 2; - } - } - message UUIDOption { - oneof content { - event_store.client.Empty structured = 1; - event_store.client.Empty string = 2; - } - } - message ControlOption { - uint32 compatibility = 1; - } - } -} - -message ReadResp { - oneof content { - ReadEvent event = 1; - SubscriptionConfirmation confirmation = 2; - Checkpoint checkpoint = 3; - StreamNotFound stream_not_found = 4; - uint64 first_stream_position = 5; - uint64 last_stream_position = 6; - AllStreamPosition last_all_stream_position = 7; - CaughtUp caught_up = 8; - FellBehind fell_behind = 9; - } - - message CaughtUp {} - - message FellBehind {} - - message ReadEvent { - RecordedEvent event = 1; - RecordedEvent link = 2; - oneof position { - uint64 commit_position = 3; - event_store.client.Empty no_position = 4; - } - - message RecordedEvent { - event_store.client.UUID id = 1; - event_store.client.StreamIdentifier stream_identifier = 2; - uint64 stream_revision = 3; - uint64 prepare_position = 4; - uint64 commit_position = 5; - map metadata = 6; - bytes custom_metadata = 7; - bytes data = 8; - } - } - message SubscriptionConfirmation { - string subscription_id = 1; - } - message Checkpoint { - uint64 commit_position = 1; - uint64 prepare_position = 2; - } - message StreamNotFound { - event_store.client.StreamIdentifier stream_identifier = 1; - } -} - -message AppendReq { - oneof content { - Options options = 1; - ProposedMessage proposed_message = 2; - } - - message Options { - event_store.client.StreamIdentifier stream_identifier = 1; - oneof expected_stream_revision { - uint64 revision = 2; - event_store.client.Empty no_stream = 3; - event_store.client.Empty any = 4; - event_store.client.Empty stream_exists = 5; - } - } - message ProposedMessage { - event_store.client.UUID id = 1; - map metadata = 2; - bytes custom_metadata = 3; - bytes data = 4; - } -} - -message AppendResp { - oneof result { - Success success = 1; - WrongExpectedVersion wrong_expected_version = 2; - } - - message Position { - uint64 commit_position = 1; - uint64 prepare_position = 2; - } - - message Success { - oneof current_revision_option { - uint64 current_revision = 1; - event_store.client.Empty no_stream = 2; - } - oneof position_option { - Position position = 3; - event_store.client.Empty no_position = 4; - } - } - - message WrongExpectedVersion { - oneof current_revision_option_20_6_0 { - uint64 current_revision_20_6_0 = 1; - event_store.client.Empty no_stream_20_6_0 = 2; - } - oneof expected_revision_option_20_6_0 { - uint64 expected_revision_20_6_0 = 3; - event_store.client.Empty any_20_6_0 = 4; - event_store.client.Empty stream_exists_20_6_0 = 5; - } - oneof current_revision_option { - uint64 current_revision = 6; - event_store.client.Empty current_no_stream = 7; - } - oneof expected_revision_option { - uint64 expected_revision = 8; - event_store.client.Empty expected_any = 9; - event_store.client.Empty expected_stream_exists = 10; - event_store.client.Empty expected_no_stream = 11; - } - - } -} - -message BatchAppendReq { - event_store.client.UUID correlation_id = 1; - Options options = 2; - repeated ProposedMessage proposed_messages = 3; - bool is_final = 4; - - message Options { - event_store.client.StreamIdentifier stream_identifier = 1; - oneof expected_stream_position { - uint64 stream_position = 2; - google.protobuf.Empty no_stream = 3; - google.protobuf.Empty any = 4; - google.protobuf.Empty stream_exists = 5; - } - oneof deadline_option { - google.protobuf.Timestamp deadline_21_10_0 = 6; - google.protobuf.Duration deadline = 7; - } - } - - message ProposedMessage { - event_store.client.UUID id = 1; - map metadata = 2; - bytes custom_metadata = 3; - bytes data = 4; - } -} - -message BatchAppendResp { - event_store.client.UUID correlation_id = 1; - oneof result { - google.rpc.Status error = 2; - Success success = 3; - } - - event_store.client.StreamIdentifier stream_identifier = 4; - - oneof expected_stream_position { - uint64 stream_position = 5; - google.protobuf.Empty no_stream = 6; - google.protobuf.Empty any = 7; - google.protobuf.Empty stream_exists = 8; - } - - message Success { - oneof current_revision_option { - uint64 current_revision = 1; - google.protobuf.Empty no_stream = 2; - } - oneof position_option { - event_store.client.AllStreamPosition position = 3; - google.protobuf.Empty no_position = 4; - } - } -} - -message DeleteReq { - Options options = 1; - - message Options { - event_store.client.StreamIdentifier stream_identifier = 1; - oneof expected_stream_revision { - uint64 revision = 2; - event_store.client.Empty no_stream = 3; - event_store.client.Empty any = 4; - event_store.client.Empty stream_exists = 5; - } - } -} - -message DeleteResp { - oneof position_option { - Position position = 1; - event_store.client.Empty no_position = 2; - } - - message Position { - uint64 commit_position = 1; - uint64 prepare_position = 2; - } -} - -message TombstoneReq { - Options options = 1; - - message Options { - event_store.client.StreamIdentifier stream_identifier = 1; - oneof expected_stream_revision { - uint64 revision = 2; - event_store.client.Empty no_stream = 3; - event_store.client.Empty any = 4; - event_store.client.Empty stream_exists = 5; - } - } -} - -message TombstoneResp { - oneof position_option { - Position position = 1; - event_store.client.Empty no_position = 2; - } - - message Position { - uint64 commit_position = 1; - uint64 prepare_position = 2; - } -} diff --git a/src/EventStore.Client/Core/protos/usermanagement.proto b/src/EventStore.Client/Core/protos/usermanagement.proto deleted file mode 100644 index 1fab5a73e..000000000 --- a/src/EventStore.Client/Core/protos/usermanagement.proto +++ /dev/null @@ -1,119 +0,0 @@ -syntax = "proto3"; -package event_store.client.users; -option java_package = "com.eventstore.client.users"; - -service Users { - rpc Create (CreateReq) returns (CreateResp); - rpc Update (UpdateReq) returns (UpdateResp); - rpc Delete (DeleteReq) returns (DeleteResp); - rpc Disable (DisableReq) returns (DisableResp); - rpc Enable (EnableReq) returns (EnableResp); - rpc Details (DetailsReq) returns (stream DetailsResp); - rpc ChangePassword (ChangePasswordReq) returns (ChangePasswordResp); - rpc ResetPassword (ResetPasswordReq) returns (ResetPasswordResp); -} - -message CreateReq { - Options options = 1; - message Options { - string login_name = 1; - string password = 2; - string full_name = 3; - repeated string groups = 4; - } -} - -message CreateResp { - -} - -message UpdateReq { - Options options = 1; - message Options { - string login_name = 1; - string password = 2; - string full_name = 3; - repeated string groups = 4; - } -} - -message UpdateResp { - -} - -message DeleteReq { - Options options = 1; - message Options { - string login_name = 1; - } -} - -message DeleteResp { - -} - -message EnableReq { - Options options = 1; - message Options { - string login_name = 1; - } -} - -message EnableResp { - -} - -message DisableReq { - Options options = 1; - message Options { - string login_name = 1; - } -} - -message DisableResp { -} - -message DetailsReq { - Options options = 1; - message Options { - string login_name = 1; - } -} - -message DetailsResp { - UserDetails user_details = 1; - message UserDetails { - string login_name = 1; - string full_name = 2; - repeated string groups = 3; - DateTime last_updated = 4; - bool disabled = 5; - - message DateTime { - int64 ticks_since_epoch = 1; - } - } -} - -message ChangePasswordReq { - Options options = 1; - message Options { - string login_name = 1; - string current_password = 2; - string new_password = 3; - } -} - -message ChangePasswordResp { -} - -message ResetPasswordReq { - Options options = 1; - message Options { - string login_name = 1; - string new_password = 2; - } -} - -message ResetPasswordResp { -} diff --git a/src/EventStore.Client/EventStore.Client.csproj b/src/EventStore.Client/EventStore.Client.csproj deleted file mode 100644 index 6aa82bad0..000000000 --- a/src/EventStore.Client/EventStore.Client.csproj +++ /dev/null @@ -1,57 +0,0 @@ - - - - EventStore.Client - The base GRPC client library for Event Store. Get the open source or commercial versions of Event Store server from https://eventstore.com/ - EventStore.Client.Grpc - - - - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/EventStore.Client/EventStore.Client.csproj.DotSettings b/src/EventStore.Client/EventStore.Client.csproj.DotSettings deleted file mode 100644 index ae68911e8..000000000 --- a/src/EventStore.Client/EventStore.Client.csproj.DotSettings +++ /dev/null @@ -1,5 +0,0 @@ - - True - True - True - True \ No newline at end of file diff --git a/src/EventStore.Client/Operations/DatabaseScavengeResult.cs b/src/EventStore.Client/Operations/DatabaseScavengeResult.cs deleted file mode 100644 index 9a09143fa..000000000 --- a/src/EventStore.Client/Operations/DatabaseScavengeResult.cs +++ /dev/null @@ -1,81 +0,0 @@ -using System; - -namespace EventStore.Client { - /// - /// A structure representing the result of a scavenge operation. - /// - public readonly struct DatabaseScavengeResult : IEquatable { - /// - /// The ID of the scavenge operation. - /// - public string ScavengeId { get; } - - /// - /// The of the scavenge operation. - /// - public ScavengeResult Result { get; } - - /// - /// A scavenge operation that has started. - /// - /// - /// - public static DatabaseScavengeResult Started(string scavengeId) => - new DatabaseScavengeResult(scavengeId, ScavengeResult.Started); - - /// - /// A scavenge operation that has stopped. - /// - /// - /// - public static DatabaseScavengeResult Stopped(string scavengeId) => - new DatabaseScavengeResult(scavengeId, ScavengeResult.Stopped); - - /// - /// A scavenge operation that is currently in progress. - /// - /// - /// - public static DatabaseScavengeResult InProgress(string scavengeId) => - new DatabaseScavengeResult(scavengeId, ScavengeResult.InProgress); - - /// - /// A scavenge operation whose state is unknown. - /// - /// - /// - public static DatabaseScavengeResult Unknown(string scavengeId) => - new DatabaseScavengeResult(scavengeId, ScavengeResult.Unknown); - - private DatabaseScavengeResult(string scavengeId, ScavengeResult result) { - ScavengeId = scavengeId; - Result = result; - } - - /// - public bool Equals(DatabaseScavengeResult other) => ScavengeId == other.ScavengeId && Result == other.Result; - - /// - public override bool Equals(object? obj) => obj is DatabaseScavengeResult other && Equals(other); - - /// - public override int GetHashCode() => HashCode.Hash.Combine(ScavengeId).Combine(Result); - - /// - /// Compares left and right for equality. - /// - /// - /// - /// True if left is equal to right. - public static bool operator ==(DatabaseScavengeResult left, DatabaseScavengeResult right) => left.Equals(right); - - /// - /// Compares left and right for inequality. - /// - /// - /// - /// True if left is not equal to right. - public static bool operator !=(DatabaseScavengeResult left, DatabaseScavengeResult right) => - !left.Equals(right); - } -} diff --git a/src/EventStore.Client/Operations/EventStoreOperationsClient.Admin.cs b/src/EventStore.Client/Operations/EventStoreOperationsClient.Admin.cs deleted file mode 100644 index bfa750145..000000000 --- a/src/EventStore.Client/Operations/EventStoreOperationsClient.Admin.cs +++ /dev/null @@ -1,103 +0,0 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using EventStore.Client.Operations; - -namespace EventStore.Client { - public partial class EventStoreOperationsClient { - private static readonly Empty EmptyResult = new Empty(); - - /// - /// Shuts down the EventStoreDB node. - /// - /// - /// - /// - /// - public async Task ShutdownAsync( - TimeSpan? deadline = null, - UserCredentials? userCredentials = null, - CancellationToken cancellationToken = default) { - var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); - using var call = new Operations.Operations.OperationsClient( - channelInfo.CallInvoker).ShutdownAsync(EmptyResult, - EventStoreCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); - await call.ResponseAsync.ConfigureAwait(false); - } - - /// - /// Initiates an index merge operation. - /// - /// - /// - /// - /// - public async Task MergeIndexesAsync( - TimeSpan? deadline = null, - UserCredentials? userCredentials = null, - CancellationToken cancellationToken = default) { - var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); - using var call = new Operations.Operations.OperationsClient( - channelInfo.CallInvoker).MergeIndexesAsync(EmptyResult, - EventStoreCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); - await call.ResponseAsync.ConfigureAwait(false); - } - - /// - /// Resigns a node. - /// - /// - /// - /// - /// - public async Task ResignNodeAsync( - TimeSpan? deadline = null, - UserCredentials? userCredentials = null, - CancellationToken cancellationToken = default) { - var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); - using var call = new Operations.Operations.OperationsClient( - channelInfo.CallInvoker).ResignNodeAsync(EmptyResult, - EventStoreCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); - await call.ResponseAsync.ConfigureAwait(false); - } - - /// - /// Sets the node priority. - /// - /// - /// - /// - /// - /// - public async Task SetNodePriorityAsync(int nodePriority, - TimeSpan? deadline = null, - UserCredentials? userCredentials = null, - CancellationToken cancellationToken = default) { - var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); - using var call = new Operations.Operations.OperationsClient( - channelInfo.CallInvoker).SetNodePriorityAsync( - new SetNodePriorityReq {Priority = nodePriority}, - EventStoreCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); - await call.ResponseAsync.ConfigureAwait(false); - } - - /// - /// Restart persistent subscriptions - /// - /// - /// - /// - /// - public async Task RestartPersistentSubscriptions( - TimeSpan? deadline = null, - UserCredentials? userCredentials = null, - CancellationToken cancellationToken = default) { - var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); - using var call = new Operations.Operations.OperationsClient( - channelInfo.CallInvoker).RestartPersistentSubscriptionsAsync( - EmptyResult, - EventStoreCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); - await call.ResponseAsync.ConfigureAwait(false); - } - } -} diff --git a/src/EventStore.Client/Operations/EventStoreOperationsClient.Scavenge.cs b/src/EventStore.Client/Operations/EventStoreOperationsClient.Scavenge.cs deleted file mode 100644 index 43dcfc50f..000000000 --- a/src/EventStore.Client/Operations/EventStoreOperationsClient.Scavenge.cs +++ /dev/null @@ -1,82 +0,0 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using EventStore.Client.Operations; - -namespace EventStore.Client { - public partial class EventStoreOperationsClient { - /// - /// Starts a scavenge operation. - /// - /// - /// - /// - /// - /// - /// - /// - public async Task StartScavengeAsync( - int threadCount = 1, - int startFromChunk = 0, - TimeSpan? deadline = null, - UserCredentials? userCredentials = null, - CancellationToken cancellationToken = default) { - if (threadCount <= 0) { - throw new ArgumentOutOfRangeException(nameof(threadCount)); - } - - if (startFromChunk < 0) { - throw new ArgumentOutOfRangeException(nameof(startFromChunk)); - } - - var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); - using var call = new Operations.Operations.OperationsClient( - channelInfo.CallInvoker).StartScavengeAsync( - new StartScavengeReq { - Options = new StartScavengeReq.Types.Options { - ThreadCount = threadCount, - StartFromChunk = startFromChunk - } - }, - EventStoreCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); - var result = await call.ResponseAsync.ConfigureAwait(false); - - return result.ScavengeResult switch { - ScavengeResp.Types.ScavengeResult.Started => DatabaseScavengeResult.Started(result.ScavengeId), - ScavengeResp.Types.ScavengeResult.Stopped => DatabaseScavengeResult.Stopped(result.ScavengeId), - ScavengeResp.Types.ScavengeResult.InProgress => DatabaseScavengeResult.InProgress(result.ScavengeId), - _ => DatabaseScavengeResult.Unknown(result.ScavengeId) - }; - } - - /// - /// Stops a scavenge operation. - /// - /// - /// - /// - /// - /// - public async Task StopScavengeAsync( - string scavengeId, - TimeSpan? deadline = null, - UserCredentials? userCredentials = null, - CancellationToken cancellationToken = default) { - var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); - var result = await new Operations.Operations.OperationsClient( - channelInfo.CallInvoker).StopScavengeAsync(new StopScavengeReq { - Options = new StopScavengeReq.Types.Options { - ScavengeId = scavengeId - } - }, EventStoreCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)) - .ResponseAsync.ConfigureAwait(false); - - return result.ScavengeResult switch { - ScavengeResp.Types.ScavengeResult.Started => DatabaseScavengeResult.Started(result.ScavengeId), - ScavengeResp.Types.ScavengeResult.Stopped => DatabaseScavengeResult.Stopped(result.ScavengeId), - ScavengeResp.Types.ScavengeResult.InProgress => DatabaseScavengeResult.InProgress(result.ScavengeId), - _ => DatabaseScavengeResult.Unknown(result.ScavengeId) - }; - } - } -} diff --git a/src/EventStore.Client/Operations/EventStoreOperationsClient.cs b/src/EventStore.Client/Operations/EventStoreOperationsClient.cs deleted file mode 100644 index 672383348..000000000 --- a/src/EventStore.Client/Operations/EventStoreOperationsClient.cs +++ /dev/null @@ -1,33 +0,0 @@ -using Grpc.Core; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Extensions.Options; - -namespace EventStore.Client; - -/// -/// The client used to perform maintenance and other administrative tasks on the EventStoreDB. -/// -public sealed partial class EventStoreOperationsClient : EventStoreClientBase { - static readonly Dictionary> ExceptionMap = - new() { - [Constants.Exceptions.ScavengeNotFound] = ex => new ScavengeNotFoundException( - ex.Trailers.FirstOrDefault(x => x.Key == Constants.Exceptions.ScavengeId)?.Value - ) - }; - - readonly ILogger _log; - - /// - /// Constructs a new . This method is not intended to be called directly in your code. - /// - /// - public EventStoreOperationsClient(IOptions options) : this(options.Value) { } - - /// - /// Constructs a new . - /// - /// - public EventStoreOperationsClient(EventStoreClientSettings? settings = null) : base(settings, ExceptionMap) => - _log = Settings.LoggerFactory?.CreateLogger() ?? new NullLogger(); -} \ No newline at end of file diff --git a/src/EventStore.Client/Operations/EventStoreOperationsClientServiceCollectionExtensions.cs b/src/EventStore.Client/Operations/EventStoreOperationsClientServiceCollectionExtensions.cs deleted file mode 100644 index 9f664277f..000000000 --- a/src/EventStore.Client/Operations/EventStoreOperationsClientServiceCollectionExtensions.cs +++ /dev/null @@ -1,74 +0,0 @@ -// ReSharper disable CheckNamespace - -using System; -using System.Net.Http; -using EventStore.Client; -using Grpc.Core.Interceptors; -using Microsoft.Extensions.DependencyInjection.Extensions; -using Microsoft.Extensions.Logging; -using EventStoreOperationsClient = EventStore.Client.EventStoreOperationsClient; - -namespace Microsoft.Extensions.DependencyInjection { - /// - /// A set of extension methods for which provide support for an . - /// - public static class EventStoreOperationsClientServiceCollectionExtensions { - - /// - /// Adds an to the . - /// - /// - /// - /// - /// - /// - public static IServiceCollection AddEventStoreOperationsClient(this IServiceCollection services, Uri address, - Func? createHttpMessageHandler = null) - => services.AddEventStoreOperationsClient(options => { - options.ConnectivitySettings.Address = address; - options.CreateHttpMessageHandler = createHttpMessageHandler; - }); - - /// - /// Adds an to the . - /// - /// - /// - /// - /// - public static IServiceCollection AddEventStoreOperationsClient(this IServiceCollection services, - Action? configureOptions = null) => - services.AddEventStoreOperationsClient(new EventStoreClientSettings(), configureOptions); - - /// - /// Adds an to the . - /// - /// - /// - /// - /// - /// - public static IServiceCollection AddEventStoreOperationsClient(this IServiceCollection services, - string connectionString, Action? configureOptions = null) => - services.AddEventStoreOperationsClient(EventStoreClientSettings.Create(connectionString), configureOptions); - - private static IServiceCollection AddEventStoreOperationsClient(this IServiceCollection services, - EventStoreClientSettings options, Action? configureOptions) { - if (services == null) { - throw new ArgumentNullException(nameof(services)); - } - - configureOptions?.Invoke(options); - - services.TryAddSingleton(provider => { - options.LoggerFactory ??= provider.GetService(); - options.Interceptors ??= provider.GetServices(); - - return new EventStoreOperationsClient(options); - }); - - return services; - } - } -} -// ReSharper restore CheckNamespace diff --git a/src/EventStore.Client/Operations/ScavengeResult.cs b/src/EventStore.Client/Operations/ScavengeResult.cs deleted file mode 100644 index 6b8802d85..000000000 --- a/src/EventStore.Client/Operations/ScavengeResult.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace EventStore.Client { - /// - /// An enumeration that represents the result of a scavenge operation. - /// - public enum ScavengeResult { - /// - /// The scavenge operation has started. - /// - Started, - /// - /// The scavenge operation is in progress. - /// - InProgress, - - /// - /// The scavenge operation has stopped. - /// - Stopped, - - /// - /// The status of the scavenge operation was unknown. - /// - Unknown - } -} diff --git a/src/EventStore.Client/PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Create.cs b/src/EventStore.Client/PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Create.cs deleted file mode 100644 index 4cb7acac0..000000000 --- a/src/EventStore.Client/PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Create.cs +++ /dev/null @@ -1,252 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using EventStore.Client.PersistentSubscriptions; - -namespace EventStore.Client { - partial class EventStorePersistentSubscriptionsClient { - private static readonly IDictionary NamedConsumerStrategyToCreateProto - = new Dictionary { - [SystemConsumerStrategies.DispatchToSingle] = CreateReq.Types.ConsumerStrategy.DispatchToSingle, - [SystemConsumerStrategies.RoundRobin] = CreateReq.Types.ConsumerStrategy.RoundRobin, - [SystemConsumerStrategies.Pinned] = CreateReq.Types.ConsumerStrategy.Pinned, - }; - - private static CreateReq.Types.StreamOptions StreamOptionsForCreateProto(string streamName, StreamPosition position) { - if (position == StreamPosition.Start) { - return new CreateReq.Types.StreamOptions { - StreamIdentifier = streamName, - Start = new Empty() - }; - } - - if (position == StreamPosition.End) { - return new CreateReq.Types.StreamOptions { - StreamIdentifier = streamName, - End = new Empty() - }; - } - - return new CreateReq.Types.StreamOptions { - StreamIdentifier = streamName, - Revision = position.ToUInt64() - }; - } - - private static CreateReq.Types.AllOptions AllOptionsForCreateProto(Position position, IEventFilter? filter) { - var allFilter = GetFilterOptions(filter); - CreateReq.Types.AllOptions allOptions; - if (position == Position.Start) { - allOptions = new CreateReq.Types.AllOptions { - Start = new Empty(), - }; - } else if (position == Position.End) { - allOptions = new CreateReq.Types.AllOptions { - End = new Empty() - }; - } else { - allOptions = new CreateReq.Types.AllOptions { - Position = new CreateReq.Types.Position { - CommitPosition = position.CommitPosition, - PreparePosition = position.PreparePosition - } - }; - } - - if (allFilter is null) { - allOptions.NoFilter = new Empty(); - } else { - allOptions.Filter = allFilter; - } - - return allOptions; - } - - private static CreateReq.Types.AllOptions.Types.FilterOptions? GetFilterOptions(IEventFilter? filter) { - if (filter == null) { - return null; - } - - var options = filter switch { - StreamFilter _ => new CreateReq.Types.AllOptions.Types.FilterOptions { - StreamIdentifier = (filter.Prefixes, filter.Regex) switch { - (PrefixFilterExpression[] _, RegularFilterExpression _) - when (filter.Prefixes?.Length ?? 0) == 0 && - filter.Regex != RegularFilterExpression.None => - new CreateReq.Types.AllOptions.Types.FilterOptions.Types.Expression - {Regex = filter.Regex}, - (PrefixFilterExpression[] _, RegularFilterExpression _) - when (filter.Prefixes?.Length ?? 0) != 0 && - filter.Regex == RegularFilterExpression.None => - new CreateReq.Types.AllOptions.Types.FilterOptions.Types.Expression { - Prefix = {Array.ConvertAll(filter.Prefixes!, e => e.ToString())} - }, - _ => throw new InvalidOperationException() - } - }, - EventTypeFilter _ => new CreateReq.Types.AllOptions.Types.FilterOptions { - EventType = (filter.Prefixes, filter.Regex) switch { - (PrefixFilterExpression[] _, RegularFilterExpression _) - when (filter.Prefixes?.Length ?? 0) == 0 && - filter.Regex != RegularFilterExpression.None => - new CreateReq.Types.AllOptions.Types.FilterOptions.Types.Expression - {Regex = filter.Regex}, - (PrefixFilterExpression[] _, RegularFilterExpression _) - when (filter.Prefixes?.Length ?? 0) != 0 && - filter.Regex == RegularFilterExpression.None => - new CreateReq.Types.AllOptions.Types.FilterOptions.Types.Expression { - Prefix = {Array.ConvertAll(filter.Prefixes!, e => e.ToString())} - }, - _ => throw new InvalidOperationException() - } - }, - _ => throw new InvalidOperationException() - }; - - if (filter.MaxSearchWindow.HasValue) { - options.Max = filter.MaxSearchWindow.Value; - } else { - options.Count = new Empty(); - } - - return options; - } - - - /// - /// Creates a persistent subscription. - /// - /// - public async Task CreateToStreamAsync(string streamName, string groupName, PersistentSubscriptionSettings settings, - TimeSpan? deadline = null, UserCredentials? userCredentials = null, - CancellationToken cancellationToken = default) => - await CreateInternalAsync(streamName, groupName, null, settings, deadline, userCredentials, - cancellationToken) - .ConfigureAwait(false); - - /// - /// Creates a persistent subscription. - /// - /// - [Obsolete("CreateAsync is no longer supported. Use CreateToStreamAsync instead.", false)] - public async Task CreateAsync(string streamName, string groupName, PersistentSubscriptionSettings settings, - TimeSpan? deadline = null, UserCredentials? userCredentials = null, - CancellationToken cancellationToken = default) => - await CreateInternalAsync(streamName, groupName, null, settings, deadline, userCredentials, - cancellationToken) - .ConfigureAwait(false); - - /// - /// Creates a filtered persistent subscription to $all. - /// - public async Task CreateToAllAsync(string groupName, IEventFilter eventFilter, - PersistentSubscriptionSettings settings, TimeSpan? deadline = null, UserCredentials? userCredentials = null, - CancellationToken cancellationToken = default) => - await CreateInternalAsync(SystemStreams.AllStream, groupName, eventFilter, settings, deadline, - userCredentials, cancellationToken) - .ConfigureAwait(false); - - /// - /// Creates a persistent subscription to $all. - /// - public async Task CreateToAllAsync(string groupName, PersistentSubscriptionSettings settings, - TimeSpan? deadline = null, UserCredentials? userCredentials = null, - CancellationToken cancellationToken = default) => - await CreateInternalAsync(SystemStreams.AllStream, groupName, null, settings, deadline, userCredentials, - cancellationToken) - .ConfigureAwait(false); - - private async Task CreateInternalAsync(string streamName, string groupName, IEventFilter? eventFilter, - PersistentSubscriptionSettings settings, TimeSpan? deadline, UserCredentials? userCredentials, - CancellationToken cancellationToken) { - if (streamName is null) { - throw new ArgumentNullException(nameof(streamName)); - } - - if (groupName is null) { - throw new ArgumentNullException(nameof(groupName)); - } - - if (settings is null) { - throw new ArgumentNullException(nameof(settings)); - } - - if (settings.ConsumerStrategyName is null) { - throw new ArgumentNullException(nameof(settings.ConsumerStrategyName)); - } - - if (streamName != SystemStreams.AllStream && settings.StartFrom != null && - settings.StartFrom is not StreamPosition) { - throw new ArgumentException( - $"{nameof(settings.StartFrom)} must be of type '{nameof(StreamPosition)}' when subscribing to a stream"); - } - - if (streamName == SystemStreams.AllStream && settings.StartFrom != null && - settings.StartFrom is not Position) { - throw new ArgumentException( - $"{nameof(settings.StartFrom)} must be of type '{nameof(Position)}' when subscribing to {SystemStreams.AllStream}"); - } - - if (eventFilter != null && streamName != SystemStreams.AllStream) { - throw new ArgumentException( - $"Filters are only supported when subscribing to {SystemStreams.AllStream}"); - } - - if (!NamedConsumerStrategyToCreateProto.ContainsKey(settings.ConsumerStrategyName)) { - throw new ArgumentException( - "The specified consumer strategy is not supported, specify one of the SystemConsumerStrategies"); - } - - var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); - - if (streamName == SystemStreams.AllStream && - !channelInfo.ServerCapabilities.SupportsPersistentSubscriptionsToAll) { - throw new InvalidOperationException("The server does not support persistent subscriptions to $all."); - } - - using var call = new PersistentSubscriptions.PersistentSubscriptions.PersistentSubscriptionsClient( - channelInfo.CallInvoker).CreateAsync(new CreateReq { - Options = new CreateReq.Types.Options { - Stream = streamName != SystemStreams.AllStream - ? StreamOptionsForCreateProto(streamName, - (StreamPosition)(settings.StartFrom ?? StreamPosition.End)) - : null, - All = streamName == SystemStreams.AllStream - ? AllOptionsForCreateProto((Position)(settings.StartFrom ?? Position.End), eventFilter) - : null, -#pragma warning disable 612 - StreamIdentifier = - streamName != SystemStreams.AllStream - ? streamName - : string.Empty, /*for backwards compatibility*/ -#pragma warning restore 612 - GroupName = groupName, - Settings = new CreateReq.Types.Settings { -#pragma warning disable 612 - Revision = streamName != SystemStreams.AllStream - ? ((StreamPosition)(settings.StartFrom ?? StreamPosition.End)).ToUInt64() - : default, /*for backwards compatibility*/ -#pragma warning restore 612 - CheckpointAfterMs = (int)settings.CheckPointAfter.TotalMilliseconds, - ExtraStatistics = settings.ExtraStatistics, - MessageTimeoutMs = (int)settings.MessageTimeout.TotalMilliseconds, - ResolveLinks = settings.ResolveLinkTos, - HistoryBufferSize = settings.HistoryBufferSize, - LiveBufferSize = settings.LiveBufferSize, - MaxCheckpointCount = settings.CheckPointUpperBound, - MaxRetryCount = settings.MaxRetryCount, - MaxSubscriberCount = settings.MaxSubscriberCount, - MinCheckpointCount = settings.CheckPointLowerBound, -#pragma warning disable 612 - /*for backwards compatibility*/ - NamedConsumerStrategy = NamedConsumerStrategyToCreateProto[settings.ConsumerStrategyName], -#pragma warning restore 612 - ReadBatchSize = settings.ReadBatchSize - } - } - }, EventStoreCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); - await call.ResponseAsync.ConfigureAwait(false); - } - } -} diff --git a/src/EventStore.Client/PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Delete.cs b/src/EventStore.Client/PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Delete.cs deleted file mode 100644 index 40ef522ba..000000000 --- a/src/EventStore.Client/PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Delete.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using EventStore.Client.PersistentSubscriptions; - -namespace EventStore.Client { - partial class EventStorePersistentSubscriptionsClient { - /// - /// Deletes a persistent subscription. - /// - [Obsolete("DeleteAsync is no longer supported. Use DeleteToStreamAsync instead.", false)] - public Task DeleteAsync(string streamName, string groupName, TimeSpan? deadline = null, - UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) => - DeleteToStreamAsync(streamName, groupName, deadline, userCredentials, cancellationToken); - - /// - /// Deletes a persistent subscription. - /// - public async Task DeleteToStreamAsync(string streamName, string groupName, TimeSpan? deadline = null, - UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) { - var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); - - if (streamName == SystemStreams.AllStream && - !channelInfo.ServerCapabilities.SupportsPersistentSubscriptionsToAll) { - throw new NotSupportedException("The server does not support persistent subscriptions to $all."); - } - - var deleteOptions = new DeleteReq.Types.Options { - GroupName = groupName - }; - - if (streamName == SystemStreams.AllStream) { - deleteOptions.All = new Empty(); - } else { - deleteOptions.StreamIdentifier = streamName; - } - - using var call = - new PersistentSubscriptions.PersistentSubscriptions.PersistentSubscriptionsClient( - channelInfo.CallInvoker) - .DeleteAsync(new DeleteReq {Options = deleteOptions}, - EventStoreCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, - cancellationToken)); - await call.ResponseAsync.ConfigureAwait(false); - } - - /// - /// Deletes a persistent subscription to $all. - /// - public async Task DeleteToAllAsync(string groupName, TimeSpan? deadline = null, - UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) => - await DeleteToStreamAsync(SystemStreams.AllStream, groupName, deadline, userCredentials, cancellationToken) - .ConfigureAwait(false); - } -} diff --git a/src/EventStore.Client/PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Info.cs b/src/EventStore.Client/PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Info.cs deleted file mode 100644 index b1cc4bebf..000000000 --- a/src/EventStore.Client/PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Info.cs +++ /dev/null @@ -1,77 +0,0 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using EventStore.Client.PersistentSubscriptions; -using Grpc.Core; - -#nullable enable -namespace EventStore.Client { - partial class EventStorePersistentSubscriptionsClient { - /// - /// Gets the status of a persistent subscription to $all - /// - public async Task GetInfoToAllAsync(string groupName, TimeSpan? deadline = null, - UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) { - - var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); - if (channelInfo.ServerCapabilities.SupportsPersistentSubscriptionsGetInfo) { - var req = new GetInfoReq() { - Options = new GetInfoReq.Types.Options{ - GroupName = groupName, - All = new Empty() - } - }; - - return await GetInfoGrpcAsync(req, deadline, userCredentials, channelInfo.CallInvoker, cancellationToken) - .ConfigureAwait(false); - } - - throw new NotSupportedException("The server does not support getting persistent subscription details for $all"); - } - - /// - /// Gets the status of a persistent subscription to a stream - /// - public async Task GetInfoToStreamAsync(string streamName, string groupName, - TimeSpan? deadline = null, UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) { - - var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); - if (channelInfo.ServerCapabilities.SupportsPersistentSubscriptionsGetInfo) { - var req = new GetInfoReq() { - Options = new GetInfoReq.Types.Options { - GroupName = groupName, - StreamIdentifier = streamName - } - }; - - return await GetInfoGrpcAsync(req, deadline, userCredentials, channelInfo.CallInvoker, cancellationToken) - .ConfigureAwait(false); - } - - return await GetInfoHttpAsync(streamName, groupName, channelInfo, deadline, userCredentials, cancellationToken) - .ConfigureAwait(false); - } - - private async Task GetInfoGrpcAsync(GetInfoReq req, TimeSpan? deadline, - UserCredentials? userCredentials, CallInvoker callInvoker, CancellationToken cancellationToken) { - - var result = await new PersistentSubscriptions.PersistentSubscriptions.PersistentSubscriptionsClient(callInvoker) - .GetInfoAsync(req, EventStoreCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)) - .ConfigureAwait(false); - - return PersistentSubscriptionInfo.From(result.SubscriptionInfo); - } - - private async Task GetInfoHttpAsync(string streamName, string groupName, - ChannelInfo channelInfo, TimeSpan? deadline, UserCredentials? userCredentials, CancellationToken cancellationToken) { - - var path = $"/subscriptions/{UrlEncode(streamName)}/{UrlEncode(groupName)}/info"; - var result = await HttpGet(path, - onNotFound: () => throw new PersistentSubscriptionNotFoundException(streamName, groupName), - channelInfo, deadline, userCredentials, cancellationToken) - .ConfigureAwait(false); - - return PersistentSubscriptionInfo.From(result); - } - } -} diff --git a/src/EventStore.Client/PersistentSubscriptions/EventStorePersistentSubscriptionsClient.List.cs b/src/EventStore.Client/PersistentSubscriptions/EventStorePersistentSubscriptionsClient.List.cs deleted file mode 100644 index ce588e3f7..000000000 --- a/src/EventStore.Client/PersistentSubscriptions/EventStorePersistentSubscriptionsClient.List.cs +++ /dev/null @@ -1,111 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using EventStore.Client.PersistentSubscriptions; -using Grpc.Core; - -#nullable enable -namespace EventStore.Client { - partial class EventStorePersistentSubscriptionsClient { - /// - /// Lists persistent subscriptions to $all. - /// - public async Task> ListToAllAsync(TimeSpan? deadline = null, - UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) { - - var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); - if (channelInfo.ServerCapabilities.SupportsPersistentSubscriptionsList) { - var req = new ListReq() { - Options = new ListReq.Types.Options{ - ListForStream = new ListReq.Types.StreamOption() { - All = new Empty() - } - } - }; - - return await ListGrpcAsync(req, deadline, userCredentials, channelInfo.CallInvoker, cancellationToken) - .ConfigureAwait(false); - } - - throw new NotSupportedException("The server does not support listing the persistent subscriptions."); - } - - /// - /// Lists persistent subscriptions to the specified stream. - /// - public async Task> ListToStreamAsync(string streamName, TimeSpan? deadline = null, - UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) { - - var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); - if (channelInfo.ServerCapabilities.SupportsPersistentSubscriptionsList) { - var req = new ListReq() { - Options = new ListReq.Types.Options { - ListForStream = new ListReq.Types.StreamOption() { - Stream = streamName - } - } - }; - - return await ListGrpcAsync(req, deadline, userCredentials, channelInfo.CallInvoker, cancellationToken) - .ConfigureAwait(false); - } - - return await ListHttpAsync(streamName, channelInfo, deadline, userCredentials, cancellationToken) - .ConfigureAwait(false); - } - - /// - /// Lists all persistent subscriptions. - /// - public async Task> ListAllAsync(TimeSpan? deadline = null, - UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) { - - var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); - if (channelInfo.ServerCapabilities.SupportsPersistentSubscriptionsList) { - var req = new ListReq() { - Options = new ListReq.Types.Options { - ListAllSubscriptions = new Empty() - } - }; - - return await ListGrpcAsync(req, deadline, userCredentials, channelInfo.CallInvoker, cancellationToken) - .ConfigureAwait(false); - } - - try { - var result = await HttpGet>("/subscriptions", - onNotFound: () => throw new PersistentSubscriptionNotFoundException(string.Empty, string.Empty), - channelInfo, deadline, userCredentials, cancellationToken) - .ConfigureAwait(false); - - return result.Select(PersistentSubscriptionInfo.From); - } catch (AccessDeniedException ex) when (userCredentials != null) { // Required to get same gRPC behavior. - throw new NotAuthenticatedException(ex.Message, ex); - } - } - - private async Task> ListGrpcAsync(ListReq req, TimeSpan? deadline, - UserCredentials? userCredentials, CallInvoker callInvoker, CancellationToken cancellationToken) { - - using var call = new PersistentSubscriptions.PersistentSubscriptions.PersistentSubscriptionsClient(callInvoker) - .ListAsync(req, EventStoreCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); - - ListResp? response = await call.ResponseAsync.ConfigureAwait(false); - - return response.Subscriptions.Select(PersistentSubscriptionInfo.From); - } - - private async Task> ListHttpAsync(string streamName, - ChannelInfo channelInfo, TimeSpan? deadline, UserCredentials? userCredentials, CancellationToken cancellationToken) { - - var path = $"/subscriptions/{UrlEncode(streamName)}"; - var result = await HttpGet>(path, - onNotFound: () => throw new PersistentSubscriptionNotFoundException(streamName, string.Empty), - channelInfo, deadline, userCredentials, cancellationToken) - .ConfigureAwait(false); - return result.Select(PersistentSubscriptionInfo.From); - } - } -} diff --git a/src/EventStore.Client/PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Read.cs b/src/EventStore.Client/PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Read.cs deleted file mode 100644 index 211ffdbeb..000000000 --- a/src/EventStore.Client/PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Read.cs +++ /dev/null @@ -1,478 +0,0 @@ -using System.Threading.Channels; -using EventStore.Client.Diagnostics; -using EventStore.Client.PersistentSubscriptions; -using Grpc.Core; - -using static EventStore.Client.PersistentSubscriptions.PersistentSubscriptions; -using static EventStore.Client.PersistentSubscriptions.ReadResp.ContentOneofCase; - -namespace EventStore.Client { - partial class EventStorePersistentSubscriptionsClient { - /// - /// Subscribes to a persistent subscription. - /// - /// - /// - /// - [Obsolete("SubscribeAsync is no longer supported. Use SubscribeToStream with manual acks instead.", false)] - public async Task SubscribeAsync( - string streamName, string groupName, - Func eventAppeared, - Action? subscriptionDropped = null, - UserCredentials? userCredentials = null, int bufferSize = 10, bool autoAck = true, - CancellationToken cancellationToken = default - ) { - if (autoAck) { - throw new InvalidOperationException( - $"AutoAck is no longer supported. Please use {nameof(SubscribeToStream)} with manual acks instead." - ); - } - - return await PersistentSubscription - .Confirm( - SubscribeToStream(streamName, groupName, bufferSize, userCredentials, cancellationToken), - eventAppeared, - subscriptionDropped ?? delegate { }, - _log, - userCredentials, - cancellationToken - ) - .ConfigureAwait(false); - } - - /// - /// Subscribes to a persistent subscription. Messages must be manually acknowledged - /// - /// - /// - /// - public async Task SubscribeToStreamAsync( - string streamName, string groupName, - Func eventAppeared, - Action? subscriptionDropped = null, - UserCredentials? userCredentials = null, int bufferSize = 10, - CancellationToken cancellationToken = default - ) { - return await PersistentSubscription - .Confirm( - SubscribeToStream(streamName, groupName, bufferSize, userCredentials, cancellationToken), - eventAppeared, - subscriptionDropped ?? delegate { }, - _log, - userCredentials, - cancellationToken - ) - .ConfigureAwait(false); - } - - /// - /// Subscribes to a persistent subscription. Messages must be manually acknowledged. - /// - /// The name of the stream to read events from. - /// The name of the persistent subscription group. - /// The size of the buffer. - /// The optional user credentials to perform operation with. - /// The optional . - /// - public PersistentSubscriptionResult SubscribeToStream( - string streamName, string groupName, int bufferSize = 10, - UserCredentials? userCredentials = null, CancellationToken cancellationToken = default - ) { - if (streamName == null) { - throw new ArgumentNullException(nameof(streamName)); - } - - if (groupName == null) { - throw new ArgumentNullException(nameof(groupName)); - } - - if (streamName == string.Empty) { - throw new ArgumentException($"{nameof(streamName)} may not be empty.", nameof(streamName)); - } - - if (groupName == string.Empty) { - throw new ArgumentException($"{nameof(groupName)} may not be empty.", nameof(groupName)); - } - - if (bufferSize <= 0) { - throw new ArgumentOutOfRangeException(nameof(bufferSize)); - } - - var readOptions = new ReadReq.Types.Options { - BufferSize = bufferSize, - GroupName = groupName, - UuidOption = new ReadReq.Types.Options.Types.UUIDOption { Structured = new Empty() } - }; - - if (streamName == SystemStreams.AllStream) { - readOptions.All = new Empty(); - } else { - readOptions.StreamIdentifier = streamName; - } - - return new PersistentSubscriptionResult( - streamName, - groupName, - async ct => { - var channelInfo = await GetChannelInfo(ct).ConfigureAwait(false); - - if (streamName == SystemStreams.AllStream && - !channelInfo.ServerCapabilities.SupportsPersistentSubscriptionsToAll) { - throw new NotSupportedException( - "The server does not support persistent subscriptions to $all." - ); - } - - return channelInfo; - }, - new() { Options = readOptions }, - Settings, - userCredentials, - cancellationToken - ); - } - - /// - /// Subscribes to a persistent subscription to $all. Messages must be manually acknowledged - /// - public async Task SubscribeToAllAsync( - string groupName, - Func eventAppeared, - Action? subscriptionDropped = null, - UserCredentials? userCredentials = null, int bufferSize = 10, - CancellationToken cancellationToken = default - ) => - await SubscribeToStreamAsync( - SystemStreams.AllStream, - groupName, - eventAppeared, - subscriptionDropped, - userCredentials, - bufferSize, - cancellationToken - ) - .ConfigureAwait(false); - - /// - /// Subscribes to a persistent subscription to $all. Messages must be manually acknowledged. - /// - /// The name of the persistent subscription group. - /// The size of the buffer. - /// The optional user credentials to perform operation with. - /// The optional . - /// - public PersistentSubscriptionResult SubscribeToAll( - string groupName, int bufferSize = 10, - UserCredentials? userCredentials = null, CancellationToken cancellationToken = default - ) => - SubscribeToStream(SystemStreams.AllStream, groupName, bufferSize, userCredentials, cancellationToken); - - /// - public class PersistentSubscriptionResult : IAsyncEnumerable, IAsyncDisposable, IDisposable { - const int MaxEventIdLength = 2000; - - readonly ReadReq _request; - readonly Channel _channel; - readonly CancellationTokenSource _cts; - readonly CallOptions _callOptions; - - AsyncDuplexStreamingCall? _call; - int _messagesEnumerated; - - /// - /// The server-generated unique identifier for the subscription. - /// - public string? SubscriptionId { get; private set; } - - /// - /// The name of the stream to read events from. - /// - public string StreamName { get; } - - /// - /// The name of the persistent subscription group. - /// - public string GroupName { get; } - - /// - /// An . Do not enumerate more than once. - /// - public IAsyncEnumerable Messages { - get { - if (Interlocked.Exchange(ref _messagesEnumerated, 1) == 1) - throw new InvalidOperationException("Messages may only be enumerated once."); - - return GetMessages(); - - async IAsyncEnumerable GetMessages() { - try { - await foreach (var message in _channel.Reader.ReadAllAsync(_cts.Token)) { - if (message is PersistentSubscriptionMessage.SubscriptionConfirmation(var subscriptionId)) - SubscriptionId = subscriptionId; - - yield return message; - } - } - finally { - _cts.Cancel(); - } - } - } - } - - internal PersistentSubscriptionResult( - string streamName, string groupName, - Func> selectChannelInfo, - ReadReq request, EventStoreClientSettings settings, UserCredentials? userCredentials, - CancellationToken cancellationToken - ) { - StreamName = streamName; - GroupName = groupName; - - _request = request; - - _callOptions = EventStoreCallOptions.CreateStreaming( - settings, - userCredentials: userCredentials, - cancellationToken: cancellationToken - ); - - _channel = Channel.CreateBounded(ReadBoundedChannelOptions); - - _cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); - - _ = PumpMessages(); - - return; - - async Task PumpMessages() { - try { - var channelInfo = await selectChannelInfo(_cts.Token).ConfigureAwait(false); - var client = new PersistentSubscriptionsClient(channelInfo.CallInvoker); - - _call = client.Read(_callOptions); - - await _call.RequestStream.WriteAsync(_request).ConfigureAwait(false); - - await foreach (var response in _call.ResponseStream.ReadAllAsync(_cts.Token).ConfigureAwait(false)) { - PersistentSubscriptionMessage subscriptionMessage = response.ContentCase switch { - SubscriptionConfirmation => new PersistentSubscriptionMessage.SubscriptionConfirmation( - response.SubscriptionConfirmation.SubscriptionId - ), - Event => new PersistentSubscriptionMessage.Event( - ConvertToResolvedEvent(response), - response.Event.CountCase switch { - ReadResp.Types.ReadEvent.CountOneofCase.RetryCount => response.Event.RetryCount, - _ => null - } - ), - _ => PersistentSubscriptionMessage.Unknown.Instance - }; - - if (subscriptionMessage is PersistentSubscriptionMessage.Event evnt) - EventStoreClientDiagnostics.ActivitySource.TraceSubscriptionEvent( - SubscriptionId, - evnt.ResolvedEvent, - channelInfo, - settings, - userCredentials - ); - - await _channel.Writer.WriteAsync(subscriptionMessage, _cts.Token).ConfigureAwait(false); - } - - _channel.Writer.TryComplete(); - } catch (Exception ex) { -#if NET48 - switch (ex) { - // The gRPC client for .NET 48 uses WinHttpHandler under the hood for sending HTTP requests. - // In certain scenarios, this can lead to exceptions of type WinHttpException being thrown. - // One such scenario is when the server abruptly closes the connection, which results in a WinHttpException with the error code 12030. - // Additionally, there are cases where the server response does not include the 'grpc-status' header. - // The absence of this header leads to an RpcException with the status code 'Cancelled' and the message "No grpc-status found on response". - // The switch statement below handles these specific exceptions and translates them into the appropriate - // PersistentSubscriptionDroppedByServerException exception. - case RpcException { StatusCode: StatusCode.Unavailable } rex1 when rex1.Status.Detail.Contains("WinHttpException: Error 12030"): - case RpcException { StatusCode: StatusCode.Cancelled } rex2 - when rex2.Status.Detail.Contains("No grpc-status found on response"): - ex = new PersistentSubscriptionDroppedByServerException(StreamName, GroupName, ex); - break; - } -#endif - if (ex is PersistentSubscriptionNotFoundException) { - await _channel.Writer - .WriteAsync(PersistentSubscriptionMessage.NotFound.Instance, cancellationToken) - .ConfigureAwait(false); - - _channel.Writer.TryComplete(); - return; - } - - _channel.Writer.TryComplete(ex); - } - } - } - - /// - /// Acknowledge that a message has completed processing (this will tell the server it has been processed). - /// - /// There is no need to ack a message if you have Auto Ack enabled. - /// The of the s to acknowledge. There should not be more than 2000 to ack at a time. - public Task Ack(params Uuid[] eventIds) => AckInternal(eventIds); - - /// - /// Acknowledge that a message has completed processing (this will tell the server it has been processed). - /// - /// There is no need to ack a message if you have Auto Ack enabled. - /// The of the s to acknowledge. There should not be more than 2000 to ack at a time. - public Task Ack(IEnumerable eventIds) => Ack(eventIds.ToArray()); - - /// - /// Acknowledge that a message has completed processing (this will tell the server it has been processed). - /// - /// There is no need to ack a message if you have Auto Ack enabled. - /// The s to acknowledge. There should not be more than 2000 to ack at a time. - public Task Ack(params ResolvedEvent[] resolvedEvents) => - Ack(Array.ConvertAll(resolvedEvents, resolvedEvent => resolvedEvent.OriginalEvent.EventId)); - - /// - /// Acknowledge that a message has completed processing (this will tell the server it has been processed). - /// - /// There is no need to ack a message if you have Auto Ack enabled. - /// The s to acknowledge. There should not be more than 2000 to ack at a time. - public Task Ack(IEnumerable resolvedEvents) => - Ack(resolvedEvents.Select(resolvedEvent => resolvedEvent.OriginalEvent.EventId)); - - /// - /// Acknowledge that a message has failed processing (this will tell the server it has not been processed). - /// - /// The to take. - /// A reason given. - /// The of the s to nak. There should not be more than 2000 to nak at a time. - /// The number of eventIds exceeded the limit of 2000. - public Task Nack(PersistentSubscriptionNakEventAction action, string reason, params Uuid[] eventIds) => - NackInternal(eventIds, action, reason); - - /// - /// Acknowledge that a message has failed processing (this will tell the server it has not been processed). - /// - /// The to take. - /// A reason given. - /// The s to nak. There should not be more than 2000 to nak at a time. - /// The number of resolvedEvents exceeded the limit of 2000. - public Task Nack(PersistentSubscriptionNakEventAction action, string reason, params ResolvedEvent[] resolvedEvents) => - Nack(action, reason, Array.ConvertAll(resolvedEvents, re => re.OriginalEvent.EventId)); - - static ResolvedEvent ConvertToResolvedEvent(ReadResp response) => new( - ConvertToEventRecord(response.Event.Event)!, - ConvertToEventRecord(response.Event.Link), - response.Event.PositionCase switch { - ReadResp.Types.ReadEvent.PositionOneofCase.CommitPosition => response.Event.CommitPosition, - _ => null - } - ); - - Task AckInternal(params Uuid[] eventIds) { - if (eventIds.Length > MaxEventIdLength) { - throw new ArgumentException( - $"The number of eventIds exceeds the maximum length of {MaxEventIdLength}.", - nameof(eventIds) - ); - } - - return _call is null - ? throw new InvalidOperationException() - : _call.RequestStream.WriteAsync( - new ReadReq { - Ack = new ReadReq.Types.Ack { - Ids = { - Array.ConvertAll(eventIds, id => id.ToDto()) - } - } - } - ); - } - - Task NackInternal(Uuid[] eventIds, PersistentSubscriptionNakEventAction action, string reason) { - if (eventIds.Length > MaxEventIdLength) { - throw new ArgumentException( - $"The number of eventIds exceeds the maximum length of {MaxEventIdLength}.", - nameof(eventIds) - ); - } - - return _call is null - ? throw new InvalidOperationException() - : _call.RequestStream.WriteAsync( - new ReadReq { - Nack = new ReadReq.Types.Nack { - Ids = { - Array.ConvertAll(eventIds, id => id.ToDto()) - }, - Action = action switch { - PersistentSubscriptionNakEventAction.Park => ReadReq.Types.Nack.Types.Action.Park, - PersistentSubscriptionNakEventAction.Retry => ReadReq.Types.Nack.Types.Action.Retry, - PersistentSubscriptionNakEventAction.Skip => ReadReq.Types.Nack.Types.Action.Skip, - PersistentSubscriptionNakEventAction.Stop => ReadReq.Types.Nack.Types.Action.Stop, - _ => ReadReq.Types.Nack.Types.Action.Unknown - }, - Reason = reason - } - } - ); - } - - static EventRecord? ConvertToEventRecord(ReadResp.Types.ReadEvent.Types.RecordedEvent? e) => - e is null - ? null - : new EventRecord( - e.StreamIdentifier!, - Uuid.FromDto(e.Id), - new StreamPosition(e.StreamRevision), - new Position(e.CommitPosition, e.PreparePosition), - e.Metadata, - e.Data.ToByteArray(), - e.CustomMetadata.ToByteArray() - ); - - /// - public async ValueTask DisposeAsync() { - await CastAndDispose(_cts).ConfigureAwait(false); - await CastAndDispose(_call).ConfigureAwait(false); - - return; - - static async Task CastAndDispose(IDisposable? resource) { - switch (resource) { - case null: - return; - - case IAsyncDisposable resourceAsyncDisposable: - await resourceAsyncDisposable.DisposeAsync().ConfigureAwait(false); - break; - - default: - resource.Dispose(); - break; - } - } - } - - /// - public void Dispose() { - _cts.Dispose(); - _call?.Dispose(); - } - - /// - public async IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = default) { - await foreach (var message in Messages.WithCancellation(cancellationToken)) { - if (message is not PersistentSubscriptionMessage.Event(var resolvedEvent, _)) - continue; - - yield return resolvedEvent; - } - } - } - } -} diff --git a/src/EventStore.Client/PersistentSubscriptions/EventStorePersistentSubscriptionsClient.ReplayParked.cs b/src/EventStore.Client/PersistentSubscriptions/EventStorePersistentSubscriptionsClient.ReplayParked.cs deleted file mode 100644 index 248ed9143..000000000 --- a/src/EventStore.Client/PersistentSubscriptions/EventStorePersistentSubscriptionsClient.ReplayParked.cs +++ /dev/null @@ -1,94 +0,0 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using EventStore.Client.PersistentSubscriptions; -using Grpc.Core; -using NotSupportedException = System.NotSupportedException; - -#nullable enable -namespace EventStore.Client { - partial class EventStorePersistentSubscriptionsClient { - /// - /// Retry the parked messages of the persistent subscription - /// - public async Task ReplayParkedMessagesToAllAsync(string groupName, long? stopAt = null, TimeSpan? deadline = null, - UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) { - - var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); - if (channelInfo.ServerCapabilities.SupportsPersistentSubscriptionsReplayParked) { - var req = new ReplayParkedReq() { - Options = new ReplayParkedReq.Types.Options{ - GroupName = groupName, - All = new Empty() - }, - }; - - await ReplayParkedGrpcAsync(req, stopAt, deadline, userCredentials, channelInfo.CallInvoker, cancellationToken) - .ConfigureAwait(false); - - return; - } - - if (channelInfo.ServerCapabilities.SupportsPersistentSubscriptionsToAll) { - await ReplayParkedHttpAsync(SystemStreams.AllStream, groupName, stopAt, channelInfo, - deadline, userCredentials, cancellationToken) - .ConfigureAwait(false); - - return; - } - - throw new NotSupportedException("The server does not support persistent subscriptions to $all."); - } - - /// - /// Retry the parked messages of the persistent subscription - /// - public async Task ReplayParkedMessagesToStreamAsync(string streamName, string groupName, long? stopAt=null, - TimeSpan? deadline=null, UserCredentials? userCredentials=null, CancellationToken cancellationToken=default) { - - var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); - if (channelInfo.ServerCapabilities.SupportsPersistentSubscriptionsReplayParked) { - var req = new ReplayParkedReq() { - Options = new ReplayParkedReq.Types.Options { - GroupName = groupName, - StreamIdentifier = streamName - }, - }; - - await ReplayParkedGrpcAsync(req, stopAt, deadline, userCredentials, channelInfo.CallInvoker, cancellationToken) - .ConfigureAwait(false); - - return; - } - - await ReplayParkedHttpAsync(streamName, groupName, stopAt, channelInfo, deadline, userCredentials, cancellationToken) - .ConfigureAwait(false); - } - - private async Task ReplayParkedGrpcAsync(ReplayParkedReq req, long? numberOfEvents, TimeSpan? deadline, - UserCredentials? userCredentials, CallInvoker callInvoker, CancellationToken cancellationToken) { - - if (numberOfEvents.HasValue) { - req.Options.StopAt = numberOfEvents.Value; - } else { - req.Options.NoLimit = new Empty(); - } - - await new PersistentSubscriptions.PersistentSubscriptions.PersistentSubscriptionsClient(callInvoker) - .ReplayParkedAsync(req, EventStoreCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)) - .ConfigureAwait(false); - } - - private async Task ReplayParkedHttpAsync(string streamName, string groupName, long? numberOfEvents, - ChannelInfo channelInfo, TimeSpan? deadline, UserCredentials? userCredentials, CancellationToken cancellationToken) { - - var path = $"/subscriptions/{UrlEncode(streamName)}/{UrlEncode(groupName)}/replayParked"; - var query = numberOfEvents.HasValue ? $"stopAt={numberOfEvents.Value}":""; - - await HttpPost(path, query, - onNotFound: () => throw new PersistentSubscriptionNotFoundException(streamName, groupName), - channelInfo, deadline, userCredentials, cancellationToken) - .ConfigureAwait(false); - } - } -} diff --git a/src/EventStore.Client/PersistentSubscriptions/EventStorePersistentSubscriptionsClient.RestartSubsystem.cs b/src/EventStore.Client/PersistentSubscriptions/EventStorePersistentSubscriptionsClient.RestartSubsystem.cs deleted file mode 100644 index 4d01967d8..000000000 --- a/src/EventStore.Client/PersistentSubscriptions/EventStorePersistentSubscriptionsClient.RestartSubsystem.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System; -using System.Threading; -using System.Threading.Tasks; - -#nullable enable -namespace EventStore.Client { - partial class EventStorePersistentSubscriptionsClient { - /// - /// Restarts the persistent subscriptions subsystem. - /// - public async Task RestartSubsystemAsync(TimeSpan? deadline = null, UserCredentials? userCredentials = null, - CancellationToken cancellationToken = default) { - - var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); - if (channelInfo.ServerCapabilities.SupportsPersistentSubscriptionsRestartSubsystem) { - await new PersistentSubscriptions.PersistentSubscriptions.PersistentSubscriptionsClient(channelInfo.CallInvoker) - .RestartSubsystemAsync(new Empty(), EventStoreCallOptions - .CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)) - .ConfigureAwait(false); - return; - } - - await HttpPost( - path: "/subscriptions/restart", - query: "", - onNotFound: () => - throw new Exception("Unexpected exception while restarting the persistent subscription subsystem."), - channelInfo, deadline, userCredentials, cancellationToken) - .ConfigureAwait(false); - } - } -} diff --git a/src/EventStore.Client/PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Update.cs b/src/EventStore.Client/PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Update.cs deleted file mode 100644 index 1eae92a25..000000000 --- a/src/EventStore.Client/PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Update.cs +++ /dev/null @@ -1,165 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using EventStore.Client.PersistentSubscriptions; - -namespace EventStore.Client { - public partial class EventStorePersistentSubscriptionsClient { - private static readonly IDictionary NamedConsumerStrategyToUpdateProto - = new Dictionary { - [SystemConsumerStrategies.DispatchToSingle] = UpdateReq.Types.ConsumerStrategy.DispatchToSingle, - [SystemConsumerStrategies.RoundRobin] = UpdateReq.Types.ConsumerStrategy.RoundRobin, - [SystemConsumerStrategies.Pinned] = UpdateReq.Types.ConsumerStrategy.Pinned, - }; - - private static UpdateReq.Types.StreamOptions StreamOptionsForUpdateProto(string streamName, - StreamPosition position) { - if (position == StreamPosition.Start) { - return new UpdateReq.Types.StreamOptions { - StreamIdentifier = streamName, - Start = new Empty() - }; - } - - if (position == StreamPosition.End) { - return new UpdateReq.Types.StreamOptions { - StreamIdentifier = streamName, - End = new Empty() - }; - } - - return new UpdateReq.Types.StreamOptions { - StreamIdentifier = streamName, - Revision = position.ToUInt64() - }; - } - - private static UpdateReq.Types.AllOptions AllOptionsForUpdateProto(Position position) { - if (position == Position.Start) { - return new UpdateReq.Types.AllOptions { - Start = new Empty() - }; - } - - if (position == Position.End) { - return new UpdateReq.Types.AllOptions { - End = new Empty() - }; - } - - return new UpdateReq.Types.AllOptions { - Position = new UpdateReq.Types.Position { - CommitPosition = position.CommitPosition, - PreparePosition = position.PreparePosition - } - }; - } - - - /// - /// Updates a persistent subscription. - /// - /// - [Obsolete("UpdateAsync is no longer supported. Use UpdateToStreamAsync instead.", false)] - public Task UpdateAsync(string streamName, string groupName, PersistentSubscriptionSettings settings, - TimeSpan? deadline = null, UserCredentials? userCredentials = null, - CancellationToken cancellationToken = default) => - UpdateToStreamAsync(streamName, groupName, settings, deadline, userCredentials, cancellationToken); - - /// - /// Updates a persistent subscription. - /// - /// - public async Task UpdateToStreamAsync(string streamName, string groupName, PersistentSubscriptionSettings settings, - TimeSpan? deadline = null, UserCredentials? userCredentials = null, - CancellationToken cancellationToken = default) { - if (streamName is null) { - throw new ArgumentNullException(nameof(streamName)); - } - - if (groupName is null) { - throw new ArgumentNullException(nameof(groupName)); - } - - if (settings is null) { - throw new ArgumentNullException(nameof(settings)); - } - - if (settings.ConsumerStrategyName is null) { - throw new ArgumentNullException(nameof(settings.ConsumerStrategyName)); - } - - if (streamName != SystemStreams.AllStream && settings.StartFrom is not null && - settings.StartFrom is not StreamPosition) { - throw new ArgumentException( - $"{nameof(settings.StartFrom)} must be of type '{nameof(StreamPosition)}' when subscribing to a stream"); - } - - if (streamName == SystemStreams.AllStream && settings.StartFrom is not null && - settings.StartFrom is not Position) { - throw new ArgumentException( - $"{nameof(settings.StartFrom)} must be of type '{nameof(Position)}' when subscribing to {SystemStreams.AllStream}"); - } - - var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); - - if (streamName == SystemStreams.AllStream && - !channelInfo.ServerCapabilities.SupportsPersistentSubscriptionsToAll) { - throw new InvalidOperationException("The server does not support persistent subscriptions to $all."); - } - - using var call = new PersistentSubscriptions.PersistentSubscriptions.PersistentSubscriptionsClient(channelInfo.CallInvoker) - .UpdateAsync(new UpdateReq { - Options = new UpdateReq.Types.Options { - GroupName = groupName, - Stream = streamName != SystemStreams.AllStream - ? StreamOptionsForUpdateProto(streamName, - (StreamPosition)(settings.StartFrom ?? StreamPosition.End)) - : null, - All = streamName == SystemStreams.AllStream - ? AllOptionsForUpdateProto((Position)(settings.StartFrom ?? Position.End)) - : null, -#pragma warning disable 612 - StreamIdentifier = - streamName != SystemStreams.AllStream - ? streamName - : string.Empty, /*for backwards compatibility*/ -#pragma warning restore 612 - Settings = new UpdateReq.Types.Settings { -#pragma warning disable 612 - Revision = streamName != SystemStreams.AllStream - ? ((StreamPosition)(settings.StartFrom ?? StreamPosition.End)).ToUInt64() - : default, /*for backwards compatibility*/ -#pragma warning restore 612 - CheckpointAfterMs = (int)settings.CheckPointAfter.TotalMilliseconds, - ExtraStatistics = settings.ExtraStatistics, - MessageTimeoutMs = (int)settings.MessageTimeout.TotalMilliseconds, - ResolveLinks = settings.ResolveLinkTos, - HistoryBufferSize = settings.HistoryBufferSize, - LiveBufferSize = settings.LiveBufferSize, - MaxCheckpointCount = settings.CheckPointUpperBound, - MaxRetryCount = settings.MaxRetryCount, - MaxSubscriberCount = settings.MaxSubscriberCount, - MinCheckpointCount = settings.CheckPointLowerBound, - NamedConsumerStrategy = - NamedConsumerStrategyToUpdateProto[settings.ConsumerStrategyName], - ReadBatchSize = settings.ReadBatchSize - } - } - }, - EventStoreCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); - await call.ResponseAsync.ConfigureAwait(false); - } - - /// - /// Updates a persistent subscription to $all. - /// - public async Task UpdateToAllAsync(string groupName, PersistentSubscriptionSettings settings, - TimeSpan? deadline = null, UserCredentials? userCredentials = null, - CancellationToken cancellationToken = default) => - await UpdateToStreamAsync(SystemStreams.AllStream, groupName, settings, deadline, userCredentials, - cancellationToken) - .ConfigureAwait(false); - } -} diff --git a/src/EventStore.Client/PersistentSubscriptions/EventStorePersistentSubscriptionsClient.cs b/src/EventStore.Client/PersistentSubscriptions/EventStorePersistentSubscriptionsClient.cs deleted file mode 100644 index 411de3e70..000000000 --- a/src/EventStore.Client/PersistentSubscriptions/EventStorePersistentSubscriptionsClient.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System.Text.Encodings.Web; -using System.Threading.Channels; -using Grpc.Core; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; - -namespace EventStore.Client { - /// - /// The client used to manage persistent subscriptions in the EventStoreDB. - /// - public sealed partial class EventStorePersistentSubscriptionsClient : EventStoreClientBase { - private static BoundedChannelOptions ReadBoundedChannelOptions = new (1) { - SingleReader = true, - SingleWriter = true, - AllowSynchronousContinuations = true - }; - - private readonly ILogger _log; - - /// - /// Constructs a new . - /// - public EventStorePersistentSubscriptionsClient(EventStoreClientSettings? settings) : base(settings, - new Dictionary> { - [Constants.Exceptions.PersistentSubscriptionDoesNotExist] = ex => new - PersistentSubscriptionNotFoundException( - ex.Trailers.First(x => x.Key == Constants.Exceptions.StreamName).Value, - ex.Trailers.FirstOrDefault(x => x.Key == Constants.Exceptions.GroupName)?.Value ?? "", ex), - [Constants.Exceptions.MaximumSubscribersReached] = ex => new - MaximumSubscribersReachedException( - ex.Trailers.First(x => x.Key == Constants.Exceptions.StreamName).Value, - ex.Trailers.First(x => x.Key == Constants.Exceptions.GroupName).Value, ex), - [Constants.Exceptions.PersistentSubscriptionDropped] = ex => new - PersistentSubscriptionDroppedByServerException( - ex.Trailers.First(x => x.Key == Constants.Exceptions.StreamName).Value, - ex.Trailers.First(x => x.Key == Constants.Exceptions.GroupName).Value, ex) - }) { - _log = Settings.LoggerFactory?.CreateLogger() - ?? new NullLogger(); - } - - private static string UrlEncode(string s) { - return UrlEncoder.Default.Encode(s); - } - } -} diff --git a/src/EventStore.Client/PersistentSubscriptions/EventStorePersistentSubscriptionsClientCollectionExtensions.cs b/src/EventStore.Client/PersistentSubscriptions/EventStorePersistentSubscriptionsClientCollectionExtensions.cs deleted file mode 100644 index f8d491dda..000000000 --- a/src/EventStore.Client/PersistentSubscriptions/EventStorePersistentSubscriptionsClientCollectionExtensions.cs +++ /dev/null @@ -1,61 +0,0 @@ -// ReSharper disable CheckNamespace - -using System; -using System.Net.Http; -using EventStore.Client; -using Grpc.Core.Interceptors; -using Microsoft.Extensions.DependencyInjection.Extensions; -using Microsoft.Extensions.Logging; - -namespace Microsoft.Extensions.DependencyInjection { - /// - /// A set of extension methods for which provide support for an . - /// - public static class EventStorePersistentSubscriptionsClientCollectionExtensions { - /// - /// Adds an to the . - /// - /// - public static IServiceCollection AddEventStorePersistentSubscriptionsClient(this IServiceCollection services, - Uri address, Func? createHttpMessageHandler = null) - => services.AddEventStorePersistentSubscriptionsClient(options => { - options.ConnectivitySettings.Address = address; - options.CreateHttpMessageHandler = createHttpMessageHandler; - }); - - /// - /// Adds an to the . - /// - /// - public static IServiceCollection AddEventStorePersistentSubscriptionsClient(this IServiceCollection services, - Action? configureSettings = null) => - services.AddEventStorePersistentSubscriptionsClient(new EventStoreClientSettings(), - configureSettings); - - /// - /// Adds an to the . - /// - /// - public static IServiceCollection AddEventStorePersistentSubscriptionsClient(this IServiceCollection services, - string connectionString, Action? configureSettings = null) => - services.AddEventStorePersistentSubscriptionsClient(EventStoreClientSettings.Create(connectionString), - configureSettings); - - private static IServiceCollection AddEventStorePersistentSubscriptionsClient(this IServiceCollection services, - EventStoreClientSettings settings, Action? configureSettings) { - if (services == null) { - throw new ArgumentNullException(nameof(services)); - } - - configureSettings?.Invoke(settings); - services.TryAddSingleton(provider => { - settings.LoggerFactory ??= provider.GetService(); - settings.Interceptors ??= provider.GetServices(); - - return new EventStorePersistentSubscriptionsClient(settings); - }); - return services; - } - } -} -// ReSharper restore CheckNamespace diff --git a/src/EventStore.Client/PersistentSubscriptions/MaximumSubscribersReachedException.cs b/src/EventStore.Client/PersistentSubscriptions/MaximumSubscribersReachedException.cs deleted file mode 100644 index 879378f85..000000000 --- a/src/EventStore.Client/PersistentSubscriptions/MaximumSubscribersReachedException.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System; - -namespace EventStore.Client { - /// - /// The exception that is thrown when the maximum number of subscribers on a persistent subscription is exceeded. - /// - public class MaximumSubscribersReachedException : Exception { - /// - /// The stream name. - /// - public readonly string StreamName; - /// - /// The group name. - /// - public readonly string GroupName; - - /// - /// Constructs a new . - /// - /// - public MaximumSubscribersReachedException(string streamName, string groupName, Exception? exception = null) - : base($"Maximum subscriptions reached for subscription group '{groupName}' on stream '{streamName}.'", - exception) { - if (streamName == null) throw new ArgumentNullException(nameof(streamName)); - if (groupName == null) throw new ArgumentNullException(nameof(groupName)); - StreamName = streamName; - GroupName = groupName; - } - } -} diff --git a/src/EventStore.Client/PersistentSubscriptions/PersistentSubscription.cs b/src/EventStore.Client/PersistentSubscriptions/PersistentSubscription.cs deleted file mode 100644 index f3d19b42e..000000000 --- a/src/EventStore.Client/PersistentSubscriptions/PersistentSubscription.cs +++ /dev/null @@ -1,205 +0,0 @@ -using EventStore.Client.PersistentSubscriptions; -using Grpc.Core; -using Microsoft.Extensions.Logging; - -namespace EventStore.Client { - /// - /// Represents a persistent subscription connection. - /// - public class PersistentSubscription : IDisposable { - private readonly EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult _persistentSubscriptionResult; - private readonly IAsyncEnumerator _enumerator; - private readonly Func _eventAppeared; - private readonly Action _subscriptionDropped; - private readonly ILogger _log; - private readonly CancellationTokenSource _cts; - - private int _subscriptionDroppedInvoked; - - /// - /// The Subscription Id. - /// - public string SubscriptionId { get; } - - internal static async Task Confirm( - EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult persistentSubscriptionResult, - Func eventAppeared, - Action subscriptionDropped, - ILogger log, UserCredentials? userCredentials, CancellationToken cancellationToken = default) { - var enumerator = persistentSubscriptionResult - .Messages - .GetAsyncEnumerator(cancellationToken); - - var result = await enumerator.MoveNextAsync(cancellationToken).ConfigureAwait(false); - - return (result, enumerator.Current) switch { - (true, PersistentSubscriptionMessage.SubscriptionConfirmation (var subscriptionId)) => - new PersistentSubscription(persistentSubscriptionResult, enumerator, subscriptionId, eventAppeared, - subscriptionDropped, log, cancellationToken), - (true, PersistentSubscriptionMessage.NotFound) => - throw new PersistentSubscriptionNotFoundException(persistentSubscriptionResult.StreamName, - persistentSubscriptionResult.GroupName), - _ => throw new InvalidOperationException("Subscription could not be confirmed.") - }; - } - - // PersistentSubscription takes responsibility for disposing the call and the disposable - private PersistentSubscription( - EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult persistentSubscriptionResult, - IAsyncEnumerator enumerator, string subscriptionId, - Func eventAppeared, - Action subscriptionDropped, ILogger log, - CancellationToken cancellationToken) { - _persistentSubscriptionResult = persistentSubscriptionResult; - _enumerator = enumerator; - SubscriptionId = subscriptionId; - _eventAppeared = eventAppeared; - _subscriptionDropped = subscriptionDropped; - _log = log; - _cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); - - Task.Run(Subscribe, _cts.Token); - } - - /// - /// Acknowledge that a message has completed processing (this will tell the server it has been processed). - /// - /// There is no need to ack a message if you have Auto Ack enabled. - /// The of the s to acknowledge. There should not be more than 2000 to ack at a time. - public Task Ack(params Uuid[] eventIds) => AckInternal(eventIds); - - /// - /// Acknowledge that a message has completed processing (this will tell the server it has been processed). - /// - /// There is no need to ack a message if you have Auto Ack enabled. - /// The of the s to acknowledge. There should not be more than 2000 to ack at a time. - public Task Ack(IEnumerable eventIds) => Ack(eventIds.ToArray()); - - /// - /// Acknowledge that a message has completed processing (this will tell the server it has been processed). - /// - /// There is no need to ack a message if you have Auto Ack enabled. - /// The s to acknowledge. There should not be more than 2000 to ack at a time. - public Task Ack(params ResolvedEvent[] resolvedEvents) => - Ack(Array.ConvertAll(resolvedEvents, resolvedEvent => resolvedEvent.OriginalEvent.EventId)); - - /// - /// Acknowledge that a message has completed processing (this will tell the server it has been processed). - /// - /// There is no need to ack a message if you have Auto Ack enabled. - /// The s to acknowledge. There should not be more than 2000 to ack at a time. - public Task Ack(IEnumerable resolvedEvents) => - Ack(resolvedEvents.Select(resolvedEvent => resolvedEvent.OriginalEvent.EventId)); - - - /// - /// Acknowledge that a message has failed processing (this will tell the server it has not been processed). - /// - /// The to take. - /// A reason given. - /// The of the s to nak. There should not be more than 2000 to nak at a time. - /// The number of eventIds exceeded the limit of 2000. - public Task Nack(PersistentSubscriptionNakEventAction action, string reason, params Uuid[] eventIds) => NackInternal(eventIds, action, reason); - - /// - /// Acknowledge that a message has failed processing (this will tell the server it has not been processed). - /// - /// The to take. - /// A reason given. - /// The s to nak. There should not be more than 2000 to nak at a time. - /// The number of resolvedEvents exceeded the limit of 2000. - public Task Nack(PersistentSubscriptionNakEventAction action, string reason, - params ResolvedEvent[] resolvedEvents) => - Nack(action, reason, - Array.ConvertAll(resolvedEvents, resolvedEvent => resolvedEvent.OriginalEvent.EventId)); - - /// - public void Dispose() => SubscriptionDropped(SubscriptionDroppedReason.Disposed); - - private async Task Subscribe() { - _log.LogDebug("Persistent Subscription {subscriptionId} confirmed.", SubscriptionId); - - try { - while (await _enumerator.MoveNextAsync(_cts.Token).ConfigureAwait(false)) { - if (_enumerator.Current is not PersistentSubscriptionMessage.Event(var resolvedEvent, var retryCount)) { - continue; - } - - if (_enumerator.Current is PersistentSubscriptionMessage.NotFound) { - if (_subscriptionDroppedInvoked != 0) { - return; - } - SubscriptionDropped(SubscriptionDroppedReason.ServerError, - new PersistentSubscriptionNotFoundException( - _persistentSubscriptionResult.StreamName, _persistentSubscriptionResult.GroupName)); - return; - } - - _log.LogTrace( - "Persistent Subscription {subscriptionId} received event {streamName}@{streamRevision} {position}", - SubscriptionId, resolvedEvent.OriginalEvent.EventStreamId, - resolvedEvent.OriginalEvent.EventNumber, resolvedEvent.OriginalEvent.Position); - - try { - await _eventAppeared( - this, - resolvedEvent, - retryCount, - _cts.Token).ConfigureAwait(false); - } catch (Exception ex) when (ex is ObjectDisposedException or OperationCanceledException) { - if (_subscriptionDroppedInvoked != 0) { - return; - } - - _log.LogWarning(ex, - "Persistent Subscription {subscriptionId} was dropped because cancellation was requested by another caller.", - SubscriptionId); - - SubscriptionDropped(SubscriptionDroppedReason.Disposed); - - return; - } catch (Exception ex) { - _log.LogError(ex, - "Persistent Subscription {subscriptionId} was dropped because the subscriber made an error.", - SubscriptionId); - SubscriptionDropped(SubscriptionDroppedReason.SubscriberError, ex); - - return; - } - } - } catch (Exception ex) { - if (_subscriptionDroppedInvoked == 0) { - _log.LogError(ex, - "Persistent Subscription {subscriptionId} was dropped because an error occurred on the server.", - SubscriptionId); - SubscriptionDropped(SubscriptionDroppedReason.ServerError, ex); - } - } finally { - if (_subscriptionDroppedInvoked == 0) { - _log.LogError( - "Persistent Subscription {subscriptionId} was unexpectedly terminated.", - SubscriptionId); - SubscriptionDropped(SubscriptionDroppedReason.ServerError); - } - } - } - - private void SubscriptionDropped(SubscriptionDroppedReason reason, Exception? ex = null) { - if (Interlocked.CompareExchange(ref _subscriptionDroppedInvoked, 1, 0) == 1) { - return; - } - - try { - _subscriptionDropped.Invoke(this, reason, ex); - } finally { - _persistentSubscriptionResult.Dispose(); - _cts.Dispose(); - } - } - - private Task AckInternal(params Uuid[] ids) => _persistentSubscriptionResult.Ack(ids); - - private Task NackInternal(Uuid[] ids, PersistentSubscriptionNakEventAction action, string reason) => - _persistentSubscriptionResult.Nack(action, reason, ids); - } -} diff --git a/src/EventStore.Client/PersistentSubscriptions/PersistentSubscriptionDroppedByServerException.cs b/src/EventStore.Client/PersistentSubscriptions/PersistentSubscriptionDroppedByServerException.cs deleted file mode 100644 index 29b42bb49..000000000 --- a/src/EventStore.Client/PersistentSubscriptions/PersistentSubscriptionDroppedByServerException.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System; - -namespace EventStore.Client { - /// - /// The exception that is thrown when the EventStoreDB drops a persistent subscription. - /// - public class PersistentSubscriptionDroppedByServerException : Exception { - /// - /// The stream name. - /// - public readonly string StreamName; - - /// - /// The group name. - /// - public readonly string GroupName; - - /// - /// Constructs a new . - /// - /// - public PersistentSubscriptionDroppedByServerException(string streamName, string groupName, - Exception? exception = null) - : base($"Subscription group '{groupName}' on stream '{streamName}' was dropped.", exception) { - if (streamName == null) throw new ArgumentNullException(nameof(streamName)); - if (groupName == null) throw new ArgumentNullException(nameof(groupName)); - StreamName = streamName; - GroupName = groupName; - } - } -} diff --git a/src/EventStore.Client/PersistentSubscriptions/PersistentSubscriptionExtraStatistic.cs b/src/EventStore.Client/PersistentSubscriptions/PersistentSubscriptionExtraStatistic.cs deleted file mode 100644 index 5b41893c4..000000000 --- a/src/EventStore.Client/PersistentSubscriptions/PersistentSubscriptionExtraStatistic.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace EventStore.Client; - -/// -/// Provides the definitions of the available extra statistics. -/// -public static class PersistentSubscriptionExtraStatistic { -#pragma warning disable CS1591 - public const string Highest = "Highest"; - public const string Mean = "Mean"; - public const string Median = "Median"; - public const string Fastest = "Fastest"; - public const string Quintile1 = "Quintile 1"; - public const string Quintile2 = "Quintile 2"; - public const string Quintile3 = "Quintile 3"; - public const string Quintile4 = "Quintile 4"; - public const string Quintile5 = "Quintile 5"; - public const string NinetyPercent = "90%"; - public const string NinetyFivePercent = "95%"; - public const string NinetyNinePercent = "99%"; - public const string NinetyNinePointFivePercent = "99.5%"; - public const string NinetyNinePointNinePercent = "99.9%"; -#pragma warning restore CS1591 -} diff --git a/src/EventStore.Client/PersistentSubscriptions/PersistentSubscriptionInfo.cs b/src/EventStore.Client/PersistentSubscriptions/PersistentSubscriptionInfo.cs deleted file mode 100644 index 40af4cdf2..000000000 --- a/src/EventStore.Client/PersistentSubscriptions/PersistentSubscriptionInfo.cs +++ /dev/null @@ -1,224 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using EventStore.Client.PersistentSubscriptions; -using Google.Protobuf.Collections; - -namespace EventStore.Client { - /// - /// Provides the details for a persistent subscription. - /// - /// The source of events for the subscription. - /// The group name given on creation. - /// The current status of the subscription. - /// Active connections to the subscription. - /// The settings used to create the persistant subscription. - /// Live statistics for the persistent subscription. - public record PersistentSubscriptionInfo(string EventSource, string GroupName, string Status, - IEnumerable Connections, - PersistentSubscriptionSettings? Settings, PersistentSubscriptionStats Stats) { - - internal static PersistentSubscriptionInfo From(SubscriptionInfo info) { - IPosition? startFrom = null; - IPosition? lastCheckpointedEventPosition = null; - IPosition? lastKnownEventPosition = null; - if (info.EventSource == SystemStreams.AllStream) { - if (Position.TryParse(info.StartFrom, out var position)) { - startFrom = position; - } - if (Position.TryParse(info.LastCheckpointedEventPosition, out position)) { - lastCheckpointedEventPosition = position; - } - if (Position.TryParse(info.LastKnownEventPosition, out position)) { - lastKnownEventPosition = position; - } - } else { - if (long.TryParse(info.StartFrom, out var streamPosition)) { - startFrom = StreamPosition.FromInt64(streamPosition); - } - if (ulong.TryParse(info.LastCheckpointedEventPosition, out var position)) { - lastCheckpointedEventPosition = new StreamPosition(position); - } - if (ulong.TryParse(info.LastKnownEventPosition, out position)) { - lastKnownEventPosition = new StreamPosition(position); - } - } - - return new PersistentSubscriptionInfo( - EventSource: info.EventSource, - GroupName: info.GroupName, - Status: info.Status, - Connections: From(info.Connections), - Settings: new PersistentSubscriptionSettings( - resolveLinkTos: info.ResolveLinkTos, - startFrom: startFrom, - extraStatistics: info.ExtraStatistics, - messageTimeout: TimeSpan.FromMilliseconds(info.MessageTimeoutMilliseconds), - maxRetryCount: info.MaxRetryCount, - liveBufferSize: info.LiveBufferSize, - readBatchSize: info.ReadBatchSize, - historyBufferSize: info.BufferSize, - checkPointAfter: TimeSpan.FromMilliseconds(info.CheckPointAfterMilliseconds), - checkPointLowerBound: info.MinCheckPointCount, - checkPointUpperBound: info.MaxCheckPointCount, - maxSubscriberCount: info.MaxSubscriberCount, - consumerStrategyName: info.NamedConsumerStrategy - ), - Stats: new PersistentSubscriptionStats( - AveragePerSecond: info.AveragePerSecond, - TotalItems: info.TotalItems, - CountSinceLastMeasurement: info.CountSinceLastMeasurement, - ReadBufferCount: info.ReadBufferCount, - LiveBufferCount: info.LiveBufferCount, - RetryBufferCount: info.RetryBufferCount, - TotalInFlightMessages: info.TotalInFlightMessages, - OutstandingMessagesCount: info.OutstandingMessagesCount, - ParkedMessageCount: info.ParkedMessageCount, - LastCheckpointedEventPosition: lastCheckpointedEventPosition, - LastKnownEventPosition: lastKnownEventPosition - ) - ); - } - - internal static PersistentSubscriptionInfo From(PersistentSubscriptionDto info) { - PersistentSubscriptionSettings? settings = null; - if (info.Config != null) { - settings = new PersistentSubscriptionSettings( - resolveLinkTos: info.Config.ResolveLinktos, - // we only need to support StreamPosition as $all was never implemented in http api. - startFrom: new StreamPosition(info.Config.StartFrom), - extraStatistics: info.Config.ExtraStatistics, - messageTimeout: TimeSpan.FromMilliseconds(info.Config.MessageTimeoutMilliseconds), - maxRetryCount: info.Config.MaxRetryCount, - liveBufferSize: info.Config.LiveBufferSize, - readBatchSize: info.Config.ReadBatchSize, - historyBufferSize: info.Config.BufferSize, - checkPointAfter: TimeSpan.FromMilliseconds(info.Config.CheckPointAfterMilliseconds), - checkPointLowerBound: info.Config.MinCheckPointCount, - checkPointUpperBound: info.Config.MaxCheckPointCount, - maxSubscriberCount: info.Config.MaxSubscriberCount, - consumerStrategyName: info.Config.NamedConsumerStrategy - ); - } - - return new PersistentSubscriptionInfo( - EventSource: info.EventStreamId, - GroupName: info.GroupName, - Status: info.Status, - Connections: PersistentSubscriptionConnectionInfo.CreateFrom(info.Connections), - Settings: settings, - Stats: new PersistentSubscriptionStats( - AveragePerSecond: (int)info.AverageItemsPerSecond, - TotalItems: info.TotalItemsProcessed, - CountSinceLastMeasurement: info.CountSinceLastMeasurement, - ReadBufferCount: info.ReadBufferCount, - LiveBufferCount: info.LiveBufferCount, - RetryBufferCount: info.RetryBufferCount, - TotalInFlightMessages: info.TotalInFlightMessages, - OutstandingMessagesCount: info.OutstandingMessagesCount, - ParkedMessageCount: info.ParkedMessageCount, - LastCheckpointedEventPosition: StreamPosition.FromInt64(info.LastProcessedEventNumber), - LastKnownEventPosition: StreamPosition.FromInt64(info.LastKnownEventNumber) - ) - ); - } - - private static IEnumerable From( - RepeatedField connections) { - foreach (var conn in connections) { - yield return new PersistentSubscriptionConnectionInfo( - From: conn.From, - Username: conn.Username, - AverageItemsPerSecond: conn.AverageItemsPerSecond, - TotalItems: conn.TotalItems, - CountSinceLastMeasurement: conn.CountSinceLastMeasurement, - AvailableSlots: conn.AvailableSlots, - InFlightMessages: conn.InFlightMessages, - ConnectionName: conn.ConnectionName, - ExtraStatistics: From(conn.ObservedMeasurements) - ); - } - } - - private static IDictionary From(IEnumerable measurements) => - measurements.ToDictionary(k => k.Key, v => v.Value); - } - - /// - /// Provides the statistics of a persistent subscription. - /// - /// Average number of events per second. - /// Total number of events processed by subscription. - /// Number of events seen since last measurement on this connection (used as the basis for ). - /// Number of events in the read buffer. - /// Number of events in the live buffer. - /// Number of events in the retry buffer. - /// Current in flight messages across all connections. - /// Current number of outstanding messages. - /// The current number of parked messages. - /// The of the last checkpoint. This will be null if there are no checkpoints. - /// The of the last known event. This will be undefined if no events have been received yet. - public record PersistentSubscriptionStats( - int AveragePerSecond, long TotalItems, long CountSinceLastMeasurement, int ReadBufferCount, - long LiveBufferCount, int RetryBufferCount, int TotalInFlightMessages, int OutstandingMessagesCount, - long ParkedMessageCount, IPosition? LastCheckpointedEventPosition, IPosition? LastKnownEventPosition); - - /// - /// Provides the details of a persistent subscription connection. - /// - /// Origin of this connection. - /// Connection username. - /// The name of the connection. - /// Average events per second on this connection. - /// Total items on this connection. - /// Number of items seen since last measurement on this connection (used as the basis for averageItemsPerSecond). - /// Number of available slots. - /// Number of in flight messages on this connection. - /// Timing measurements for the connection. Can be enabled with the ExtraStatistics setting. - public record PersistentSubscriptionConnectionInfo(string From, string Username, string ConnectionName, int AverageItemsPerSecond, - long TotalItems, long CountSinceLastMeasurement, int AvailableSlots, int InFlightMessages, - IDictionary ExtraStatistics) { - - internal static IEnumerable CreateFrom( - IEnumerable connections) { - - foreach (var connection in connections) { - yield return CreateFrom(connection); - } - } - - private static PersistentSubscriptionConnectionInfo CreateFrom(PersistentSubscriptionConnectionInfoDto connection) { - return new PersistentSubscriptionConnectionInfo( - From: connection.From, - Username: connection.Username, - ConnectionName: connection.ConnectionName, - AverageItemsPerSecond: (int)connection.AverageItemsPerSecond, - TotalItems: connection.TotalItems, - CountSinceLastMeasurement: connection.CountSinceLastMeasurement, - AvailableSlots: connection.AvailableSlots, - InFlightMessages: connection.InFlightMessages, - ExtraStatistics: CreateFrom(connection.ExtraStatistics) - ); - } - - private static IDictionary CreateFrom(IEnumerable extraStatistics) => - extraStatistics.ToDictionary(k => k.Key, v => v.Value); - } - - internal record PersistentSubscriptionDto(string EventStreamId, string GroupName, - string Status, float AverageItemsPerSecond, long TotalItemsProcessed, long CountSinceLastMeasurement, - long LastProcessedEventNumber, long LastKnownEventNumber, int ReadBufferCount, long LiveBufferCount, - int RetryBufferCount, int TotalInFlightMessages, int OutstandingMessagesCount, int ParkedMessageCount, - PersistentSubscriptionConfig? Config, IEnumerable Connections); - - internal record PersistentSubscriptionConfig(bool ResolveLinktos, ulong StartFrom, string StartPosition, - int MessageTimeoutMilliseconds, bool ExtraStatistics, int MaxRetryCount, int LiveBufferSize, int BufferSize, - int ReadBatchSize, int CheckPointAfterMilliseconds, int MinCheckPointCount, int MaxCheckPointCount, - int MaxSubscriberCount, string NamedConsumerStrategy); - - internal record PersistentSubscriptionConnectionInfoDto(string From, string Username, float AverageItemsPerSecond, - long TotalItems, long CountSinceLastMeasurement, int AvailableSlots, int InFlightMessages, string ConnectionName, - IEnumerable ExtraStatistics); - - internal record PersistentSubscriptionMeasurementInfoDto(string Key, long Value); -} diff --git a/src/EventStore.Client/PersistentSubscriptions/PersistentSubscriptionMessage.cs b/src/EventStore.Client/PersistentSubscriptions/PersistentSubscriptionMessage.cs deleted file mode 100644 index eabde9b62..000000000 --- a/src/EventStore.Client/PersistentSubscriptions/PersistentSubscriptionMessage.cs +++ /dev/null @@ -1,33 +0,0 @@ -namespace EventStore.Client { - /// - /// The base record of all stream messages. - /// - public abstract record PersistentSubscriptionMessage { - /// - /// A that represents a . - /// - /// The . - /// The number of times the has been retried. - public record Event(ResolvedEvent ResolvedEvent, int? RetryCount) : PersistentSubscriptionMessage; - - /// - /// A representing a stream that was not found. - /// - public record NotFound : PersistentSubscriptionMessage { - internal static readonly NotFound Instance = new(); - } - - /// - /// A indicating that the subscription is ready to send additional messages. - /// - /// The unique identifier of the subscription. - public record SubscriptionConfirmation(string SubscriptionId) : PersistentSubscriptionMessage; - - /// - /// A that could not be identified, usually indicating a lower client compatibility level than the server supports. - /// - public record Unknown : PersistentSubscriptionMessage { - internal static readonly Unknown Instance = new(); - } - } -} diff --git a/src/EventStore.Client/PersistentSubscriptions/PersistentSubscriptionNakEventAction.cs b/src/EventStore.Client/PersistentSubscriptions/PersistentSubscriptionNakEventAction.cs deleted file mode 100644 index ce24b6552..000000000 --- a/src/EventStore.Client/PersistentSubscriptions/PersistentSubscriptionNakEventAction.cs +++ /dev/null @@ -1,31 +0,0 @@ -namespace EventStore.Client { - /// - /// Actions to be taken by server in the case of a client NAK - /// - public enum PersistentSubscriptionNakEventAction { - /// - /// Client unknown on action. Let server decide - /// - Unknown = 0, - - /// - /// Park message do not resend. Put on poison queue - /// - Park = 1, - - /// - /// Explicitly retry the message. - /// - Retry = 2, - - /// - /// Skip this message do not resend do not put in poison queue - /// - Skip = 3, - - /// - /// Stop the subscription. - /// - Stop = 4 - } -} diff --git a/src/EventStore.Client/PersistentSubscriptions/PersistentSubscriptionNotFoundException.cs b/src/EventStore.Client/PersistentSubscriptions/PersistentSubscriptionNotFoundException.cs deleted file mode 100644 index 81e023ade..000000000 --- a/src/EventStore.Client/PersistentSubscriptions/PersistentSubscriptionNotFoundException.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System; - -namespace EventStore.Client { - /// - /// The exception that is thrown when a persistent subscription is not found. - /// - public class PersistentSubscriptionNotFoundException : Exception { - /// - /// The stream name. - /// - public readonly string StreamName; - /// - /// The group name. - /// - public readonly string GroupName; - - /// - /// Constructs a new . - /// - /// - public PersistentSubscriptionNotFoundException(string streamName, string groupName, Exception? exception = null) - : base($"Subscription group '{groupName}' on stream '{streamName}' does not exist.", exception) { - if (streamName == null) throw new ArgumentNullException(nameof(streamName)); - if (groupName == null) throw new ArgumentNullException(nameof(groupName)); - StreamName = streamName; - GroupName = groupName; - } - } -} diff --git a/src/EventStore.Client/PersistentSubscriptions/PersistentSubscriptionSettings.cs b/src/EventStore.Client/PersistentSubscriptions/PersistentSubscriptionSettings.cs deleted file mode 100644 index a36bfff87..000000000 --- a/src/EventStore.Client/PersistentSubscriptions/PersistentSubscriptionSettings.cs +++ /dev/null @@ -1,112 +0,0 @@ -using System; - -namespace EventStore.Client { - /// - /// A class representing the settings of a persistent subscription. - /// - public sealed class PersistentSubscriptionSettings { - /// - /// Whether the should resolve linkTo events to their linked events. - /// - public readonly bool ResolveLinkTos; - - /// - /// Which event position in the stream or transaction file the subscription should start from. - /// - public readonly IPosition? StartFrom; - - /// - /// Whether to track latency statistics on this subscription. - /// - public readonly bool ExtraStatistics; - - /// - /// The amount of time after which to consider a message as timed out and retried. - /// - public readonly TimeSpan MessageTimeout; - - /// - /// The maximum number of retries (due to timeout) before a message is considered to be parked. - /// - public readonly int MaxRetryCount; - - /// - /// The size of the buffer (in-memory) listening to live messages as they happen before paging occurs. - /// - public readonly int LiveBufferSize; - - /// - /// The number of events read at a time when paging through history. - /// - public readonly int ReadBatchSize; - - /// - /// The number of events to cache when paging through history. - /// - public readonly int HistoryBufferSize; - - /// - /// The amount of time to try to checkpoint after. - /// - public readonly TimeSpan CheckPointAfter; - - /// - /// The minimum number of messages to process before a checkpoint may be written. - /// - public readonly int CheckPointLowerBound; - - /// - /// The maximum number of messages not checkpointed before forcing a checkpoint. - /// - public readonly int CheckPointUpperBound; - - /// - /// The maximum number of subscribers allowed. - /// - public readonly int MaxSubscriberCount; - - /// - /// The strategy to use for distributing events to client consumers. See for system supported strategies. - /// - public readonly string ConsumerStrategyName; - - /// - /// Constructs a new . - /// - /// - public PersistentSubscriptionSettings(bool resolveLinkTos = false, IPosition? startFrom = null, - bool extraStatistics = false, TimeSpan? messageTimeout = null, int maxRetryCount = 10, - int liveBufferSize = 500, int readBatchSize = 20, int historyBufferSize = 500, - TimeSpan? checkPointAfter = null, int checkPointLowerBound = 10, int checkPointUpperBound = 1000, - int maxSubscriberCount = 0, string consumerStrategyName = SystemConsumerStrategies.RoundRobin) { - messageTimeout ??= TimeSpan.FromSeconds(30); - checkPointAfter ??= TimeSpan.FromSeconds(2); - - if (messageTimeout.Value < TimeSpan.Zero || messageTimeout.Value.TotalMilliseconds > int.MaxValue) { - throw new ArgumentOutOfRangeException( - nameof(messageTimeout), - $"{nameof(messageTimeout)} must be greater than {TimeSpan.Zero} and less than or equal to {TimeSpan.FromMilliseconds(int.MaxValue)}"); - } - - if (checkPointAfter.Value < TimeSpan.Zero || checkPointAfter.Value.TotalMilliseconds > int.MaxValue) { - throw new ArgumentOutOfRangeException( - nameof(checkPointAfter), - $"{nameof(checkPointAfter)} must be greater than {TimeSpan.Zero} and less than or equal to {TimeSpan.FromMilliseconds(int.MaxValue)}"); - } - - ResolveLinkTos = resolveLinkTos; - StartFrom = startFrom; - ExtraStatistics = extraStatistics; - MessageTimeout = messageTimeout.Value; - MaxRetryCount = maxRetryCount; - LiveBufferSize = liveBufferSize; - ReadBatchSize = readBatchSize; - HistoryBufferSize = historyBufferSize; - CheckPointAfter = checkPointAfter.Value; - CheckPointLowerBound = checkPointLowerBound; - CheckPointUpperBound = checkPointUpperBound; - MaxSubscriberCount = maxSubscriberCount; - ConsumerStrategyName = consumerStrategyName; - } - } -} diff --git a/src/EventStore.Client/PersistentSubscriptions/SystemConsumerStrategies.cs b/src/EventStore.Client/PersistentSubscriptions/SystemConsumerStrategies.cs deleted file mode 100644 index 6183461ca..000000000 --- a/src/EventStore.Client/PersistentSubscriptions/SystemConsumerStrategies.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace EventStore.Client { - /// - /// System supported consumer strategies for use with persistent subscriptions. - /// - public static class SystemConsumerStrategies { - /// - /// Distributes events to a single client until it is full. Then round robin to the next client. - /// - public const string DispatchToSingle = nameof(DispatchToSingle); - - /// - /// Distribute events to each client in a round robin fashion. - /// - public const string RoundRobin = nameof(RoundRobin); - - /// - /// Distribute events of the same streamId to the same client until it disconnects on a best efforts basis. - /// Designed to be used with indexes such as the category projection. - /// - public const string Pinned = nameof(Pinned); - } -} diff --git a/src/EventStore.Client/ProjectionManagement/EventStoreProjectionManagementClient.Control.cs b/src/EventStore.Client/ProjectionManagement/EventStoreProjectionManagementClient.Control.cs deleted file mode 100644 index e21da92fc..000000000 --- a/src/EventStore.Client/ProjectionManagement/EventStoreProjectionManagementClient.Control.cs +++ /dev/null @@ -1,102 +0,0 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using EventStore.Client.Projections; - -namespace EventStore.Client { - public partial class EventStoreProjectionManagementClient { - /// - /// Enables a projection. - /// - /// - /// - /// - /// - /// - public async Task EnableAsync(string name, TimeSpan? deadline = null, UserCredentials? userCredentials = null, - CancellationToken cancellationToken = default) { - var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); - using var call = new Projections.Projections.ProjectionsClient( - channelInfo.CallInvoker).EnableAsync(new EnableReq { - Options = new EnableReq.Types.Options { - Name = name - } - }, EventStoreCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); - await call.ResponseAsync.ConfigureAwait(false); - } - - /// - /// Resets a projection. This will re-emit events. Streams that are written to from the projection will also be soft deleted. - /// - /// - /// - /// - /// - /// - public async Task ResetAsync(string name, TimeSpan? deadline = null, UserCredentials? userCredentials = null, - CancellationToken cancellationToken = default) { - var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); - using var call = new Projections.Projections.ProjectionsClient( - channelInfo.CallInvoker).ResetAsync(new ResetReq { - Options = new ResetReq.Types.Options { - Name = name, - WriteCheckpoint = true - } - }, EventStoreCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); - await call.ResponseAsync.ConfigureAwait(false); - } - - /// - /// Aborts a projection. Does not save the projection's checkpoint. - /// - /// - /// - /// - /// - /// - public Task AbortAsync(string name, TimeSpan? deadline = null, UserCredentials? userCredentials = null, - CancellationToken cancellationToken = default) => - DisableInternalAsync(name, false, deadline, userCredentials, cancellationToken); - - /// - /// Disables a projection. Saves the projection's checkpoint. - /// - /// - /// - /// - /// - /// - public Task DisableAsync(string name, TimeSpan? deadline = null, UserCredentials? userCredentials = null, - CancellationToken cancellationToken = default) => - DisableInternalAsync(name, true, deadline, userCredentials, cancellationToken); - - /// - /// Restarts the projection subsystem. - /// - /// - /// - /// - /// - public async Task RestartSubsystemAsync(TimeSpan? deadline = null, UserCredentials? userCredentials = null, - CancellationToken cancellationToken = default) { - var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); - using var call = new Projections.Projections.ProjectionsClient( - channelInfo.CallInvoker).RestartSubsystemAsync(new Empty(), - EventStoreCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); - await call.ResponseAsync.ConfigureAwait(false); - } - - private async Task DisableInternalAsync(string name, bool writeCheckpoint, TimeSpan? deadline, - UserCredentials? userCredentials, CancellationToken cancellationToken) { - var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); - using var call = new Projections.Projections.ProjectionsClient( - channelInfo.CallInvoker).DisableAsync(new DisableReq { - Options = new DisableReq.Types.Options { - Name = name, - WriteCheckpoint = writeCheckpoint - } - }, EventStoreCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); - await call.ResponseAsync.ConfigureAwait(false); - } - } -} diff --git a/src/EventStore.Client/ProjectionManagement/EventStoreProjectionManagementClient.Create.cs b/src/EventStore.Client/ProjectionManagement/EventStoreProjectionManagementClient.Create.cs deleted file mode 100644 index 54498cabf..000000000 --- a/src/EventStore.Client/ProjectionManagement/EventStoreProjectionManagementClient.Create.cs +++ /dev/null @@ -1,80 +0,0 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using EventStore.Client.Projections; - -namespace EventStore.Client { - public partial class EventStoreProjectionManagementClient { - /// - /// Creates a one-time projection. - /// - /// - /// - /// - /// - /// - public async Task CreateOneTimeAsync(string query, TimeSpan? deadline = null, - UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) { - var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); - using var call = new Projections.Projections.ProjectionsClient( - channelInfo.CallInvoker).CreateAsync(new CreateReq { - Options = new CreateReq.Types.Options { - OneTime = new Empty(), - Query = query - } - }, EventStoreCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); - await call.ResponseAsync.ConfigureAwait(false); - } - - /// - /// Creates a continuous projection. - /// - /// - /// - /// - /// - /// - /// - /// - public async Task CreateContinuousAsync(string name, string query, bool trackEmittedStreams = false, - TimeSpan? deadline = null, UserCredentials? userCredentials = null, - CancellationToken cancellationToken = default) { - var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); - using var call = new Projections.Projections.ProjectionsClient( - channelInfo.CallInvoker).CreateAsync(new CreateReq { - Options = new CreateReq.Types.Options { - Continuous = new CreateReq.Types.Options.Types.Continuous { - Name = name, - TrackEmittedStreams = trackEmittedStreams - }, - Query = query - } - }, EventStoreCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); - await call.ResponseAsync.ConfigureAwait(false); - } - - /// - /// Creates a transient projection. - /// - /// - /// - /// - /// - /// - /// - public async Task CreateTransientAsync(string name, string query, TimeSpan? deadline = null, - UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) { - var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); - using var call = new Projections.Projections.ProjectionsClient( - channelInfo.CallInvoker).CreateAsync(new CreateReq { - Options = new CreateReq.Types.Options { - Transient = new CreateReq.Types.Options.Types.Transient { - Name = name - }, - Query = query - } - }, EventStoreCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); - await call.ResponseAsync.ConfigureAwait(false); - } - } -} diff --git a/src/EventStore.Client/ProjectionManagement/EventStoreProjectionManagementClient.State.cs b/src/EventStore.Client/ProjectionManagement/EventStoreProjectionManagementClient.State.cs deleted file mode 100644 index 64187fe1f..000000000 --- a/src/EventStore.Client/ProjectionManagement/EventStoreProjectionManagementClient.State.cs +++ /dev/null @@ -1,205 +0,0 @@ -using System; -using System.IO; -using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; -using EventStore.Client.Projections; -using Google.Protobuf.WellKnownTypes; -using Type = System.Type; - -namespace EventStore.Client { - public partial class EventStoreProjectionManagementClient { - static readonly JsonSerializerOptions DefaultJsonSerializerOptions = new JsonSerializerOptions(); - - /// - /// Gets the result of a projection as an untyped document. - /// - /// - /// - /// - /// - /// - /// - public async Task GetResultAsync(string name, string? partition = null, - TimeSpan? deadline = null, UserCredentials? userCredentials = null, - CancellationToken cancellationToken = default) { - var value = await GetResultInternalAsync(name, partition, deadline, userCredentials, cancellationToken) - .ConfigureAwait(false); - -#if NET - await using var stream = new MemoryStream(); -#else - using var stream = new MemoryStream(); -#endif - await using var writer = new Utf8JsonWriter(stream); - var serializer = new ValueSerializer(); - serializer.Write(writer, value, DefaultJsonSerializerOptions); - await writer.FlushAsync(cancellationToken).ConfigureAwait(false); - stream.Position = 0; - - return JsonDocument.Parse(stream); - } - - /// - /// Gets the result of a projection. - /// - /// - /// - /// - /// - /// - /// - /// - /// - public async Task GetResultAsync(string name, string? partition = null, - JsonSerializerOptions? serializerOptions = null, - TimeSpan? deadline = null, UserCredentials? userCredentials = null, - CancellationToken cancellationToken = default) { - var value = await GetResultInternalAsync(name, partition, deadline, userCredentials, cancellationToken) - .ConfigureAwait(false); -#if NET - await using var stream = new MemoryStream(); -#else - using var stream = new MemoryStream(); -#endif - await using var writer = new Utf8JsonWriter(stream); - var serializer = new ValueSerializer(); - serializer.Write(writer, value, DefaultJsonSerializerOptions); - await writer.FlushAsync(cancellationToken).ConfigureAwait(false); - stream.Position = 0; - - return JsonSerializer.Deserialize(stream.ToArray(), serializerOptions)!; - } - - private async ValueTask GetResultInternalAsync(string name, string? partition, - TimeSpan? deadline, UserCredentials? userCredentials, CancellationToken cancellationToken) { - var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); - using var call = new Projections.Projections.ProjectionsClient( - channelInfo.CallInvoker).ResultAsync(new ResultReq { - Options = new ResultReq.Types.Options { - Name = name, - Partition = partition ?? string.Empty - } - }, EventStoreCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); - - var response = await call.ResponseAsync.ConfigureAwait(false); - return response.Result; - } - - /// - /// Gets the state of a projection as an untyped document. - /// - /// - /// - /// - /// - /// - /// - public async Task GetStateAsync(string name, string? partition = null, - TimeSpan? deadline = null, UserCredentials? userCredentials = null, - CancellationToken cancellationToken = default) { - var value = await GetStateInternalAsync(name, partition, deadline, userCredentials, cancellationToken) - .ConfigureAwait(false); - -#if NET - await using var stream = new MemoryStream(); -#else - using var stream = new MemoryStream(); -#endif - await using var writer = new Utf8JsonWriter(stream); - var serializer = new ValueSerializer(); - serializer.Write(writer, value, DefaultJsonSerializerOptions); - stream.Position = 0; - await writer.FlushAsync(cancellationToken).ConfigureAwait(false); - - return JsonDocument.Parse(stream); - } - - /// - /// Gets the state of a projection. - /// - /// - /// - /// - /// - /// - /// - /// - /// - public async Task GetStateAsync(string name, string? partition = null, - JsonSerializerOptions? serializerOptions = null, TimeSpan? deadline = null, - UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) { - var value = await GetStateInternalAsync(name, partition, deadline, userCredentials, cancellationToken) - .ConfigureAwait(false); - -#if NET - await using var stream = new MemoryStream(); -#else - using var stream = new MemoryStream(); -#endif - await using var writer = new Utf8JsonWriter(stream); - var serializer = new ValueSerializer(); - serializer.Write(writer, value, DefaultJsonSerializerOptions); - await writer.FlushAsync(cancellationToken).ConfigureAwait(false); - stream.Position = 0; - - return JsonSerializer.Deserialize(stream.ToArray(), serializerOptions)!; - } - - private async ValueTask GetStateInternalAsync(string name, string? partition, TimeSpan? deadline, - UserCredentials? userCredentials, CancellationToken cancellationToken) { - var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); - using var call = new Projections.Projections.ProjectionsClient( - channelInfo.CallInvoker).StateAsync(new StateReq { - Options = new StateReq.Types.Options { - Name = name, - Partition = partition ?? string.Empty - } - }, EventStoreCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); - - var response = await call.ResponseAsync.ConfigureAwait(false); - return response.State; - } - - private class ValueSerializer : System.Text.Json.Serialization.JsonConverter { - public override Value Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => - throw new NotSupportedException(); - - public override void Write(Utf8JsonWriter writer, Value value, JsonSerializerOptions options) { - switch (value.KindCase) { - case Value.KindOneofCase.None: - break; - case Value.KindOneofCase.BoolValue: - writer.WriteBooleanValue(value.BoolValue); - break; - case Value.KindOneofCase.NullValue: - writer.WriteNullValue(); - break; - case Value.KindOneofCase.NumberValue: - writer.WriteNumberValue(value.NumberValue); - break; - case Value.KindOneofCase.StringValue: - writer.WriteStringValue(value.StringValue); - break; - case Value.KindOneofCase.ListValue: - writer.WriteStartArray(); - foreach (var item in value.ListValue.Values) { - Write(writer, item, options); - } - - writer.WriteEndArray(); - break; - case Value.KindOneofCase.StructValue: - writer.WriteStartObject(); - foreach (var map in value.StructValue.Fields) { - writer.WritePropertyName(map.Key); - Write(writer, map.Value, options); - } - - writer.WriteEndObject(); - break; - } - } - } - } -} diff --git a/src/EventStore.Client/ProjectionManagement/EventStoreProjectionManagementClient.Statistics.cs b/src/EventStore.Client/ProjectionManagement/EventStoreProjectionManagementClient.Statistics.cs deleted file mode 100644 index 0b49b8b80..000000000 --- a/src/EventStore.Client/ProjectionManagement/EventStoreProjectionManagementClient.Statistics.cs +++ /dev/null @@ -1,104 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.CompilerServices; -using System.Threading; -using System.Threading.Tasks; -using EventStore.Client.Projections; -using Grpc.Core; - -namespace EventStore.Client { - public partial class EventStoreProjectionManagementClient { - /// - /// List the of all one-time projections. - /// - /// - /// - /// - /// - public IAsyncEnumerable ListOneTimeAsync(TimeSpan? deadline = null, - UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) => - ListInternalAsync(new StatisticsReq.Types.Options { - OneTime = new Empty() - }, - EventStoreCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken), - cancellationToken); - - /// - /// List the of all continuous projections. - /// - /// - /// - /// - /// - public IAsyncEnumerable ListContinuousAsync(TimeSpan? deadline = null, - UserCredentials? userCredentials = null, - CancellationToken cancellationToken = default) => - ListInternalAsync(new StatisticsReq.Types.Options { - Continuous = new Empty() - }, - EventStoreCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken), - cancellationToken); - - /// - /// Gets the status of a projection. - /// - /// - /// - /// - /// - /// - public Task GetStatusAsync(string name, - TimeSpan? deadline = null, UserCredentials? userCredentials = null, - CancellationToken cancellationToken = default) => ListInternalAsync(new StatisticsReq.Types.Options { - Name = name - }, - EventStoreCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken), - cancellationToken) - .FirstOrDefaultAsync(cancellationToken).AsTask(); - - /// - /// List the of all projections. - /// - /// - /// - /// - /// - public IAsyncEnumerable ListAllAsync(TimeSpan? deadline = null, - UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) => - ListInternalAsync(new StatisticsReq.Types.Options { - All = new Empty() - }, - EventStoreCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken), - cancellationToken); - - private async IAsyncEnumerable ListInternalAsync(StatisticsReq.Types.Options options, - CallOptions callOptions, - [EnumeratorCancellation] CancellationToken cancellationToken) { - var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); - using var call = new Projections.Projections.ProjectionsClient( - channelInfo.CallInvoker).Statistics(new StatisticsReq { - Options = options - }, callOptions); - - await foreach (var projectionDetails in call.ResponseStream - .ReadAllAsync(cancellationToken) - .Select(ConvertToProjectionDetails) - .WithCancellation(cancellationToken) - .ConfigureAwait(false)) { - yield return projectionDetails; - } - } - - private static ProjectionDetails ConvertToProjectionDetails(StatisticsResp response) { - var details = response.Details; - - return new ProjectionDetails(details.CoreProcessingTime, details.Version, details.Epoch, - details.EffectiveName, details.WritesInProgress, details.ReadsInProgress, details.PartitionsCached, - details.Status, details.StateReason, details.Name, details.Mode, details.Position, details.Progress, - details.LastCheckpoint, details.EventsProcessedAfterRestart, details.CheckpointStatus, - details.BufferedEvents, details.WritePendingEventsBeforeCheckpoint, - details.WritePendingEventsAfterCheckpoint); - } - } -} diff --git a/src/EventStore.Client/ProjectionManagement/EventStoreProjectionManagementClient.Update.cs b/src/EventStore.Client/ProjectionManagement/EventStoreProjectionManagementClient.Update.cs deleted file mode 100644 index 8f577e92c..000000000 --- a/src/EventStore.Client/ProjectionManagement/EventStoreProjectionManagementClient.Update.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using EventStore.Client.Projections; - -namespace EventStore.Client { - public partial class EventStoreProjectionManagementClient { - /// - /// Updates a projection. - /// - /// - /// - /// - /// - /// - /// - /// - public async Task UpdateAsync(string name, string query, bool? emitEnabled = null, - TimeSpan? deadline = null, UserCredentials? userCredentials = null, - CancellationToken cancellationToken = default) { - var options = new UpdateReq.Types.Options { - Name = name, - Query = query - }; - if (emitEnabled.HasValue) { - options.EmitEnabled = emitEnabled.Value; - } else { - options.NoEmitOptions = new Empty(); - } - - var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); - using var call = new Projections.Projections.ProjectionsClient( - channelInfo.CallInvoker).UpdateAsync(new UpdateReq { - Options = options - }, EventStoreCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); - - await call.ResponseAsync.ConfigureAwait(false); - } - } -} diff --git a/src/EventStore.Client/ProjectionManagement/EventStoreProjectionManagementClient.cs b/src/EventStore.Client/ProjectionManagement/EventStoreProjectionManagementClient.cs deleted file mode 100644 index be0679928..000000000 --- a/src/EventStore.Client/ProjectionManagement/EventStoreProjectionManagementClient.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System; -using System.Collections.Generic; -using Grpc.Core; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Extensions.Options; - -namespace EventStore.Client { - /// - ///The client used to manage projections on the EventStoreDB. - /// - public sealed partial class EventStoreProjectionManagementClient : EventStoreClientBase { - private readonly ILogger _log; - - /// - /// Constructs a new . This method is not intended to be called directly from your code. - /// - /// - public EventStoreProjectionManagementClient(IOptions options) : this(options.Value) { - } - - /// - /// Constructs a new . - /// - /// - public EventStoreProjectionManagementClient(EventStoreClientSettings? settings) : base(settings, - new Dictionary>()) { - _log = settings?.LoggerFactory?.CreateLogger() ?? - new NullLogger(); - } - } -} diff --git a/src/EventStore.Client/ProjectionManagement/EventStoreProjectionManagementClientCollectionExtensions.cs b/src/EventStore.Client/ProjectionManagement/EventStoreProjectionManagementClientCollectionExtensions.cs deleted file mode 100644 index ea1f634f6..000000000 --- a/src/EventStore.Client/ProjectionManagement/EventStoreProjectionManagementClientCollectionExtensions.cs +++ /dev/null @@ -1,74 +0,0 @@ -// ReSharper disable CheckNamespace - -using System; -using System.Net.Http; -using EventStore.Client; -using Grpc.Core.Interceptors; -using Microsoft.Extensions.DependencyInjection.Extensions; -using Microsoft.Extensions.Logging; - -namespace Microsoft.Extensions.DependencyInjection { - /// - /// A set of extension methods for which provide support for an . - /// - public static class EventStoreProjectionManagementClientCollectionExtensions { - /// - /// Adds an to the . - /// - /// - /// - /// - /// - /// - public static IServiceCollection AddEventStoreProjectionManagementClient(this IServiceCollection services, - Uri address, - Func? createHttpMessageHandler = null) - => services.AddEventStoreProjectionManagementClient(options => { - options.ConnectivitySettings.Address = address; - options.CreateHttpMessageHandler = createHttpMessageHandler; - }); - - /// - /// Adds an to the . - /// - /// - /// - /// - /// - public static IServiceCollection AddEventStoreProjectionManagementClient(this IServiceCollection services, - Action? configureSettings = null) => - services.AddEventStoreProjectionManagementClient(new EventStoreClientSettings(), configureSettings); - - /// - /// Adds an to the . - /// - /// - /// - /// - /// - /// - public static IServiceCollection AddEventStoreProjectionManagementClient(this IServiceCollection services, - string connectionString, Action? configureSettings = null) => - services.AddEventStoreProjectionManagementClient(EventStoreClientSettings.Create(connectionString), - configureSettings); - - private static IServiceCollection AddEventStoreProjectionManagementClient(this IServiceCollection services, - EventStoreClientSettings settings, Action? configureSettings) { - if (services == null) { - throw new ArgumentNullException(nameof(services)); - } - - configureSettings?.Invoke(settings); - - services.TryAddSingleton(provider => { - settings.LoggerFactory ??= provider.GetService(); - settings.Interceptors ??= provider.GetServices(); - - return new EventStoreProjectionManagementClient(settings); - }); - - return services; - } - } -} -// ReSharper restore CheckNamespace diff --git a/src/EventStore.Client/ProjectionManagement/ProjectionDetails.cs b/src/EventStore.Client/ProjectionManagement/ProjectionDetails.cs deleted file mode 100644 index c604362b1..000000000 --- a/src/EventStore.Client/ProjectionManagement/ProjectionDetails.cs +++ /dev/null @@ -1,164 +0,0 @@ -namespace EventStore.Client { - /// - /// Provides the details for a projection. - /// - public sealed class ProjectionDetails { - /// - /// The CoreProcessingTime - /// - public readonly long CoreProcessingTime; - - /// - /// The projection version - /// - public readonly long Version; - - /// - /// The Epoch - /// - public readonly long Epoch; - - /// - /// The projection EffectiveName - /// - public readonly string EffectiveName; - - /// - /// The projection WritesInProgress - /// - public readonly int WritesInProgress; - - /// - /// The projection ReadsInProgress - /// - public readonly int ReadsInProgress; - - /// - /// The projection PartitionsCached - /// - public readonly int PartitionsCached; - - /// - /// The projection Status - /// - public readonly string Status; - - /// - /// The projection StateReason - /// - public readonly string StateReason; - - /// - /// The projection Name - /// - public readonly string Name; - - /// - /// The projection Mode - /// - public readonly string Mode; - - /// - /// The projection Position - /// - public readonly string Position; - - /// - /// The projection Progress - /// - public readonly float Progress; - - /// - /// LastCheckpoint - /// - public readonly string LastCheckpoint; - - /// - /// The projection EventsProcessedAfterRestart - /// - public readonly long EventsProcessedAfterRestart; - - /// - /// The projection CheckpointStatus - /// - public readonly string CheckpointStatus; - - /// - /// The projection BufferedEvents - /// - public readonly long BufferedEvents; - - /// - /// The projection WritePendingEventsBeforeCheckpoint - /// - public readonly int WritePendingEventsBeforeCheckpoint; - - /// - /// The projection WritePendingEventsAfterCheckpoint - /// - public readonly int WritePendingEventsAfterCheckpoint; - - /// - /// create a new class. - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - public ProjectionDetails( - long coreProcessingTime, - long version, - long epoch, - string effectiveName, - int writesInProgress, - int readsInProgress, - int partitionsCached, - string status, - string stateReason, - string name, - string mode, - string position, - float progress, - string lastCheckpoint, - long eventsProcessedAfterRestart, - string checkpointStatus, - long bufferedEvents, - int writePendingEventsBeforeCheckpoint, - int writePendingEventsAfterCheckpoint) { - CoreProcessingTime = coreProcessingTime; - Version = version; - Epoch = epoch; - EffectiveName = effectiveName; - WritesInProgress = writesInProgress; - ReadsInProgress = readsInProgress; - PartitionsCached = partitionsCached; - Status = status; - StateReason = stateReason; - Name = name; - Mode = mode; - Position = position; - Progress = progress; - LastCheckpoint = lastCheckpoint; - EventsProcessedAfterRestart = eventsProcessedAfterRestart; - CheckpointStatus = checkpointStatus; - BufferedEvents = bufferedEvents; - WritePendingEventsBeforeCheckpoint = writePendingEventsBeforeCheckpoint; - WritePendingEventsAfterCheckpoint = writePendingEventsAfterCheckpoint; - } - } -} diff --git a/src/EventStore.Client/Streams/ConditionalWriteResult.cs b/src/EventStore.Client/Streams/ConditionalWriteResult.cs deleted file mode 100644 index f51ef577b..000000000 --- a/src/EventStore.Client/Streams/ConditionalWriteResult.cs +++ /dev/null @@ -1,87 +0,0 @@ -using System; - -namespace EventStore.Client { - /// - /// A structure that represents the result of a conditional write. - /// - public readonly struct ConditionalWriteResult : IEquatable { - /// - /// Indicates that the stream the operation is targeting was deleted. - /// - public static readonly ConditionalWriteResult StreamDeleted = - new ConditionalWriteResult(StreamRevision.None, Position.End, ConditionalWriteStatus.StreamDeleted); - - /// - /// The correct expected version to use when writing to the stream next. - /// - public long NextExpectedVersion { get; } - - /// - /// The of the write in the transaction file. - /// - public Position LogPosition { get; } - - /// - /// The . - /// - public ConditionalWriteStatus Status { get; } - - /// - /// The correct to use when writing to the stream next. - /// - public StreamRevision NextExpectedStreamRevision { get; } - - private ConditionalWriteResult(StreamRevision nextExpectedStreamRevision, Position logPosition, - ConditionalWriteStatus status = ConditionalWriteStatus.Succeeded) { - NextExpectedStreamRevision = nextExpectedStreamRevision; - NextExpectedVersion = nextExpectedStreamRevision.ToInt64(); - LogPosition = logPosition; - Status = status; - } - - internal static ConditionalWriteResult FromWriteResult(IWriteResult writeResult) - => writeResult switch { - WrongExpectedVersionResult wrongExpectedVersion => - new ConditionalWriteResult(wrongExpectedVersion.NextExpectedStreamRevision, Position.End, - ConditionalWriteStatus.VersionMismatch), - _ => new ConditionalWriteResult(writeResult.NextExpectedStreamRevision, writeResult.LogPosition) - }; - - internal static ConditionalWriteResult FromWrongExpectedVersion(WrongExpectedVersionException ex) - => new ConditionalWriteResult(ex.ExpectedStreamRevision, Position.End, - ConditionalWriteStatus.VersionMismatch); - - /// - public bool Equals(ConditionalWriteResult other) => - NextExpectedStreamRevision == other.NextExpectedStreamRevision && - LogPosition.Equals(other.LogPosition) && - Status == other.Status; - - /// - public override bool Equals(object? obj) => obj is ConditionalWriteResult other && Equals(other); - - /// - public override int GetHashCode() => - HashCode.Hash.Combine(NextExpectedVersion).Combine(LogPosition).Combine(Status); - - /// - /// Compares left and right for equality. - /// - /// - /// - /// True if left is equal to right. - public static bool operator ==(ConditionalWriteResult left, ConditionalWriteResult right) => left.Equals(right); - - /// - /// Compares left and right for inequality. - /// - /// - /// - /// True if left is not equal to right. - public static bool operator !=(ConditionalWriteResult left, ConditionalWriteResult right) => - !left.Equals(right); - - /// - public override string ToString() => $"{Status}:{NextExpectedVersion}:{LogPosition}"; - } -} diff --git a/src/EventStore.Client/Streams/ConditionalWriteStatus.cs b/src/EventStore.Client/Streams/ConditionalWriteStatus.cs deleted file mode 100644 index 93bf82fe7..000000000 --- a/src/EventStore.Client/Streams/ConditionalWriteStatus.cs +++ /dev/null @@ -1,21 +0,0 @@ -namespace EventStore.Client { - /// - /// The reason why a conditional write fails - /// - public enum ConditionalWriteStatus { - /// - /// The write operation succeeded - /// - Succeeded = 0, - - /// - /// The expected version does not match actual stream version - /// - VersionMismatch = 1, - - /// - /// The stream has been deleted - /// - StreamDeleted = 2 - } -} diff --git a/src/EventStore.Client/Streams/DeadLine.cs b/src/EventStore.Client/Streams/DeadLine.cs deleted file mode 100644 index b4ba29b49..000000000 --- a/src/EventStore.Client/Streams/DeadLine.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; - -namespace EventStore.Client { -#pragma warning disable CS1591 - public static class DeadLine { -#pragma warning restore CS1591 - /// - /// Represents no deadline (i.e., wait infinitely) - /// - public static TimeSpan? None = null; - } -} diff --git a/src/EventStore.Client/Streams/DeleteResult.cs b/src/EventStore.Client/Streams/DeleteResult.cs deleted file mode 100644 index ea5195b1d..000000000 --- a/src/EventStore.Client/Streams/DeleteResult.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System; - -namespace EventStore.Client { - /// - /// A structure that represents the result of a delete operation. - /// - public readonly struct DeleteResult : IEquatable { - /// - public bool Equals(DeleteResult other) => LogPosition.Equals(other.LogPosition); - - /// - public override bool Equals(object? obj) => obj is DeleteResult other && Equals(other); - - /// - public override int GetHashCode() => LogPosition.GetHashCode(); - - /// - /// Compares left and right for equality. - /// - /// - /// - /// True if left is equal to right. - public static bool operator ==(DeleteResult left, DeleteResult right) => left.Equals(right); - - /// - /// Compares left and right for inequality. - /// - /// - /// - /// True if left is not equal to right. - public static bool operator !=(DeleteResult left, DeleteResult right) => !left.Equals(right); - - /// - /// The of the delete in the transaction file. - /// - public readonly Position LogPosition; - - /// - /// Constructs a new . - /// - /// - public DeleteResult(Position logPosition) { - LogPosition = logPosition; - } - } -} diff --git a/src/EventStore.Client/Streams/Direction.cs b/src/EventStore.Client/Streams/Direction.cs deleted file mode 100644 index 40e9489da..000000000 --- a/src/EventStore.Client/Streams/Direction.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace EventStore.Client { - /// - /// An enumeration that indicates the direction of the read operation. - /// - public enum Direction { - /// - /// Read backwards. - /// - Backwards, - - /// - /// Read forwards. - /// - Forwards - } -} diff --git a/src/EventStore.Client/Streams/EventStoreClient.Append.cs b/src/EventStore.Client/Streams/EventStoreClient.Append.cs deleted file mode 100644 index 9b244b3a4..000000000 --- a/src/EventStore.Client/Streams/EventStoreClient.Append.cs +++ /dev/null @@ -1,430 +0,0 @@ -using System.Collections.Concurrent; -using System.Diagnostics; -using System.Threading.Channels; -using Google.Protobuf; -using EventStore.Client.Streams; -using Grpc.Core; -using Microsoft.Extensions.Logging; -using EventStore.Client.Diagnostics; -using EventStore.Diagnostics; -using EventStore.Diagnostics.Telemetry; -using EventStore.Diagnostics.Tracing; -using static EventStore.Client.Streams.AppendResp.Types.WrongExpectedVersion; -using static EventStore.Client.Streams.Streams; - -namespace EventStore.Client { - public partial class EventStoreClient { - /// - /// Appends events asynchronously to a stream. - /// - /// The name of the stream to append events to. - /// The expected of the stream to append to. - /// An to append to the stream. - /// An to configure the operation's options. - /// - /// The for the operation. - /// The optional . - /// - public async Task AppendToStreamAsync( - string streamName, - StreamRevision expectedRevision, - IEnumerable eventData, - Action? configureOperationOptions = null, - TimeSpan? deadline = null, - UserCredentials? userCredentials = null, - CancellationToken cancellationToken = default - ) { - var options = Settings.OperationOptions.Clone(); - configureOperationOptions?.Invoke(options); - - _log.LogDebug("Append to stream - {streamName}@{expectedRevision}.", streamName, expectedRevision); - - var task = userCredentials is null && await BatchAppender.IsUsable().ConfigureAwait(false) - ? BatchAppender.Append(streamName, expectedRevision, eventData, deadline, cancellationToken) - : AppendToStreamInternal( - await GetChannelInfo(cancellationToken).ConfigureAwait(false), - new AppendReq { - Options = new() { - StreamIdentifier = streamName, - Revision = expectedRevision - } - }, - eventData, - options, - deadline, - userCredentials, - cancellationToken - ); - - return (await task.ConfigureAwait(false)).OptionallyThrowWrongExpectedVersionException(options); - } - - /// - /// Appends events asynchronously to a stream. - /// - /// The name of the stream to append events to. - /// The expected of the stream to append to. - /// An to append to the stream. - /// An to configure the operation's options. - /// - /// The for the operation. - /// The optional . - /// - public async Task AppendToStreamAsync( - string streamName, - StreamState expectedState, - IEnumerable eventData, - Action? configureOperationOptions = null, - TimeSpan? deadline = null, - UserCredentials? userCredentials = null, - CancellationToken cancellationToken = default - ) { - var operationOptions = Settings.OperationOptions.Clone(); - configureOperationOptions?.Invoke(operationOptions); - - _log.LogDebug("Append to stream - {streamName}@{expectedState}.", streamName, expectedState); - - var task = - userCredentials == null && await BatchAppender.IsUsable().ConfigureAwait(false) - ? BatchAppender.Append(streamName, expectedState, eventData, deadline, cancellationToken) - : AppendToStreamInternal( - await GetChannelInfo(cancellationToken).ConfigureAwait(false), - new AppendReq { - Options = new() { - StreamIdentifier = streamName - } - }.WithAnyStreamRevision(expectedState), - eventData, - operationOptions, - deadline, - userCredentials, - cancellationToken - ); - - return (await task.ConfigureAwait(false)).OptionallyThrowWrongExpectedVersionException(operationOptions); - } - - ValueTask AppendToStreamInternal( - ChannelInfo channelInfo, - AppendReq header, - IEnumerable eventData, - EventStoreClientOperationOptions operationOptions, - TimeSpan? deadline, - UserCredentials? userCredentials, - CancellationToken cancellationToken - ) { - return EventStoreClientDiagnostics.ActivitySource.TraceClientOperation(Operation, TracingConstants.Operations.Append, AppendTags); - - async ValueTask Operation() { - using var call = new StreamsClient(channelInfo.CallInvoker) - .Append(EventStoreCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); - - await call.RequestStream - .WriteAsync(header) - .ConfigureAwait(false); - - foreach (var e in eventData) { - var appendReq = new AppendReq { - ProposedMessage = new() { - Id = e.EventId.ToDto(), - Data = ByteString.CopyFrom(e.Data.Span), - CustomMetadata = ByteString.CopyFrom(e.Metadata.InjectTracingContext(Activity.Current)), - Metadata = { - { Constants.Metadata.Type, e.Type }, - { Constants.Metadata.ContentType, e.ContentType } - } - } - }; - - await call.RequestStream.WriteAsync(appendReq).ConfigureAwait(false); - } - - await call.RequestStream.CompleteAsync().ConfigureAwait(false); - - var response = await call.ResponseAsync.ConfigureAwait(false); - - if (response.Success is not null) - return HandleSuccessAppend(response, header); - - if (response.WrongExpectedVersion is null) - throw new InvalidOperationException("The operation completed with an unexpected result."); - - return HandleWrongExpectedRevision(response, header, operationOptions); - } - - ActivityTagsCollection AppendTags() => new ActivityTagsCollection() - .WithRequiredTag(TelemetryTags.EventStore.Stream, header.Options.StreamIdentifier.StreamName.ToStringUtf8()) - .WithGrpcChannelServerTags(Settings, channelInfo) - .WithClientSettingsServerTags(Settings) - .WithOptionalTag(TelemetryTags.Database.User, userCredentials?.Username ?? Settings.DefaultCredentials?.Username); - } - - IWriteResult HandleSuccessAppend(AppendResp response, AppendReq header) { - var currentRevision = response.Success.CurrentRevisionOptionCase == AppendResp.Types.Success.CurrentRevisionOptionOneofCase.NoStream - ? StreamRevision.None - : new StreamRevision(response.Success.CurrentRevision); - - var position = response.Success.PositionOptionCase == AppendResp.Types.Success.PositionOptionOneofCase.Position - ? new Position(response.Success.Position.CommitPosition, response.Success.Position.PreparePosition) - : default; - - _log.LogDebug( - "Append to stream succeeded - {streamName}@{logPosition}/{nextExpectedVersion}.", - header.Options.StreamIdentifier, - position, - currentRevision - ); - - return new SuccessResult(currentRevision, position); - } - - IWriteResult HandleWrongExpectedRevision( - AppendResp response, AppendReq header, EventStoreClientOperationOptions operationOptions - ) { - var actualStreamRevision = response.WrongExpectedVersion.CurrentRevisionOptionCase == CurrentRevisionOptionOneofCase.CurrentRevision - ? new StreamRevision(response.WrongExpectedVersion.CurrentRevision) - : StreamRevision.None; - - _log.LogDebug( - "Append to stream failed with Wrong Expected Version - {streamName}/{expectedRevision}/{currentRevision}", - header.Options.StreamIdentifier, - new StreamRevision(header.Options.Revision), - actualStreamRevision - ); - - if (operationOptions.ThrowOnAppendFailure) { - if (response.WrongExpectedVersion.ExpectedRevisionOptionCase == ExpectedRevisionOptionOneofCase.ExpectedRevision) { - throw new WrongExpectedVersionException( - header.Options.StreamIdentifier!, - new StreamRevision(response.WrongExpectedVersion.ExpectedRevision), - actualStreamRevision - ); - } - - var expectedStreamState = response.WrongExpectedVersion.ExpectedRevisionOptionCase switch { - ExpectedRevisionOptionOneofCase.ExpectedAny => StreamState.Any, - ExpectedRevisionOptionOneofCase.ExpectedNoStream => StreamState.NoStream, - ExpectedRevisionOptionOneofCase.ExpectedStreamExists => StreamState.StreamExists, - _ => StreamState.Any - }; - - throw new WrongExpectedVersionException( - header.Options.StreamIdentifier!, - expectedStreamState, - actualStreamRevision - ); - } - - var expectedRevision = response.WrongExpectedVersion.ExpectedRevisionOptionCase == ExpectedRevisionOptionOneofCase.ExpectedRevision - ? new StreamRevision(response.WrongExpectedVersion.ExpectedRevision) - : StreamRevision.None; - - return new WrongExpectedVersionResult( - header.Options.StreamIdentifier!, - expectedRevision, - actualStreamRevision - ); - } - - class StreamAppender : IDisposable { - readonly EventStoreClientSettings _settings; - readonly CancellationToken _cancellationToken; - readonly Action _onException; - readonly Channel _channel; - readonly ConcurrentDictionary> _pendingRequests; - readonly TaskCompletionSource _isUsable; - - ChannelInfo? _channelInfo; - AsyncDuplexStreamingCall? _call; - - public StreamAppender( - EventStoreClientSettings settings, - ValueTask channelInfoTask, - CancellationToken cancellationToken, - Action onException - ) { - _settings = settings; - _cancellationToken = cancellationToken; - _onException = onException; - _channel = Channel.CreateBounded(10000); - _pendingRequests = new ConcurrentDictionary>(); - _isUsable = new TaskCompletionSource(); - - _ = Task.Run(() => Duplex(channelInfoTask), cancellationToken); - } - - public ValueTask Append( - string streamName, StreamRevision expectedStreamPosition, - IEnumerable events, TimeSpan? timeoutAfter, - CancellationToken cancellationToken = default - ) => - AppendInternal( - BatchAppendReq.Types.Options.Create(streamName, expectedStreamPosition, timeoutAfter), - events, - cancellationToken - ); - - public ValueTask Append( - string streamName, StreamState expectedStreamState, - IEnumerable events, TimeSpan? timeoutAfter, - CancellationToken cancellationToken = default - ) => - AppendInternal( - BatchAppendReq.Types.Options.Create(streamName, expectedStreamState, timeoutAfter), - events, - cancellationToken - ); - - public Task IsUsable() => _isUsable.Task; - - ValueTask AppendInternal( - BatchAppendReq.Types.Options options, - IEnumerable events, - CancellationToken cancellationToken - ) { - return EventStoreClientDiagnostics.ActivitySource.TraceClientOperation( - Operation, - TracingConstants.Operations.Append, - AppendTags - ); - - async ValueTask Operation() { - var correlationId = Uuid.NewUuid(); - - var complete = _pendingRequests.GetOrAdd(correlationId, new TaskCompletionSource()); - - try { - foreach (var appendRequest in GetRequests(events, options, correlationId)) - await _channel.Writer.WriteAsync(appendRequest, cancellationToken).ConfigureAwait(false); - } - catch (ChannelClosedException ex) { - // channel is closed, our tcs won't necessarily get completed, don't wait for it. - throw ex.InnerException ?? ex; - } - - return await complete.Task.ConfigureAwait(false); - } - - ActivityTagsCollection AppendTags() => new ActivityTagsCollection() - .WithRequiredTag(TelemetryTags.EventStore.Stream, options.StreamIdentifier.StreamName.ToStringUtf8()) - .WithGrpcChannelServerTags(_settings, _channelInfo) - .WithClientSettingsServerTags(_settings) - .WithOptionalTag(TelemetryTags.Database.User, _settings.DefaultCredentials?.Username); - } - - async Task Duplex(ValueTask channelInfoTask) { - try { - _channelInfo = await channelInfoTask.ConfigureAwait(false); - if (!_channelInfo.ServerCapabilities.SupportsBatchAppend) { - _channel.Writer.TryComplete(new NotSupportedException("Server does not support batch append")); - _isUsable.TrySetResult(false); - return; - } - - _call = new StreamsClient(_channelInfo.CallInvoker).BatchAppend( - EventStoreCallOptions.CreateStreaming( - _settings, - userCredentials: _settings.DefaultCredentials, - cancellationToken: _cancellationToken - ) - ); - - _ = Task.Run(Send, _cancellationToken); - _ = Task.Run(Receive, _cancellationToken); - - _isUsable.TrySetResult(true); - } - catch (Exception ex) { - _isUsable.TrySetException(ex); - _onException(ex); - } - - return; - - async Task Send() { - if (_call is null) return; - - await foreach (var appendRequest in _channel.Reader.ReadAllAsync(_cancellationToken).ConfigureAwait(false)) - await _call.RequestStream.WriteAsync(appendRequest).ConfigureAwait(false); - - await _call.RequestStream.CompleteAsync().ConfigureAwait(false); - } - - async Task Receive() { - if (_call is null) return; - - try { - await foreach (var response in _call.ResponseStream.ReadAllAsync(_cancellationToken).ConfigureAwait(false)) { - if (!_pendingRequests.TryRemove(Uuid.FromDto(response.CorrelationId), out var writeResult)) { - continue; // TODO: Log? - } - - try { - writeResult.TrySetResult(response.ToWriteResult()); - } - catch (Exception ex) { - writeResult.TrySetException(ex); - } - } - } - catch (Exception ex) { - // signal that no tcs added to _pendingRequests after this point will necessarily complete - _channel.Writer.TryComplete(ex); - - // complete whatever tcs's we have - foreach (var request in _pendingRequests) - request.Value.TrySetException(ex); - - _onException(ex); - } - } - } - - IEnumerable GetRequests(IEnumerable events, BatchAppendReq.Types.Options options, Uuid correlationId) { - var batchSize = 0; - var first = true; - var correlationIdDto = correlationId.ToDto(); - var proposedMessages = new List(); - - foreach (var eventData in events) { - var proposedMessage = new BatchAppendReq.Types.ProposedMessage { - Data = ByteString.CopyFrom(eventData.Data.Span), - CustomMetadata = ByteString.CopyFrom(eventData.Metadata.InjectTracingContext(Activity.Current)), - Id = eventData.EventId.ToDto(), - Metadata = { - { Constants.Metadata.Type, eventData.Type }, - { Constants.Metadata.ContentType, eventData.ContentType } - } - }; - - proposedMessages.Add(proposedMessage); - - if ((batchSize += proposedMessage.CalculateSize()) < _settings.OperationOptions.BatchAppendSize) - continue; - - yield return new BatchAppendReq { - ProposedMessages = { proposedMessages }, - CorrelationId = correlationIdDto, - Options = first ? options : null - }; - - first = false; - proposedMessages.Clear(); - batchSize = 0; - } - - yield return new BatchAppendReq { - ProposedMessages = { proposedMessages }, - IsFinal = true, - CorrelationId = correlationIdDto, - Options = first ? options : null - }; - } - - public void Dispose() { - _channel.Writer.TryComplete(); - _call?.Dispose(); - } - } - } -} diff --git a/src/EventStore.Client/Streams/EventStoreClient.Delete.cs b/src/EventStore.Client/Streams/EventStoreClient.Delete.cs deleted file mode 100644 index dfaac235f..000000000 --- a/src/EventStore.Client/Streams/EventStoreClient.Delete.cs +++ /dev/null @@ -1,65 +0,0 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using EventStore.Client.Streams; -using Microsoft.Extensions.Logging; - -namespace EventStore.Client { - public partial class EventStoreClient { - /// - /// Deletes a stream asynchronously. - /// - /// The name of the stream to delete. - /// The expected of the stream being deleted. - /// The maximum time to wait before terminating the call. - /// The optional to perform operation with. - /// The optional . - /// - public Task DeleteAsync( - string streamName, - StreamRevision expectedRevision, - TimeSpan? deadline = null, - UserCredentials? userCredentials = null, - CancellationToken cancellationToken = default) => - DeleteInternal(new DeleteReq { - Options = new DeleteReq.Types.Options { - StreamIdentifier = streamName, - Revision = expectedRevision - } - }, deadline, userCredentials, cancellationToken); - - /// - /// Deletes a stream asynchronously. - /// - /// The name of the stream to delete. - /// The expected of the stream being deleted. - /// The maximum time to wait before terminating the call. - /// The optional to perform operation with. - /// The optional . - /// - public Task DeleteAsync( - string streamName, - StreamState expectedState, - TimeSpan? deadline = null, - UserCredentials? userCredentials = null, - CancellationToken cancellationToken = default) => DeleteInternal(new DeleteReq { - Options = new DeleteReq.Types.Options { - StreamIdentifier = streamName - } - }.WithAnyStreamRevision(expectedState), deadline, userCredentials, cancellationToken); - - private async Task DeleteInternal(DeleteReq request, - TimeSpan? deadline, - UserCredentials? userCredentials, - CancellationToken cancellationToken) { - _log.LogDebug("Deleting stream {streamName}.", request.Options.StreamIdentifier); - var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); - using var call = new Streams.Streams.StreamsClient( - channelInfo.CallInvoker).DeleteAsync(request, - EventStoreCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); - var result = await call.ResponseAsync.ConfigureAwait(false); - - return new DeleteResult(new Position(result.Position.CommitPosition, result.Position.PreparePosition)); - } - } -} diff --git a/src/EventStore.Client/Streams/EventStoreClient.Metadata.cs b/src/EventStore.Client/Streams/EventStoreClient.Metadata.cs deleted file mode 100644 index 19de629e7..000000000 --- a/src/EventStore.Client/Streams/EventStoreClient.Metadata.cs +++ /dev/null @@ -1,106 +0,0 @@ -using System; -using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; -using EventStore.Client.Streams; -using Microsoft.Extensions.Logging; - -namespace EventStore.Client { - public partial class EventStoreClient { - /// - /// Asynchronously reads the metadata for a stream - /// - /// The name of the stream to read the metadata for. - /// - /// The optional to perform operation with. - /// The optional . - /// - public async Task GetStreamMetadataAsync(string streamName, TimeSpan? deadline = null, - UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) { - _log.LogDebug("Read stream metadata for {streamName}.", streamName); - - try { - var result = ReadStreamAsync(Direction.Backwards, SystemStreams.MetastreamOf(streamName), - StreamPosition.End, 1, false, deadline, userCredentials, cancellationToken); - await foreach (var message in result.Messages.ConfigureAwait(false)) { - if (message is not StreamMessage.Event(var resolvedEvent)) { - continue; - } - - return StreamMetadataResult.Create(streamName, resolvedEvent.OriginalEventNumber, - JsonSerializer.Deserialize(resolvedEvent.Event.Data.Span, - StreamMetadataJsonSerializerOptions)); - } - - } catch (StreamNotFoundException) { - } - _log.LogWarning("Stream metadata for {streamName} not found.", streamName); - return StreamMetadataResult.None(streamName); - } - - /// - /// Asynchronously sets the metadata for a stream. - /// - /// The name of the stream to set metadata for. - /// The of the stream to append to. - /// A representing the new metadata. - /// An to configure the operation's options. - /// - /// The optional to perform operation with. - /// The optional . - /// - public Task SetStreamMetadataAsync(string streamName, StreamState expectedState, - StreamMetadata metadata, Action? configureOperationOptions = null, - TimeSpan? deadline = null, UserCredentials? userCredentials = null, - CancellationToken cancellationToken = default) { - var options = Settings.OperationOptions.Clone(); - configureOperationOptions?.Invoke(options); - - return SetStreamMetadataInternal(metadata, new AppendReq { - Options = new AppendReq.Types.Options { - StreamIdentifier = SystemStreams.MetastreamOf(streamName) - } - }.WithAnyStreamRevision(expectedState), options, deadline, userCredentials, cancellationToken); - } - - /// - /// Asynchronously sets the metadata for a stream. - /// - /// The name of the stream to set metadata for. - /// The of the stream to append to. - /// A representing the new metadata. - /// An to configure the operation's options. - /// - /// The optional to perform operation with. - /// The optional . - /// - public Task SetStreamMetadataAsync(string streamName, StreamRevision expectedRevision, - StreamMetadata metadata, Action? configureOperationOptions = null, - TimeSpan? deadline = null, UserCredentials? userCredentials = null, - CancellationToken cancellationToken = default) { - var options = Settings.OperationOptions.Clone(); - configureOperationOptions?.Invoke(options); - - return SetStreamMetadataInternal(metadata, new AppendReq { - Options = new AppendReq.Types.Options { - StreamIdentifier = SystemStreams.MetastreamOf(streamName), - Revision = expectedRevision - } - }, options, deadline, userCredentials, cancellationToken); - } - - private async Task SetStreamMetadataInternal(StreamMetadata metadata, - AppendReq appendReq, - EventStoreClientOperationOptions operationOptions, - TimeSpan? deadline, - UserCredentials? userCredentials, - CancellationToken cancellationToken) { - - var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); - return await AppendToStreamInternal(channelInfo, appendReq, new[] { - new EventData(Uuid.NewUuid(), SystemEventTypes.StreamMetadata, - JsonSerializer.SerializeToUtf8Bytes(metadata, StreamMetadataJsonSerializerOptions)), - }, operationOptions, deadline, userCredentials, cancellationToken).ConfigureAwait(false); - } - } -} diff --git a/src/EventStore.Client/Streams/EventStoreClient.Read.cs b/src/EventStore.Client/Streams/EventStoreClient.Read.cs deleted file mode 100644 index 7960622ea..000000000 --- a/src/EventStore.Client/Streams/EventStoreClient.Read.cs +++ /dev/null @@ -1,468 +0,0 @@ -using System.Threading.Channels; -using EventStore.Client.Streams; -using Grpc.Core; -using static EventStore.Client.Streams.ReadResp; -using static EventStore.Client.Streams.ReadResp.ContentOneofCase; - -namespace EventStore.Client { - public partial class EventStoreClient { - /// - /// Asynchronously reads all events. - /// - /// The in which to read. - /// The to start reading from. - /// The maximum count to read. - /// Whether to resolve LinkTo events automatically. - /// - /// The optional to perform operation with. - /// The optional . - /// - public ReadAllStreamResult ReadAllAsync( - Direction direction, - Position position, - long maxCount = long.MaxValue, - bool resolveLinkTos = false, - TimeSpan? deadline = null, - UserCredentials? userCredentials = null, - CancellationToken cancellationToken = default - ) => ReadAllAsync( - direction, - position, - eventFilter: null, - maxCount, - resolveLinkTos, - deadline, - userCredentials, - cancellationToken - ); - - /// - /// Asynchronously reads all events with filtering. - /// - /// The in which to read. - /// The to start reading from. - /// The to apply. - /// The maximum count to read. - /// Whether to resolve LinkTo events automatically. - /// - /// The optional to perform operation with. - /// The optional . - /// - public ReadAllStreamResult ReadAllAsync( - Direction direction, - Position position, - IEventFilter? eventFilter, - long maxCount = long.MaxValue, - bool resolveLinkTos = false, - TimeSpan? deadline = null, - UserCredentials? userCredentials = null, - CancellationToken cancellationToken = default - ) { - if (maxCount <= 0) - throw new ArgumentOutOfRangeException(nameof(maxCount)); - - var readReq = new ReadReq { - Options = new() { - ReadDirection = direction switch { - Direction.Backwards => ReadReq.Types.Options.Types.ReadDirection.Backwards, - Direction.Forwards => ReadReq.Types.Options.Types.ReadDirection.Forwards, - _ => throw InvalidOption(direction) - }, - ResolveLinks = resolveLinkTos, - All = new() { - Position = new() { - CommitPosition = position.CommitPosition, - PreparePosition = position.PreparePosition - } - }, - Count = (ulong)maxCount, - UuidOption = new() { Structured = new() }, - ControlOption = new() { Compatibility = 1 }, - Filter = GetFilterOptions(eventFilter) - } - }; - - return new ReadAllStreamResult( - async _ => { - var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); - return channelInfo.CallInvoker; - }, - readReq, - Settings, - deadline, - userCredentials, - cancellationToken - ); - } - - /// - /// A class that represents the result of a read operation on the $all stream. You may either enumerate this instance directly or . Do not enumerate more than once. - /// - public class ReadAllStreamResult : IAsyncEnumerable { - readonly Channel _channel; - readonly CancellationTokenSource _cts; - - int _messagesEnumerated; - - /// - /// The last of the $all stream, if available. - /// - public Position? LastPosition { get; private set; } - - /// - /// An . Do not enumerate more than once. - /// - public IAsyncEnumerable Messages { - get { - return GetMessages(); - - async IAsyncEnumerable GetMessages() { - if (Interlocked.Exchange(ref _messagesEnumerated, 1) == 1) { - throw new InvalidOperationException("Messages may only be enumerated once."); - } - - try { - await foreach (var message in _channel.Reader.ReadAllAsync(_cts.Token) - .ConfigureAwait(false)) { - if (message is StreamMessage.LastAllStreamPosition(var position)) { - LastPosition = position; - } - - yield return message; - } - } - finally { - _cts.Cancel(); - } - } - } - } - - internal ReadAllStreamResult( - Func> selectCallInvoker, ReadReq request, - EventStoreClientSettings settings, TimeSpan? deadline, UserCredentials? userCredentials, - CancellationToken cancellationToken - ) { - var callOptions = EventStoreCallOptions.CreateStreaming( - settings, - deadline, - userCredentials, - cancellationToken - ); - - _channel = Channel.CreateBounded(ReadBoundedChannelOptions); - - _cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); - var linkedCancellationToken = _cts.Token; - - if (request.Options.FilterOptionCase == ReadReq.Types.Options.FilterOptionOneofCase.None) - request.Options.NoFilter = new(); - - _ = PumpMessages(); - - return; - - async Task PumpMessages() { - try { - var callInvoker = await selectCallInvoker(linkedCancellationToken).ConfigureAwait(false); - var client = new Streams.Streams.StreamsClient(callInvoker); - using var call = client.Read(request, callOptions); - await foreach (var response in call.ResponseStream.ReadAllAsync(linkedCancellationToken) - .ConfigureAwait(false)) { - await _channel.Writer.WriteAsync( - response.ContentCase switch { - StreamNotFound => StreamMessage.NotFound.Instance, - Event => new StreamMessage.Event(ConvertToResolvedEvent(response.Event)), - FirstStreamPosition => new StreamMessage.FirstStreamPosition(new StreamPosition(response.FirstStreamPosition)), - LastStreamPosition => new StreamMessage.LastStreamPosition(new StreamPosition(response.LastStreamPosition)), - LastAllStreamPosition => new StreamMessage.LastAllStreamPosition( - new Position( - response.LastAllStreamPosition.CommitPosition, - response.LastAllStreamPosition.PreparePosition - ) - ), - _ => StreamMessage.Unknown.Instance - }, - linkedCancellationToken - ).ConfigureAwait(false); - } - - _channel.Writer.Complete(); - } - catch (Exception ex) { - _channel.Writer.TryComplete(ex); - } - } - } - - /// - public async IAsyncEnumerator GetAsyncEnumerator( - CancellationToken cancellationToken = default - ) { - try { - await foreach (var message in _channel.Reader.ReadAllAsync(cancellationToken).ConfigureAwait(false)) { - if (message is not StreamMessage.Event e) { - continue; - } - - yield return e.ResolvedEvent; - } - } - finally { - _cts.Cancel(); - } - } - } - - /// - /// Asynchronously reads all the events from a stream. - /// - /// The result could also be inspected as a means to avoid handling exceptions as the would indicate whether or not the stream is readable./> - /// - /// The in which to read. - /// The name of the stream to read. - /// The to start reading from. - /// The number of events to read from the stream. - /// Whether to resolve LinkTo events automatically. - /// - /// The optional to perform operation with. - /// The optional . - /// - public ReadStreamResult ReadStreamAsync( - Direction direction, - string streamName, - StreamPosition revision, - long maxCount = long.MaxValue, - bool resolveLinkTos = false, - TimeSpan? deadline = null, - UserCredentials? userCredentials = null, - CancellationToken cancellationToken = default - ) { - if (maxCount <= 0) - throw new ArgumentOutOfRangeException(nameof(maxCount)); - - return new ReadStreamResult( - async _ => { - var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); - return channelInfo.CallInvoker; - }, - new ReadReq { - Options = new() { - ReadDirection = direction switch { - Direction.Backwards => ReadReq.Types.Options.Types.ReadDirection.Backwards, - Direction.Forwards => ReadReq.Types.Options.Types.ReadDirection.Forwards, - _ => throw InvalidOption(direction) - }, - ResolveLinks = resolveLinkTos, - Stream = ReadReq.Types.Options.Types.StreamOptions.FromStreamNameAndRevision( - streamName, - revision - ), - Count = (ulong)maxCount, - UuidOption = new() { Structured = new() }, - NoFilter = new(), - ControlOption = new() { Compatibility = 1 } - } - }, - Settings, - deadline, - userCredentials, - cancellationToken - ); - } - - /// - /// A class that represents the result of a read operation on a stream. You may either enumerate this instance directly or . Do not enumerate more than once. - /// - public class ReadStreamResult : IAsyncEnumerable { - readonly Channel _channel; - readonly CancellationTokenSource _cts; - - int _messagesEnumerated; - - /// - /// The name of the stream. - /// - public string StreamName { get; } - - /// - /// The of the first message in this stream. Will only be filled once has been enumerated. - /// - public StreamPosition? FirstStreamPosition { get; private set; } - - /// - /// The of the last message in this stream. Will only be filled once has been enumerated. - /// - public StreamPosition? LastStreamPosition { get; private set; } - - /// - /// An . Do not enumerate more than once. - /// - public IAsyncEnumerable Messages { - get { - return GetMessages(); - - async IAsyncEnumerable GetMessages() { - if (Interlocked.Exchange(ref _messagesEnumerated, 1) == 1) { - throw new InvalidOperationException("Messages may only be enumerated once."); - } - - try { - await foreach (var message in _channel.Reader.ReadAllAsync(_cts.Token).ConfigureAwait(false)) { - switch (message) { - case StreamMessage.FirstStreamPosition(var streamPosition): - FirstStreamPosition = streamPosition; - break; - - case StreamMessage.LastStreamPosition(var lastStreamPosition): - LastStreamPosition = lastStreamPosition; - break; - - default: - break; - } - - yield return message; - } - } - finally { - _cts.Cancel(); - } - } - } - } - - /// - /// The . - /// - public Task ReadState { get; } - - internal ReadStreamResult( - Func> selectCallInvoker, ReadReq request, - EventStoreClientSettings settings, TimeSpan? deadline, UserCredentials? userCredentials, - CancellationToken cancellationToken - ) { - var callOptions = EventStoreCallOptions.CreateStreaming( - settings, - deadline, - userCredentials, - cancellationToken - ); - - _channel = Channel.CreateBounded(ReadBoundedChannelOptions); - - StreamName = request.Options.Stream.StreamIdentifier!; - - var tcs = new TaskCompletionSource(); - _cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); - var linkedCancellationToken = _cts.Token; -#pragma warning disable CS0612 - ReadState = tcs.Task; -#pragma warning restore CS0612 - - _ = PumpMessages(); - - return; - - async Task PumpMessages() { - var firstMessageRead = false; - - try { - var callInvoker = await selectCallInvoker(linkedCancellationToken).ConfigureAwait(false); - var client = new Streams.Streams.StreamsClient(callInvoker); - using var call = client.Read(request, callOptions); - - await foreach (var response in call.ResponseStream.ReadAllAsync(linkedCancellationToken) - .ConfigureAwait(false)) { - if (!firstMessageRead) { - firstMessageRead = true; - - if (response.ContentCase != StreamNotFound || request.Options.Stream == null) { - await _channel.Writer.WriteAsync(StreamMessage.Ok.Instance, linkedCancellationToken) - .ConfigureAwait(false); - - tcs.SetResult(Client.ReadState.Ok); - } - else { - tcs.SetResult(Client.ReadState.StreamNotFound); - } - } - - await _channel.Writer.WriteAsync( - response.ContentCase switch { - StreamNotFound => StreamMessage.NotFound.Instance, - Event => new StreamMessage.Event(ConvertToResolvedEvent(response.Event)), - ContentOneofCase.FirstStreamPosition => new StreamMessage.FirstStreamPosition( - new StreamPosition(response.FirstStreamPosition) - ), - ContentOneofCase.LastStreamPosition => new StreamMessage.LastStreamPosition( - new StreamPosition(response.LastStreamPosition) - ), - LastAllStreamPosition => new StreamMessage.LastAllStreamPosition( - new Position( - response.LastAllStreamPosition.CommitPosition, - response.LastAllStreamPosition.PreparePosition - ) - ), - _ => StreamMessage.Unknown.Instance - }, - linkedCancellationToken - ).ConfigureAwait(false); - } - - _channel.Writer.Complete(); - } - catch (Exception ex) { - tcs.TrySetException(ex); - _channel.Writer.TryComplete(ex); - } - } - } - - /// - public async IAsyncEnumerator GetAsyncEnumerator( - CancellationToken cancellationToken = default - ) { - try { - await foreach (var message in _channel.Reader.ReadAllAsync(cancellationToken).ConfigureAwait(false)) { - if (message is StreamMessage.NotFound) { - throw new StreamNotFoundException(StreamName); - } - - if (message is not StreamMessage.Event e) { - continue; - } - - yield return e.ResolvedEvent; - } - } - finally { - _cts.Cancel(); - } - } - } - - static ResolvedEvent ConvertToResolvedEvent(ReadResp.Types.ReadEvent readEvent) => - new ResolvedEvent( - ConvertToEventRecord(readEvent.Event)!, - ConvertToEventRecord(readEvent.Link), - readEvent.PositionCase switch { - ReadResp.Types.ReadEvent.PositionOneofCase.CommitPosition => readEvent.CommitPosition, - _ => null - } - ); - - static EventRecord? ConvertToEventRecord(ReadResp.Types.ReadEvent.Types.RecordedEvent? e) => - e == null - ? null - : new EventRecord( - e.StreamIdentifier!, - Uuid.FromDto(e.Id), - new StreamPosition(e.StreamRevision), - new Position(e.CommitPosition, e.PreparePosition), - e.Metadata, - e.Data.ToByteArray(), - e.CustomMetadata.ToByteArray() - ); - } -} \ No newline at end of file diff --git a/src/EventStore.Client/Streams/EventStoreClient.Subscriptions.cs b/src/EventStore.Client/Streams/EventStoreClient.Subscriptions.cs deleted file mode 100644 index 6f3b2f2c6..000000000 --- a/src/EventStore.Client/Streams/EventStoreClient.Subscriptions.cs +++ /dev/null @@ -1,302 +0,0 @@ -using System.Threading.Channels; -using EventStore.Client.Diagnostics; -using EventStore.Client.Streams; -using Grpc.Core; - -using static EventStore.Client.Streams.ReadResp.ContentOneofCase; - -namespace EventStore.Client { - public partial class EventStoreClient { - /// - /// Subscribes to all events. - /// - /// A (exclusive of) to start the subscription from. - /// A Task invoked and awaited when a new event is received over the subscription. - /// Whether to resolve LinkTo events automatically. - /// An action invoked if the subscription is dropped. - /// The optional to apply. - /// The optional user credentials to perform operation with. - /// The optional . - /// - public Task SubscribeToAllAsync( - FromAll start, - Func eventAppeared, - bool resolveLinkTos = false, - Action? subscriptionDropped = default, - SubscriptionFilterOptions? filterOptions = null, - UserCredentials? userCredentials = null, - CancellationToken cancellationToken = default - ) => StreamSubscription.Confirm( - SubscribeToAll(start, resolveLinkTos, filterOptions, userCredentials, cancellationToken), - eventAppeared, - subscriptionDropped, - _log, - filterOptions?.CheckpointReached, - cancellationToken: cancellationToken - ); - - /// - /// Subscribes to all events. - /// - /// A (exclusive of) to start the subscription from. - /// Whether to resolve LinkTo events automatically. - /// The optional to apply. - /// The optional user credentials to perform operation with. - /// The optional . - /// - public StreamSubscriptionResult SubscribeToAll( - FromAll start, - bool resolveLinkTos = false, - SubscriptionFilterOptions? filterOptions = null, - UserCredentials? userCredentials = null, - CancellationToken cancellationToken = default - ) => new( - async _ => await GetChannelInfo(cancellationToken).ConfigureAwait(false), - new ReadReq { - Options = new ReadReq.Types.Options { - ReadDirection = ReadReq.Types.Options.Types.ReadDirection.Forwards, - ResolveLinks = resolveLinkTos, - All = ReadReq.Types.Options.Types.AllOptions.FromSubscriptionPosition(start), - Subscription = new ReadReq.Types.Options.Types.SubscriptionOptions(), - Filter = GetFilterOptions(filterOptions)!, - UuidOption = new() { Structured = new() } - } - }, - Settings, - userCredentials, - cancellationToken - ); - - /// - /// Subscribes to a stream from a checkpoint. - /// - /// A (exclusive of) to start the subscription from. - /// The name of the stream to read events from. - /// A Task invoked and awaited when a new event is received over the subscription. - /// Whether to resolve LinkTo events automatically. - /// An action invoked if the subscription is dropped. - /// The optional user credentials to perform operation with. - /// The optional . - /// - public Task SubscribeToStreamAsync( - string streamName, - FromStream start, - Func eventAppeared, - bool resolveLinkTos = false, - Action? subscriptionDropped = default, - UserCredentials? userCredentials = null, - CancellationToken cancellationToken = default - ) => StreamSubscription.Confirm( - SubscribeToStream(streamName, start, resolveLinkTos, userCredentials, cancellationToken), - eventAppeared, - subscriptionDropped, - _log, - cancellationToken: cancellationToken - ); - - /// - /// Subscribes to a stream from a checkpoint. - /// - /// A (exclusive of) to start the subscription from. - /// The name of the stream to read events from. - /// Whether to resolve LinkTo events automatically. - /// The optional user credentials to perform operation with. - /// The optional . - /// - public StreamSubscriptionResult SubscribeToStream( - string streamName, - FromStream start, - bool resolveLinkTos = false, - UserCredentials? userCredentials = null, - CancellationToken cancellationToken = default - ) => new( - async _ => await GetChannelInfo(cancellationToken).ConfigureAwait(false), - new ReadReq { - Options = new ReadReq.Types.Options { - ReadDirection = ReadReq.Types.Options.Types.ReadDirection.Forwards, - ResolveLinks = resolveLinkTos, - Stream = ReadReq.Types.Options.Types.StreamOptions.FromSubscriptionPosition(streamName, start), - Subscription = new ReadReq.Types.Options.Types.SubscriptionOptions(), - UuidOption = new() { Structured = new() } - } - }, - Settings, - userCredentials, - cancellationToken - ); - - /// - /// A class that represents the result of a subscription operation. You may either enumerate this instance directly or . Do not enumerate more than once. - /// - public class StreamSubscriptionResult : IAsyncEnumerable, IAsyncDisposable, IDisposable { - private readonly ReadReq _request; - private readonly Channel _channel; - private readonly CancellationTokenSource _cts; - private readonly CallOptions _callOptions; - private readonly EventStoreClientSettings _settings; - private AsyncServerStreamingCall? _call; - - private int _messagesEnumerated; - - /// - /// The server-generated unique identifier for the subscription. - /// - public string? SubscriptionId { get; private set; } - - /// - /// An . Do not enumerate more than once. - /// - public IAsyncEnumerable Messages { - get { - if (Interlocked.Exchange(ref _messagesEnumerated, 1) == 1) - throw new InvalidOperationException("Messages may only be enumerated once."); - - return GetMessages(); - - async IAsyncEnumerable GetMessages() { - try { - await foreach (var message in _channel.Reader.ReadAllAsync(_cts.Token)) { - if (message is StreamMessage.SubscriptionConfirmation(var subscriptionId)) - SubscriptionId = subscriptionId; - - yield return message; - } - } - finally { -#if NET8_0_OR_GREATER - await _cts.CancelAsync().ConfigureAwait(false); -#else - _cts.Cancel(); -#endif - } - } - } - } - - internal StreamSubscriptionResult( - Func> selectChannelInfo, - ReadReq request, EventStoreClientSettings settings, UserCredentials? userCredentials, - CancellationToken cancellationToken - ) { - _request = request; - _settings = settings; - - _callOptions = EventStoreCallOptions.CreateStreaming( - settings, - userCredentials: userCredentials, - cancellationToken: cancellationToken - ); - - _channel = Channel.CreateBounded(ReadBoundedChannelOptions); - - _cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); - - if (_request.Options.FilterOptionCase == ReadReq.Types.Options.FilterOptionOneofCase.None) { - _request.Options.NoFilter = new(); - } - - _ = PumpMessages(); - - return; - - async Task PumpMessages() { - try { - var channelInfo = await selectChannelInfo(_cts.Token).ConfigureAwait(false); - var client = new Streams.Streams.StreamsClient(channelInfo.CallInvoker); - _call = client.Read(_request, _callOptions); - await foreach (var response in _call.ResponseStream.ReadAllAsync(_cts.Token).ConfigureAwait(false)) { - StreamMessage subscriptionMessage = - response.ContentCase switch { - Confirmation => new StreamMessage.SubscriptionConfirmation(response.Confirmation.SubscriptionId), - Event => new StreamMessage.Event(ConvertToResolvedEvent(response.Event)), - FirstStreamPosition => new StreamMessage.FirstStreamPosition(new StreamPosition(response.FirstStreamPosition)), - LastStreamPosition => new StreamMessage.LastStreamPosition(new StreamPosition(response.LastStreamPosition)), - LastAllStreamPosition => new StreamMessage.LastAllStreamPosition( - new Position( - response.LastAllStreamPosition.CommitPosition, - response.LastAllStreamPosition.PreparePosition - ) - ), - Checkpoint => new StreamMessage.AllStreamCheckpointReached( - new Position( - response.Checkpoint.CommitPosition, - response.Checkpoint.PreparePosition - ) - ), - CaughtUp => StreamMessage.CaughtUp.Instance, - FellBehind => StreamMessage.FellBehind.Instance, - _ => StreamMessage.Unknown.Instance - }; - - if (subscriptionMessage is StreamMessage.Event evt) - EventStoreClientDiagnostics.ActivitySource.TraceSubscriptionEvent( - SubscriptionId, - evt.ResolvedEvent, - channelInfo, - _settings, - userCredentials - ); - - await _channel.Writer - .WriteAsync(subscriptionMessage, _cts.Token) - .ConfigureAwait(false); - } - - _channel.Writer.Complete(); - } catch (Exception ex) { - _channel.Writer.TryComplete(ex); - } - } - } - - /// - public async ValueTask DisposeAsync() { - //TODO SS: Check if `CastAndDispose` is still relevant - await CastAndDispose(_cts).ConfigureAwait(false); - await CastAndDispose(_call).ConfigureAwait(false); - - return; - - static async ValueTask CastAndDispose(IDisposable? resource) { - switch (resource) { - case null: - return; - - case IAsyncDisposable disposable: - await disposable.DisposeAsync().ConfigureAwait(false); - break; - - default: - resource.Dispose(); - break; - } - } - } - - /// - public void Dispose() { - _cts.Dispose(); - _call?.Dispose(); - } - - /// - public async IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = default) { - try { - await foreach (var message in _channel.Reader.ReadAllAsync(cancellationToken).ConfigureAwait(false)) { - if (message is not StreamMessage.Event e) - continue; - - yield return e.ResolvedEvent; - } - } - finally { -#if NET8_0_OR_GREATER - await _cts.CancelAsync().ConfigureAwait(false); -#else - _cts.Cancel(); -#endif - } - } - } - } -} diff --git a/src/EventStore.Client/Streams/EventStoreClient.Tombstone.cs b/src/EventStore.Client/Streams/EventStoreClient.Tombstone.cs deleted file mode 100644 index 9fcddfeb4..000000000 --- a/src/EventStore.Client/Streams/EventStoreClient.Tombstone.cs +++ /dev/null @@ -1,63 +0,0 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using EventStore.Client.Streams; -using Microsoft.Extensions.Logging; - -namespace EventStore.Client { - public partial class EventStoreClient { - /// - /// Tombstones a stream asynchronously. Note: Tombstoned streams can never be recreated. - /// - /// The name of the stream to tombstone. - /// The expected of the stream being deleted. - /// - /// The optional to perform operation with. - /// The optional . - /// - public Task TombstoneAsync( - string streamName, - StreamRevision expectedRevision, - TimeSpan? deadline = null, - UserCredentials? userCredentials = null, - CancellationToken cancellationToken = default) => TombstoneInternal(new TombstoneReq { - Options = new TombstoneReq.Types.Options { - StreamIdentifier = streamName, - Revision = expectedRevision - } - }, deadline, userCredentials, cancellationToken); - - /// - /// Tombstones a stream asynchronously. Note: Tombstoned streams can never be recreated. - /// - /// The name of the stream to tombstone. - /// The expected of the stream being deleted. - /// - /// The optional to perform operation with. - /// The optional . - /// - public Task TombstoneAsync( - string streamName, - StreamState expectedState, - TimeSpan? deadline = null, - UserCredentials? userCredentials = null, - CancellationToken cancellationToken = default) => TombstoneInternal(new TombstoneReq { - Options = new TombstoneReq.Types.Options { - StreamIdentifier = streamName - } - }.WithAnyStreamRevision(expectedState), deadline, userCredentials, cancellationToken); - - private async Task TombstoneInternal(TombstoneReq request, TimeSpan? deadline, - UserCredentials? userCredentials, CancellationToken cancellationToken) { - _log.LogDebug("Tombstoning stream {streamName}.", request.Options.StreamIdentifier); - - var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); - using var call = new Streams.Streams.StreamsClient( - channelInfo.CallInvoker).TombstoneAsync(request, - EventStoreCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); - var result = await call.ResponseAsync.ConfigureAwait(false); - - return new DeleteResult(new Position(result.Position.CommitPosition, result.Position.PreparePosition)); - } - } -} diff --git a/src/EventStore.Client/Streams/EventStoreClient.cs b/src/EventStore.Client/Streams/EventStoreClient.cs deleted file mode 100644 index 9474ff653..000000000 --- a/src/EventStore.Client/Streams/EventStoreClient.cs +++ /dev/null @@ -1,166 +0,0 @@ -using System.Text.Json; -using System.Threading.Channels; -using Grpc.Core; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Extensions.Options; -using ReadReq = EventStore.Client.Streams.ReadReq; - -namespace EventStore.Client { - /// - /// The client used for operations on streams. - /// - public sealed partial class EventStoreClient : EventStoreClientBase { - static readonly JsonSerializerOptions StreamMetadataJsonSerializerOptions = new() { - Converters = { - StreamMetadataJsonConverter.Instance - }, - }; - - static BoundedChannelOptions ReadBoundedChannelOptions = new(1) { - SingleReader = true, - SingleWriter = true, - AllowSynchronousContinuations = true - }; - - readonly ILogger _log; - Lazy _batchAppenderLazy; - StreamAppender BatchAppender => _batchAppenderLazy.Value; - readonly CancellationTokenSource _disposedTokenSource; - - static readonly Dictionary> ExceptionMap = new() { - [Constants.Exceptions.InvalidTransaction] = ex => new InvalidTransactionException(ex.Message, ex), - [Constants.Exceptions.StreamDeleted] = ex => new StreamDeletedException( - ex.Trailers.FirstOrDefault(x => x.Key == Constants.Exceptions.StreamName)?.Value ?? "", - ex - ), - [Constants.Exceptions.WrongExpectedVersion] = ex => new WrongExpectedVersionException( - ex.Trailers.FirstOrDefault(x => x.Key == Constants.Exceptions.StreamName)?.Value!, - ex.Trailers.GetStreamRevision(Constants.Exceptions.ExpectedVersion), - ex.Trailers.GetStreamRevision(Constants.Exceptions.ActualVersion), - ex, - ex.Message - ), - [Constants.Exceptions.MaximumAppendSizeExceeded] = ex => new MaximumAppendSizeExceededException( - ex.Trailers.GetIntValueOrDefault(Constants.Exceptions.MaximumAppendSize), - ex - ), - [Constants.Exceptions.StreamNotFound] = ex => new StreamNotFoundException( - ex.Trailers.FirstOrDefault(x => x.Key == Constants.Exceptions.StreamName)?.Value!, - ex - ), - [Constants.Exceptions.MissingRequiredMetadataProperty] = ex => new RequiredMetadataPropertyMissingException( - ex.Trailers.FirstOrDefault(x => x.Key == Constants.Exceptions.MissingRequiredMetadataProperty)?.Value!, - ex - ), - }; - - /// - /// Constructs a new . This is not intended to be called directly from your code. - /// - /// - public EventStoreClient(IOptions options) : this(options.Value) { } - - /// - /// Constructs a new . - /// - /// - public EventStoreClient(EventStoreClientSettings? settings = null) : base(settings, ExceptionMap) { - _log = Settings.LoggerFactory?.CreateLogger() ?? new NullLogger(); - _disposedTokenSource = new CancellationTokenSource(); - _batchAppenderLazy = new Lazy(CreateStreamAppender); - } - - void SwapStreamAppender(Exception ex) => - Interlocked.Exchange(ref _batchAppenderLazy, new Lazy(CreateStreamAppender)).Value - .Dispose(); - - // todo: might be nice to have two different kinds of appenders and we decide which to instantiate according to the server caps. - StreamAppender CreateStreamAppender() => new StreamAppender( - Settings, - GetChannelInfo(_disposedTokenSource.Token), - _disposedTokenSource.Token, - SwapStreamAppender - ); - - static ReadReq.Types.Options.Types.FilterOptions? GetFilterOptions( - IEventFilter? filter, uint checkpointInterval = 0 - ) { - if (filter == null - || filter.Equals(StreamFilter.None) - || filter.Equals(EventTypeFilter.None)) - return null; - - var options = filter switch { - StreamFilter => new ReadReq.Types.Options.Types.FilterOptions { - StreamIdentifier = (filter.Prefixes, filter.Regex) switch { - (_, _) - when (filter.Prefixes?.Length ?? 0) == 0 && - filter.Regex != RegularFilterExpression.None => - new ReadReq.Types.Options.Types.FilterOptions.Types.Expression - { Regex = filter.Regex }, - (_, _) - when (filter.Prefixes?.Length ?? 0) != 0 && - filter.Regex == RegularFilterExpression.None => - new ReadReq.Types.Options.Types.FilterOptions.Types.Expression { - Prefix = { Array.ConvertAll(filter.Prefixes!, e => e.ToString()) } - }, - _ => throw new InvalidOperationException() - } - }, - EventTypeFilter => new ReadReq.Types.Options.Types.FilterOptions { - EventType = (filter.Prefixes, filter.Regex) switch { - (_, _) - when (filter.Prefixes?.Length ?? 0) == 0 && - filter.Regex != RegularFilterExpression.None => - new ReadReq.Types.Options.Types.FilterOptions.Types.Expression - { Regex = filter.Regex }, - (_, _) - when (filter.Prefixes?.Length ?? 0) != 0 && - filter.Regex == RegularFilterExpression.None => - new ReadReq.Types.Options.Types.FilterOptions.Types.Expression { - Prefix = { Array.ConvertAll(filter.Prefixes!, e => e.ToString()) } - }, - _ => throw new InvalidOperationException() - } - }, - _ => null - }; - - if (options == null) - return null; - - if (filter.MaxSearchWindow.HasValue) - options.Max = filter.MaxSearchWindow.Value; - else - options.Count = new Empty(); - - options.CheckpointIntervalMultiplier = checkpointInterval; - - return options; - } - - static ReadReq.Types.Options.Types.FilterOptions? GetFilterOptions( - SubscriptionFilterOptions? filterOptions - ) - => filterOptions == null ? null : GetFilterOptions(filterOptions.Filter, filterOptions.CheckpointInterval); - - /// - public override void Dispose() { - if (_batchAppenderLazy.IsValueCreated) - _batchAppenderLazy.Value.Dispose(); - - _disposedTokenSource.Dispose(); - base.Dispose(); - } - - /// - public override async ValueTask DisposeAsync() { - if (_batchAppenderLazy.IsValueCreated) - _batchAppenderLazy.Value.Dispose(); - - _disposedTokenSource.Dispose(); - await base.DisposeAsync().ConfigureAwait(false); - } - } -} \ No newline at end of file diff --git a/src/EventStore.Client/Streams/EventStoreClientExtensions.cs b/src/EventStore.Client/Streams/EventStoreClientExtensions.cs deleted file mode 100644 index 85a78dbe5..000000000 --- a/src/EventStore.Client/Streams/EventStoreClientExtensions.cs +++ /dev/null @@ -1,109 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; - -namespace EventStore.Client { - /// - /// A set of extension methods for an . - /// - public static class EventStoreClientExtensions { - private static readonly JsonSerializerOptions SystemSettingsJsonSerializerOptions = new JsonSerializerOptions { - Converters = { - SystemSettingsJsonConverter.Instance - }, - }; - - /// - /// Writes to the $settings stream. - /// - /// - /// - /// - /// - /// - /// - /// - public static Task SetSystemSettingsAsync( - this EventStoreClient client, - SystemSettings settings, - TimeSpan? deadline = null, UserCredentials? userCredentials = null, - CancellationToken cancellationToken = default) { - if (client == null) throw new ArgumentNullException(nameof(client)); - return client.AppendToStreamAsync(SystemStreams.SettingsStream, StreamState.Any, - new[] { - new EventData(Uuid.NewUuid(), SystemEventTypes.Settings, - JsonSerializer.SerializeToUtf8Bytes(settings, SystemSettingsJsonSerializerOptions)) - }, deadline: deadline, userCredentials: userCredentials, cancellationToken: cancellationToken); - } - - /// - /// Appends to a stream conditionally. - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - public static async Task ConditionalAppendToStreamAsync( - this EventStoreClient client, - string streamName, - StreamRevision expectedRevision, - IEnumerable eventData, - TimeSpan? deadline = null, - UserCredentials? userCredentials = null, - CancellationToken cancellationToken = default) { - if (client == null) { - throw new ArgumentNullException(nameof(client)); - } - try { - var result = await client.AppendToStreamAsync(streamName, expectedRevision, eventData, - options => options.ThrowOnAppendFailure = false, deadline, userCredentials, cancellationToken) - .ConfigureAwait(false); - return ConditionalWriteResult.FromWriteResult(result); - } catch (StreamDeletedException) { - return ConditionalWriteResult.StreamDeleted; - } - } - - /// - /// Appends to a stream conditionally. - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - public static async Task ConditionalAppendToStreamAsync( - this EventStoreClient client, - string streamName, - StreamState expectedState, - IEnumerable eventData, - TimeSpan? deadline = null, - UserCredentials? userCredentials = null, - CancellationToken cancellationToken = default) { - if (client == null) { - throw new ArgumentNullException(nameof(client)); - } - try { - var result = await client.AppendToStreamAsync(streamName, expectedState, eventData, - options => options.ThrowOnAppendFailure = false, deadline, userCredentials, cancellationToken) - .ConfigureAwait(false); - return ConditionalWriteResult.FromWriteResult(result); - } catch (StreamDeletedException) { - return ConditionalWriteResult.StreamDeleted; - } catch (WrongExpectedVersionException ex) { - return ConditionalWriteResult.FromWrongExpectedVersion(ex); - } - } - } -} diff --git a/src/EventStore.Client/Streams/EventStoreClientServiceCollectionExtensions.cs b/src/EventStore.Client/Streams/EventStoreClientServiceCollectionExtensions.cs deleted file mode 100644 index 5278f4a6d..000000000 --- a/src/EventStore.Client/Streams/EventStoreClientServiceCollectionExtensions.cs +++ /dev/null @@ -1,153 +0,0 @@ -// ReSharper disable CheckNamespace - -using System; -using System.Net.Http; -using EventStore.Client; -using Grpc.Core.Interceptors; -using Microsoft.Extensions.DependencyInjection.Extensions; -using Microsoft.Extensions.Logging; - -namespace Microsoft.Extensions.DependencyInjection { - /// - /// A set of extension methods for which provide support for an . - /// - public static class EventStoreClientServiceCollectionExtensions { - /// - /// Adds an to the . - /// - /// - /// - /// - /// - /// - public static IServiceCollection AddEventStoreClient(this IServiceCollection services, Uri address, - Func? createHttpMessageHandler = null) - => services.AddEventStoreClient(options => { - options.ConnectivitySettings.Address = address; - options.CreateHttpMessageHandler = createHttpMessageHandler; - }); - - /// - /// Adds an to the . - /// - /// - /// - /// - /// - /// - public static IServiceCollection AddEventStoreClient(this IServiceCollection services, - Func addressFactory, - Func? createHttpMessageHandler = null) - => services.AddEventStoreClient(provider => options => { - options.ConnectivitySettings.Address = addressFactory(provider); - options.CreateHttpMessageHandler = createHttpMessageHandler; - }); - - /// - /// Adds an to the . - /// - /// - /// - /// - /// - public static IServiceCollection AddEventStoreClient(this IServiceCollection services, - Action? configureSettings = null) => - services.AddEventStoreClient(new EventStoreClientSettings(), configureSettings); - - /// - /// Adds an to the . - /// - /// - /// - /// - /// - public static IServiceCollection AddEventStoreClient(this IServiceCollection services, - Func> configureSettings) => - services.AddEventStoreClient(new EventStoreClientSettings(), - configureSettings); - - /// - /// Adds an to the . - /// - /// - /// - /// - /// - /// - public static IServiceCollection AddEventStoreClient(this IServiceCollection services, - string connectionString, Action? configureSettings = null) { - if (services == null) { - throw new ArgumentNullException(nameof(services)); - } - - return services.AddEventStoreClient(EventStoreClientSettings.Create(connectionString), configureSettings); - } - - /// - /// Adds an to the . - /// - /// - /// - /// - /// - /// - public static IServiceCollection AddEventStoreClient(this IServiceCollection services, - Func connectionStringFactory, - Action? configureSettings = null) { - if (services == null) { - throw new ArgumentNullException(nameof(services)); - } - - return services.AddEventStoreClient(provider => EventStoreClientSettings.Create(connectionStringFactory(provider)), configureSettings); - } - - private static IServiceCollection AddEventStoreClient(this IServiceCollection services, - EventStoreClientSettings settings, - Action? configureSettings) { - configureSettings?.Invoke(settings); - - services.TryAddSingleton(provider => { - settings.LoggerFactory ??= provider.GetService(); - settings.Interceptors ??= provider.GetServices(); - - return new EventStoreClient(settings); - }); - - return services; - } - - private static IServiceCollection AddEventStoreClient(this IServiceCollection services, - Func settingsFactory, - Action? configureSettings = null) { - - services.TryAddSingleton(provider => { - var settings = settingsFactory(provider); - configureSettings?.Invoke(settings); - - settings.LoggerFactory ??= provider.GetService(); - settings.Interceptors ??= provider.GetServices(); - - return new EventStoreClient(settings); - }); - - return services; - } - - private static IServiceCollection AddEventStoreClient(this IServiceCollection services, - EventStoreClientSettings settings, - Func> configureSettingsFactory) { - - services.TryAddSingleton(provider => { - configureSettingsFactory(provider).Invoke(settings); - - settings.LoggerFactory ??= provider.GetService(); - settings.Interceptors ??= provider.GetServices(); - - return new EventStoreClient(settings); - }); - - return services; - } - } -} -// ReSharper restore CheckNamespace diff --git a/src/EventStore.Client/Streams/IWriteResult.cs b/src/EventStore.Client/Streams/IWriteResult.cs deleted file mode 100644 index 8fe9d530c..000000000 --- a/src/EventStore.Client/Streams/IWriteResult.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System; - -namespace EventStore.Client { - /// - /// An interface representing the result of a write operation. - /// - public interface IWriteResult { - /// - /// The version the stream is currently at. - /// - [Obsolete("Please use NextExpectedStreamRevision instead. This property will be removed in a future version.", - true)] - long NextExpectedVersion { get; } - /// - /// The of the in the transaction file. - /// - Position LogPosition { get; } - /// - /// The the stream is currently at. - /// - StreamRevision NextExpectedStreamRevision { get; } - } -} diff --git a/src/EventStore.Client/Streams/InvalidTransactionException.cs b/src/EventStore.Client/Streams/InvalidTransactionException.cs deleted file mode 100644 index 17933b654..000000000 --- a/src/EventStore.Client/Streams/InvalidTransactionException.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System; -using System.Runtime.Serialization; - -namespace EventStore.Client; - -/// -/// Exception thrown if there is an attempt to operate inside a -/// transaction which does not exist. -/// -public class InvalidTransactionException : Exception { - /// - /// Constructs a new . - /// - public InvalidTransactionException() { } - - /// - /// Constructs a new . - /// - public InvalidTransactionException(string message) : base(message) { } - - /// - /// Constructs a new . - /// - public InvalidTransactionException(string message, Exception innerException) : base(message, innerException) { } - - /// - /// Constructs a new . - /// - [Obsolete("Obsolete")] - protected InvalidTransactionException(SerializationInfo info, StreamingContext context) : base(info, context) { } -} \ No newline at end of file diff --git a/src/EventStore.Client/Streams/MaximumAppendSizeExceededException.cs b/src/EventStore.Client/Streams/MaximumAppendSizeExceededException.cs deleted file mode 100644 index 7c785b951..000000000 --- a/src/EventStore.Client/Streams/MaximumAppendSizeExceededException.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System; - -namespace EventStore.Client { - /// - /// Exception thrown when an append exceeds the maximum size set by the server. - /// - public class MaximumAppendSizeExceededException : Exception { - /// - /// The configured maximum append size. - /// - public uint MaxAppendSize { get; } - - /// - /// Constructs a new . - /// - /// - /// - public MaximumAppendSizeExceededException(uint maxAppendSize, Exception? innerException = null) : - base($"Maximum Append Size of {maxAppendSize} Exceeded.", innerException) { - MaxAppendSize = maxAppendSize; - } - - /// - /// Constructs a new . - /// - /// - /// - public MaximumAppendSizeExceededException(int maxAppendSize, Exception? innerException = null) : this( - (uint)maxAppendSize, innerException) { - - } - } -} diff --git a/src/EventStore.Client/Streams/ReadState.cs b/src/EventStore.Client/Streams/ReadState.cs deleted file mode 100644 index 6f0497081..000000000 --- a/src/EventStore.Client/Streams/ReadState.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace EventStore.Client { - /// - /// An enumeration representing the state of a read operation. - /// - public enum ReadState { - /// - /// The stream does not exist. - /// - StreamNotFound, - /// - /// The stream exists. - /// - Ok - } -} diff --git a/src/EventStore.Client/Streams/StreamAcl.cs b/src/EventStore.Client/Streams/StreamAcl.cs deleted file mode 100644 index 60f70a669..000000000 --- a/src/EventStore.Client/Streams/StreamAcl.cs +++ /dev/null @@ -1,107 +0,0 @@ -using System; -using System.Linq; - -namespace EventStore.Client { - /// - /// Represents an access control list for a stream - /// - public sealed class StreamAcl { - /// - /// Roles and users permitted to read the stream - /// - public string[]? ReadRoles { get; } - - /// - /// Roles and users permitted to write to the stream - /// - public string[]? WriteRoles { get; } - - /// - /// Roles and users permitted to delete the stream - /// - public string[]? DeleteRoles { get; } - - /// - /// Roles and users permitted to read stream metadata - /// - public string[]? MetaReadRoles { get; } - - /// - /// Roles and users permitted to write stream metadata - /// - public string[]? MetaWriteRoles { get; } - - - /// - /// Creates a new Stream Access Control List - /// - /// Role and user permitted to read the stream - /// Role and user permitted to write to the stream - /// Role and user permitted to delete the stream - /// Role and user permitted to read stream metadata - /// Role and user permitted to write stream metadata - public StreamAcl(string? readRole = null, string? writeRole = null, string? deleteRole = null, - string? metaReadRole = null, string? metaWriteRole = null) - : this(readRole == null ? null : new[] {readRole}, - writeRole == null ? null : new[] {writeRole}, - deleteRole == null ? null : new[] {deleteRole}, - metaReadRole == null ? null : new[] {metaReadRole}, - metaWriteRole == null ? null : new[] {metaWriteRole}) { - } - - /// - /// - /// - /// Roles and users permitted to read the stream - /// Roles and users permitted to write to the stream - /// Roles and users permitted to delete the stream - /// Roles and users permitted to read stream metadata - /// Roles and users permitted to write stream metadata - public StreamAcl(string[]? readRoles = null, string[]? writeRoles = null, string[]? deleteRoles = null, - string[]? metaReadRoles = null, string[]? metaWriteRoles = null) { - ReadRoles = readRoles; - WriteRoles = writeRoles; - DeleteRoles = deleteRoles; - MetaReadRoles = metaReadRoles; - MetaWriteRoles = metaWriteRoles; - } - - private bool Equals(StreamAcl other) => - (ReadRoles ?? Array.Empty()).SequenceEqual(other.ReadRoles ?? Array.Empty()) && - (WriteRoles ?? Array.Empty()).SequenceEqual(other.WriteRoles ?? Array.Empty()) && - (DeleteRoles ?? Array.Empty()).SequenceEqual(other.DeleteRoles ?? Array.Empty()) && - (MetaReadRoles ?? Array.Empty()).SequenceEqual(other.MetaReadRoles ?? Array.Empty()) && - (MetaWriteRoles ?? Array.Empty()).SequenceEqual(other.MetaWriteRoles ?? Array.Empty()); - - /// - public override bool Equals(object? obj) => - !ReferenceEquals(null, obj) && - (ReferenceEquals(this, obj) || obj.GetType() == GetType() && Equals((StreamAcl)obj)); - - /// - /// Compares left and right for equality. - /// - /// - /// - /// True if left is equal to right. - public static bool operator ==(StreamAcl? left, StreamAcl? right) => Equals(left, right); - - /// - /// Compares left and right for inequality. - /// - /// - /// - /// True if left is not equal to right. - public static bool operator !=(StreamAcl? left, StreamAcl? right) => !Equals(left, right); - - /// - public override int GetHashCode() => - HashCode.Hash.Combine(ReadRoles).Combine(WriteRoles).Combine(DeleteRoles).Combine(MetaReadRoles) - .Combine(MetaWriteRoles); - - - /// - public override string ToString() => - $"Read: {(ReadRoles == null ? "" : "[" + string.Join(",", ReadRoles) + "]")}, Write: {(WriteRoles == null ? "" : "[" + string.Join(",", WriteRoles) + "]")}, Delete: {(DeleteRoles == null ? "" : "[" + string.Join(",", DeleteRoles) + "]")}, MetaRead: {(MetaReadRoles == null ? "" : "[" + string.Join(",", MetaReadRoles) + "]")}, MetaWrite: {(MetaWriteRoles == null ? "" : "[" + string.Join(",", MetaWriteRoles) + "]")}"; - } -} diff --git a/src/EventStore.Client/Streams/StreamAclJsonConverter.cs b/src/EventStore.Client/Streams/StreamAclJsonConverter.cs deleted file mode 100644 index a487ddee7..000000000 --- a/src/EventStore.Client/Streams/StreamAclJsonConverter.cs +++ /dev/null @@ -1,111 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace EventStore.Client { - internal class StreamAclJsonConverter : JsonConverter { - public static readonly StreamAclJsonConverter Instance = new StreamAclJsonConverter(); - - public override StreamAcl Read(ref Utf8JsonReader reader, Type typeToConvert, - JsonSerializerOptions options) { - string[]? read = null, - write = default, - delete = default, - metaRead = default, - metaWrite = default; - if (reader.TokenType != JsonTokenType.StartObject) { - throw new InvalidOperationException(); - } - - while (reader.Read()) { - if (reader.TokenType == JsonTokenType.EndObject) { - break; - } - - if (reader.TokenType != JsonTokenType.PropertyName) { - throw new InvalidOperationException(); - } - - switch (reader.GetString()) { - case SystemMetadata.AclRead: - read = ReadRoles(ref reader); - break; - case SystemMetadata.AclWrite: - write = ReadRoles(ref reader); - break; - case SystemMetadata.AclDelete: - delete = ReadRoles(ref reader); - break; - case SystemMetadata.AclMetaRead: - metaRead = ReadRoles(ref reader); - break; - case SystemMetadata.AclMetaWrite: - metaWrite = ReadRoles(ref reader); - break; - } - } - - return new StreamAcl(read, write, delete, metaRead, metaWrite); - } - - private static string[]? ReadRoles(ref Utf8JsonReader reader) { - if (!reader.Read()) { - throw new InvalidOperationException(); - } - - if (reader.TokenType == JsonTokenType.Null) { - return null; - } - - if (reader.TokenType == JsonTokenType.String) { - return new[] {reader.GetString()!}; - } - - if (reader.TokenType != JsonTokenType.StartArray) { - throw new InvalidOperationException(); - } - - var roles = new List(); - - while (reader.Read()) { - if (reader.TokenType == JsonTokenType.EndArray) { - return roles.Count == 0 ? Array.Empty() : roles.ToArray(); - } - - if (reader.TokenType != JsonTokenType.String) { - throw new InvalidOperationException(); - } - - roles.Add(reader.GetString()!); - } - - return roles.ToArray(); - } - - public override void Write(Utf8JsonWriter writer, StreamAcl value, JsonSerializerOptions options) { - writer.WriteStartObject(); - - WriteRoles(writer, SystemMetadata.AclRead, value.ReadRoles); - WriteRoles(writer, SystemMetadata.AclWrite, value.WriteRoles); - WriteRoles(writer, SystemMetadata.AclDelete, value.DeleteRoles); - WriteRoles(writer, SystemMetadata.AclMetaRead, value.MetaReadRoles); - WriteRoles(writer, SystemMetadata.AclMetaWrite, value.MetaWriteRoles); - - writer.WriteEndObject(); - } - - private static void WriteRoles(Utf8JsonWriter writer, string name, string[]? roles) { - if (roles == null) { - return; - } - writer.WritePropertyName(name); - writer.WriteStartArray(); - foreach (var role in roles) { - writer.WriteStringValue(role); - } - - writer.WriteEndArray(); - } - } -} diff --git a/src/EventStore.Client/Streams/StreamMessage.cs b/src/EventStore.Client/Streams/StreamMessage.cs deleted file mode 100644 index 4f1a87559..000000000 --- a/src/EventStore.Client/Streams/StreamMessage.cs +++ /dev/null @@ -1,83 +0,0 @@ -namespace EventStore.Client { - /// - /// The base record of all stream messages. - /// - public abstract record StreamMessage { - /// - /// A that represents a . - /// - /// The . - public record Event(ResolvedEvent ResolvedEvent) : StreamMessage; - - /// - /// A representing a stream that was not found. - /// - public record NotFound : StreamMessage { - internal static readonly NotFound Instance = new(); - } - - /// - /// A representing a successful read operation. - /// - public record Ok : StreamMessage { - internal static readonly Ok Instance = new(); - }; - - /// - /// A indicating the first position of a stream. - /// - /// The . - public record FirstStreamPosition(StreamPosition StreamPosition) : StreamMessage; - - /// - /// A indicating the last position of a stream. - /// - /// The . - public record LastStreamPosition(StreamPosition StreamPosition) : StreamMessage; - - /// - /// A indicating the last position of the $all stream. - /// - /// The . - public record LastAllStreamPosition(Position Position) : StreamMessage; - - /// - /// A indicating that the subscription is ready to send additional messages. - /// - /// The unique identifier of the subscription. - public record SubscriptionConfirmation(string SubscriptionId) : StreamMessage; - - /// - /// A indicating that a checkpoint has been reached. - /// - /// The . - public record AllStreamCheckpointReached(Position Position) : StreamMessage; - - /// - /// A indicating that a checkpoint has been reached. - /// - /// The . - public record StreamCheckpointReached(StreamPosition StreamPosition) : StreamMessage; - - /// - /// A indicating that the subscription is live. - /// - public record CaughtUp : StreamMessage { - internal static readonly CaughtUp Instance = new(); - } - - /// - /// A indicating that the subscription has switched to catch up mode. - /// - public record FellBehind : StreamMessage { - internal static readonly FellBehind Instance = new(); - } - - /// - /// A that could not be identified, usually indicating a lower client compatibility level than the server supports. - /// - public record Unknown : StreamMessage { - internal static readonly Unknown Instance = new(); - } - } -} diff --git a/src/EventStore.Client/Streams/StreamMetadata.cs b/src/EventStore.Client/Streams/StreamMetadata.cs deleted file mode 100644 index 2b53dd977..000000000 --- a/src/EventStore.Client/Streams/StreamMetadata.cs +++ /dev/null @@ -1,111 +0,0 @@ -using System; -using System.Text.Json; - -namespace EventStore.Client { - /// - /// A structure representing a stream's custom metadata with strongly typed properties - /// for system values and a dictionary-like interface for custom values. - /// - public readonly struct StreamMetadata : IEquatable { - /// - /// The optional maximum age of events allowed in the stream. - /// - public TimeSpan? MaxAge { get; } - - /// - /// The optional from which previous events can be scavenged. - /// This is used to implement soft-deletion of streams. - /// - - public StreamPosition? TruncateBefore { get; } - - /// - /// The optional amount of time for which the stream head is cacheable. - /// - public TimeSpan? CacheControl { get; } - - /// - /// The optional for the stream. - /// - public StreamAcl? Acl { get; } - - /// - /// The optional maximum number of events allowed in the stream. - /// - public int? MaxCount { get; } - - /// - /// The optional of user provided metadata. - /// - public JsonDocument? CustomMetadata { get; } - - /// - /// Constructs a new . - /// - /// - /// - /// - /// - /// - /// - /// - public StreamMetadata( - int? maxCount = null, - TimeSpan? maxAge = null, - StreamPosition? truncateBefore = null, - TimeSpan? cacheControl = null, - StreamAcl? acl = null, - JsonDocument? customMetadata = null) : this() { - if (maxCount <= 0) { - throw new ArgumentOutOfRangeException(nameof(maxCount)); - } - - if (maxAge <= TimeSpan.Zero) { - throw new ArgumentOutOfRangeException(nameof(maxAge)); - } - - if (cacheControl <= TimeSpan.Zero) { - throw new ArgumentOutOfRangeException(nameof(cacheControl)); - } - - MaxAge = maxAge; - TruncateBefore = truncateBefore; - CacheControl = cacheControl; - Acl = acl; - MaxCount = maxCount; - CustomMetadata = customMetadata ?? JsonDocument.Parse("{}"); - } - - /// - public bool Equals(StreamMetadata other) => Nullable.Equals(MaxAge, other.MaxAge) && - Nullable.Equals(TruncateBefore, other.TruncateBefore) && - Nullable.Equals(CacheControl, other.CacheControl) && - Equals(Acl, other.Acl) && MaxCount == other.MaxCount && - string.Equals( - CustomMetadata?.RootElement.GetRawText(), - other.CustomMetadata?.RootElement.GetRawText()); - - /// - public override bool Equals(object? obj) => obj is StreamMetadata other && other.Equals(this); - - /// - public override int GetHashCode() => HashCode.Hash.Combine(MaxAge).Combine(TruncateBefore).Combine(CacheControl) - .Combine(Acl?.GetHashCode()).Combine(MaxCount); - - /// - /// Compares left and right for equality. - /// - /// - /// - /// True if left is equal to right. - public static bool operator ==(StreamMetadata left, StreamMetadata right) => Equals(left, right); - - /// - /// Compares left and right for inequality. - /// - /// - /// - /// True if left is not equal to right. - public static bool operator !=(StreamMetadata left, StreamMetadata right) => !Equals(left, right); - } -} diff --git a/src/EventStore.Client/Streams/StreamMetadataJsonConverter.cs b/src/EventStore.Client/Streams/StreamMetadataJsonConverter.cs deleted file mode 100644 index 68757a27b..000000000 --- a/src/EventStore.Client/Streams/StreamMetadataJsonConverter.cs +++ /dev/null @@ -1,145 +0,0 @@ -using System; -using System.IO; -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace EventStore.Client { - internal class StreamMetadataJsonConverter : JsonConverter { - public static readonly StreamMetadataJsonConverter Instance = new StreamMetadataJsonConverter(); - - public override StreamMetadata Read(ref Utf8JsonReader reader, Type typeToConvert, - JsonSerializerOptions options) { - int? maxCount = null; - TimeSpan? maxAge = null, cacheControl = null; - StreamPosition? truncateBefore = null; - StreamAcl? acl = null; - using var stream = new MemoryStream(); - using var customMetadataWriter = new Utf8JsonWriter(stream); - - if (reader.TokenType != JsonTokenType.StartObject) { - throw new InvalidOperationException(); - } - - customMetadataWriter.WriteStartObject(); - - while (reader.Read()) { - if (reader.TokenType == JsonTokenType.EndObject) { - break; - } - - if (reader.TokenType != JsonTokenType.PropertyName) { - throw new InvalidOperationException(); - } - - switch (reader.GetString()) { - case SystemMetadata.MaxCount: - if (!reader.Read()) { - throw new InvalidOperationException(); - } - - maxCount = reader.GetInt32(); - break; - case SystemMetadata.MaxAge: - if (!reader.Read()) { - throw new InvalidOperationException(); - } - - var int64 = reader.GetInt64(); - maxAge = TimeSpan.FromSeconds(int64); - break; - case SystemMetadata.CacheControl: - if (!reader.Read()) { - throw new InvalidOperationException(); - } - - cacheControl = TimeSpan.FromSeconds(reader.GetInt64()); - break; - case SystemMetadata.TruncateBefore: - if (!reader.Read()) { - throw new InvalidOperationException(); - } - - var value = reader.GetInt64(); - truncateBefore = value == long.MaxValue - ? StreamPosition.End - : StreamPosition.FromInt64(value); - break; - case SystemMetadata.Acl: - if (!reader.Read()) { - throw new InvalidOperationException(); - } - - acl = StreamAclJsonConverter.Instance.Read(ref reader, typeof(StreamAcl), options); - break; - default: - customMetadataWriter.WritePropertyName(reader.GetString()!); - reader.Read(); - switch (reader.TokenType) { - case JsonTokenType.Comment: - customMetadataWriter.WriteCommentValue(reader.GetComment()); - break; - case JsonTokenType.String: - customMetadataWriter.WriteStringValue(reader.GetString()); - break; - case JsonTokenType.Number: - customMetadataWriter.WriteNumberValue(reader.GetDouble()); - break; - case JsonTokenType.True: - case JsonTokenType.False: - customMetadataWriter.WriteBooleanValue(reader.GetBoolean()); - break; - case JsonTokenType.Null: - reader.Read(); - customMetadataWriter.WriteNullValue(); - break; - default: - throw new ArgumentOutOfRangeException(); - } - - break; - } - } - - customMetadataWriter.WriteEndObject(); - customMetadataWriter.Flush(); - - stream.Position = 0; - - return new StreamMetadata(maxCount, maxAge, truncateBefore, cacheControl, acl, - JsonDocument.Parse(stream)); - } - - public override void Write(Utf8JsonWriter writer, StreamMetadata value, JsonSerializerOptions options) { - writer.WriteStartObject(); - - if (value.MaxCount.HasValue) { - writer.WriteNumber(SystemMetadata.MaxCount, value.MaxCount.Value); - } - - if (value.MaxAge.HasValue) { - writer.WriteNumber(SystemMetadata.MaxAge, (long)value.MaxAge.Value.TotalSeconds); - } - - if (value.TruncateBefore.HasValue) { - writer.WriteNumber(SystemMetadata.TruncateBefore, value.TruncateBefore.Value.ToInt64()); - } - - if (value.CacheControl.HasValue) { - writer.WriteNumber(SystemMetadata.CacheControl, (long)value.CacheControl.Value.TotalSeconds); - } - - if (value.Acl != null) { - writer.WritePropertyName(SystemMetadata.Acl); - StreamAclJsonConverter.Instance.Write(writer, value.Acl, options); - } - - if (value.CustomMetadata != null) { - foreach (var property in value.CustomMetadata.RootElement.EnumerateObject()) { - property.WriteTo(writer); - } - } - - writer.WriteEndObject(); - } - } -} diff --git a/src/EventStore.Client/Streams/StreamMetadataResult.cs b/src/EventStore.Client/Streams/StreamMetadataResult.cs deleted file mode 100644 index 080ba15ef..000000000 --- a/src/EventStore.Client/Streams/StreamMetadataResult.cs +++ /dev/null @@ -1,83 +0,0 @@ -using System; - -namespace EventStore.Client { - /// - /// Represents stream metadata as a series of properties for system - /// data (e.g., MaxAge) and a object for user metadata. - /// - public struct StreamMetadataResult : IEquatable { - /// - /// The name of the stream. - /// - public readonly string StreamName; - - /// - /// True if the stream is deleted. - /// - public readonly bool StreamDeleted; - - /// - /// A containing user-specified metadata. - /// - public readonly StreamMetadata Metadata; - - /// - /// A of the version of the metadata. - /// - public readonly StreamPosition? MetastreamRevision; - - /// - public override int GetHashCode() => - HashCode.Hash.Combine(StreamName).Combine(Metadata).Combine(MetastreamRevision); - - /// - public bool Equals(StreamMetadataResult other) => - StreamName == other.StreamName && StreamDeleted == other.StreamDeleted && - Equals(Metadata, other.Metadata) && Nullable.Equals(MetastreamRevision, other.MetastreamRevision); - - /// - public override bool Equals(object? obj) => obj is StreamMetadataResult other && Equals(other); - - /// - /// Compares left and right for equality. - /// - /// - /// - /// True if left is equal to right. - public static bool operator ==(StreamMetadataResult left, StreamMetadataResult right) => left.Equals(right); - - /// - /// Compares left and right for inequality. - /// - /// - /// - /// True if left is not equal to right. - public static bool operator !=(StreamMetadataResult left, StreamMetadataResult right) => !left.Equals(right); - - /// - /// A representing no metadata. - /// - /// - /// - public static StreamMetadataResult None(string streamName) => new StreamMetadataResult(streamName); - - /// - /// A factory method to create a new . - /// - /// - /// - /// - /// - /// - public static StreamMetadataResult Create(string streamName, StreamPosition revision, - StreamMetadata metadata) => new StreamMetadataResult(streamName, revision, metadata); - - private StreamMetadataResult(string streamName, StreamPosition? metastreamRevision = null, - StreamMetadata metadata = default, bool streamDeleted = false) { - StreamName = streamName; - StreamDeleted = streamDeleted; - Metadata = metadata; - MetastreamRevision = metastreamRevision; - } - } -} diff --git a/src/EventStore.Client/Streams/StreamSubscription.cs b/src/EventStore.Client/Streams/StreamSubscription.cs deleted file mode 100644 index f70080c17..000000000 --- a/src/EventStore.Client/Streams/StreamSubscription.cs +++ /dev/null @@ -1,165 +0,0 @@ -using Grpc.Core; -using Microsoft.Extensions.Logging; - -namespace EventStore.Client { - /// - /// A class representing a . - /// - public class StreamSubscription : IDisposable { - private readonly EventStoreClient.StreamSubscriptionResult _subscription; - private readonly IAsyncEnumerator _messages; - private readonly Func _eventAppeared; - private readonly Func _checkpointReached; - private readonly Action? _subscriptionDropped; - private readonly ILogger _log; - private readonly CancellationTokenSource _cts; - private int _subscriptionDroppedInvoked; - - /// - /// The id of the set by the server. - /// - public string SubscriptionId { get; } - - internal static async Task Confirm( - EventStoreClient.StreamSubscriptionResult subscription, - Func eventAppeared, - Action? subscriptionDropped, - ILogger log, - Func? checkpointReached = null, - CancellationToken cancellationToken = default - ) { - var messages = subscription.Messages; - - var enumerator = messages.GetAsyncEnumerator(cancellationToken); - if (!await enumerator.MoveNextAsync().ConfigureAwait(false) || - enumerator.Current is not StreamMessage.SubscriptionConfirmation(var subscriptionId)) { - throw new InvalidOperationException($"Subscription to {enumerator} could not be confirmed."); - } - - return new StreamSubscription( - subscription, - enumerator, - subscriptionId, - eventAppeared, - subscriptionDropped, - log, - checkpointReached, - cancellationToken - ); - } - - private StreamSubscription( - EventStoreClient.StreamSubscriptionResult subscription, - IAsyncEnumerator messages, string subscriptionId, - Func eventAppeared, - Action? subscriptionDropped, - ILogger log, - Func? checkpointReached, - CancellationToken cancellationToken = default - ) { - _cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); - _subscription = subscription; - _messages = messages; - _eventAppeared = eventAppeared; - _checkpointReached = checkpointReached ?? ((_, _, _) => Task.CompletedTask); - _subscriptionDropped = subscriptionDropped; - _log = log; - _subscriptionDroppedInvoked = 0; - SubscriptionId = subscriptionId; - - _log.LogDebug("Subscription {subscriptionId} confirmed.", SubscriptionId); - - Task.Run(Subscribe, cancellationToken); - } - - private async Task Subscribe() { - using var _ = _cts; - - try { - while (await _messages.MoveNextAsync().ConfigureAwait(false)) { - var message = _messages.Current; - try { - switch (message) { - case StreamMessage.Event(var resolvedEvent): - _log.LogTrace( - "Subscription {subscriptionId} received event {streamName}@{streamRevision} {position}", - SubscriptionId, - resolvedEvent.OriginalEvent.EventStreamId, - resolvedEvent.OriginalEvent.EventNumber, - resolvedEvent.OriginalEvent.Position - ); - - await _eventAppeared(this, resolvedEvent, _cts.Token).ConfigureAwait(false); - break; - - case StreamMessage.AllStreamCheckpointReached (var position): - await _checkpointReached(this, position, _cts.Token) - .ConfigureAwait(false); - - break; - } - } catch (Exception ex) when - (ex is ObjectDisposedException or OperationCanceledException) { - if (_subscriptionDroppedInvoked != 0) { - return; - } - - _log.LogWarning( - ex, - "Subscription {subscriptionId} was dropped because cancellation was requested by another caller.", - SubscriptionId - ); - - SubscriptionDropped(SubscriptionDroppedReason.Disposed); - - return; - } catch (Exception ex) { - _log.LogError( - ex, - "Subscription {subscriptionId} was dropped because the subscriber made an error.", - SubscriptionId - ); - - SubscriptionDropped(SubscriptionDroppedReason.SubscriberError, ex); - - return; - } - } - } catch (RpcException ex) when (ex.Status.StatusCode == StatusCode.Cancelled && - ex.Status.Detail.Contains("Call canceled by the client.")) { - _log.LogInformation( - "Subscription {subscriptionId} was dropped because cancellation was requested by the client.", - SubscriptionId - ); - - SubscriptionDropped(SubscriptionDroppedReason.Disposed, ex); - } catch (Exception ex) { - if (_subscriptionDroppedInvoked == 0) { - _log.LogError( - ex, - "Subscription {subscriptionId} was dropped because an error occurred on the server.", - SubscriptionId - ); - - SubscriptionDropped(SubscriptionDroppedReason.ServerError, ex); - } - } - } - - /// - public void Dispose() => SubscriptionDropped(SubscriptionDroppedReason.Disposed); - - private void SubscriptionDropped(SubscriptionDroppedReason reason, Exception? ex = null) { - if (Interlocked.CompareExchange(ref _subscriptionDroppedInvoked, 1, 0) == 1) { - return; - } - - try { - _subscriptionDropped?.Invoke(this, reason, ex); - } finally { - _subscription.Dispose(); - _cts.Dispose(); - } - } - } -} diff --git a/src/EventStore.Client/Streams/Streams/AppendReq.cs b/src/EventStore.Client/Streams/Streams/AppendReq.cs deleted file mode 100644 index f32611530..000000000 --- a/src/EventStore.Client/Streams/Streams/AppendReq.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace EventStore.Client.Streams { - partial class AppendReq { - public AppendReq WithAnyStreamRevision(StreamState expectedState) { - if (expectedState == StreamState.Any) { - Options.Any = new Empty(); - } else if (expectedState == StreamState.NoStream) { - Options.NoStream = new Empty(); - } else if (expectedState == StreamState.StreamExists) { - Options.StreamExists = new Empty(); - } - - return this; - } - } -} diff --git a/src/EventStore.Client/Streams/Streams/BatchAppendReq.cs b/src/EventStore.Client/Streams/Streams/BatchAppendReq.cs deleted file mode 100644 index f9e1146c0..000000000 --- a/src/EventStore.Client/Streams/Streams/BatchAppendReq.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System; -using Google.Protobuf.WellKnownTypes; - -namespace EventStore.Client.Streams { - partial class BatchAppendReq { - partial class Types { - partial class Options { - public static Options Create(StreamIdentifier streamIdentifier, - StreamRevision expectedStreamRevision, TimeSpan? timeoutAfter) => new() { - StreamIdentifier = streamIdentifier, - StreamPosition = expectedStreamRevision.ToUInt64(), - Deadline21100 = Timestamp.FromDateTime(timeoutAfter.HasValue - ? DateTime.UtcNow + timeoutAfter.Value - : DateTime.SpecifyKind(DateTime.MaxValue, DateTimeKind.Utc)) - }; - public static Options Create(StreamIdentifier streamIdentifier, StreamState expectedState, - TimeSpan? timeoutAfter) => new() { - StreamIdentifier = streamIdentifier, - expectedStreamPositionCase_ = expectedState switch { - { } when expectedState == StreamState.Any => ExpectedStreamPositionOneofCase.Any, - { } when expectedState == StreamState.NoStream => ExpectedStreamPositionOneofCase.NoStream, - { } when expectedState == StreamState.StreamExists => ExpectedStreamPositionOneofCase - .StreamExists, - _ => ExpectedStreamPositionOneofCase.None - }, - expectedStreamPosition_ = new Google.Protobuf.WellKnownTypes.Empty(), - Deadline21100 = Timestamp.FromDateTime(timeoutAfter.HasValue - ? DateTime.UtcNow + timeoutAfter.Value - : DateTime.SpecifyKind(DateTime.MaxValue, DateTimeKind.Utc)) - }; - } - } - } -} diff --git a/src/EventStore.Client/Streams/Streams/BatchAppendResp.cs b/src/EventStore.Client/Streams/Streams/BatchAppendResp.cs deleted file mode 100644 index 926dcee25..000000000 --- a/src/EventStore.Client/Streams/Streams/BatchAppendResp.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System; -using Grpc.Core; -using static EventStore.Client.WrongExpectedVersion.CurrentStreamRevisionOptionOneofCase; -using static EventStore.Client.WrongExpectedVersion.ExpectedStreamPositionOptionOneofCase; - -namespace EventStore.Client.Streams { - partial class BatchAppendResp { - public IWriteResult ToWriteResult() => ResultCase switch { - ResultOneofCase.Success => new SuccessResult( - Success.CurrentRevisionOptionCase switch { - Types.Success.CurrentRevisionOptionOneofCase.CurrentRevision => - new StreamRevision(Success.CurrentRevision), - _ => StreamRevision.None - }, Success.PositionOptionCase switch { - Types.Success.PositionOptionOneofCase.Position => new Position( - Success.Position.CommitPosition, - Success.Position.PreparePosition), - _ => Position.End - }), - ResultOneofCase.Error => Error.Details switch { - { } when Error.Details.Is(WrongExpectedVersion.Descriptor) => - FromWrongExpectedVersion(StreamIdentifier, Error.Details.Unpack()), - { } when Error.Details.Is(StreamDeleted.Descriptor) => - throw new StreamDeletedException(StreamIdentifier!), - { } when Error.Details.Is(AccessDenied.Descriptor) => throw new AccessDeniedException(), - { } when Error.Details.Is(Timeout.Descriptor) => throw new RpcException( - new Status(StatusCode.DeadlineExceeded, Error.Message)), - { } when Error.Details.Is(Unknown.Descriptor) => throw new InvalidOperationException(Error.Message), - { } when Error.Details.Is(MaximumAppendSizeExceeded.Descriptor) => - throw new MaximumAppendSizeExceededException( - Error.Details.Unpack().MaxAppendSize), - { } when Error.Details.Is(BadRequest.Descriptor) => throw new InvalidOperationException(Error.Details - .Unpack().Message), - _ => throw new InvalidOperationException($"Could not recognize {Error.Message}") - }, - _ => throw new InvalidOperationException() - }; - - private static WrongExpectedVersionResult FromWrongExpectedVersion(StreamIdentifier streamIdentifier, - WrongExpectedVersion wrongExpectedVersion) => new(streamIdentifier!, - wrongExpectedVersion.ExpectedStreamPositionOptionCase switch { - ExpectedStreamPosition => wrongExpectedVersion.ExpectedStreamPosition, - _ => StreamRevision.None - }, wrongExpectedVersion.CurrentStreamRevisionOptionCase switch { - CurrentStreamRevision => wrongExpectedVersion.CurrentStreamRevision, - _ => StreamRevision.None - }); - } -} diff --git a/src/EventStore.Client/Streams/Streams/DeleteReq.cs b/src/EventStore.Client/Streams/Streams/DeleteReq.cs deleted file mode 100644 index 94600138c..000000000 --- a/src/EventStore.Client/Streams/Streams/DeleteReq.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace EventStore.Client.Streams { - partial class DeleteReq { - public DeleteReq WithAnyStreamRevision(StreamState expectedState) { - if (expectedState == StreamState.Any) { - Options.Any = new Empty(); - } else if (expectedState == StreamState.NoStream) { - Options.NoStream = new Empty(); - } else if (expectedState == StreamState.StreamExists) { - Options.StreamExists = new Empty(); - } - - return this; - } - } -} diff --git a/src/EventStore.Client/Streams/Streams/ReadReq.cs b/src/EventStore.Client/Streams/Streams/ReadReq.cs deleted file mode 100644 index ed5ca3af7..000000000 --- a/src/EventStore.Client/Streams/Streams/ReadReq.cs +++ /dev/null @@ -1,86 +0,0 @@ -using System; - -namespace EventStore.Client.Streams { - partial class ReadReq { - partial class Types { - partial class Options { - partial class Types { - partial class StreamOptions { - public static StreamOptions FromSubscriptionPosition(string streamName, - FromStream fromStream) { - if (fromStream == FromStream.End) { - return new StreamOptions { - StreamIdentifier = streamName, - End = new Empty() - }; - } - - if (fromStream == FromStream.Start) { - return new StreamOptions { - StreamIdentifier = streamName, - Start = new Empty() - }; - } - - return new StreamOptions { - StreamIdentifier = streamName, - Revision = fromStream.ToUInt64() - }; - } - public static StreamOptions FromStreamNameAndRevision( - string streamName, - StreamPosition streamRevision) { - if (streamName == null) { - throw new ArgumentNullException(nameof(streamName)); - } - - if (streamRevision == StreamPosition.End) { - return new StreamOptions { - StreamIdentifier = streamName, - End = new Empty() - }; - } - - if (streamRevision == StreamPosition.Start) { - return new StreamOptions { - StreamIdentifier = streamName, - Start = new Empty() - }; - } - - return new StreamOptions { - StreamIdentifier = streamName, - Revision = streamRevision - }; - } - } - - partial class AllOptions { - public static AllOptions FromSubscriptionPosition(FromAll position) { - if (position == FromAll.End) { - return new AllOptions { - End = new Empty() - }; - } - - if (position == FromAll.Start) { - return new AllOptions { - Start = new Empty() - }; - } - - var (c, p) = position.ToUInt64(); - - return new AllOptions { - Position = new Position { - CommitPosition = c, - PreparePosition = p - } - }; - } - } - } - } - } - } -} diff --git a/src/EventStore.Client/Streams/Streams/TombstoneReq.cs b/src/EventStore.Client/Streams/Streams/TombstoneReq.cs deleted file mode 100644 index cbe1ef0a0..000000000 --- a/src/EventStore.Client/Streams/Streams/TombstoneReq.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace EventStore.Client.Streams { - partial class TombstoneReq { - public TombstoneReq WithAnyStreamRevision(StreamState expectedState) { - if (expectedState == StreamState.Any) { - Options.Any = new Empty(); - } else if (expectedState == StreamState.NoStream) { - Options.NoStream = new Empty(); - } else if (expectedState == StreamState.StreamExists) { - Options.StreamExists = new Empty(); - } - - return this; - } - } -} diff --git a/src/EventStore.Client/Streams/SubscriptionFilterOptions.cs b/src/EventStore.Client/Streams/SubscriptionFilterOptions.cs deleted file mode 100644 index b6eb22dd0..000000000 --- a/src/EventStore.Client/Streams/SubscriptionFilterOptions.cs +++ /dev/null @@ -1,54 +0,0 @@ -namespace EventStore.Client { - /// - /// A class representing the options to use when filtering read operations. - /// - public class SubscriptionFilterOptions { - /// - /// The to apply. - /// - public IEventFilter Filter { get; } - - /// - /// Sets how often the checkpointReached callback is called. - /// - public uint CheckpointInterval { get; } - - /// - /// A Task invoked and await when a checkpoint is reached. - /// Set the checkpointInterval to define how often this method is called. - /// - public Func CheckpointReached { get; } = null!; - - /// - /// - /// - /// The to apply. - /// Sets how often the checkpointReached callback is called. - /// - /// A Task invoked and await when a checkpoint is reached. - /// Set the checkpointInterval to define how often this method is called. - /// - /// - public SubscriptionFilterOptions(IEventFilter filter, uint checkpointInterval, - Func? checkpointReached) - : this(filter, checkpointInterval) { - CheckpointReached = checkpointReached ?? ((_, __, ct) => Task.CompletedTask); - } - - /// - /// - /// - /// The to apply. - /// Sets how often the checkpointReached callback is called. - /// - public SubscriptionFilterOptions(IEventFilter filter, uint checkpointInterval = 1) { - if (checkpointInterval == 0) { - throw new ArgumentOutOfRangeException(nameof(checkpointInterval), - checkpointInterval, $"{nameof(checkpointInterval)} must be greater than 0."); - } - - Filter = filter; - CheckpointInterval = checkpointInterval; - } - } -} diff --git a/src/EventStore.Client/Streams/SuccessResult.cs b/src/EventStore.Client/Streams/SuccessResult.cs deleted file mode 100644 index 0624d6fcc..000000000 --- a/src/EventStore.Client/Streams/SuccessResult.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System; - -namespace EventStore.Client { - /// - /// An that indicates a successful append to a stream. - /// - public readonly struct SuccessResult : IWriteResult, IEquatable { - /// - public long NextExpectedVersion { get; } - - /// - public Position LogPosition { get; } - - /// - public StreamRevision NextExpectedStreamRevision { get; } - - /// - /// Constructs a new . - /// - /// - /// - public SuccessResult(StreamRevision nextExpectedStreamRevision, Position logPosition) { - NextExpectedStreamRevision = nextExpectedStreamRevision; - LogPosition = logPosition; - NextExpectedVersion = nextExpectedStreamRevision.ToInt64(); - } - - /// - public bool Equals(SuccessResult other) => - NextExpectedStreamRevision == other.NextExpectedStreamRevision && LogPosition.Equals(other.LogPosition); - - /// - public override bool Equals(object? obj) => obj is SuccessResult other && Equals(other); - - /// - /// Compares left and right for equality. - /// - /// - /// - /// True if left is equal to right. - public static bool operator ==(SuccessResult left, SuccessResult right) => left.Equals(right); - - /// - /// Compares left and right for inequality. - /// - /// - /// - /// True if left is equal not to right. - public static bool operator !=(SuccessResult left, SuccessResult right) => !left.Equals(right); - - /// - public override int GetHashCode() => HashCode.Hash.Combine(NextExpectedVersion).Combine(LogPosition); - - /// - public override string ToString() => $"{NextExpectedStreamRevision}:{LogPosition}"; - } -} diff --git a/src/EventStore.Client/Streams/SystemEventTypes.cs b/src/EventStore.Client/Streams/SystemEventTypes.cs deleted file mode 100644 index 48e585160..000000000 --- a/src/EventStore.Client/Streams/SystemEventTypes.cs +++ /dev/null @@ -1,31 +0,0 @@ -namespace EventStore.Client { - /// - ///Constants for System event types - /// - public static class SystemEventTypes { - /// - /// event type for stream deleted - /// - public const string StreamDeleted = "$streamDeleted"; - - /// - /// event type for statistics - /// - public const string StatsCollection = "$statsCollected"; - - /// - /// event type for linkTo - /// - public const string LinkTo = "$>"; - - /// - /// event type for stream metadata - /// - public const string StreamMetadata = "$metadata"; - - /// - /// event type for the system settings - /// - public const string Settings = "$settings"; - } -} diff --git a/src/EventStore.Client/Streams/SystemMetadata.cs b/src/EventStore.Client/Streams/SystemMetadata.cs deleted file mode 100644 index 7cce81c90..000000000 --- a/src/EventStore.Client/Streams/SystemMetadata.cs +++ /dev/null @@ -1,71 +0,0 @@ -namespace EventStore.Client { - /// - ///Constants for information in stream metadata - /// - internal static class SystemMetadata { - /// - ///The definition of the MaxAge value assigned to stream metadata - ///Setting this allows all events older than the limit to be deleted - /// - public const string MaxAge = "$maxAge"; - - /// - ///The definition of the MaxCount value assigned to stream metadata - ///setting this allows all events with a sequence less than current -maxcount to be deleted - /// - public const string MaxCount = "$maxCount"; - - /// - ///The definition of the Truncate Before value assigned to stream metadata - ///setting this allows all events prior to the integer value to be deleted - /// - public const string TruncateBefore = "$tb"; - - /// - /// Sets the cache control in seconds for the head of the stream. - /// - public const string CacheControl = "$cacheControl"; - - - /// - /// The acl definition in metadata - /// - public const string Acl = "$acl"; - - /// - /// to read from a stream - /// - public const string AclRead = "$r"; - - /// - /// to write to a stream - /// - public const string AclWrite = "$w"; - - /// - /// to delete a stream - /// - public const string AclDelete = "$d"; - - /// - /// to read metadata - /// - public const string AclMetaRead = "$mr"; - - /// - /// to write metadata - /// - public const string AclMetaWrite = "$mw"; - - - /// - /// The user default acl stream - /// - public const string UserStreamAcl = "$userStreamAcl"; - - /// - /// the system stream defaults acl stream - /// - public const string SystemStreamAcl = "$systemStreamAcl"; - } -} diff --git a/src/EventStore.Client/Streams/SystemSettings.cs b/src/EventStore.Client/Streams/SystemSettings.cs deleted file mode 100644 index 37ccd661e..000000000 --- a/src/EventStore.Client/Streams/SystemSettings.cs +++ /dev/null @@ -1,53 +0,0 @@ -namespace EventStore.Client { - /// - /// A class representing default access control lists. - /// - public sealed class SystemSettings { - /// - /// Default access control list for new user streams. - /// - public StreamAcl? UserStreamAcl { get; } - - /// - /// Default access control list for new system streams. - /// - public StreamAcl? SystemStreamAcl { get; } - - /// - /// Constructs a new . - /// - /// - /// - public SystemSettings(StreamAcl? userStreamAcl = null, StreamAcl? systemStreamAcl = null) { - UserStreamAcl = userStreamAcl; - SystemStreamAcl = systemStreamAcl; - } - - private bool Equals(SystemSettings other) - => Equals(UserStreamAcl, other.UserStreamAcl) && Equals(SystemStreamAcl, other.SystemStreamAcl); - - /// - public override bool Equals(object? obj) - => ReferenceEquals(this, obj) || obj is SystemSettings other && Equals(other); - - /// - /// Compares left and right for equality. - /// - /// - /// - /// True if left is equal to right. - public static bool operator ==(SystemSettings? left, SystemSettings? right) => Equals(left, right); - - /// - /// Compares left and right for inequality. - /// - /// - /// - /// True if left is not equal to right. - public static bool operator !=(SystemSettings? left, SystemSettings? right) => !Equals(left, right); - - /// - public override int GetHashCode() => HashCode.Hash.Combine(UserStreamAcl?.GetHashCode()) - .Combine(SystemStreamAcl?.GetHashCode()); - } -} diff --git a/src/EventStore.Client/Streams/SystemSettingsJsonConverter.cs b/src/EventStore.Client/Streams/SystemSettingsJsonConverter.cs deleted file mode 100644 index 03d7e7b9c..000000000 --- a/src/EventStore.Client/Streams/SystemSettingsJsonConverter.cs +++ /dev/null @@ -1,62 +0,0 @@ -using System; -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace EventStore.Client { - internal class SystemSettingsJsonConverter : JsonConverter { - public static readonly SystemSettingsJsonConverter Instance = new SystemSettingsJsonConverter(); - - public override SystemSettings Read(ref Utf8JsonReader reader, Type typeToConvert, - JsonSerializerOptions options) { - if (reader.TokenType != JsonTokenType.StartObject) { - throw new InvalidOperationException(); - } - - StreamAcl? system = null, user = null; - - while (reader.Read()) { - if (reader.TokenType == JsonTokenType.EndObject) { - break; - } - - if (reader.TokenType != JsonTokenType.PropertyName) { - throw new InvalidOperationException(); - } - - switch (reader.GetString()) { - case SystemMetadata.SystemStreamAcl: - if (!reader.Read()) { - throw new InvalidOperationException(); - } - - system = StreamAclJsonConverter.Instance.Read(ref reader, typeof(StreamAcl), options); - break; - case SystemMetadata.UserStreamAcl: - if (!reader.Read()) { - throw new InvalidOperationException(); - } - - user = StreamAclJsonConverter.Instance.Read(ref reader, typeof(StreamAcl), options); - break; - } - } - - return new SystemSettings(user, system); - } - - public override void Write(Utf8JsonWriter writer, SystemSettings value, JsonSerializerOptions options) { - writer.WriteStartObject(); - if (value.UserStreamAcl != null) { - writer.WritePropertyName(SystemMetadata.UserStreamAcl); - StreamAclJsonConverter.Instance.Write(writer, value.UserStreamAcl, options); - } - - if (value.SystemStreamAcl != null) { - writer.WritePropertyName(SystemMetadata.SystemStreamAcl); - StreamAclJsonConverter.Instance.Write(writer, value.SystemStreamAcl, options); - } - - writer.WriteEndObject(); - } - } -} diff --git a/src/EventStore.Client/Streams/WriteResultExtensions.cs b/src/EventStore.Client/Streams/WriteResultExtensions.cs deleted file mode 100644 index ed83c532e..000000000 --- a/src/EventStore.Client/Streams/WriteResultExtensions.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace EventStore.Client { - internal static class WriteResultExtensions { - public static IWriteResult OptionallyThrowWrongExpectedVersionException(this IWriteResult writeResult, - EventStoreClientOperationOptions options) => - (options.ThrowOnAppendFailure, writeResult) switch { - (true, WrongExpectedVersionResult wrongExpectedVersionResult) - => throw new WrongExpectedVersionException(wrongExpectedVersionResult.StreamName, - writeResult.NextExpectedStreamRevision, wrongExpectedVersionResult.ActualStreamRevision), - _ => writeResult - }; - } -} diff --git a/src/EventStore.Client/Streams/WrongExpectedVersionResult.cs b/src/EventStore.Client/Streams/WrongExpectedVersionResult.cs deleted file mode 100644 index 7dd4887ca..000000000 --- a/src/EventStore.Client/Streams/WrongExpectedVersionResult.cs +++ /dev/null @@ -1,58 +0,0 @@ -namespace EventStore.Client { - /// - /// An that indicates a failed append to a stream. - /// - public readonly struct WrongExpectedVersionResult : IWriteResult { - /// - /// The name of the stream. - /// - public string StreamName { get; } - - /// - public long NextExpectedVersion { get; } - - /// - /// The version the stream is at. - /// - public long ActualVersion { get; } - - /// - /// The the stream is at. - /// - public StreamRevision ActualStreamRevision { get; } - - /// - public Position LogPosition { get; } - - /// - public StreamRevision NextExpectedStreamRevision { get; } - - /// - /// Construct a new . - /// - /// - /// - public WrongExpectedVersionResult(string streamName, StreamRevision nextExpectedStreamRevision) { - StreamName = streamName; - ActualVersion = NextExpectedVersion = nextExpectedStreamRevision.ToInt64(); - ActualStreamRevision = NextExpectedStreamRevision = nextExpectedStreamRevision; - LogPosition = default; - } - - /// - /// Construct a new . - /// - /// - /// - /// - public WrongExpectedVersionResult(string streamName, StreamRevision nextExpectedStreamRevision, - StreamRevision actualStreamRevision) { - StreamName = streamName; - ActualVersion = actualStreamRevision.ToInt64(); - ActualStreamRevision = actualStreamRevision; - NextExpectedVersion = nextExpectedStreamRevision.ToInt64(); - NextExpectedStreamRevision = nextExpectedStreamRevision; - LogPosition = default; - } - } -} diff --git a/src/EventStore.Client/UserManagement/EventStoreUserManagementClient.cs b/src/EventStore.Client/UserManagement/EventStoreUserManagementClient.cs deleted file mode 100644 index 6b86e81b4..000000000 --- a/src/EventStore.Client/UserManagement/EventStoreUserManagementClient.cs +++ /dev/null @@ -1,279 +0,0 @@ -using System.Runtime.CompilerServices; -using EventStore.Client.Users; -using Grpc.Core; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; - -namespace EventStore.Client { - /// - /// The client used for operations on internal users. - /// - public sealed class EventStoreUserManagementClient : EventStoreClientBase { - private readonly ILogger _log; - - /// - /// Constructs a new . - /// - /// - public EventStoreUserManagementClient(EventStoreClientSettings? settings = null) : - base(settings, ExceptionMap) { - _log = Settings.LoggerFactory?.CreateLogger() ?? - new NullLogger(); - } - - /// - /// Creates an internal user. - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - public async Task CreateUserAsync(string loginName, string fullName, string[] groups, string password, - TimeSpan? deadline = null, UserCredentials? userCredentials = null, - CancellationToken cancellationToken = default) { - if (loginName == null) throw new ArgumentNullException(nameof(loginName)); - if (fullName == null) throw new ArgumentNullException(nameof(fullName)); - if (groups == null) throw new ArgumentNullException(nameof(groups)); - if (password == null) throw new ArgumentNullException(nameof(password)); - if (loginName == string.Empty) throw new ArgumentOutOfRangeException(nameof(loginName)); - if (fullName == string.Empty) throw new ArgumentOutOfRangeException(nameof(fullName)); - if (password == string.Empty) throw new ArgumentOutOfRangeException(nameof(password)); - - var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); - using var call = new Users.Users.UsersClient( - channelInfo.CallInvoker).CreateAsync(new CreateReq { - Options = new CreateReq.Types.Options { - LoginName = loginName, - FullName = fullName, - Password = password, - Groups = {groups} - } - }, EventStoreCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); - await call.ResponseAsync.ConfigureAwait(false); - } - - /// - /// Gets the of an internal user. - /// - /// - /// - /// - /// - /// - /// - /// - public async Task GetUserAsync(string loginName, TimeSpan? deadline = null, - UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) { - if (loginName == null) { - throw new ArgumentNullException(nameof(loginName)); - } - - if (loginName == string.Empty) { - throw new ArgumentOutOfRangeException(nameof(loginName)); - } - - var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); - using var call = new Users.Users.UsersClient( - channelInfo.CallInvoker).Details(new DetailsReq { - Options = new DetailsReq.Types.Options { - LoginName = loginName - } - }, EventStoreCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); - - await call.ResponseStream.MoveNext().ConfigureAwait(false); - var userDetails = call.ResponseStream.Current.UserDetails; - return ConvertUserDetails(userDetails); - } - - private static UserDetails ConvertUserDetails(DetailsResp.Types.UserDetails userDetails) => - new UserDetails(userDetails.LoginName, userDetails.FullName, userDetails.Groups.ToArray(), - userDetails.Disabled, userDetails.LastUpdated?.TicksSinceEpoch.FromTicksSinceEpoch()); - - /// - /// Deletes an internal user. - /// - /// - /// - /// - /// - /// - /// - /// - public async Task DeleteUserAsync(string loginName, TimeSpan? deadline = null, - UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) { - if (loginName == null) { - throw new ArgumentNullException(nameof(loginName)); - } - - if (loginName == string.Empty) { - throw new ArgumentOutOfRangeException(nameof(loginName)); - } - - var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); - var call = new Users.Users.UsersClient( - channelInfo.CallInvoker).DeleteAsync(new DeleteReq { - Options = new DeleteReq.Types.Options { - LoginName = loginName - } - }, EventStoreCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); - await call.ResponseAsync.ConfigureAwait(false); - } - - /// - /// Enables a previously disabled internal user. - /// - /// - /// - /// - /// - /// - /// - /// - public async Task EnableUserAsync(string loginName, TimeSpan? deadline = null, - UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) { - if (loginName == null) { - throw new ArgumentNullException(nameof(loginName)); - } - - if (loginName == string.Empty) { - throw new ArgumentOutOfRangeException(nameof(loginName)); - } - - var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); - using var call = new Users.Users.UsersClient( - channelInfo.CallInvoker).EnableAsync(new EnableReq { - Options = new EnableReq.Types.Options { - LoginName = loginName - } - }, EventStoreCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); - await call.ResponseAsync.ConfigureAwait(false); - } - - /// - /// Disables an internal user. - /// - /// - /// - /// - /// - /// - /// - public async Task DisableUserAsync(string loginName, TimeSpan? deadline = null, - UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) { - if (loginName == string.Empty) throw new ArgumentOutOfRangeException(nameof(loginName)); - - var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); - var call = new Users.Users.UsersClient( - channelInfo.CallInvoker).DisableAsync(new DisableReq { - Options = new DisableReq.Types.Options { - LoginName = loginName - } - }, EventStoreCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); - await call.ResponseAsync.ConfigureAwait(false); - } - - /// - /// Lists the of all internal users. - /// - /// - /// - /// - /// - public async IAsyncEnumerable ListAllAsync(TimeSpan? deadline = null, - UserCredentials? userCredentials = null, - [EnumeratorCancellation] CancellationToken cancellationToken = default) { - var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); - using var call = new Users.Users.UsersClient( - channelInfo.CallInvoker).Details(new DetailsReq(), - EventStoreCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); - - await foreach (var userDetail in call.ResponseStream - .ReadAllAsync(cancellationToken) - .Select(x => ConvertUserDetails(x.UserDetails)) - .WithCancellation(cancellationToken) - .ConfigureAwait(false)) { - yield return userDetail; - } - } - - /// - /// Changes the password of an internal user. - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - public async Task ChangePasswordAsync(string loginName, string currentPassword, string newPassword, - TimeSpan? deadline = null, UserCredentials? userCredentials = null, - CancellationToken cancellationToken = default) { - if (loginName == null) throw new ArgumentNullException(nameof(loginName)); - if (currentPassword == null) throw new ArgumentNullException(nameof(currentPassword)); - if (newPassword == null) throw new ArgumentNullException(nameof(newPassword)); - if (loginName == string.Empty) throw new ArgumentOutOfRangeException(nameof(loginName)); - if (currentPassword == string.Empty) throw new ArgumentOutOfRangeException(nameof(currentPassword)); - if (newPassword == string.Empty) throw new ArgumentOutOfRangeException(nameof(newPassword)); - - var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); - using var call = new Users.Users.UsersClient( - channelInfo.CallInvoker).ChangePasswordAsync( - new ChangePasswordReq { - Options = new ChangePasswordReq.Types.Options { - CurrentPassword = currentPassword, - NewPassword = newPassword, - LoginName = loginName - } - }, - EventStoreCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); - await call.ResponseAsync.ConfigureAwait(false); - } - - /// - /// Resets the password of an internal user. - /// - /// - /// - /// - /// - /// - /// - /// - /// - public async Task ResetPasswordAsync(string loginName, string newPassword, - TimeSpan? deadline = null, UserCredentials? userCredentials = null, - CancellationToken cancellationToken = default) { - if (loginName == null) throw new ArgumentNullException(nameof(loginName)); - if (newPassword == null) throw new ArgumentNullException(nameof(newPassword)); - if (loginName == string.Empty) throw new ArgumentOutOfRangeException(nameof(loginName)); - if (newPassword == string.Empty) throw new ArgumentOutOfRangeException(nameof(newPassword)); - - var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); - var call = new Users.Users.UsersClient( - channelInfo.CallInvoker).ResetPasswordAsync( - new ResetPasswordReq { - Options = new ResetPasswordReq.Types.Options { - NewPassword = newPassword, - LoginName = loginName - } - }, - EventStoreCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); - await call.ResponseAsync.ConfigureAwait(false); - } - - private static readonly Dictionary> ExceptionMap = - new Dictionary> { - [Constants.Exceptions.UserNotFound] = ex => new UserNotFoundException( - ex.Trailers.First(x => x.Key == Constants.Exceptions.LoginName).Value), - }; - } -} diff --git a/src/EventStore.Client/UserManagement/EventStoreUserManagementClientCollectionExtensions.cs b/src/EventStore.Client/UserManagement/EventStoreUserManagementClientCollectionExtensions.cs deleted file mode 100644 index 2b25f816c..000000000 --- a/src/EventStore.Client/UserManagement/EventStoreUserManagementClientCollectionExtensions.cs +++ /dev/null @@ -1,72 +0,0 @@ -// ReSharper disable CheckNamespace - -using System.Net.Http; -using EventStore.Client; -using Grpc.Core.Interceptors; -using Microsoft.Extensions.DependencyInjection.Extensions; -using Microsoft.Extensions.Logging; - -namespace Microsoft.Extensions.DependencyInjection { - /// - /// A set of extension methods for which provide support for an . - /// - public static class EventStoreUserManagementClientCollectionExtensions { - /// - /// Adds an to the . - /// - /// - /// - /// - /// - /// - public static IServiceCollection AddEventStoreUserManagementClient(this IServiceCollection services, - Uri address, Func? createHttpMessageHandler = null) - => services.AddEventStoreUserManagementClient(options => { - options.ConnectivitySettings.Address = address; - options.CreateHttpMessageHandler = createHttpMessageHandler; - }); - - /// - /// Adds an to the . - /// - /// - /// - /// - /// - /// - public static IServiceCollection AddEventStoreUserManagementClient(this IServiceCollection services, - string connectionString, Action? configureSettings = null) - => services.AddEventStoreUserManagementClient(EventStoreClientSettings.Create(connectionString), - configureSettings); - - - /// - /// Adds an to the . - /// - /// - /// - /// - /// - public static IServiceCollection AddEventStoreUserManagementClient(this IServiceCollection services, - Action? configureSettings = null) => - services.AddEventStoreUserManagementClient(new EventStoreClientSettings(), configureSettings); - - private static IServiceCollection AddEventStoreUserManagementClient(this IServiceCollection services, - EventStoreClientSettings settings, Action? configureSettings = null) { - configureSettings?.Invoke(settings); - if (services == null) { - throw new ArgumentNullException(nameof(services)); - } - - services.TryAddSingleton(provider => { - settings.LoggerFactory ??= provider.GetService(); - settings.Interceptors ??= provider.GetServices(); - - return new EventStoreUserManagementClient(settings); - }); - - return services; - } - } -} -// ReSharper restore CheckNamespace diff --git a/src/EventStore.Client/UserManagement/EventStoreUserManagerClientExtensions.cs b/src/EventStore.Client/UserManagement/EventStoreUserManagerClientExtensions.cs deleted file mode 100644 index a43ae43d7..000000000 --- a/src/EventStore.Client/UserManagement/EventStoreUserManagerClientExtensions.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace EventStore.Client; - -/// -/// A set of extension methods for an . -/// -public static class EventStoreUserManagerClientExtensions { - /// - /// Gets the of the internal user specified by the supplied . - /// - /// - /// - /// - /// - /// - public static Task GetCurrentUserAsync( - this EventStoreUserManagementClient users, - UserCredentials userCredentials, TimeSpan? deadline = null, CancellationToken cancellationToken = default - ) => - users.GetUserAsync( - userCredentials.Username!, deadline, userCredentials, - cancellationToken - ); -} \ No newline at end of file diff --git a/src/EventStore.Client/UserManagement/UserDetails.cs b/src/EventStore.Client/UserManagement/UserDetails.cs deleted file mode 100644 index 92c345eea..000000000 --- a/src/EventStore.Client/UserManagement/UserDetails.cs +++ /dev/null @@ -1,97 +0,0 @@ -namespace EventStore.Client; - -/// -/// Provides the details for a user. -/// -public readonly struct UserDetails : IEquatable { - /// - /// The users login name. - /// - public readonly string LoginName; - - /// - /// The full name of the user. - /// - public readonly string FullName; - - /// - /// The groups the user is a member of. - /// - public readonly string[] Groups; - - /// - /// The date/time the user was updated in UTC format. - /// - public readonly DateTimeOffset? DateLastUpdated; - - /// - /// Whether the user disable or not. - /// - public readonly bool Disabled; - - /// - /// create a new class. - /// - /// The login name of the user. - /// The users full name. - /// The groups this user is a member if. - /// Is this user disabled or not. - /// The datt/time this user was last updated in UTC format. - public UserDetails( - string loginName, string fullName, string[] groups, bool disabled, DateTimeOffset? dateLastUpdated) { - if (loginName == null) { - throw new ArgumentNullException(nameof(loginName)); - } - - if (fullName == null) { - throw new ArgumentNullException(nameof(fullName)); - } - - if (groups == null) { - throw new ArgumentNullException(nameof(groups)); - } - - LoginName = loginName; - FullName = fullName; - Groups = groups; - Disabled = disabled; - DateLastUpdated = dateLastUpdated; - } - - /// - public bool Equals(UserDetails other) => - LoginName == other.LoginName && FullName == other.FullName && Groups.SequenceEqual(other.Groups) && - Nullable.Equals(DateLastUpdated, other.DateLastUpdated) && Disabled == other.Disabled; - - /// - public override bool Equals(object? obj) => obj is UserDetails other && Equals(other); - - /// - public override int GetHashCode() => HashCode.Hash.Combine(LoginName).Combine(FullName).Combine(Groups) - .Combine(Disabled).Combine(DateLastUpdated); - - /// - /// Compares left and right for equality. - /// - /// - /// - /// True if left is equal to right. - public static bool operator ==(UserDetails left, UserDetails right) => left.Equals(right); - - /// - /// Compares left and right for inequality. - /// - /// - /// - /// True if left is not equal to right. - public static bool operator !=(UserDetails left, UserDetails right) => !left.Equals(right); - - /// - public override string ToString() => - new { - Disabled, - FullName, - LoginName, - Groups = string.Join(",", Groups) - }?.ToString()!; -} \ No newline at end of file diff --git a/src/Kurrent.Client/OpenTelemetry/TracerProviderBuilderExtensions.cs b/src/Kurrent.Client/OpenTelemetry/TracerProviderBuilderExtensions.cs index dd1478b18..19885375b 100644 --- a/src/Kurrent.Client/OpenTelemetry/TracerProviderBuilderExtensions.cs +++ b/src/Kurrent.Client/OpenTelemetry/TracerProviderBuilderExtensions.cs @@ -14,6 +14,6 @@ public static class TracerProviderBuilderExtensions { /// /// being configured. /// The instance of to chain configuration. - public static TracerProviderBuilder AddEventStoreClientInstrumentation(this TracerProviderBuilder builder) => + public static TracerProviderBuilder AddKurrentClientInstrumentation(this TracerProviderBuilder builder) => builder.AddSource(KurrentClientDiagnostics.InstrumentationName); } diff --git a/test/Kurrent.Client.Tests.Common/Fixtures/KurrentFixtureOptions.cs b/test/Kurrent.Client.Tests.Common/Fixtures/KurrentFixtureOptions.cs index 51e1e4f78..db5ce022e 100644 --- a/test/Kurrent.Client.Tests.Common/Fixtures/KurrentFixtureOptions.cs +++ b/test/Kurrent.Client.Tests.Common/Fixtures/KurrentFixtureOptions.cs @@ -6,6 +6,9 @@ public record KurrentFixtureOptions( KurrentClientSettings ClientSettings, IDictionary Environment ) { + public KurrentFixtureOptions RunInMemory(bool runInMemory = true) => + this with { Environment = Environment.With(x => x["EVENTSTORE_MEM_DB"] = runInMemory.ToString()) }; + public KurrentFixtureOptions WithoutDefaultCredentials() => this with { ClientSettings = ClientSettings.With(x => x.DefaultCredentials = null) }; public KurrentFixtureOptions RunProjections(bool runProjections = true) => diff --git a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/Obsolete/SubscribeToAllConnectToExistingWithStartFromNotSetObsoleteTests.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/Obsolete/SubscribeToAllConnectToExistingWithStartFromNotSetObsoleteTests.cs new file mode 100644 index 000000000..1eb96f88a --- /dev/null +++ b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/Obsolete/SubscribeToAllConnectToExistingWithStartFromNotSetObsoleteTests.cs @@ -0,0 +1,51 @@ +using EventStore.Client; +using Kurrent.Client.Tests.TestNode; + +namespace Kurrent.Client.Tests.PersistentSubscriptions; + +[Trait("Category", "Target:PersistentSubscriptions")] +public class SubscribeToAllConnectToExistingWithStartFromNotSetObsoleteTests(ITestOutputHelper output, KurrentTemporaryFixture fixture) + : KurrentTemporaryTests(output, fixture) { + [RetryFact] + public async Task connect_to_existing_with_start_from_not_set() { + var group = Fixture.GetGroupName(); + var stream = Fixture.GetStreamName(); + + TaskCompletionSource firstNonSystemEventSource = new(); + + foreach (var @event in Fixture.CreateTestEvents(10)) + await Fixture.Streams.AppendToStreamAsync( + stream, + StreamState.Any, + new[] { + @event + } + ); + + await Fixture.Subscriptions.CreateToAllAsync( + group, + new(), + userCredentials: TestCredentials.Root + ); + + using var subscription = await Fixture.Subscriptions.SubscribeToAllAsync( + group, + async (subscription, e, r, ct) => { + if (SystemStreams.IsSystemStream(e.OriginalStreamId)) { + await subscription.Ack(e); + return; + } + + firstNonSystemEventSource.TrySetResult(e); + await subscription.Ack(e); + }, + (subscription, reason, ex) => { + if (reason != SubscriptionDroppedReason.Disposed) + firstNonSystemEventSource.TrySetException(ex!); + }, + TestCredentials.Root + ); + + await Assert.ThrowsAsync(() => firstNonSystemEventSource.Task.WithTimeout()); + } +} diff --git a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/Obsolete/SubscribeToAllConnectToExistingWithStartFromSetToEndPositionObsoleteTests.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/Obsolete/SubscribeToAllConnectToExistingWithStartFromSetToEndPositionObsoleteTests.cs new file mode 100644 index 000000000..2706e0669 --- /dev/null +++ b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/Obsolete/SubscribeToAllConnectToExistingWithStartFromSetToEndPositionObsoleteTests.cs @@ -0,0 +1,46 @@ +using EventStore.Client; +using Kurrent.Client.Tests.TestNode; + +namespace Kurrent.Client.Tests.PersistentSubscriptions; + +[Trait("Category", "Target:PersistentSubscriptions")] +public class SubscribeToAllConnectToExistingWithStartFromSetToEndPositionObsoleteTests(ITestOutputHelper output, KurrentTemporaryFixture fixture) + : KurrentTemporaryTests(output, fixture) { + [RetryFact] + public async Task connect_to_existing_with_start_from_set_to_end_position() { + var group = Fixture.GetGroupName(); + var stream = Fixture.GetStreamName(); + + TaskCompletionSource firstNonSystemEventSource = new(); + + foreach (var @event in Fixture.CreateTestEvents(10)) { + await Fixture.Streams.AppendToStreamAsync( + stream, + StreamState.Any, + [@event] + ); + } + + await Fixture.Subscriptions.CreateToAllAsync(group, new(startFrom: Position.End), userCredentials: TestCredentials.Root); + + using var subscription = await Fixture.Subscriptions.SubscribeToAllAsync( + group, + async (subscription, e, r, ct) => { + if (SystemStreams.IsSystemStream(e.OriginalStreamId)) { + await subscription.Ack(e); + return; + } + + firstNonSystemEventSource.TrySetResult(e); + await subscription.Ack(e); + }, + (subscription, reason, ex) => { + if (reason != SubscriptionDroppedReason.Disposed) + firstNonSystemEventSource.TrySetException(ex!); + }, + TestCredentials.Root + ); + + await Assert.ThrowsAsync(() => firstNonSystemEventSource.Task.WithTimeout()); + } +} diff --git a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/Obsolete/SubscribeToAllConnectWithoutReadPermissionsObsoleteTests.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/Obsolete/SubscribeToAllConnectWithoutReadPermissionsObsoleteTests.cs new file mode 100644 index 000000000..f7004b890 --- /dev/null +++ b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/Obsolete/SubscribeToAllConnectWithoutReadPermissionsObsoleteTests.cs @@ -0,0 +1,34 @@ +using EventStore.Client; +using Kurrent.Client.Tests.TestNode; + +namespace Kurrent.Client.Tests.PersistentSubscriptions; + +[Trait("Category", "Target:PersistentSubscriptions")] +public class SubscribeToAllConnectWithoutReadPermissionsObsoleteTests(ITestOutputHelper output, KurrentTemporaryFixture fixture) + : KurrentTemporaryTests(output, fixture) { + [RetryFact] + public async Task connect_to_existing_without_read_all_permissions() { + var group = Fixture.GetGroupName(); + var user = Fixture.GetUserCredentials(); + + await Fixture.Subscriptions.CreateToAllAsync(group, new(), userCredentials: TestCredentials.Root); + + await Fixture.Users.CreateUserWithRetry( + user.Username!, + user.Username!, + [], + user.Password!, + TestCredentials.Root + ); + + await Assert.ThrowsAsync( + async () => { + using var _ = await Fixture.Subscriptions.SubscribeToAllAsync( + group, + delegate { return Task.CompletedTask; }, + userCredentials: user + ); + } + ); + } +} diff --git a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/Obsolete/SubscribeToAllFilterObsoleteTests.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/Obsolete/SubscribeToAllFilterObsoleteTests.cs new file mode 100644 index 000000000..95c3aba2b --- /dev/null +++ b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/Obsolete/SubscribeToAllFilterObsoleteTests.cs @@ -0,0 +1,131 @@ +using EventStore.Client; +using Kurrent.Client.Tests.TestNode; +using Kurrent.Client.Tests; + +namespace Kurrent.Client.Tests.PersistentSubscriptions; + +[Trait("Category", "Target:PersistentSubscriptions")] +public class SubscribeToAllFilterObsoleteTests(ITestOutputHelper output, KurrentTemporaryFixture fixture) + : KurrentTemporaryTests(output, fixture) { + [RetryTheory] + [MemberData(nameof(FilterCases))] + public async Task happy_case_filtered_reads_all_existing_filtered_events(string filterName) { + var streamPrefix = $"{filterName}-{Fixture.GetStreamName()}"; + var group = Fixture.GetGroupName(); + var (getFilter, prepareEvent) = Filters.GetFilter(filterName); + var filter = getFilter(streamPrefix); + + await Fixture.Streams.AppendToStreamAsync( + Guid.NewGuid().ToString(), + StreamState.NoStream, + Fixture.CreateTestEvents(256) + ); + + await Fixture.Streams.SetStreamMetadataAsync( + SystemStreams.AllStream, + StreamState.Any, + new(acl: new(SystemRoles.All)), + userCredentials: TestCredentials.Root + ); + + var appearedEvents = new List(); + var events = Fixture.CreateTestEvents(20).Select(e => prepareEvent(streamPrefix, e)).ToArray(); + + foreach (var e in events) { + await Fixture.Streams.AppendToStreamAsync( + $"{streamPrefix}_{Guid.NewGuid():n}", + StreamState.NoStream, + [e] + ); + } + + await Fixture.Subscriptions.CreateToAllAsync( + group, + filter, + new(startFrom: Position.Start), + userCredentials: TestCredentials.Root + ); + + await using var subscription = Fixture.Subscriptions.SubscribeToAll(group, userCredentials: TestCredentials.Root); + + await subscription.Messages + .OfType() + .Take(events.Length) + .ForEachAwaitAsync( + async e => { + var (resolvedEvent, _) = e; + appearedEvents.Add(resolvedEvent.Event); + await subscription.Ack(resolvedEvent); + } + ) + .WithTimeout(); + + Assert.Equal(events.Select(x => x.EventId), appearedEvents.Select(x => x.EventId)); + } + + [RetryTheory] + [MemberData(nameof(FilterCases))] + public async Task happy_case_filtered_with_start_from_set(string filterName) { + var group = Fixture.GetGroupName(); + var streamPrefix = $"{filterName}-{Fixture.GetStreamName()}"; + var (getFilter, prepareEvent) = Filters.GetFilter(filterName); + var filter = getFilter(streamPrefix); + + var events = Fixture.CreateTestEvents(20).Select(e => prepareEvent(streamPrefix, e)).ToArray(); + var eventsToSkip = events.Take(10).ToArray(); + var eventsToCapture = events.Skip(10).ToArray(); + + IWriteResult? eventToCaptureResult = null; + + await Fixture.Streams.AppendToStreamAsync( + Guid.NewGuid().ToString(), + StreamState.NoStream, + Fixture.CreateTestEvents(256) + ); + + await Fixture.Streams.SetStreamMetadataAsync( + SystemStreams.AllStream, + StreamState.Any, + new(acl: new(SystemRoles.All)), + userCredentials: TestCredentials.Root + ); + + foreach (var e in eventsToSkip) { + await Fixture.Streams.AppendToStreamAsync( + $"{streamPrefix}_{Guid.NewGuid():n}", + StreamState.NoStream, + new[] { e } + ); + } + + foreach (var e in eventsToCapture) { + var result = await Fixture.Streams.AppendToStreamAsync( + $"{streamPrefix}_{Guid.NewGuid():n}", + StreamState.NoStream, + new[] { e } + ); + + eventToCaptureResult ??= result; + } + + await Fixture.Subscriptions.CreateToAllAsync( + group, + filter, + new(startFrom: eventToCaptureResult!.LogPosition), + userCredentials: TestCredentials.Root + ); + + await using var subscription = Fixture.Subscriptions.SubscribeToAll(group, userCredentials: TestCredentials.Root); + + var appearedEvents = await subscription.Messages.OfType() + .Take(10) + .Select(e => e.ResolvedEvent.Event) + .ToArrayAsync() + .AsTask() + .WithTimeout(); + + Assert.Equal(eventsToCapture.Select(x => x.EventId), appearedEvents.Select(x => x.EventId)); + } + + public static IEnumerable FilterCases() => Filters.All.Select(filter => new object[] { filter }); +} diff --git a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/Obsolete/SubscribeToAllGetInfoObsoleteTests.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/Obsolete/SubscribeToAllGetInfoObsoleteTests.cs new file mode 100644 index 000000000..2b8532ad4 --- /dev/null +++ b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/Obsolete/SubscribeToAllGetInfoObsoleteTests.cs @@ -0,0 +1,97 @@ +// ReSharper disable InconsistentNaming + +using EventStore.Client; +using Kurrent.Client.Tests.TestNode; + +namespace Kurrent.Client.Tests.PersistentSubscriptions; + +[Trait("Category", "Target:PersistentSubscriptions")] +public class SubscribeToAllGetInfoObsoleteTests(SubscribeToAllGetInfoObsoleteTests.CustomFixture fixture) + : IClassFixture { + static readonly PersistentSubscriptionSettings Settings = new( + resolveLinkTos: true, + startFrom: Position.Start, + extraStatistics: true, + messageTimeout: TimeSpan.FromSeconds(9), + maxRetryCount: 11, + liveBufferSize: 303, + readBatchSize: 30, + historyBufferSize: 909, + checkPointAfter: TimeSpan.FromSeconds(1), + checkPointLowerBound: 1, + checkPointUpperBound: 1, + maxSubscriberCount: 500, + consumerStrategyName: SystemConsumerStrategies.Pinned + ); + + [RetryFact] + public async Task throws_with_non_existing_subscription() { + var group = $"NonExisting-{fixture.GetGroupName()}"; + + await Assert.ThrowsAsync( + async () => await fixture.Subscriptions.GetInfoToAllAsync(group, userCredentials: TestCredentials.Root) + ); + } + + [RetryFact] + public async Task throws_with_no_credentials() { + var group = $"NonExisting-{fixture.GetGroupName()}"; + + await Assert.ThrowsAsync( + async () => + await fixture.Subscriptions.GetInfoToAllAsync(group) + ); + } + + [RetryFact] + public async Task throws_with_non_existing_user() { + var group = $"NonExisting-{fixture.GetGroupName()}"; + + await Assert.ThrowsAsync( + async () => + await fixture.Subscriptions.GetInfoToAllAsync(group, userCredentials: TestCredentials.TestBadUser) + ); + } + + [RetryFact] + public async Task returns_result_with_normal_user_credentials() { + var result = await fixture.Subscriptions.GetInfoToAllAsync(fixture.Group, userCredentials: TestCredentials.Root); + + Assert.Equal("$all", result.EventSource); + } + + public class CustomFixture : KurrentTemporaryFixture { + public string Group { get; } + + public CustomFixture() : base(x => x.WithoutDefaultCredentials()) { + Group = GetGroupName(); + + OnSetup += async () => { + await Subscriptions.CreateToAllAsync(Group, Settings, userCredentials: TestCredentials.Root); + + var counter = 0; + var tcs = new TaskCompletionSource(); + + await Subscriptions.SubscribeToAllAsync( + Group, + (s, e, r, ct) => { + counter++; + + switch (counter) { + case 1: + s.Nack(PersistentSubscriptionNakEventAction.Park, "Test", e); + break; + + case > 10: + tcs.TrySetResult(); + break; + } + + return Task.CompletedTask; + }, + userCredentials: TestCredentials.Root + ); + }; + } + }; +} diff --git a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/Obsolete/SubscribeToAllListWithIncorrectCredentialsObsoleteTests.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/Obsolete/SubscribeToAllListWithIncorrectCredentialsObsoleteTests.cs new file mode 100644 index 000000000..7c1426073 --- /dev/null +++ b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/Obsolete/SubscribeToAllListWithIncorrectCredentialsObsoleteTests.cs @@ -0,0 +1,64 @@ +using EventStore.Client; +using Kurrent.Client.Tests.TestNode; + +namespace Kurrent.Client.Tests.PersistentSubscriptions; + +[Trait("Category", "Target:PersistentSubscriptions")] +public class SubscribeToAllListWithIncorrectCredentialsObsoleteTests(ITestOutputHelper output, SubscribeToAllListWithIncorrectCredentialsObsoleteTests.CustomFixture fixture) + : KurrentTemporaryTests(output, fixture) { + [RetryFact] + public async Task throws_with_no_credentials() { + var group = Fixture.GetGroupName(); + var stream = Fixture.GetStreamName(); + + const int streamSubscriptionCount = 4; + const int allStreamSubscriptionCount = 3; + + for (var i = 0; i < streamSubscriptionCount; i++) + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group + i, + new(), + userCredentials: TestCredentials.Root + ); + + for (var i = 0; i < allStreamSubscriptionCount; i++) + await Fixture.Subscriptions.CreateToAllAsync( + group + i, + new(), + userCredentials: TestCredentials.Root + ); + + await Assert.ThrowsAsync(async () => await Fixture.Subscriptions.ListToAllAsync()); + } + + [RetryFact] + public async Task throws_with_non_existing_user() { + var group = Fixture.GetGroupName(); + var stream = Fixture.GetStreamName(); + + const int streamSubscriptionCount = 4; + const int allStreamSubscriptionCount = 3; + + for (var i = 0; i < streamSubscriptionCount; i++) + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group + i, + new(), + userCredentials: TestCredentials.Root + ); + + for (var i = 0; i < allStreamSubscriptionCount; i++) + await Fixture.Subscriptions.CreateToAllAsync( + group + i, + new(), + userCredentials: TestCredentials.Root + ); + + await Assert.ThrowsAsync( + async () => await Fixture.Subscriptions.ListToAllAsync(userCredentials: TestCredentials.TestBadUser) + ); + } + + public class CustomFixture() : KurrentTemporaryFixture(x => x.WithoutDefaultCredentials()); +} diff --git a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/Obsolete/SubscribeToAllNoDefaultCredentialsObsoleteTests.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/Obsolete/SubscribeToAllNoDefaultCredentialsObsoleteTests.cs new file mode 100644 index 000000000..36685373f --- /dev/null +++ b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/Obsolete/SubscribeToAllNoDefaultCredentialsObsoleteTests.cs @@ -0,0 +1,103 @@ +using EventStore.Client; + +namespace Kurrent.Client.Tests.PersistentSubscriptions; + +[Trait("Category", "Target:PersistentSubscriptions")] +public class SubscribeToAllNoDefaultCredentialsObsoleteTests(ITestOutputHelper output, SubscribeToAllNoDefaultCredentialsObsoleteTests.CustomFixture fixture) + : KurrentPermanentTests(output, fixture) { + [RetryFact] + public async Task connect_to_existing_without_permissions() { + var group = Fixture.GetGroupName(); + + await Fixture.Subscriptions.CreateToAllAsync(group, new(), userCredentials: TestCredentials.Root); + + await Assert.ThrowsAsync( + async () => { + using var _ = await Fixture.Subscriptions.SubscribeToAllAsync( + group, + delegate { return Task.CompletedTask; } + ); + } + ); + } + + [RetryFact] + public async Task throws_persistent_subscription_not_found() { + var group = Fixture.GetGroupName(); + + var ex = await Assert.ThrowsAsync( + async () => { + using var _ = await Fixture.Subscriptions.SubscribeToAllAsync( + group, + delegate { return Task.CompletedTask; }, + userCredentials: TestCredentials.Root + ); + } + ).WithTimeout(); + + Assert.Equal(SystemStreams.AllStream, ex.StreamName); + Assert.Equal(group, ex.GroupName); + } + + [RetryFact] + public async Task deleting_without_permissions() { + await Assert.ThrowsAsync(() => Fixture.Subscriptions.DeleteToAllAsync(Guid.NewGuid().ToString())); + } + + [RetryFact] + public async Task create_without_permissions() { + var group = Fixture.GetGroupName(); + + await Assert.ThrowsAsync( + () => + Fixture.Subscriptions.CreateToAllAsync( + group, + new() + ) + ); + } + + [RetryFact] + public async Task update_existing_without_permissions() { + var group = Fixture.GetGroupName(); + await Fixture.Subscriptions.CreateToAllAsync( + group, + new(), + userCredentials: TestCredentials.Root + ); + + await Assert.ThrowsAsync( + () => Fixture.Subscriptions.UpdateToAllAsync( + group, + new() + ) + ); + } + + [RetryFact] + public async Task update_non_existent() { + var group = Fixture.GetGroupName(); + await Assert.ThrowsAsync( + () => Fixture.Subscriptions.UpdateToAllAsync( + group, + new(), + userCredentials: TestCredentials.Root + ) + ); + } + + [RetryFact] + public Task update_with_prepare_position_larger_than_commit_position() { + var group = Fixture.GetGroupName(); + return Assert.ThrowsAsync( + () => + Fixture.Subscriptions.UpdateToAllAsync( + group, + new(startFrom: new Position(0, 1)), + userCredentials: TestCredentials.Root + ) + ); + } + + public class CustomFixture() : KurrentPermanentFixture(x => x.WithoutDefaultCredentials()); +} diff --git a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/Obsolete/SubscribeToAllObsoleteTests.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/Obsolete/SubscribeToAllObsoleteTests.cs new file mode 100644 index 000000000..957b42c69 --- /dev/null +++ b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/Obsolete/SubscribeToAllObsoleteTests.cs @@ -0,0 +1,786 @@ +using System.Text; +using EventStore.Client; +using Grpc.Core; + +namespace Kurrent.Client.Tests.PersistentSubscriptions; + +[Trait("Category", "Target:PersistentSubscriptions")] +public class SubscribeToAllObsoleteTests(ITestOutputHelper output, KurrentPermanentFixture fixture) + : KurrentPermanentTests(output, fixture) { + [RetryFact] + public async Task connect_to_existing_with_max_one_client() { + // Arrange + var group = Fixture.GetGroupName(); + + await Fixture.Subscriptions.CreateToAllAsync(group, new(maxSubscriberCount: 1), userCredentials: TestCredentials.Root); + + using var first = await Fixture.Subscriptions.SubscribeToAllAsync( + group, + delegate { return Task.CompletedTask; }, + userCredentials: TestCredentials.Root + ).WithTimeout(); + + var ex = await Assert.ThrowsAsync( + async () => { + using var _ = await Fixture.Subscriptions.SubscribeToAllAsync( + group, + delegate { return Task.CompletedTask; }, + userCredentials: TestCredentials.Root + ); + } + ).WithTimeout(); + + Assert.Equal(SystemStreams.AllStream, ex.StreamName); + Assert.Equal(group, ex.GroupName); + } + + [RetryFact] + public async Task connect_to_existing_with_permissions() { + // Arrange + var group = Fixture.GetGroupName(); + await Fixture.Subscriptions.CreateToAllAsync(group, new(), userCredentials: TestCredentials.Root); + + var dropped = new TaskCompletionSource<(SubscriptionDroppedReason, Exception?)>(); + using var subscription = await Fixture.Subscriptions.SubscribeToAllAsync( + group, + delegate { return Task.CompletedTask; }, + (s, reason, ex) => dropped.TrySetResult((reason, ex)), + TestCredentials.Root + ).WithTimeout(); + + Assert.NotNull(subscription); + + await Assert.ThrowsAsync(() => dropped.Task.WithTimeout()); + } + + [RetryFact] + public async Task connect_to_existing_with_start_from_beginning() { + var group = Fixture.GetGroupName(); + + TaskCompletionSource firstEventSource = new(); + + // append 10 events to random streams to make sure we have at least 10 events in the transaction file + foreach (var @event in Fixture.CreateTestEvents(10)) { + await Fixture.Streams.AppendToStreamAsync( + Guid.NewGuid().ToString(), + StreamState.NoStream, + [@event] + ); + } + + var events = await Fixture.Streams + .ReadAllAsync(Direction.Forwards, Position.Start, 10, userCredentials: TestCredentials.Root) + .ToArrayAsync(); + + await Fixture.Subscriptions.CreateToAllAsync(group, new(startFrom: Position.Start), userCredentials: TestCredentials.Root); + + using var subscription = await Fixture.Subscriptions.SubscribeToAllAsync( + group, + async (subscription, e, r, ct) => { + firstEventSource.TrySetResult(e); + await subscription.Ack(e); + }, + (subscription, reason, ex) => { + if (reason != SubscriptionDroppedReason.Disposed) + firstEventSource.TrySetException(ex!); + }, + TestCredentials.Root + ); + + var resolvedEvent = await firstEventSource.Task.WithTimeout(TimeSpan.FromSeconds(10)); + Assert.Equal(events![0].Event.EventId, resolvedEvent.Event.EventId); + } + + [RetryFact] + public async Task connect_to_existing_with_start_from_not_set_then_event_written() { + var group = Fixture.GetGroupName(); + var stream = Fixture.GetStreamName(); + + var expectedStreamId = Guid.NewGuid().ToString(); + var expectedEvent = Fixture.CreateTestEvents(1).First(); + + TaskCompletionSource firstNonSystemEventSource = new(); + + foreach (var @event in Fixture.CreateTestEvents(10)) { + await Fixture.Streams.AppendToStreamAsync( + stream, + StreamState.Any, + [@event] + ); + } + + await Fixture.Subscriptions.CreateToAllAsync(group, new(), userCredentials: TestCredentials.Root); + using var subscription = await Fixture.Subscriptions.SubscribeToAllAsync( + group, + async (subscription, e, r, ct) => { + if (SystemStreams.IsSystemStream(e.OriginalStreamId)) { + await subscription.Ack(e); + return; + } + + firstNonSystemEventSource.TrySetResult(e); + await subscription.Ack(e); + }, + (subscription, reason, ex) => { + if (reason != SubscriptionDroppedReason.Disposed) + firstNonSystemEventSource.TrySetException(ex!); + }, + TestCredentials.Root + ); + + await Fixture.Streams.AppendToStreamAsync(expectedStreamId, StreamState.NoStream, [expectedEvent]); + + var resolvedEvent = await firstNonSystemEventSource.Task.WithTimeout(); + Assert.Equal(expectedEvent!.EventId, resolvedEvent.Event.EventId); + Assert.Equal(expectedStreamId, resolvedEvent.Event.EventStreamId); + } + + [RetryFact] + public async Task connect_to_existing_with_start_from_set_to_end_position_then_event_written() { + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + var expectedStreamId = Fixture.GetStreamName(); + var expectedEvent = Fixture.CreateTestEvents().First(); + + TaskCompletionSource firstNonSystemEventSource = new(); + + foreach (var @event in Fixture.CreateTestEvents(10)) { + await Fixture.Streams.AppendToStreamAsync( + stream, + StreamState.Any, + [@event] + ); + } + + await Fixture.Subscriptions.CreateToAllAsync(group, new(startFrom: Position.End), userCredentials: TestCredentials.Root); + using var subscription = await Fixture.Subscriptions.SubscribeToAllAsync( + group, + async (subscription, e, r, ct) => { + if (SystemStreams.IsSystemStream(e.OriginalStreamId)) { + await subscription.Ack(e); + return; + } + + firstNonSystemEventSource.TrySetResult(e); + await subscription.Ack(e); + }, + (subscription, reason, ex) => { + if (reason != SubscriptionDroppedReason.Disposed) + firstNonSystemEventSource.TrySetException(ex!); + }, + TestCredentials.Root + ); + + await Fixture.Streams.AppendToStreamAsync(expectedStreamId, StreamState.NoStream, [expectedEvent]); + + var resolvedEvent = await firstNonSystemEventSource.Task.WithTimeout(); + Assert.Equal(expectedEvent.EventId, resolvedEvent.Event.EventId); + Assert.Equal(expectedStreamId, resolvedEvent.Event.EventStreamId); + } + + [RetryFact] + public async Task connect_to_existing_with_start_from_set_to_invalid_middle_position() { + var group = Fixture.GetGroupName(); + + TaskCompletionSource<(SubscriptionDroppedReason, Exception?)> dropped = new(); + + var invalidPosition = new Position(1L, 1L); + await Fixture.Subscriptions.CreateToAllAsync( + group, + new(startFrom: invalidPosition), + userCredentials: TestCredentials.Root + ); + + using var subscription = await Fixture.Subscriptions.SubscribeToAllAsync( + group, + async (subscription, e, r, ct) => await subscription.Ack(e), + (subscription, reason, ex) => { dropped.TrySetResult((reason, ex)); }, + TestCredentials.Root + ); + + var (reason, exception) = await dropped.Task.WithTimeout(); + Assert.Equal(SubscriptionDroppedReason.ServerError, reason); + Assert.IsType(exception); + } + + [RetryFact] + public async Task connect_to_existing_with_start_from_set_to_valid_middle_position() { + var group = Fixture.GetGroupName(); + + TaskCompletionSource firstEventSource = new(); + + var events = await Fixture.Streams + .ReadAllAsync(Direction.Forwards, Position.Start, 10, userCredentials: TestCredentials.Root) + .ToArrayAsync(); + + var expectedEvent = events[events.Length / 2]; //just a random event in the middle of the results + + await Fixture.Subscriptions.CreateToAllAsync( + group, + new(startFrom: expectedEvent.OriginalPosition), + userCredentials: TestCredentials.Root + ); + + using var subscription = await Fixture.Subscriptions.SubscribeToAllAsync( + group, + async (subscription, e, r, ct) => { + firstEventSource.TrySetResult(e); + await subscription.Ack(e); + }, + (subscription, reason, ex) => { + if (reason != SubscriptionDroppedReason.Disposed) + firstEventSource.TrySetException(ex!); + }, + TestCredentials.Root + ); + + var resolvedEvent = await firstEventSource.Task.WithTimeout(); + Assert.Equal(expectedEvent.OriginalPosition, resolvedEvent.Event.Position); + Assert.Equal(expectedEvent.Event.EventId, resolvedEvent.Event.EventId); + Assert.Equal(expectedEvent.Event.EventStreamId, resolvedEvent.Event.EventStreamId); + } + + [RetryFact] + public async Task connect_with_retries() { + // Arrange + var group = Fixture.GetGroupName(); + + TaskCompletionSource retryCountSource = new(); + + await Fixture.Subscriptions.CreateToAllAsync(group, new(startFrom: Position.Start), userCredentials: TestCredentials.Root); + + // Act + using var subscription = await Fixture.Subscriptions.SubscribeToAllAsync( + group, + async (subscription, e, r, ct) => { + if (r > 4) { + retryCountSource.TrySetResult(r.Value); + await subscription.Ack(e.Event.EventId); + } else { + await subscription.Nack( + PersistentSubscriptionNakEventAction.Retry, + "Not yet tried enough times", + e + ); + } + }, + (subscription, reason, ex) => { + if (reason != SubscriptionDroppedReason.Disposed) + retryCountSource.TrySetException(ex!); + }, + TestCredentials.Root + ); + + // Assert + Assert.Equal(5, await retryCountSource.Task.WithTimeout()); + } + + [RetryFact] + public async Task deleting_existing_with_subscriber() { + var group = Fixture.GetGroupName(); + + TaskCompletionSource<(SubscriptionDroppedReason, Exception?)> dropped = new(); + + await Fixture.Subscriptions.CreateToAllAsync(group, new(), userCredentials: TestCredentials.Root); + + using var subscription = await Fixture.Subscriptions.SubscribeToAllAsync( + group, + async (s, e, i, ct) => await s.Ack(e), + (s, r, e) => dropped.TrySetResult((r, e)), + TestCredentials.Root + ); + + // todo: investigate why this test is flaky without this delay + await Task.Delay(500); + + await Fixture.Subscriptions.DeleteToAllAsync(group, userCredentials: TestCredentials.Root); + + var (reason, exception) = await dropped.Task.WithTimeout(); + Assert.Equal(SubscriptionDroppedReason.ServerError, reason); + var ex = Assert.IsType(exception); + + Assert.Equal(SystemStreams.AllStream, ex.StreamName); + Assert.Equal(group, ex.GroupName); + } + + [RetryFact] + public async Task happy_case_catching_up_to_link_to_events_manual_ack() { + var group = Fixture.GetGroupName(); + var bufferCount = 10; + var eventWriteCount = bufferCount * 2; + TaskCompletionSource eventsReceived = new(); + int eventReceivedCount = 0; + + var events = Fixture.CreateTestEvents(eventWriteCount) + .Select( + (e, i) => new EventData( + e.EventId, + SystemEventTypes.LinkTo, + Encoding.UTF8.GetBytes($"{i}@test"), + contentType: Constants.Metadata.ContentTypes.ApplicationOctetStream + ) + ) + .ToArray(); + + foreach (var e in events) { + await Fixture.Streams.AppendToStreamAsync("test-" + Guid.NewGuid(), StreamState.Any, new[] { e }); + } + + await Fixture.Subscriptions.CreateToAllAsync( + group, + new(startFrom: Position.Start, resolveLinkTos: true), + userCredentials: TestCredentials.Root + ); + + using var subscription = await Fixture.Subscriptions.SubscribeToAllAsync( + group, + async (subscription, e, retryCount, ct) => { + await subscription.Ack(e); + + if (e.OriginalStreamId.StartsWith("test-") + && Interlocked.Increment(ref eventReceivedCount) == events.Length) + eventsReceived.TrySetResult(true); + }, + (s, r, e) => { + if (e != null) + eventsReceived.TrySetException(e); + }, + bufferSize: bufferCount, + userCredentials: TestCredentials.Root + ); + + await eventsReceived.Task.WithTimeout(); + } + + [RetryFact] + public async Task happy_case_catching_up_to_normal_events_manual_ack() { + var group = Fixture.GetGroupName(); + var stream = Fixture.GetStreamName(); + var bufferCount = 10; + var eventWriteCount = bufferCount * 2; + int eventReceivedCount = 0; + + TaskCompletionSource eventsReceived = new(); + + var events = Fixture.CreateTestEvents(eventWriteCount).ToArray(); + + foreach (var e in events) + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.Any, [e]); + + await Fixture.Subscriptions.CreateToAllAsync( + group, + new(startFrom: Position.Start, resolveLinkTos: true), + userCredentials: TestCredentials.Root + ); + + using var subscription = await Fixture.Subscriptions.SubscribeToAllAsync( + group, + async (subscription, e, retryCount, ct) => { + await subscription.Ack(e); + + if (e.OriginalStreamId.StartsWith("test-") + && Interlocked.Increment(ref eventReceivedCount) == events.Length) + eventsReceived.TrySetResult(true); + }, + (s, r, e) => { + if (e != null) + eventsReceived.TrySetException(e); + }, + bufferSize: bufferCount, + userCredentials: TestCredentials.Root + ); + + await eventsReceived.Task.WithTimeout(); + } + + [RetryFact] + public async Task happy_case_writing_and_subscribing_to_normal_events_manual_ack() { + var group = Fixture.GetGroupName(); + var bufferCount = 10; + var eventWriteCount = bufferCount * 2; + int eventReceivedCount = 0; + + TaskCompletionSource eventsReceived = new(); + + var events = Fixture.CreateTestEvents(eventWriteCount).ToArray(); + + await Fixture.Subscriptions.CreateToAllAsync( + group, + new(startFrom: Position.End, resolveLinkTos: true), + userCredentials: TestCredentials.Root + ); + + using var subscription = await Fixture.Subscriptions.SubscribeToAllAsync( + group, + async (subscription, e, retryCount, ct) => { + await subscription.Ack(e); + if (e.OriginalStreamId.StartsWith("test-") + && Interlocked.Increment(ref eventReceivedCount) == events.Length) + eventsReceived.TrySetResult(true); + }, + (s, r, e) => { + if (e != null) + eventsReceived.TrySetException(e); + }, + bufferSize: bufferCount, + userCredentials: TestCredentials.Root + ); + + foreach (var e in events) { + await Fixture.Streams.AppendToStreamAsync("test-" + Guid.NewGuid(), StreamState.Any, new[] { e }); + } + + await eventsReceived.Task.WithTimeout(); + } + + [RetryFact] + public async Task update_existing_with_check_point() { + var group = Fixture.GetGroupName(); + var events = Fixture.CreateTestEvents(5).ToArray(); + var appearedEvents = new List(); + + TaskCompletionSource appeared = new(); + TaskCompletionSource<(SubscriptionDroppedReason, Exception?)> droppedSource = new(); + TaskCompletionSource resumedSource = new(); + Position checkPoint = default; + + foreach (var e in events) + await Fixture.Streams.AppendToStreamAsync("test-" + Guid.NewGuid(), StreamState.Any, [e]); + + await Fixture.Subscriptions.CreateToAllAsync( + group, + new(checkPointLowerBound: 5, checkPointAfter: TimeSpan.FromSeconds(1), startFrom: Position.Start), + userCredentials: TestCredentials.Root + ); + + using var firstSubscription = await Fixture.Subscriptions.SubscribeToAllAsync( + group, + async (s, e, r, ct) => { + appearedEvents.Add(e); + + if (appearedEvents.Count == events.Length) + appeared.TrySetResult(true); + + await s.Ack(e); + }, + (subscription, reason, ex) => droppedSource.TrySetResult((reason, ex)), + TestCredentials.Root + ); + + await Task.WhenAll(appeared.Task, WaitForCheckpoint().WithTimeout()); + + // Force restart of the subscription + await Fixture.Subscriptions.UpdateToAllAsync(group, new(), userCredentials: TestCredentials.Root); + + await droppedSource.Task.WithTimeout(); + + using var secondSubscription = await Fixture.Subscriptions.SubscribeToAllAsync( + group, + async (s, e, r, ct) => { + resumedSource.TrySetResult(e); + await s.Ack(e); + s.Dispose(); + }, + (_, reason, ex) => { + if (ex is not null) + resumedSource.TrySetException(ex); + else + resumedSource.TrySetResult(default); + }, + TestCredentials.Root + ); + + foreach (var e in events) + await Fixture.Streams.AppendToStreamAsync("test-" + Guid.NewGuid(), StreamState.NoStream, new[] { e }); + + var resumedEvent = await resumedSource.Task.WithTimeout(TimeSpan.FromSeconds(10)); + Assert.True(resumedEvent.Event.Position > checkPoint); + + return; + + async Task WaitForCheckpoint() { + await using var subscription = Fixture.Streams.SubscribeToStream( + $"$persistentsubscription-$all::{group}-checkpoint", + FromStream.Start, + userCredentials: TestCredentials.Root + ); + + await foreach (var message in subscription.Messages) { + if (message is not StreamMessage.Event(var resolvedEvent)) { + continue; + } + + checkPoint = resolvedEvent.Event.Data.ParsePosition(); + return; + } + } + } + + [RetryFact] + public async Task update_existing_with_check_point_filtered() { + var group = Fixture.GetGroupName(); + + TaskCompletionSource<(SubscriptionDroppedReason, Exception?)> droppedSource = new(); + TaskCompletionSource resumedSource = new(); + TaskCompletionSource appeared = new(); + + List appearedEvents = []; + + EventData[] events = Fixture.CreateTestEvents(5).ToArray(); + + Position checkPoint = default; + + foreach (var e in events) + await Fixture.Streams.AppendToStreamAsync("test-" + Guid.NewGuid(), StreamState.NoStream, [e]); + + await Fixture.Subscriptions.CreateToAllAsync( + group, + StreamFilter.Prefix("test"), + new( + checkPointLowerBound: 5, + checkPointAfter: TimeSpan.FromSeconds(1), + startFrom: Position.Start + ), + userCredentials: TestCredentials.Root + ); + + await Fixture.Subscriptions.SubscribeToAllAsync( + group, + async (s, e, r, ct) => { + appearedEvents.Add(e); + + if (appearedEvents.Count == events.Length) + appeared.TrySetResult(true); + + await s.Ack(e); + }, + (subscription, reason, ex) => droppedSource.TrySetResult((reason, ex)), + TestCredentials.Root + ); + + await Task.WhenAll(appeared.Task, Checkpointed()).WithTimeout(); + + // Force restart of the subscription + await Fixture.Subscriptions.UpdateToAllAsync(group, new(), userCredentials: TestCredentials.Root); + + await droppedSource.Task.WithTimeout(); + + await Fixture.Subscriptions.SubscribeToAllAsync( + group, + async (s, e, r, ct) => { + resumedSource.TrySetResult(e); + await s.Ack(e); + s.Dispose(); + }, + (_, reason, ex) => { + if (ex is not null) + resumedSource.TrySetException(ex); + else + resumedSource.TrySetResult(default); + }, + TestCredentials.Root + ); + + foreach (var e in events) + await Fixture.Streams.AppendToStreamAsync("test-" + Guid.NewGuid(), StreamState.NoStream, new[] { e }); + + Task resumed = resumedSource.Task; + + var resumedEvent = await resumed.WithTimeout(TimeSpan.FromSeconds(10)); + Assert.True(resumedEvent.Event.Position > checkPoint); + + return; + + async Task Checkpointed() { + await using var subscription = Fixture.Streams.SubscribeToStream( + $"$persistentsubscription-$all::{group}-checkpoint", + FromStream.Start, + userCredentials: TestCredentials.Root + ); + + await foreach (var message in subscription.Messages) { + if (message is not StreamMessage.Event(var resolvedEvent)) { + continue; + } + + checkPoint = resolvedEvent.Event.Data.ParsePosition(); + return; + } + } + } + + [RetryFact] + public async Task update_existing_with_subscribers() { + var group = Fixture.GetGroupName(); + + TaskCompletionSource<(SubscriptionDroppedReason, Exception?)> droppedSource = new(); + + await Fixture.Subscriptions.CreateToAllAsync(group, new(startFrom: Position.Start), userCredentials: TestCredentials.Root); + + using var subscription = Fixture.Subscriptions.SubscribeToAllAsync( + group, + delegate { return Task.CompletedTask; }, + (subscription, reason, ex) => droppedSource.TrySetResult((reason, ex)), + TestCredentials.Root + ); + + // todo: investigate why this test is flaky without this delay + await Task.Delay(500); + + await Fixture.Subscriptions.UpdateToAllAsync( + group, + new(), + userCredentials: TestCredentials.Root + ); + + var (reason, exception) = await droppedSource.Task.WithTimeout(TimeSpan.FromSeconds(10)); + Assert.Equal(SubscriptionDroppedReason.ServerError, reason); + var ex = Assert.IsType(exception); + + Assert.Equal(SystemStreams.AllStream, ex.StreamName); + Assert.Equal(group, ex.GroupName); + } + + [RetryFact] + public async Task when_writing_and_filtering_out_events() { + var events = Fixture.CreateTestEvents(10).ToArray(); + var group = Fixture.GetGroupName(); + var prefix = Guid.NewGuid().ToString("N"); + + TaskCompletionSource appeared = new(); + + Position firstCheckPoint = default; + Position secondCheckPoint = default; + List appearedEvents = []; + + foreach (var e in events) + await Fixture.Streams.AppendToStreamAsync(prefix + Guid.NewGuid(), StreamState.Any, [e]); + + await Fixture.Subscriptions.CreateToAllAsync( + group, + StreamFilter.Prefix(prefix), + new( + checkPointLowerBound: 1, + checkPointUpperBound: 5, + checkPointAfter: TimeSpan.FromSeconds(1), + startFrom: Position.Start + ), + userCredentials: TestCredentials.Root + ); + + using var subscription = await Fixture.Subscriptions.SubscribeToAllAsync( + group, + async (s, e, r, ct) => { + appearedEvents.Add(e); + + if (appearedEvents.Count == events.Length) + appeared.TrySetResult(true); + + await s.Ack(e); + }, + userCredentials: TestCredentials.Root + ); + + await Task.WhenAll(appeared.Task, WaitForCheckpoints().WithTimeout()); + + foreach (var e in events) { + await Fixture.Streams.AppendToStreamAsync( + "filtered-out-stream-" + Guid.NewGuid(), + StreamState.Any, + [e] + ); + } + + Assert.True(secondCheckPoint > firstCheckPoint); + Assert.Equal(events.Select(e => e.EventId), appearedEvents.Select(e => e.Event.EventId)); + + return; + + async Task WaitForCheckpoints() { + bool firstCheckpointSet = false; + await using var subscription = Fixture.Streams.SubscribeToStream( + $"$persistentsubscription-$all::{group}-checkpoint", + FromStream.Start, + userCredentials: TestCredentials.Root + ); + + await foreach (var message in subscription.Messages) { + if (message is not StreamMessage.Event(var resolvedEvent)) { + continue; + } + + if (!firstCheckpointSet) { + firstCheckPoint = resolvedEvent.Event.Data.ParsePosition(); + firstCheckpointSet = true; + } else { + secondCheckPoint = resolvedEvent.Event.Data.ParsePosition(); + return; + } + } + } + } + + // [RetryFact] + // public async Task when_writing_and_subscribing_to_normal_events_manual_nack() { + // var group = Fixture.GetGroupName(); + // var bufferCount = 10; + // var eventWriteCount = bufferCount * 2; + // var prefix = $"{Guid.NewGuid():N}-"; + // + // var events = Fixture.CreateTestEvents(eventWriteCount).ToArray(); + // + // await Fixture.Subscriptions.CreateToAllAsync( + // group, + // new(startFrom: Position.Start, resolveLinkTos: true), + // userCredentials: TestCredentials.Root + // ); + // + // var subscription = Fixture.Subscriptions.SubscribeToAll(group, bufferSize: bufferCount, userCredentials: TestCredentials.Root); + // + // foreach (var e in events) + // await Fixture.Streams.AppendToStreamAsync(prefix + Guid.NewGuid(), StreamState.Any, [e]); + // + // await subscription.Messages.OfType() + // .SelectAwait( + // async e => { + // await subscription.Nack(PersistentSubscriptionNakEventAction.Park, "fail", e.ResolvedEvent); + // return e; + // } + // ) + // .Where(e => e.ResolvedEvent.OriginalStreamId.StartsWith(prefix)) + // .Take(events.Length) + // .ToArrayAsync() + // .AsTask() + // .WithTimeout(); + // } + + // [RetryFact] + // public async Task update_existing_with_commit_position_larger_than_last_indexed_position() { + // var group = Fixture.GetGroupName(); + // + // await Fixture.Subscriptions.CreateToAllAsync( + // group, + // new(), + // userCredentials: TestCredentials.Root + // ); + // + // var lastEvent = await Fixture.Streams.ReadAllAsync( + // Direction.Backwards, + // Position.End, + // 1, + // userCredentials: TestCredentials.Root + // ).FirstAsync(); + // + // var lastCommitPosition = lastEvent.OriginalPosition?.CommitPosition ?? throw new(); + // var ex = await Assert.ThrowsAsync( + // () => + // Fixture.Subscriptions.UpdateToAllAsync( + // group, + // new(startFrom: new Position(lastCommitPosition + 1, lastCommitPosition)), + // userCredentials: TestCredentials.Root + // ) + // ); + // + // Assert.Equal(StatusCode.Internal, ex.StatusCode); + // } +} diff --git a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/Obsolete/SubscribeToAllReplayParkedObsoleteTests.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/Obsolete/SubscribeToAllReplayParkedObsoleteTests.cs new file mode 100644 index 000000000..7e1f2366d --- /dev/null +++ b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/Obsolete/SubscribeToAllReplayParkedObsoleteTests.cs @@ -0,0 +1,87 @@ +using EventStore.Client; + +namespace Kurrent.Client.Tests.PersistentSubscriptions; + +[Trait("Category", "Target:PersistentSubscriptions")] +public class SubscribeToAllReplayParkedObsoleteTests(ITestOutputHelper output, SubscribeToAllReplayParkedTests.CustomFixture fixture) + : KurrentPermanentTests(output, fixture) { + [RetryFact] + public async Task does_not_throw() { + var group = Fixture.GetGroupName(); + + await Fixture.Subscriptions.CreateToAllAsync(group, new(), userCredentials: TestCredentials.Root); + + await Fixture.Subscriptions.ReplayParkedMessagesToAllAsync( + group, + userCredentials: TestCredentials.Root + ); + + await Fixture.Subscriptions.ReplayParkedMessagesToAllAsync( + group, + 100, + userCredentials: TestCredentials.Root + ); + } + + [RetryFact] + public async Task throws_when_given_non_existing_subscription() { + var group = Fixture.GetGroupName(); + + await Fixture.Subscriptions.CreateToAllAsync(group, new(), userCredentials: TestCredentials.Root); + + var nonExistingGroup = Fixture.GetGroupName(); + await Assert.ThrowsAsync( + () => + Fixture.Subscriptions.ReplayParkedMessagesToAllAsync( + nonExistingGroup, + userCredentials: TestCredentials.Root + ) + ); + } + + [RetryFact] + public async Task throws_with_no_credentials() { + var group = Fixture.GetGroupName(); + + await Fixture.Subscriptions.CreateToAllAsync(group, new(), userCredentials: TestCredentials.Root); + + await Assert.ThrowsAsync( + () => + Fixture.Subscriptions.ReplayParkedMessagesToAllAsync(group) + ); + } + + [RetryFact] + public async Task throws_with_non_existing_user() { + var group = Fixture.GetGroupName(); + + await Fixture.Subscriptions.CreateToAllAsync(group, new(), userCredentials: TestCredentials.Root); + + await Assert.ThrowsAsync( + () => + Fixture.Subscriptions.ReplayParkedMessagesToAllAsync( + group, + userCredentials: TestCredentials.TestBadUser + ) + ); + } + + [RetryFact] + public async Task throws_with_normal_user_credentials() { + var user = Fixture.GetUserCredentials(); + + await Fixture.Users + .CreateUserWithRetry(user.Username!, user.Username!, [], user.Password!, TestCredentials.Root) + .WithTimeout(); + + await Assert.ThrowsAsync( + () => + Fixture.Subscriptions.ReplayParkedMessagesToAllAsync( + Fixture.GetGroupName(), + userCredentials: user + ) + ); + } + + public class CustomFixture() : KurrentPermanentFixture(x => x.WithoutDefaultCredentials()); +} diff --git a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/Obsolete/SubscribeToAllResultWithNormalUserCredentialsObsoleteTests.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/Obsolete/SubscribeToAllResultWithNormalUserCredentialsObsoleteTests.cs new file mode 100644 index 000000000..e1815bf9b --- /dev/null +++ b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/Obsolete/SubscribeToAllResultWithNormalUserCredentialsObsoleteTests.cs @@ -0,0 +1,35 @@ +using Kurrent.Client.Tests.TestNode; +using Kurrent.Client.Tests; + +namespace Kurrent.Client.Tests.PersistentSubscriptions; + +[Trait("Category", "Target:PersistentSubscriptions")] +public class SubscribeToAllResultWithNormalUserCredentialsObsoleteTests(ITestOutputHelper output, KurrentTemporaryFixture fixture) + : KurrentTemporaryTests(output, fixture) { + [RetryFact] + public async Task returns_result_with_normal_user_credentials() { + var group = Fixture.GetGroupName(); + var stream = Fixture.GetStreamName(); + + const int streamSubscriptionCount = 4; + const int allStreamSubscriptionCount = 3; + + for (var i = 0; i < streamSubscriptionCount; i++) + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group + i, + new(), + userCredentials: TestCredentials.Root + ); + + for (var i = 0; i < allStreamSubscriptionCount; i++) + await Fixture.Subscriptions.CreateToAllAsync( + group + i, + new(), + userCredentials: TestCredentials.Root + ); + + var result = await Fixture.Subscriptions.ListToAllAsync(userCredentials: TestCredentials.Root); + Assert.Equal(allStreamSubscriptionCount, result.Count()); + } +} diff --git a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/Obsolete/SubscribeToAllReturnsAllSubscriptionsObsoleteTests.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/Obsolete/SubscribeToAllReturnsAllSubscriptionsObsoleteTests.cs new file mode 100644 index 000000000..07035b60b --- /dev/null +++ b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/Obsolete/SubscribeToAllReturnsAllSubscriptionsObsoleteTests.cs @@ -0,0 +1,42 @@ +using Kurrent.Client.Tests.TestNode; +using Kurrent.Client.Tests; + +namespace Kurrent.Client.Tests.PersistentSubscriptions; + +[Trait("Category", "Target:PersistentSubscriptions")] +public class SubscribeToAllReturnsAllSubscriptionsObsoleteTests(ITestOutputHelper output, SubscribeToAllReturnsAllSubscriptions.CustomFixture fixture) + : KurrentTemporaryTests(output, fixture) { + [RetryFact] + public async Task returns_all_subscriptions() { + var group = Fixture.GetGroupName(); + var stream = Fixture.GetStreamName(); + + const int streamSubscriptionCount = 4; + const int allStreamSubscriptionCount = 3; + const int totalSubscriptionCount = streamSubscriptionCount + allStreamSubscriptionCount; + + for (var i = 0; i < streamSubscriptionCount; i++) + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group + i, + new(), + userCredentials: TestCredentials.Root + ); + + for (var i = 0; i < allStreamSubscriptionCount; i++) + await Fixture.Subscriptions.CreateToAllAsync( + group + i, + new(), + userCredentials: TestCredentials.Root + ); + + var result = (await Fixture.Subscriptions.ListAllAsync(userCredentials: TestCredentials.Root)).ToList(); + Assert.Equal(totalSubscriptionCount, result.Count); + } + + public class CustomFixture : KurrentTemporaryFixture { + public CustomFixture() { + SkipPsWarmUp = true; + } + } +} diff --git a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/Obsolete/SubscribeToAllReturnsSubscriptionsToAllStreamObsoleteTests.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/Obsolete/SubscribeToAllReturnsSubscriptionsToAllStreamObsoleteTests.cs new file mode 100644 index 000000000..e0c1f647b --- /dev/null +++ b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/Obsolete/SubscribeToAllReturnsSubscriptionsToAllStreamObsoleteTests.cs @@ -0,0 +1,36 @@ +using Kurrent.Client.Tests.TestNode; +using Kurrent.Client.Tests; + +namespace Kurrent.Client.Tests.PersistentSubscriptions; + +[Trait("Category", "Target:PersistentSubscriptions")] +public class SubscribeToAllReturnsSubscriptionsToAllStreamObsoleteTests(ITestOutputHelper output, KurrentTemporaryFixture fixture) + : KurrentTemporaryTests(output, fixture) { + [RetryFact] + public async Task returns_subscriptions_to_all_stream() { + var group = Fixture.GetGroupName(); + var stream = Fixture.GetStreamName(); + + const int streamSubscriptionCount = 4; + const int allStreamSubscriptionCount = 3; + + for (var i = 0; i < streamSubscriptionCount; i++) + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group + i, + new(), + userCredentials: TestCredentials.Root + ); + + for (var i = 0; i < allStreamSubscriptionCount; i++) + await Fixture.Subscriptions.CreateToAllAsync( + group + i, + new(), + userCredentials: TestCredentials.Root + ); + + var result = (await Fixture.Subscriptions.ListToAllAsync(userCredentials: TestCredentials.Root)).ToList(); + Assert.Equal(allStreamSubscriptionCount, result.Count); + Assert.All(result, s => Assert.Equal("$all", s.EventSource)); + } +} diff --git a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/Obsolete/SubscribeToAllUpdateExistingWithCheckpointObsoleteTests.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/Obsolete/SubscribeToAllUpdateExistingWithCheckpointObsoleteTests.cs new file mode 100644 index 000000000..844b24644 --- /dev/null +++ b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/Obsolete/SubscribeToAllUpdateExistingWithCheckpointObsoleteTests.cs @@ -0,0 +1,88 @@ +using EventStore.Client; +using Kurrent.Client.Tests.TestNode; +using Kurrent.Client.Tests; + +namespace Kurrent.Client.Tests.PersistentSubscriptions; + +[Trait("Category", "Target:PersistentSubscriptions")] +public class SubscribeToAllUpdateExistingWithCheckpointObsoleteTests(ITestOutputHelper output, KurrentTemporaryFixture fixture) + : KurrentTemporaryTests(output, fixture) { + [RetryFact] + public async Task update_existing_with_check_point_should_resumes_from_check_point() { + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + + StreamPosition checkPoint = default; + + var events = Fixture.CreateTestEvents(5).ToArray(); + + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events); + + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group, + new(checkPointLowerBound: 5, checkPointAfter: TimeSpan.FromSeconds(1), startFrom: StreamPosition.Start), + userCredentials: TestCredentials.Root + ); + + await using var subscription = Fixture.Subscriptions.SubscribeToStream(stream, group, userCredentials: TestCredentials.Root); + + await using var enumerator = subscription.Messages.GetAsyncEnumerator(); + + await enumerator.MoveNextAsync(); + + await Task.WhenAll(Subscribe().WithTimeout(), WaitForCheckpoint().WithTimeout()); + + // Force restart of the subscription + await Fixture.Subscriptions.UpdateToStreamAsync(stream, group, new(), userCredentials: TestCredentials.Root); + + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.Any, Fixture.CreateTestEvents(1)); + + await using var sub = Fixture.Subscriptions.SubscribeToStream(stream, group, userCredentials: TestCredentials.Root); + + var resolvedEvent = await sub.Messages + .OfType() + .Select(e => e.ResolvedEvent) + .FirstAsync() + .AsTask() + .WithTimeout(); + + Assert.Equal(checkPoint.Next(), resolvedEvent.Event.EventNumber); + + return; + + async Task Subscribe() { + var count = 0; + + while (await enumerator.MoveNextAsync()) { + if (enumerator.Current is not PersistentSubscriptionMessage.Event(var resolvedEvent, _)) { + continue; + } + + count++; + + await subscription.Ack(resolvedEvent); + if (count >= events.Length) { + break; + } + } + } + + async Task WaitForCheckpoint() { + await using var subscription = Fixture.Streams.SubscribeToStream( + $"$persistentsubscription-{stream}::{group}-checkpoint", + FromStream.Start, + userCredentials: TestCredentials.Root + ); + + await foreach (var message in subscription.Messages) { + if (message is not StreamMessage.Event (var resolvedEvent)) { + continue; + } + + checkPoint = resolvedEvent.Event.Data.ParseStreamPosition(); + return; + } + } + } +} diff --git a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/Obsolete/SubscribeToAllWithoutPSObsoleteTests.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/Obsolete/SubscribeToAllWithoutPSObsoleteTests.cs new file mode 100644 index 000000000..f676c9cb0 --- /dev/null +++ b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/Obsolete/SubscribeToAllWithoutPSObsoleteTests.cs @@ -0,0 +1,17 @@ +using EventStore.Client; +using Kurrent.Client.Tests.TestNode; +using Kurrent.Client.Tests; + +namespace Kurrent.Client.Tests.PersistentSubscriptions; + +[Trait("Category", "Target:PersistentSubscriptions")] +public class SubscribeToAllWithoutPSObsoleteTests(ITestOutputHelper output, KurrentTemporaryFixture fixture) + : KurrentTemporaryTests(output, fixture) { + [RetryFact] + public async Task list_without_persistent_subscriptions() { + await Assert.ThrowsAsync( + async () => + await Fixture.Subscriptions.ListToAllAsync(userCredentials: TestCredentials.Root) + ); + } +} diff --git a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllConnectToExistingWithStartFromNotSetTests.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/SubscribeToAllConnectToExistingWithStartFromNotSetTests.cs similarity index 90% rename from test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllConnectToExistingWithStartFromNotSetTests.cs rename to test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/SubscribeToAllConnectToExistingWithStartFromNotSetTests.cs index b6a05c5cc..d69349f3d 100644 --- a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllConnectToExistingWithStartFromNotSetTests.cs +++ b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/SubscribeToAllConnectToExistingWithStartFromNotSetTests.cs @@ -19,7 +19,7 @@ await Fixture.Streams.AppendToStreamAsync( ); await Fixture.Subscriptions.CreateToAllAsync(group, new(), userCredentials: TestCredentials.Root); - var subscription = Fixture.Subscriptions.SubscribeToAll(group, userCredentials: TestCredentials.Root); + await using var subscription = Fixture.Subscriptions.SubscribeToAll(group, userCredentials: TestCredentials.Root); await Assert.ThrowsAsync( () => subscription.Messages diff --git a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllConnectToExistingWithStartFromSetToEndPositionTests.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/SubscribeToAllConnectToExistingWithStartFromSetToEndPositionTests.cs similarity index 100% rename from test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllConnectToExistingWithStartFromSetToEndPositionTests.cs rename to test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/SubscribeToAllConnectToExistingWithStartFromSetToEndPositionTests.cs diff --git a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllConnectWithoutReadPermissionsTests.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/SubscribeToAllConnectWithoutReadPermissionsTests.cs similarity index 100% rename from test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllConnectWithoutReadPermissionsTests.cs rename to test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/SubscribeToAllConnectWithoutReadPermissionsTests.cs diff --git a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllFilterTests.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/SubscribeToAllFilterTests.cs similarity index 100% rename from test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllFilterTests.cs rename to test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/SubscribeToAllFilterTests.cs diff --git a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllGetInfoTests.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/SubscribeToAllGetInfoTests.cs similarity index 100% rename from test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllGetInfoTests.cs rename to test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/SubscribeToAllGetInfoTests.cs diff --git a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllListWithIncorrectCredentialsTests.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/SubscribeToAllListWithIncorrectCredentialsTests.cs similarity index 100% rename from test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllListWithIncorrectCredentialsTests.cs rename to test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/SubscribeToAllListWithIncorrectCredentialsTests.cs diff --git a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllNoDefaultCredentialsTests.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/SubscribeToAllNoDefaultCredentialsTests.cs similarity index 100% rename from test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllNoDefaultCredentialsTests.cs rename to test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/SubscribeToAllNoDefaultCredentialsTests.cs diff --git a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllReplayParkedTests.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/SubscribeToAllReplayParkedTests.cs similarity index 100% rename from test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllReplayParkedTests.cs rename to test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/SubscribeToAllReplayParkedTests.cs diff --git a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllResultWithNormalUserCredentialsTests.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/SubscribeToAllResultWithNormalUserCredentialsTests.cs similarity index 100% rename from test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllResultWithNormalUserCredentialsTests.cs rename to test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/SubscribeToAllResultWithNormalUserCredentialsTests.cs diff --git a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllReturnsAllSubscriptions.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/SubscribeToAllReturnsAllSubscriptions.cs similarity index 100% rename from test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllReturnsAllSubscriptions.cs rename to test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/SubscribeToAllReturnsAllSubscriptions.cs diff --git a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllReturnsSubscriptionsToAllStreamTests.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/SubscribeToAllReturnsSubscriptionsToAllStreamTests.cs similarity index 100% rename from test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllReturnsSubscriptionsToAllStreamTests.cs rename to test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/SubscribeToAllReturnsSubscriptionsToAllStreamTests.cs diff --git a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllTests.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/SubscribeToAllTests.cs similarity index 100% rename from test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllTests.cs rename to test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/SubscribeToAllTests.cs diff --git a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllUpdateExistingWithCheckpointTest.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/SubscribeToAllUpdateExistingWithCheckpointTest.cs similarity index 100% rename from test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllUpdateExistingWithCheckpointTest.cs rename to test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/SubscribeToAllUpdateExistingWithCheckpointTest.cs diff --git a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllWithoutPSTests.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/SubscribeToAllWithoutPSTests.cs similarity index 100% rename from test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllWithoutPSTests.cs rename to test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/SubscribeToAllWithoutPSTests.cs diff --git a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToStream/Obsolete/SubscribeToStreamConnectToExistingWithoutPermissionObsoleteTests.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToStream/Obsolete/SubscribeToStreamConnectToExistingWithoutPermissionObsoleteTests.cs new file mode 100644 index 000000000..fa2ca1a32 --- /dev/null +++ b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToStream/Obsolete/SubscribeToStreamConnectToExistingWithoutPermissionObsoleteTests.cs @@ -0,0 +1,36 @@ +using EventStore.Client; +using Kurrent.Client.Tests.TestNode; + +namespace Kurrent.Client.Tests.PersistentSubscriptions; + +[Trait("Category", "Target:PersistentSubscriptions")] +public class SubscribeToStreamConnectToExistingWithoutPermissionObsoleteTests( + ITestOutputHelper output, + SubscribeToStreamConnectToExistingWithoutPermissionObsoleteTests.CustomFixture fixture +) + : KurrentTemporaryTests(output, fixture) { + [Fact] + public async Task connect_to_existing_without_permissions() { + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group, + new(), + userCredentials: TestCredentials.Root + ); + + await Assert.ThrowsAsync( + async () => { + using var _ = await Fixture.Subscriptions.SubscribeToStreamAsync( + stream, + group, + delegate { return Task.CompletedTask; } + ); + } + ).WithTimeout(); + } + + public class CustomFixture() : KurrentTemporaryFixture(x => x.WithoutDefaultCredentials()); +} diff --git a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToStream/Obsolete/SubscribeToStreamGetInfoObsoleteTests.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToStream/Obsolete/SubscribeToStreamGetInfoObsoleteTests.cs new file mode 100644 index 000000000..5381dc013 --- /dev/null +++ b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToStream/Obsolete/SubscribeToStreamGetInfoObsoleteTests.cs @@ -0,0 +1,201 @@ +using EventStore.Client; +using Kurrent.Client.Tests.TestNode; + +namespace Kurrent.Client.Tests.PersistentSubscriptions; + +[Trait("Category", "Target:PersistentSubscriptions")] +public class SubscribeToStreamGetInfoObsoleteTests(SubscribeToStreamGetInfoObsoleteTests.CustomFixture fixture) + : IClassFixture { + static readonly PersistentSubscriptionSettings Settings = new( + true, + StreamPosition.Start, + true, + TimeSpan.FromSeconds(9), + 11, + 303, + 30, + 909, + TimeSpan.FromSeconds(1), + 1, + 1, + 500, + SystemConsumerStrategies.RoundRobin + ); + + public static IEnumerable AllowedUsers() { + yield return new object[] { TestCredentials.Root }; + } + + [Theory] + [MemberData(nameof(AllowedUsers))] + public async Task returns_expected_result(UserCredentials credentials) { + var result = await fixture.Subscriptions.GetInfoToStreamAsync(fixture.Stream, fixture.Group, userCredentials: credentials); + + Assert.Equal(fixture.Stream, result.EventSource); + Assert.Equal(fixture.Group, result.GroupName); + Assert.NotNull(Settings.StartFrom); + Assert.True(result.Stats.TotalItems > 0); + Assert.True(result.Stats.OutstandingMessagesCount > 0); + Assert.True(result.Stats.AveragePerSecond >= 0); + Assert.True(result.Stats.ParkedMessageCount >= 0); + Assert.True(result.Stats.AveragePerSecond >= 0); + Assert.True(result.Stats.ReadBufferCount >= 0); + Assert.True(result.Stats.RetryBufferCount >= 0); + Assert.True(result.Stats.CountSinceLastMeasurement >= 0); + Assert.True(result.Stats.TotalInFlightMessages >= 0); + Assert.NotNull(result.Stats.LastKnownEventPosition); + Assert.NotNull(result.Stats.LastCheckpointedEventPosition); + Assert.True(result.Stats.LiveBufferCount >= 0); + + Assert.NotNull(result.Connections); + Assert.NotEmpty(result.Connections); + var connection = result.Connections.First(); + Assert.NotNull(connection.From); + Assert.Equal(TestCredentials.Root.Username, connection.Username); + Assert.NotEmpty(connection.ConnectionName); + Assert.True(connection.AverageItemsPerSecond >= 0); + Assert.True(connection.TotalItems >= 0); + Assert.True(connection.CountSinceLastMeasurement >= 0); + Assert.True(connection.AvailableSlots >= 0); + Assert.True(connection.InFlightMessages >= 0); + Assert.NotNull(connection.ExtraStatistics); + Assert.NotEmpty(connection.ExtraStatistics); + + AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.Highest); + AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.Mean); + AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.Median); + AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.Fastest); + AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.Quintile1); + AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.Quintile2); + AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.Quintile3); + AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.Quintile4); + AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.Quintile5); + AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.NinetyPercent); + AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.NinetyFivePercent); + AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.NinetyNinePercent); + AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.NinetyNinePointFivePercent); + AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.NinetyNinePointNinePercent); + + Assert.NotNull(result.Settings); + Assert.Equal(Settings.StartFrom, result.Settings!.StartFrom); + Assert.Equal(Settings.ResolveLinkTos, result.Settings!.ResolveLinkTos); + Assert.Equal(Settings.ExtraStatistics, result.Settings!.ExtraStatistics); + Assert.Equal(Settings.MessageTimeout, result.Settings!.MessageTimeout); + Assert.Equal(Settings.MaxRetryCount, result.Settings!.MaxRetryCount); + Assert.Equal(Settings.LiveBufferSize, result.Settings!.LiveBufferSize); + Assert.Equal(Settings.ReadBatchSize, result.Settings!.ReadBatchSize); + Assert.Equal(Settings.HistoryBufferSize, result.Settings!.HistoryBufferSize); + Assert.Equal(Settings.CheckPointAfter, result.Settings!.CheckPointAfter); + Assert.Equal(Settings.CheckPointLowerBound, result.Settings!.CheckPointLowerBound); + Assert.Equal(Settings.CheckPointUpperBound, result.Settings!.CheckPointUpperBound); + Assert.Equal(Settings.MaxSubscriberCount, result.Settings!.MaxSubscriberCount); + Assert.Equal(Settings.ConsumerStrategyName, result.Settings!.ConsumerStrategyName); + } + + [RetryFact] + public async Task throws_when_given_non_existing_subscription() => + await Assert.ThrowsAsync( + async () => { + await fixture.Subscriptions.GetInfoToStreamAsync( + "NonExisting", + "NonExisting", + userCredentials: TestCredentials.Root + ); + } + ); + + [Fact(Skip = "Unable to produce same behavior with HTTP fallback!")] + public async Task throws_with_non_existing_user() { + var group = $"NonExisting-{fixture.GetGroupName()}"; + var stream = $"NonExisting-{fixture.GetStreamName()}"; + + await Assert.ThrowsAsync( + async () => { + await fixture.Subscriptions.GetInfoToStreamAsync( + stream, + group, + userCredentials: TestCredentials.TestBadUser + ); + } + ); + } + + [RetryFact] + public async Task throws_with_no_credentials() { + var group = $"NonExisting-{fixture.GetGroupName()}"; + var stream = $"NonExisting-{fixture.GetStreamName()}"; + + await Assert.ThrowsAsync( + async () => { + await fixture.Subscriptions.GetInfoToStreamAsync( + stream, + group + ); + } + ); + } + + [RetryFact] + public async Task returns_result_for_normal_user() { + var result = await fixture.Subscriptions.GetInfoToStreamAsync( + fixture.Stream, + fixture.Group, + userCredentials: TestCredentials.Root + ); + + Assert.NotNull(result); + } + + public class CustomFixture : KurrentTemporaryFixture { + public string Group { get; set; } + public string Stream { get; set; } + + public CustomFixture() : base(x => x.WithoutDefaultCredentials()) { + Group = GetGroupName(); + Stream = GetStreamName(); + + OnSetup += async () => { + await Subscriptions.CreateToStreamAsync( + groupName: Group, + streamName: Stream, + settings: Settings, + userCredentials: TestCredentials.Root + ); + + var counter = 0; + var tcs = new TaskCompletionSource(); + + await Subscriptions.SubscribeToStreamAsync( + Stream, + Group, + (s, e, r, ct) => { + counter++; + + if (counter == 1) + s.Nack(PersistentSubscriptionNakEventAction.Park, "Test", e); + + if (counter > 10) + tcs.TrySetResult(); + + return Task.CompletedTask; + }, + userCredentials: TestCredentials.Root + ); + + for (var i = 0; i < 15; i++) { + await Streams.AppendToStreamAsync( + Stream, + StreamState.Any, + [new EventData(Uuid.NewUuid(), "test-event", ReadOnlyMemory.Empty)], + userCredentials: TestCredentials.Root + ); + } + }; + } + }; + + void AssertKeyAndValue(IDictionary items, string key) { + Assert.True(items.ContainsKey(key)); + Assert.True(items[key] > 0); + } +} diff --git a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToStream/Obsolete/SubscribeToStreamObsoleteTests.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToStream/Obsolete/SubscribeToStreamObsoleteTests.cs new file mode 100644 index 000000000..2f192c426 --- /dev/null +++ b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToStream/Obsolete/SubscribeToStreamObsoleteTests.cs @@ -0,0 +1,948 @@ +using System.Text; +using EventStore.Client; + +namespace Kurrent.Client.Tests.PersistentSubscriptions; + +[Trait("Category", "Target:PersistentSubscriptions")] +public class SubscribeToStreamObsoleteTests(ITestOutputHelper output, KurrentPermanentFixture fixture) + : KurrentPermanentTests(output, fixture) { + [RetryFact] + public async Task connect_to_existing_with_max_one_client() { + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group, + new(maxSubscriberCount: 1), + userCredentials: TestCredentials.Root + ); + + using var first = await Fixture.Subscriptions.SubscribeToStreamAsync( + stream, + group, + delegate { return Task.CompletedTask; }, + userCredentials: TestCredentials.Root + ).WithTimeout(); + + var ex = await Assert.ThrowsAsync( + async () => { + using var _ = await Fixture.Subscriptions.SubscribeToStreamAsync( + stream, + group, + delegate { return Task.CompletedTask; }, + userCredentials: TestCredentials.Root + ); + } + ).WithTimeout(); + + Assert.Equal(stream, ex.StreamName); + Assert.Equal(group, ex.GroupName); + } + + [RetryFact] + public async Task connect_to_existing_with_permissions() { + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group, + new(), + userCredentials: TestCredentials.Root + ); + + var dropped = new TaskCompletionSource<(SubscriptionDroppedReason, Exception?)>(); + using var subscription = await Fixture.Subscriptions.SubscribeToStreamAsync( + stream, + group, + delegate { return Task.CompletedTask; }, + (s, reason, ex) => dropped.TrySetResult((reason, ex)), + TestCredentials.Root + ).WithTimeout(); + + Assert.NotNull(subscription); + + await Assert.ThrowsAsync(() => dropped.Task.WithTimeout()); + } + + [RetryFact] + public async Task connect_to_existing_with_start_from_beginning_and_events_in_it() { + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + + TaskCompletionSource firstEventSource = new(); + + var events = Fixture.CreateTestEvents(10).ToArray(); + + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events); + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group, + new(startFrom: StreamPosition.Start), + userCredentials: TestCredentials.Root + ); + + using var subscription = await Fixture.Subscriptions.SubscribeToStreamAsync( + stream, + group, + async (subscription, e, r, ct) => { + firstEventSource.TrySetResult(e); + await subscription.Ack(e); + }, + (subscription, reason, ex) => { + if (reason != SubscriptionDroppedReason.Disposed) + firstEventSource.TrySetException(ex!); + }, + TestCredentials.Root + ); + + var firstEvent = firstEventSource.Task; + + var resolvedEvent = await firstEvent.WithTimeout(TimeSpan.FromSeconds(10)); + Assert.Equal(StreamPosition.Start, resolvedEvent.Event.EventNumber); + Assert.Equal(events[0].EventId, resolvedEvent.Event.EventId); + } + + [RetryFact] + public async Task connect_to_existing_with_start_from_beginning_and_no_stream() { + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + + TaskCompletionSource firstEventSource = new(); + + var events = Fixture.CreateTestEvents().ToArray(); + + var eventId = events.Single().EventId; + + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group, + new(), + userCredentials: TestCredentials.Root + ); + + using var subscription = await Fixture.Subscriptions.SubscribeToStreamAsync( + stream, + group, + async (subscription, e, r, ct) => { + firstEventSource.TrySetResult(e); + await subscription.Ack(e); + }, + (subscription, reason, ex) => { + if (reason != SubscriptionDroppedReason.Disposed) + firstEventSource.TrySetException(ex!); + }, + TestCredentials.Root + ); + + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events); + + var firstEvent = await firstEventSource.Task.WithTimeout(TimeSpan.FromSeconds(10)); + Assert.Equal(StreamPosition.Start, firstEvent.Event.EventNumber); + Assert.Equal(eventId, firstEvent.Event.EventId); + } + + [RetryFact] + public async Task connect_to_existing_with_start_from_not_set_and_events_in_it() { + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + TaskCompletionSource firstEventSource = new(); + + var events = Fixture.CreateTestEvents(10).ToArray(); + + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events); + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group, + new(), + userCredentials: TestCredentials.Root + ); + + using var subscription = await Fixture.Subscriptions.SubscribeToStreamAsync( + stream, + group, + async (subscription, e, r, ct) => { + firstEventSource.TrySetResult(e); + await subscription.Ack(e); + }, + (subscription, reason, ex) => { + if (reason != SubscriptionDroppedReason.Disposed) + firstEventSource.TrySetException(ex!); + }, + TestCredentials.Root + ); + + var firstEvent = firstEventSource.Task; + + await Assert.ThrowsAsync(() => firstEvent.WithTimeout()); + } + + [RetryFact] + public async Task connect_to_existing_with_start_from_not_set_and_events_in_it_then_event_written() { + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + + TaskCompletionSource firstEventSource = new(); + + var events = Fixture.CreateTestEvents(11).ToArray(); + + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events.Take(10)); + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group, + new(), + userCredentials: TestCredentials.Root + ); + + using var subscription = await Fixture.Subscriptions.SubscribeToStreamAsync( + stream, + group, + async (subscription, e, r, ct) => { + firstEventSource.TrySetResult(e); + await subscription.Ack(e); + }, + (subscription, reason, ex) => { + if (reason != SubscriptionDroppedReason.Disposed) + firstEventSource.TrySetException(ex!); + }, + TestCredentials.Root + ); + + await Fixture.Streams.AppendToStreamAsync(stream, new StreamRevision(9), events.Skip(10)); + + var firstEvent = firstEventSource.Task; + + var resolvedEvent = await firstEvent.WithTimeout(); + Assert.Equal(new(10), resolvedEvent.Event.EventNumber); + Assert.Equal(events.Last().EventId, resolvedEvent.Event.EventId); + } + + [RetryFact] + public async Task connect_to_existing_with_start_from_set_to_end_position_and_events_in_it() { + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + + TaskCompletionSource firstEventSource = new(); + + var events = Fixture.CreateTestEvents(10).ToArray(); + + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events); + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group, + new(startFrom: StreamPosition.End), + userCredentials: TestCredentials.Root + ); + + using var subscription = await Fixture.Subscriptions.SubscribeToStreamAsync( + stream, + group, + async (subscription, e, r, ct) => { + firstEventSource.TrySetResult(e); + await subscription.Ack(e); + }, + (subscription, reason, ex) => { + if (reason != SubscriptionDroppedReason.Disposed) + firstEventSource.TrySetException(ex!); + }, + TestCredentials.Root + ); + + var firstEvent = firstEventSource.Task; + await Assert.ThrowsAsync(() => firstEvent.WithTimeout()); + } + + [RetryFact] + public async Task connect_to_existing_with_start_from_set_to_end_position_and_events_in_it_then_event_written() { + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + + TaskCompletionSource firstEventSource = new(); + + var events = Fixture.CreateTestEvents(11).ToArray(); + + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events.Take(10)); + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group, + new(startFrom: StreamPosition.End), + userCredentials: TestCredentials.Root + ); + + using var subscription = await Fixture.Subscriptions.SubscribeToStreamAsync( + stream, + group, + async (subscription, e, r, ct) => { + firstEventSource.TrySetResult(e); + await subscription.Ack(e); + }, + (subscription, reason, ex) => { + if (reason != SubscriptionDroppedReason.Disposed) + firstEventSource.TrySetException(ex!); + }, + TestCredentials.Root + ); + + await Fixture.Streams.AppendToStreamAsync(stream, new StreamRevision(9), events.Skip(10)); + + var firstEvent = firstEventSource.Task; + + var resolvedEvent = await firstEvent.WithTimeout(); + Assert.Equal(new(10), resolvedEvent.Event.EventNumber); + Assert.Equal(events.Last().EventId, resolvedEvent.Event.EventId); + } + + [RetryFact] + public async Task connect_to_existing_with_start_from_two_and_no_stream() { + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + + TaskCompletionSource firstEventSource = new(); + + var events = Fixture.CreateTestEvents(3).ToArray(); + + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group, + new(startFrom: new StreamPosition(2)), + userCredentials: TestCredentials.Root + ); + + using var subscription = await Fixture.Subscriptions.SubscribeToStreamAsync( + stream, + group, + async (subscription, e, r, ct) => { + firstEventSource.TrySetResult(e); + await subscription.Ack(e); + }, + (subscription, reason, ex) => { + if (reason != SubscriptionDroppedReason.Disposed) + firstEventSource.TrySetException(ex!); + }, + TestCredentials.Root + ); + + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events); + + var firstEvent = firstEventSource.Task; + var resolvedEvent = await firstEvent.WithTimeout(TimeSpan.FromSeconds(10)); + var eventId = events.Last().EventId; + + Assert.Equal(new(2), resolvedEvent.Event.EventNumber); + Assert.Equal(eventId, resolvedEvent.Event.EventId); + } + + [RetryFact] + public async Task connect_to_existing_with_start_from_x_set_and_events_in_it() { + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + + TaskCompletionSource firstEventSource = new(); + + var events = Fixture.CreateTestEvents(10).ToArray(); + + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events.Take(10)); + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group, + new(startFrom: new StreamPosition(4)), + userCredentials: TestCredentials.Root + ); + + using var subscription = await Fixture.Subscriptions.SubscribeToStreamAsync( + stream, + group, + async (subscription, e, r, ct) => { + firstEventSource.TrySetResult(e); + await subscription.Ack(e); + }, + (subscription, reason, ex) => { + if (reason != SubscriptionDroppedReason.Disposed) + firstEventSource.TrySetException(ex!); + }, + TestCredentials.Root + ); + + await Fixture.Streams.AppendToStreamAsync(stream, new StreamRevision(9), events.Skip(10)); + + var firstEvent = firstEventSource.Task; + + var resolvedEvent = await firstEvent.WithTimeout(); + Assert.Equal(new(4), resolvedEvent.Event.EventNumber); + Assert.Equal(events.Skip(4).First().EventId, resolvedEvent.Event.EventId); + } + + [RetryFact] + public async Task connect_to_existing_with_start_from_x_set_and_events_in_it_then_event_written() { + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + + TaskCompletionSource firstEventSource = new(); + + var events = Fixture.CreateTestEvents(11).ToArray(); + + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events.Take(10)); + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group, + new(startFrom: new StreamPosition(10)), + userCredentials: TestCredentials.Root + ); + + using var subscription = await Fixture.Subscriptions.SubscribeToStreamAsync( + stream, + group, + async (subscription, e, r, ct) => { + firstEventSource.TrySetResult(e); + await subscription.Ack(e); + }, + (subscription, reason, ex) => { + if (reason != SubscriptionDroppedReason.Disposed) + firstEventSource.TrySetException(ex!); + }, + TestCredentials.Root + ); + + await Fixture.Streams.AppendToStreamAsync(stream, new StreamRevision(9), events.Skip(10)); + + var firstEvent = firstEventSource.Task; + + var resolvedEvent = await firstEvent.WithTimeout(); + Assert.Equal(new(10), resolvedEvent.Event.EventNumber); + Assert.Equal(events.Last().EventId, resolvedEvent.Event.EventId); + } + + [RetryFact] + public async Task connect_to_existing_with_start_from_x_set_higher_than_x_and_events_in_it_then_event_written() { + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + + TaskCompletionSource firstEventSource = new(); + + var events = Fixture.CreateTestEvents(12).ToArray(); + + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events.Take(11)); + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group, + new(startFrom: new StreamPosition(11)), + userCredentials: TestCredentials.Root + ); + + using var subscription = await Fixture.Subscriptions.SubscribeToStreamAsync( + stream, + group, + async (subscription, e, r, ct) => { + firstEventSource.TrySetResult(e); + await subscription.Ack(e); + }, + (subscription, reason, ex) => { + if (reason != SubscriptionDroppedReason.Disposed) + firstEventSource.TrySetException(ex!); + }, + TestCredentials.Root + ); + + await Fixture.Streams.AppendToStreamAsync(stream, new StreamRevision(10), events.Skip(11)); + + var firstEvent = firstEventSource.Task; + + var resolvedEvent = await firstEvent.WithTimeout(); + Assert.Equal(new(11), resolvedEvent.Event.EventNumber); + Assert.Equal(events.Last().EventId, resolvedEvent.Event.EventId); + } + + [RetryFact] + public async Task connect_to_non_existing_with_permissions() { + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + + var ex = await Assert.ThrowsAsync( + async () => { + using var _ = await Fixture.Subscriptions.SubscribeToStreamAsync( + stream, + group, + delegate { return Task.CompletedTask; }, + userCredentials: TestCredentials.Root + ); + } + ).WithTimeout(); + + Assert.Equal(stream, ex.StreamName); + Assert.Equal(group, ex.GroupName); + } + + [RetryFact] + public async Task connect_with_retries() { + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + + TaskCompletionSource retryCountSource = new(); + + var events = Fixture.CreateTestEvents().ToArray(); + + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events); + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group, + new(startFrom: StreamPosition.Start), + userCredentials: TestCredentials.Root + ); + + using var subscription = await Fixture.Subscriptions.SubscribeToStreamAsync( + stream, + group, + async (subscription, e, r, ct) => { + if (r > 4) { + retryCountSource.TrySetResult(r.Value); + await subscription.Ack(e.Event.EventId); + } else { + await subscription.Nack( + PersistentSubscriptionNakEventAction.Retry, + "Not yet tried enough times", + e + ); + } + }, + (subscription, reason, ex) => { + if (reason != SubscriptionDroppedReason.Disposed) + retryCountSource.TrySetException(ex!); + }, + TestCredentials.Root + ); + + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events); + + Task retryCount = retryCountSource.Task; + Assert.Equal(5, await retryCount.WithTimeout()); + } + + [RetryFact] + public async Task connecting_to_a_persistent_subscription() { + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + + TaskCompletionSource firstEventSource = new(); + + var events = Fixture.CreateTestEvents(12).ToArray(); + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events.Take(11)); + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group, + new(startFrom: new StreamPosition(11)), + userCredentials: TestCredentials.Root + ); + + using var subscription = await Fixture.Subscriptions.SubscribeToStreamAsync( + stream, + group, + async (subscription, e, r, ct) => { + firstEventSource.TrySetResult(e); + await subscription.Ack(e); + }, + (subscription, reason, ex) => { + if (reason != SubscriptionDroppedReason.Disposed) + firstEventSource.TrySetException(ex!); + }, + TestCredentials.Root + ); + + await Fixture.Streams.AppendToStreamAsync(stream, new StreamRevision(10), events.Skip(11)); + + Task firstEvent = firstEventSource.Task; + + var resolvedEvent = await firstEvent.WithTimeout(); + Assert.Equal(new(11), resolvedEvent.Event.EventNumber); + Assert.Equal(events.Last().EventId, resolvedEvent.Event.EventId); + } + + [RetryFact] + public async Task deleting_existing_with_subscriber_the_subscription_is_dropped() { + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + + TaskCompletionSource<(SubscriptionDroppedReason, Exception?)> dropped = new(); + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group, + new(), + userCredentials: TestCredentials.Root + ); + + using var subscription = await Fixture.Subscriptions.SubscribeToStreamAsync( + stream, + group, + (_, _, _, _) => Task.CompletedTask, + (_, r, e) => dropped.TrySetResult((r, e)), + TestCredentials.Root + ); + + await Fixture.Subscriptions.DeleteToStreamAsync( + stream, + group, + userCredentials: TestCredentials.Root + ); + + var (reason, exception) = await dropped.Task.WithTimeout(); + Assert.Equal(SubscriptionDroppedReason.ServerError, reason); + var ex = Assert.IsType(exception); + + Assert.Equal(stream, ex.StreamName); + Assert.Equal(group, ex.GroupName); + } + + [Fact(Skip = "Isn't this how it should work?")] + public async Task deleting_existing_with_subscriber_the_subscription_is_dropped_with_not_found() { + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + + TaskCompletionSource<(SubscriptionDroppedReason, Exception?)> _dropped = new(); + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group, + new(), + userCredentials: TestCredentials.Root + ); + + using var subscription = await Fixture.Subscriptions.SubscribeToStreamAsync( + stream, + group, + (_, _, _, _) => Task.CompletedTask, + (_, r, e) => _dropped.TrySetResult((r, e)), + TestCredentials.Root + ); + + await Fixture.Subscriptions.DeleteToStreamAsync( + stream, + group, + userCredentials: TestCredentials.Root + ); + + Task<(SubscriptionDroppedReason, Exception?)> dropped = _dropped.Task; + + var (reason, exception) = await dropped.WithTimeout(); + Assert.Equal(SubscriptionDroppedReason.ServerError, reason); + var ex = Assert.IsType(exception); + Assert.Equal(stream, ex.StreamName); + Assert.Equal("groupname123", ex.GroupName); + } + + [RetryFact] + public async Task happy_case_catching_up_to_link_to_events_manual_ack() { + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + + const int bufferCount = 10; + const int eventWriteCount = bufferCount * 2; + + EventData[] events; + TaskCompletionSource eventsReceived = new(); + int eventReceivedCount = 0; + + events = Fixture.CreateTestEvents(eventWriteCount) + .Select( + (e, i) => new EventData( + e.EventId, + SystemEventTypes.LinkTo, + Encoding.UTF8.GetBytes($"{i}@{stream}"), + contentType: Constants.Metadata.ContentTypes.ApplicationOctetStream + ) + ) + .ToArray(); + + foreach (var e in events) + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.Any, new[] { e }); + + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group, + new(startFrom: StreamPosition.Start, resolveLinkTos: true), + userCredentials: TestCredentials.Root + ); + + using var subscription = await Fixture.Subscriptions.SubscribeToStreamAsync( + stream, + group, + async (subscription, e, retryCount, ct) => { + await subscription.Ack(e); + + if (Interlocked.Increment(ref eventReceivedCount) == events.Length) + eventsReceived.TrySetResult(true); + }, + (s, r, e) => { + if (e != null) + eventsReceived.TrySetException(e); + }, + bufferSize: bufferCount, + userCredentials: TestCredentials.Root + ); + + await eventsReceived.Task.WithTimeout(); + } + + [RetryFact] + public async Task happy_case_catching_up_to_normal_events_manual_ack() { + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + + const int bufferCount = 10; + const int eventWriteCount = bufferCount * 2; + + int eventReceivedCount = 0; + + var events = Fixture.CreateTestEvents(eventWriteCount).ToArray(); + TaskCompletionSource eventsReceived = new(); + + foreach (var e in events) + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.Any, new[] { e }); + + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group, + new(startFrom: StreamPosition.Start, resolveLinkTos: true), + userCredentials: TestCredentials.Root + ); + + using var subscription = await Fixture.Subscriptions.SubscribeToStreamAsync( + stream, + group, + async (subscription, e, retryCount, ct) => { + await subscription.Ack(e); + + if (Interlocked.Increment(ref eventReceivedCount) == events.Length) + eventsReceived.TrySetResult(true); + }, + (s, r, e) => { + if (e != null) + eventsReceived.TrySetException(e); + }, + bufferSize: bufferCount, + userCredentials: TestCredentials.Root + ); + + await eventsReceived.Task.WithTimeout(); + } + + [RetryFact] + public async Task happy_case_writing_and_subscribing_to_normal_events_manual_ack() { + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + + const int bufferCount = 10; + const int eventWriteCount = bufferCount * 2; + + int eventReceivedCount = 0; + + var events = Fixture.CreateTestEvents(eventWriteCount).ToArray(); + TaskCompletionSource eventsReceived = new(); + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group, + new(startFrom: StreamPosition.End, resolveLinkTos: true), + userCredentials: TestCredentials.Root + ); + + using var subscription = await Fixture.Subscriptions.SubscribeToStreamAsync( + stream, + group, + async (subscription, e, retryCount, ct) => { + await subscription.Ack(e); + if (Interlocked.Increment(ref eventReceivedCount) == events.Length) + eventsReceived.TrySetResult(true); + }, + (s, r, e) => { + if (e != null) + eventsReceived.TrySetException(e); + }, + bufferSize: bufferCount, + userCredentials: TestCredentials.Root + ); + + foreach (var e in events) + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.Any, [e]); + + await eventsReceived.Task.WithTimeout(); + } + + [RetryFact] + public async Task update_existing_with_check_point() { + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + + StreamPosition checkPoint = default; + + TaskCompletionSource<(SubscriptionDroppedReason, Exception?)> droppedSource = new(); + TaskCompletionSource resumedSource = new(); + TaskCompletionSource appeared = new(); + List appearedEvents = []; + EventData[] events = Fixture.CreateTestEvents(5).ToArray(); + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events); + + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group, + new( + checkPointLowerBound: 5, + checkPointAfter: TimeSpan.FromSeconds(1), + startFrom: StreamPosition.Start + ), + userCredentials: TestCredentials.Root + ); + + var checkPointStream = $"$persistentsubscription-{stream}::{group}-checkpoint"; + + await Fixture.Subscriptions.SubscribeToStreamAsync( + stream, + group, + async (s, e, _, _) => { + appearedEvents.Add(e); + await s.Ack(e); + + if (appearedEvents.Count == events.Length) + appeared.TrySetResult(true); + }, + (_, reason, ex) => droppedSource.TrySetResult((reason, ex)), + TestCredentials.Root + ); + + await Task.WhenAll(appeared.Task.WithTimeout(), Checkpointed()); + + // Force restart of the subscription + await Fixture.Subscriptions.UpdateToStreamAsync( + stream, + group, + new(), + userCredentials: TestCredentials.Root + ); + + await droppedSource.Task.WithTimeout(); + + await Fixture.Subscriptions.SubscribeToStreamAsync( + stream, + group, + async (s, e, _, _) => { + resumedSource.TrySetResult(e); + await s.Ack(e); + }, + (_, reason, ex) => { + if (ex is not null) + resumedSource.TrySetException(ex); + else + resumedSource.TrySetResult(default); + }, + TestCredentials.Root + ); + + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.Any, Fixture.CreateTestEvents(1)); + + var resumedEvent = await resumedSource.Task.WithTimeout(TimeSpan.FromSeconds(10)); + Assert.Equal(checkPoint.Next(), resumedEvent.Event.EventNumber); + + return; + + async Task Checkpointed() { + await using var subscription = Fixture.Streams.SubscribeToStream( + checkPointStream, + FromStream.Start, + userCredentials: TestCredentials.Root + ); + + await foreach (var message in subscription.Messages) { + if (message is not StreamMessage.Event(var resolvedEvent)) { + continue; + } + + checkPoint = resolvedEvent.Event.Data.ParseStreamPosition(); + return; + } + + throw new InvalidOperationException(); + } + } + + [RetryFact] + public async Task update_existing_with_subscribers() { + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + + TaskCompletionSource<(SubscriptionDroppedReason, Exception?)> droppedSource = new(); + + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, Fixture.CreateTestEvents()); + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group, + new(), + userCredentials: TestCredentials.Root + ); + + await Fixture.Subscriptions.SubscribeToStreamAsync( + stream, + group, + delegate { return Task.CompletedTask; }, + (_, reason, ex) => droppedSource.TrySetResult((reason, ex)), + TestCredentials.Root + ); + + await Fixture.Subscriptions.UpdateToStreamAsync( + stream, + group, + new(), + userCredentials: TestCredentials.Root + ); + + var dropped = droppedSource.Task; + + var (reason, exception) = await dropped.WithTimeout(TimeSpan.FromSeconds(10)); + Assert.Equal(SubscriptionDroppedReason.ServerError, reason); + var ex = Assert.IsType(exception); + + Assert.Equal(stream, ex.StreamName); + Assert.Equal(group, ex.GroupName); + } + + [RetryFact] + public async Task when_writing_and_subscribing_to_normal_events_manual_nack() { + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + + const int bufferCount = 10; + const int eventWriteCount = bufferCount * 2; + + int eventReceivedCount = 0; + + EventData[] events = Fixture.CreateTestEvents(eventWriteCount) + .ToArray(); + + TaskCompletionSource eventsReceived = new(); + + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group, + new(startFrom: StreamPosition.Start, resolveLinkTos: true), + userCredentials: TestCredentials.Root + ); + + using var subscription = await Fixture.Subscriptions.SubscribeToStreamAsync( + stream, + group, + async (subscription, e, retryCount, ct) => { + await subscription.Nack(PersistentSubscriptionNakEventAction.Park, "fail", e); + + if (Interlocked.Increment(ref eventReceivedCount) == events.Length) + eventsReceived.TrySetResult(true); + }, + (s, r, e) => { + if (e != null) + eventsReceived.TrySetException(e); + }, + bufferSize: bufferCount, + userCredentials: TestCredentials.Root + ); + + foreach (var e in events) + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.Any, [e]); + + await eventsReceived.Task.WithTimeout(); + } +} diff --git a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToStreamConnectToExistingWithStartFromBeginningTests.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToStream/SubscribeToStreamConnectToExistingWithStartFromBeginningTests.cs similarity index 100% rename from test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToStreamConnectToExistingWithStartFromBeginningTests.cs rename to test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToStream/SubscribeToStreamConnectToExistingWithStartFromBeginningTests.cs diff --git a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToStreamGetInfoTests.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToStream/SubscribeToStreamGetInfoTests.cs similarity index 96% rename from test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToStreamGetInfoTests.cs rename to test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToStream/SubscribeToStreamGetInfoTests.cs index 52efa2647..d0c593078 100644 --- a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToStreamGetInfoTests.cs +++ b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToStream/SubscribeToStreamGetInfoTests.cs @@ -6,8 +6,8 @@ namespace Kurrent.Client.Tests.PersistentSubscriptions; [Trait("Category", "Target:PersistentSubscriptions")] -public class SubscribeToStreamGetInfoTests(SubscribeToStreamGetInfoTests.NoDefaultCredentialsFixture fixture) - : IClassFixture { +public class SubscribeToStreamGetInfoTests(SubscribeToStreamGetInfoTests.CustomFixture fixture) + : IClassFixture { static readonly PersistentSubscriptionSettings Settings = new( true, StreamPosition.Start, @@ -148,14 +148,14 @@ public async Task returns_result_for_normal_user() { Assert.NotNull(result); } - public class NoDefaultCredentialsFixture : KurrentTemporaryFixture { + public class CustomFixture : KurrentTemporaryFixture { public string Group { get; set; } public string Stream { get; set; } KurrentPersistentSubscriptionsClient.PersistentSubscriptionResult? Subscription; - IAsyncEnumerator? Enumerator; + IAsyncEnumerator? Enumerator; - public NoDefaultCredentialsFixture() : base(x => x.WithoutDefaultCredentials()) { + public CustomFixture() : base(x => x.WithoutDefaultCredentials()) { Group = GetGroupName(); Stream = GetStreamName(); diff --git a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToStreamListTests.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToStream/SubscribeToStreamListTests.cs similarity index 100% rename from test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToStreamListTests.cs rename to test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToStream/SubscribeToStreamListTests.cs diff --git a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToStreamNoDefaultCredentialsTests.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToStream/SubscribeToStreamNoDefaultCredentialsTests.cs similarity index 100% rename from test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToStreamNoDefaultCredentialsTests.cs rename to test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToStream/SubscribeToStreamNoDefaultCredentialsTests.cs diff --git a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToStreamReplayParkedTests.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToStream/SubscribeToStreamReplayParkedTests.cs similarity index 100% rename from test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToStreamReplayParkedTests.cs rename to test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToStream/SubscribeToStreamReplayParkedTests.cs diff --git a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToStreamTests.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToStream/SubscribeToStreamTests.cs similarity index 100% rename from test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToStreamTests.cs rename to test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToStream/SubscribeToStreamTests.cs diff --git a/test/Kurrent.Client.Tests/Streams/Subscriptions/Obsolete/SubscribeToAllObsoleteTests.cs b/test/Kurrent.Client.Tests/Streams/Subscriptions/Obsolete/SubscribeToAllObsoleteTests.cs new file mode 100644 index 000000000..b42cf782e --- /dev/null +++ b/test/Kurrent.Client.Tests/Streams/Subscriptions/Obsolete/SubscribeToAllObsoleteTests.cs @@ -0,0 +1,616 @@ +using EventStore.Client; +using Kurrent.Client.Tests.Streams; +using Kurrent.Client.Tests.TestNode; + +namespace Kurrent.Client.Tests; + +[Trait("Category", "Target:Streams")] +[Trait("Category", "Operation:Subscriptions")] +[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] +public class SubscribeToAllObsoleteTests(ITestOutputHelper output, SubscribeToAllObsoleteTests.CustomFixture fixture) + : KurrentTemporaryTests(output, fixture) { + [Fact] + public async Task receives_all_events_from_start() { + var receivedAllEvents = new TaskCompletionSource(); + var subscriptionDropped = new TaskCompletionSource(); + + var seedEvents = Fixture.CreateTestEvents(10).ToArray(); + var pageSize = seedEvents.Length / 2; + + var availableEvents = new HashSet(seedEvents.Select(x => x.EventId)); + + foreach (var evt in seedEvents.Take(pageSize)) + await Fixture.Streams.AppendToStreamAsync($"stream-{evt.EventId.ToGuid():N}", StreamState.NoStream, new[] { evt }); + + using var subscription = await Fixture.Streams + .SubscribeToAllAsync(FromAll.Start, OnReceived, false, OnDropped) + .WithTimeout(); + + foreach (var evt in seedEvents.Skip(pageSize)) + await Fixture.Streams.AppendToStreamAsync($"stream-{evt.EventId.ToGuid():N}", StreamState.NoStream, new[] { evt }); + + await receivedAllEvents.Task.WithTimeout(); + + // if the subscription dropped before time, raise the reason why + if (subscriptionDropped.Task.IsCompleted) + subscriptionDropped.Task.IsCompleted.ShouldBe(false, subscriptionDropped.Task.Result.ToString()); + + // stop the subscription + subscription.Dispose(); + var result = await subscriptionDropped.Task.WithTimeout(); + result.ShouldBe(SubscriptionDroppedResult.Disposed()); + + return; + + Task OnReceived(StreamSubscription sub, ResolvedEvent re, CancellationToken ct) { + availableEvents.RemoveWhere(x => x == re.OriginalEvent.EventId); + + if (availableEvents.Count == 0) { + receivedAllEvents.TrySetResult(true); + Fixture.Log.Information("Received all {TotalEventsCount} expected events", seedEvents.Length); + } + + return Task.CompletedTask; + } + + void OnDropped(StreamSubscription sub, SubscriptionDroppedReason reason, Exception? ex) => + subscriptionDropped.SetResult(new(reason, ex)); + } + + [Fact] + public async Task receives_all_events_from_end() { + var receivedAllEvents = new TaskCompletionSource(); + var subscriptionDropped = new TaskCompletionSource(); + + var seedEvents = Fixture.CreateTestEvents(10).ToArray(); + + var availableEvents = new HashSet(seedEvents.Select(x => x.EventId)); + + using var subscription = await Fixture.Streams + .SubscribeToAllAsync(FromAll.End, OnReceived, false, OnDropped) + .WithTimeout(); + + // add the events we want to receive after we start the subscription + foreach (var evt in seedEvents) + await Fixture.Streams.AppendToStreamAsync($"stream-{evt.EventId.ToGuid():N}", StreamState.NoStream, new[] { evt }); + + await receivedAllEvents.Task.WithTimeout(); + + // if the subscription dropped before time, raise the reason why + if (subscriptionDropped.Task.IsCompleted) + subscriptionDropped.Task.IsCompleted.ShouldBe(false, subscriptionDropped.Task.Result.ToString()); + + // stop the subscription + subscription.Dispose(); + var result = await subscriptionDropped.Task.WithTimeout(); + result.ShouldBe(SubscriptionDroppedResult.Disposed()); + + return; + + Task OnReceived(StreamSubscription sub, ResolvedEvent re, CancellationToken ct) { + availableEvents.RemoveWhere(x => x == re.OriginalEvent.EventId); + + if (availableEvents.Count == 0) { + receivedAllEvents.TrySetResult(true); + Fixture.Log.Information("Received all {TotalEventsCount} expected events", seedEvents.Length); + } + + return Task.CompletedTask; + } + + void OnDropped(StreamSubscription sub, SubscriptionDroppedReason reason, Exception? ex) => + subscriptionDropped.SetResult(new(reason, ex)); + } + + [Fact] + public async Task receives_all_events_from_position() { + var receivedAllEvents = new TaskCompletionSource(); + var subscriptionDropped = new TaskCompletionSource(); + + var seedEvents = Fixture.CreateTestEvents(10).ToArray(); + var pageSize = seedEvents.Length / 2; + + // only the second half of the events will be received + var availableEvents = new HashSet(seedEvents.Skip(pageSize).Select(x => x.EventId)); + + IWriteResult writeResult = new SuccessResult(); + foreach (var evt in seedEvents.Take(pageSize)) + writeResult = await Fixture.Streams.AppendToStreamAsync($"stream-{evt.EventId.ToGuid():N}", StreamState.NoStream, new[] { evt }); + + var position = FromAll.After(writeResult.LogPosition); + + using var subscription = await Fixture.Streams + .SubscribeToAllAsync(position, OnReceived, false, OnDropped) + .WithTimeout(); + + foreach (var evt in seedEvents.Skip(pageSize)) + await Fixture.Streams.AppendToStreamAsync($"stream-{evt.EventId.ToGuid():N}", StreamState.NoStream, new[] { evt }); + + await receivedAllEvents.Task.WithTimeout(); + + // if the subscription dropped before time, raise the reason why + if (subscriptionDropped.Task.IsCompleted) + subscriptionDropped.Task.IsCompleted.ShouldBe(false, subscriptionDropped.Task.Result.ToString()); + + // stop the subscription + subscription.Dispose(); + var result = await subscriptionDropped.Task.WithTimeout(); + result.ShouldBe(SubscriptionDroppedResult.Disposed()); + + return; + + Task OnReceived(StreamSubscription sub, ResolvedEvent re, CancellationToken ct) { + availableEvents.RemoveWhere(x => x == re.OriginalEvent.EventId); + + if (availableEvents.Count == 0) { + receivedAllEvents.TrySetResult(true); + Fixture.Log.Information("Received all {TotalEventsCount} expected events", pageSize); + } + + return Task.CompletedTask; + } + + void OnDropped(StreamSubscription sub, SubscriptionDroppedReason reason, Exception? ex) => + subscriptionDropped.SetResult(new(reason, ex)); + } + + [Fact] + public async Task receives_all_events_with_resolved_links() { + var streamName = Fixture.GetStreamName(); + + var receivedAllEvents = new TaskCompletionSource(); + var subscriptionDropped = new TaskCompletionSource(); + + var seedEvents = Fixture.CreateTestEvents(3).ToArray(); + var availableEvents = new HashSet(seedEvents.Select(x => x.EventId)); + + await Fixture.Streams.AppendToStreamAsync(streamName, StreamState.NoStream, seedEvents); + + using var subscription = await Fixture.Streams + .SubscribeToAllAsync(FromAll.Start, OnReceived, true, OnDropped) + .WithTimeout(); + + await receivedAllEvents.Task.WithTimeout(); + + // if the subscription dropped before time, raise the reason why + if (subscriptionDropped.Task.IsCompleted) + subscriptionDropped.Task.IsCompleted.ShouldBe(false, subscriptionDropped.Task.Result.ToString()); + + // stop the subscription + subscription.Dispose(); + var result = await subscriptionDropped.Task.WithTimeout(); + result.ShouldBe(SubscriptionDroppedResult.Disposed()); + + return; + + Task OnReceived(StreamSubscription sub, ResolvedEvent re, CancellationToken ct) { + var hasResolvedLink = re.OriginalEvent.EventStreamId.StartsWith($"$et-{KurrentTemporaryFixture.TestEventType}"); + if (availableEvents.RemoveWhere(x => x == re.Event.EventId && hasResolvedLink) == 0) { + Fixture.Log.Debug("Received unexpected event {EventId} from stream {StreamId}", re.Event.EventId, re.OriginalEvent.EventStreamId); + return Task.CompletedTask; + } + + if (availableEvents.Count == 0) { + receivedAllEvents.TrySetResult(true); + Fixture.Log.Information("Received all {TotalEventsCount} expected events", seedEvents.Length); + } + + return Task.CompletedTask; + } + + void OnDropped(StreamSubscription sub, SubscriptionDroppedReason reason, Exception? ex) => + subscriptionDropped.SetResult(new(reason, ex)); + } + + [Theory] + [MemberData(nameof(SubscriptionFilter.TestCases), MemberType = typeof(SubscriptionFilter))] + public async Task receives_all_filtered_events_from_start(SubscriptionFilter filter) { + var streamPrefix = $"{nameof(receives_all_filtered_events_from_start)}-{filter.Name}-{Guid.NewGuid():N}"; + + Fixture.Log.Information("Using filter {FilterName} with prefix {StreamPrefix}", filter.Name, streamPrefix); + + var receivedAllEvents = new TaskCompletionSource(); + var subscriptionDropped = new TaskCompletionSource(); + var checkpointReached = new TaskCompletionSource(); + + var seedEvents = Fixture.CreateTestEvents(64) + .Select(evt => filter.PrepareEvent(streamPrefix, evt)) + .ToArray(); + + var pageSize = seedEvents.Length / 2; + + var availableEvents = new HashSet(seedEvents.Select(x => x.EventId)); + + // add noise + await Fixture.Streams.AppendToStreamAsync(Fixture.GetStreamName(), StreamState.NoStream, Fixture.CreateTestEvents(3)); + + var existingEventsCount = await Fixture.Streams.ReadAllAsync(Direction.Forwards, Position.Start).CountAsync(); + Fixture.Log.Debug("Existing events count: {ExistingEventsCount}", existingEventsCount); + + // Debugging: + // await foreach (var evt in Fixture.Streams.ReadAllAsync(Direction.Forwards, Position.Start)) + // Fixture.Log.Debug("Read event {EventId} from {StreamId}.", evt.OriginalEvent.EventId, evt.OriginalEvent.EventStreamId); + + // add some of the events we want to see before we start the subscription + foreach (var evt in seedEvents.Take(pageSize)) + await Fixture.Streams.AppendToStreamAsync($"{streamPrefix}-{evt.EventId.ToGuid():N}", StreamState.NoStream, new[] { evt }); + + var filterOptions = new SubscriptionFilterOptions(filter.Create(streamPrefix), 1, CheckpointReached); + + using var subscription = await Fixture.Streams + .SubscribeToAllAsync(FromAll.Start, OnReceived, false, OnDropped, filterOptions) + .WithTimeout(); + + // add some of the events we want to see after we start the subscription + foreach (var evt in seedEvents.Skip(pageSize)) + await Fixture.Streams.AppendToStreamAsync($"{streamPrefix}-{evt.EventId.ToGuid():N}", StreamState.NoStream, new[] { evt }); + + // wait until all events were received and at least one checkpoint was reached? + await receivedAllEvents.Task.WithTimeout(); + await checkpointReached.Task.WithTimeout(); + + // await Task.WhenAll(receivedAllEvents.Task, checkpointReached.Task).WithTimeout(); + + // if the subscription dropped before time, raise the reason why + if (subscriptionDropped.Task.IsCompleted) + subscriptionDropped.Task.IsCompleted.ShouldBe(false, subscriptionDropped.Task.Result.ToString()); + + // stop the subscription + subscription.Dispose(); + var result = await subscriptionDropped.Task.WithTimeout(); + result.ShouldBe(SubscriptionDroppedResult.Disposed()); + + Task OnReceived(StreamSubscription sub, ResolvedEvent re, CancellationToken ct) { + if (availableEvents.RemoveWhere(x => x == re.OriginalEvent.EventId) == 0) { + Fixture.Log.Error( + "Received unexpected event {EventId} from {StreamId}", + re.OriginalEvent.EventId, + re.OriginalEvent.EventStreamId + ); + + receivedAllEvents.TrySetException( + new InvalidOperationException($"Received unexpected event {re.OriginalEvent.EventId} from stream {re.OriginalEvent.EventStreamId}") + ); + } else { + Fixture.Log.Verbose("Received expected event {EventId} from {StreamId}.", re.OriginalEvent.EventId, re.OriginalEvent.EventStreamId); + } + + if (availableEvents.Count == 0) { + receivedAllEvents.TrySetResult(true); + Fixture.Log.Information("Received all {TotalEventsCount} expected events.", seedEvents.Length); + } + + return Task.CompletedTask; + } + + void OnDropped(StreamSubscription sub, SubscriptionDroppedReason reason, Exception? ex) { + subscriptionDropped.SetResult(new(reason, ex)); + if (reason != SubscriptionDroppedReason.Disposed) { + receivedAllEvents.TrySetException(ex!); + checkpointReached.TrySetException(ex!); + } + } + + Task CheckpointReached(StreamSubscription sub, Position position, CancellationToken ct) { + Fixture.Log.Verbose( + "Checkpoint reached {Position}. Received {ReceivedEventsCount}/{TotalEventsCount} events", + position, + seedEvents.Length - availableEvents.Count, + seedEvents.Length + ); + + checkpointReached.TrySetResult(true); + return Task.CompletedTask; + } + } + + [Theory] + [MemberData(nameof(SubscriptionFilter.TestCases), MemberType = typeof(SubscriptionFilter))] + public async Task receives_all_filtered_events_from_end(SubscriptionFilter filter) { + var streamPrefix = $"{nameof(receives_all_filtered_events_from_end)}-{filter.Name}-{Guid.NewGuid():N}"; + + Fixture.Log.Information("Using filter {FilterName} with prefix {StreamPrefix}", filter.Name, streamPrefix); + + var receivedAllEvents = new TaskCompletionSource(); + var subscriptionDropped = new TaskCompletionSource(); + var checkpointReached = new TaskCompletionSource(); + + var seedEvents = Fixture.CreateTestEvents(64) + .Select(evt => filter.PrepareEvent(streamPrefix, evt)) + .ToArray(); + + var pageSize = seedEvents.Length / 2; + + // only the second half of the events will be received + var availableEvents = new HashSet(seedEvents.Skip(pageSize).Select(x => x.EventId)); + + // add noise + await Fixture.Streams.AppendToStreamAsync(Fixture.GetStreamName(), StreamState.NoStream, Fixture.CreateTestEvents(3)); + + var existingEventsCount = await Fixture.Streams.ReadAllAsync(Direction.Forwards, Position.Start).CountAsync(); + Fixture.Log.Debug("Existing events count: {ExistingEventsCount}", existingEventsCount); + + // add some of the events that are a match to the filter but will not be received + foreach (var evt in seedEvents.Take(pageSize)) + await Fixture.Streams.AppendToStreamAsync($"{streamPrefix}-{evt.EventId.ToGuid():N}", StreamState.NoStream, new[] { evt }); + + var filterOptions = new SubscriptionFilterOptions(filter.Create(streamPrefix), 1, CheckpointReached); + + using var subscription = await Fixture.Streams + .SubscribeToAllAsync(FromAll.End, OnReceived, false, OnDropped, filterOptions) + .WithTimeout(); + + // add the events we want to receive after we start the subscription + foreach (var evt in seedEvents.Skip(pageSize)) + await Fixture.Streams.AppendToStreamAsync($"{streamPrefix}-{evt.EventId.ToGuid():N}", StreamState.NoStream, new[] { evt }); + + // wait until all events were received and at least one checkpoint was reached? + await receivedAllEvents.Task.WithTimeout(); + await checkpointReached.Task.WithTimeout(); + + // if the subscription dropped before time, raise the reason why + if (subscriptionDropped.Task.IsCompleted) + subscriptionDropped.Task.IsCompleted.ShouldBe(false, subscriptionDropped.Task.Result.ToString()); + + // stop the subscription + subscription.Dispose(); + var result = await subscriptionDropped.Task.WithTimeout(); + result.ShouldBe(SubscriptionDroppedResult.Disposed()); + + return; + + Task OnReceived(StreamSubscription sub, ResolvedEvent re, CancellationToken ct) { + if (availableEvents.RemoveWhere(x => x == re.OriginalEvent.EventId) == 0) { + Fixture.Log.Error( + "Received unexpected event {EventId} from {StreamId}", + re.OriginalEvent.EventId, + re.OriginalEvent.EventStreamId + ); + + receivedAllEvents.TrySetException( + new InvalidOperationException($"Received unexpected event {re.OriginalEvent.EventId} from stream {re.OriginalEvent.EventStreamId}") + ); + } else { + Fixture.Log.Verbose("Received expected event {EventId} from {StreamId}", re.OriginalEvent.EventId, re.OriginalEvent.EventStreamId); + } + + if (availableEvents.Count == 0) { + receivedAllEvents.TrySetResult(true); + Fixture.Log.Information("Received all {TotalEventsCount} expected events", pageSize); + } + + return Task.CompletedTask; + } + + void OnDropped(StreamSubscription sub, SubscriptionDroppedReason reason, Exception? ex) { + subscriptionDropped.SetResult(new(reason, ex)); + if (reason != SubscriptionDroppedReason.Disposed) { + receivedAllEvents.TrySetException(ex!); + checkpointReached.TrySetException(ex!); + } + } + + Task CheckpointReached(StreamSubscription sub, Position position, CancellationToken ct) { + Fixture.Log.Verbose( + "Checkpoint reached {Position}. Received {ReceivedEventsCount}/{TotalEventsCount} events", + position, + pageSize - availableEvents.Count, + pageSize + ); + + checkpointReached.TrySetResult(true); + return Task.CompletedTask; + } + } + + [Theory] + [MemberData(nameof(SubscriptionFilter.TestCases), MemberType = typeof(SubscriptionFilter))] + public async Task receives_all_filtered_events_from_position(SubscriptionFilter filter) { + var streamPrefix = $"{nameof(receives_all_filtered_events_from_position)}-{filter.Name}-{Guid.NewGuid():N}"; + + Fixture.Log.Information("Using filter {FilterName} with prefix {StreamPrefix}", filter.Name, streamPrefix); + + var receivedAllEvents = new TaskCompletionSource(); + var subscriptionDropped = new TaskCompletionSource(); + var checkpointReached = new TaskCompletionSource(); + + var seedEvents = Fixture.CreateTestEvents(64) + .Select(evt => filter.PrepareEvent(streamPrefix, evt)) + .ToArray(); + + var pageSize = seedEvents.Length / 2; + + // only the second half of the events will be received + var availableEvents = new HashSet(seedEvents.Skip(pageSize).Select(x => x.EventId)); + + // add noise + await Fixture.Streams.AppendToStreamAsync(Fixture.GetStreamName(), StreamState.NoStream, Fixture.CreateTestEvents(3)); + + var existingEventsCount = await Fixture.Streams.ReadAllAsync(Direction.Forwards, Position.Start).CountAsync(); + Fixture.Log.Debug("Existing events count: {ExistingEventsCount}", existingEventsCount); + + // add some of the events that are a match to the filter but will not be received + IWriteResult writeResult = new SuccessResult(); + foreach (var evt in seedEvents.Take(pageSize)) + writeResult = await Fixture.Streams.AppendToStreamAsync($"{streamPrefix}-{evt.EventId.ToGuid():N}", StreamState.NoStream, new[] { evt }); + + var position = FromAll.After(writeResult.LogPosition); + + var filterOptions = new SubscriptionFilterOptions(filter.Create(streamPrefix), 1, CheckpointReached); + + using var subscription = await Fixture.Streams + .SubscribeToAllAsync(position, OnReceived, false, OnDropped, filterOptions) + .WithTimeout(); + + // add the events we want to receive after we start the subscription + foreach (var evt in seedEvents.Skip(pageSize)) + await Fixture.Streams.AppendToStreamAsync($"{streamPrefix}-{evt.EventId.ToGuid():N}", StreamState.NoStream, new[] { evt }); + + // wait until all events were received and at least one checkpoint was reached? + await receivedAllEvents.Task.WithTimeout(); + await checkpointReached.Task.WithTimeout(); + + // if the subscription dropped before time, raise the reason why + if (subscriptionDropped.Task.IsCompleted) + subscriptionDropped.Task.IsCompleted.ShouldBe(false, subscriptionDropped.Task.Result.ToString()); + + // stop the subscription + subscription.Dispose(); + var result = await subscriptionDropped.Task.WithTimeout(); + result.ShouldBe(SubscriptionDroppedResult.Disposed()); + + return; + + Task OnReceived(StreamSubscription sub, ResolvedEvent re, CancellationToken ct) { + if (availableEvents.RemoveWhere(x => x == re.OriginalEvent.EventId) == 0) { + Fixture.Log.Error( + "Received unexpected event {EventId} from {StreamId}", + re.OriginalEvent.EventId, + re.OriginalEvent.EventStreamId + ); + + receivedAllEvents.TrySetException( + new InvalidOperationException($"Received unexpected event {re.OriginalEvent.EventId} from stream {re.OriginalEvent.EventStreamId}") + ); + } else { + Fixture.Log.Verbose("Received expected event {EventId} from {StreamId}", re.OriginalEvent.EventId, re.OriginalEvent.EventStreamId); + } + + if (availableEvents.Count == 0) { + receivedAllEvents.TrySetResult(true); + Fixture.Log.Information("Received all {TotalEventsCount} expected events", pageSize); + } + + return Task.CompletedTask; + } + + void OnDropped(StreamSubscription sub, SubscriptionDroppedReason reason, Exception? ex) { + subscriptionDropped.SetResult(new(reason, ex)); + if (reason != SubscriptionDroppedReason.Disposed) { + receivedAllEvents.TrySetException(ex!); + checkpointReached.TrySetException(ex!); + } + } + + Task CheckpointReached(StreamSubscription sub, Position position, CancellationToken ct) { + Fixture.Log.Verbose( + "Checkpoint reached {Position}. Received {ReceivedEventsCount}/{TotalEventsCount} events", + position, + pageSize - availableEvents.Count, + pageSize + ); + + checkpointReached.TrySetResult(true); + return Task.CompletedTask; + } + } + + [Fact] + public async Task receives_all_filtered_events_with_resolved_links() { + var streamName = Fixture.GetStreamName(); + + var receivedAllEvents = new TaskCompletionSource(); + var subscriptionDropped = new TaskCompletionSource(); + + var seedEvents = Fixture.CreateTestEvents(3).ToArray(); + var availableEvents = new HashSet(seedEvents.Select(x => x.EventId)); + + await Fixture.Streams.AppendToStreamAsync(streamName, StreamState.NoStream, seedEvents); + + var options = new SubscriptionFilterOptions(StreamFilter.Prefix($"$et-{KurrentTemporaryFixture.TestEventType}")); + + using var subscription = await Fixture.Streams + .SubscribeToAllAsync(FromAll.Start, OnReceived, true, OnDropped, options) + .WithTimeout(); + + await receivedAllEvents.Task.WithTimeout(); + + // if the subscription dropped before time, raise the reason why + if (subscriptionDropped.Task.IsCompleted) + subscriptionDropped.Task.IsCompleted.ShouldBe(false, subscriptionDropped.Task.Result.ToString()); + + // stop the subscription + subscription.Dispose(); + var result = await subscriptionDropped.Task.WithTimeout(); + result.ShouldBe(SubscriptionDroppedResult.Disposed()); + + return; + + Task OnReceived(StreamSubscription sub, ResolvedEvent re, CancellationToken ct) { + var hasResolvedLink = re.OriginalEvent.EventStreamId.StartsWith($"$et-{KurrentTemporaryFixture.TestEventType}"); + if (availableEvents.RemoveWhere(x => x == re.Event.EventId && hasResolvedLink) == 0) { + Fixture.Log.Debug("Received unexpected event {EventId} from stream {StreamId}", re.Event.EventId, re.OriginalEvent.EventStreamId); + return Task.CompletedTask; + } + + if (availableEvents.Count == 0) { + receivedAllEvents.TrySetResult(true); + Fixture.Log.Information("Received all {TotalEventsCount} expected events", seedEvents.Length); + } + + return Task.CompletedTask; + } + + void OnDropped(StreamSubscription sub, SubscriptionDroppedReason reason, Exception? ex) => + subscriptionDropped.SetResult(new(reason, ex)); + } + + [Fact] + public async Task drops_when_disposed() { + var subscriptionDropped = new TaskCompletionSource(); + + using var subscription = await Fixture.Streams + .SubscribeToAllAsync( + FromAll.Start, + (sub, re, ct) => Task.CompletedTask, + false, + (sub, reason, ex) => subscriptionDropped.SetResult(new(reason, ex)) + ) + .WithTimeout(); + + // if the subscription dropped before time, raise the reason why + if (subscriptionDropped.Task.IsCompleted) + subscriptionDropped.Task.IsCompleted.ShouldBe(false, subscriptionDropped.Task.Result.ToString()); + + // stop the subscription + subscription.Dispose(); + var result = await subscriptionDropped.Task.WithTimeout(); + result.ShouldBe(SubscriptionDroppedResult.Disposed()); + } + + [Fact] + public async Task drops_when_subscriber_error() { + var expectedResult = SubscriptionDroppedResult.SubscriberError(); + + var subscriptionDropped = new TaskCompletionSource(); + + using var subscription = await Fixture.Streams + .SubscribeToAllAsync( + FromAll.Start, + (sub, re, ct) => expectedResult.Throw(), + false, + (sub, reason, ex) => subscriptionDropped.SetResult(new(reason, ex)) + ) + .WithTimeout(); + + await Fixture.Streams.AppendToStreamAsync(Fixture.GetStreamName(), StreamState.NoStream, Fixture.CreateTestEvents()); + + var result = await subscriptionDropped.Task.WithTimeout(); + result.ShouldBe(expectedResult); + } + + public class CustomFixture : KurrentTemporaryFixture { + public CustomFixture() : base(x => x.RunProjections()) { + OnSetup = async () => { + await Streams.SetStreamMetadataAsync( + SystemStreams.AllStream, + StreamState.NoStream, + new(acl: new(SystemRoles.All)), + userCredentials: TestCredentials.Root + ); + + await Streams.AppendToStreamAsync($"SubscriptionsFixture-Noise-{Guid.NewGuid():N}", StreamState.NoStream, CreateTestEvents(10)); + }; + } + } +} diff --git a/test/Kurrent.Client.Tests/Streams/Subscriptions/Obsolete/SubscribeToStreamObsoleteTests.cs b/test/Kurrent.Client.Tests/Streams/Subscriptions/Obsolete/SubscribeToStreamObsoleteTests.cs new file mode 100644 index 000000000..2a2c3f10f --- /dev/null +++ b/test/Kurrent.Client.Tests/Streams/Subscriptions/Obsolete/SubscribeToStreamObsoleteTests.cs @@ -0,0 +1,322 @@ +using EventStore.Client; +using Kurrent.Client.Tests.TestNode; + +namespace Kurrent.Client.Tests; + +[Trait("Category", "Target:Streams")] +[Trait("Category", "Operation:Subscriptions")] +[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] +public class SubscribeToStreamObsoleteTests(ITestOutputHelper output, SubscribeToStreamObsoleteTests.CustomFixture fixture) + : KurrentTemporaryTests(output, fixture) { + [Fact] + public async Task receives_all_events_from_start() { + var streamName = Fixture.GetStreamName(); + + var receivedAllEvents = new TaskCompletionSource(); + var subscriptionDropped = new TaskCompletionSource(); + + var seedEvents = Fixture.CreateTestEvents(10).ToArray(); + var pageSize = seedEvents.Length / 2; + + var availableEvents = new HashSet(seedEvents.Select(x => x.EventId)); + + await Fixture.Streams.AppendToStreamAsync(streamName, StreamState.NoStream, seedEvents.Take(pageSize)); + + using var subscription = await Fixture.Streams + .SubscribeToStreamAsync(streamName, FromStream.Start, OnReceived, false, OnDropped) + .WithTimeout(); + + await Fixture.Streams.AppendToStreamAsync(streamName, StreamState.StreamExists, seedEvents.Skip(pageSize)); + + await receivedAllEvents.Task.WithTimeout(); + + // if the subscription dropped before time, raise the reason why + if (subscriptionDropped.Task.IsCompleted) + subscriptionDropped.Task.IsCompleted.ShouldBe(false, subscriptionDropped.Task.Result.ToString()); + + // stop the subscription + subscription.Dispose(); + var result = await subscriptionDropped.Task.WithTimeout(); + result.ShouldBe(SubscriptionDroppedResult.Disposed()); + + return; + + Task OnReceived(StreamSubscription sub, ResolvedEvent re, CancellationToken ct) { + availableEvents.RemoveWhere(x => x == re.OriginalEvent.EventId); + + if (availableEvents.Count == 0) { + receivedAllEvents.TrySetResult(true); + Fixture.Log.Information("Received all {TotalEventsCount} expected events", seedEvents.Length); + } + + return Task.CompletedTask; + } + + void OnDropped(StreamSubscription sub, SubscriptionDroppedReason reason, Exception? ex) => + subscriptionDropped.SetResult(new(reason, ex)); + } + + [Fact] + public async Task receives_all_events_from_position() { + var streamName = Fixture.GetStreamName(); + + var receivedAllEvents = new TaskCompletionSource(); + var subscriptionDropped = new TaskCompletionSource(); + + var seedEvents = Fixture.CreateTestEvents(10).ToArray(); + var pageSize = seedEvents.Length / 2; + + // only the second half of the events will be received + var availableEvents = new HashSet(seedEvents.Skip(pageSize).Select(x => x.EventId)); + + var writeResult = await Fixture.Streams.AppendToStreamAsync(streamName, StreamState.NoStream, seedEvents.Take(pageSize)); + var streamPosition = StreamPosition.FromStreamRevision(writeResult.NextExpectedStreamRevision); + var checkpoint = FromStream.After(streamPosition); + + using var subscription = await Fixture.Streams + .SubscribeToStreamAsync(streamName, checkpoint, OnReceived, false, OnDropped) + .WithTimeout(); + + await Fixture.Streams.AppendToStreamAsync(streamName, writeResult.NextExpectedStreamRevision, seedEvents.Skip(pageSize)); + + await receivedAllEvents.Task.WithTimeout(); + + // if the subscription dropped before time, raise the reason why + if (subscriptionDropped.Task.IsCompleted) + subscriptionDropped.Task.IsCompleted.ShouldBe(false, subscriptionDropped.Task.Result.ToString()); + + // stop the subscription + subscription.Dispose(); + var result = await subscriptionDropped.Task.WithTimeout(); + result.ShouldBe(SubscriptionDroppedResult.Disposed()); + + return; + + Task OnReceived(StreamSubscription sub, ResolvedEvent re, CancellationToken ct) { + availableEvents.RemoveWhere(x => x == re.OriginalEvent.EventId); + + if (availableEvents.Count == 0) { + receivedAllEvents.TrySetResult(true); + Fixture.Log.Information("Received all {TotalEventsCount} expected events", pageSize); + } + + return Task.CompletedTask; + } + + void OnDropped(StreamSubscription sub, SubscriptionDroppedReason reason, Exception? ex) => + subscriptionDropped.SetResult(new(reason, ex)); + } + + [Fact] + public async Task receives_all_events_from_non_existing_stream() { + var streamName = Fixture.GetStreamName(); + + var receivedAllEvents = new TaskCompletionSource(); + var subscriptionDropped = new TaskCompletionSource(); + + var seedEvents = Fixture.CreateTestEvents(10).ToArray(); + + var availableEvents = new HashSet(seedEvents.Select(x => x.EventId)); + + using var subscription = await Fixture.Streams + .SubscribeToStreamAsync(streamName, FromStream.Start, OnReceived, false, OnDropped) + .WithTimeout(); + + await Fixture.Streams.AppendToStreamAsync(streamName, StreamState.NoStream, seedEvents); + + await receivedAllEvents.Task.WithTimeout(); + + // if the subscription dropped before time, raise the reason why + if (subscriptionDropped.Task.IsCompleted) + subscriptionDropped.Task.IsCompleted.ShouldBe(false, subscriptionDropped.Task.Result.ToString()); + + // stop the subscription + subscription.Dispose(); + var result = await subscriptionDropped.Task.WithTimeout(); + result.ShouldBe(SubscriptionDroppedResult.Disposed()); + + return; + + Task OnReceived(StreamSubscription sub, ResolvedEvent re, CancellationToken ct) { + availableEvents.RemoveWhere(x => x == re.OriginalEvent.EventId); + + if (availableEvents.Count == 0) { + receivedAllEvents.TrySetResult(true); + Fixture.Log.Information("Received all {TotalEventsCount} expected events", seedEvents.Length); + } + + return Task.CompletedTask; + } + + void OnDropped(StreamSubscription sub, SubscriptionDroppedReason reason, Exception? ex) => + subscriptionDropped.SetResult(new(reason, ex)); + } + + [Fact] + public async Task allow_multiple_subscriptions_to_same_stream() { + var streamName = Fixture.GetStreamName(); + + var receivedAllEvents = new TaskCompletionSource(); + + var seedEvents = Fixture.CreateTestEvents(5).ToArray(); + + var targetEventsCount = seedEvents.Length * 2; + + await Fixture.Streams.AppendToStreamAsync(streamName, StreamState.NoStream, seedEvents); + + using var subscription1 = await Fixture.Streams + .SubscribeToStreamAsync(streamName, FromStream.Start, OnReceived) + .WithTimeout(); + + using var subscription2 = await Fixture.Streams + .SubscribeToStreamAsync(streamName, FromStream.Start, OnReceived) + .WithTimeout(); + + await receivedAllEvents.Task.WithTimeout(); + + Task OnReceived(StreamSubscription sub, ResolvedEvent re, CancellationToken ct) { + if (--targetEventsCount == 0) { + receivedAllEvents.TrySetResult(true); + Fixture.Log.Information("Received all {TotalEventsCount} expected events", seedEvents.Length); + } + + return Task.CompletedTask; + } + } + + [Fact] + public async Task drops_when_disposed() { + var streamName = Fixture.GetStreamName(); + + var subscriptionDropped = new TaskCompletionSource(); + + using var subscription = await Fixture.Streams + .SubscribeToStreamAsync( + streamName, + FromStream.Start, + (sub, re, ct) => Task.CompletedTask, + false, + (sub, reason, ex) => subscriptionDropped.SetResult(new(reason, ex)) + ) + .WithTimeout(); + + // if the subscription dropped before time, raise the reason why + if (subscriptionDropped.Task.IsCompleted) + subscriptionDropped.Task.IsCompleted.ShouldBe(false, subscriptionDropped.Task.Result.ToString()); + + // stop the subscription + subscription.Dispose(); + var result = await subscriptionDropped.Task.WithTimeout(); + result.ShouldBe(SubscriptionDroppedResult.Disposed()); + } + + [Fact] + public async Task drops_when_subscriber_error() { + var streamName = Fixture.GetStreamName(); + + var expectedResult = SubscriptionDroppedResult.SubscriberError(); + + var subscriptionDropped = new TaskCompletionSource(); + + using var subscription = await Fixture.Streams + .SubscribeToStreamAsync( + streamName, + FromStream.Start, + (sub, re, ct) => expectedResult.Throw(), + false, + (sub, reason, ex) => subscriptionDropped.SetResult(new(reason, ex)) + ) + .WithTimeout(); + + await Fixture.Streams.AppendToStreamAsync(streamName, StreamState.NoStream, Fixture.CreateTestEvents()); + + var result = await subscriptionDropped.Task.WithTimeout(); + result.ShouldBe(expectedResult); + } + + [Fact] + public async Task drops_when_stream_tombstoned() { + var streamName = Fixture.GetStreamName(); + + var subscriptionDropped = new TaskCompletionSource(); + + using var subscription = await Fixture.Streams + .SubscribeToStreamAsync( + streamName, + FromStream.Start, + (sub, re, ct) => Task.CompletedTask, + false, + (sub, reason, ex) => { subscriptionDropped.SetResult(new(reason, ex)); } + ) + .WithTimeout(); + + // rest in peace + await Fixture.Streams.TombstoneAsync(streamName, StreamState.NoStream); + + var result = await subscriptionDropped.Task.WithTimeout(); + result.Error.ShouldBeOfType().Stream.ShouldBe(streamName); + } + + [Fact] + public async Task receives_all_events_with_resolved_links() { + var streamName = Fixture.GetStreamName(); + + var receivedAllEvents = new TaskCompletionSource(); + var subscriptionDropped = new TaskCompletionSource(); + + var seedEvents = Fixture.CreateTestEvents(3).ToArray(); + var availableEvents = new HashSet(seedEvents.Select(x => x.EventId)); + + await Fixture.Streams.AppendToStreamAsync(streamName, StreamState.NoStream, seedEvents); + + using var subscription = await Fixture.Streams + .SubscribeToStreamAsync($"$et-{KurrentTemporaryFixture.TestEventType}", FromStream.Start, OnReceived, true, OnDropped) + .WithTimeout(); + + await receivedAllEvents.Task.WithTimeout(); + + // if the subscription dropped before time, raise the reason why + if (subscriptionDropped.Task.IsCompleted) + subscriptionDropped.Task.IsCompleted.ShouldBe(false, subscriptionDropped.Task.Result.ToString()); + + // stop the subscription + subscription.Dispose(); + var result = await subscriptionDropped.Task.WithTimeout(); + result.ShouldBe(SubscriptionDroppedResult.Disposed()); + + return; + + Task OnReceived(StreamSubscription sub, ResolvedEvent re, CancellationToken ct) { + var hasResolvedLink = re.OriginalEvent.EventStreamId.StartsWith($"$et-{KurrentTemporaryFixture.TestEventType}"); + if (availableEvents.RemoveWhere(x => x == re.Event.EventId && hasResolvedLink) == 0) { + Fixture.Log.Debug("Received unexpected event {EventId} from stream {StreamId}", re.Event.EventId, re.OriginalEvent.EventStreamId); + return Task.CompletedTask; + } + + if (availableEvents.Count == 0) { + receivedAllEvents.TrySetResult(true); + Fixture.Log.Information("Received all {TotalEventsCount} expected events", seedEvents.Length); + } + + return Task.CompletedTask; + } + + void OnDropped(StreamSubscription sub, SubscriptionDroppedReason reason, Exception? ex) => + subscriptionDropped.SetResult(new(reason, ex)); + } + + public class CustomFixture : KurrentTemporaryFixture { + public CustomFixture() : base(x => x.RunProjections()) { + OnSetup = async () => { + await Streams.SetStreamMetadataAsync( + SystemStreams.AllStream, + StreamState.NoStream, + new(acl: new(SystemRoles.All)), + userCredentials: TestCredentials.Root + ); + + await Streams.AppendToStreamAsync($"SubscriptionsFixture-Noise-{Guid.NewGuid():N}", StreamState.NoStream, CreateTestEvents(10)); + }; + } + } +} diff --git a/test/Kurrent.Client.Tests/Streams/Subscriptions/SubscribeToAllTests.cs b/test/Kurrent.Client.Tests/Streams/Subscriptions/SubscribeToAllTests.cs new file mode 100644 index 000000000..33d63b280 --- /dev/null +++ b/test/Kurrent.Client.Tests/Streams/Subscriptions/SubscribeToAllTests.cs @@ -0,0 +1,497 @@ +using EventStore.Client; +using Kurrent.Client.Tests.Streams; +using Kurrent.Client.Tests.TestNode; + +namespace Kurrent.Client.Tests; + +[Trait("Category", "Subscriptions")] +[Trait("Category", "Target:Streams")] +public class SubscribeToAllTests(ITestOutputHelper output, SubscribeToAllTests.CustomFixture fixture) + : KurrentTemporaryTests(output, fixture) { + [Fact] + public async Task receives_all_events_from_start() { + var seedEvents = Fixture.CreateTestEvents(10).ToArray(); + var pageSize = seedEvents.Length / 2; + + var availableEvents = new HashSet(seedEvents.Select(x => x.EventId)); + + foreach (var evt in seedEvents.Take(pageSize)) + await Fixture.Streams.AppendToStreamAsync( + $"stream-{evt.EventId.ToGuid():N}", + StreamState.NoStream, + new[] { evt } + ); + + await using var subscription = Fixture.Streams.SubscribeToAll(FromAll.Start); + await using var enumerator = subscription.Messages.GetAsyncEnumerator(); + + Assert.True(await enumerator.MoveNextAsync()); + + Assert.IsType(enumerator.Current); + + foreach (var evt in seedEvents.Skip(pageSize)) + await Fixture.Streams.AppendToStreamAsync( + $"stream-{evt.EventId.ToGuid():N}", + StreamState.NoStream, + new[] { evt } + ); + + await Subscribe().WithTimeout(); + + return; + + async Task Subscribe() { + while (await enumerator.MoveNextAsync()) { + if (enumerator.Current is not StreamMessage.Event(var resolvedEvent)) { + continue; + } + + availableEvents.Remove(resolvedEvent.Event.EventId); + + if (availableEvents.Count == 0) { + return; + } + } + } + } + + [Fact] + public async Task receives_all_events_from_end() { + var seedEvents = Fixture.CreateTestEvents(10).ToArray(); + + var availableEvents = new HashSet(seedEvents.Select(x => x.EventId)); + + await using var subscription = Fixture.Streams.SubscribeToAll(FromAll.End); + await using var enumerator = subscription.Messages.GetAsyncEnumerator(); + + Assert.True(await enumerator.MoveNextAsync()); + + Assert.IsType(enumerator.Current); + + // add the events we want to receive after we start the subscription + foreach (var evt in seedEvents) + await Fixture.Streams.AppendToStreamAsync( + $"stream-{evt.EventId.ToGuid():N}", + StreamState.NoStream, + new[] { evt } + ); + + await Subscribe().WithTimeout(); + + return; + + async Task Subscribe() { + while (await enumerator.MoveNextAsync()) { + if (enumerator.Current is not StreamMessage.Event(var resolvedEvent)) { + continue; + } + + availableEvents.Remove(resolvedEvent.OriginalEvent.EventId); + + if (availableEvents.Count == 0) { + return; + } + } + } + } + + [Fact] + public async Task receives_all_events_from_position() { + var seedEvents = Fixture.CreateTestEvents(10).ToArray(); + var pageSize = seedEvents.Length / 2; + + // only the second half of the events will be received + var availableEvents = new HashSet(seedEvents.Skip(pageSize).Select(x => x.EventId)); + + IWriteResult writeResult = new SuccessResult(); + foreach (var evt in seedEvents.Take(pageSize)) + writeResult = await Fixture.Streams.AppendToStreamAsync( + $"stream-{evt.EventId.ToGuid():N}", + StreamState.NoStream, + new[] { evt } + ); + + var position = FromAll.After(writeResult.LogPosition); + + await using var subscription = Fixture.Streams.SubscribeToAll(position); + await using var enumerator = subscription.Messages.GetAsyncEnumerator(); + + Assert.True(await enumerator.MoveNextAsync()); + + Assert.IsType(enumerator.Current); + + foreach (var evt in seedEvents.Skip(pageSize)) + await Fixture.Streams.AppendToStreamAsync( + $"stream-{evt.EventId.ToGuid():N}", + StreamState.NoStream, + new[] { evt } + ); + + await Subscribe().WithTimeout(); + + return; + + async Task Subscribe() { + while (await enumerator.MoveNextAsync()) { + if (enumerator.Current is not StreamMessage.Event(var resolvedEvent)) { + continue; + } + + availableEvents.Remove(resolvedEvent.Event.EventId); + + if (availableEvents.Count == 0) { + return; + } + } + } + } + + [Fact] + public async Task receives_all_events_with_resolved_links() { + var streamName = Fixture.GetStreamName(); + + var seedEvents = Fixture.CreateTestEvents(3).ToArray(); + var availableEvents = new HashSet(seedEvents.Select(x => x.EventId)); + + await Fixture.Streams.AppendToStreamAsync(streamName, StreamState.NoStream, seedEvents); + + await using var subscription = Fixture.Streams.SubscribeToAll(FromAll.Start, true); + await using var enumerator = subscription.Messages.GetAsyncEnumerator(); + + Assert.True(await enumerator.MoveNextAsync()); + + Assert.IsType(enumerator.Current); + + await Subscribe().WithTimeout(); + + return; + + async Task Subscribe() { + while (await enumerator.MoveNextAsync()) { + if (enumerator.Current is not StreamMessage.Event(var resolvedEvent)) { + continue; + } + + availableEvents.Remove(resolvedEvent.Event.EventId); + + if (availableEvents.Count == 0) { + return; + } + } + } + } + + [Theory] + [MemberData(nameof(SubscriptionFilter.TestCases), MemberType = typeof(SubscriptionFilter))] + public async Task receives_all_filtered_events_from_start(SubscriptionFilter filter) { + var streamPrefix = $"{nameof(receives_all_filtered_events_from_start)}-{filter.Name}-{Guid.NewGuid():N}"; + + Fixture.Log.Information("Using filter {FilterName} with prefix {StreamPrefix}", filter.Name, streamPrefix); + + var seedEvents = Fixture.CreateTestEvents(64) + .Select(evt => filter.PrepareEvent(streamPrefix, evt)) + .ToArray(); + + var pageSize = seedEvents.Length / 2; + + var availableEvents = new HashSet(seedEvents.Select(x => x.EventId)); + + // add noise + await Fixture.Streams.AppendToStreamAsync( + Fixture.GetStreamName(), + StreamState.NoStream, + Fixture.CreateTestEvents(3) + ); + + var existingEventsCount = await Fixture.Streams.ReadAllAsync(Direction.Forwards, Position.Start) + .Messages.CountAsync(); + + Fixture.Log.Debug("Existing events count: {ExistingEventsCount}", existingEventsCount); + + // Debugging: + // await foreach (var evt in Fixture.Streams.ReadAllAsync(Direction.Forwards, Position.Start)) + // Fixture.Log.Debug("Read event {EventId} from {StreamId}.", evt.OriginalEvent.EventId, evt.OriginalEvent.EventStreamId); + + // add some of the events we want to see before we start the subscription + foreach (var evt in seedEvents.Take(pageSize)) + await Fixture.Streams.AppendToStreamAsync( + $"{streamPrefix}-{evt.EventId.ToGuid():N}", + StreamState.NoStream, + new[] { evt } + ); + + var filterOptions = new SubscriptionFilterOptions(filter.Create(streamPrefix), 1); + + await using var subscription = Fixture.Streams.SubscribeToAll(FromAll.Start, filterOptions: filterOptions); + await using var enumerator = subscription.Messages.GetAsyncEnumerator(); + + Assert.True(await enumerator.MoveNextAsync()); + + Assert.IsType(enumerator.Current); + + // add some of the events we want to see after we start the subscription + foreach (var evt in seedEvents.Skip(pageSize)) + await Fixture.Streams.AppendToStreamAsync( + $"{streamPrefix}-{evt.EventId.ToGuid():N}", + StreamState.NoStream, + new[] { evt } + ); + + bool checkpointReached = false; + + await Subscribe().WithTimeout(); + + Assert.True(checkpointReached); + + return; + + async Task Subscribe() { + while (await enumerator.MoveNextAsync()) { + switch (enumerator.Current) { + case StreamMessage.AllStreamCheckpointReached: + checkpointReached = true; + + break; + + case StreamMessage.Event(var resolvedEvent): { + availableEvents.Remove(resolvedEvent.Event.EventId); + + if (availableEvents.Count == 0) { + return; + } + + break; + } + } + } + } + } + + [Theory] + [MemberData(nameof(SubscriptionFilter.TestCases), MemberType = typeof(SubscriptionFilter))] + public async Task receives_all_filtered_events_from_end(SubscriptionFilter filter) { + var streamPrefix = $"{nameof(receives_all_filtered_events_from_end)}-{filter.Name}-{Guid.NewGuid():N}"; + + Fixture.Log.Information("Using filter {FilterName} with prefix {StreamPrefix}", filter.Name, streamPrefix); + + var seedEvents = Fixture.CreateTestEvents(64) + .Select(evt => filter.PrepareEvent(streamPrefix, evt)) + .ToArray(); + + var pageSize = seedEvents.Length / 2; + + // only the second half of the events will be received + var availableEvents = new HashSet(seedEvents.Skip(pageSize).Select(x => x.EventId)); + + // add noise + await Fixture.Streams.AppendToStreamAsync( + Fixture.GetStreamName(), + StreamState.NoStream, + Fixture.CreateTestEvents(3) + ); + + var existingEventsCount = await Fixture.Streams.ReadAllAsync(Direction.Forwards, Position.Start) + .Messages.CountAsync(); + + Fixture.Log.Debug("Existing events count: {ExistingEventsCount}", existingEventsCount); + + // Debugging: + // await foreach (var evt in Fixture.Streams.ReadAllAsync(Direction.Forwards, Position.Start)) + // Fixture.Log.Debug("Read event {EventId} from {StreamId}.", evt.OriginalEvent.EventId, evt.OriginalEvent.EventStreamId); + + // add some of the events we want to see before we start the subscription + foreach (var evt in seedEvents.Take(pageSize)) + await Fixture.Streams.AppendToStreamAsync( + $"{streamPrefix}-{evt.EventId.ToGuid():N}", + StreamState.NoStream, + new[] { evt } + ); + + var filterOptions = new SubscriptionFilterOptions(filter.Create(streamPrefix), 1); + + await using var subscription = Fixture.Streams.SubscribeToAll(FromAll.End, filterOptions: filterOptions); + await using var enumerator = subscription.Messages.GetAsyncEnumerator(); + + Assert.True(await enumerator.MoveNextAsync()); + + Assert.IsType(enumerator.Current); + + // add some of the events we want to see after we start the subscription + foreach (var evt in seedEvents.Skip(pageSize)) + await Fixture.Streams.AppendToStreamAsync( + $"{streamPrefix}-{evt.EventId.ToGuid():N}", + StreamState.NoStream, + new[] { evt } + ); + + bool checkpointReached = false; + + await Subscribe().WithTimeout(); + + Assert.True(checkpointReached); + + return; + + async Task Subscribe() { + while (await enumerator.MoveNextAsync()) { + switch (enumerator.Current) { + case StreamMessage.AllStreamCheckpointReached: + checkpointReached = true; + + break; + + case StreamMessage.Event(var resolvedEvent): { + availableEvents.Remove(resolvedEvent.Event.EventId); + + if (availableEvents.Count == 0) { + return; + } + + break; + } + } + } + } + } + + [Theory] + [MemberData(nameof(SubscriptionFilter.TestCases), MemberType = typeof(SubscriptionFilter))] + public async Task receives_all_filtered_events_from_position(SubscriptionFilter filter) { + var streamPrefix = $"{nameof(receives_all_filtered_events_from_position)}-{filter.Name}-{Guid.NewGuid():N}"; + + Fixture.Log.Information("Using filter {FilterName} with prefix {StreamPrefix}", filter.Name, streamPrefix); + + var seedEvents = Fixture.CreateTestEvents(64) + .Select(evt => filter.PrepareEvent(streamPrefix, evt)) + .ToArray(); + + var pageSize = seedEvents.Length / 2; + + // only the second half of the events will be received + var availableEvents = new HashSet(seedEvents.Skip(pageSize).Select(x => x.EventId)); + + // add noise + await Fixture.Streams.AppendToStreamAsync( + Fixture.GetStreamName(), + StreamState.NoStream, + Fixture.CreateTestEvents(3) + ); + + var existingEventsCount = await Fixture.Streams.ReadAllAsync(Direction.Forwards, Position.Start) + .Messages.CountAsync(); + + Fixture.Log.Debug("Existing events count: {ExistingEventsCount}", existingEventsCount); + + // add some of the events that are a match to the filter but will not be received + IWriteResult writeResult = new SuccessResult(); + foreach (var evt in seedEvents.Take(pageSize)) + writeResult = await Fixture.Streams.AppendToStreamAsync( + $"{streamPrefix}-{evt.EventId.ToGuid():N}", + StreamState.NoStream, + new[] { evt } + ); + + var position = FromAll.After(writeResult.LogPosition); + + var filterOptions = new SubscriptionFilterOptions(filter.Create(streamPrefix), 1); + + await using var subscription = Fixture.Streams.SubscribeToAll(position, filterOptions: filterOptions); + await using var enumerator = subscription.Messages.GetAsyncEnumerator(); + + Assert.True(await enumerator.MoveNextAsync()); + + Assert.IsType(enumerator.Current); + + // add the events we want to receive after we start the subscription + foreach (var evt in seedEvents.Skip(pageSize)) + await Fixture.Streams.AppendToStreamAsync( + $"{streamPrefix}-{evt.EventId.ToGuid():N}", + StreamState.NoStream, + new[] { evt } + ); + + bool checkpointReached = false; + + await Subscribe().WithTimeout(); + + Assert.True(checkpointReached); + + return; + + async Task Subscribe() { + while (await enumerator.MoveNextAsync()) { + switch (enumerator.Current) { + case StreamMessage.AllStreamCheckpointReached: + checkpointReached = true; + + break; + + case StreamMessage.Event(var resolvedEvent): { + availableEvents.Remove(resolvedEvent.Event.EventId); + + if (availableEvents.Count == 0) { + return; + } + + break; + } + } + } + } + } + + [Fact] + public async Task receives_all_filtered_events_with_resolved_links() { + var streamName = Fixture.GetStreamName(); + + var seedEvents = Fixture.CreateTestEvents(3).ToArray(); + var availableEvents = new HashSet(seedEvents.Select(x => x.EventId)); + + await Fixture.Streams.AppendToStreamAsync(streamName, StreamState.NoStream, seedEvents); + + var filterOptions = new SubscriptionFilterOptions(StreamFilter.Prefix($"$et-{KurrentPermanentFixture.TestEventType}")); + + await using var subscription = + Fixture.Streams.SubscribeToAll(FromAll.Start, true, filterOptions: filterOptions); + + await using var enumerator = subscription.Messages.GetAsyncEnumerator(); + + Assert.True(await enumerator.MoveNextAsync()); + + Assert.IsType(enumerator.Current); + + await Subscribe().WithTimeout(); + + return; + + async Task Subscribe() { + while (await enumerator.MoveNextAsync()) { + if (enumerator.Current is not StreamMessage.Event(var resolvedEvent) || + !resolvedEvent.OriginalEvent.EventStreamId.StartsWith($"$et-{KurrentPermanentFixture.TestEventType}")) { + continue; + } + + availableEvents.Remove(resolvedEvent.Event.EventId); + + if (availableEvents.Count == 0) { + return; + } + } + } + } + + public class CustomFixture : KurrentTemporaryFixture { + public CustomFixture() : base(x => x.RunProjections()) { + OnSetup = async () => { + await Streams.SetStreamMetadataAsync( + SystemStreams.AllStream, + StreamState.NoStream, + new(acl: new(SystemRoles.All)), + userCredentials: TestCredentials.Root + ); + + await Streams.AppendToStreamAsync($"SubscriptionsFixture-Noise-{Guid.NewGuid():N}", StreamState.NoStream, CreateTestEvents(10)); + }; + } + } +} diff --git a/test/Kurrent.Client.Tests/Streams/SubscribeToStreamTests.cs b/test/Kurrent.Client.Tests/Streams/Subscriptions/SubscribeToStreamTests.cs similarity index 100% rename from test/Kurrent.Client.Tests/Streams/SubscribeToStreamTests.cs rename to test/Kurrent.Client.Tests/Streams/Subscriptions/SubscribeToStreamTests.cs diff --git a/test/Kurrent.Client.Tests/Streams/Subscriptions/SubscriptionDroppedResult.cs b/test/Kurrent.Client.Tests/Streams/Subscriptions/SubscriptionDroppedResult.cs new file mode 100644 index 000000000..6157c6cb5 --- /dev/null +++ b/test/Kurrent.Client.Tests/Streams/Subscriptions/SubscriptionDroppedResult.cs @@ -0,0 +1,18 @@ +using EventStore.Client; + +namespace Kurrent.Client.Tests; + +public record SubscriptionDroppedResult(SubscriptionDroppedReason Reason, Exception? Error) { + public Task Throw() => Task.FromException(Error!); + + public static SubscriptionDroppedResult ServerError(Exception? error = null) => + new(SubscriptionDroppedReason.ServerError, error ?? new Exception("Server error")); + + public static SubscriptionDroppedResult SubscriberError(Exception? error = null) => + new(SubscriptionDroppedReason.SubscriberError, error ?? new Exception("Subscriber error")); + + public static SubscriptionDroppedResult Disposed(Exception? error = null) => + new(SubscriptionDroppedReason.Disposed, error); + + public override string ToString() => $"{Reason} {Error?.Message ?? string.Empty}".Trim(); +} From d98910a1e533e59697704f8eb3f75d2751421dbb Mon Sep 17 00:00:00 2001 From: William Chong Date: Wed, 8 Jan 2025 17:08:33 +0400 Subject: [PATCH 13/15] Temporarily disable Public actions --- .github/workflows/publish.yml | 401 +++++++++++++++++----------------- Kurrent.Client.sln | 14 -- samples/Directory.Build.props | 2 +- 3 files changed, 202 insertions(+), 215 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 0aabc306f..ef6dfb6bc 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,200 +1,201 @@ -name: Publish - -on: - pull_request: - push: - branches: - - master - tags: - - v* - -jobs: - vulnerability-scan: - timeout-minutes: 10 - strategy: - fail-fast: false - matrix: - framework: [ net8.0, net9.0 ] - os: [ ubuntu-latest, windows-latest ] - runs-on: ${{ matrix.os }} - name: scan-vulnerabilities/${{ matrix.os }}/${{ matrix.framework }} - steps: - - name: Checkout - uses: actions/checkout@v3 - - name: Install dotnet SDKs - uses: actions/setup-dotnet@v3 - with: - dotnet-version: | - 8.0.x - 9.0.x - - name: Scan for Vulnerabilities - shell: bash - run: | - dotnet nuget list source - dotnet restore - dotnet list package --vulnerable --include-transitive --framework ${{ matrix.framework }} | tee vulnerabilities.txt - ! cat vulnerabilities.txt | grep -q "has the following vulnerable packages" - - build-samples: - timeout-minutes: 5 - name: build-samples/${{ matrix.framework }} - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - framework: [ net8.0, net9.0 ] - services: - esdb: - image: docker.eventstore.com/eventstore-ce/eventstoredb-ce:lts - env: - EVENTSTORE_INSECURE: true - EVENTSTORE_MEM_DB: false - EVENTSTORE_RUN_PROJECTIONS: all - EVENTSTORE_START_STANDARD_PROJECTIONS: true - ports: - - 2113:2113 - options: --health-cmd "exit 0" - steps: - - name: Checkout - uses: actions/checkout@v3 - - name: Install dotnet SDKs - uses: actions/setup-dotnet@v3 - with: - dotnet-version: | - 8.0.x - 9.0.x - - name: Compile - shell: bash - run: | - dotnet build samples - - name: Run - shell: bash - run: | - find samples/ -type f -iname "*.csproj" -print0 | xargs -0L1 dotnet run --framework ${{ matrix.framework }} --project - - generate-certificates: - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - name: Generate certificates - run: | - mkdir -p certs - docker run --rm --user root --volume "$PWD/certs:/tmp" docker.eventstore.com/eventstore-utils/es-gencert-cli:latest create-ca -out /tmp/ca - docker run --rm --user root --volume "$PWD/certs:/tmp" docker.eventstore.com/eventstore-utils/es-gencert-cli:latest create-node -ca-certificate /tmp/ca/ca.crt -ca-key /tmp/ca/ca.key -out /tmp/node -ip-addresses 127.0.0.1 -dns-names localhost - docker run --rm --user root --volume "$PWD/certs:/tmp" docker.eventstore.com/eventstore-utils/es-gencert-cli:latest create-user -username admin -ca-certificate /tmp/ca/ca.crt -ca-key /tmp/ca/ca.key -out /tmp/user-admin - docker run --rm --user root --volume "$PWD/certs:/tmp" docker.eventstore.com/eventstore-utils/es-gencert-cli:latest create-user -username invalid -ca-certificate /tmp/ca/ca.crt -ca-key /tmp/ca/ca.key -out /tmp/user-invalid - - name: Set permissions on certificates - run: | - sudo chown -R $USER:$USER certs - sudo chmod -R 755 certs - - name: Upload certificates - uses: actions/upload-artifact@v4 - with: - name: certs - path: certs - - test: - needs: generate-certificates - timeout-minutes: 10 - strategy: - fail-fast: false - matrix: - framework: [ net8.0, 9.0 ] - os: [ ubuntu-latest, windows-latest ] - configuration: [ release ] - test: [ Streams, PersistentSubscriptions, Operations, ProjectionManagement, UserManagement, Security, Misc ] - runs-on: ${{ matrix.os }} - name: ${{ inputs.test }} (${{ matrix.os }}, ${{ matrix.framework }}) - steps: - - name: Checkout - uses: actions/checkout@v3 - - shell: bash - run: | - git fetch --prune --unshallow - - name: Install dotnet SDKs - uses: actions/setup-dotnet@v3 - with: - dotnet-version: | - 8.0.x - 9.0.x - - name: Compile - shell: bash - run: | - dotnet build --configuration ${{ matrix.configuration }} --framework ${{ matrix.framework }} src/Kurrent.Client - - name: Download certificates - uses: actions/download-artifact@v4 - with: - name: certs - path: certs - - name: Run Tests (Linux) - if: runner.os == 'Linux' - shell: bash - run: | - dotnet test --configuration ${{ matrix.configuration }} --blame \ - --logger:"GitHubActions;report-warnings=false" --logger:"console;verbosity=normal" \ - --filter "Category=Target:${{ inputs.test }}" \ - --framework ${{ matrix.framework }} \ - test/Kurrent.Client.Tests - - name: Run Tests (Windows) - if: runner.os == 'Windows' - shell: pwsh - run: | - dotnet test --configuration ${{ matrix.configuration }} --blame ` - --logger:"GitHubActions;report-warnings=false" --logger:"console;verbosity=normal" ` - --framework ${{ matrix.framework }} ` - --filter "Category=Target:${{ inputs.test }}" ` - test/Kurrent.Client.Tests - - publish: - timeout-minutes: 5 - needs: [ vulnerability-scan, test, build-samples ] - runs-on: ubuntu-latest - name: publish - steps: - - name: Checkout - uses: actions/checkout@v3 - - name: Get Version - id: get_version - run: | - echo "branch=${GITHUB_REF:10}" >> $GITHUB_OUTPUT - dotnet nuget list source - dotnet tool restore - version=$(dotnet tool run minver -- --tag-prefix=v) - echo "version=${version}" >> $GITHUB_OUTPUT - - shell: bash - run: | - git fetch --prune --unshallow - - name: Install dotnet SDKs - uses: actions/setup-dotnet@v3 - with: - dotnet-version: | - 8.0.x - 9.0.x - - name: Dotnet Pack - shell: bash - run: | - mkdir -p packages - dotnet pack /p:Version=${{ steps.get_version.outputs.version }} --configuration=Release \ - /p:PublishDir=./packages \ - /p:NoWarn=NU5105 \ - /p:RepositoryUrl=https://github.com/EventStore/EventStore-Client-Dotnet \ - /p:RepositoryType=git - - name: Publish Artifacts - uses: actions/upload-artifact@v4 - with: - path: packages - name: nuget-packages - - name: Dotnet Push to Github Packages - shell: bash - if: github.event_name == 'push' - run: | - dotnet tool restore - find . -name "*.nupkg" | xargs -n1 dotnet nuget push --api-key=${{ secrets.github_token }} --source https://nuget.pkg.github.com/EventStore/index.json --skip-duplicate - - name: Dotnet Push to Nuget.org - shell: bash - if: contains(steps.get_version.outputs.branch, 'v') - run: | - dotnet nuget list source - dotnet tool restore - find . -name "*.nupkg" | xargs -n1 dotnet nuget push --api-key=${{ secrets.nuget_key }} --source https://api.nuget.org/v3/index.json --skip-duplicate +#name: Publish +# +#on: +# pull_request: +# push: +# branches: +# - master +# tags: +# - v* +# +#jobs: +# vulnerability-scan: +# timeout-minutes: 10 +# strategy: +# fail-fast: false +# matrix: +# framework: [ net8.0, net9.0 ] +# os: [ ubuntu-latest, windows-latest ] +# runs-on: ${{ matrix.os }} +# name: scan-vulnerabilities/${{ matrix.os }}/${{ matrix.framework }} +# steps: +# - name: Checkout +# uses: actions/checkout@v3 +# - name: Install dotnet SDKs +# uses: actions/setup-dotnet@v3 +# with: +# dotnet-version: | +# 8.0.x +# 9.0.x +# - name: Scan for Vulnerabilities +# shell: bash +# run: | +# dotnet nuget list source +# dotnet restore ./src/Kurrent.Client/Kurrent.Client.csproj +# dotnet restore ./test/Kurrent.Client.Tests/Kurrent.Client.Tests.csproj +# dotnet list package --vulnerable --include-transitive --framework ${{ matrix.framework }} | tee vulnerabilities.txt +# ! cat vulnerabilities.txt | grep -q "has the following vulnerable packages" +# +# build-samples: +# timeout-minutes: 5 +# name: build-samples/${{ matrix.framework }} +# runs-on: ubuntu-latest +# strategy: +# fail-fast: false +# matrix: +# framework: [ net8.0, net9.0 ] +# services: +# esdb: +# image: docker.eventstore.com/eventstore-ce/eventstoredb-ce:lts +# env: +# EVENTSTORE_INSECURE: true +# EVENTSTORE_MEM_DB: false +# EVENTSTORE_RUN_PROJECTIONS: all +# EVENTSTORE_START_STANDARD_PROJECTIONS: true +# ports: +# - 2113:2113 +# options: --health-cmd "exit 0" +# steps: +# - name: Checkout +# uses: actions/checkout@v3 +# - name: Install dotnet SDKs +# uses: actions/setup-dotnet@v3 +# with: +# dotnet-version: | +# 8.0.x +# 9.0.x +# - name: Compile +# shell: bash +# run: | +# dotnet build samples +# - name: Run +# shell: bash +# run: | +# find samples/ -type f -iname "*.csproj" -print0 | xargs -0L1 dotnet run --framework ${{ matrix.framework }} --project +# +# generate-certificates: +# runs-on: ubuntu-latest +# steps: +# - name: Checkout code +# uses: actions/checkout@v4 +# - name: Generate certificates +# run: | +# mkdir -p certs +# docker run --rm --user root --volume "$PWD/certs:/tmp" docker.eventstore.com/eventstore-utils/es-gencert-cli:latest create-ca -out /tmp/ca +# docker run --rm --user root --volume "$PWD/certs:/tmp" docker.eventstore.com/eventstore-utils/es-gencert-cli:latest create-node -ca-certificate /tmp/ca/ca.crt -ca-key /tmp/ca/ca.key -out /tmp/node -ip-addresses 127.0.0.1 -dns-names localhost +# docker run --rm --user root --volume "$PWD/certs:/tmp" docker.eventstore.com/eventstore-utils/es-gencert-cli:latest create-user -username admin -ca-certificate /tmp/ca/ca.crt -ca-key /tmp/ca/ca.key -out /tmp/user-admin +# docker run --rm --user root --volume "$PWD/certs:/tmp" docker.eventstore.com/eventstore-utils/es-gencert-cli:latest create-user -username invalid -ca-certificate /tmp/ca/ca.crt -ca-key /tmp/ca/ca.key -out /tmp/user-invalid +# - name: Set permissions on certificates +# run: | +# sudo chown -R $USER:$USER certs +# sudo chmod -R 755 certs +# - name: Upload certificates +# uses: actions/upload-artifact@v4 +# with: +# name: certs +# path: certs +# +# test: +# needs: generate-certificates +# timeout-minutes: 10 +# strategy: +# fail-fast: false +# matrix: +# framework: [ net8.0, net9.0 ] +# os: [ ubuntu-latest, windows-latest ] +# configuration: [ release ] +# test: [ Streams, PersistentSubscriptions, Operations, ProjectionManagement, UserManagement, Security, Misc ] +# runs-on: ${{ matrix.os }} +# name: ${{ matrix.test }} (${{ matrix.os }}, ${{ matrix.framework }}) +# steps: +# - name: Checkout +# uses: actions/checkout@v3 +# - shell: bash +# run: | +# git fetch --prune --unshallow +# - name: Install dotnet SDKs +# uses: actions/setup-dotnet@v3 +# with: +# dotnet-version: | +# 8.0.x +# 9.0.x +# - name: Compile +# shell: bash +# run: | +# dotnet build --configuration ${{ matrix.configuration }} --framework ${{ matrix.framework }} src/Kurrent.Client +# - name: Download certificates +# uses: actions/download-artifact@v4 +# with: +# name: certs +# path: certs +# - name: Run Tests (Linux) +# if: runner.os == 'Linux' +# shell: bash +# run: | +# dotnet test --configuration ${{ matrix.configuration }} --blame \ +# --logger:"GitHubActions;report-warnings=false" --logger:"console;verbosity=normal" \ +# --framework ${{ matrix.framework }} \ +# --filter "Category=Target:${{ matrix.test }}" \ +# test/Kurrent.Client.Tests +# - name: Run Tests (Windows) +# if: runner.os == 'Windows' +# shell: pwsh +# run: | +# dotnet test --configuration ${{ matrix.configuration }} --blame ` +# --logger:"GitHubActions;report-warnings=false" --logger:"console;verbosity=normal" ` +# --framework ${{ matrix.framework }} ` +# --filter "Category=Target:${{ matrix.test }}" ` +# test/Kurrent.Client.Tests +# +# publish: +# timeout-minutes: 5 +# needs: [ vulnerability-scan, test, build-samples ] +# runs-on: ubuntu-latest +# name: publish +# steps: +# - name: Checkout +# uses: actions/checkout@v3 +# - name: Get Version +# id: get_version +# run: | +# echo "branch=${GITHUB_REF:10}" >> $GITHUB_OUTPUT +# dotnet nuget list source +# dotnet tool restore +# version=$(dotnet tool run minver -- --tag-prefix=v) +# echo "version=${version}" >> $GITHUB_OUTPUT +# - shell: bash +# run: | +# git fetch --prune --unshallow +# - name: Install dotnet SDKs +# uses: actions/setup-dotnet@v3 +# with: +# dotnet-version: | +# 8.0.x +# 9.0.x +# - name: Dotnet Pack +# shell: bash +# run: | +# mkdir -p packages +# dotnet pack /p:Version=${{ steps.get_version.outputs.version }} --configuration=Release \ +# /p:PublishDir=./packages \ +# /p:NoWarn=NU5105 \ +# /p:RepositoryUrl=https://github.com/EventStore/EventStore-Client-Dotnet \ +# /p:RepositoryType=git +# - name: Publish Artifacts +# uses: actions/upload-artifact@v4 +# with: +# path: packages +# name: nuget-packages +# - name: Dotnet Push to Github Packages +# shell: bash +# if: github.event_name == 'push' +# run: | +# dotnet tool restore +# find . -name "*.nupkg" | xargs -n1 dotnet nuget push --api-key=${{ secrets.github_token }} --source https://nuget.pkg.github.com/EventStore/index.json --skip-duplicate +# - name: Dotnet Push to Nuget.org +# shell: bash +# if: contains(steps.get_version.outputs.branch, 'v') +# run: | +# dotnet nuget list source +# dotnet tool restore +# find . -name "*.nupkg" | xargs -n1 dotnet nuget push --api-key=${{ secrets.nuget_key }} --source https://api.nuget.org/v3/index.json --skip-duplicate diff --git a/Kurrent.Client.sln b/Kurrent.Client.sln index 63cdbc278..00544398a 100644 --- a/Kurrent.Client.sln +++ b/Kurrent.Client.sln @@ -5,14 +5,10 @@ VisualStudioVersion = 15.0.26124.0 MinimumVisualStudioVersion = 15.0.26124.0 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{EA59C1CB-16DA-4F68-AF8A-642A969B4CF8}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventStore.Client", "src\EventStore.Client\EventStore.Client.csproj", "{8853D875-4A8E-450B-A1BE-9CEF8BEDABC3}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{C51F2C69-45A9-4D0D-A708-4FC319D5D340}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kurrent.Client.Tests", "test\Kurrent.Client.Tests\Kurrent.Client.Tests.csproj", "{FC829F1B-43AD-4C96-9002-23D04BBA3AF3}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventStore.Client.Extensions.OpenTelemetry", "src\EventStore.Client.Extensions.OpenTelemetry\EventStore.Client.Extensions.OpenTelemetry.csproj", "{F6A7B391-36F1-4838-AD08-E0EE0F2FE57E}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kurrent.Client", "src\Kurrent.Client\Kurrent.Client.csproj", "{762EECAA-122E-4B0C-BC50-5AA4F72CA4E0}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kurrent.Client.Tests.Common", "test\Kurrent.Client.Tests.Common\Kurrent.Client.Tests.Common.csproj", "{47BF715B-A0BF-4044-B335-717E56422550}" @@ -26,18 +22,10 @@ Global HideSolutionNode = FALSE EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {8853D875-4A8E-450B-A1BE-9CEF8BEDABC3}.Debug|x64.ActiveCfg = Debug|Any CPU - {8853D875-4A8E-450B-A1BE-9CEF8BEDABC3}.Debug|x64.Build.0 = Debug|Any CPU - {8853D875-4A8E-450B-A1BE-9CEF8BEDABC3}.Release|x64.ActiveCfg = Release|Any CPU - {8853D875-4A8E-450B-A1BE-9CEF8BEDABC3}.Release|x64.Build.0 = Release|Any CPU {FC829F1B-43AD-4C96-9002-23D04BBA3AF3}.Debug|x64.ActiveCfg = Debug|Any CPU {FC829F1B-43AD-4C96-9002-23D04BBA3AF3}.Debug|x64.Build.0 = Debug|Any CPU {FC829F1B-43AD-4C96-9002-23D04BBA3AF3}.Release|x64.ActiveCfg = Release|Any CPU {FC829F1B-43AD-4C96-9002-23D04BBA3AF3}.Release|x64.Build.0 = Release|Any CPU - {F6A7B391-36F1-4838-AD08-E0EE0F2FE57E}.Debug|x64.ActiveCfg = Debug|Any CPU - {F6A7B391-36F1-4838-AD08-E0EE0F2FE57E}.Debug|x64.Build.0 = Debug|Any CPU - {F6A7B391-36F1-4838-AD08-E0EE0F2FE57E}.Release|x64.ActiveCfg = Release|Any CPU - {F6A7B391-36F1-4838-AD08-E0EE0F2FE57E}.Release|x64.Build.0 = Release|Any CPU {762EECAA-122E-4B0C-BC50-5AA4F72CA4E0}.Debug|x64.ActiveCfg = Debug|Any CPU {762EECAA-122E-4B0C-BC50-5AA4F72CA4E0}.Debug|x64.Build.0 = Debug|Any CPU {762EECAA-122E-4B0C-BC50-5AA4F72CA4E0}.Release|x64.ActiveCfg = Release|Any CPU @@ -48,9 +36,7 @@ Global {47BF715B-A0BF-4044-B335-717E56422550}.Release|x64.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution - {8853D875-4A8E-450B-A1BE-9CEF8BEDABC3} = {EA59C1CB-16DA-4F68-AF8A-642A969B4CF8} {FC829F1B-43AD-4C96-9002-23D04BBA3AF3} = {C51F2C69-45A9-4D0D-A708-4FC319D5D340} - {F6A7B391-36F1-4838-AD08-E0EE0F2FE57E} = {EA59C1CB-16DA-4F68-AF8A-642A969B4CF8} {762EECAA-122E-4B0C-BC50-5AA4F72CA4E0} = {EA59C1CB-16DA-4F68-AF8A-642A969B4CF8} {47BF715B-A0BF-4044-B335-717E56422550} = {C51F2C69-45A9-4D0D-A708-4FC319D5D340} EndGlobalSection diff --git a/samples/Directory.Build.props b/samples/Directory.Build.props index 4e510486d..1ab3aed15 100644 --- a/samples/Directory.Build.props +++ b/samples/Directory.Build.props @@ -1,6 +1,6 @@ - net8.0 + net8.0;net9.0 enable enable true From d62b55861be04fa4b77f61dd6867536da7eb7b3f Mon Sep 17 00:00:00 2001 From: William Chong Date: Wed, 8 Jan 2025 17:51:38 +0400 Subject: [PATCH 14/15] Fixup --- .../Fixtures/KurrentPermanentFixture.cs | 18 ++++++------------ .../Fixtures/KurrentTemporaryFixture.cs | 15 ++++----------- .../ShutdownNodeAuthenticationTests.cs | 1 - 3 files changed, 10 insertions(+), 24 deletions(-) diff --git a/test/Kurrent.Client.Tests.Common/Fixtures/KurrentPermanentFixture.cs b/test/Kurrent.Client.Tests.Common/Fixtures/KurrentPermanentFixture.cs index 8e9f5bf56..c70fa8360 100644 --- a/test/Kurrent.Client.Tests.Common/Fixtures/KurrentPermanentFixture.cs +++ b/test/Kurrent.Client.Tests.Common/Fixtures/KurrentPermanentFixture.cs @@ -1,3 +1,5 @@ +// ReSharper disable InconsistentNaming + using System.Net; using Ductus.FluentDocker.Builders; using Ductus.FluentDocker.Extensions; @@ -71,10 +73,8 @@ protected KurrentPermanentFixture(ConfigureFixture configure) { DefaultDeadline = Options.ClientSettings.DefaultDeadline }; - InterlockedBoolean WarmUpCompleted { get; } = new InterlockedBoolean(); - SemaphoreSlim WarmUpGatekeeper { get; } = new(1, 1); - - static readonly SemaphoreSlim ContainerSemaphore = new(1, 1); + InterlockedBoolean WarmUpCompleted { get; } = new InterlockedBoolean(); + static readonly SemaphoreSlim WarmUpGatekeeper = new(1, 1); public void CaptureTestRun(ITestOutputHelper outputHelper) { var testRunId = Logging.CaptureLogs(outputHelper); @@ -84,19 +84,13 @@ public void CaptureTestRun(ITestOutputHelper outputHelper) { } public async Task InitializeAsync() { - await ContainerSemaphore.WaitAsync(); + await WarmUpGatekeeper.WaitAsync(); + try { await Service.Start(); EventStoreVersion = GetKurrentVersion(); EventStoreHasLastStreamPosition = (EventStoreVersion?.Major ?? int.MaxValue) >= 21; - // EventStoreHasLastStreamPosition = true; - } finally { - ContainerSemaphore.Release(); - } - - await WarmUpGatekeeper.WaitAsync(); - try { if (!WarmUpCompleted.CurrentValue) { Logger.Warning("*** Warmup started ***"); diff --git a/test/Kurrent.Client.Tests.Common/Fixtures/KurrentTemporaryFixture.cs b/test/Kurrent.Client.Tests.Common/Fixtures/KurrentTemporaryFixture.cs index 3be5f5229..4cea1be60 100644 --- a/test/Kurrent.Client.Tests.Common/Fixtures/KurrentTemporaryFixture.cs +++ b/test/Kurrent.Client.Tests.Common/Fixtures/KurrentTemporaryFixture.cs @@ -74,9 +74,8 @@ protected KurrentTemporaryFixture(ConfigureFixture configure) { DefaultDeadline = Options.ClientSettings.DefaultDeadline }; - InterlockedBoolean WarmUpCompleted { get; } = new InterlockedBoolean(); - SemaphoreSlim WarmUpGatekeeper { get; } = new(1, 1); - static readonly SemaphoreSlim ContainerSemaphore = new(1, 1); + InterlockedBoolean WarmUpCompleted { get; } = new InterlockedBoolean(); + static readonly SemaphoreSlim WarmUpGatekeeper = new(1, 1); public void CaptureTestRun(ITestOutputHelper outputHelper) { var testRunId = Logging.CaptureLogs(outputHelper); @@ -86,19 +85,13 @@ public void CaptureTestRun(ITestOutputHelper outputHelper) { } public async Task InitializeAsync() { - await ContainerSemaphore.WaitAsync(); + await WarmUpGatekeeper.WaitAsync(); + try { await Service.Start(); EventStoreVersion = GetKurrentVersion(); EventStoreHasLastStreamPosition = (EventStoreVersion?.Major ?? int.MaxValue) >= 21; - // EventStoreHasLastStreamPosition = true; - } finally { - ContainerSemaphore.Release(); - } - await WarmUpGatekeeper.WaitAsync(); - - try { if (!WarmUpCompleted.CurrentValue) { Logger.Warning("*** Warmup started ***"); diff --git a/test/Kurrent.Client.Tests/Operations/ShutdownNodeAuthenticationTests.cs b/test/Kurrent.Client.Tests/Operations/ShutdownNodeAuthenticationTests.cs index 31d2a0600..f88fa32c8 100644 --- a/test/Kurrent.Client.Tests/Operations/ShutdownNodeAuthenticationTests.cs +++ b/test/Kurrent.Client.Tests/Operations/ShutdownNodeAuthenticationTests.cs @@ -1,6 +1,5 @@ using EventStore.Client; using Kurrent.Client.Tests.TestNode; -using Kurrent.Client.Tests; namespace Kurrent.Client.Tests; From 55d154785356f5f6e1d34945d38873caa1016064 Mon Sep 17 00:00:00 2001 From: William Chong Date: Thu, 9 Jan 2025 11:26:07 +0400 Subject: [PATCH 15/15] Test warmup --- .../Fixtures/KurrentPermanentFixture.cs | 10 +- .../Fixtures/KurrentTemporaryFixture.cs | 10 +- .../Obsolete/SubscribeToAllObsoleteTests.cs | 178 +++++++++--------- 3 files changed, 99 insertions(+), 99 deletions(-) diff --git a/test/Kurrent.Client.Tests.Common/Fixtures/KurrentPermanentFixture.cs b/test/Kurrent.Client.Tests.Common/Fixtures/KurrentPermanentFixture.cs index c70fa8360..0b657b61f 100644 --- a/test/Kurrent.Client.Tests.Common/Fixtures/KurrentPermanentFixture.cs +++ b/test/Kurrent.Client.Tests.Common/Fixtures/KurrentPermanentFixture.cs @@ -95,14 +95,14 @@ public async Task InitializeAsync() { Logger.Warning("*** Warmup started ***"); await Task.WhenAll( - InitClient(async x => Users = await x.WarmUp()), - InitClient(async x => Streams = await x.WarmUp()), + InitClient(async x => Users = await Task.FromResult(x)), + InitClient(async x => Streams = await Task.FromResult(x)), InitClient( - async x => Projections = await x.WarmUp(), + async x => Projections = await Task.FromResult(x), Options.Environment["EVENTSTORE_RUN_PROJECTIONS"] != "None" ), - InitClient(async x => Subscriptions = SkipPsWarmUp ? x : await x.WarmUp()), - InitClient(async x => Operations = await x.WarmUp()) + InitClient(async x => Subscriptions = SkipPsWarmUp ? x : await Task.FromResult(x)), + InitClient(async x => Operations = await Task.FromResult(x)) ); WarmUpCompleted.EnsureCalledOnce(); diff --git a/test/Kurrent.Client.Tests.Common/Fixtures/KurrentTemporaryFixture.cs b/test/Kurrent.Client.Tests.Common/Fixtures/KurrentTemporaryFixture.cs index 4cea1be60..8a71891ca 100644 --- a/test/Kurrent.Client.Tests.Common/Fixtures/KurrentTemporaryFixture.cs +++ b/test/Kurrent.Client.Tests.Common/Fixtures/KurrentTemporaryFixture.cs @@ -96,14 +96,14 @@ public async Task InitializeAsync() { Logger.Warning("*** Warmup started ***"); await Task.WhenAll( - InitClient(async x => Users = await x.WarmUp()), - InitClient(async x => Streams = await x.WarmUp()), + InitClient(async x => Users = await Task.FromResult(x)), + InitClient(async x => Streams = await Task.FromResult(x)), InitClient( - async x => Projections = await x.WarmUp(), + async x => Projections = await Task.FromResult(x), Options.Environment["EVENTSTORE_RUN_PROJECTIONS"] != "None" ), - InitClient(async x => Subscriptions = SkipPsWarmUp ? x : await x.WarmUp()), - InitClient(async x => Operations = await x.WarmUp()) + InitClient(async x => Subscriptions = SkipPsWarmUp ? x : await Task.FromResult(x)), + InitClient(async x => Operations = await Task.FromResult(x)) ); WarmUpCompleted.EnsureCalledOnce(); diff --git a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/Obsolete/SubscribeToAllObsoleteTests.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/Obsolete/SubscribeToAllObsoleteTests.cs index 957b42c69..05dc4baff 100644 --- a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/Obsolete/SubscribeToAllObsoleteTests.cs +++ b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/Obsolete/SubscribeToAllObsoleteTests.cs @@ -303,95 +303,95 @@ public async Task deleting_existing_with_subscriber() { Assert.Equal(group, ex.GroupName); } - [RetryFact] - public async Task happy_case_catching_up_to_link_to_events_manual_ack() { - var group = Fixture.GetGroupName(); - var bufferCount = 10; - var eventWriteCount = bufferCount * 2; - TaskCompletionSource eventsReceived = new(); - int eventReceivedCount = 0; - - var events = Fixture.CreateTestEvents(eventWriteCount) - .Select( - (e, i) => new EventData( - e.EventId, - SystemEventTypes.LinkTo, - Encoding.UTF8.GetBytes($"{i}@test"), - contentType: Constants.Metadata.ContentTypes.ApplicationOctetStream - ) - ) - .ToArray(); - - foreach (var e in events) { - await Fixture.Streams.AppendToStreamAsync("test-" + Guid.NewGuid(), StreamState.Any, new[] { e }); - } - - await Fixture.Subscriptions.CreateToAllAsync( - group, - new(startFrom: Position.Start, resolveLinkTos: true), - userCredentials: TestCredentials.Root - ); - - using var subscription = await Fixture.Subscriptions.SubscribeToAllAsync( - group, - async (subscription, e, retryCount, ct) => { - await subscription.Ack(e); - - if (e.OriginalStreamId.StartsWith("test-") - && Interlocked.Increment(ref eventReceivedCount) == events.Length) - eventsReceived.TrySetResult(true); - }, - (s, r, e) => { - if (e != null) - eventsReceived.TrySetException(e); - }, - bufferSize: bufferCount, - userCredentials: TestCredentials.Root - ); - - await eventsReceived.Task.WithTimeout(); - } - - [RetryFact] - public async Task happy_case_catching_up_to_normal_events_manual_ack() { - var group = Fixture.GetGroupName(); - var stream = Fixture.GetStreamName(); - var bufferCount = 10; - var eventWriteCount = bufferCount * 2; - int eventReceivedCount = 0; - - TaskCompletionSource eventsReceived = new(); - - var events = Fixture.CreateTestEvents(eventWriteCount).ToArray(); - - foreach (var e in events) - await Fixture.Streams.AppendToStreamAsync(stream, StreamState.Any, [e]); - - await Fixture.Subscriptions.CreateToAllAsync( - group, - new(startFrom: Position.Start, resolveLinkTos: true), - userCredentials: TestCredentials.Root - ); - - using var subscription = await Fixture.Subscriptions.SubscribeToAllAsync( - group, - async (subscription, e, retryCount, ct) => { - await subscription.Ack(e); - - if (e.OriginalStreamId.StartsWith("test-") - && Interlocked.Increment(ref eventReceivedCount) == events.Length) - eventsReceived.TrySetResult(true); - }, - (s, r, e) => { - if (e != null) - eventsReceived.TrySetException(e); - }, - bufferSize: bufferCount, - userCredentials: TestCredentials.Root - ); - - await eventsReceived.Task.WithTimeout(); - } + // [RetryFact] + // public async Task happy_case_catching_up_to_link_to_events_manual_ack() { + // var group = Fixture.GetGroupName(); + // var bufferCount = 10; + // var eventWriteCount = bufferCount * 2; + // TaskCompletionSource eventsReceived = new(); + // int eventReceivedCount = 0; + // + // var events = Fixture.CreateTestEvents(eventWriteCount) + // .Select( + // (e, i) => new EventData( + // e.EventId, + // SystemEventTypes.LinkTo, + // Encoding.UTF8.GetBytes($"{i}@test"), + // contentType: Constants.Metadata.ContentTypes.ApplicationOctetStream + // ) + // ) + // .ToArray(); + // + // foreach (var e in events) { + // await Fixture.Streams.AppendToStreamAsync("test-" + Guid.NewGuid(), StreamState.Any, new[] { e }); + // } + // + // await Fixture.Subscriptions.CreateToAllAsync( + // group, + // new(startFrom: Position.Start, resolveLinkTos: true), + // userCredentials: TestCredentials.Root + // ); + // + // using var subscription = await Fixture.Subscriptions.SubscribeToAllAsync( + // group, + // async (subscription, e, retryCount, ct) => { + // await subscription.Ack(e); + // + // if (e.OriginalStreamId.StartsWith("test-") + // && Interlocked.Increment(ref eventReceivedCount) == events.Length) + // eventsReceived.TrySetResult(true); + // }, + // (s, r, e) => { + // if (e != null) + // eventsReceived.TrySetException(e); + // }, + // bufferSize: bufferCount, + // userCredentials: TestCredentials.Root + // ); + // + // await eventsReceived.Task.WithTimeout(); + // } + // + // [RetryFact] + // public async Task happy_case_catching_up_to_normal_events_manual_ack() { + // var group = Fixture.GetGroupName(); + // var stream = Fixture.GetStreamName(); + // var bufferCount = 10; + // var eventWriteCount = bufferCount * 2; + // int eventReceivedCount = 0; + // + // TaskCompletionSource eventsReceived = new(); + // + // var events = Fixture.CreateTestEvents(eventWriteCount).ToArray(); + // + // foreach (var e in events) + // await Fixture.Streams.AppendToStreamAsync(stream, StreamState.Any, [e]); + // + // await Fixture.Subscriptions.CreateToAllAsync( + // group, + // new(startFrom: Position.Start, resolveLinkTos: true), + // userCredentials: TestCredentials.Root + // ); + // + // using var subscription = await Fixture.Subscriptions.SubscribeToAllAsync( + // group, + // async (subscription, e, retryCount, ct) => { + // await subscription.Ack(e); + // + // if (e.OriginalStreamId.StartsWith("test-") + // && Interlocked.Increment(ref eventReceivedCount) == events.Length) + // eventsReceived.TrySetResult(true); + // }, + // (s, r, e) => { + // if (e != null) + // eventsReceived.TrySetException(e); + // }, + // bufferSize: bufferCount, + // userCredentials: TestCredentials.Root + // ); + // + // await eventsReceived.Task.WithTimeout(); + // } [RetryFact] public async Task happy_case_writing_and_subscribing_to_normal_events_manual_ack() {