Skip to content

Commit

Permalink
Extend assembly assertions with BeSignedWithPublicKey and `BeUnsign…
Browse files Browse the repository at this point in the history
…ed` (#2207)
  • Loading branch information
Corniel committed Aug 16, 2023
1 parent 818e7dc commit 329060d
Show file tree
Hide file tree
Showing 13 changed files with 245 additions and 8 deletions.
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)
{
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);
}

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>();
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

0 comments on commit 329060d

Please sign in to comment.