Skip to content

Commit 1cc2278

Browse files
author
Bart Koelman
committed
Added tests for obfuscated IDs
1 parent 2c6197b commit 1cc2278

File tree

20 files changed

+779
-211
lines changed

20 files changed

+779
-211
lines changed
Lines changed: 2 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,16 @@
1-
using System.Threading.Tasks;
21
using JsonApiDotNetCore.Configuration;
32
using JsonApiDotNetCore.Controllers;
43
using JsonApiDotNetCore.Services;
54
using JsonApiDotNetCoreExample.Models;
6-
using Microsoft.AspNetCore.Mvc;
75
using Microsoft.Extensions.Logging;
86

97
namespace JsonApiDotNetCoreExample.Controllers
108
{
11-
public sealed class PassportsController : BaseJsonApiController<Passport>
9+
public sealed class PassportsController : JsonApiController<Passport>
1210
{
13-
public PassportsController(
14-
IJsonApiOptions options,
15-
ILoggerFactory loggerFactory,
16-
IResourceService<Passport, int> resourceService)
11+
public PassportsController(IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService<Passport, int> resourceService)
1712
: base(options, loggerFactory, resourceService)
18-
{ }
19-
20-
[HttpGet]
21-
public override async Task<IActionResult> GetAsync() => await base.GetAsync();
22-
23-
[HttpGet("{id}")]
24-
public async Task<IActionResult> GetAsync(string id)
25-
{
26-
int idValue = HexadecimalObfuscationCodec.Decode(id);
27-
return await base.GetAsync(idValue);
28-
}
29-
30-
[HttpPatch("{id}")]
31-
public async Task<IActionResult> PatchAsync(string id, [FromBody] Passport resource)
32-
{
33-
int idValue = HexadecimalObfuscationCodec.Decode(id);
34-
return await base.PatchAsync(idValue, resource);
35-
}
36-
37-
[HttpPost]
38-
public override async Task<IActionResult> PostAsync([FromBody] Passport resource)
39-
{
40-
return await base.PostAsync(resource);
41-
}
42-
43-
[HttpDelete("{id}")]
44-
public async Task<IActionResult> DeleteAsync(string id)
4513
{
46-
int idValue = HexadecimalObfuscationCodec.Decode(id);
47-
return await base.DeleteAsync(idValue);
4814
}
4915
}
5016
}

src/Examples/JsonApiDotNetCoreExample/Models/Passport.cs

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,6 @@ public class Passport : Identifiable
1414
private readonly ISystemClock _systemClock;
1515
private int? _socialSecurityNumber;
1616

17-
protected override string GetStringId(int value)
18-
{
19-
return HexadecimalObfuscationCodec.Encode(value);
20-
}
21-
22-
protected override int GetTypedId(string value)
23-
{
24-
return HexadecimalObfuscationCodec.Decode(value);
25-
}
26-
2717
[Attr]
2818
public int? SocialSecurityNumber
2919
{

src/JsonApiDotNetCore/Controllers/BaseJsonApiController.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ public virtual async Task<IActionResult> PostAsync([FromBody] TResource resource
160160
if (_create == null)
161161
throw new RequestMethodNotAllowedException(HttpMethod.Post);
162162

163-
if (!_options.AllowClientGeneratedIds && !string.IsNullOrEmpty(resource.StringId))
163+
if (!_options.AllowClientGeneratedIds && resource.StringId != null)
164164
throw new ResourceIdInPostRequestNotAllowedException();
165165

166166
if (_options.ValidateModelState && !ModelState.IsValid)

src/JsonApiDotNetCore/Resources/Identifiable.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ protected virtual string GetStringId(TId value)
3737
/// </summary>
3838
protected virtual TId GetTypedId(string value)
3939
{
40-
return string.IsNullOrEmpty(value) ? default : (TId)TypeHelper.ConvertType(value, typeof(TId));
40+
return value == null ? default : (TId)TypeHelper.ConvertType(value, typeof(TId));
4141
}
4242
}
4343
}

src/JsonApiDotNetCore/Serialization/Building/ResourceObjectBuilder.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ public virtual ResourceObject Build(IIdentifiable resource, IReadOnlyCollection<
2929
var resourceContext = ResourceContextProvider.GetResourceContext(resource.GetType());
3030

3131
// populating the top-level "type" and "id" members.
32-
var resourceObject = new ResourceObject { Type = resourceContext.PublicName, Id = resource.StringId == string.Empty ? null : resource.StringId };
32+
var resourceObject = new ResourceObject { Type = resourceContext.PublicName, Id = resource.StringId };
3333

3434
// populating the top-level "attribute" member of a resource object. never include "id" as an attribute
3535
if (attributes != null && (attributes = attributes.Where(attr => attr.Property.Name != nameof(Identifiable.Id)).ToArray()).Any())

src/JsonApiDotNetCore/Serialization/JsonApiReader.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ private void ValidatePrimaryIdValue(object model, PathString requestPath)
180180
/// </summary>
181181
private bool HasMissingId(object model)
182182
{
183-
return TryGetId(model, out string id) && string.IsNullOrEmpty(id);
183+
return TryGetId(model, out string id) && id == null;
184184
}
185185

186186
/// <summary>
@@ -190,7 +190,7 @@ private bool HasMissingId(IEnumerable models)
190190
{
191191
foreach (var model in models)
192192
{
193-
if (TryGetId(model, out string id) && string.IsNullOrEmpty(id))
193+
if (TryGetId(model, out string id) && id == null)
194194
{
195195
return true;
196196
}

test/JsonApiDotNetCoreExampleTests/Acceptance/InjectableResourceTests.cs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -200,9 +200,8 @@ public async Task Can_Get_Passports_With_Sparse_Fieldset()
200200
public async Task Fail_When_Deleting_Missing_Passport()
201201
{
202202
// Arrange
203-
string passportId = HexadecimalObfuscationCodec.Encode(1234567890);
204203

205-
var request = new HttpRequestMessage(HttpMethod.Delete, "/api/v1/passports/" + passportId);
204+
var request = new HttpRequestMessage(HttpMethod.Delete, "/api/v1/passports/1234567890");
206205

207206
// Act
208207
var response = await _fixture.Client.SendAsync(request);
@@ -215,7 +214,7 @@ public async Task Fail_When_Deleting_Missing_Passport()
215214
Assert.Single(errorDocument.Errors);
216215
Assert.Equal(HttpStatusCode.NotFound, errorDocument.Errors[0].StatusCode);
217216
Assert.Equal("The requested resource does not exist.", errorDocument.Errors[0].Title);
218-
Assert.Equal("Resource of type 'passports' with ID '" + passportId + "' does not exist.", errorDocument.Errors[0].Detail);
217+
Assert.Equal("Resource of type 'passports' with ID '1234567890' does not exist.", errorDocument.Errors[0].Detail);
219218
}
220219
}
221220
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
using System;
2+
using System.Diagnostics;
3+
using System.Linq;
4+
using System.Reflection;
5+
using Xunit;
6+
7+
namespace JsonApiDotNetCoreExampleTests.IntegrationTests
8+
{
9+
internal abstract class FakerContainer
10+
{
11+
protected static int GetFakerSeed()
12+
{
13+
// The goal here is to have stable data over multiple test runs, but at the same time different data per test case.
14+
15+
MethodBase testMethod = GetTestMethod();
16+
var testName = testMethod.DeclaringType?.FullName + "." + testMethod.Name;
17+
18+
return GetDeterministicHashCode(testName);
19+
}
20+
21+
private static MethodBase GetTestMethod()
22+
{
23+
var stackTrace = new StackTrace();
24+
25+
var testMethod = stackTrace.GetFrames()
26+
.Select(stackFrame => stackFrame?.GetMethod())
27+
.FirstOrDefault(IsTestMethod);
28+
29+
if (testMethod == null)
30+
{
31+
// If called after the first await statement, the test method is no longer on the stack,
32+
// but has been replaced with the compiler-generated async/wait state machine.
33+
throw new InvalidOperationException("Fakers can only be used from within (the start of) a test method.");
34+
}
35+
36+
return testMethod;
37+
}
38+
39+
private static bool IsTestMethod(MethodBase method)
40+
{
41+
if (method == null)
42+
{
43+
return false;
44+
}
45+
46+
return method.GetCustomAttribute(typeof(FactAttribute)) != null || method.GetCustomAttribute(typeof(TheoryAttribute)) != null;
47+
}
48+
49+
private static int GetDeterministicHashCode(string source)
50+
{
51+
// https://andrewlock.net/why-is-string-gethashcode-different-each-time-i-run-my-program-in-net-core/
52+
unchecked
53+
{
54+
int hash1 = (5381 << 16) + 5381;
55+
int hash2 = hash1;
56+
57+
for (int i = 0; i < source.Length; i += 2)
58+
{
59+
hash1 = ((hash1 << 5) + hash1) ^ source[i];
60+
61+
if (i == source.Length - 1)
62+
{
63+
break;
64+
}
65+
66+
hash2 = ((hash2 << 5) + hash2) ^ source[i + 1];
67+
}
68+
69+
return hash1 + hash2 * 1566083941;
70+
}
71+
}
72+
}
73+
}

test/JsonApiDotNetCoreExampleTests/IntegrationTests/Filtering/FilterTests.cs

Lines changed: 0 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
using System.Collections.Generic;
21
using System.Net;
32
using System.Threading.Tasks;
43
using FluentAssertions;
@@ -110,87 +109,5 @@ await _testContext.RunOnDatabaseAsync(async dbContext =>
110109
responseDocument.ManyData[0].Id.Should().Be(person.StringId);
111110
responseDocument.ManyData[0].Attributes["firstName"].Should().Be(person.FirstName);
112111
}
113-
114-
[Fact]
115-
public async Task Can_filter_on_obfuscated_ID()
116-
{
117-
// Arrange
118-
Passport passport = null;
119-
120-
await _testContext.RunOnDatabaseAsync(async dbContext =>
121-
{
122-
passport = new Passport(dbContext)
123-
{
124-
SocialSecurityNumber = 123,
125-
BirthCountry = new Country()
126-
};
127-
128-
await dbContext.ClearTableAsync<Passport>();
129-
dbContext.Passports.AddRange(passport, new Passport(dbContext));
130-
131-
await dbContext.SaveChangesAsync();
132-
});
133-
134-
var route = $"/api/v1/passports?filter=equals(id,'{passport.StringId}')";
135-
136-
// Act
137-
var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync<Document>(route);
138-
139-
// Assert
140-
httpResponse.Should().HaveStatusCode(HttpStatusCode.OK);
141-
142-
responseDocument.ManyData.Should().HaveCount(1);
143-
responseDocument.ManyData[0].Id.Should().Be(passport.StringId);
144-
responseDocument.ManyData[0].Attributes["socialSecurityNumber"].Should().Be(passport.SocialSecurityNumber);
145-
}
146-
147-
[Fact]
148-
public async Task Can_filter_in_set_on_obfuscated_ID()
149-
{
150-
// Arrange
151-
var passports = new List<Passport>();
152-
153-
await _testContext.RunOnDatabaseAsync(async dbContext =>
154-
{
155-
passports.AddRange(new[]
156-
{
157-
new Passport(dbContext)
158-
{
159-
SocialSecurityNumber = 123,
160-
BirthCountry = new Country()
161-
},
162-
new Passport(dbContext)
163-
{
164-
SocialSecurityNumber = 456,
165-
BirthCountry = new Country()
166-
},
167-
new Passport(dbContext)
168-
{
169-
BirthCountry = new Country()
170-
}
171-
});
172-
173-
await dbContext.ClearTableAsync<Passport>();
174-
dbContext.Passports.AddRange(passports);
175-
176-
await dbContext.SaveChangesAsync();
177-
});
178-
179-
var route = $"/api/v1/passports?filter=any(id,'{passports[0].StringId}','{passports[1].StringId}')";
180-
181-
// Act
182-
var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync<Document>(route);
183-
184-
// Assert
185-
httpResponse.Should().HaveStatusCode(HttpStatusCode.OK);
186-
187-
responseDocument.ManyData.Should().HaveCount(2);
188-
189-
responseDocument.ManyData[0].Id.Should().Be(passports[0].StringId);
190-
responseDocument.ManyData[0].Attributes["socialSecurityNumber"].Should().Be(passports[0].SocialSecurityNumber);
191-
192-
responseDocument.ManyData[1].Id.Should().Be(passports[1].StringId);
193-
responseDocument.ManyData[1].Attributes["socialSecurityNumber"].Should().Be(passports[1].SocialSecurityNumber);
194-
}
195112
}
196113
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
using System.Collections.Generic;
2+
using JsonApiDotNetCore.Resources.Annotations;
3+
4+
namespace JsonApiDotNetCoreExampleTests.IntegrationTests.IdObfuscation
5+
{
6+
public sealed class BankAccount : ObfuscatedIdentifiable
7+
{
8+
[Attr]
9+
public string Iban { get; set; }
10+
11+
[HasMany]
12+
public IList<DebitCard> Cards { get; set; }
13+
}
14+
}

0 commit comments

Comments
 (0)