Skip to content

fix!: reject an empty prefix or suffix expectation - #1258

Merged
vbreuss merged 1 commit into
mainfrom
fix/empty-prefix-suffix-guard
Sep 19, 2026
Merged

vbreuss merged 1 commit into
mainfrom
fix/empty-prefix-suffix-guard

Conversation

@vbreuss

@vbreuss vbreuss commented Sep 19, 2026

Copy link
Copy Markdown
Member

StartsWith("") and EndsWith("") throw because an empty expectation is almost always a mistake - an accidentally empty variable - but the same expectation written as IsEqualTo("").AsPrefix() or .AsSuffix() silently passed. One expectation had two spellings that disagreed, and the silent one is the dangerous one, so the guard now also applies to the match type, wherever it is reached: IsEqualTo, Contains, HasItem and the string collection overloads.

Just like a regex or wildcard pattern, a prefix is only known when the expectation is verified, so the guard sits next to them in StringEqualityOptions and fires after the normalization: a prefix that only becomes empty through IgnoringIndentation() says just as little as a literal empty one. A null prefix or suffix is not rejected, because unlike a null regex it still expresses that the subject is null, an expectation that can fail in either polarity, and IsEqualTo deliberately accepts a nullable argument.

`StartsWith("")` and `EndsWith("")` throw because an empty expectation is almost always a mistake - an accidentally empty variable - but the same expectation written as `IsEqualTo("").AsPrefix()` or `.AsSuffix()` silently passed. One expectation had two spellings that disagreed, and the silent one is the dangerous one, so the guard now also applies to the match type, wherever it is reached: `IsEqualTo`, `Contains`, `HasItem` and the string collection overloads.

Just like a regex or wildcard pattern, a prefix is only known when the expectation is verified, so the guard sits next to them in `StringEqualityOptions` and fires after the normalization: a prefix that only becomes empty through `IgnoringIndentation()` says just as little as a literal empty one. A `null` prefix or suffix is not rejected, because unlike a null regex it still expresses that the subject is null, an expectation that can fail in either polarity, and `IsEqualTo` deliberately accepts a nullable argument.

FluentAssertions follows the BCL, where every string starts with the empty string, so migrating users can hit the new exception.
@vbreuss vbreuss self-assigned this Sep 19, 2026
@vbreuss
vbreuss enabled auto-merge (squash) September 19, 2026 17:06
@sonarqubecloud

Copy link
Copy Markdown

@github-actions

Copy link
Copy Markdown
Contributor

Test Results

     6 files  ±0       6 suites  ±0   1m 41s ⏱️ -3s
 1 972 tests ±0   1 972 ✅ ±0  0 💤 ±0  0 ❌ ±0 
11 202 runs  ±0  11 202 ✅ ±0  0 💤 ±0  0 ❌ ±0 

Results for commit cac677a. ± Comparison against base commit 08dca50.

This pull request removes 126 and adds 126 tests. Note that renamed tests count towards both.
aweXpect.Core.Tests.Core.Exceptions.FailExceptionTests ‑ InnerException_ShouldBeSet(message: "message2b376b34-49fa-4cc9-844a-09e305203bbf")
aweXpect.Core.Tests.Core.Exceptions.FailExceptionTests ‑ InnerException_ShouldBeSet(message: "message343b7088-0df7-4272-b27f-23cbe83552a1")
aweXpect.Core.Tests.Core.Exceptions.FailExceptionTests ‑ InnerException_ShouldBeSet(message: "message696ecdb0-2055-4f0e-9081-b07715c4322a")
aweXpect.Core.Tests.Core.Exceptions.FailExceptionTests ‑ InnerException_ShouldBeSet(message: "message757f0017-bb11-419f-b952-a3f9dea888db")
aweXpect.Core.Tests.Core.Exceptions.FailExceptionTests ‑ InnerException_ShouldBeSet(message: "messageb0c492e5-12f3-454b-8306-901d058a0d41")
aweXpect.Core.Tests.Core.Exceptions.FailExceptionTests ‑ InnerException_ShouldBeSet(message: "messageb37fd547-d269-49af-96ea-d72e49c567a7")
aweXpect.Core.Tests.Core.Exceptions.FailExceptionTests ‑ Message_ShouldBeSet(message: "message2162412f-ae4a-4cdf-9c8b-ed96a0ed7265")
aweXpect.Core.Tests.Core.Exceptions.FailExceptionTests ‑ Message_ShouldBeSet(message: "message873eb069-cad5-4280-bb08-727d830eda8c")
aweXpect.Core.Tests.Core.Exceptions.FailExceptionTests ‑ Message_ShouldBeSet(message: "messagec965e694-7b23-4ec5-ad83-210a90d23f6f")
aweXpect.Core.Tests.Core.Exceptions.FailExceptionTests ‑ Message_ShouldBeSet(message: "messagecd4da603-a8e0-4827-a7cd-56fc624336a2")
…
aweXpect.Core.Tests.Core.Exceptions.FailExceptionTests ‑ InnerException_ShouldBeSet(message: "message4e9bb02d-9422-42c8-b578-f226fb0ed364")
aweXpect.Core.Tests.Core.Exceptions.FailExceptionTests ‑ InnerException_ShouldBeSet(message: "message818e26c5-b133-4544-b8c6-dbe23849895c")
aweXpect.Core.Tests.Core.Exceptions.FailExceptionTests ‑ InnerException_ShouldBeSet(message: "messageaee77901-33fb-4672-b107-0a3fdf24b104")
aweXpect.Core.Tests.Core.Exceptions.FailExceptionTests ‑ InnerException_ShouldBeSet(message: "messaged98c5735-22e7-4719-8170-33ab2c115216")
aweXpect.Core.Tests.Core.Exceptions.FailExceptionTests ‑ InnerException_ShouldBeSet(message: "messageec623379-aaa5-440f-abd3-77b443b36b4d")
aweXpect.Core.Tests.Core.Exceptions.FailExceptionTests ‑ InnerException_ShouldBeSet(message: "messagef356d6a4-f311-4d38-99ef-f36e511003fd")
aweXpect.Core.Tests.Core.Exceptions.FailExceptionTests ‑ Message_ShouldBeSet(message: "message107c7e87-10fd-49bd-83ec-118be184e7c4")
aweXpect.Core.Tests.Core.Exceptions.FailExceptionTests ‑ Message_ShouldBeSet(message: "message2619405e-fd4d-4ba9-99d6-87bd550bcfe3")
aweXpect.Core.Tests.Core.Exceptions.FailExceptionTests ‑ Message_ShouldBeSet(message: "message3c42f3d1-fc60-4746-9aaa-64b286786b57")
aweXpect.Core.Tests.Core.Exceptions.FailExceptionTests ‑ Message_ShouldBeSet(message: "message6671652c-5914-4a77-9f2f-703f8d64182e")
…

@vbreuss
vbreuss merged commit 3c03424 into main Sep 19, 2026
16 checks passed
@vbreuss
vbreuss deleted the fix/empty-prefix-suffix-guard branch September 19, 2026 17:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant