Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,15 @@ public static string Write(Dictionary<string, string> headers) =>
public static Dictionary<string, string> Read(string headersJson) =>
JsonSerializer.Deserialize(headersJson, context.DictionaryStringString) ?? [];

// The relaxed encoder is only unsafe for JSON embedded in HTML, and this JSON never reaches HTML. Full text search needs apostrophes, plus signs and non-ASCII letters written as they are, or it cannot find the words next to them.
static readonly HeadersJsonContext context = new(new JsonSerializerOptions { Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping });
static readonly HeadersJsonContext context = new(new JsonSerializerOptions
{
// The relaxed encoder is only unsafe for JSON embedded in HTML, and this JSON never reaches HTML. Full text search needs apostrophes, plus signs and non-ASCII letters written as they are, or it cannot find the words next to them.
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,

// In compact JSON, SQL Server's word breaker keeps JSON quotes and colons on the words before an email address or a URL, and a search cannot find those words. An indent of 0 adds only the whitespace that stops this: a line break after each entry and a space after each colon.
WriteIndented = true,
IndentSize = 0
});
}

[JsonSerializable(typeof(Dictionary<string, string>))]
Expand Down
26 changes: 26 additions & 0 deletions src/ServiceControl.Audit.Persistence.Tests/EFCore/EFQueryTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,32 @@ public async Task Does_not_match_messages_that_share_part_of_the_message_id()
Assert.That(found.Results.Select(view => view.MessageId), Is.EqualTo(new[] { "billing-1234-5678" }));
}

// In compact JSON, SQL Server's word breaker keeps JSON quotes and colons on words up to a few hundred characters before an email address.
// The reply-to address goes straight after the id, so the test does not depend on how many headers MakeMessage writes.
[Test]
public async Task Finds_the_message_id_when_a_header_after_it_looks_like_an_email_address()
{
var message = MakeMessage(messageId: "billing-3333-5678");
var headers = new Dictionary<string, string>
{
[Headers.MessageId] = "billing-3333-5678",
[Headers.ReplyToAddress] = "Sales@web01.corp.example.com"
};

foreach (var header in message.Headers)
{
headers.TryAdd(header.Key, header.Value);
}

message.Headers = headers;

await Ingest(message);

var found = await MessagesViewStore.QueryMessages("billing-3333-5678", new PagingInfo(), new SortInfo("time_sent", "desc"));

Assert.That(found.Results.Select(view => view.MessageId), Is.EqualTo(new[] { "billing-3333-5678" }));
}

[Test]
public async Task Matches_a_term_ending_in_a_star_as_a_prefix()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,15 @@ public static string Write(Dictionary<string, string> headers) =>
public static Dictionary<string, string> Read(string headersJson) =>
JsonSerializer.Deserialize(headersJson, context.DictionaryStringString) ?? [];

// The relaxed encoder is only unsafe for JSON embedded in HTML, and this JSON never reaches HTML. Full text search needs apostrophes, plus signs and non-ASCII letters written as they are, or it cannot find the words next to them.
static readonly HeadersJsonContext context = new(new JsonSerializerOptions { Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping });
static readonly HeadersJsonContext context = new(new JsonSerializerOptions
{
// The relaxed encoder is only unsafe for JSON embedded in HTML, and this JSON never reaches HTML. Full text search needs apostrophes, plus signs and non-ASCII letters written as they are, or it cannot find the words next to them.
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,

// In compact JSON, SQL Server's word breaker keeps JSON quotes and colons on the words before an email address or a URL, and a search cannot find those words. An indent of 0 adds only the whitespace that stops this: a line break after each entry and a space after each colon.
WriteIndented = true,
IndentSize = 0
});
}

// Source generated serialization, which keeps the reflection-based serializer off the ingestion hot path.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,20 @@ public async Task Searches_the_message_id()
await AssertSearchFinds(matching.MessageId, matching);
}

/// <summary>
/// In compact JSON, SQL Server's word breaker keeps JSON quotes and colons on words up to a few hundred characters before an email address.
/// The reply-to address goes straight after the id, so the test does not depend on how many other headers there are.
/// </summary>
[Test]
public async Task Finds_the_message_id_when_a_header_after_it_looks_like_an_email_address()
{
var matching = new IngestedFailure { ReplyToAddress = "Sales@web01.corp.example.com" };

await Ingest(matching);

await AssertSearchFinds(matching.MessageId, matching);
}

[Test]
public async Task Ors_the_search_terms()
{
Expand Down
26 changes: 16 additions & 10 deletions src/ServiceControl.Persistence.Tests/IngestedFailure.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ class IngestedFailure
public string MessageType { get; init; } = "MyCompany.Sales.OrderPlaced";
public string ConversationId { get; init; } = Guid.NewGuid().ToString();
public string QueueAddress { get; init; } = "error";
public string ReplyToAddress { get; init; }
public string FailingEndpointAddress { get; init; }
public string ExceptionType { get; init; } = "System.InvalidOperationException";
public string ExceptionMessage { get; init; } = "Something went wrong";
Expand All @@ -42,17 +43,21 @@ class IngestedFailure

Dictionary<string, string> BuildHeaders()
{
var headers = new Dictionary<string, string>
var headers = new Dictionary<string, string> { [NServiceBus.Headers.MessageId] = MessageId };

// Straight after the id, so a search test controls what is stored next to it.
if (ReplyToAddress != null)
{
[NServiceBus.Headers.MessageId] = MessageId,
[NServiceBus.Headers.ProcessingEndpoint] = EndpointName,
[NServiceBus.Headers.ContentType] = ContentType,
[NServiceBus.Headers.EnclosedMessageTypes] = MessageType,
[NServiceBus.Headers.MessageIntent] = MessageIntent.ToString(),
["NServiceBus.FailedQ"] = QueueAddress,
["NServiceBus.ExceptionInfo.ExceptionType"] = ExceptionType,
["NServiceBus.ExceptionInfo.Message"] = ExceptionMessage
};
headers[NServiceBus.Headers.ReplyToAddress] = ReplyToAddress;
}

headers[NServiceBus.Headers.ProcessingEndpoint] = EndpointName;
headers[NServiceBus.Headers.ContentType] = ContentType;
headers[NServiceBus.Headers.EnclosedMessageTypes] = MessageType;
headers[NServiceBus.Headers.MessageIntent] = MessageIntent.ToString();
headers["NServiceBus.FailedQ"] = QueueAddress;
headers["NServiceBus.ExceptionInfo.ExceptionType"] = ExceptionType;
headers["NServiceBus.ExceptionInfo.Message"] = ExceptionMessage;

if (ExceptionSource != null)
{
Expand Down Expand Up @@ -120,6 +125,7 @@ Dictionary<string, string> BuildHeaders()
MessageType = MessageType,
ConversationId = ConversationId,
QueueAddress = QueueAddress,
ReplyToAddress = ReplyToAddress,
FailingEndpointAddress = FailingEndpointAddress,
ExceptionType = ExceptionType,
ExceptionMessage = ExceptionMessage,
Expand Down
Loading