Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extend AssemblyAssertions with HavePublicKey and BeUnsigned #2207

Merged
merged 30 commits into from
Aug 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
425baee
Setup for extending Assembly assertions.
May 16, 2023
78fc862
Simplify tests
May 16, 2023
13181ba
Use extension method.
May 26, 2023
9400676
Add unsigned assembly.
May 26, 2023
d82e22e
Extend assertions.
May 26, 2023
5b95444
Update Src/FluentAssertions/Specialized/AssemblyAssertions.cs
Corniel May 26, 2023
3508d41
Never null.
May 26, 2023
a79c333
renamed variables to improve readability.
May 30, 2023
8026664
Change message.
May 30, 2023
b238c28
Use AssemblyStub to test signing.
Jun 8, 2023
37e9d34
Changed contract.
Jun 8, 2023
5e2a60d
Naming only.
Jun 23, 2023
998194b
Deal with null cases.
Corniel Jul 18, 2023
7cc98b3
Update API contract.
Corniel Jul 20, 2023
095a906
Applied suggestions.
Corniel Jul 30, 2023
89104f5
Update Src/FluentAssertions/Specialized/AssemblyAssertions.cs
Corniel Jul 30, 2023
fbbc2c9
Update Tests/FluentAssertions.Specs/FindAssembly.cs
Corniel Jul 30, 2023
18e6a12
Use Convert.ToHexString(bytes) for .NET 6 and up.
Corniel Jul 30, 2023
21599b9
Add reason to fail with.
Corniel Jul 30, 2023
623d6a7
Assembly null and array.empty tests for GetPublicKey().
Corniel Jul 31, 2023
c86d0a4
Applied qodana suggestions.
Corniel Aug 15, 2023
afc50bf
Added documentation.
Corniel Aug 15, 2023
ab2f9dd
Updated the release notes.
Corniel Aug 15, 2023
193ea73
Update docs/_pages/assemblies.md
Corniel Aug 15, 2023
3a2ddb6
Update docs/_pages/releases.md
Corniel Aug 15, 2023
7b427f2
Add example.
Corniel Aug 15, 2023
0cd5424
Update docs/_pages/assemblies.md
Corniel Aug 16, 2023
2500600
Now fully solved.
Corniel Aug 16, 2023
a942b5e
revert.
Corniel Aug 16, 2023
4de6b37
Update docs/_pages/assemblies.md
Corniel Aug 16, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 1 addition & 2 deletions FluentAssertions.sln
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.3.32505.426
Expand Down Expand Up @@ -52,7 +51,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FluentAssertions.Equivalenc
EndProject
Project("{778DAE3C-4631-46EA-AA77-85C1314464D9}") = "VB.Specs", "Tests\VB.Specs\VB.Specs.vbproj", "{0C0211B6-D185-4518-A15A-38AC092EDC50}"
EndProject
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "FSharp.Specs", "Tests\FSharp.Specs\FSharp.Specs.fsproj", "{0A69DC62-CA14-44E5-BAF9-2EB2E2E2CADF}"
Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "FSharp.Specs", "Tests\FSharp.Specs\FSharp.Specs.fsproj", "{0A69DC62-CA14-44E5-BAF9-2EB2E2E2CADF}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Expand Down
76 changes: 75 additions & 1 deletion Src/FluentAssertions/Specialized/AssemblyAssertions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ public AndConstraint<AssemblyAssertions> Reference(Assembly assembly, string bec
}

/// <summary>
/// Asserts that the Assembly defines a type called <paramref name="namespace"/> and <paramref name="name"/>.
/// Asserts that the assembly defines a type called <paramref name="namespace"/> and <paramref name="name"/>.
/// </summary>
/// <param name="namespace">The namespace of the class.</param>
/// <param name="name">The name of the class.</param>
Expand Down Expand Up @@ -139,6 +139,80 @@ public AndConstraint<AssemblyAssertions> Reference(Assembly assembly, string bec
return new AndWhichConstraint<AssemblyAssertions, Type>(this, foundType);
}

/// <summary>Asserts that the assembly is unsigned.</summary>
/// <param name="because">
/// A formatted phrase as is supported by <see cref="string.Format(string,object[])" /> explaining why the assertion
/// is needed. If the phrase does not start with the word <i>because</i>, it is prepended automatically.
/// </param>
/// <param name="becauseArgs">
/// Zero or more objects to format using the placeholders in <paramref name="because"/>.
/// </param>
public AndConstraint<AssemblyAssertions> BeUnsigned(string because = "", params object[] becauseArgs)
dennisdoomen marked this conversation as resolved.
Show resolved Hide resolved
{
bool success = Execute.Assertion
.ForCondition(Subject is not null)
.FailWith("Can't check for assembly signing if {context:assembly} reference is <null>.");

if (success)
{
Execute.Assertion
.BecauseOf(because, becauseArgs)
.ForCondition(Subject.GetName().GetPublicKey() is not { Length: > 0 })
.FailWith(
"Did not expect the assembly {0} to be signed{reason}, but it is.", Subject.FullName);
Corniel marked this conversation as resolved.
Show resolved Hide resolved
Corniel marked this conversation as resolved.
Show resolved Hide resolved
}

return new AndConstraint<AssemblyAssertions>(this);
}

