public async Task Cannot_process_more_operations_than_maximum() { // Arrange var options = (JsonApiOptions)_testContext.Factory.Services.GetRequiredService <IJsonApiOptions>(); options.MaximumOperationsPerRequest = 2; var requestBody = new { atomic__operations = new object[] { new { op = "add", data = new { } }, new { op = "remove", data = new { } }, new { op = "update", data = new { } } } }; const string route = "/operations"; // Act (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync <ErrorDocument>(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request exceeds the maximum number of operations."); error.Detail.Should().Be("The number of operations in this request (3) is higher than 2."); error.Source.Pointer.Should().BeNull(); }
public async Task Can_clear_HasMany_relationship() { // Arrange MusicTrack existingTrack = _fakers.MusicTrack.Generate(); existingTrack.Performers = _fakers.Performer.Generate(2); await _testContext.RunOnDatabaseAsync(async dbContext => { await dbContext.ClearTableAsync <Performer>(); dbContext.MusicTracks.Add(existingTrack); await dbContext.SaveChangesAsync(); }); var requestBody = new { atomic__operations = new[] { new { op = "update", @ref = new { type = "musicTracks", id = existingTrack.StringId, relationship = "performers" }, data = new object[0] } } }; const string route = "/operations"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAtomicAsync <string>(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); await _testContext.RunOnDatabaseAsync(async dbContext => { MusicTrack trackInDatabase = await dbContext.MusicTracks.Include(musicTrack => musicTrack.Performers).FirstWithIdAsync(existingTrack.Id); trackInDatabase.Performers.Should().BeEmpty(); List <Performer> performersInDatabase = await dbContext.Performers.ToListAsync(); performersInDatabase.Should().HaveCount(2); }); }
public async Task Cannot_remove_from_HasOne_relationship() { // Arrange MusicTrack existingTrack = _fakers.MusicTrack.Generate(); existingTrack.OwnedBy = _fakers.RecordCompany.Generate(); await _testContext.RunOnDatabaseAsync(async dbContext => { dbContext.MusicTracks.Add(existingTrack); await dbContext.SaveChangesAsync(); }); var requestBody = new { atomic__operations = new[] { new { op = "remove", @ref = new { type = "musicTracks", id = existingTrack.StringId, relationship = "ownedBy" }, data = new { type = "recordCompanies", id = existingTrack.OwnedBy.StringId } } } }; const string route = "/operations"; // Act (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync <ErrorDocument>(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Only to-many relationships can be targeted in 'remove' operations."); error.Detail.Should().Be("Relationship 'ownedBy' must be a to-many relationship."); }
public async Task Can_create_resource() { // Arrange var newArtistName = _fakers.Performer.Generate().ArtistName; var newBornAt = _fakers.Performer.Generate().BornAt; var requestBody = new { atomic__operations = new[] { new { op = "add", data = new { type = "performers", attributes = new { artistName = newArtistName, bornAt = newBornAt } } } } }; const string route = "/operations"; // Act var(httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync <AtomicOperationsDocument>(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.Results.Should().HaveCount(1); responseDocument.Results[0].SingleData.Should().NotBeNull(); responseDocument.Results[0].SingleData.Type.Should().Be("performers"); responseDocument.Results[0].SingleData.Attributes["artistName"].Should().Be(newArtistName); responseDocument.Results[0].SingleData.Attributes["bornAt"].Should().BeCloseTo(newBornAt); responseDocument.Results[0].SingleData.Relationships.Should().BeNull(); var newPerformerId = int.Parse(responseDocument.Results[0].SingleData.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { var performerInDatabase = await dbContext.Performers.FirstWithIdAsync(newPerformerId); performerInDatabase.ArtistName.Should().Be(newArtistName); performerInDatabase.BornAt.Should().BeCloseTo(newBornAt); }); }
public async Task Can_create_resource_with_client_generated_guid_ID_having_side_effects() { // Arrange TextLanguage newLanguage = _fakers.TextLanguage.Generate(); newLanguage.Id = Guid.NewGuid(); var requestBody = new { atomic__operations = new[] { new { op = "add", data = new { type = "textLanguages", id = newLanguage.StringId, attributes = new { isoCode = newLanguage.IsoCode } } } } }; const string route = "/operations"; // Act (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = await _testContext.ExecutePostAtomicAsync <AtomicOperationsDocument>(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.Results.Should().HaveCount(1); responseDocument.Results[0].SingleData.Should().NotBeNull(); responseDocument.Results[0].SingleData.Type.Should().Be("textLanguages"); responseDocument.Results[0].SingleData.Attributes["isoCode"].Should().Be(newLanguage.IsoCode); responseDocument.Results[0].SingleData.Attributes.Should().NotContainKey("concurrencyToken"); responseDocument.Results[0].SingleData.Relationships.Should().NotBeEmpty(); await _testContext.RunOnDatabaseAsync(async dbContext => { TextLanguage languageInDatabase = await dbContext.TextLanguages.FirstWithIdAsync(newLanguage.Id); languageInDatabase.IsoCode.Should().Be(newLanguage.IsoCode); }); }
public async Task Returns_JsonApi_ContentType_header_with_AtomicOperations_extension() { // Arrange var requestBody = new { atomic__operations = new[] { new { op = "add", data = new { type = "policies", attributes = new { name = "some" } } } } }; var route = "/operations"; // Act var(httpResponse, _) = await _testContext.ExecutePostAtomicAsync <Document>(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); httpResponse.Content.Headers.ContentType.ToString().Should().Be(HeaderConstants.AtomicOperationsMediaType); }
public async Task Can_clear_OneToOne_relationship_from_principal_side() { // Arrange Lyric existingLyric = _fakers.Lyric.Generate(); existingLyric.Track = _fakers.MusicTrack.Generate(); await _testContext.RunOnDatabaseAsync(async dbContext => { await dbContext.ClearTableAsync <MusicTrack>(); dbContext.Lyrics.Add(existingLyric); await dbContext.SaveChangesAsync(); }); var requestBody = new { atomic__operations = new[] { new { op = "update", @ref = new { type = "lyrics", id = existingLyric.StringId, relationship = "track" }, data = (object)null } } }; const string route = "/operations"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAtomicAsync <string>(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); await _testContext.RunOnDatabaseAsync(async dbContext => { Lyric lyricInDatabase = await dbContext.Lyrics.Include(lyric => lyric.Track).FirstWithIdAsync(existingLyric.Id); lyricInDatabase.Track.Should().BeNull(); List <MusicTrack> tracksInDatabase = await dbContext.MusicTracks.ToListAsync(); tracksInDatabase.Should().HaveCount(1); }); }
public async Task Can_create_resources_for_matching_resource_type() { // Arrange var newTitle1 = _fakers.MusicTrack.Generate().Title; var newTitle2 = _fakers.MusicTrack.Generate().Title; var requestBody = new { atomic__operations = new[] { new { op = "add", data = new { type = "musicTracks", attributes = new { title = newTitle1 } } }, new { op = "add", data = new { type = "musicTracks", attributes = new { title = newTitle2 } } } } }; var route = "/operations/musicTracks/create"; // Act var(httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync <AtomicOperationsDocument>(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.Results.Should().HaveCount(2); }
public async Task Returns_resource_meta_in_create_resource_with_side_effects() { // Arrange var hitCounter = _testContext.Factory.Services.GetRequiredService <ResourceDefinitionHitCounter>(); string newTitle1 = _fakers.MusicTrack.Generate().Title; string newTitle2 = _fakers.MusicTrack.Generate().Title; var requestBody = new { atomic__operations = new[] { new { op = "add", data = new { type = "musicTracks", attributes = new { title = newTitle1, releasedAt = 1.January(2018) } } }, new { op = "add", data = new { type = "musicTracks", attributes = new { title = newTitle2, releasedAt = 23.August(1994) } } } } }; const string route = "/operations"; // Act (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = await _testContext.ExecutePostAtomicAsync <AtomicOperationsDocument>(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.Results.Should().HaveCount(2); responseDocument.Results[0].SingleData.Meta.Should().HaveCount(1); responseDocument.Results[0].SingleData.Meta["Copyright"].Should().Be("(C) 2018. All rights reserved."); responseDocument.Results[1].SingleData.Meta.Should().HaveCount(1); responseDocument.Results[1].SingleData.Meta["Copyright"].Should().Be("(C) 1994. All rights reserved."); hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[]
public async Task Cannot_create_resource_with_multiple_violations() { // Arrange var requestBody = new { atomic__operations = new[] { new { op = "add", data = new { type = "musicTracks", attributes = new { lengthInSeconds = -1 } } } } }; const string route = "/operations"; // Act (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync <ErrorDocument>(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(2); Error error1 = responseDocument.Errors[0]; error1.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error1.Title.Should().Be("Input validation failed."); error1.Detail.Should().Be("The Title field is required."); error1.Source.Pointer.Should().Be("/atomic:operations[0]/data/attributes/title"); Error error2 = responseDocument.Errors[1]; error2.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error2.Title.Should().Be("Input validation failed."); error2.Detail.Should().Be("The field LengthInSeconds must be between 1 and 1440."); error2.Source.Pointer.Should().Be("/atomic:operations[0]/data/attributes/lengthInSeconds"); }
public async Task Returns_top_level_meta_in_create_resource_with_side_effects() { // Arrange var requestBody = new { atomic__operations = new[] { new { op = "add", data = new { type = "performers", attributes = new { } } } } }; const string route = "/operations"; // Act (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = await _testContext.ExecutePostAtomicAsync <AtomicOperationsDocument>(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.Meta.Should().HaveCount(3); responseDocument.Meta["license"].Should().Be("MIT"); responseDocument.Meta["projectUrl"].Should().Be("https://github.com/json-api-dotnet/JsonApiDotNetCore/"); string[] versionArray = ((IEnumerable <JToken>)responseDocument.Meta["versions"]).Select(token => token.ToString()).ToArray(); versionArray.Should().HaveCount(4); versionArray.Should().Contain("v4.0.0"); versionArray.Should().Contain("v3.1.0"); versionArray.Should().Contain("v2.5.2"); versionArray.Should().Contain("v1.3.1"); }
public async Task Cannot_use_non_transactional_repository() { // Arrange var requestBody = new { atomic__operations = new object[] { new { op = "add", data = new { type = "performers", attributes = new { } } } } }; const string route = "/operations"; // Act var(httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync <ErrorDocument>(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); var error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Unsupported resource type in atomic:operations request."); error.Detail.Should().Be("Operations on resources of type 'performers' cannot be used because transaction support is unavailable."); error.Source.Pointer.Should().Be("/atomic:operations[0]"); }
public async Task Cannot_include_on_operations_endpoint() { // Arrange var requestBody = new { atomic__operations = new object[] { new { op = "add", data = new { type = "recordCompanies", attributes = new { } } } } }; const string route = "/operations?include=recordCompanies"; // Act (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync <ErrorDocument>(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Usage of one or more query string parameters is not allowed at the requested endpoint."); error.Detail.Should().Be("The parameter 'include' cannot be used at this endpoint."); error.Source.Parameter.Should().Be("include"); }
public async Task Can_delete_existing_resource() { // Arrange Performer existingPerformer = _fakers.Performer.Generate(); await _testContext.RunOnDatabaseAsync(async dbContext => { dbContext.Performers.Add(existingPerformer); await dbContext.SaveChangesAsync(); }); var requestBody = new { atomic__operations = new[] { new { op = "remove", @ref = new { type = "performers", id = existingPerformer.StringId } } } }; const string route = "/operations"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAtomicAsync <string>(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); responseDocument.Should().BeEmpty(); await _testContext.RunOnDatabaseAsync(async dbContext => { Performer performerInDatabase = await dbContext.Performers.FirstWithIdOrDefaultAsync(existingPerformer.Id); performerInDatabase.Should().BeNull(); }); }
public async Task Update_resource_with_side_effects_returns_absolute_links() { // Arrange TextLanguage existingLanguage = _fakers.TextLanguage.Generate(); RecordCompany existingCompany = _fakers.RecordCompany.Generate(); await _testContext.RunOnDatabaseAsync(async dbContext => { dbContext.AddRange(existingLanguage, existingCompany); await dbContext.SaveChangesAsync(); }); var requestBody = new { atomic__operations = new object[] { new { op = "update", data = new { type = "textLanguages", id = existingLanguage.StringId, attributes = new { } } }, new { op = "update", data = new { type = "recordCompanies", id = existingCompany.StringId, attributes = new { } } } } }; const string route = "/operations"; // Act (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = await _testContext.ExecutePostAtomicAsync <AtomicOperationsDocument>(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.Results.Should().HaveCount(2); string languageLink = HostPrefix + "/textLanguages/" + existingLanguage.StringId; ResourceObject singleData1 = responseDocument.Results[0].SingleData; singleData1.Should().NotBeNull(); singleData1.Links.Should().NotBeNull(); singleData1.Links.Self.Should().Be(languageLink); singleData1.Relationships.Should().NotBeEmpty(); singleData1.Relationships["lyrics"].Links.Should().NotBeNull(); singleData1.Relationships["lyrics"].Links.Self.Should().Be(languageLink + "/relationships/lyrics"); singleData1.Relationships["lyrics"].Links.Related.Should().Be(languageLink + "/lyrics"); string companyLink = HostPrefix + "/recordCompanies/" + existingCompany.StringId; ResourceObject singleData2 = responseDocument.Results[1].SingleData; singleData2.Should().NotBeNull(); singleData2.Links.Should().NotBeNull(); singleData2.Links.Self.Should().Be(companyLink); singleData2.Relationships.Should().NotBeEmpty(); singleData2.Relationships["tracks"].Links.Should().NotBeNull(); singleData2.Relationships["tracks"].Links.Self.Should().Be(companyLink + "/relationships/tracks"); singleData2.Relationships["tracks"].Links.Related.Should().Be(companyLink + "/tracks"); }
public async Task Can_create_OneToOne_relationship_from_principal_side() { // Arrange MusicTrack existingTrack = _fakers.MusicTrack.Generate(); await _testContext.RunOnDatabaseAsync(async dbContext => { dbContext.MusicTracks.Add(existingTrack); await dbContext.SaveChangesAsync(); }); var requestBody = new { atomic__operations = new[] { new { op = "add", data = new { type = "lyrics", relationships = new { track = new { data = new { type = "musicTracks", id = existingTrack.StringId } } } } } } }; const string route = "/operations"; // Act (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = await _testContext.ExecutePostAtomicAsync <AtomicOperationsDocument>(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.Results.Should().HaveCount(1); responseDocument.Results[0].SingleData.Should().NotBeNull(); responseDocument.Results[0].SingleData.Type.Should().Be("lyrics"); responseDocument.Results[0].SingleData.Attributes.Should().NotBeEmpty(); responseDocument.Results[0].SingleData.Relationships.Should().NotBeEmpty(); long newLyricId = long.Parse(responseDocument.Results[0].SingleData.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { Lyric lyricInDatabase = await dbContext.Lyrics.Include(lyric => lyric.Track).FirstWithIdAsync(newLyricId); lyricInDatabase.Track.Should().NotBeNull(); lyricInDatabase.Track.Id.Should().Be(existingTrack.Id); }); }
public async Task Update_resource_with_side_effects_returns_absolute_links() { // Arrange var existingLanguage = _fakers.TextLanguage.Generate(); var existingCompany = _fakers.RecordCompany.Generate(); await _testContext.RunOnDatabaseAsync(async dbContext => { dbContext.AddRange(existingLanguage, existingCompany); await dbContext.SaveChangesAsync(); }); var requestBody = new { atomic__operations = new object[] { new { op = "update", data = new { type = "textLanguages", id = existingLanguage.StringId, attributes = new { } } }, new { op = "update", data = new { type = "recordCompanies", id = existingCompany.StringId, attributes = new { } } } } }; var route = "/operations"; // Act var(httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync <AtomicOperationsDocument>(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.Results.Should().HaveCount(2); responseDocument.Results[0].SingleData.Should().NotBeNull(); responseDocument.Results[0].SingleData.Links.Should().NotBeNull(); responseDocument.Results[0].SingleData.Links.Self.Should().Be("http://localhost/textLanguages/" + existingLanguage.StringId); responseDocument.Results[0].SingleData.Relationships.Should().NotBeEmpty(); responseDocument.Results[0].SingleData.Relationships["lyrics"].Links.Should().NotBeNull(); responseDocument.Results[0].SingleData.Relationships["lyrics"].Links.Self.Should().Be($"http://localhost/textLanguages/{existingLanguage.StringId}/relationships/lyrics"); responseDocument.Results[0].SingleData.Relationships["lyrics"].Links.Related.Should().Be($"http://localhost/textLanguages/{existingLanguage.StringId}/lyrics"); responseDocument.Results[1].SingleData.Should().NotBeNull(); responseDocument.Results[1].SingleData.Links.Should().NotBeNull(); responseDocument.Results[1].SingleData.Links.Self.Should().Be("http://localhost/recordCompanies/" + existingCompany.StringId); responseDocument.Results[1].SingleData.Relationships.Should().NotBeEmpty(); responseDocument.Results[1].SingleData.Relationships["tracks"].Links.Should().NotBeNull(); responseDocument.Results[1].SingleData.Relationships["tracks"].Links.Self.Should().Be($"http://localhost/recordCompanies/{existingCompany.StringId}/relationships/tracks"); responseDocument.Results[1].SingleData.Relationships["tracks"].Links.Related.Should().Be($"http://localhost/recordCompanies/{existingCompany.StringId}/tracks"); }
public async Task Create_resource_with_side_effects_returns_relative_links() { // Arrange var requestBody = new { atomic__operations = new object[] { new { op = "add", data = new { type = "textLanguages", attributes = new { } } }, new { op = "add", data = new { type = "recordCompanies", attributes = new { } } } } }; const string route = "/api/operations"; // Act (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = await _testContext.ExecutePostAtomicAsync <AtomicOperationsDocument>(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.Results.Should().HaveCount(2); responseDocument.Results[0].SingleData.Should().NotBeNull(); string languageLink = "/api/textLanguages/" + Guid.Parse(responseDocument.Results[0].SingleData.Id); responseDocument.Results[0].SingleData.Links.Should().NotBeNull(); responseDocument.Results[0].SingleData.Links.Self.Should().Be(languageLink); responseDocument.Results[0].SingleData.Relationships.Should().NotBeEmpty(); responseDocument.Results[0].SingleData.Relationships["lyrics"].Links.Should().NotBeNull(); responseDocument.Results[0].SingleData.Relationships["lyrics"].Links.Self.Should().Be(languageLink + "/relationships/lyrics"); responseDocument.Results[0].SingleData.Relationships["lyrics"].Links.Related.Should().Be(languageLink + "/lyrics"); responseDocument.Results[1].SingleData.Should().NotBeNull(); string companyLink = "/api/recordCompanies/" + short.Parse(responseDocument.Results[1].SingleData.Id); responseDocument.Results[1].SingleData.Links.Should().NotBeNull(); responseDocument.Results[1].SingleData.Links.Self.Should().Be(companyLink); responseDocument.Results[1].SingleData.Relationships.Should().NotBeEmpty(); responseDocument.Results[1].SingleData.Relationships["tracks"].Links.Should().NotBeNull(); responseDocument.Results[1].SingleData.Relationships["tracks"].Links.Self.Should().Be(companyLink + "/relationships/tracks"); responseDocument.Results[1].SingleData.Relationships["tracks"].Links.Related.Should().Be(companyLink + "/tracks"); }
public async Task Can_rollback_on_error() { // Arrange var newArtistName = _fakers.Performer.Generate().ArtistName; var newBornAt = _fakers.Performer.Generate().BornAt; var newTitle = _fakers.MusicTrack.Generate().Title; await _testContext.RunOnDatabaseAsync(async dbContext => { await dbContext.ClearTablesAsync <Performer, MusicTrack>(); }); var requestBody = new { atomic__operations = new object[] { new { op = "add", data = new { type = "performers", attributes = new { artistName = newArtistName, bornAt = newBornAt } } }, new { op = "add", data = new { type = "musicTracks", attributes = new { title = newTitle }, relationships = new { performers = new { data = new[] { new { type = "performers", id = 99999999 } } } } } } } }; var route = "/operations"; // Act var(httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync <ErrorDocument>(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.NotFound); responseDocument.Errors[0].Title.Should().Be("A related resource does not exist."); responseDocument.Errors[0].Detail.Should().Be("Related resource of type 'performers' with ID '99999999' in relationship 'performers' does not exist."); responseDocument.Errors[0].Source.Pointer.Should().Be("/atomic:operations[1]"); await _testContext.RunOnDatabaseAsync(async dbContext => { var performersInDatabase = await dbContext.Performers.ToListAsync(); performersInDatabase.Should().BeEmpty(); var tracksInDatabase = await dbContext.MusicTracks.ToListAsync(); tracksInDatabase.Should().BeEmpty(); }); }
public async Task Includes_version_with_ext_on_operations_endpoint() { // Arrange const int newArtistId = 12345; string newArtistName = _fakers.Performer.Generate().ArtistName; await _testContext.RunOnDatabaseAsync(async dbContext => { await dbContext.ClearTableAsync <Performer>(); }); var requestBody = new { atomic__operations = new[] { new { op = "add", data = new { type = "performers", id = newArtistId, attributes = new { artistName = newArtistName } } } } }; const string route = "/operations"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAtomicAsync <string>(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.Should().BeJson(@"{ ""jsonapi"": { ""version"": ""1.1"", ""ext"": [ ""https://jsonapi.org/ext/atomic"" ] }, ""atomic:results"": [ { ""data"": { ""type"": ""performers"", ""id"": """ + newArtistId + @""", ""attributes"": { ""artistName"": """ + newArtistName + @""", ""bornAt"": ""0001-01-01T01:00:00+01:00"" }, ""links"": { ""self"": ""http://localhost/performers/" + newArtistId + @""" } } } ] }"); }
public async Task Transforms_on_create_resource_with_side_effects() { // Arrange var hitCounter = _testContext.Factory.Services.GetRequiredService <ResourceDefinitionHitCounter>(); List <RecordCompany> newCompanies = _fakers.RecordCompany.Generate(2); await _testContext.RunOnDatabaseAsync(async dbContext => { await dbContext.ClearTableAsync <RecordCompany>(); }); var requestBody = new { atomic__operations = new[] { new { op = "add", data = new { type = "recordCompanies", attributes = new { name = newCompanies[0].Name, countryOfResidence = newCompanies[0].CountryOfResidence } } }, new { op = "add", data = new { type = "recordCompanies", attributes = new { name = newCompanies[1].Name, countryOfResidence = newCompanies[1].CountryOfResidence } } } } }; const string route = "/operations"; // Act (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = await _testContext.ExecutePostAtomicAsync <AtomicOperationsDocument>(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.Results.Should().HaveCount(2); responseDocument.Results[0].SingleData.Attributes["name"].Should().Be(newCompanies[0].Name.ToUpperInvariant()); responseDocument.Results[0].SingleData.Attributes["countryOfResidence"].Should().Be(newCompanies[0].CountryOfResidence.ToUpperInvariant()); responseDocument.Results[1].SingleData.Attributes["name"].Should().Be(newCompanies[1].Name.ToUpperInvariant()); responseDocument.Results[1].SingleData.Attributes["countryOfResidence"].Should().Be(newCompanies[1].CountryOfResidence.ToUpperInvariant()); await _testContext.RunOnDatabaseAsync(async dbContext => { List <RecordCompany> companiesInDatabase = await dbContext.RecordCompanies.ToListAsync(); companiesInDatabase.Should().HaveCount(2); companiesInDatabase[0].Name.Should().Be(newCompanies[0].Name.ToUpperInvariant()); companiesInDatabase[0].CountryOfResidence.Should().Be(newCompanies[0].CountryOfResidence); companiesInDatabase[1].Name.Should().Be(newCompanies[1].Name.ToUpperInvariant()); companiesInDatabase[1].CountryOfResidence.Should().Be(newCompanies[1].CountryOfResidence); }); hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[]
public async Task Hides_text_in_create_resource_with_side_effects() { // Arrange var provider = _testContext.Factory.Services.GetRequiredService <LyricPermissionProvider>(); provider.CanViewText = false; provider.HitCount = 0; var newLyrics = _fakers.Lyric.Generate(2); var requestBody = new { atomic__operations = new[] { new { op = "add", data = new { type = "lyrics", attributes = new { format = newLyrics[0].Format, text = newLyrics[0].Text } } }, new { op = "add", data = new { type = "lyrics", attributes = new { format = newLyrics[1].Format, text = newLyrics[1].Text } } } } }; const string route = "/operations"; // Act var(httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync <AtomicOperationsDocument>(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.Results.Should().HaveCount(2); responseDocument.Results[0].SingleData.Attributes["format"].Should().Be(newLyrics[0].Format); responseDocument.Results[0].SingleData.Attributes.Should().NotContainKey("text"); responseDocument.Results[1].SingleData.Attributes["format"].Should().Be(newLyrics[1].Format); responseDocument.Results[1].SingleData.Attributes.Should().NotContainKey("text"); provider.HitCount.Should().Be(4); }
public async Task Cannot_process_for_missing_request_body() { // Arrange const string route = "/operations"; // Act (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync <ErrorDocument>(route, null); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Missing request body."); error.Detail.Should().BeNull(); error.Source.Pointer.Should().BeNull(); await _testContext.RunOnDatabaseAsync(async dbContext => { List <Performer> performersInDatabase = await dbContext.Performers.ToListAsync(); performersInDatabase.Should().BeEmpty(); List <MusicTrack> tracksInDatabase = await dbContext.MusicTracks.ToListAsync(); tracksInDatabase.Should().BeEmpty(); }); }
public async Task Can_create_HasMany_relationship() { // Arrange List <Performer> existingPerformers = _fakers.Performer.Generate(2); string newTitle = _fakers.MusicTrack.Generate().Title; await _testContext.RunOnDatabaseAsync(async dbContext => { dbContext.Performers.AddRange(existingPerformers); await dbContext.SaveChangesAsync(); }); var requestBody = new { atomic__operations = new[] { new { op = "add", data = new { type = "musicTracks", attributes = new { title = newTitle }, relationships = new { performers = new { data = new[] { new { type = "performers", id = existingPerformers[0].StringId }, new { type = "performers", id = existingPerformers[1].StringId } } } } } } } }; const string route = "/operations"; // Act (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = await _testContext.ExecutePostAtomicAsync <AtomicOperationsDocument>(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.Results.Should().HaveCount(1); responseDocument.Results[0].SingleData.Should().NotBeNull(); responseDocument.Results[0].SingleData.Type.Should().Be("musicTracks"); responseDocument.Results[0].SingleData.Attributes.Should().NotBeEmpty(); responseDocument.Results[0].SingleData.Relationships.Should().NotBeEmpty(); Guid newTrackId = Guid.Parse(responseDocument.Results[0].SingleData.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { MusicTrack trackInDatabase = await dbContext.MusicTracks.Include(musicTrack => musicTrack.Performers).FirstWithIdAsync(newTrackId); trackInDatabase.Performers.Should().HaveCount(2); trackInDatabase.Performers.Should().ContainSingle(performer => performer.Id == existingPerformers[0].Id); trackInDatabase.Performers.Should().ContainSingle(performer => performer.Id == existingPerformers[1].Id); }); }