Skip to content

Commit

Permalink
✨ adding matchers for HttpResponse cookies
Browse files Browse the repository at this point in the history
  • Loading branch information
fluffynuts committed Apr 4, 2024
1 parent 22b3d90 commit 7f0a6d6
Show file tree
Hide file tree
Showing 6 changed files with 154 additions and 209 deletions.
135 changes: 104 additions & 31 deletions src/NExpect.Matchers.AspNetCore.Tests/TestHttpResponseMatchers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,64 @@ public void ShouldTestCookieSecureHttpOnlyDomain()
// Assert
}

// [Test]
// public void ShouldBeAbleToAssertCookieNeverExpires()
// {
// // Arrange
// var eternalKey = GetRandomString();
// var eternalValue = GetRandomString();
// var expiringKey = GetAnother(eternalKey);
// var expiringValue = GetRandomString();
// var res = HttpResponseBuilder.Create()
// .WithCookie(eternalKey, eternalValue)
// .WithCookie(
// expiringKey,
// expiringValue,
// new CookieOptions()
// {
// Expires = DateTimeOffset.Now.AddHours(1)
// }
// )
// .Build();
// var ints = new[] { 1, 2, 3 };
// Expect(ints)
// .To.Contain.Only(3).Items()
// .And
// .To.Contain.All
// // Act
// Assert.That(
// () =>
// {
// Expect(res)
// .To.Have.Cookie(eternalKey)
// .With.Value(eternalValue)
// .Which.Does.Not.Expire();
// },
// Throws.Nothing
// );
// Assert.That(
// () =>
// {
// Expect(res)
// .To.Have.Cookie(eternalKey)
// .With.Value(eternalValue)
// .Which.Expires();
// },
// Throws.Exception.InstanceOf<UnmetExpectationException>()
// );
// Assert.That(
// () =>
// {
// Expect(res)
// .To.Have.Cookie(expiringKey)
// .With.Value(expiringValue)
// .Which.Expires();
// },
// Throws.Nothing
// );
// // Assert
// }

[TestFixture]
public class Issues
{
Expand Down Expand Up @@ -235,39 +293,54 @@ public void ShouldCorrectlyParsePathAndSameSite()
{
// Arrange
var res = HttpResponseBuilder.Create()
.WithHeader("Set-Cookie", "le_cookie=le_value; Path=/; SameSite=le_same_site; Secure; HttpOnly")
.WithHeader("Set-Cookie", "le_cookie=le_value; Path=/; SameSite=Lax; Secure; HttpOnly")
.Build();
// Act
Assert.That(() =>
{
Expect(res)
.To.Have.Cookie("le_cookie")
.With.Path("/");
}, Throws.Nothing);
Assert.That(() =>
{
Expect(res)
.To.Have.Cookie("le_cookie")
.With.Value("le_value");
}, Throws.Nothing);
Assert.That(() =>
{
Expect(res)
.To.Have.Cookie("le_cookie")
.With.SameSite("le_same_site");
}, Throws.Nothing);
Assert.That(() =>
{
Expect(res)
.To.Have.Cookie("le_cookie")
.Which.Is.HttpOnly();
}, Throws.Nothing);
Assert.That(() =>
{
Expect(res)
.To.Have.Cookie("le_cookie")
.Which.Is.Secure();
}, Throws.Nothing);
Assert.That(
() =>
{
Expect(res)
.To.Have.Cookie("le_cookie")
.With.Path("/");
},
Throws.Nothing
);
Assert.That(
() =>
{
Expect(res)
.To.Have.Cookie("le_cookie")
.With.Value("le_value");
},
Throws.Nothing
);
Assert.That(
() =>
{
Expect(res)
.To.Have.Cookie("le_cookie")
.With.SameSite(SameSiteMode.Lax);
},
Throws.Nothing
);
Assert.That(
() =>
{
Expect(res)
.To.Have.Cookie("le_cookie")
.Which.Is.HttpOnly();
},
Throws.Nothing
);
Assert.That(
() =>
{
Expect(res)
.To.Have.Cookie("le_cookie")
.Which.Is.Secure();
},
Throws.Nothing
);
// Assert
}
}
Expand Down
153 changes: 9 additions & 144 deletions src/NExpect.Matchers.AspNetCore/HttpResponseMatchers.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Web;
using Imported.PeanutButter.Utils;
using Microsoft.AspNetCore.Http;
using NExpect.Implementations;
using NExpect.Interfaces;
using NExpect.MatcherLogic;
using Imported.PeanutButter.TestUtils.AspNetCore;
using Imported.PeanutButter.Utils;

namespace NExpect
{
Expand Down Expand Up @@ -65,20 +64,16 @@ Func<string> customMessageGenerator
Cookie resolvedCookie = null;
have.AddMatcher(actual =>
{
var cookies = actual.Headers.Where(
h => h.Key == "Set-Cookie"
).Select(h => h.Value
.Select(ParseCookieHeader)
.SelectMany(o => o)
)
.SelectMany(o => o)
.ToArray();
resolvedCookie = cookies.FirstOrDefault(c => c.Name.Equals(name));
var passed = resolvedCookie != null;
var cookies = actual.Headers.ParseCookies();
var encodedName = name;
resolvedCookie = cookies.FirstOrDefault(c => c.Name.Equals(encodedName));
resolvedCookie?.SetMetadata("response", actual);

var passed = resolvedCookie is not null;
return new MatcherResult(
passed,
MessageHelpers.FinalMessageFor(
() => $"Expected {passed.AsNot()}to find set-cookie header for '{name}'",
() => $"Expected {passed.AsNot()}to find set-cookie header for '{encodedName}'",
customMessageGenerator
)
);
Expand All @@ -89,136 +84,6 @@ have as IExpectationContext
);
}

private static IEnumerable<Cookie> ParseCookieHeader(
string header
)
{
var headerParts = header.Split(',').Select(p => p.Trim());
foreach (var cookiePart in headerParts)
{
var parts = cookiePart
.Split(';')
.Trim();
yield return parts.Aggregate(
new Cookie(),
(acc, cur) =>
{
var subs = cur.Split('=');
var key = subs[0].Trim();
var value = string.Join("=", subs.Skip(1));
if (string.IsNullOrWhiteSpace(acc.Name))
{
acc.Name = HttpUtility.UrlDecode(key);
acc.Value = HttpUtility.UrlDecode(value);
}
else
{
if (CookieMutations.TryGetValue(key, out var modifier))
{
modifier(acc, value);
}
}

return acc;
}
);
}
}

private static readonly Dictionary<string, Action<Cookie, string>>
CookieMutations = new Dictionary<string, Action<Cookie, string>>(
StringComparer.InvariantCultureIgnoreCase
)
{
["Expires"] = SetCookieExpiration,
["Max-Age"] = SetCookieMaxAge,
["Domain"] = SetCookieDomain,
["Secure"] = SetCookieSecure,
["HttpOnly"] = SetCookieHttpOnly,
["SameSite"] = SetCookieSameSite,
["Path"] = SetCookiePath
};

private static void SetCookiePath(
Cookie cookie,
string value
)
{
cookie.Path = value;
}

private static void SetCookieSameSite(
Cookie cookie,
string value
)
{
// Cookie object doesn't natively support the SameSite property, yet
cookie.SetMetadata("SameSite", value);
}

private static void SetCookieHttpOnly(
Cookie cookie,
string value
)
{
cookie.HttpOnly = true;
}

private static void SetCookieSecure(
Cookie cookie,
string value
)
{
cookie.Secure = true;
}

private static void SetCookieDomain(
Cookie cookie,
string value
)
{
cookie.Domain = value;
}

private static void SetCookieMaxAge(
Cookie cookie,
string value
)
{
if (!int.TryParse(value, out var seconds))
{
throw new ArgumentException(
$"Unable to parse '{value}' as an integer value"
);
}

cookie.Expires = DateTime.Now.AddSeconds(seconds);
cookie.Expired = seconds < 1;
cookie.SetMetadata("MaxAge", seconds);
}

private static void SetCookieExpiration(
Cookie cookie,
string value
)
{
if (cookie.TryGetMetadata<int>("MaxAge", out _))
{
// Max-Age takes precedence over Expires
return;
}

if (!DateTime.TryParse(value, out var expires))
{
throw new ArgumentException(
$"Unable to parse '{value}' as a date-time value"
);
}

cookie.Expires = expires;
cookie.Expired = expires <= DateTime.Now;
}

private static string Name(this Cookie cookie)
{
return cookie?.Name ?? "unknown cookie";
Expand Down
Loading

0 comments on commit 7f0a6d6

Please sign in to comment.