/// <summary>Asserts that the assembly is signed with the specified public key.</summary>
/// <param name="publicKey">
/// The base-16 string representation of the public key, like "e0851575614491c6d25018fadb75".
/// </param>
/// <param name="because">
/// A formatted phrase as is supported by <see cref="string.Format(string,object[])" /> explaining why the assertion
/// is needed. If the phrase does not start with the word <i>because</i>, it is prepended automatically.
/// </param>
/// <param name="becauseArgs">
/// Zero or more objects to format using the placeholders in <paramref name="because"/>.
/// </param>
/// <exception cref="ArgumentNullException"><paramref name="publicKey"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException"><paramref name="publicKey"/> is empty.</exception>
public AndConstraint<AssemblyAssertions> BeSignedWithPublicKey(string publicKey, string because = "", params object[] becauseArgs)
{
Guard.ThrowIfArgumentIsNullOrEmpty(publicKey);

bool success = Execute.Assertion
.ForCondition(Subject is not null)
.FailWith("Can't check for assembly signing if {context:assembly} reference is <null>.");

if (success)
{
var bytes = Subject.GetName().GetPublicKey() ?? Array.Empty<byte>();
jnyrup marked this conversation as resolved.
Show resolved Hide resolved
string assemblyKey = ToHexString(bytes);

Execute.Assertion
.BecauseOf(because, becauseArgs)
.WithExpectation("Expected assembly {0} to have public key {1} ", Subject.FullName, publicKey)
.ForCondition(bytes.Length != 0)
.FailWith("{reason}, but it is unsigned.")
.Then
.ForCondition(string.Equals(assemblyKey, publicKey, StringComparison.OrdinalIgnoreCase))
.FailWith("{reason}, but it has {0} instead.", assemblyKey)
.Then
.ClearExpectation();
}

return new AndConstraint<AssemblyAssertions>(this);
}

private static string ToHexString(byte[] bytes) =>
#if NET6_0_OR_GREATER
Convert.ToHexString(bytes);
#else
BitConverter.ToString(bytes).Replace("-", string.Empty, StringComparison.Ordinal);
#endif

