Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,71 @@ public AnnounceControllerTests()
this._controller.ControllerContext.HttpContext = new DefaultHttpContext();
}

/// <summary>
/// <see cref="AnnounceController.DeleteAsync(string, CancellationToken)"/>
/// returns <see cref="BadRequestObjectResult"/> when no <see cref="Area"/> objects are provided
/// with request
/// </summary>
[TestMethod]
public async Task DeleteAsync_BadRequestObjectWithInvalidMessageId()
{
// Arrange
string invalidId = "this is an invalid ID.";

// Act
ActionResult controllerResponse = await this._controller
.DeleteAsync(invalidId, CancellationToken.None);

// Assert
Assert.IsNotNull(controllerResponse);
Assert.IsInstanceOfType(controllerResponse, typeof(BadRequestObjectResult));
}

/// <summary>
/// <see cref="AnnounceController.DeleteAsync(string, CancellationToken)"/>
/// returns <see cref="NotFoundResult"/> when no <see cref="NarrowcastMessage"/>
/// objects are matched by the provided 'messageId' parameter
/// </summary>
[TestMethod]
public async Task DeleteAsync_NotFoundWithInvalidMessageId()
{
// Arrange
string unmatchedId = "00000000-0000-0000-0000-000000000001";
this._repo
.Setup(r => r.DeleteAsync(It.IsAny<string>(), CancellationToken.None))
.Returns(Task.FromResult(true));

// Act
ActionResult controllerResponse = await this._controller
.DeleteAsync(unmatchedId, CancellationToken.None);

// Assert
Assert.IsNotNull(controllerResponse);
Assert.IsInstanceOfType(controllerResponse, typeof(NotFoundResult));
}

/// <summary>
/// <see cref="AnnounceController.DeleteAsync(string, CancellationToken)"/>
/// returns <see cref="OkResult"/> with valid input data
/// </summary>
[TestMethod]
public async Task DeleteAsync_OkWithValidInputs()
{
// Arrange
string validId = "00000000-0000-0000-0000-000000000001";
this._repo
.Setup(r => r.DeleteAsync(validId, CancellationToken.None))
.Returns(Task.FromResult(true));

// Act
ActionResult controllerResponse = await this._controller
.DeleteAsync(validId, CancellationToken.None);

// Assert
Assert.IsNotNull(controllerResponse);
Assert.IsInstanceOfType(controllerResponse, typeof(OkResult));
}

/// <summary>
/// <see cref="AnnounceController.PutAsync(NarrowcastMessage, CancellationToken)"/>
/// returns <see cref="BadRequestObjectResult"/> when no <see cref="Area"/> objects are provided
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Threading;
using System.Threading.Tasks;
Expand Down Expand Up @@ -42,13 +43,53 @@ public AnnounceController(IMapper map, IMessageService reportService)
this._reportService = reportService;
}

/// <summary>
/// Deletes a <see cref="NarrowcastMessage"/>
/// </summary>
/// <remarks>
/// Sample request:
///
/// DELETE /api/Messages/Announce?messageId=00000000-0000-0000-0000-000000000000&amp;api-version=2020-06-11
///
/// </remarks>
/// <param name="messageId">Unique <see cref="NarrowcastMessage"/> to delete</param>
/// <param name="cancellationToken">Cancellation token (not required in API call)</param>
/// <response code="200">Delete successful</response>
/// <response code="400">Malformed or invalid request</response>
[HttpDelete]
[Consumes("application/x-protobuf", "application/json")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)]
public async Task<ActionResult> DeleteAsync([Required] string messageId, CancellationToken cancellationToken = default)
{
try
{
// Attempt delete
await this._reportService.DeleteMessageByIdAsync(messageId, cancellationToken);
return Ok();
}
catch (KeyNotFoundException)
{
return NotFound();
}
catch (RequestValidationFailedException ex)
{
// Only return validation issues
return BadRequest(ex.ValidationResult);
}
catch (ArgumentNullException)
{
return BadRequest();
}
}

