From 290bec0e246fe6b622c4e2db1728ac9c48374ccc Mon Sep 17 00:00:00 2001 From: Justin Yoo Date: Sat, 7 Sep 2024 14:13:07 +0900 Subject: [PATCH] Add AdminResourceDetails model --- .../Models/AdminEventDetails.cs | 118 ++-- .../Models/AdminResourceDetails.cs | 84 +++ .../Models/EventDetails.cs | 72 +-- .../Models/AdminResourceDetailsTests.cs | 69 +++ .../CreateChatCompletionResponseTests.cs | 555 +++++++++--------- 5 files changed, 526 insertions(+), 372 deletions(-) create mode 100644 src/AzureOpenAIProxy.ApiApp/Models/AdminResourceDetails.cs create mode 100644 test/AzureOpenAIProxy.ApiApp.Tests/Models/AdminResourceDetailsTests.cs diff --git a/src/AzureOpenAIProxy.ApiApp/Models/AdminEventDetails.cs b/src/AzureOpenAIProxy.ApiApp/Models/AdminEventDetails.cs index a3ccdf25..aa5dc597 100644 --- a/src/AzureOpenAIProxy.ApiApp/Models/AdminEventDetails.cs +++ b/src/AzureOpenAIProxy.ApiApp/Models/AdminEventDetails.cs @@ -1,60 +1,60 @@ -using System.Text.Json.Serialization; - -namespace AzureOpenAIProxy.ApiApp.Models; - -/// -/// This represent the event detail data for response by admin event endpoint. -/// -public class AdminEventDetails : EventDetails -{ - /// - /// Gets or sets the event description. - /// - public string? Description { get; set; } - - /// - /// Gets or sets the event start date. - /// - [JsonRequired] - public DateTimeOffset DateStart { get; set; } - - /// - /// Gets or sets the event end date. - /// - [JsonRequired] - public DateTimeOffset DateEnd { get; set; } - - /// - /// Gets or sets the event start to end date timezone. - /// - [JsonRequired] - public string TimeZone { get; set; } = string.Empty; - - /// - /// Gets or sets the event active status. - /// - [JsonRequired] - public bool IsActive { get; set; } - - /// - /// Gets or sets the event organizer name. - /// - [JsonRequired] - public string OrganizerName { get; set; } = string.Empty; - - /// - /// Gets or sets the event organizer email. - /// - [JsonRequired] - public string OrganizerEmail { get; set; } = string.Empty; - - /// - /// Gets or sets the event coorganizer name. - /// - public string? CoorganizerName { get; set; } - - /// - /// Gets or sets the event coorganizer email. - /// - public string? CoorganizerEmail { get; set; } +using System.Text.Json.Serialization; + +namespace AzureOpenAIProxy.ApiApp.Models; + +/// +/// This represent the entity for the event details for admin. +/// +public class AdminEventDetails : EventDetails +{ + /// + /// Gets or sets the event description. + /// + public string? Description { get; set; } + + /// + /// Gets or sets the event start date. + /// + [JsonRequired] + public DateTimeOffset DateStart { get; set; } + + /// + /// Gets or sets the event end date. + /// + [JsonRequired] + public DateTimeOffset DateEnd { get; set; } + + /// + /// Gets or sets the event start to end date timezone. + /// + [JsonRequired] + public string TimeZone { get; set; } = string.Empty; + + /// + /// Gets or sets the event active status. + /// + [JsonRequired] + public bool IsActive { get; set; } + + /// + /// Gets or sets the event organizer name. + /// + [JsonRequired] + public string OrganizerName { get; set; } = string.Empty; + + /// + /// Gets or sets the event organizer email. + /// + [JsonRequired] + public string OrganizerEmail { get; set; } = string.Empty; + + /// + /// Gets or sets the event coorganizer name. + /// + public string? CoorganizerName { get; set; } + + /// + /// Gets or sets the event coorganizer email. + /// + public string? CoorganizerEmail { get; set; } } \ No newline at end of file diff --git a/src/AzureOpenAIProxy.ApiApp/Models/AdminResourceDetails.cs b/src/AzureOpenAIProxy.ApiApp/Models/AdminResourceDetails.cs new file mode 100644 index 00000000..b1464200 --- /dev/null +++ b/src/AzureOpenAIProxy.ApiApp/Models/AdminResourceDetails.cs @@ -0,0 +1,84 @@ +using System.Runtime.Serialization; +using System.Text.Json.Serialization; + +using AzureOpenAIProxy.ApiApp.Converters; + +namespace AzureOpenAIProxy.ApiApp.Models; + +/// +/// This represent the entity for the resource details for admin. +/// +public class AdminResourceDetails +{ + /// + /// Gets or sets the event id. + /// + [JsonRequired] + public Guid ResourceId { get; set; } + + /// + /// Gets or sets the friendly name of the resource. + /// + [JsonRequired] + public string FriendlyName { get; set; } = string.Empty; + + /// + /// Gets or sets the deployment name of the resource. + /// + [JsonRequired] + public string DeploymentName { get; set; } = string.Empty; + + /// + /// Gets or sets the resource type. + /// + [JsonRequired] + public ResourceType ResourceType { get; set; } = ResourceType.None; + + /// + /// Gets or sets the resource endpoint. + /// + [JsonRequired] + public string Endpoint { get; set; } = string.Empty; + + /// + /// Gets or sets the resource API key. + /// + [JsonRequired] + public string ApiKey { get; set; } = string.Empty; + + /// + /// Gets or sets the resource region. + /// + [JsonRequired] + public string Region { get; set; } = string.Empty; + + /// + /// Gets or sets the value indicating whether the resource is active. + /// + [JsonRequired] + public bool IsActive { get; set; } +} + +/// +/// /// This defines the type of the resource. +[JsonConverter(typeof(EnumMemberConverter))] +public enum ResourceType +{ + /// + /// Indicates the resource type is not defined. + /// + [EnumMember(Value = "none")] + None, + + /// + /// Indicates the chat resource type. + /// + [EnumMember(Value = "chat")] + Chat, + + /// + /// Indicates the image resource type. + /// + [EnumMember(Value = "image")] + Image, +} \ No newline at end of file diff --git a/src/AzureOpenAIProxy.ApiApp/Models/EventDetails.cs b/src/AzureOpenAIProxy.ApiApp/Models/EventDetails.cs index ae3594f2..459ff655 100644 --- a/src/AzureOpenAIProxy.ApiApp/Models/EventDetails.cs +++ b/src/AzureOpenAIProxy.ApiApp/Models/EventDetails.cs @@ -1,37 +1,37 @@ -using System.Text.Json.Serialization; - -/// -/// This represents the event's detailed data for response by EventEndpoint. -/// -public class EventDetails -{ - /// - /// Gets or sets the event id. - /// - [JsonRequired] - public Guid EventId { get; set; } - - /// - /// Gets or sets the event title name. - /// - [JsonRequired] - public string Title { get; set; } = string.Empty; - - /// - /// Gets or sets the event summary. - /// - [JsonRequired] - public string Summary { get; set; } = string.Empty; - - /// - /// Gets or sets the Azure OpenAI Service request max token capacity. - /// - [JsonRequired] - public int MaxTokenCap { get; set; } - - /// - /// Gets or sets the Azure OpenAI Service daily request capacity. - /// - [JsonRequired] - public int DailyRequestCap { get; set; } +using System.Text.Json.Serialization; + +/// +/// This represent the entity for the event details for users. +/// +public class EventDetails +{ + /// + /// Gets or sets the event id. + /// + [JsonRequired] + public Guid EventId { get; set; } + + /// + /// Gets or sets the event title name. + /// + [JsonRequired] + public string Title { get; set; } = string.Empty; + + /// + /// Gets or sets the event summary. + /// + [JsonRequired] + public string Summary { get; set; } = string.Empty; + + /// + /// Gets or sets the Azure OpenAI Service request max token capacity. + /// + [JsonRequired] + public int MaxTokenCap { get; set; } + + /// + /// Gets or sets the Azure OpenAI Service daily request capacity. + /// + [JsonRequired] + public int DailyRequestCap { get; set; } } \ No newline at end of file diff --git a/test/AzureOpenAIProxy.ApiApp.Tests/Models/AdminResourceDetailsTests.cs b/test/AzureOpenAIProxy.ApiApp.Tests/Models/AdminResourceDetailsTests.cs new file mode 100644 index 00000000..fb94114b --- /dev/null +++ b/test/AzureOpenAIProxy.ApiApp.Tests/Models/AdminResourceDetailsTests.cs @@ -0,0 +1,69 @@ +using System.Text.Json; + +using AzureOpenAIProxy.ApiApp.Models; + +using FluentAssertions; + +namespace AzureOpenAIProxy.ApiApp.Tests.Models; + +public class AdminResourceDetailsTests +{ + private static readonly AdminResourceDetails examplePayload = new() + { + ResourceId = Guid.Parse("67f410a3-c5e4-4326-a3ad-5812b9adfc06"), + FriendlyName = "Test Resource", + DeploymentName = "Test Deployment", + ResourceType = ResourceType.Chat, + Endpoint = "https://test.endpoint.com", + ApiKey = "test-api-key", + Region = "test-region", + IsActive = true + }; + + private static readonly string exampleJson = """ + { + "resourceId": "67f410a3-c5e4-4326-a3ad-5812b9adfc06", + "friendlyName": "Test Resource", + "deploymentName": "Test Deployment", + "resourceType": "chat", + "endpoint": "https://test.endpoint.com", + "apiKey": "test-api-key", + "region": "test-region", + "isActive": true + } + """; + + private static readonly JsonSerializerOptions options = new() + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + WriteIndented = true + }; + + [Fact] + public void Given_ExamplePayload_When_Serialized_Then_It_Should_Match_Json() + { + // Act + var serialised = JsonSerializer.Serialize(examplePayload, options); + + // Assert + serialised.Should().ContainAll( + "\"resourceId\":", "\"67f410a3-c5e4-4326-a3ad-5812b9adfc06\"", + "\"friendlyName\":", "\"Test Resource\"", + "\"deploymentName\":", "\"Test Deployment\"", + "\"resourceType\":", "\"chat\"", + "\"endpoint\":", "\"https://test.endpoint.com\"", + "\"apiKey\":", "\"test-api-key\"", + "\"region\":", "\"test-region\"", + "\"isActive\":", "true"); + } + + [Fact] + public void Given_ExampleJson_When_Deserialized_Then_It_Should_Match_Object() + { + // Arrange & Act + var deserialised = JsonSerializer.Deserialize(exampleJson, options); + + // Assert + deserialised.Should().BeEquivalentTo(examplePayload); + } +} \ No newline at end of file diff --git a/test/AzureOpenAIProxy.ApiApp.Tests/Models/CreateChatCompletionResponseTests.cs b/test/AzureOpenAIProxy.ApiApp.Tests/Models/CreateChatCompletionResponseTests.cs index 6cdcdf5d..ed1e58ff 100644 --- a/test/AzureOpenAIProxy.ApiApp.Tests/Models/CreateChatCompletionResponseTests.cs +++ b/test/AzureOpenAIProxy.ApiApp.Tests/Models/CreateChatCompletionResponseTests.cs @@ -1,278 +1,279 @@ -using System.Text.Json; -using AzureOpenAIProxy.ApiApp.Models; -using FluentAssertions; - -namespace AzureOpenAIProxy.ApiApp.Tests.Models; - -public class CreateChatCompletionResponseTests -{ - private readonly string _exampleJson = @" - { - ""id"": ""string"", - ""object"": ""chat.completion"", - ""created"": 1620241923, - ""model"": ""string"", - ""usage"": { - ""prompt_tokens"": 0, - ""completion_tokens"": 0, - ""total_tokens"": 0 - }, - ""system_fingerprint"": ""string"", - ""prompt_filter_results"": [ - { - ""prompt_index"": 0, - ""content_filter_results"": { - ""sexual"": { - ""filtered"": true, - ""severity"": ""safe"" - }, - ""violence"": { - ""filtered"": true, - ""severity"": ""safe"" - }, - ""hate"": { - ""filtered"": true, - ""severity"": ""safe"" - }, - ""self_harm"": { - ""filtered"": true, - ""severity"": ""safe"" - }, - ""profanity"": { - ""filtered"": true, - ""detected"": true - }, - ""error"": { - ""code"": ""string"", - ""message"": ""string"" - }, - ""jailbreak"": { - ""filtered"": true, - ""detected"": true - } - } - } - ], - ""choices"": [ - { - ""index"": 0, - ""finish_reason"": ""string"", - ""message"": { - ""role"": ""assistant"", - ""content"": ""string"", - ""tool_calls"": [ - { - ""id"": ""string"", - ""type"": ""function"", - ""function"": { - ""name"": ""string"", - ""arguments"": ""string"" - } - } - ], - ""function_call"": { - ""name"": ""string"", - ""arguments"": ""string"" - }, - ""context"": { - ""citations"": [ - { - ""content"": ""string"", - ""title"": ""string"", - ""url"": ""string"", - ""filepath"": ""string"", - ""chunk_id"": ""string"" - } - ], - ""intent"": ""string"" - } - }, - ""content_filter_results"": { - ""sexual"": { - ""filtered"": true, - ""severity"": ""safe"" - }, - ""violence"": { - ""filtered"": true, - ""severity"": ""safe"" - }, - ""hate"": { - ""filtered"": true, - ""severity"": ""safe"" - }, - ""self_harm"": { - ""filtered"": true, - ""severity"": ""safe"" - }, - ""profanity"": { - ""filtered"": true, - ""detected"": true - }, - ""error"": { - ""code"": ""string"", - ""message"": ""string"" - }, - ""protected_material_text"": { - ""filtered"": true, - ""detected"": true - }, - ""protected_material_code"": { - ""filtered"": true, - ""detected"": true, - ""citation"": { - ""URL"": ""string"", - ""license"": ""string"" - } - } - }, - ""logprobs"": { - ""content"": [ - { - ""token"": ""string"", - ""logprob"": 0, - ""bytes"": [ - 0 - ], - ""top_logprobs"": [ - { - ""token"": ""string"", - ""logprob"": 0, - ""bytes"": [ - 0 - ] - } - ] - } - ] - } - } - ] - }"; - - private readonly CreateChatCompletionResponse exampleResponse = new CreateChatCompletionResponse - { - Id = "string", - Object = ChatCompletionResponseObject.ChatCompletion, - Created = 1620241923, - Model = "string", - Usage = new CompletionUsage - { - PromptTokens = 0, - CompletionTokens = 0, - TotalTokens = 0 - }, - SystemFingerprint = "string", - PromptFilterResults = new List - { - new PromptFilterResult - { - PromptIndex = 0, - ContentFilterResults = new ContentFilterPromptResults - { - Sexual = new ContentFilterSeverityResult { Filtered = true, Severity = ContentFilterSeverity.Safe }, - Violence = new ContentFilterSeverityResult { Filtered = true, Severity = ContentFilterSeverity.Safe }, - Hate = new ContentFilterSeverityResult { Filtered = true, Severity = ContentFilterSeverity.Safe }, - SelfHarm = new ContentFilterSeverityResult { Filtered = true, Severity = ContentFilterSeverity.Safe }, - Profanity = new ContentFilterDetectedResult { Filtered = true, Detected = true }, - Error = new ErrorBase { Code = "string", Message = "string" }, - Jailbreak = new ContentFilterDetectedResult { Filtered = true, Detected = true } - } - } - }, - Choices = new List - { - new ChatCompletionChoice - { - Index = 0, - FinishReason = "string", - Message = new ChatCompletionResponseMessage - { - Role = ChatCompletionResponseMessageRole.Assistant, - Content = "string", - ToolCalls = new List - { - new ChatCompletionMessageToolCall - { - Id = "string", - Type = ToolCallType.Function, - Function = new FunctionObject - { - Name = "string", - Arguments = "string" - } - } - }, - FunctionCall = new ChatCompletionFunctionCall { Name = "string", Arguments = "string" }, - Context = new AzureChatExtensionsMessageContext - { - Citations = new List - { - new Citation - { - Content = "string", - Title = "string", - Url = "string", - Filepath = "string", - ChunkId = "string" - } - }, - Intent = "string" - } - }, - ContentFilterResults = new ContentFilterChoiceResults - { - Sexual = new ContentFilterSeverityResult { Filtered = true, Severity = ContentFilterSeverity.Safe }, - Violence = new ContentFilterSeverityResult { Filtered = true, Severity = ContentFilterSeverity.Safe }, - Hate = new ContentFilterSeverityResult { Filtered = true, Severity = ContentFilterSeverity.Safe }, - SelfHarm = new ContentFilterSeverityResult { Filtered = true, Severity = ContentFilterSeverity.Safe }, - Profanity = new ContentFilterDetectedResult { Filtered = true, Detected = true }, - Error = new ErrorBase { Code = "string", Message = "string" }, - ProtectedMaterialText = new ContentFilterDetectedResult { Filtered = true, Detected = true }, - ProtectedMaterialCode = new ContentFilterDetectedWithCitationResult - { - Filtered = true, - Detected = true, - Citation = new CitationObject { URL = "string", License = "string" } - } - }, - LogProbs = new ChatCompletionChoiceLogProbs - { - Content = new List - { - new ChatCompletionTokenLogProb - { - Token = "string", - LogProb = 0, - Bytes = new List { 0 }, - TopLogProbs = new List - { - new TopLogProbs { Token = "string", LogProb = 0, Bytes = new List { 0 } } - } - } - } - } - } - } - }; - - [Fact] - public void Given_ExampleResponse_When_Serialized_Then_ShouldMatchExpectedJson() - { - // Act - var serializedJson = JsonSerializer.Serialize(exampleResponse, new JsonSerializerOptions { WriteIndented = false }); - - // Assert - serializedJson.Should().Be(_exampleJson.Replace("\r", "").Replace("\n", "").Replace(" ", "")); - } - - [Fact] - public void Given_ExampleJson_When_Deserialized_Then_ShouldReturnValidObject() - { - // Arrange & Act - var deserializedResponse = JsonSerializer.Deserialize(_exampleJson); - - // Assert - deserializedResponse.Should().NotBeNull(); - deserializedResponse.Should().BeEquivalentTo(exampleResponse); - } +using System.Text.Json; + +using AzureOpenAIProxy.ApiApp.Models; + +using FluentAssertions; + +namespace AzureOpenAIProxy.ApiApp.Tests.Models; + +public class CreateChatCompletionResponseTests +{ + private static readonly string exampleJson = @" + { + ""id"": ""string"", + ""object"": ""chat.completion"", + ""created"": 1620241923, + ""model"": ""string"", + ""usage"": { + ""prompt_tokens"": 0, + ""completion_tokens"": 0, + ""total_tokens"": 0 + }, + ""system_fingerprint"": ""string"", + ""prompt_filter_results"": [ + { + ""prompt_index"": 0, + ""content_filter_results"": { + ""sexual"": { + ""filtered"": true, + ""severity"": ""safe"" + }, + ""violence"": { + ""filtered"": true, + ""severity"": ""safe"" + }, + ""hate"": { + ""filtered"": true, + ""severity"": ""safe"" + }, + ""self_harm"": { + ""filtered"": true, + ""severity"": ""safe"" + }, + ""profanity"": { + ""filtered"": true, + ""detected"": true + }, + ""error"": { + ""code"": ""string"", + ""message"": ""string"" + }, + ""jailbreak"": { + ""filtered"": true, + ""detected"": true + } + } + } + ], + ""choices"": [ + { + ""index"": 0, + ""finish_reason"": ""string"", + ""message"": { + ""role"": ""assistant"", + ""content"": ""string"", + ""tool_calls"": [ + { + ""id"": ""string"", + ""type"": ""function"", + ""function"": { + ""name"": ""string"", + ""arguments"": ""string"" + } + } + ], + ""function_call"": { + ""name"": ""string"", + ""arguments"": ""string"" + }, + ""context"": { + ""citations"": [ + { + ""content"": ""string"", + ""title"": ""string"", + ""url"": ""string"", + ""filepath"": ""string"", + ""chunk_id"": ""string"" + } + ], + ""intent"": ""string"" + } + }, + ""content_filter_results"": { + ""sexual"": { + ""filtered"": true, + ""severity"": ""safe"" + }, + ""violence"": { + ""filtered"": true, + ""severity"": ""safe"" + }, + ""hate"": { + ""filtered"": true, + ""severity"": ""safe"" + }, + ""self_harm"": { + ""filtered"": true, + ""severity"": ""safe"" + }, + ""profanity"": { + ""filtered"": true, + ""detected"": true + }, + ""error"": { + ""code"": ""string"", + ""message"": ""string"" + }, + ""protected_material_text"": { + ""filtered"": true, + ""detected"": true + }, + ""protected_material_code"": { + ""filtered"": true, + ""detected"": true, + ""citation"": { + ""URL"": ""string"", + ""license"": ""string"" + } + } + }, + ""logprobs"": { + ""content"": [ + { + ""token"": ""string"", + ""logprob"": 0, + ""bytes"": [ + 0 + ], + ""top_logprobs"": [ + { + ""token"": ""string"", + ""logprob"": 0, + ""bytes"": [ + 0 + ] + } + ] + } + ] + } + } + ] + }"; + + private static readonly CreateChatCompletionResponse examplePayload = new () + { + Id = "string", + Object = ChatCompletionResponseObject.ChatCompletion, + Created = 1620241923, + Model = "string", + Usage = new CompletionUsage + { + PromptTokens = 0, + CompletionTokens = 0, + TotalTokens = 0 + }, + SystemFingerprint = "string", + PromptFilterResults = new List + { + new PromptFilterResult + { + PromptIndex = 0, + ContentFilterResults = new ContentFilterPromptResults + { + Sexual = new ContentFilterSeverityResult { Filtered = true, Severity = ContentFilterSeverity.Safe }, + Violence = new ContentFilterSeverityResult { Filtered = true, Severity = ContentFilterSeverity.Safe }, + Hate = new ContentFilterSeverityResult { Filtered = true, Severity = ContentFilterSeverity.Safe }, + SelfHarm = new ContentFilterSeverityResult { Filtered = true, Severity = ContentFilterSeverity.Safe }, + Profanity = new ContentFilterDetectedResult { Filtered = true, Detected = true }, + Error = new ErrorBase { Code = "string", Message = "string" }, + Jailbreak = new ContentFilterDetectedResult { Filtered = true, Detected = true } + } + } + }, + Choices = new List + { + new ChatCompletionChoice + { + Index = 0, + FinishReason = "string", + Message = new ChatCompletionResponseMessage + { + Role = ChatCompletionResponseMessageRole.Assistant, + Content = "string", + ToolCalls = new List + { + new ChatCompletionMessageToolCall + { + Id = "string", + Type = ToolCallType.Function, + Function = new FunctionObject + { + Name = "string", + Arguments = "string" + } + } + }, + FunctionCall = new ChatCompletionFunctionCall { Name = "string", Arguments = "string" }, + Context = new AzureChatExtensionsMessageContext + { + Citations = new List + { + new Citation + { + Content = "string", + Title = "string", + Url = "string", + Filepath = "string", + ChunkId = "string" + } + }, + Intent = "string" + } + }, + ContentFilterResults = new ContentFilterChoiceResults + { + Sexual = new ContentFilterSeverityResult { Filtered = true, Severity = ContentFilterSeverity.Safe }, + Violence = new ContentFilterSeverityResult { Filtered = true, Severity = ContentFilterSeverity.Safe }, + Hate = new ContentFilterSeverityResult { Filtered = true, Severity = ContentFilterSeverity.Safe }, + SelfHarm = new ContentFilterSeverityResult { Filtered = true, Severity = ContentFilterSeverity.Safe }, + Profanity = new ContentFilterDetectedResult { Filtered = true, Detected = true }, + Error = new ErrorBase { Code = "string", Message = "string" }, + ProtectedMaterialText = new ContentFilterDetectedResult { Filtered = true, Detected = true }, + ProtectedMaterialCode = new ContentFilterDetectedWithCitationResult + { + Filtered = true, + Detected = true, + Citation = new CitationObject { URL = "string", License = "string" } + } + }, + LogProbs = new ChatCompletionChoiceLogProbs + { + Content = new List + { + new ChatCompletionTokenLogProb + { + Token = "string", + LogProb = 0, + Bytes = new List { 0 }, + TopLogProbs = new List + { + new TopLogProbs { Token = "string", LogProb = 0, Bytes = new List { 0 } } + } + } + } + } + } + } + }; + + [Fact] + public void Given_ExamplePayload_When_Serialized_Then_It_Should_Match_Json() + { + // Act + var serialised = JsonSerializer.Serialize(examplePayload, new JsonSerializerOptions { WriteIndented = false }); + + // Assert + serialised.Should().Be(exampleJson.Replace("\r", "").Replace("\n", "").Replace(" ", "")); + } + + [Fact] + public void Given_ExampleJson_When_Deserialized_Then_It_Should__Match_Object() + { + // Arrange & Act + var deserialised = JsonSerializer.Deserialize(exampleJson); + + // Assert + deserialised.Should().BeEquivalentTo(examplePayload); + } } \ No newline at end of file