/// <summary>
/// Returns the type of the subject the assertion applies on.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2244,6 +2244,8 @@ namespace FluentAssertions.Reflection
{
public AssemblyAssertions(System.Reflection.Assembly assembly) { }
protected override string Identifier { get; }
public FluentAssertions.AndConstraint<FluentAssertions.Reflection.AssemblyAssertions> BeSignedWithPublicKey(string publicKey, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<FluentAssertions.Reflection.AssemblyAssertions> BeUnsigned(string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndWhichConstraint<FluentAssertions.Reflection.AssemblyAssertions, System.Type> DefineType(string @namespace, string name, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<FluentAssertions.Reflection.AssemblyAssertions> NotReference(System.Reflection.Assembly assembly, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<FluentAssertions.Reflection.AssemblyAssertions> Reference(System.Reflection.Assembly assembly, string because = "", params object[] becauseArgs) { }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2365,6 +2365,8 @@ namespace FluentAssertions.Reflection
{
public AssemblyAssertions(System.Reflection.Assembly assembly) { }
protected override string Identifier { get; }
public FluentAssertions.AndConstraint<FluentAssertions.Reflection.AssemblyAssertions> BeSignedWithPublicKey(string publicKey, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<FluentAssertions.Reflection.AssemblyAssertions> BeUnsigned(string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndWhichConstraint<FluentAssertions.Reflection.AssemblyAssertions, System.Type> DefineType(string @namespace, string name, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<FluentAssertions.Reflection.AssemblyAssertions> NotReference(System.Reflection.Assembly assembly, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<FluentAssertions.Reflection.AssemblyAssertions> Reference(System.Reflection.Assembly assembly, string because = "", params object[] becauseArgs) { }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2244,6 +2244,8 @@ namespace FluentAssertions.Reflection
{
public AssemblyAssertions(System.Reflection.Assembly assembly) { }
protected override string Identifier { get; }
public FluentAssertions.AndConstraint<FluentAssertions.Reflection.AssemblyAssertions> BeSignedWithPublicKey(string publicKey, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<FluentAssertions.Reflection.AssemblyAssertions> BeUnsigned(string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndWhichConstraint<FluentAssertions.Reflection.AssemblyAssertions, System.Type> DefineType(string @namespace, string name, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<FluentAssertions.Reflection.AssemblyAssertions> NotReference(System.Reflection.Assembly assembly, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<FluentAssertions.Reflection.AssemblyAssertions> Reference(System.Reflection.Assembly assembly, string because = "", params object[] becauseArgs) { }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2244,6 +2244,8 @@ namespace FluentAssertions.Reflection
{
public AssemblyAssertions(System.Reflection.Assembly assembly) { }
protected override string Identifier { get; }
public FluentAssertions.AndConstraint<FluentAssertions.Reflection.AssemblyAssertions> BeSignedWithPublicKey(string publicKey, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<FluentAssertions.Reflection.AssemblyAssertions> BeUnsigned(string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndWhichConstraint<FluentAssertions.Reflection.AssemblyAssertions, System.Type> DefineType(string @namespace, string name, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<FluentAssertions.Reflection.AssemblyAssertions> NotReference(System.Reflection.Assembly assembly, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<FluentAssertions.Reflection.AssemblyAssertions> Reference(System.Reflection.Assembly assembly, string because = "", params object[] becauseArgs) { }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2195,6 +2195,8 @@ namespace FluentAssertions.Reflection
{
public AssemblyAssertions(System.Reflection.Assembly assembly) { }
protected override string Identifier { get; }
public FluentAssertions.AndConstraint<FluentAssertions.Reflection.AssemblyAssertions> BeSignedWithPublicKey(string publicKey, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<FluentAssertions.Reflection.AssemblyAssertions> BeUnsigned(string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndWhichConstraint<FluentAssertions.Reflection.AssemblyAssertions, System.Type> DefineType(string @namespace, string name, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<FluentAssertions.Reflection.AssemblyAssertions> NotReference(System.Reflection.Assembly assembly, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<FluentAssertions.Reflection.AssemblyAssertions> Reference(System.Reflection.Assembly assembly, string because = "", params object[] becauseArgs) { }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2244,6 +2244,8 @@ namespace FluentAssertions.Reflection
{
public AssemblyAssertions(System.Reflection.Assembly assembly) { }
protected override string Identifier { get; }
public FluentAssertions.AndConstraint<FluentAssertions.Reflection.AssemblyAssertions> BeSignedWithPublicKey(string publicKey, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<FluentAssertions.Reflection.AssemblyAssertions> BeUnsigned(string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndWhichConstraint<FluentAssertions.Reflection.AssemblyAssertions, System.Type> DefineType(string @namespace, string name, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<FluentAssertions.Reflection.AssemblyAssertions> NotReference(System.Reflection.Assembly assembly, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<FluentAssertions.Reflection.AssemblyAssertions> Reference(System.Reflection.Assembly assembly, string because = "", params object[] becauseArgs) { }
Expand Down
45 changes: 42 additions & 3 deletions Tests/FluentAssertions.Specs/FindAssembly.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,50 @@
using System.Reflection;
using System;
using System.Reflection;

namespace FluentAssertions.Specs;

public static class FindAssembly
{
public static Assembly Containing<T>()
public static Assembly Containing<T>() => typeof(T).Assembly;

public static Assembly Stub(string publicKey) => new AssemblyStub(publicKey);

private sealed class AssemblyStub : Assembly
{
return typeof(T).Assembly;
private readonly AssemblyName assemblyName = new();

public override string FullName => nameof(AssemblyStub);

public AssemblyStub(string publicKey)
{
assemblyName.SetPublicKey(FromHexString(publicKey));
}

public override AssemblyName GetName() => assemblyName;

#if NET6_0_OR_GREATER
private static byte[] FromHexString(string chars)
=> chars is null
? null
: Convert.FromHexString(chars);
#else
private static byte[] FromHexString(string chars)
{
if (chars is null)
{
return null;
}

var bytes = new byte[chars.Length / 2];

for (var i = 0; i < bytes.Length; i++)
{
var bits = chars.Substring(i * 2, 2);
bytes[i] = Convert.ToByte(bits, 16);
}

return bytes;
}
#endif
}
}
2 changes: 1 addition & 1 deletion Tests/FluentAssertions.Specs/FluentAssertions.Specs.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<SignAssembly>True</SignAssembly>
<AssemblyOriginatorKeyFile>..\..\Src\FluentAssertions\FluentAssertions.snk</AssemblyOriginatorKeyFile>
<IsPackable>false</IsPackable>
<NoWarn>$(NoWarn),IDE0052,1573,1591,1712</NoWarn>
<NoWarn>$(NoWarn),IDE0052,1573,1591,1712,CS8002</NoWarn>
<DebugType>full</DebugType>
</PropertyGroup>

Expand Down
103 changes: 103 additions & 0 deletions Tests/FluentAssertions.Specs/Specialized/AssemblyAssertionSpecs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,109 @@ public void When_an_assembly_is_null_and_Should_BeNull_is_asserted_it_should_suc
act.Should().NotThrow();
}
}

public class BeUnsigned
{
[Theory]
[InlineData(null)]
[InlineData("")]
public void Guards_for_unsigned_assembly(string noKey)
{
// Arrange
var unsignedAssembly = FindAssembly.Stub(noKey);

// Act & Assert
unsignedAssembly.Should().BeUnsigned();
}

[Fact]
public void Throws_for_signed_assembly()
{
// Arrange
var signedAssembly = FindAssembly.Stub("0123456789ABCEF007");

// Act
Action act = () => signedAssembly.Should().BeUnsigned("this assembly is never shipped");

// Assert
act.Should().Throw<XunitException>()
.WithMessage("Did not expect the assembly * to be signed because this assembly is never shipped, but it is.");
}

[Fact]
public void Throws_for_null_subject()
{
// Arrange
Assembly nullAssembly = null;

// Act
Action act = () => nullAssembly.Should().BeUnsigned();

// Assert
act.Should().Throw<XunitException>()
.WithMessage("Can't check for assembly signing if nullAssembly reference is <null>.");
}
}

public class BeSignedWithPublicKey
{
[Theory]
[InlineData("0123456789ABCEF007")]
[InlineData("0123456789abcef007")]
[InlineData("0123456789ABcef007")]
public void Guards_for_signed_assembly_with_expected_public_key(string publicKey)
{
// Arrange
var signedAssembly = FindAssembly.Stub("0123456789ABCEF007");

// Act & Assert
signedAssembly.Should().BeSignedWithPublicKey(publicKey);
}

[Theory]
[InlineData(null)]
[InlineData("")]
public void Throws_for_unsigned_assembly(string noKey)
{
// Arrange
var unsignedAssembly = FindAssembly.Stub(noKey);

// Act
Action act = () => unsignedAssembly.Should().BeSignedWithPublicKey("1234", "signing is part of the contract");

// Assert
act.Should().Throw<XunitException>()
.WithMessage("Expected assembly * to have public key \"1234\" because signing is part of the contract, but it is unsigned.");
}

[Fact]
public void Throws_signed_assembly_with_different_public_key()
{
// Arrange
var signedAssembly = FindAssembly.Stub("0123456789ABCEF007");

// Act
Action act = () => signedAssembly.Should().BeSignedWithPublicKey("1234", "signing is part of the contract");

// Assert
act.Should().Throw<XunitException>()
.WithMessage("Expected assembly * to have public key \"1234\" because signing is part of the contract, but it has * instead.");
}

[Fact]
public void Throws_for_null_assembly()
{
// Arrange
Assembly nullAssembly = null;

// Act
Action act = () => nullAssembly.Should().BeSignedWithPublicKey("1234");

// Assert
act.Should().Throw<XunitException>()
.WithMessage("Can't check for assembly signing if nullAssembly reference is <null>.");
}
}
}

[DummyClass("name", true)]
Expand Down
11 changes: 10 additions & 1 deletion docs/_pages/assemblies.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,20 @@ sidebar:
nav: "sidebar"
---

You also have access to methods to assert an assembly does or does not reference another assembly.
You have access to methods to assert an assembly does or does not reference another assembly.
These are typically used to enforce layers within an application, such as for example, asserting the web layer does not reference the data layer.
To assert the references, use the following syntax:

```csharp
assembly.Should().Reference(otherAssembly);
assembly.Should().NotReference(otherAssembly);
```

Furthermore, you can assert if an assembly is assigned with a specific public key, or that the assembly is not signed at all.
The first can be useful, to ensure that the public key of your package is not changed unintentionally, the latter to prevent unintended signing.
To assert this, use the following syntax:

```csharp
assembly.Should().HavePublicKey("e0851575614491c6d25018fadb75");
assembly.Should().BeUnsigned();
```
1 change: 1 addition & 0 deletions docs/_pages/releases.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ sidebar:

### What's new
* Added `Be`, `NotBe` and `BeOneOf` for object comparisons with custom comparer - [#2111](https://github.com/fluentassertions/fluentassertions/pull/2111)
* Added `BeSignedWithPublicKey()` and `BeUnsigned()` for assertions on `Assembly` - [#2207](https://github.com/fluentassertions/fluentassertions/pull/2207)

### Fixes
* `because` and `becauseArgs` were not included in the error message when collections of enums were not equivalent - [#2214](https://github.com/fluentassertions/fluentassertions/pull/2214)
Expand Down