/// <summary>
/// Publish a <see cref="NarrowcastMessage"/> for distribution among devices
/// </summary>
/// <remarks>
/// Sample request:
///
/// PUT /api/Messages/Announce&amp;api-version=2020-06-11
/// PUT /api/Messages/Announce?api-version=2020-06-11
/// {
/// "userMessage": "Monitor symptoms for one week.",
/// "area": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using CovidSafe.DAL.Services;
using CovidSafe.Entities.Geospatial;
using CovidSafe.Entities.Messages;
using CovidSafe.Entities.Validation;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;

Expand Down Expand Up @@ -40,6 +41,87 @@ public MessageServiceTests()
this._service = new MessageService(this._repo.Object);
}

/// <summary>
/// <see cref="MessageService.DeleteMessageByIdAsync(string, CancellationToken)"/>
/// throws <see cref="ArgumentNullException"/> with empty 'id' parameter
/// </summary>
[TestMethod]
[ExpectedException(typeof(ArgumentNullException))]
public async Task DeleteMessageById_ArgumentNullOnEmptyId()
{
// Arrange
// N/A

// Act
await this._service
.DeleteMessageByIdAsync(null, CancellationToken.None);

// Assert
// Exception caught by decorator
}

/// <summary>
/// <see cref="MessageService.DeleteMessageByIdAsync(string, CancellationToken)"/>
/// throws <see cref="KeyNotFoundException"/> with unmatched 'id' parameter
/// </summary>
[TestMethod]
[ExpectedException(typeof(KeyNotFoundException))]
public async Task DeleteMessageById_KeyNotFoundOnUnmatchedId()
{
// Arrange
string unmatchedId = "00000000-0000-0000-0000-000000000001";
this._repo
.Setup(r => r.DeleteAsync(It.IsAny<string>(), CancellationToken.None))
.Returns(Task.FromResult(false));

// Act
await this._service
.DeleteMessageByIdAsync(unmatchedId, CancellationToken.None);

// Assert
// Exception caught by decorator
}

/// <summary>
/// <see cref="MessageService.DeleteMessageByIdAsync(string, CancellationToken)"/>
/// throws <see cref="RequestValidationFailedException"/> with non-GUID 'id' parameter
/// </summary>
[TestMethod]
[ExpectedException(typeof(RequestValidationFailedException))]
public async Task DeleteMessageById_RequestValidationFailedOnInvalidId()
{
// Arrange
string invalidId = "this is not a valid ID";

// Act
await this._service
.DeleteMessageByIdAsync(invalidId, CancellationToken.None);

// Assert
// Exception caught by decorator
}

/// <summary>
/// <see cref="MessageService.DeleteMessageByIdAsync(string, CancellationToken)"/>
/// succeeds with valid 'id' parameter
/// </summary>
[TestMethod]
public async Task DeleteMessageById_SucceedsOnValidId()
{
// Arrange
string validId = "00000000-0000-0000-0000-000000000001";
this._repo
.Setup(r => r.DeleteAsync(validId, CancellationToken.None))
.Returns(Task.FromResult(true));

// Act
await this._service
.DeleteMessageByIdAsync(validId, CancellationToken.None);

// Assert
// No exceptions should be thrown
}

/// <summary>
/// <see cref="MessageService.GetByIdsAsync(IEnumerable{string}, CancellationToken)"/>
/// throws <see cref="ArgumentNullException"/> with empty 'ids' parameter
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
using CovidSafe.Entities.Messages;
using Microsoft.Azure.Cosmos;
using Microsoft.Azure.Cosmos.Linq;
using Microsoft.Azure.Cosmos.Spatial;

namespace CovidSafe.DAL.Repositories.Cosmos
{
Expand All @@ -32,6 +31,44 @@ public CosmosMessageContainerRepository(CosmosContext dbContext) : base(dbContex
);
}

