From 6c37df4659f356cf74ba1c57c6788dcb730dfb23 Mon Sep 17 00:00:00 2001 From: Blake Niemyjski Date: Wed, 11 Nov 2026 09:05:14 -0600 Subject: [PATCH 1/3] Upgrade to .NET 11 --- .github/workflows/copilot-setup-steps.yml | 4 +-- README.md | 3 +- .../Exceptionless.Core.csproj | 1 + .../Extensions/PersistentEventExtensions.cs | 2 +- .../Exceptionless.Insulation.csproj | 5 +++ .../ClientApp/src/lib/generated/api.ts | 6 ++++ .../ClientApp/src/lib/generated/schemas.ts | 1 + .../Exceptionless.Web.csproj | 1 + .../Exceptionless.Tests/Api/Data/openapi.json | 2 +- .../Assistant/AssistantServiceTests.cs | 9 ++++- .../Exceptionless.Tests.csproj | 1 + .../PersistentEventExtensionsTests.cs | 25 ++++++++++++++ .../Jobs/CloseInactiveSessionsJobTests.cs | 33 +++++++++++-------- 13 files changed, 73 insertions(+), 20 deletions(-) create mode 100644 tests/Exceptionless.Tests/Extensions/PersistentEventExtensionsTests.cs diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index 5634b5a32d..c01529d4a1 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -31,9 +31,9 @@ jobs: run: | export PATH="$HOME/.dotnet/tools:$PATH" if dotnet tool list --global | grep -Eiq '^aspire\.cli[[:space:]]'; then - dotnet tool update --global Aspire.Cli --version 13.6.1 + dotnet tool update --global Aspire.Cli --version 13.6.5 else - dotnet tool install --global Aspire.Cli --version 13.6.1 + dotnet tool install --global Aspire.Cli --version 13.6.5 fi echo "$HOME/.dotnet/tools" >> "$GITHUB_PATH" aspire --version diff --git a/README.md b/README.md index e53da947ee..35485632f9 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,8 @@ Notes: 1. Running `Exceptionless.AppHost` starts the app and required infrastructure together. 2. Backend tests bootstrap required infrastructure automatically. -3. Use Aspire CLI 13.6.0 or newer (`aspire update --self` to update). The AppHost uses the CLI bundle for the dashboard and resource orchestrator; when no CLI is installed, the SDK downloads the matching version through DNX. +3. Use Aspire CLI 13.6.5 or newer (`aspire update --self` to update). The AppHost uses the CLI bundle for the dashboard and resource orchestrator; when no CLI is installed, the SDK downloads the matching version through DNX. +4. Try `dotnet build -mt` for multithreaded builds. Performance depends on the workload and machine. ## UI Development diff --git a/src/Exceptionless.Core/Exceptionless.Core.csproj b/src/Exceptionless.Core/Exceptionless.Core.csproj index ae25582e69..468abe3544 100644 --- a/src/Exceptionless.Core/Exceptionless.Core.csproj +++ b/src/Exceptionless.Core/Exceptionless.Core.csproj @@ -14,6 +14,7 @@ + diff --git a/src/Exceptionless.Core/Extensions/PersistentEventExtensions.cs b/src/Exceptionless.Core/Extensions/PersistentEventExtensions.cs index 98aa5b4f6b..a0126b98e8 100644 --- a/src/Exceptionless.Core/Extensions/PersistentEventExtensions.cs +++ b/src/Exceptionless.Core/Extensions/PersistentEventExtensions.cs @@ -156,7 +156,7 @@ public static bool UpdateSessionStart(this PersistentEvent ev, DateTime lastActi if (duration < 0) duration = 0; - decimal newDuration = (decimal)(lastActivityUtc - ev.Date.UtcDateTime).TotalSeconds; + decimal newDuration = (lastActivityUtc - ev.Date.UtcDateTime).Ticks / (decimal)TimeSpan.TicksPerSecond; if (duration >= newDuration) lastActivityUtc = ev.Date.UtcDateTime.AddSeconds((double)duration); else diff --git a/src/Exceptionless.Insulation/Exceptionless.Insulation.csproj b/src/Exceptionless.Insulation/Exceptionless.Insulation.csproj index 1c0ca3b50c..a40b95cee5 100644 --- a/src/Exceptionless.Insulation/Exceptionless.Insulation.csproj +++ b/src/Exceptionless.Insulation/Exceptionless.Insulation.csproj @@ -13,6 +13,7 @@ + @@ -20,6 +21,10 @@ + + + + diff --git a/src/Exceptionless.Web/ClientApp/src/lib/generated/api.ts b/src/Exceptionless.Web/ClientApp/src/lib/generated/api.ts index 4010d85b3e..1664c77105 100644 --- a/src/Exceptionless.Web/ClientApp/src/lib/generated/api.ts +++ b/src/Exceptionless.Web/ClientApp/src/lib/generated/api.ts @@ -755,6 +755,12 @@ export interface User { id: string; /** The organizations that the user has access to. */ organization_ids: string[]; + /** + * Session revocation generation, rotated when a password changes or resets, or an external login is removed or transferred away. + * Remains null until rotation; active users with a null generation accept only matching legacy null token generations. + * This value does not describe the password hashing format. + */ + authentication_version?: null | string; password?: null | string; salt?: null | string; password_reset_token?: null | string; diff --git a/src/Exceptionless.Web/ClientApp/src/lib/generated/schemas.ts b/src/Exceptionless.Web/ClientApp/src/lib/generated/schemas.ts index a0ad26433e..71367e1d2f 100644 --- a/src/Exceptionless.Web/ClientApp/src/lib/generated/schemas.ts +++ b/src/Exceptionless.Web/ClientApp/src/lib/generated/schemas.ts @@ -889,6 +889,7 @@ export const UserSchema = object({ .length(24, "Id must be exactly 24 characters") .regex(/^[a-fA-F0-9]{24}$/, "Id has invalid format"), organization_ids: array(string()), + authentication_version: string().nullable().optional(), password: string().min(1, "Password is required").nullable().optional(), salt: string().min(1, "Salt is required").nullable().optional(), password_reset_token: string() diff --git a/src/Exceptionless.Web/Exceptionless.Web.csproj b/src/Exceptionless.Web/Exceptionless.Web.csproj index f1c53fce69..0e9b650746 100644 --- a/src/Exceptionless.Web/Exceptionless.Web.csproj +++ b/src/Exceptionless.Web/Exceptionless.Web.csproj @@ -17,6 +17,7 @@ + diff --git a/tests/Exceptionless.Tests/Api/Data/openapi.json b/tests/Exceptionless.Tests/Api/Data/openapi.json index dbca567ca0..8748a377df 100644 --- a/tests/Exceptionless.Tests/Api/Data/openapi.json +++ b/tests/Exceptionless.Tests/Api/Data/openapi.json @@ -15224,4 +15224,4 @@ "name": "WebHook" } ] -} +} \ No newline at end of file diff --git a/tests/Exceptionless.Tests/Assistant/AssistantServiceTests.cs b/tests/Exceptionless.Tests/Assistant/AssistantServiceTests.cs index d2d307d96f..ac221f6e07 100644 --- a/tests/Exceptionless.Tests/Assistant/AssistantServiceTests.cs +++ b/tests/Exceptionless.Tests/Assistant/AssistantServiceTests.cs @@ -1687,6 +1687,7 @@ public async Task StreamAsync_ProviderThrows_RecordsFailureCategory(string failu [InlineData("{\"usage\":{\"prompt_tokens\":\"private invalid token count\"}}")] public async Task StreamAsync_InvalidProviderShape_RecordsProviderAndTurnFailure(string payload) { + // Arrange var options = AppOptions.ReadFromConfiguration(new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary { ["BaseURL"] = "https://localhost", ["Assistant:ApiKey"] = "test-key" }) .Build()); @@ -1701,17 +1702,23 @@ public async Task StreamAsync_InvalidProviderShape_RecordsProviderAndTurnFailure using var response = new MemoryStream(); context.Response.Body = response; + // Act await AssistantEndpoints.WriteResponseAsync(context, service.StreamAsync(new AssistantChatRequest([new AssistantChatMessage("user", "private question")]), "user-id", CreatePlanOptions(), diagnostics, TestContext.Current.CancellationToken), usageService, "organization-id", diagnostics, TestContext.Current.CancellationToken); + // Assert var providerEntry = Assert.Single(logger.Entries, entry => entry.Properties.ContainsKey("ProviderOutcome")); var turnEntry = Assert.Single(logger.Entries, entry => entry.Properties.ContainsKey("Outcome")); Assert.Equal("invalid_provider_response", providerEntry.Properties["ProviderOutcome"]); Assert.Equal("invalid_provider_response", turnEntry.Properties["FailureReason"]); Assert.Equal("failed", turnEntry.Properties["Outcome"]); - Assert.All(logger.Entries, entry => Assert.DoesNotContain("private", entry.Message)); + Assert.All(logger.Entries, entry => + { + Assert.DoesNotContain("private question", entry.Message); + Assert.DoesNotContain("private invalid token count", entry.Message); + }); } [Theory] diff --git a/tests/Exceptionless.Tests/Exceptionless.Tests.csproj b/tests/Exceptionless.Tests/Exceptionless.Tests.csproj index aa169912c0..92e4c4feb1 100644 --- a/tests/Exceptionless.Tests/Exceptionless.Tests.csproj +++ b/tests/Exceptionless.Tests/Exceptionless.Tests.csproj @@ -20,6 +20,7 @@ + diff --git a/tests/Exceptionless.Tests/Extensions/PersistentEventExtensionsTests.cs b/tests/Exceptionless.Tests/Extensions/PersistentEventExtensionsTests.cs new file mode 100644 index 0000000000..bce52641f9 --- /dev/null +++ b/tests/Exceptionless.Tests/Extensions/PersistentEventExtensionsTests.cs @@ -0,0 +1,25 @@ +using Exceptionless.Core.Extensions; +using Exceptionless.Core.Models; +using Foundatio.Xunit; +using Xunit; + +namespace Exceptionless.Tests.Extensions; + +public sealed class PersistentEventExtensionsTests(ITestOutputHelper output) : TestWithLoggingBase(output) +{ + [Fact] + public void UpdateSessionStart_FractionalSeconds_PreservesTickPrecision() + { + // Arrange + var start = new DateTimeOffset(2026, 1, 1, 0, 0, 0, TimeSpan.Zero); + var session = new PersistentEvent { Type = Event.KnownTypes.Session, Date = start }; + var lastActivity = start.UtcDateTime.AddTicks(1_234_567_891); + + // Act + bool updated = session.UpdateSessionStart(lastActivity); + + // Assert + Assert.True(updated); + Assert.Equal(123.4567891m, session.Value); + } +} diff --git a/tests/Exceptionless.Tests/Jobs/CloseInactiveSessionsJobTests.cs b/tests/Exceptionless.Tests/Jobs/CloseInactiveSessionsJobTests.cs index ad5b69d765..9086d32769 100644 --- a/tests/Exceptionless.Tests/Jobs/CloseInactiveSessionsJobTests.cs +++ b/tests/Exceptionless.Tests/Jobs/CloseInactiveSessionsJobTests.cs @@ -286,22 +286,30 @@ public async Task WillNotCloseDuplicateIdentitySessionsWithSessionIdHeartbeat() [InlineData(60, false, null, false)] public async Task CloseInactiveSessions(int defaultInactivePeriodInMinutes, bool willCloseSession, int? sessionHeartbeatUpdatedAgoInSeconds, bool heartbeatClosesSession) { + // Arrange const string userId = "blake@exceptionless.io"; var ev = GenerateEvent(DateTimeOffset.Now.SubtractMinutes(5), userId); + // Act var context = await _pipeline.RunAsync(ev, _organizationData.GenerateSampleOrganization(_billingManager, _plans), _projectData.GenerateSampleProject()); + + // Assert Assert.False(context.HasError, context.ErrorMessage); Assert.False(context.IsCancelled); Assert.True(context.IsProcessed); + // Arrange await RefreshDataAsync(); var events = await _eventRepository.GetAllAsync(); + + // Assert Assert.Equal(2, events.Total); Assert.Single(events.Documents.Where(e => !String.IsNullOrEmpty(e.GetSessionId())).Select(e => e.GetSessionId()).Distinct()); var sessionStart = events.Documents.First(e => e.IsSessionStart()); Assert.Equal(0, sessionStart.Value); Assert.False(sessionStart.HasSessionEndTime()); + // Arrange var utcNow = DateTime.UtcNow; if (sessionHeartbeatUpdatedAgoInSeconds.HasValue) { @@ -311,25 +319,22 @@ public async Task CloseInactiveSessions(int defaultInactivePeriodInMinutes, bool } _job.DefaultInactivePeriod = TimeSpan.FromMinutes(defaultInactivePeriodInMinutes); - Assert.Equal(JobResult.Success, await _job.RunAsync(TestCancellationToken)); + + // Act + var result = await _job.RunAsync(TestCancellationToken); await RefreshDataAsync(); events = await _eventRepository.GetAllAsync(); + + // Assert + Assert.Equal(JobResult.Success, result); Assert.Equal(2, events.Total); sessionStart = events.Documents.First(e => e.IsSessionStart()); - // Event values are persisted as doubles, so compare at that precision. - double sessionStartDuration = sessionHeartbeatUpdatedAgoInSeconds.HasValue ? (utcNow.SubtractSeconds(sessionHeartbeatUpdatedAgoInSeconds.Value) - sessionStart.Date.UtcDateTime).TotalSeconds : 0; - double? sessionStartValue = (double?)sessionStart.Value; - if (willCloseSession) - { - Assert.Equal(sessionStartDuration, sessionStartValue); - Assert.True(sessionStart.HasSessionEndTime()); - } - else - { - Assert.Equal(sessionStartDuration, sessionStartValue); - Assert.False(sessionStart.HasSessionEndTime()); - } + decimal sessionStartDuration = sessionHeartbeatUpdatedAgoInSeconds.HasValue + ? (utcNow.SubtractSeconds(sessionHeartbeatUpdatedAgoInSeconds.Value) - sessionStart.Date.UtcDateTime).Ticks / (decimal)TimeSpan.TicksPerSecond + : 0; + Assert.Equal(sessionStartDuration, sessionStart.Value); + Assert.Equal(willCloseSession, sessionStart.HasSessionEndTime()); } private async Task CreateDataAsync(BillingPlan? plan = null) From 15c4cee6b4029bc9564f9424a6bc74627844592d Mon Sep 17 00:00:00 2001 From: Blake Niemyjski Date: Wed, 11 Nov 2026 09:27:20 -0600 Subject: [PATCH 2/3] Build frontend with official Node image --- Dockerfile | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/Dockerfile b/Dockerfile index 54a0746134..2e2fd353da 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,12 +18,13 @@ COPY . . RUN dotnet build ./src/Exceptionless.Web/Exceptionless.Web.csproj -c Release --no-restore /p:MinVerVersionOverride=${MinVerVersionOverride} \ && dotnet build ./src/Exceptionless.Job/Exceptionless.Job.csproj -c Release --no-restore /p:MinVerVersionOverride=${MinVerVersionOverride} +FROM node:24-bookworm-slim AS node + FROM build AS build-node -RUN apt-get update -yq \ - && apt-get install -yq curl ca-certificates \ - && curl -fsSL https://deb.nodesource.com/setup_24.x | bash - \ - && apt-get install -yq nodejs \ - && rm -rf /var/lib/apt/lists/* +COPY --from=node /usr/local/bin/node /usr/local/bin/node +COPY --from=node /usr/local/lib/node_modules/ /usr/local/lib/node_modules/ +RUN ln -s ../lib/node_modules/npm/bin/npm-cli.js /usr/local/bin/npm \ + && ln -s ../lib/node_modules/npm/bin/npx-cli.js /usr/local/bin/npx # testrunner From 56a10781fd03039ee9f6ac72788e7ec73dc805d7 Mon Sep 17 00:00:00 2001 From: Blake Niemyjski Date: Wed, 11 Nov 2026 18:02:05 -0600 Subject: [PATCH 3/3] Target OpenAPI 3.2 and narrow the .NET 11 follow-up --- .github/workflows/copilot-setup-steps.yml | 4 +-- Dockerfile | 11 +++---- .../Exceptionless.Core.csproj | 1 - .../Extensions/PersistentEventExtensions.cs | 1 + .../Exceptionless.Insulation.csproj | 5 --- .../Exceptionless.Web.csproj | 1 - ...nlessOpenApiServiceCollectionExtensions.cs | 3 -- .../Exceptionless.Tests/Api/Data/openapi.json | 2 +- .../Endpoints/OrganizationEndpointTests.cs | 8 +++++ .../Api/OpenApiSnapshotTests.cs | 1 + .../Exceptionless.Tests.csproj | 1 - .../Jobs/CloseInactiveSessionsJobTests.cs | 33 ++++++++----------- 12 files changed, 32 insertions(+), 39 deletions(-) diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index c01529d4a1..5634b5a32d 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -31,9 +31,9 @@ jobs: run: | export PATH="$HOME/.dotnet/tools:$PATH" if dotnet tool list --global | grep -Eiq '^aspire\.cli[[:space:]]'; then - dotnet tool update --global Aspire.Cli --version 13.6.5 + dotnet tool update --global Aspire.Cli --version 13.6.1 else - dotnet tool install --global Aspire.Cli --version 13.6.5 + dotnet tool install --global Aspire.Cli --version 13.6.1 fi echo "$HOME/.dotnet/tools" >> "$GITHUB_PATH" aspire --version diff --git a/Dockerfile b/Dockerfile index 2e2fd353da..54a0746134 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,13 +18,12 @@ COPY . . RUN dotnet build ./src/Exceptionless.Web/Exceptionless.Web.csproj -c Release --no-restore /p:MinVerVersionOverride=${MinVerVersionOverride} \ && dotnet build ./src/Exceptionless.Job/Exceptionless.Job.csproj -c Release --no-restore /p:MinVerVersionOverride=${MinVerVersionOverride} -FROM node:24-bookworm-slim AS node - FROM build AS build-node -COPY --from=node /usr/local/bin/node /usr/local/bin/node -COPY --from=node /usr/local/lib/node_modules/ /usr/local/lib/node_modules/ -RUN ln -s ../lib/node_modules/npm/bin/npm-cli.js /usr/local/bin/npm \ - && ln -s ../lib/node_modules/npm/bin/npx-cli.js /usr/local/bin/npx +RUN apt-get update -yq \ + && apt-get install -yq curl ca-certificates \ + && curl -fsSL https://deb.nodesource.com/setup_24.x | bash - \ + && apt-get install -yq nodejs \ + && rm -rf /var/lib/apt/lists/* # testrunner diff --git a/src/Exceptionless.Core/Exceptionless.Core.csproj b/src/Exceptionless.Core/Exceptionless.Core.csproj index 468abe3544..ae25582e69 100644 --- a/src/Exceptionless.Core/Exceptionless.Core.csproj +++ b/src/Exceptionless.Core/Exceptionless.Core.csproj @@ -14,7 +14,6 @@ - diff --git a/src/Exceptionless.Core/Extensions/PersistentEventExtensions.cs b/src/Exceptionless.Core/Extensions/PersistentEventExtensions.cs index a0126b98e8..3d57154a20 100644 --- a/src/Exceptionless.Core/Extensions/PersistentEventExtensions.cs +++ b/src/Exceptionless.Core/Extensions/PersistentEventExtensions.cs @@ -156,6 +156,7 @@ public static bool UpdateSessionStart(this PersistentEvent ev, DateTime lastActi if (duration < 0) duration = 0; + // Avoid losing tick precision through TotalSeconds' double representation. decimal newDuration = (lastActivityUtc - ev.Date.UtcDateTime).Ticks / (decimal)TimeSpan.TicksPerSecond; if (duration >= newDuration) lastActivityUtc = ev.Date.UtcDateTime.AddSeconds((double)duration); diff --git a/src/Exceptionless.Insulation/Exceptionless.Insulation.csproj b/src/Exceptionless.Insulation/Exceptionless.Insulation.csproj index a40b95cee5..1c0ca3b50c 100644 --- a/src/Exceptionless.Insulation/Exceptionless.Insulation.csproj +++ b/src/Exceptionless.Insulation/Exceptionless.Insulation.csproj @@ -13,7 +13,6 @@ - @@ -21,10 +20,6 @@ - - - - diff --git a/src/Exceptionless.Web/Exceptionless.Web.csproj b/src/Exceptionless.Web/Exceptionless.Web.csproj index 0e9b650746..f1c53fce69 100644 --- a/src/Exceptionless.Web/Exceptionless.Web.csproj +++ b/src/Exceptionless.Web/Exceptionless.Web.csproj @@ -17,7 +17,6 @@ - diff --git a/src/Exceptionless.Web/Utility/OpenApi/ExceptionlessOpenApiServiceCollectionExtensions.cs b/src/Exceptionless.Web/Utility/OpenApi/ExceptionlessOpenApiServiceCollectionExtensions.cs index 667623ba32..829c3a2243 100644 --- a/src/Exceptionless.Web/Utility/OpenApi/ExceptionlessOpenApiServiceCollectionExtensions.cs +++ b/src/Exceptionless.Web/Utility/OpenApi/ExceptionlessOpenApiServiceCollectionExtensions.cs @@ -1,5 +1,4 @@ using Microsoft.Extensions.DependencyInjection; -using Microsoft.OpenApi; namespace Exceptionless.Web.Utility.OpenApi; @@ -11,8 +10,6 @@ public static IServiceCollection AddExceptionlessOpenApi(this IServiceCollection { services.AddOpenApi(options => { - // ASP.NET Core 11 defaults to OpenAPI 3.2; keep publishing 3.1 for existing client generators. - options.OpenApiVersion = OpenApiSpecVersion.OpenApi3_1; options.CreateSchemaReferenceId = SchemaReferenceIdHelper.CreateSchemaReferenceId; options.AddDocumentTransformer(); options.AddDocumentTransformer(); diff --git a/tests/Exceptionless.Tests/Api/Data/openapi.json b/tests/Exceptionless.Tests/Api/Data/openapi.json index 8748a377df..f8abf4f1e5 100644 --- a/tests/Exceptionless.Tests/Api/Data/openapi.json +++ b/tests/Exceptionless.Tests/Api/Data/openapi.json @@ -1,5 +1,5 @@ { - "openapi": "3.1.2", + "openapi": "3.2.0", "info": { "title": "Exceptionless API", "termsOfService": "https://exceptionless.com/terms/", diff --git a/tests/Exceptionless.Tests/Api/Endpoints/OrganizationEndpointTests.cs b/tests/Exceptionless.Tests/Api/Endpoints/OrganizationEndpointTests.cs index dbd4f791cb..7fcf38ad6d 100644 --- a/tests/Exceptionless.Tests/Api/Endpoints/OrganizationEndpointTests.cs +++ b/tests/Exceptionless.Tests/Api/Endpoints/OrganizationEndpointTests.cs @@ -1104,6 +1104,9 @@ public async Task AddUserAsync_ExistingUserWithoutMembership_AddsOrganizationMem var existingUser = await _userRepository.GetByEmailAddressAsync(SampleDataService.FREE_USER_EMAIL); Assert.NotNull(existingUser); Assert.DoesNotContain(SampleDataService.TEST_ORG_ID, existingUser.OrganizationIds); + existingUser.AuthenticationVersion = "private-authentication-version"; + existingUser.PasswordResetToken = "private-password-reset-token"; + await _userRepository.SaveAsync(existingUser, o => o.ImmediateConsistency().Cache()); // Act var user = await SendRequestAsAsync(r => r @@ -1116,6 +1119,11 @@ public async Task AddUserAsync_ExistingUserWithoutMembership_AddsOrganizationMem // Assert Assert.NotNull(user); Assert.Equal(SampleDataService.FREE_USER_EMAIL, user.EmailAddress); + Assert.Null(user.AuthenticationVersion); + Assert.Null(user.Password); + Assert.Null(user.Salt); + Assert.Null(user.PasswordResetToken); + Assert.Null(user.VerifyEmailAddressToken); existingUser = await _userRepository.GetByEmailAddressAsync(SampleDataService.FREE_USER_EMAIL); Assert.NotNull(existingUser); diff --git a/tests/Exceptionless.Tests/Api/OpenApiSnapshotTests.cs b/tests/Exceptionless.Tests/Api/OpenApiSnapshotTests.cs index 06dd4f3b17..149624ce8d 100644 --- a/tests/Exceptionless.Tests/Api/OpenApiSnapshotTests.cs +++ b/tests/Exceptionless.Tests/Api/OpenApiSnapshotTests.cs @@ -47,6 +47,7 @@ public async Task GetOpenApiJson_Default_ContainsExpectedRoutesOperationsAndResp var paths = document.RootElement.GetProperty("paths"); // Assert + Assert.Equal("3.2.0", document.RootElement.GetProperty("openapi").GetString()); Assert.True(paths.TryGetProperty("/api/v2/auth/login", out var loginPath)); Assert.True(loginPath.TryGetProperty("post", out var loginPost)); Assert.True(loginPost.TryGetProperty("requestBody", out _)); diff --git a/tests/Exceptionless.Tests/Exceptionless.Tests.csproj b/tests/Exceptionless.Tests/Exceptionless.Tests.csproj index 92e4c4feb1..aa169912c0 100644 --- a/tests/Exceptionless.Tests/Exceptionless.Tests.csproj +++ b/tests/Exceptionless.Tests/Exceptionless.Tests.csproj @@ -20,7 +20,6 @@ - diff --git a/tests/Exceptionless.Tests/Jobs/CloseInactiveSessionsJobTests.cs b/tests/Exceptionless.Tests/Jobs/CloseInactiveSessionsJobTests.cs index 9086d32769..ad5b69d765 100644 --- a/tests/Exceptionless.Tests/Jobs/CloseInactiveSessionsJobTests.cs +++ b/tests/Exceptionless.Tests/Jobs/CloseInactiveSessionsJobTests.cs @@ -286,30 +286,22 @@ public async Task WillNotCloseDuplicateIdentitySessionsWithSessionIdHeartbeat() [InlineData(60, false, null, false)] public async Task CloseInactiveSessions(int defaultInactivePeriodInMinutes, bool willCloseSession, int? sessionHeartbeatUpdatedAgoInSeconds, bool heartbeatClosesSession) { - // Arrange const string userId = "blake@exceptionless.io"; var ev = GenerateEvent(DateTimeOffset.Now.SubtractMinutes(5), userId); - // Act var context = await _pipeline.RunAsync(ev, _organizationData.GenerateSampleOrganization(_billingManager, _plans), _projectData.GenerateSampleProject()); - - // Assert Assert.False(context.HasError, context.ErrorMessage); Assert.False(context.IsCancelled); Assert.True(context.IsProcessed); - // Arrange await RefreshDataAsync(); var events = await _eventRepository.GetAllAsync(); - - // Assert Assert.Equal(2, events.Total); Assert.Single(events.Documents.Where(e => !String.IsNullOrEmpty(e.GetSessionId())).Select(e => e.GetSessionId()).Distinct()); var sessionStart = events.Documents.First(e => e.IsSessionStart()); Assert.Equal(0, sessionStart.Value); Assert.False(sessionStart.HasSessionEndTime()); - // Arrange var utcNow = DateTime.UtcNow; if (sessionHeartbeatUpdatedAgoInSeconds.HasValue) { @@ -319,22 +311,25 @@ public async Task CloseInactiveSessions(int defaultInactivePeriodInMinutes, bool } _job.DefaultInactivePeriod = TimeSpan.FromMinutes(defaultInactivePeriodInMinutes); - - // Act - var result = await _job.RunAsync(TestCancellationToken); + Assert.Equal(JobResult.Success, await _job.RunAsync(TestCancellationToken)); await RefreshDataAsync(); events = await _eventRepository.GetAllAsync(); - - // Assert - Assert.Equal(JobResult.Success, result); Assert.Equal(2, events.Total); sessionStart = events.Documents.First(e => e.IsSessionStart()); - decimal sessionStartDuration = sessionHeartbeatUpdatedAgoInSeconds.HasValue - ? (utcNow.SubtractSeconds(sessionHeartbeatUpdatedAgoInSeconds.Value) - sessionStart.Date.UtcDateTime).Ticks / (decimal)TimeSpan.TicksPerSecond - : 0; - Assert.Equal(sessionStartDuration, sessionStart.Value); - Assert.Equal(willCloseSession, sessionStart.HasSessionEndTime()); + // Event values are persisted as doubles, so compare at that precision. + double sessionStartDuration = sessionHeartbeatUpdatedAgoInSeconds.HasValue ? (utcNow.SubtractSeconds(sessionHeartbeatUpdatedAgoInSeconds.Value) - sessionStart.Date.UtcDateTime).TotalSeconds : 0; + double? sessionStartValue = (double?)sessionStart.Value; + if (willCloseSession) + { + Assert.Equal(sessionStartDuration, sessionStartValue); + Assert.True(sessionStart.HasSessionEndTime()); + } + else + { + Assert.Equal(sessionStartDuration, sessionStartValue); + Assert.False(sessionStart.HasSessionEndTime()); + } } private async Task CreateDataAsync(BillingPlan? plan = null)