/// <summary>
/// Retrieves a <see cref="MessageContainerRecord"/> object by its unique identifier
/// </summary>
/// <param name="id">Unique <see cref="MessageContainerRecord"/> identifier</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns><see cref="MessageContainerRecord"/> matching provided identifier, or null</returns>
/// <remarks>
/// Some functions require the Cosmos record attributes of a matching document, whereas
/// <see cref="GetAsync(string, CancellationToken)"/> by definition returns the object represented
/// by this repository. Hence, this function was created to retrieve the actual <see cref="CosmosRecord{T}"/>
/// implementation to allow examination of the additional attributes.
/// </remarks>
private async Task<MessageContainerRecord> _getRecordById(string id, CancellationToken cancellationToken)
{
// Create LINQ query
var queryable = this.Container
.GetItemLinqQueryable<MessageContainerRecord>();

// Execute query
var iterator = queryable
.Where(r =>
r.Id == id
&& r.Version == MessageContainerRecord.CURRENT_RECORD_VERSION
)
.ToFeedIterator();

List<MessageContainerRecord> results = new List<MessageContainerRecord>();

while (iterator.HasMoreResults)
{
results.AddRange(await iterator.ReadNextAsync());
}

// There should only ever be one result
// This is a semantic of how SELECT works in the LINQ/CosmosDB SDK
return results.FirstOrDefault();
}

/// <summary>
/// Returns the most restrictive timestamp filter, based on the application
/// configuration and the one provided by the user
Expand Down Expand Up @@ -59,29 +96,52 @@ private long _getTimestampFilter(long timestampFilter)
}

/// <inheritdoc/>
public async Task<MessageContainer> GetAsync(string messageId, CancellationToken cancellationToken = default)
public async Task<bool> DeleteAsync(string id, CancellationToken cancellationToken = default)
{
// Create LINQ query
var queryable = this.Container
.GetItemLinqQueryable<MessageContainerRecord>();
if(String.IsNullOrEmpty(id))
{
throw new ArgumentNullException(nameof(id));
}

// Execute query
var iterator = queryable
.Where(r =>
r.Id == messageId
&& r.Version == MessageContainerRecord.CURRENT_RECORD_VERSION
)
.Select(r => r.Value)
.ToFeedIterator();
// Retrieve the record to be deleted
// Necessary to get PartitionKey
MessageContainerRecord toDelete = await this._getRecordById(id, cancellationToken);

List<MessageContainer> results = new List<MessageContainer>();
if(toDelete != null)
{
await this.Container.DeleteItemAsync<MessageContainerRecord>(
id,
new PartitionKey(toDelete.PartitionKey),
cancellationToken: cancellationToken
);

while (iterator.HasMoreResults)
return true;
}
else
{
results.AddRange(await iterator.ReadNextAsync());
return false;
}
}

return results.FirstOrDefault();
/// <inheritdoc/>
public async Task<MessageContainer> GetAsync(string messageId, CancellationToken cancellationToken = default)
{
if(String.IsNullOrEmpty(messageId))
{
throw new ArgumentNullException(nameof(messageId));
}

// Get result from private function
MessageContainerRecord record = await this._getRecordById(messageId, cancellationToken);

if (record != null)
{
return record.Value;
}
else
{
return null;
}
}

/// <inheritdoc/>
Expand Down
7 changes: 7 additions & 0 deletions CovidSafe/CovidSafe.DAL/Repositories/IRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@ namespace CovidSafe.DAL.Repositories
/// <typeparam name="TT">Type used by the primary key</typeparam>
public interface IRepository<T, TT>
{
/// <summary>
/// Deletes an object matching the provided identifier
/// </summary>
/// <param name="id">Unique object identifier</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>True if record was found and deleted, false if no matching record was found</returns>
Task<bool> DeleteAsync(TT id, CancellationToken cancellationToken = default);
/// <summary>
/// Retrieves an object which matches the provided identifier
/// </summary>
Expand Down
6 changes: 6 additions & 0 deletions CovidSafe/CovidSafe.DAL/Services/IMessageService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ namespace CovidSafe.DAL.Services
/// </summary>
public interface IMessageService : IService
{
/// <summary>
/// Deletes a <see cref="MessageContainer"/> based on its unique identifier
/// </summary>
/// <param name="id">Identifier of <see cref="MessageContainer"/> to delete</param>
/// <param name="cancellationToken">Cancellation token</param>
Task DeleteMessageByIdAsync(string id, CancellationToken cancellationToken = default);
/// <summary>
/// Retrieves a collection of <see cref="MessageContainer"/> objects by their unique identifiers
/// </summary>
Expand Down
Loading