├── CovidSafe ├── CovidSafe.Entities │ ├── README.md │ ├── Validation │ │ ├── RequestValidationIssue.cs │ │ ├── IValidatable.cs │ │ ├── RequestValidationProperty.cs │ │ ├── RequestValidationFailedException.cs │ │ ├── RequestValidationFailure.cs │ │ └── RequestValidationResult.cs │ ├── Geospatial │ │ ├── RegionComparer.cs │ │ ├── RegionBoundary.cs │ │ ├── Coordinates.cs │ │ ├── NarrowcastArea.cs │ │ └── Region.cs │ ├── Messages │ │ ├── MessageContainerMetadata.cs │ │ ├── BluetoothSeedMessage.cs │ │ ├── NarrowcastMessage.cs │ │ └── MessageContainer.cs │ └── CovidSafe.Entities.csproj ├── CovidSafe.API │ ├── appsettings.Development.json │ ├── .config │ │ └── dotnet-tools.json │ ├── appsettings.json │ ├── Properties │ │ └── launchSettings.json │ ├── CovidSafe.API.csproj │ ├── Program.cs │ ├── Swagger │ │ └── SwaggerConfigureOptions.cs │ ├── v20200611 │ │ ├── Protos │ │ │ └── Interactions.proto │ │ ├── Controllers │ │ │ ├── MessageControllers │ │ │ │ ├── AnnounceController.cs │ │ │ │ └── ListController.cs │ │ │ └── MessageController.cs │ │ └── MappingProfiles.cs │ ├── v20200505 │ │ ├── Protos │ │ │ └── Interactions.proto │ │ ├── Controllers │ │ │ ├── MessageControllers │ │ │ │ ├── AreaReportController.cs │ │ │ │ ├── SeedReportController.cs │ │ │ │ └── ListController.cs │ │ │ └── MessageController.cs │ │ └── MappingProfiles.cs │ ├── v20200415 │ │ ├── Protos │ │ │ └── Interactions.proto │ │ ├── Controllers │ │ │ ├── MessageControllers │ │ │ │ ├── AreaReportController.cs │ │ │ │ ├── SeedReportController.cs │ │ │ │ └── ListController.cs │ │ │ └── MessageController.cs │ │ └── MappingProfiles.cs │ └── Startup.cs ├── .runsettings ├── CovidSafe.DAL │ ├── Services │ │ ├── IService.cs │ │ └── IMessageService.cs │ ├── CovidSafe.DAL.csproj │ ├── Repositories │ │ ├── IRepository.cs │ │ ├── Cosmos │ │ │ ├── Client │ │ │ │ ├── CosmosSchemaConfigurationSection.cs │ │ │ │ ├── CosmosRepository.cs │ │ │ │ ├── CosmosConnectionFactory.cs │ │ │ │ └── CosmosContext.cs │ │ │ └── Records │ │ │ │ ├── CosmosRecord.cs │ │ │ │ └── MessageContainerRecord.cs │ │ └── IMessageContainerRepository.cs │ └── Helpers │ │ ├── PayloadSizeHelper.cs │ │ ├── GeoHelper.cs │ │ └── PrecisionHelper.cs ├── Local.testsettings ├── CovidSafe.Entities.Tests │ └── CovidSafe.Entities.Tests.csproj ├── CovidSafe.API.Tests │ ├── v20200611 │ │ ├── MappingProfilesTests.cs │ │ └── Controllers │ │ │ └── MessageControllers │ │ │ └── AnnounceControllerTests.cs │ ├── v20200415 │ │ ├── MappingProfilesTests.cs │ │ └── Controllers │ │ │ └── MessageControllers │ │ │ ├── AreaReportControllerTests.cs │ │ │ └── SeedReportControllerTests.cs │ ├── v20200505 │ │ ├── MappingProfilesTests.cs │ │ └── Controllers │ │ │ └── MessageControllers │ │ │ ├── AreaReportControllerTests.cs │ │ │ └── SeedReportControllerTests.cs │ └── CovidSafe.API.Tests.csproj ├── CovidSafe.DAL.Tests │ ├── CovidSafe.DAL.Tests.csproj │ └── Helpers │ │ └── PrecisionHelperTests.cs ├── CovidSafe.API.Tests.Performance │ ├── Properties │ │ └── AssemblyInfo.cs │ ├── CovidSafe.API.Tests.Performance.csproj │ └── WebTest.webtest └── CovidSafe.sln ├── LICENSE.md └── README.md /CovidSafe/CovidSafe.Entities/README.md: -------------------------------------------------------------------------------- 1 | ## ATTENTION! 2 | 3 | Use protobuf-net for proto compilation, which has better support for .NET. 4 | 5 | https://github.com/protobuf-net/protobuf-net -------------------------------------------------------------------------------- /CovidSafe/CovidSafe.API/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Debug", 5 | "System": "Information", 6 | "Microsoft": "Information" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /CovidSafe/.runsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 6 | MethodLevel 7 | 8 | 9 | -------------------------------------------------------------------------------- /CovidSafe/CovidSafe.DAL/Services/IService.cs: -------------------------------------------------------------------------------- 1 | namespace CovidSafe.DAL.Services 2 | { 3 | /// 4 | /// Base service layer definition 5 | /// 6 | public interface IService 7 | { 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /CovidSafe/CovidSafe.API/.config/dotnet-tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "isRoot": true, 4 | "tools": { 5 | "dotnet-ef": { 6 | "version": "3.1.3", 7 | "commands": [ 8 | "dotnet-ef" 9 | ] 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /CovidSafe/CovidSafe.Entities/Validation/RequestValidationIssue.cs: -------------------------------------------------------------------------------- 1 | namespace CovidSafe.Entities.Validation 2 | { 3 | /// 4 | /// issue type enumeration 5 | /// 6 | public enum RequestValidationIssue 7 | { 8 | InputEmpty, 9 | InputInvalid, 10 | InputNull 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /CovidSafe/CovidSafe.Entities/Validation/IValidatable.cs: -------------------------------------------------------------------------------- 1 | namespace CovidSafe.Entities.Validation 2 | { 3 | /// 4 | /// Object is validatable contract 5 | /// 6 | public interface IValidatable 7 | { 8 | /// 9 | /// Determines if the current object is valid 10 | /// 11 | /// summary 12 | RequestValidationResult Validate(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /CovidSafe/CovidSafe.API/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "AllowedHosts": "*", 3 | "ApplicationInsights": { 4 | "InstrumentationKey": "" 5 | }, 6 | "ConnectionStrings": { 7 | "CosmosConnection": "" 8 | }, 9 | "CosmosSchema": { 10 | "DatabaseName": "traces", 11 | "MaxDataAgeToReturnDays": 14, 12 | "MessageContainerName": "messages_api_v3" 13 | }, 14 | "DefaultApiVersion": "2020-06-11", 15 | "KeyVaultUrl": "", 16 | "Logging": { 17 | "LogLevel": { 18 | "Default": "Warning" 19 | } 20 | }, 21 | "SwaggerHosts": "https://localhost:44347/" 22 | } -------------------------------------------------------------------------------- /CovidSafe/Local.testsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | These are default test settings for a local test run. 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /CovidSafe/CovidSafe.Entities/Validation/RequestValidationProperty.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | 3 | namespace CovidSafe.Entities.Validation 4 | { 5 | /// 6 | /// Request validation property name constants 7 | /// 8 | [ExcludeFromCodeCoverage] 9 | public static class RequestValidationProperty 10 | { 11 | /// 12 | /// String reference used when multiple properties cause a 13 | /// 14 | /// 15 | public const string Multiple = "multiple"; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /CovidSafe/CovidSafe.DAL/CovidSafe.DAL.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | true 5 | $(NoWarn);1591 6 | netstandard2.0 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /CovidSafe/CovidSafe.Entities.Tests/CovidSafe.Entities.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.1 5 | 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /CovidSafe/CovidSafe.Entities/Geospatial/RegionComparer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace CovidSafe.Entities.Geospatial 5 | { 6 | /// 7 | /// Compares two objects 8 | /// 9 | public class RegionComparer : IEqualityComparer 10 | { 11 | /// 12 | public bool Equals(Region x, Region y) 13 | { 14 | return x.LatitudePrefix == y.LatitudePrefix 15 | && x.LongitudePrefix == y.LongitudePrefix 16 | && x.Precision == y.Precision; 17 | } 18 | 19 | /// 20 | public int GetHashCode(Region obj) 21 | { 22 | return Tuple.Create(obj.LatitudePrefix, obj.LongitudePrefix, obj.LongitudePrefix) 23 | .GetHashCode(); 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /CovidSafe/CovidSafe.DAL/Repositories/IRepository.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | 4 | namespace CovidSafe.DAL.Repositories 5 | { 6 | /// 7 | /// Base repository definition 8 | /// 9 | /// Object type handled by repository implementation 10 | /// Type used by the primary key 11 | public interface IRepository 12 | { 13 | /// 14 | /// Retrieves an object which matches the provided identifier 15 | /// 16 | /// Unique object identifier 17 | /// Cancellation token 18 | /// Object or null 19 | Task GetAsync(TT id, CancellationToken cancellationToken = default); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /CovidSafe/CovidSafe.API/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:1049", 7 | "sslPort": 44347 8 | } 9 | }, 10 | "$schema": "http://json.schemastore.org/launchsettings.json", 11 | "profiles": { 12 | "IIS Express": { 13 | "commandName": "IISExpress", 14 | "launchBrowser": true, 15 | "launchUrl": "swagger", 16 | "environmentVariables": { 17 | "ASPNETCORE_ENVIRONMENT": "Development" 18 | } 19 | }, 20 | "CovidSafe.API": { 21 | "commandName": "Project", 22 | "launchBrowser": true, 23 | "launchUrl": "swagger", 24 | "environmentVariables": { 25 | "ASPNETCORE_ENVIRONMENT": "Development" 26 | }, 27 | "applicationUrl": "https://localhost:5001;http://localhost:5000" 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /CovidSafe/CovidSafe.Entities/Messages/MessageContainerMetadata.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace CovidSafe.Entities.Messages 4 | { 5 | /// 6 | /// Basic metadata 7 | /// 8 | [JsonObject(MemberSerialization = MemberSerialization.OptIn)] 9 | public class MessageContainerMetadata 10 | { 11 | /// 12 | /// unique identifier 13 | /// 14 | [JsonProperty("id", Required = Required.Always)] 15 | public string Id { get; set; } 16 | /// 17 | /// Timestamp of creation 18 | /// 19 | /// 20 | /// Reported in milliseconds (ms) since the UNIX epoch. 21 | /// 22 | [JsonProperty("timestampMs", Required = Required.Always)] 23 | public long Timestamp { get; set; } 24 | } 25 | } -------------------------------------------------------------------------------- /CovidSafe/CovidSafe.Entities/Validation/RequestValidationFailedException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace CovidSafe.Entities.Validation 4 | { 5 | /// 6 | /// used for failed objects 7 | /// 8 | public class RequestValidationFailedException : Exception 9 | { 10 | /// 11 | /// from process 12 | /// 13 | public RequestValidationResult ValidationResult { get; private set; } 14 | 15 | /// 16 | /// Creates a new instance 17 | /// 18 | /// 19 | public RequestValidationFailedException(RequestValidationResult validationResult) : base() 20 | { 21 | this.ValidationResult = validationResult; 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /CovidSafe/CovidSafe.API.Tests/v20200611/MappingProfilesTests.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using CovidSafe.API.v20200611; 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | 5 | namespace CovidSafe.API.Tests.v20200611 6 | { 7 | /// 8 | /// Unit Tests for AutoMapper 9 | /// 10 | [TestClass] 11 | public class MappingProfilesTests 12 | { 13 | /// 14 | /// pass their internal validation using 15 | /// AutoMapper 16 | /// 17 | [TestMethod] 18 | public void MappingProfiles_PassValidation() 19 | { 20 | // Assemble 21 | var mapperConfig = new MapperConfiguration( 22 | opts => opts.AddProfile() 23 | ); 24 | 25 | // Act 26 | mapperConfig.AssertConfigurationIsValid(); 27 | 28 | // Assert 29 | // Exception thrown on invalid mappings 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /CovidSafe/CovidSafe.API.Tests/v20200415/MappingProfilesTests.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using CovidSafe.API.v20200415; 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | 5 | namespace CovidSafe.API.Tests.v20200415 6 | { 7 | /// 8 | /// Unit Tests for AutoMapper 9 | /// 10 | [TestClass] 11 | public class MappingProfilesTests 12 | { 13 | /// 14 | /// pass their internal validation using 15 | /// AutoMapper 16 | /// 17 | [TestMethod] 18 | public void MappingProfiles_PassValidation() 19 | { 20 | // Assemble 21 | var mapperConfig = new MapperConfiguration( 22 | opts => opts.AddProfile() 23 | ); 24 | 25 | // Act 26 | mapperConfig.AssertConfigurationIsValid(); 27 | 28 | // Assert 29 | // Exception thrown on invalid mappings 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /CovidSafe/CovidSafe.API.Tests/v20200505/MappingProfilesTests.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using CovidSafe.API.v20200505; 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | 5 | namespace CovidSafe.API.Tests.v20200505 6 | { 7 | /// 8 | /// Unit Tests for AutoMapper 9 | /// 10 | [TestClass] 11 | public class MappingProfilesTests 12 | { 13 | /// 14 | /// pass their internal validation using 15 | /// AutoMapper 16 | /// 17 | [TestMethod] 18 | public void MappingProfiles_PassValidation() 19 | { 20 | // Assemble 21 | var mapperConfig = new MapperConfiguration( 22 | opts => opts.AddProfile() 23 | ); 24 | 25 | // Act 26 | mapperConfig.AssertConfigurationIsValid(); 27 | 28 | // Assert 29 | // Exception thrown on invalid mappings 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /CovidSafe/CovidSafe.DAL.Tests/CovidSafe.DAL.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp2.1 5 | 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | all 16 | runtime; build; native; contentfiles; analyzers; buildtransitive 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2020 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /CovidSafe/CovidSafe.DAL/Repositories/Cosmos/Client/CosmosSchemaConfigurationSection.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Azure.Cosmos; 2 | 3 | namespace CovidSafe.DAL.Repositories.Cosmos.Client 4 | { 5 | /// 6 | /// Settings object for using Cosmos with the CovidSafe schema 7 | /// 8 | public class CosmosSchemaConfigurationSection 9 | { 10 | /// 11 | /// Cosmos target database name 12 | /// 13 | public string DatabaseName { get; set; } 14 | /// 15 | /// Maximum age of data to return in queries, in number of days 16 | /// 17 | public int MaxDataAgeToReturnDays { get; set; } 18 | /// 19 | /// Name of used to store objects 20 | /// 21 | public string MessageContainerName { get; set; } 22 | 23 | /// 24 | /// Creates a new instance 25 | /// 26 | public CosmosSchemaConfigurationSection() 27 | { 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /CovidSafe/CovidSafe.DAL/Repositories/Cosmos/Client/CosmosRepository.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | using Microsoft.Azure.Cosmos; 4 | 5 | namespace CovidSafe.DAL.Repositories.Cosmos.Client 6 | { 7 | /// 8 | /// Base Cosmos-connected repository class 9 | /// 10 | public abstract class CosmosRepository 11 | { 12 | /// 13 | /// object used for database interaction 14 | /// 15 | public CosmosContext Context { get; private set; } 16 | /// 17 | /// used by the inheriting 18 | /// 19 | public Container Container { get; protected set; } 20 | 21 | /// 22 | /// Creates a new instance 23 | /// 24 | /// 25 | public CosmosRepository(CosmosContext dbContext) 26 | { 27 | // Set local variables 28 | this.Context = dbContext; 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /CovidSafe/CovidSafe.Entities/CovidSafe.Entities.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | true 5 | $(NoWarn);1591 6 | netstandard2.0 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | True 18 | True 19 | ValidationMessages.resx 20 | 21 | 22 | 23 | 24 | 25 | PublicResXFileCodeGenerator 26 | ValidationMessages.Designer.cs 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /CovidSafe/CovidSafe.Entities/Validation/RequestValidationFailure.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using ProtoBuf; 3 | 4 | namespace CovidSafe.Entities.Validation 5 | { 6 | /// 7 | /// Detail of individual request validation failure 8 | /// 9 | [JsonObject] 10 | [ProtoContract] 11 | public class RequestValidationFailure 12 | { 13 | /// 14 | /// classification 15 | /// 16 | [JsonIgnore] 17 | [ProtoIgnore] 18 | public RequestValidationIssue Issue { get; set; } 19 | /// 20 | /// Failure detail message 21 | /// 22 | [JsonProperty("message", Required = Required.Always)] 23 | [ProtoMember(1)] 24 | public string Message { get; set; } 25 | /// 26 | /// Name of object property which failed validation 27 | /// 28 | [JsonProperty("property", Required = Required.AllowNull, NullValueHandling = NullValueHandling.Ignore)] 29 | [ProtoMember(2)] 30 | public string Property { get; set; } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /CovidSafe/CovidSafe.DAL/Helpers/PayloadSizeHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Runtime.Serialization.Formatters.Binary; 4 | 5 | namespace CovidSafe.DAL.Helpers 6 | { 7 | /// 8 | /// Helper for determining size (in bytes) of objects 9 | /// 10 | public static class PayloadSizeHelper 11 | { 12 | /// 13 | /// Calculates the size (in bytes) of an 14 | /// 15 | /// Source object 16 | /// Message size, in bytes 17 | public static long GetSize(object message) 18 | { 19 | if(message != null) 20 | { 21 | using (MemoryStream stream = new MemoryStream()) 22 | { 23 | BinaryFormatter formatter = new BinaryFormatter(); 24 | formatter.Serialize(stream, message); 25 | return stream.ToArray().Length; 26 | } 27 | } 28 | else 29 | { 30 | throw new ArgumentNullException(nameof(message)); 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CovidSafe Backend API 2 | 3 | Contains source code for the backend service API, which provides data to CovidSafe client applications. 4 | 5 | ## Build Status 6 | 7 | Build + Unit Test Pipeline: 8 | 9 | [![Build status](https://dev.azure.com/msresearch/CovidSafe/_apis/build/status/Builds/CovidSafe-BuildTests)](https://dev.azure.com/msresearch/CovidSafe/_build/latest?definitionId=2388) 10 | 11 | Compliance Pipeline: 12 | 13 | [![Build status](https://dev.azure.com/msresearch/CovidSafe/_apis/build/status/Compliance/Backend-Compliance%20Assessment)](https://dev.azure.com/msresearch/CovidSafe/_build/latest?definitionId=2384) 14 | 15 | Compilance Scans include: 16 | 17 | * [BinSkim](https://github.com/Microsoft/binskim) 18 | * [CredScan](https://secdevtools.azurewebsites.net/helpcredscan.html) 19 | * PoliCheck 20 | * Scans source code for words/phrases which may be insensitive. 21 | * [Roslyn Analyzers](https://secdevtools.azurewebsites.net/helpRoslynAnalyzers.html) 22 | 23 | ## Organization 24 | 25 | ### CovidSafe.API 26 | 27 | Web API project which contains controllers for each endpoint. 28 | 29 | ### CovidSafe.DAL 30 | 31 | Data Access Layer project which has the actual database interaction (repositories, etc.) and service layer classes. 32 | 33 | ### CovidSafe.Entities 34 | 35 | Library with all entity types used across the other projects. 36 | -------------------------------------------------------------------------------- /CovidSafe/CovidSafe.API.Tests/CovidSafe.API.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp2.1 5 | 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | all 23 | runtime; build; native; contentfiles; analyzers; buildtransitive 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /CovidSafe/CovidSafe.DAL/Helpers/GeoHelper.cs: -------------------------------------------------------------------------------- 1 | using CovidSafe.Entities.Geospatial; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Text; 5 | 6 | namespace CovidSafe.DAL.Helpers 7 | { 8 | public static class GeoHelper 9 | { 10 | /// 11 | /// Get distance in meters between two points 12 | /// 13 | /// Coordinates of the first point 14 | /// Coordinates of the second point 15 | /// Distance between points in meters 16 | /// 17 | /// https://en.wikipedia.org/wiki/Haversine_formula 18 | /// reimplementation based on GeoCoordinate.GetDistanceTo System.Device.dll (available only for .NET Framework) 19 | /// 20 | public static float DistanceMeters(Coordinates first, Coordinates second) 21 | { 22 | double radius = 6376500.0; 23 | double d1 = first.Latitude * (Math.PI / 180.0); 24 | double num1 = first.Longitude * (Math.PI / 180.0); 25 | double d2 = second.Latitude * (Math.PI / 180.0); 26 | double num2 = second.Longitude * (Math.PI / 180.0) - num1; 27 | double d3 = Math.Pow(Math.Sin((d2 - d1) / 2.0), 2.0) + Math.Cos(d1) * Math.Cos(d2) * Math.Pow(Math.Sin(num2 / 2.0), 2.0); 28 | 29 | return (float)(radius * (2.0 * Math.Atan2(Math.Sqrt(d3), Math.Sqrt(1.0 - d3)))); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /CovidSafe/CovidSafe.API.Tests.Performance/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("CovidSafe.API.WebPerformanceAndLoadTests")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("CovidSafe.API.WebPerformanceAndLoadTests")] 13 | [assembly: AssemblyCopyright("Copyright © 2020")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("b5f256b9-c0dc-4ba6-9c60-7bcf8e13f15a")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | [assembly: AssemblyVersion("1.0.0.0")] 35 | [assembly: AssemblyFileVersion("1.0.0.0")] 36 | -------------------------------------------------------------------------------- /CovidSafe/CovidSafe.Entities/Geospatial/RegionBoundary.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | using Newtonsoft.Json; 4 | 5 | namespace CovidSafe.Entities.Geospatial 6 | { 7 | /// 8 | /// Defines boundaries of a 9 | /// 10 | [JsonObject(MemberSerialization = MemberSerialization.OptIn)] 11 | [Serializable] 12 | public class RegionBoundary 13 | { 14 | /// 15 | /// Maximum boundary coordinates 16 | /// 17 | [JsonProperty("Max", Required = Required.Always)] 18 | public Coordinates Max { get; set; } 19 | /// 20 | /// Maximum boundary coordinates 21 | /// 22 | [JsonProperty("Min", Required = Required.Always)] 23 | public Coordinates Min { get; set; } 24 | 25 | /// 26 | /// Creates a new instance 27 | /// 28 | public RegionBoundary() 29 | { 30 | } 31 | 32 | /// 33 | /// Creates a new instance 34 | /// 35 | /// Source 36 | public RegionBoundary(RegionBoundary boundary) 37 | { 38 | if (boundary != null) 39 | { 40 | this.Max = new Coordinates { Longitude = boundary.Max.Longitude, Latitude = boundary.Max.Latitude }; 41 | this.Min = new Coordinates { Longitude = boundary.Min.Longitude, Latitude = boundary.Min.Latitude }; 42 | } 43 | else 44 | { 45 | throw new ArgumentNullException(nameof(boundary)); 46 | } 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /CovidSafe/CovidSafe.Entities/Geospatial/Coordinates.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | using CovidSafe.Entities.Validation; 4 | using Newtonsoft.Json; 5 | 6 | namespace CovidSafe.Entities.Geospatial 7 | { 8 | /// 9 | /// Geographic location coordinates 10 | /// 11 | [JsonObject(MemberSerialization = MemberSerialization.OptIn)] 12 | [Serializable] 13 | public class Coordinates : IValidatable 14 | { 15 | /// 16 | /// Maximum allowable latitude 17 | /// 18 | public const double MAX_LATITUDE = 90; 19 | /// 20 | /// Maximum allowable longitude 21 | /// 22 | public const double MAX_LONGITUDE = 180; 23 | /// 24 | /// Minimum allowable latitude 25 | /// 26 | public const double MIN_LATITUDE = -90; 27 | /// 28 | /// Minimum allowable longitude 29 | /// 30 | public const double MIN_LONGITUDE = -180; 31 | 32 | /// 33 | /// Latitude of coordinate 34 | /// 35 | [JsonProperty("lat", Required = Required.Always)] 36 | public double Latitude { get; set; } 37 | /// 38 | /// Longitude of coordinate 39 | /// 40 | [JsonProperty("lng", Required = Required.Always)] 41 | public double Longitude { get; set; } 42 | 43 | /// 44 | public RequestValidationResult Validate() 45 | { 46 | RequestValidationResult result = new RequestValidationResult(); 47 | 48 | // Ensure lat/lng are within range 49 | result.Combine(Validator.ValidateLatitude(this.Latitude, nameof(this.Latitude))); 50 | result.Combine(Validator.ValidateLongitude(this.Longitude, nameof(this.Longitude))); 51 | 52 | return result; 53 | } 54 | } 55 | } -------------------------------------------------------------------------------- /CovidSafe/CovidSafe.DAL/Repositories/Cosmos/Records/CosmosRecord.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel.DataAnnotations; 3 | 4 | using Microsoft.Azure.Cosmos; 5 | using Newtonsoft.Json; 6 | 7 | namespace CovidSafe.DAL.Repositories.Cosmos.Records 8 | { 9 | /// 10 | /// Cosmos Database record object 11 | /// 12 | /// Object type to store 13 | [JsonObject] 14 | public abstract class CosmosRecord 15 | { 16 | /// 17 | /// Unique identifier 18 | /// 19 | /// 20 | /// Do NOT set this to 'Required'. Let CosmosDB auto-generate this. 21 | /// 22 | [JsonProperty("id")] 23 | public string Id { get; set; } 24 | /// 25 | /// Partition Key value 26 | /// 27 | [JsonProperty("partitionKey", Required = Required.Always)] 28 | [Required] 29 | public string PartitionKey { get; set; } 30 | /// 31 | /// Timestamp of record database insert, in ms since UNIX epoch 32 | /// 33 | [JsonProperty("timestamp", Required = Required.Always)] 34 | [Required] 35 | public long Timestamp { get; set; } 36 | /// 37 | /// Object value 38 | /// 39 | [JsonProperty("Value", Required = Required.Always)] 40 | [Required] 41 | public T Value { get; set; } 42 | /// 43 | /// Record schema version 44 | /// 45 | [JsonProperty("version", Required = Required.Always)] 46 | public string Version { get; set; } = ""; 47 | 48 | /// 49 | /// Creates a new instance 50 | /// 51 | public CosmosRecord() 52 | { 53 | // Set default local values 54 | this.Id = Guid.NewGuid().ToString(); 55 | this.Timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /CovidSafe/CovidSafe.DAL/Repositories/Cosmos/Client/CosmosConnectionFactory.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Azure.Cosmos; 2 | using Microsoft.Extensions.Configuration; 3 | 4 | namespace CovidSafe.DAL.Repositories.Cosmos.Client 5 | { 6 | /// 7 | /// Helper class for creating a Cosmos account connection 8 | /// 9 | public class CosmosConnectionFactory 10 | { 11 | /// 12 | /// CosmosDB Connection String 13 | /// 14 | private string _connectionString; 15 | /// 16 | /// Default 17 | /// 18 | public CosmosClientOptions DefaultClientOptions { get; private set; } = new CosmosClientOptions 19 | { 20 | ApplicationRegion = Regions.EastUS 21 | }; 22 | 23 | /// 24 | /// Creates a new instance 25 | /// 26 | /// Database connection string 27 | public CosmosConnectionFactory(string connectionString) 28 | { 29 | // Store local variables 30 | this._connectionString = connectionString; 31 | } 32 | 33 | /// 34 | /// Creates a new instance with default 35 | /// 36 | /// 37 | /// instance 38 | public CosmosClient GetClient() 39 | { 40 | // Use overload for simplicity 41 | return this.GetClient(this.DefaultClientOptions); 42 | } 43 | 44 | /// 45 | /// Creates a new instance with a custom configuration 46 | /// 47 | /// 48 | /// instance 49 | public CosmosClient GetClient(CosmosClientOptions options) 50 | { 51 | return new CosmosClient(this._connectionString, options); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /CovidSafe/CovidSafe.API/CovidSafe.API.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | true 5 | $(NoWarn);1591 6 | netcoreapp2.1 7 | ca6741f8-30f9-472c-a5c3-c6e0a78d1961 8 | Windows 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /CovidSafe/CovidSafe.Entities/Messages/BluetoothSeedMessage.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | using CovidSafe.Entities.Validation; 4 | using Newtonsoft.Json; 5 | 6 | namespace CovidSafe.Entities.Messages 7 | { 8 | /// 9 | /// Bluetooth seed 10 | /// 11 | [JsonObject(MemberSerialization = MemberSerialization.OptIn)] 12 | [Serializable] 13 | public class BluetoothSeedMessage : IValidatable 14 | { 15 | /// 16 | /// Start of validity period 17 | /// 18 | /// 19 | /// Reported in milliseconds (ms) since the UNIX epoch. 20 | /// 21 | [JsonProperty("beginTimestampMs", Required = Required.Always)] 22 | public long BeginTimestamp { get; set; } 23 | /// 24 | /// End of validity period 25 | /// 26 | /// 27 | /// Reported in milliseconds (ms) since the UNIX epoch. 28 | /// 29 | [JsonProperty("endTimestampMs", Required = Required.Always)] 30 | public long EndTimestamp { get; set; } 31 | /// 32 | /// Seed backing field 33 | /// 34 | [NonSerialized] 35 | private string _seed; 36 | /// 37 | /// Seed value 38 | /// 39 | [JsonProperty("seed", Required = Required.Always)] 40 | public string Seed 41 | { 42 | get { return this._seed; } 43 | set { this._seed = value; } 44 | } 45 | 46 | /// 47 | public RequestValidationResult Validate() 48 | { 49 | RequestValidationResult result = new RequestValidationResult(); 50 | 51 | // Seed validation 52 | result.Combine(Validator.ValidateSeed(this.Seed, nameof(this.Seed))); 53 | 54 | // Ensure times are valid 55 | result.Combine(Validator.ValidateTimestamp(this.BeginTimestamp, parameterName: nameof(this.BeginTimestamp))); 56 | result.Combine(Validator.ValidateTimestamp(this.EndTimestamp, parameterName: nameof(this.EndTimestamp))); 57 | result.Combine(Validator.ValidateTimeRange(this.BeginTimestamp, this.EndTimestamp)); 58 | 59 | return result; 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /CovidSafe/CovidSafe.Entities/Messages/NarrowcastMessage.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | using CovidSafe.Entities.Geospatial; 4 | using CovidSafe.Entities.Validation; 5 | using CovidSafe.Entities.Validation.Resources; 6 | using Newtonsoft.Json; 7 | 8 | namespace CovidSafe.Entities.Messages 9 | { 10 | /// 11 | /// Narrowcast message container 12 | /// 13 | [JsonObject(MemberSerialization = MemberSerialization.OptIn)] 14 | [Serializable] 15 | public class NarrowcastMessage : IValidatable 16 | { 17 | /// 18 | /// targeted by this 19 | /// 20 | [JsonProperty("Area", Required = Required.Always)] 21 | public NarrowcastArea Area { get; set; } 22 | /// 23 | /// Internal UserMessage backing field 24 | /// 25 | [NonSerialized] 26 | private string _userMessage; 27 | /// 28 | /// Message displayed to user on positive match 29 | /// 30 | [JsonProperty("userMessage", NullValueHandling = NullValueHandling.Ignore)] 31 | public string UserMessage 32 | { 33 | get { return this._userMessage; } 34 | set { _userMessage = value; } 35 | } 36 | 37 | /// 38 | public RequestValidationResult Validate() 39 | { 40 | RequestValidationResult result = new RequestValidationResult(); 41 | 42 | // Validate provided NarrowcastArea 43 | if (this.Area != null) 44 | { 45 | // Use NarrowcastArea.Validate() 46 | result.Combine(this.Area.Validate()); 47 | } 48 | else 49 | { 50 | result.Fail( 51 | RequestValidationIssue.InputEmpty, 52 | nameof(this.Area), 53 | ValidationMessages.EmptyAreas 54 | ); 55 | } 56 | 57 | // Validate message 58 | if (String.IsNullOrEmpty(this.UserMessage)) 59 | { 60 | result.Fail( 61 | RequestValidationIssue.InputEmpty, 62 | nameof(this.UserMessage), 63 | ValidationMessages.EmptyMessage 64 | ); 65 | } 66 | 67 | return result; 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /CovidSafe/CovidSafe.API/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics.CodeAnalysis; 3 | 4 | using Microsoft.AspNetCore; 5 | using Microsoft.AspNetCore.Hosting; 6 | using Microsoft.Azure.KeyVault; 7 | using Microsoft.Azure.Services.AppAuthentication; 8 | using Microsoft.Extensions.Configuration; 9 | using Microsoft.Extensions.Configuration.AzureKeyVault; 10 | 11 | namespace CovidSafe.API 12 | { 13 | /// 14 | /// Main application class 15 | /// 16 | /// 17 | /// CS1591: Ignores missing documentation warnings. 18 | /// CodeCoverageExclusion: Generic program entry point. 19 | /// 20 | [ExcludeFromCodeCoverage] 21 | #pragma warning disable CS1591 22 | public class Program 23 | { 24 | /// 25 | /// Application entry point 26 | /// 27 | /// Command-line arguments 28 | public static void Main(string[] args) 29 | { 30 | CreateWebHostBuilder(args).Build().Run(); 31 | } 32 | 33 | public static IWebHostBuilder CreateWebHostBuilder(string[] args) => 34 | WebHost.CreateDefaultBuilder(args) 35 | .ConfigureAppConfiguration((context, config) => 36 | { 37 | // We need to generate the original config before we can get the Key Vault URL 38 | var builtConfig = config.Build(); 39 | 40 | // Is there a Key Vault URL specified? 41 | if(!String.IsNullOrEmpty(builtConfig["KeyVaultUrl"])) 42 | { 43 | // Get Managed Service Identity token 44 | AzureServiceTokenProvider tokenProvider = new AzureServiceTokenProvider(); 45 | KeyVaultClient kvClient = new KeyVaultClient( 46 | new KeyVaultClient.AuthenticationCallback( 47 | tokenProvider.KeyVaultTokenCallback 48 | ) 49 | ); 50 | 51 | config.AddAzureKeyVault( 52 | builtConfig["KeyVaultUrl"], 53 | kvClient, 54 | new DefaultKeyVaultSecretManager() 55 | ); 56 | } 57 | }) 58 | .UseStartup(); 59 | } 60 | #pragma warning restore CS1591 61 | } 62 | -------------------------------------------------------------------------------- /CovidSafe/CovidSafe.DAL/Helpers/PrecisionHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace CovidSafe.DAL.Helpers 4 | { 5 | /// 6 | /// Helper functions for working with rounding numbers with controlled precision. 7 | /// 8 | public static class PrecisionHelper 9 | { 10 | public static int GetStep(int precision) 11 | { 12 | int bits = Math.Max(8 - precision, 0); 13 | return 1 << bits; 14 | } 15 | 16 | /// 17 | /// Rounds given number and precision parameter. 18 | /// 19 | /// Any integer 20 | /// Precision parameter, any integer 21 | /// Nearest number aligned with precision grid, equal to d or closer to 0. 22 | /// Precision=0 maps any int from [-256, 256] to 0. 23 | /// Precision>=8 maps any int to itself 24 | public static int Round(int d, int precision) 25 | { 26 | int bits = Math.Max(8 - precision, 0); 27 | return d >= 0 ? d >> bits << bits : -(-d >> bits << bits); 28 | } 29 | 30 | /// 31 | /// Rounds given number and precision parameter. Method for legacy API that supports float values. 32 | /// 33 | /// Any double value 34 | /// Precision parameter, any integer 35 | /// Nearest number aligned with precision grid, equal to d or closer to 0. 36 | /// Precision=0 maps any double from [-256, 256] to 0. 37 | /// Precision=9 maps any double to its int lower bound 38 | public static int Round(double d, int precision) 39 | { 40 | return Round((int)d, precision); 41 | } 42 | 43 | /// 44 | /// For given number and precision parameter, returns range [xmin, xmax] containing the number, 45 | /// where xmin < xmax, xmin and xmax are to numbers next to each other on the grid aligned with given precision 46 | /// Rounds given number and precision parameter. Method for legacy API that supports float values. 47 | /// 48 | /// Any double number 49 | /// Precision parameter, any integer 50 | /// Tuple of ints (xmin, xmax) 51 | public static Tuple GetRange(int d, int precision) 52 | { 53 | int rounded = Round(d, precision); 54 | int step = GetStep(precision); 55 | 56 | if (rounded == 0) 57 | return Tuple.Create(-step, step); 58 | else if (rounded > 0) 59 | return Tuple.Create(rounded, rounded + step); 60 | return Tuple.Create(rounded - step, rounded); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /CovidSafe/CovidSafe.DAL/Repositories/Cosmos/Records/MessageContainerRecord.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | using CovidSafe.DAL.Helpers; 4 | using CovidSafe.Entities.Geospatial; 5 | using CovidSafe.Entities.Messages; 6 | using Newtonsoft.Json; 7 | 8 | namespace CovidSafe.DAL.Repositories.Cosmos.Records 9 | { 10 | /// 11 | /// implementation of 12 | /// 13 | public class MessageContainerRecord : CosmosRecord 14 | { 15 | /// 16 | /// Boundary allowed by region 17 | /// 18 | [JsonProperty("RegionBoundary", Required = Required.Always)] 19 | [Required] 20 | public RegionBoundary RegionBoundary { get; set; } 21 | 22 | /// 23 | /// region 24 | /// 25 | [JsonProperty("Region", Required = Required.Always)] 26 | [Required] 27 | public Region Region { get; set; } 28 | 29 | /// 30 | /// Size of the record , in bytes 31 | /// 32 | [JsonProperty("size", Required = Required.Always)] 33 | [Required] 34 | public long Size { get; set; } 35 | 36 | /// 37 | /// Current version of record schema 38 | /// 39 | [JsonIgnore] 40 | public const string CURRENT_RECORD_VERSION = "4.0.0"; 41 | 42 | /// 43 | /// Creates a new instance 44 | /// 45 | public MessageContainerRecord() 46 | { 47 | } 48 | 49 | /// 50 | /// Creates a new instance 51 | /// 52 | /// to store 53 | public MessageContainerRecord(MessageContainer report) 54 | { 55 | this.Size = PayloadSizeHelper.GetSize(report); 56 | this.Value = report; 57 | this.Version = CURRENT_RECORD_VERSION; 58 | } 59 | 60 | /// 61 | /// Generates a new Partition Key value for the record 62 | /// 63 | /// Partition Key value 64 | public static string GetPartitionKey(Region region) 65 | { 66 | return $"{region.LatitudePrefix},{region.LongitudePrefix},{region.Precision}"; 67 | } 68 | } 69 | } -------------------------------------------------------------------------------- /CovidSafe/CovidSafe.DAL/Repositories/Cosmos/Client/CosmosContext.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Azure.Cosmos; 2 | using Microsoft.Extensions.Options; 3 | 4 | namespace CovidSafe.DAL.Repositories.Cosmos.Client 5 | { 6 | /// 7 | /// Cosmos database context 8 | /// 9 | public class CosmosContext 10 | { 11 | /// 12 | /// reference for 13 | /// 14 | /// 15 | /// Enables configuration change detection without restarting service 16 | /// 17 | private IOptionsMonitor _schemaConfig { get; set; } 18 | /// 19 | /// instance 20 | /// 21 | public CosmosClient Client { get; private set; } 22 | /// 23 | /// reference 24 | /// 25 | public Database Database { get; private set; } 26 | /// 27 | /// instance 28 | /// 29 | public CosmosSchemaConfigurationSection SchemaOptions 30 | { 31 | get 32 | { 33 | return this._schemaConfig.CurrentValue; 34 | } 35 | } 36 | 37 | /// 38 | /// Creates a new instance 39 | /// 40 | /// Database connection factory instance 41 | /// Schema configuration provider 42 | public CosmosContext(CosmosConnectionFactory connectionFactory, IOptionsMonitor schemaConfig) 43 | { 44 | // Set local variables 45 | this._schemaConfig = schemaConfig; 46 | this.Client = connectionFactory.GetClient(); 47 | 48 | // Create Database object reference 49 | this.Database = this.Client.GetDatabase(this.SchemaOptions.DatabaseName); 50 | } 51 | 52 | /// 53 | /// Gets a reference to the specified from the 54 | /// local instance 55 | /// 56 | /// Target name 57 | /// reference 58 | public Container GetContainer(string containerName) 59 | { 60 | return this.Database.GetContainer(containerName); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /CovidSafe/CovidSafe.API/Swagger/SwaggerConfigureOptions.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | 3 | using Microsoft.AspNetCore.Mvc.ApiExplorer; 4 | using Microsoft.Extensions.DependencyInjection; 5 | using Microsoft.Extensions.Options; 6 | using Microsoft.OpenApi.Models; 7 | using Swashbuckle.AspNetCore.SwaggerGen; 8 | 9 | namespace CovidSafe.API.Swagger 10 | { 11 | /// 12 | /// Configures Swagger generation options 13 | /// 14 | /// 15 | /// CodeCoverageExclusion: Does not provide core functionality. 16 | /// 17 | [ExcludeFromCodeCoverage] 18 | public class SwaggerConfigureOptions : IConfigureOptions 19 | { 20 | /// 21 | /// Configures API version description handling 22 | /// 23 | readonly IApiVersionDescriptionProvider _provider; 24 | 25 | /// 26 | /// Creates a new instance 27 | /// 28 | /// API Version Description provider implementation 29 | public SwaggerConfigureOptions(IApiVersionDescriptionProvider provider) => this._provider = provider; 30 | 31 | /// 32 | public void Configure(SwaggerGenOptions options) 33 | { 34 | // Generate a SwaggerDoc for each API version discovered in the project 35 | foreach(var description in this._provider.ApiVersionDescriptions) 36 | { 37 | options.SwaggerDoc( 38 | description.GroupName, 39 | CreateInfoForApiVersion(description) 40 | ); 41 | } 42 | } 43 | 44 | /// 45 | /// Builds the description portion of a SwaggerDoc for each API version in the project 46 | /// 47 | /// Target API version information 48 | /// 49 | public static OpenApiInfo CreateInfoForApiVersion(ApiVersionDescription description) 50 | { 51 | OpenApiInfo info = new OpenApiInfo() 52 | { 53 | Description = "Enables communication between CovidSafe client applications and backend data storage.", 54 | Title = "CovidSafe API", 55 | Version = description.ApiVersion.ToString(), 56 | }; 57 | 58 | if(description.IsDeprecated) 59 | { 60 | info.Description = "This API version has been deprecated. Please migrate to a newer version."; 61 | } 62 | 63 | return info; 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /CovidSafe/CovidSafe.Entities/Validation/RequestValidationResult.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | using Newtonsoft.Json; 5 | using ProtoBuf; 6 | 7 | namespace CovidSafe.Entities.Validation 8 | { 9 | /// 10 | /// Object validation summary 11 | /// 12 | [JsonObject] 13 | [ProtoContract] 14 | public class RequestValidationResult 15 | { 16 | /// 17 | /// Tells if the result passed (true if yes, false if no) 18 | /// 19 | [JsonIgnore] 20 | [ProtoIgnore] 21 | public bool Passed 22 | { 23 | get 24 | { 25 | return this.Failures.Count == 0; 26 | } 27 | } 28 | 29 | /// 30 | /// Collection of objects 31 | /// 32 | [JsonProperty("validationFailures")] 33 | [ProtoMember(1)] 34 | public List Failures { get; set; } = new List(); 35 | 36 | /// 37 | /// Add failures to this object 38 | /// 39 | public void Combine(RequestValidationResult other) 40 | { 41 | this.Failures.AddRange(other.Failures); 42 | } 43 | 44 | /// 45 | /// Report a new 46 | /// 47 | /// classification 48 | /// Failing property name 49 | /// Failure text 50 | public void Fail(RequestValidationIssue issue, string property, string message) 51 | { 52 | this.Failures.Add(new RequestValidationFailure 53 | { 54 | Issue = issue, 55 | Message = message 56 | }); 57 | } 58 | 59 | /// 60 | /// Report a new 61 | /// 62 | /// classification 63 | /// Failing property name 64 | /// Failure text 65 | /// Failure text arguments (akin to 66 | public void Fail(RequestValidationIssue issue, string property, string message, params string[] args) 67 | { 68 | // Use overload 69 | this.Fail(issue, property, String.Format(message, args)); 70 | } 71 | } 72 | } -------------------------------------------------------------------------------- /CovidSafe/CovidSafe.API/v20200611/Protos/Interactions.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package Corona; 4 | option csharp_namespace = "CovidSafe.API.v20200611.Protos"; 5 | 6 | // Geographic location at a point in time; 7 | message Area { 8 | // Geographic location coordinates; 9 | Location location = 1; 10 | // Radius, in meters, of coordinate coverage; 11 | float radius_meters = 2; 12 | // Alert start time; 13 | int64 begin_time = 3; 14 | // Alert end time; 15 | int64 end_time = 4; 16 | } 17 | 18 | // Geographic location; 19 | message Location { 20 | // Latitude; 21 | double latitude = 1; 22 | // Longitude; 23 | double longitude = 2; 24 | } 25 | 26 | // Message metadata object; 27 | message MessageInfo { 28 | // Unique Message identifier; 29 | string message_id = 1; 30 | // Message timestamp; 31 | int64 message_timestamp = 2; 32 | } 33 | 34 | // Phone -> Server; 35 | // Request for metadata of added or updated messages based on client timestamp; 36 | message MessageListRequest { 37 | // Region targeted by request; 38 | Region region = 1; 39 | // Timestamp of most recent API request from the client; 40 | int64 last_query_time = 2; 41 | } 42 | 43 | // Server -> Phone; 44 | // Collection of MessageInfo matching MessageListRequest; 45 | message MessageListResponse { 46 | // Matching Message metadata collection; 47 | repeated MessageInfo message_info = 1; 48 | // Latest Message timestamp included in the MessageInfo collection; 49 | int64 max_response_timestamp = 2; 50 | } 51 | 52 | // Phone -> Server GetMessages(new_message_ids); 53 | // Request to download the details of given query ids; 54 | message MessageRequest { 55 | // Collection of Message metadata, used to pull specific Message objects; 56 | repeated MessageInfo requested_queries = 1; 57 | } 58 | 59 | // Server -> Phone (list of messages corresponding to touch points where 60 | // infection can occur); 61 | message MessageResponse { 62 | // Collection of Narrowcast messages matching MessageRequest criteria 63 | repeated NarrowcastMessage narrowcast_messages = 1; 64 | } 65 | 66 | // Phone <-> Server; 67 | // Narrowcast Message object; 68 | message NarrowcastMessage { 69 | // Message displayed to user on match; 70 | string user_message = 1; 71 | // Area of infection risk; 72 | Area area = 2; 73 | } 74 | 75 | // Geographic region quantized by precision of lat/long; 76 | message Region { 77 | // Latitude source, no decimal; 78 | int32 latitude_prefix = 1; 79 | // Longitude source, no decimal; 80 | int32 longitude_prefix = 2; 81 | // Mantissa mask/precision bits; 82 | int32 precision = 3; 83 | } 84 | 85 | // Request error message; 86 | message RequestValidationResult { 87 | // Collection of individual request validation failures; 88 | repeated RequestValidationFailure failures = 1; 89 | } 90 | 91 | // Individual request failure message; 92 | message RequestValidationFailure { 93 | // Validation error message; 94 | string message = 1; 95 | // Name of parameter failing validation; 96 | string property = 2; 97 | } -------------------------------------------------------------------------------- /CovidSafe/CovidSafe.DAL/Repositories/IMessageContainerRepository.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | 5 | using CovidSafe.Entities.Geospatial; 6 | using CovidSafe.Entities.Messages; 7 | 8 | namespace CovidSafe.DAL.Repositories 9 | { 10 | /// 11 | /// repository definition 12 | /// 13 | public interface IMessageContainerRepository : IRepository 14 | { 15 | /// 16 | /// Pulls a list of the latest objects, based on client parameters 17 | /// 18 | /// Target 19 | /// Timestamp of latest client for region, in ms since UNIX epoch 20 | /// Cancellation token 21 | /// Collection of objects 22 | Task> GetLatestAsync(Region region, long lastTimestamp, CancellationToken cancellationToken = default); 23 | /// 24 | /// Retrieves the data size, in bytes, based on a region's latest data 25 | /// 26 | /// Target 27 | /// Timestamp of latest client for region, in ms since UNIX epoch 28 | /// Cancellation token 29 | /// Data size, in bytes 30 | Task GetLatestRegionSizeAsync(Region region, long lastTimestamp, CancellationToken cancellationToken = default); 31 | /// 32 | /// Pulls a collection of objects, based on provided identifiers 33 | /// 34 | /// Collection of identifiers 35 | /// Cancellation token 36 | /// Collection of objects 37 | Task> GetRangeAsync(IEnumerable ids, CancellationToken cancellationToken); 38 | /// 39 | /// Store a new in the repository 40 | /// 41 | /// to store 42 | /// Target 43 | /// Cancellation token 44 | /// Unique identifier of stored object 45 | Task InsertAsync(MessageContainer message, Region region, CancellationToken cancellationToken = default); 46 | /// 47 | /// Store a new in the repository to multiple regions 48 | /// 49 | /// to store 50 | /// Target 51 | /// Cancellation token 52 | Task InsertAsync(MessageContainer message, IEnumerable regions, CancellationToken cancellationToken = default); 53 | } 54 | } -------------------------------------------------------------------------------- /CovidSafe/CovidSafe.API/v20200505/Protos/Interactions.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package Corona; 4 | option csharp_namespace = "CovidSafe.API.v20200505.Protos"; 5 | 6 | // Phone -> Server; 7 | // Get list of messages queued for a region since last query time 8 | message MessageListRequest { 9 | Region region = 1; 10 | int64 last_query_time = 2; 11 | } 12 | 13 | // Server -> Phone; list of messages for region; 14 | // as_of tells a client the current server time to enable clock synchronization 15 | message MessageListResponse { 16 | repeated MessageInfo message_info = 1; 17 | int64 max_response_timestamp = 2; 18 | } 19 | 20 | // Phone -> Server GetMessages(new_message_ids) 21 | // Request to download the details of given query ids 22 | message MessageRequest { repeated MessageInfo requested_queries = 1; } 23 | 24 | // Server -> Phone (list of messages corresponding to touch points where 25 | // infection can occour) 26 | message MatchMessageResponse { 27 | repeated MatchMessage match_messages = 1; 28 | } 29 | 30 | message MatchMessage { 31 | // Not used at the moment. Will eventually express a boolean relationship between 32 | // elements in the area_match and bluetooth_match collections 33 | string bool_expression = 1; 34 | // at or around for more than around 35 | repeated AreaMatch area_matches = 2; 36 | // near ; where person is identified by some kind of blue tooth beacon 37 | repeated BlueToothSeed bluetooth_seeds = 3; 38 | // The message to display to the user if there is a blue tooth match. 39 | string bluetooth_match_message = 4; 40 | } 41 | 42 | // Phone <-> Server; 43 | // 2 known uses 44 | // 1) Register a new announcement of area of interest 45 | // 2) Part of MatchMessage which aggregates AreaMatches and BluetoothMatches 46 | message AreaMatch { 47 | // Message to be displayed to the user if there is a match 48 | string user_message = 1; 49 | // Areas of interest 50 | repeated Area areas = 2; 51 | } 52 | 53 | // Phone -> Server; 54 | // Message to self-register a phone as infected 55 | message SelfReportRequest { 56 | // Anonymized blue tooth beacons (or other exact match identifiers if 57 | // available) 58 | repeated BluetoothSeed seeds = 1; 59 | // Coarse region that this request applies to specified 60 | // to maximum allowed privacy preserving precision 61 | Region region = 2; 62 | } 63 | 64 | // Server -> Phone 65 | // Generic response to requests that don't have any other response. 66 | // Contains status that details success (0) or failure (>0) 67 | // and has message that details the failure. 68 | message Status { 69 | int32 status_code = 1; 70 | string status_message = 2; 71 | } 72 | 73 | // Represents a geographic region quantized by precision of lat/long 74 | message Region { 75 | double latitude_prefix = 1; 76 | double longitude_prefix = 2; 77 | // Mantissa mask. Number of bits of Mantissa that should be preserved 78 | int32 precision = 3; 79 | } 80 | 81 | // Metadata about each query 82 | message MessageInfo { 83 | string message_id = 1; 84 | int64 message_timestamp = 2; 85 | } 86 | 87 | message BlueToothSeed{ 88 | string seed = 1; 89 | // Reporting user's 2wk old seed. 90 | int64 sequence_start_time = 2; 91 | // The time that seed is valid till 92 | int64 sequence_end_time = 3; 93 | } 94 | 95 | message Area { 96 | Location location = 1; 97 | float radius_meters = 2; 98 | int64 begin_time = 3; 99 | int64 end_time = 4; 100 | } 101 | 102 | message Location { 103 | double latitude = 1; 104 | double longitude = 2; 105 | } -------------------------------------------------------------------------------- /CovidSafe/CovidSafe.API/v20200415/Protos/Interactions.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package Corona; 4 | option csharp_namespace = "CovidSafe.API.v20200415.Protos"; 5 | 6 | // Phone -> Server; 7 | // Get list of messages queued for a region since last query time 8 | message MessageListRequest { 9 | Region region = 1; 10 | int64 last_query_time = 2; 11 | } 12 | 13 | // Server -> Phone; list of messages for region; 14 | // as_of tells a client the current server time to enable clock synchronization 15 | message MessageListResponse { 16 | repeated MessageInfo message_info = 1; 17 | int64 max_response_timestamp = 2; 18 | } 19 | 20 | // Phone -> Server GetMessages(new_message_ids) 21 | // Request to download the details of given query ids 22 | message MessageRequest { repeated MessageInfo requested_queries = 1; } 23 | 24 | // Server -> Phone (list of messages corresponding to touch points where 25 | // infection can occour) 26 | message MatchMessage { 27 | // Not used at the moment. Will eventually express a boolean relationship between 28 | // elements in the area_match and bluetooth_match collections 29 | string bool_expression = 1; 30 | // at or around for more than around 31 | repeated AreaMatch area_matches = 2; 32 | // near ; where person is identified by some kind of blue tooth beacon 33 | repeated BluetoothMatch bluetooth_matches = 3; 34 | } 35 | 36 | // Phone <-> Server; 37 | // 2 known uses 38 | // 1) Register a new announcement of area of interest 39 | // 2) Part of MatchMessage which aggregates AreaMatches and BluetoothMatches 40 | message AreaMatch { 41 | // Message to be displayed to the user if there is a match 42 | string user_message = 1; 43 | // Areas of interest 44 | repeated Area areas = 2; 45 | } 46 | 47 | // Phone -> Server; 48 | // Message to self-register a phone as infected 49 | message SelfReportRequest { 50 | // Anonymized blue tooth beacons (or other exact match identifiers if 51 | // available) 52 | repeated BlueToothSeed seeds = 1; 53 | // Coarse region that this request applies to specified 54 | // to maximum allowed privacy preserving precision 55 | Region region = 2; 56 | } 57 | 58 | // Server -> Phone 59 | // Generic response to requests that don't have any other response. 60 | // Contains status that details success (0) or failure (>0) 61 | // and has message that details the failure. 62 | message Status { 63 | int32 status_code = 1; 64 | string status_message = 2; 65 | } 66 | 67 | // Represents a geographic region quantized by precision of lat/long 68 | message Region { 69 | double latitude_prefix = 1; 70 | double longitude_prefix = 2; 71 | // Mantissa mask. Number of bits of Mantissa that should be preserved 72 | int32 precision = 3; 73 | } 74 | 75 | // Metadata about each query 76 | message MessageInfo { 77 | string message_id = 1; 78 | int64 message_timestamp = 2; 79 | } 80 | 81 | // List of blue tooth beacons 82 | message BluetoothMatch { 83 | // bluetooth_query Message to be displayed to the user if there is a match 84 | string user_message = 1; 85 | // Anonymized blue tooth beacons (or other exact match identifiers if 86 | // available) 87 | repeated BlueToothSeed seeds = 2; 88 | } 89 | 90 | message BlueToothSeed{ 91 | string seed = 1; 92 | int64 sequence_start_time = 2; 93 | int64 sequence_end_time = 3; 94 | } 95 | 96 | message Area { 97 | Location location = 1; 98 | float radius_meters = 2; 99 | int64 begin_time = 3; 100 | int64 end_time = 4; 101 | } 102 | 103 | message Location { 104 | double latitude = 1; 105 | double longitude = 2; 106 | } -------------------------------------------------------------------------------- /CovidSafe/CovidSafe.Entities/Geospatial/NarrowcastArea.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | using CovidSafe.Entities.Validation; 4 | using CovidSafe.Entities.Validation.Resources; 5 | using Newtonsoft.Json; 6 | 7 | namespace CovidSafe.Entities.Geospatial 8 | { 9 | /// 10 | /// Area of Narrowcast message applicability 11 | /// 12 | [JsonObject(MemberSerialization = MemberSerialization.OptIn)] 13 | [Serializable] 14 | public class NarrowcastArea : IValidatable 15 | { 16 | /// 17 | /// Maximum allowed age of a Narrowcast message, in days 18 | /// 19 | private const int FUTURE_TIME_WINDOW_DAYS = 365; 20 | 21 | /// 22 | /// Maximum allowed age of a Narrowcast message, in days 23 | /// 24 | private const int PAST_TIME_WINDOW_DAYS = 14; 25 | 26 | /// 27 | /// Start time of infection risk period 28 | /// 29 | /// 30 | /// Reported in milliseconds (ms) since the UNIX epoch. 31 | /// 32 | [JsonProperty("beginTimestampMs", Required = Required.Always)] 33 | public long BeginTimestamp { get; set; } 34 | /// 35 | /// End time of infection risk period 36 | /// 37 | /// 38 | /// Reported in milliseconds (ms) since the UNIX epoch. 39 | /// 40 | [JsonProperty("endTimestampMs", Required = Required.Always)] 41 | public long EndTimestamp { get; set; } 42 | /// 43 | /// Geographic coordinates of the center of the infection risk zone 44 | /// 45 | [JsonProperty("Location", Required = Required.Always)] 46 | public Coordinates Location { get; set; } 47 | /// 48 | /// Radius of infection risk area 49 | /// 50 | /// 51 | /// Reported in meters (m) 52 | /// 53 | [JsonProperty("radiusMeters", Required = Required.Always)] 54 | public float RadiusMeters { get; set; } 55 | 56 | /// 57 | public RequestValidationResult Validate() 58 | { 59 | RequestValidationResult result = new RequestValidationResult(); 60 | 61 | // Validate location 62 | if (this.Location == null) 63 | { 64 | result.Fail( 65 | RequestValidationIssue.InputNull, 66 | nameof(this.Location), 67 | ValidationMessages.NullLocation 68 | ); 69 | } 70 | else 71 | { 72 | // Validate using Coordinates.Validate() 73 | result.Combine(this.Location.Validate()); 74 | } 75 | 76 | // Validate timestamps 77 | result.Combine(Validator.ValidateTimestamp(this.BeginTimestamp, 78 | asOf: DateTimeOffset.UtcNow.AddDays(FUTURE_TIME_WINDOW_DAYS).ToUnixTimeMilliseconds(), 79 | maxAgeDays: FUTURE_TIME_WINDOW_DAYS + PAST_TIME_WINDOW_DAYS, 80 | parameterName: nameof(this.BeginTimestamp))); 81 | result.Combine(Validator.ValidateTimestamp(this.EndTimestamp, 82 | asOf: DateTimeOffset.UtcNow.AddDays(FUTURE_TIME_WINDOW_DAYS).ToUnixTimeMilliseconds(), 83 | maxAgeDays: FUTURE_TIME_WINDOW_DAYS + PAST_TIME_WINDOW_DAYS, 84 | parameterName: nameof(this.EndTimestamp))); 85 | result.Combine(Validator.ValidateTimeRange(this.BeginTimestamp, this.EndTimestamp)); 86 | 87 | return result; 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /CovidSafe/CovidSafe.DAL/Services/IMessageService.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | 5 | using CovidSafe.Entities.Geospatial; 6 | using CovidSafe.Entities.Messages; 7 | 8 | namespace CovidSafe.DAL.Services 9 | { 10 | /// 11 | /// service layer definition 12 | /// 13 | public interface IMessageService : IService 14 | { 15 | /// 16 | /// Retrieves a collection of objects by their unique identifiers 17 | /// 18 | /// Collection of identifiers 19 | /// Cancellation token 20 | /// Collection of objects 21 | Task> GetByIdsAsync(IEnumerable ids, CancellationToken cancellationToken = default); 22 | /// 23 | /// Retrieves the latest for a client 24 | /// 25 | /// Target 26 | /// Timestamp of latest client , in ms since UNIX epoch 27 | /// Cancellation token 28 | /// Collection of objects 29 | /// 30 | /// Sister call to . 31 | /// 32 | Task> GetLatestInfoAsync(Region region, long lastTimestamp, CancellationToken cancellationToken = default); 33 | /// 34 | /// Returns the latest size of data for a given 35 | /// 36 | /// Target 37 | /// Timestamp of latest client , in ms since UNIX epoch 38 | /// Cancellation token 39 | /// 40 | Task GetLatestRegionDataSizeAsync(Region region, long lastTimestamp, CancellationToken cancellationToken = default); 41 | /// 42 | /// Store a new based on a 43 | /// 44 | /// content 45 | /// Cancellation token 46 | /// Collection of published identifiers 47 | Task PublishAreaAsync(NarrowcastMessage areaMatch, CancellationToken cancellationToken = default); 48 | /// 49 | /// Store a new collection of objects 50 | /// 51 | /// Collection of objects to store 52 | /// Target of seeds 53 | /// Server timestamp when request was received, in ms since UNIX epoch (for calculating skew) 54 | /// Cancellation token 55 | /// Published identifier 56 | Task PublishAsync(IEnumerable seeds, Region region, long serverTimestamp, CancellationToken cancellationToken = default); 57 | } 58 | } -------------------------------------------------------------------------------- /CovidSafe/CovidSafe.Entities/Messages/MessageContainer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | using CovidSafe.Entities.Validation; 5 | using CovidSafe.Entities.Validation.Resources; 6 | using Newtonsoft.Json; 7 | 8 | namespace CovidSafe.Entities.Messages 9 | { 10 | /// 11 | /// Wraps all message-related types into a single, reportable object which enables mixed messages in a single result 12 | /// 13 | [JsonObject(MemberSerialization = MemberSerialization.OptIn)] 14 | [Serializable] 15 | public class MessageContainer : IValidatable 16 | { 17 | /// 18 | /// BluetoothMatchMessage backing property 19 | /// 20 | [NonSerialized] 21 | private string _bluetoothMatchMessage; 22 | /// 23 | /// Message displayed to user if there is a Bluetooth match 24 | /// 25 | [JsonProperty("bluetoothMatchMessage", NullValueHandling = NullValueHandling.Ignore)] 26 | public string BluetoothMatchMessage 27 | { 28 | get { return this._bluetoothMatchMessage; } 29 | set { this._bluetoothMatchMessage = value; } 30 | } 31 | /// 32 | /// Bluetooth Seed-based infection reports 33 | /// 34 | [JsonProperty("Seeds", NullValueHandling = NullValueHandling.Ignore)] 35 | public IList BluetoothSeeds { get; set; } = new List(); 36 | /// 37 | /// BooleanExpression backing property 38 | /// 39 | [NonSerialized] 40 | private string _booleanExpression; 41 | /// 42 | /// Reserved for later use 43 | /// 44 | [JsonProperty("booleanExpression", NullValueHandling = NullValueHandling.Ignore)] 45 | public string BooleanExpression 46 | { 47 | get { return this._booleanExpression; } 48 | set { this._booleanExpression = value; } 49 | } 50 | /// 51 | /// collection 52 | /// 53 | [JsonProperty("Narrowcasts", NullValueHandling = NullValueHandling.Ignore)] 54 | public IList Narrowcasts { get; set; } = new List(); 55 | 56 | /// 57 | public RequestValidationResult Validate() 58 | { 59 | RequestValidationResult result = new RequestValidationResult(); 60 | 61 | // Must contain at least one of either BluetoothSeeds or NarrowcastMessages 62 | if (this.BluetoothSeeds.Count == 0 && this.Narrowcasts.Count == 0) 63 | { 64 | result.Fail( 65 | RequestValidationIssue.InputEmpty, 66 | RequestValidationProperty.Multiple, 67 | ValidationMessages.EmptyMessage 68 | ); 69 | } 70 | 71 | // Validate individual messages 72 | if (this.BluetoothSeeds.Count > 0) 73 | { 74 | // Validate individual Bluetooth matches 75 | foreach (BluetoothSeedMessage seed in this.BluetoothSeeds) 76 | { 77 | // Use BluetoothSeed.Validate() 78 | result.Combine(seed.Validate()); 79 | } 80 | } 81 | if (this.Narrowcasts.Count > 0) 82 | { 83 | // Validate individual area matches 84 | foreach (NarrowcastMessage message in this.Narrowcasts) 85 | { 86 | // Use NarrowcastMessage.Validate() 87 | result.Combine(message.Validate()); 88 | } 89 | } 90 | 91 | return result; 92 | } 93 | } 94 | } -------------------------------------------------------------------------------- /CovidSafe/CovidSafe.Entities/Geospatial/Region.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | using CovidSafe.Entities.Validation; 4 | using CovidSafe.Entities.Validation.Resources; 5 | using Newtonsoft.Json; 6 | 7 | namespace CovidSafe.Entities.Geospatial 8 | { 9 | /// 10 | /// Geographic region 11 | /// 12 | [JsonObject(MemberSerialization = MemberSerialization.OptIn)] 13 | [Serializable] 14 | public class Region : IValidatable 15 | { 16 | /// 17 | /// Maximum allowed precision value; 18 | /// 19 | public const int MAX_PRECISION = 8; 20 | /// 21 | /// Minimum allowed precision value 22 | /// 23 | public const int MIN_PRECISION = 0; 24 | /// 25 | /// Latitude prefix of this 26 | /// 27 | /// 28 | /// A region lat/lng should not have decimals, to avoid unmasking a user 29 | /// while getting the necessary amount of information to provide 30 | /// accurate messaging. 31 | /// 32 | [JsonProperty("latPrefix", Required = Required.Always)] 33 | public int LatitudePrefix { get; set; } 34 | /// 35 | /// Longitude prefix of this 36 | /// 37 | /// 38 | /// A region lat/lng should not have decimals, to avoid unmasking a user 39 | /// while getting the necessary amount of information to provide 40 | /// accurate messaging. 41 | /// 42 | [JsonProperty("lngPrefix", Required = Required.Always)] 43 | public int LongitudePrefix { get; set; } 44 | /// 45 | /// Precision of this 46 | /// 47 | /// 48 | /// Used as a mantissa mask. The maximum available precision value is 49 | /// 9, which should be assumed in most cases when a direct latitude or 50 | /// longitude is provided. 51 | /// 52 | [JsonProperty("precision", Required = Required.Always)] 53 | public int Precision { get; set; } = MAX_PRECISION; 54 | 55 | /// 56 | /// Default constructor 57 | /// 58 | public Region() { } 59 | 60 | /// 61 | /// Creates a new 62 | /// 63 | /// Latitude prefix 64 | /// Longitude prefix 65 | /// Precision value 66 | public Region(int latPrefix, int lngPrefix, int precision = MAX_PRECISION) 67 | { 68 | this.LatitudePrefix = latPrefix; 69 | this.LongitudePrefix = lngPrefix; 70 | this.Precision = precision; 71 | } 72 | 73 | /// 74 | public RequestValidationResult Validate() 75 | { 76 | RequestValidationResult result = new RequestValidationResult(); 77 | 78 | // Check if lat/lng are in expected values 79 | result.Combine(Validator.ValidateLatitude(this.LatitudePrefix, nameof(this.LatitudePrefix))); 80 | result.Combine(Validator.ValidateLongitude(this.LongitudePrefix, nameof(this.LongitudePrefix))); 81 | 82 | // Validate precision 83 | if (this.Precision < MIN_PRECISION || this.Precision > MAX_PRECISION) 84 | { 85 | result.Fail( 86 | RequestValidationIssue.InputInvalid, 87 | nameof(this.Precision), 88 | ValidationMessages.InvalidPrecision, 89 | this.Precision.ToString(), 90 | MIN_PRECISION.ToString(), 91 | MAX_PRECISION.ToString() 92 | ); 93 | } 94 | 95 | return result; 96 | } 97 | } 98 | } -------------------------------------------------------------------------------- /CovidSafe/CovidSafe.API/v20200505/Controllers/MessageControllers/AreaReportController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel.DataAnnotations; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | 6 | using AutoMapper; 7 | using CovidSafe.API.v20200505.Protos; 8 | using CovidSafe.DAL.Services; 9 | using CovidSafe.Entities.Messages; 10 | using CovidSafe.Entities.Validation; 11 | using Microsoft.AspNetCore.Http; 12 | using Microsoft.AspNetCore.Mvc; 13 | 14 | namespace CovidSafe.API.v20200505.Controllers.MessageControllers 15 | { 16 | /// 17 | /// Handles requests for submitting messages 18 | /// 19 | [ApiController] 20 | [ApiVersion("2020-05-05", Deprecated = true)] 21 | [Route("api/Messages/[controller]")] 22 | public class AreaReportController : ControllerBase 23 | { 24 | /// 25 | /// AutoMapper instance for object resolution 26 | /// 27 | private readonly IMapper _map; 28 | /// 29 | /// service layer 30 | /// 31 | private readonly IMessageService _reportService; 32 | 33 | /// 34 | /// Creates a new instance 35 | /// 36 | /// AutoMapper instance 37 | /// service layer 38 | public AreaReportController(IMapper map, IMessageService reportService) 39 | { 40 | // Assign local values 41 | this._map = map; 42 | this._reportService = reportService; 43 | } 44 | 45 | /// 46 | /// Publish a for distribution among devices 47 | /// 48 | /// 49 | /// Sample request: 50 | /// 51 | /// PUT /api/Messages/AreaReport&api-version=2020-05-05 52 | /// { 53 | /// "userMessage": "Monitor symptoms for one week.", 54 | /// "areas": [{ 55 | /// "location": { 56 | /// "latitude": 74.12345, 57 | /// "longitude": -39.12345 58 | /// }, 59 | /// "radiusMeters": 100, 60 | /// "beginTime": 1586083599, 61 | /// "endTime": 1586085189 62 | /// }] 63 | /// } 64 | /// 65 | /// 66 | /// to be stored 67 | /// Cancellation token (not required in API call) 68 | /// Submission successful 69 | /// Malformed or invalid request 70 | [HttpPut] 71 | [Consumes("application/x-protobuf", "application/json")] 72 | [Produces("application/x-protobuf", "application/json")] 73 | [ProducesResponseType(StatusCodes.Status200OK)] 74 | [ProducesResponseType(typeof(ValidationResult), StatusCodes.Status400BadRequest)] 75 | [ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)] 76 | public async Task PutAsync([Required] AreaMatch request, CancellationToken cancellationToken = default) 77 | { 78 | try 79 | { 80 | // Parse AreaMatch to AreaReport type 81 | NarrowcastMessage report = this._map.Map(request); 82 | 83 | // Publish area 84 | await this._reportService.PublishAreaAsync(report, cancellationToken); 85 | return Ok(); 86 | } 87 | catch (RequestValidationFailedException ex) 88 | { 89 | // Only return validation issues 90 | return BadRequest(ex.ValidationResult); 91 | } 92 | catch (ArgumentNullException) 93 | { 94 | return BadRequest(); 95 | } 96 | } 97 | } 98 | } -------------------------------------------------------------------------------- /CovidSafe/CovidSafe.API/v20200415/Controllers/MessageControllers/AreaReportController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel.DataAnnotations; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | 6 | using AutoMapper; 7 | using CovidSafe.API.v20200415.Protos; 8 | using CovidSafe.DAL.Services; 9 | using CovidSafe.Entities.Messages; 10 | using CovidSafe.Entities.Validation; 11 | using Microsoft.AspNetCore.Http; 12 | using Microsoft.AspNetCore.Mvc; 13 | 14 | namespace CovidSafe.API.v20200415.Controllers.MessageControllers 15 | { 16 | /// 17 | /// Handles requests for infected clients volunteering identifiers 18 | /// 19 | [ApiController] 20 | [ApiVersion("2020-04-15", Deprecated = true)] 21 | [Route("api/Messages/[controller]")] 22 | public class AreaReportController : ControllerBase 23 | { 24 | /// 25 | /// AutoMapper instance for object resolution 26 | /// 27 | private readonly IMapper _map; 28 | /// 29 | /// service layer 30 | /// 31 | private IMessageService _reportService; 32 | 33 | /// 34 | /// Creates a new instance 35 | /// 36 | /// AutoMapper instance 37 | /// service layer 38 | public AreaReportController(IMapper map, IMessageService reportService) 39 | { 40 | // Assign local values 41 | this._map = map; 42 | this._reportService = reportService; 43 | } 44 | 45 | /// 46 | /// Publish a for distribution among devices 47 | /// 48 | /// 49 | /// Sample request: 50 | /// 51 | /// PUT /api/Messages/AreaReport&api-version=2020-04-15 52 | /// { 53 | /// "userMessage": "Monitor symptoms for one week.", 54 | /// "areas": [{ 55 | /// "location": { 56 | /// "latitude": 74.12345, 57 | /// "longitude": -39.12345 58 | /// }, 59 | /// "radiusMeters": 100, 60 | /// "beginTime": 1586083599, 61 | /// "endTime": 1586085189 62 | /// }] 63 | /// } 64 | /// 65 | /// 66 | /// to be stored 67 | /// Cancellation token (not required in API call) 68 | /// Submission successful 69 | /// Malformed or invalid request 70 | [HttpPut] 71 | [Consumes("application/x-protobuf", "application/json")] 72 | [Produces("application/x-protobuf", "application/json")] 73 | [ProducesResponseType(StatusCodes.Status200OK)] 74 | [ProducesResponseType(typeof(ValidationResult), StatusCodes.Status400BadRequest)] 75 | [ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)] 76 | public async Task PutAsync([Required] AreaMatch request, CancellationToken cancellationToken = default) 77 | { 78 | try 79 | { 80 | // Parse AreaMatch to AreaReport type 81 | NarrowcastMessage report = this._map.Map(request); 82 | 83 | // Publish area 84 | await this._reportService.PublishAreaAsync(report, cancellationToken); 85 | return Ok(); 86 | } 87 | catch (RequestValidationFailedException ex) 88 | { 89 | // Only return validation issues 90 | return BadRequest(ex.ValidationResult); 91 | } 92 | catch (ArgumentNullException) 93 | { 94 | return BadRequest(); 95 | } 96 | } 97 | } 98 | } -------------------------------------------------------------------------------- /CovidSafe/CovidSafe.API/v20200611/Controllers/MessageControllers/AnnounceController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel.DataAnnotations; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | 6 | using AutoMapper; 7 | using CovidSafe.API.v20200611.Protos; 8 | using CovidSafe.DAL.Services; 9 | using CovidSafe.Entities.Messages; 10 | using CovidSafe.Entities.Validation; 11 | using Microsoft.AspNetCore.Http; 12 | using Microsoft.AspNetCore.Mvc; 13 | 14 | namespace CovidSafe.API.v20200611.Controllers.MessageControllers 15 | { 16 | /// 17 | /// Handles announcements 18 | /// 19 | [ApiController] 20 | [ApiVersion("2020-06-11")] 21 | [Route("api/Messages/[controller]")] 22 | public class AnnounceController : ControllerBase 23 | { 24 | /// 25 | /// AutoMapper instance for object resolution 26 | /// 27 | private readonly IMapper _map; 28 | /// 29 | /// service layer 30 | /// 31 | private readonly IMessageService _reportService; 32 | 33 | /// 34 | /// Creates a new instance 35 | /// 36 | /// AutoMapper instance 37 | /// service layer 38 | public AnnounceController(IMapper map, IMessageService reportService) 39 | { 40 | // Assign local values 41 | this._map = map; 42 | this._reportService = reportService; 43 | } 44 | 45 | /// 46 | /// Publish a for distribution among devices 47 | /// 48 | /// 49 | /// Sample request: 50 | /// 51 | /// PUT /api/Messages/Announce&api-version=2020-06-11 52 | /// { 53 | /// "userMessage": "Monitor symptoms for one week.", 54 | /// "area": { 55 | /// "location": { 56 | /// "latitude": 74.12345, 57 | /// "longitude": -39.12345 58 | /// }, 59 | /// "radiusMeters": 100, 60 | /// "beginTime": 1591997285105, 61 | /// "endTime": 1591997385105 62 | /// } 63 | /// } 64 | /// 65 | /// 66 | /// to be stored 67 | /// Cancellation token (not required in API call) 68 | /// Submission successful 69 | /// Malformed or invalid request 70 | [HttpPut] 71 | [Consumes("application/x-protobuf", "application/json")] 72 | [Produces("application/x-protobuf", "application/json")] 73 | [ProducesResponseType(StatusCodes.Status200OK)] 74 | [ProducesResponseType(typeof(ValidationResult), StatusCodes.Status400BadRequest)] 75 | [ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)] 76 | public async Task PutAsync([Required] Protos.NarrowcastMessage request, CancellationToken cancellationToken = default) 77 | { 78 | try 79 | { 80 | // Parse AreaMatch to AreaReport type 81 | Entities.Messages.NarrowcastMessage report = this._map.Map(request); 82 | 83 | // Publish area 84 | await this._reportService.PublishAreaAsync(report, cancellationToken); 85 | return Ok(); 86 | } 87 | catch (RequestValidationFailedException ex) 88 | { 89 | // Only return validation issues 90 | return BadRequest(ex.ValidationResult); 91 | } 92 | catch (ArgumentNullException) 93 | { 94 | return BadRequest(); 95 | } 96 | } 97 | } 98 | } -------------------------------------------------------------------------------- /CovidSafe/CovidSafe.API/v20200611/MappingProfiles.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Diagnostics; 3 | using System.IO.Compression; 4 | using System.Linq; 5 | 6 | using AutoMapper; 7 | using CovidSafe.API.v20200611.Protos; 8 | using CovidSafe.Entities.Geospatial; 9 | using CovidSafe.Entities.Messages; 10 | 11 | namespace CovidSafe.API.v20200611 12 | { 13 | /// 14 | /// Maps proto types to their internal database representations 15 | /// 16 | public class MappingProfiles : Profile 17 | { 18 | /// 19 | /// Creates a new instance 20 | /// 21 | public MappingProfiles() 22 | { 23 | // Location -> Coordinates 24 | CreateMap() 25 | // Properties have the same name+type 26 | .ReverseMap(); 27 | 28 | // Region -> Region 29 | CreateMap() 30 | // Properties have the same name+type 31 | .ReverseMap(); 32 | 33 | // MessageInfo -> MessageContainerMetadata 34 | CreateMap() 35 | .ForMember( 36 | im => im.Id, 37 | op => op.MapFrom(mi => mi.MessageId) 38 | ) 39 | .ForMember( 40 | im => im.Timestamp, 41 | op => op.MapFrom(mi => mi.MessageTimestamp) 42 | ) 43 | .ReverseMap(); 44 | 45 | // IEnumerable -> MessageListResponse 46 | // This is a one-way response so no ReverseMap is necessary 47 | CreateMap, MessageListResponse>() 48 | .ForMember( 49 | mr => mr.MessageInfoes, 50 | op => op.MapFrom(im => im) 51 | ) 52 | .ForMember( 53 | mr => mr.MaxResponseTimestamp, 54 | op => op.MapFrom(im => im.Count() > 0 ? im.Max(o => o.Timestamp) : 0) 55 | ); 56 | 57 | // Area -> NarrowcastArea 58 | CreateMap() 59 | .ForMember( 60 | ia => ia.BeginTimestamp, 61 | op => op.MapFrom(a => a.BeginTime) 62 | ) 63 | .ForMember( 64 | ia => ia.EndTimestamp, 65 | op => op.MapFrom(a => a.EndTime) 66 | ) 67 | .ForMember( 68 | ia => ia.Location, 69 | op => op.MapFrom(a => a.Location) 70 | ) 71 | .ForMember( 72 | ia => ia.RadiusMeters, 73 | op => op.MapFrom(a => a.RadiusMeters) 74 | ) 75 | .ReverseMap(); 76 | 77 | // MessageResponse -> MessageContainer 78 | CreateMap() 79 | .ForMember( 80 | ir => ir.Narrowcasts, 81 | op => op.MapFrom(mm => mm.NarrowcastMessages) 82 | ) 83 | .ForMember( 84 | // Not supported in v20200611+ 85 | ir => ir.BluetoothSeeds, 86 | op => op.Ignore() 87 | ) 88 | .ForMember( 89 | // Not supported in v20200611+ 90 | ir => ir.BooleanExpression, 91 | op => op.Ignore() 92 | ) 93 | .ForMember( 94 | // Not supported in v20200415+ 95 | ir => ir.BluetoothMatchMessage, 96 | op => op.Ignore() 97 | ) 98 | // Other properties have the same name+type 99 | .ReverseMap(); 100 | 101 | // NarrowcastMessage -> NarrowcastMessage 102 | CreateMap() 103 | .ForMember( 104 | src => src.Area, 105 | op => op.MapFrom(dst => dst.Area) 106 | ) 107 | .ForMember( 108 | src => src.UserMessage, 109 | op => op.MapFrom(dst => dst.UserMessage) 110 | ) 111 | .ReverseMap(); 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /CovidSafe/CovidSafe.API/v20200415/Controllers/MessageControllers/SeedReportController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | 7 | using AutoMapper; 8 | using CovidSafe.API.v20200415.Protos; 9 | using CovidSafe.DAL.Services; 10 | using CovidSafe.Entities.Messages; 11 | using CovidSafe.Entities.Validation; 12 | using Microsoft.AspNetCore.Http; 13 | using Microsoft.AspNetCore.Mvc; 14 | 15 | namespace CovidSafe.API.v20200415.Controllers.MessageControllers 16 | { 17 | /// 18 | /// Handles requests for infected clients volunteering identifiers 19 | /// 20 | [ApiController] 21 | [ApiVersion("2020-04-15", Deprecated = true)] 22 | [Route("api/Messages/[controller]")] 23 | public class SeedReportController : ControllerBase 24 | { 25 | /// 26 | /// AutoMapper instance for object resolution 27 | /// 28 | private readonly IMapper _map; 29 | /// 30 | /// service layer 31 | /// 32 | private readonly IMessageService _reportService; 33 | 34 | /// 35 | /// Creates a new instance 36 | /// 37 | /// AutoMapper instance 38 | /// service layer 39 | public SeedReportController(IMapper map, IMessageService reportService) 40 | { 41 | // Assign local values 42 | this._map = map; 43 | this._reportService = reportService; 44 | } 45 | 46 | /// 47 | /// Publish a for distribution among devices relevant to 48 | /// 49 | /// 50 | /// 51 | /// Sample request: 52 | /// 53 | /// PUT /api/Messages/SeedReport&api-version=2020-04-15 54 | /// { 55 | /// "seeds": [{ 56 | /// "seed": "00000000-0000-0000-0000-000000000001", 57 | /// "sequenceStartTime": 1586406048649, 58 | /// "sequenceEndTime": 1586408048649 59 | /// }], 60 | /// "region": { 61 | /// "latitudePrefix": 74.12, 62 | /// "longitudePrefix": -39.12, 63 | /// "precision": 2 64 | /// } 65 | /// } 66 | /// 67 | /// 68 | /// content 69 | /// Cancellation token (not required in API call) 70 | /// Submission successful 71 | /// Malformed or invalid request 72 | [HttpPut] 73 | [Consumes("application/x-protobuf", "application/json")] 74 | [Produces("application/x-protobuf", "application/json")] 75 | [ProducesResponseType(StatusCodes.Status200OK)] 76 | [ProducesResponseType(typeof(RequestValidationResult), StatusCodes.Status400BadRequest)] 77 | [ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)] 78 | public async Task PutAsync(SelfReportRequest request, CancellationToken cancellationToken = default) 79 | { 80 | // Get server timestamp at request immediately 81 | long serverTimestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); 82 | 83 | try 84 | { 85 | // Parse request 86 | Entities.Geospatial.Region region = this._map.Map(request.Region); 87 | IEnumerable seeds = request.Seeds 88 | .Select(s => this._map.Map(s)); 89 | 90 | // Store submitted data 91 | await this._reportService.PublishAsync(seeds, region, serverTimestamp, cancellationToken); 92 | 93 | return Ok(); 94 | } 95 | catch (RequestValidationFailedException ex) 96 | { 97 | // Only return validation results 98 | return BadRequest(ex.ValidationResult); 99 | } 100 | catch (ArgumentNullException) 101 | { 102 | return BadRequest(); 103 | } 104 | } 105 | } 106 | } -------------------------------------------------------------------------------- /CovidSafe/CovidSafe.API/v20200505/Controllers/MessageControllers/SeedReportController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | 7 | using AutoMapper; 8 | using CovidSafe.API.v20200505.Protos; 9 | using CovidSafe.DAL.Services; 10 | using CovidSafe.Entities.Messages; 11 | using CovidSafe.Entities.Validation; 12 | using Microsoft.AspNetCore.Http; 13 | using Microsoft.AspNetCore.Mvc; 14 | 15 | namespace CovidSafe.API.v20200505.Controllers.MessageControllers 16 | { 17 | /// 18 | /// Handles requests for infected clients volunteering identifiers 19 | /// 20 | [ApiController] 21 | [ApiVersion("2020-05-05", Deprecated = true)] 22 | [Route("api/Messages/[controller]")] 23 | public class SeedReportController : ControllerBase 24 | { 25 | /// 26 | /// AutoMapper instance for object resolution 27 | /// 28 | private readonly IMapper _map; 29 | /// 30 | /// service layer 31 | /// 32 | private readonly IMessageService _reportService; 33 | 34 | /// 35 | /// Creates a new instance 36 | /// 37 | /// AutoMapper instance 38 | /// service layer 39 | public SeedReportController(IMapper map, IMessageService reportService) 40 | { 41 | // Assign local values 42 | this._map = map; 43 | this._reportService = reportService; 44 | } 45 | 46 | /// 47 | /// Publish a for distribution among devices relevant to 48 | /// 49 | /// 50 | /// 51 | /// Sample request: 52 | /// 53 | /// PUT /api/Messages/SeedReport&api-version=2020-05-05 54 | /// { 55 | /// "seeds": [{ 56 | /// "seed": "00000000-0000-0000-0000-000000000001", 57 | /// "sequenceStartTime": 1586406048649, 58 | /// "sequenceEndTime": 1586408048649 59 | /// }], 60 | /// "region": { 61 | /// "latitudePrefix": 74.12, 62 | /// "longitudePrefix": -39.12, 63 | /// "precision": 2 64 | /// } 65 | /// } 66 | /// 67 | /// 68 | /// content 69 | /// Cancellation token (not required in API call) 70 | /// Submission successful 71 | /// Malformed or invalid request 72 | [HttpPut] 73 | [Consumes("application/x-protobuf", "application/json")] 74 | [Produces("application/x-protobuf", "application/json")] 75 | [ProducesResponseType(StatusCodes.Status200OK)] 76 | [ProducesResponseType(typeof(RequestValidationResult), StatusCodes.Status400BadRequest)] 77 | [ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)] 78 | public async Task PutAsync(SelfReportRequest request, CancellationToken cancellationToken = default) 79 | { 80 | // Get server timestamp at request immediately 81 | long serverTimestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); 82 | 83 | try 84 | { 85 | // Parse request 86 | Entities.Geospatial.Region region = this._map.Map(request.Region); 87 | IEnumerable seeds = request.Seeds 88 | .Select(s => this._map.Map(s)); 89 | 90 | // Store submitted data 91 | await this._reportService.PublishAsync(seeds, region, serverTimestamp, cancellationToken); 92 | 93 | return Ok(); 94 | } 95 | catch (RequestValidationFailedException ex) 96 | { 97 | // Only return validation results 98 | return BadRequest(ex.ValidationResult); 99 | } 100 | catch (ArgumentNullException) 101 | { 102 | return BadRequest(); 103 | } 104 | } 105 | } 106 | } -------------------------------------------------------------------------------- /CovidSafe/CovidSafe.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.29911.84 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CovidSafe.API", "CovidSafe.API\CovidSafe.API.csproj", "{41649BC0-BBC9-4A47-872B-9E0DDC7678E5}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CovidSafe.Entities", "CovidSafe.Entities\CovidSafe.Entities.csproj", "{1142E516-7A78-413C-A98F-4A8DC22CE974}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CovidSafe.DAL", "CovidSafe.DAL\CovidSafe.DAL.csproj", "{AB7BA3D0-9A41-4D7C-AAAA-66B8BE10C2ED}" 11 | EndProject 12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CovidSafe.DAL.Tests", "CovidSafe.DAL.Tests\CovidSafe.DAL.Tests.csproj", "{CF8D6EDC-E318-4B47-AA82-F0994AA0718A}" 13 | EndProject 14 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{5323748A-8DB7-446E-947B-71D92A0F752E}" 15 | ProjectSection(SolutionItems) = preProject 16 | .runsettings = .runsettings 17 | Local.testsettings = Local.testsettings 18 | ..\README.md = ..\README.md 19 | EndProjectSection 20 | EndProject 21 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CovidSafe.API.Tests", "CovidSafe.API.Tests\CovidSafe.API.Tests.csproj", "{18CBD5A5-A9E6-4F7E-A3A6-ABB30AAD50E5}" 22 | EndProject 23 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CovidSafe.Entities.Tests", "CovidSafe.Entities.Tests\CovidSafe.Entities.Tests.csproj", "{143B3AB2-7F1A-4901-BF1D-A1BA7DE2EDFC}" 24 | EndProject 25 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CovidSafe.API.Tests.Performance", "CovidSafe.API.Tests.Performance\CovidSafe.API.Tests.Performance.csproj", "{B5F256B9-C0DC-4BA6-9C60-7BCF8E13F15A}" 26 | EndProject 27 | Global 28 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 29 | Debug|Any CPU = Debug|Any CPU 30 | Release|Any CPU = Release|Any CPU 31 | EndGlobalSection 32 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 33 | {41649BC0-BBC9-4A47-872B-9E0DDC7678E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 34 | {41649BC0-BBC9-4A47-872B-9E0DDC7678E5}.Debug|Any CPU.Build.0 = Debug|Any CPU 35 | {41649BC0-BBC9-4A47-872B-9E0DDC7678E5}.Release|Any CPU.ActiveCfg = Release|Any CPU 36 | {41649BC0-BBC9-4A47-872B-9E0DDC7678E5}.Release|Any CPU.Build.0 = Release|Any CPU 37 | {1142E516-7A78-413C-A98F-4A8DC22CE974}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 38 | {1142E516-7A78-413C-A98F-4A8DC22CE974}.Debug|Any CPU.Build.0 = Debug|Any CPU 39 | {1142E516-7A78-413C-A98F-4A8DC22CE974}.Release|Any CPU.ActiveCfg = Release|Any CPU 40 | {1142E516-7A78-413C-A98F-4A8DC22CE974}.Release|Any CPU.Build.0 = Release|Any CPU 41 | {AB7BA3D0-9A41-4D7C-AAAA-66B8BE10C2ED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 42 | {AB7BA3D0-9A41-4D7C-AAAA-66B8BE10C2ED}.Debug|Any CPU.Build.0 = Debug|Any CPU 43 | {AB7BA3D0-9A41-4D7C-AAAA-66B8BE10C2ED}.Release|Any CPU.ActiveCfg = Release|Any CPU 44 | {AB7BA3D0-9A41-4D7C-AAAA-66B8BE10C2ED}.Release|Any CPU.Build.0 = Release|Any CPU 45 | {CF8D6EDC-E318-4B47-AA82-F0994AA0718A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 46 | {CF8D6EDC-E318-4B47-AA82-F0994AA0718A}.Debug|Any CPU.Build.0 = Debug|Any CPU 47 | {CF8D6EDC-E318-4B47-AA82-F0994AA0718A}.Release|Any CPU.ActiveCfg = Release|Any CPU 48 | {CF8D6EDC-E318-4B47-AA82-F0994AA0718A}.Release|Any CPU.Build.0 = Release|Any CPU 49 | {18CBD5A5-A9E6-4F7E-A3A6-ABB30AAD50E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 50 | {18CBD5A5-A9E6-4F7E-A3A6-ABB30AAD50E5}.Debug|Any CPU.Build.0 = Debug|Any CPU 51 | {18CBD5A5-A9E6-4F7E-A3A6-ABB30AAD50E5}.Release|Any CPU.ActiveCfg = Release|Any CPU 52 | {18CBD5A5-A9E6-4F7E-A3A6-ABB30AAD50E5}.Release|Any CPU.Build.0 = Release|Any CPU 53 | {143B3AB2-7F1A-4901-BF1D-A1BA7DE2EDFC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 54 | {143B3AB2-7F1A-4901-BF1D-A1BA7DE2EDFC}.Debug|Any CPU.Build.0 = Debug|Any CPU 55 | {143B3AB2-7F1A-4901-BF1D-A1BA7DE2EDFC}.Release|Any CPU.ActiveCfg = Release|Any CPU 56 | {143B3AB2-7F1A-4901-BF1D-A1BA7DE2EDFC}.Release|Any CPU.Build.0 = Release|Any CPU 57 | {B5F256B9-C0DC-4BA6-9C60-7BCF8E13F15A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 58 | {B5F256B9-C0DC-4BA6-9C60-7BCF8E13F15A}.Debug|Any CPU.Build.0 = Debug|Any CPU 59 | {B5F256B9-C0DC-4BA6-9C60-7BCF8E13F15A}.Release|Any CPU.ActiveCfg = Release|Any CPU 60 | {B5F256B9-C0DC-4BA6-9C60-7BCF8E13F15A}.Release|Any CPU.Build.0 = Release|Any CPU 61 | EndGlobalSection 62 | GlobalSection(SolutionProperties) = preSolution 63 | HideSolutionNode = FALSE 64 | EndGlobalSection 65 | GlobalSection(ExtensibilityGlobals) = postSolution 66 | SolutionGuid = {C7AD4C91-E2D7-4192-B2C5-25E1A04FB7FD} 67 | EndGlobalSection 68 | EndGlobal 69 | -------------------------------------------------------------------------------- /CovidSafe/CovidSafe.API.Tests.Performance/CovidSafe.API.Tests.Performance.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | Debug 4 | AnyCPU 5 | 6 | 7 | 2.0 8 | {B5F256B9-C0DC-4BA6-9C60-7BCF8E13F15A} 9 | Library 10 | Properties 11 | CovidSafe.API.WebPerformanceAndLoadTests 12 | CovidSafe.API.WebPerformanceAndLoadTests 13 | v4.7.2 14 | 512 15 | {3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} 16 | WebTest 17 | 10.0 18 | $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) 19 | $(ProgramFiles)\Common Files\microsoft shared\VSTT\$(VisualStudioVersion)\UITestExtensionPackages 20 | False 21 | 22 | 23 | true 24 | full 25 | false 26 | bin\Debug\ 27 | DEBUG;TRACE 28 | prompt 29 | 4 30 | 31 | 32 | pdbonly 33 | true 34 | bin\Release\ 35 | TRACE 36 | prompt 37 | 4 38 | 39 | 40 | 41 | False 42 | 43 | 44 | 45 | 46 | 47 | 48 | False 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | Always 57 | 58 | 59 | PreserveNewest 60 | 61 | 62 | 63 | 64 | 65 | 66 | False 67 | 68 | 69 | False 70 | 71 | 72 | False 73 | 74 | 75 | False 76 | 77 | 78 | 79 | 80 | 81 | 82 | 89 | -------------------------------------------------------------------------------- /CovidSafe/CovidSafe.DAL.Tests/Helpers/PrecisionHelperTests.cs: -------------------------------------------------------------------------------- 1 | using CovidSafe.DAL.Helpers; 2 | using Microsoft.VisualStudio.TestTools.UnitTesting; 3 | using System; 4 | 5 | namespace CovidSafe.Tests.Helpers 6 | { 7 | [TestClass] 8 | public class PrecisionHelperTests 9 | { 10 | [TestMethod] 11 | public void RoundTest() 12 | { 13 | Assert.AreEqual(0, PrecisionHelper.Round(0, 0)); 14 | Assert.AreEqual(0, PrecisionHelper.Round(0, 1)); 15 | Assert.AreEqual(0, PrecisionHelper.Round(0, 2)); 16 | Assert.AreEqual(0, PrecisionHelper.Round(0, 3)); 17 | Assert.AreEqual(0, PrecisionHelper.Round(0, 4)); 18 | Assert.AreEqual(0, PrecisionHelper.Round(0, 5)); 19 | Assert.AreEqual(0, PrecisionHelper.Round(0, 6)); 20 | Assert.AreEqual(0, PrecisionHelper.Round(0, 7)); 21 | Assert.AreEqual(0, PrecisionHelper.Round(0, 8)); 22 | 23 | Assert.AreEqual(0, PrecisionHelper.Round(1, 0)); 24 | Assert.AreEqual(0, PrecisionHelper.Round(1, 1)); 25 | Assert.AreEqual(0, PrecisionHelper.Round(1, 2)); 26 | Assert.AreEqual(0, PrecisionHelper.Round(1, 3)); 27 | Assert.AreEqual(0, PrecisionHelper.Round(1, 4)); 28 | Assert.AreEqual(0, PrecisionHelper.Round(1, 5)); 29 | Assert.AreEqual(0, PrecisionHelper.Round(1, 6)); 30 | Assert.AreEqual(0, PrecisionHelper.Round(1, 7)); 31 | Assert.AreEqual(1, PrecisionHelper.Round(1, 8)); 32 | 33 | Assert.AreEqual(0, PrecisionHelper.Round(-1, 0)); 34 | Assert.AreEqual(0, PrecisionHelper.Round(-1, 1)); 35 | Assert.AreEqual(0, PrecisionHelper.Round(-1, 2)); 36 | Assert.AreEqual(0, PrecisionHelper.Round(-1, 3)); 37 | Assert.AreEqual(0, PrecisionHelper.Round(-1, 4)); 38 | Assert.AreEqual(0, PrecisionHelper.Round(-1, 5)); 39 | Assert.AreEqual(0, PrecisionHelper.Round(-1, 6)); 40 | Assert.AreEqual(0, PrecisionHelper.Round(-1, 7)); 41 | Assert.AreEqual(-1, PrecisionHelper.Round(-1, 8)); 42 | 43 | Assert.AreEqual(0, PrecisionHelper.Round(37, 0)); 44 | Assert.AreEqual(0, PrecisionHelper.Round(37, 1)); 45 | Assert.AreEqual(0, PrecisionHelper.Round(37, 2)); 46 | Assert.AreEqual(32, PrecisionHelper.Round(37, 3)); 47 | Assert.AreEqual(32, PrecisionHelper.Round(37, 4)); 48 | Assert.AreEqual(32, PrecisionHelper.Round(37, 5)); 49 | Assert.AreEqual(36, PrecisionHelper.Round(37, 6)); 50 | Assert.AreEqual(36, PrecisionHelper.Round(37, 7)); 51 | Assert.AreEqual(37, PrecisionHelper.Round(37, 8)); 52 | 53 | Assert.AreEqual(0, PrecisionHelper.Round(-37, 0)); 54 | Assert.AreEqual(0, PrecisionHelper.Round(-37, 1)); 55 | Assert.AreEqual(0, PrecisionHelper.Round(-37, 2)); 56 | Assert.AreEqual(-32, PrecisionHelper.Round(-37, 3)); 57 | Assert.AreEqual(-32, PrecisionHelper.Round(-37, 4)); 58 | Assert.AreEqual(-32, PrecisionHelper.Round(-37, 5)); 59 | Assert.AreEqual(-36, PrecisionHelper.Round(-37, 6)); 60 | Assert.AreEqual(-36, PrecisionHelper.Round(-37, 7)); 61 | Assert.AreEqual(-37, PrecisionHelper.Round(-37, 8)); 62 | 63 | Assert.AreEqual(0, PrecisionHelper.Round(179.999, 0)); 64 | Assert.AreEqual(128, PrecisionHelper.Round(179.999, 1)); 65 | Assert.AreEqual(128, PrecisionHelper.Round(179.999, 2)); 66 | Assert.AreEqual(160, PrecisionHelper.Round(179.999, 3)); 67 | Assert.AreEqual(176, PrecisionHelper.Round(179.999, 4)); 68 | Assert.AreEqual(176, PrecisionHelper.Round(179.999, 5)); 69 | Assert.AreEqual(176, PrecisionHelper.Round(179.999, 6)); 70 | Assert.AreEqual(178, PrecisionHelper.Round(179.999, 7)); 71 | Assert.AreEqual(179, PrecisionHelper.Round(179.999, 8)); 72 | } 73 | 74 | [TestMethod] 75 | public void GetRangeTest() 76 | { 77 | Assert.AreEqual(Tuple.Create(7, 8), PrecisionHelper.GetRange(7, 8)); 78 | Assert.AreEqual(Tuple.Create(6, 8), PrecisionHelper.GetRange(7, 7)); 79 | Assert.AreEqual(Tuple.Create(4, 8), PrecisionHelper.GetRange(7, 6)); 80 | Assert.AreEqual(Tuple.Create(-8, 8), PrecisionHelper.GetRange(7, 5)); 81 | Assert.AreEqual(Tuple.Create(-16, 16), PrecisionHelper.GetRange(7, 4)); 82 | Assert.AreEqual(Tuple.Create(-32, 32), PrecisionHelper.GetRange(7, 3)); 83 | Assert.AreEqual(Tuple.Create(-64, 64), PrecisionHelper.GetRange(7, 2)); 84 | Assert.AreEqual(Tuple.Create(-128, 128), PrecisionHelper.GetRange(7, 1)); 85 | Assert.AreEqual(Tuple.Create(-256, 256), PrecisionHelper.GetRange(7, 0)); 86 | 87 | Assert.AreEqual(Tuple.Create(-8, -7), PrecisionHelper.GetRange(-7, 8)); 88 | Assert.AreEqual(Tuple.Create(-8, -6), PrecisionHelper.GetRange(-7, 7)); 89 | Assert.AreEqual(Tuple.Create(-8, -4), PrecisionHelper.GetRange(-7, 6)); 90 | Assert.AreEqual(Tuple.Create(-8, 8), PrecisionHelper.GetRange(-7, 5)); 91 | Assert.AreEqual(Tuple.Create(-16, 16), PrecisionHelper.GetRange(-7, 4)); 92 | Assert.AreEqual(Tuple.Create(-32, 32), PrecisionHelper.GetRange(-7, 3)); 93 | Assert.AreEqual(Tuple.Create(-64, 64), PrecisionHelper.GetRange(-7, 2)); 94 | Assert.AreEqual(Tuple.Create(-128, 128), PrecisionHelper.GetRange(-7, 1)); 95 | Assert.AreEqual(Tuple.Create(-256, 256), PrecisionHelper.GetRange(-7, 0)); 96 | } 97 | } 98 | } -------------------------------------------------------------------------------- /CovidSafe/CovidSafe.API/v20200505/Controllers/MessageController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | 7 | using AutoMapper; 8 | using CovidSafe.API.v20200505.Protos; 9 | using CovidSafe.DAL.Services; 10 | using CovidSafe.Entities.Messages; 11 | using CovidSafe.Entities.Validation; 12 | using Microsoft.AspNetCore.Http; 13 | using Microsoft.AspNetCore.Mvc; 14 | 15 | namespace CovidSafe.API.v20200505.Controllers 16 | { 17 | /// 18 | /// Handles CRUD operations 19 | /// 20 | [ApiController] 21 | [ApiVersion("2020-05-05", Deprecated = true)] 22 | [Route("api/[controller]")] 23 | public class MessageController : ControllerBase 24 | { 25 | /// 26 | /// AutoMapper instance for object resolution 27 | /// 28 | private readonly IMapper _map; 29 | /// 30 | /// service layer 31 | /// 32 | private readonly IMessageService _reportService; 33 | 34 | /// 35 | /// Creates a new instance 36 | /// 37 | /// AutoMapper instance 38 | /// service layer 39 | public MessageController(IMapper map, IMessageService reportService) 40 | { 41 | // Assign local values 42 | this._map = map; 43 | this._reportService = reportService; 44 | } 45 | 46 | /// 47 | /// Get objects matching the provided identifiers 48 | /// 49 | /// 50 | /// Sample request: 51 | /// 52 | /// POST /api/Message&api-version=2020-05-05 53 | /// { 54 | /// "RequestedQueries": [{ 55 | /// "messageId": "baa0ebe1-e6dd-447d-8d82-507644991e07", 56 | /// "messageTimestamp": 1586199635012 57 | /// }] 58 | /// } 59 | /// 60 | /// 61 | /// parameters 62 | /// Cancellation token (not required in API call) 63 | /// Successful request with results 64 | /// Malformed or invalid request provided 65 | /// of reports matching provided request parameters 66 | [HttpPost] 67 | [Consumes("application/x-protobuf", "application/json")] 68 | [Produces("application/x-protobuf", "application/json")] 69 | [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] 70 | [ProducesResponseType(typeof(RequestValidationResult), StatusCodes.Status400BadRequest)] 71 | [ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)] 72 | public async Task> PostAsync([FromBody] MessageRequest request, CancellationToken cancellationToken = default) 73 | { 74 | try 75 | { 76 | // Submit request 77 | IEnumerable reports = await this._reportService 78 | .GetByIdsAsync( 79 | request.RequestedQueries.Select(r => r.MessageId), 80 | cancellationToken 81 | ); 82 | 83 | // Map results to expected return type 84 | MatchMessageResponse response = new MatchMessageResponse(); 85 | response.MatchMessages.AddRange( 86 | reports.Select(r => this._map.Map(r)) 87 | ); 88 | 89 | return Ok(response); 90 | } 91 | catch(RequestValidationFailedException ex) 92 | { 93 | // Only return validation results 94 | return BadRequest(ex.ValidationResult); 95 | } 96 | catch(ArgumentNullException) 97 | { 98 | return BadRequest(); 99 | } 100 | } 101 | 102 | /// 103 | /// Service status request endpoint, used mostly by Azure services to determine if 104 | /// an endpoint is alive 105 | /// 106 | /// 107 | /// Sample request: 108 | /// 109 | /// HEAD /api/Message 110 | /// 111 | /// 112 | /// Cancellation token 113 | /// Successful request 114 | [ApiExplorerSettings(IgnoreApi = true)] 115 | [HttpHead] 116 | [ProducesResponseType(StatusCodes.Status200OK)] 117 | [ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)] 118 | public Task HeadAsync(CancellationToken cancellationToken = default) 119 | { 120 | return Task.FromResult(Ok()); 121 | } 122 | } 123 | } -------------------------------------------------------------------------------- /CovidSafe/CovidSafe.API/v20200611/Controllers/MessageController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | 7 | using AutoMapper; 8 | using CovidSafe.API.v20200611.Protos; 9 | using CovidSafe.DAL.Services; 10 | using CovidSafe.Entities.Messages; 11 | using CovidSafe.Entities.Validation; 12 | using Microsoft.AspNetCore.Http; 13 | using Microsoft.AspNetCore.Mvc; 14 | 15 | namespace CovidSafe.API.v20200611.Controllers 16 | { 17 | /// 18 | /// Handles CRUD operations 19 | /// 20 | [ApiController] 21 | [ApiVersion("2020-06-11")] 22 | [Route("api/[controller]")] 23 | public class MessageController : ControllerBase 24 | { 25 | /// 26 | /// AutoMapper instance for object resolution 27 | /// 28 | private readonly IMapper _map; 29 | /// 30 | /// service layer 31 | /// 32 | private readonly IMessageService _reportService; 33 | 34 | /// 35 | /// Creates a new instance 36 | /// 37 | /// AutoMapper instance 38 | /// service layer 39 | public MessageController(IMapper map, IMessageService reportService) 40 | { 41 | // Assign local values 42 | this._map = map; 43 | this._reportService = reportService; 44 | } 45 | 46 | /// 47 | /// Get objects matching the provided identifiers 48 | /// 49 | /// 50 | /// Sample request: 51 | /// 52 | /// POST /api/Message&api-version=2020-06-11 53 | /// { 54 | /// "RequestedQueries": [{ 55 | /// "messageId": "baa0ebe1-e6dd-447d-8d82-507644991e07", 56 | /// "messageTimestamp": 1586199635012 57 | /// }] 58 | /// } 59 | /// 60 | /// 61 | /// parameters 62 | /// Cancellation token (not required in API call) 63 | /// Successful request with results 64 | /// Malformed or invalid request provided 65 | /// of reports matching provided request parameters 66 | [HttpPost] 67 | [Consumes("application/x-protobuf", "application/json")] 68 | [Produces("application/x-protobuf", "application/json")] 69 | [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] 70 | [ProducesResponseType(typeof(Protos.RequestValidationResult), StatusCodes.Status400BadRequest)] 71 | [ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)] 72 | public async Task> PostAsync([FromBody] MessageRequest request, CancellationToken cancellationToken = default) 73 | { 74 | try 75 | { 76 | // Submit request 77 | IEnumerable reports = await this._reportService 78 | .GetByIdsAsync( 79 | request.RequestedQueries.Select(r => r.MessageId), 80 | cancellationToken 81 | ); 82 | 83 | // Map results to expected return type 84 | MessageResponse response = new MessageResponse(); 85 | foreach(MessageContainer container in reports) 86 | { 87 | response.NarrowcastMessages.AddRange( 88 | container.Narrowcasts.Select( 89 | c => this._map.Map(c) 90 | ) 91 | ); 92 | } 93 | 94 | return Ok(response); 95 | } 96 | catch(RequestValidationFailedException ex) 97 | { 98 | // Only return validation results 99 | return BadRequest(ex.ValidationResult); 100 | } 101 | catch(ArgumentNullException) 102 | { 103 | return BadRequest(); 104 | } 105 | } 106 | 107 | /// 108 | /// Service status request endpoint, used mostly by Azure services to determine if 109 | /// an endpoint is alive 110 | /// 111 | /// 112 | /// Sample request: 113 | /// 114 | /// HEAD /api/Message 115 | /// 116 | /// 117 | /// Cancellation token 118 | /// Successful request 119 | [ApiExplorerSettings(IgnoreApi = true)] 120 | [HttpHead] 121 | [ProducesResponseType(StatusCodes.Status200OK)] 122 | [ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)] 123 | public Task HeadAsync(CancellationToken cancellationToken = default) 124 | { 125 | return Task.FromResult(Ok()); 126 | } 127 | } 128 | } -------------------------------------------------------------------------------- /CovidSafe/CovidSafe.API.Tests/v20200415/Controllers/MessageControllers/AreaReportControllerTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | 5 | using AutoMapper; 6 | using CovidSafe.API.v20200415.Controllers.MessageControllers; 7 | using CovidSafe.API.v20200415.Protos; 8 | using CovidSafe.DAL.Repositories; 9 | using CovidSafe.DAL.Services; 10 | using Microsoft.AspNetCore.Http; 11 | using Microsoft.AspNetCore.Mvc; 12 | using Microsoft.VisualStudio.TestTools.UnitTesting; 13 | using Moq; 14 | 15 | namespace CovidSafe.API.v20200415.Tests.Controllers.MessageControllers 16 | { 17 | /// 18 | /// Unit tests for the class 19 | /// 20 | [TestClass] 21 | public class AreaReportControllerTests 22 | { 23 | /// 24 | /// Test instance 25 | /// 26 | private AreaReportController _controller; 27 | /// 28 | /// Mock instance 29 | /// 30 | private Mock _repo; 31 | /// 32 | /// instance 33 | /// 34 | private MessageService _service; 35 | 36 | /// 37 | /// Creates a new instance 38 | /// 39 | public AreaReportControllerTests() 40 | { 41 | // Configure repo mock 42 | this._repo = new Mock(); 43 | 44 | // Configure service 45 | this._service = new MessageService(this._repo.Object); 46 | 47 | // Create AutoMapper instance 48 | MapperConfiguration mapperConfig = new MapperConfiguration( 49 | opts => opts.AddProfile() 50 | ); 51 | IMapper mapper = mapperConfig.CreateMapper(); 52 | 53 | // Configure controller 54 | this._controller = new AreaReportController(mapper, this._service); 55 | this._controller.ControllerContext = new ControllerContext(); 56 | this._controller.ControllerContext.HttpContext = new DefaultHttpContext(); 57 | } 58 | 59 | /// 60 | /// 61 | /// returns when no objects are provided 62 | /// with request 63 | /// 64 | [TestMethod] 65 | public async Task PutAsync_BadRequestObjectWithNoAreas() 66 | { 67 | // Arrange 68 | AreaMatch requestObj = new AreaMatch 69 | { 70 | UserMessage = "This is a message" 71 | }; 72 | 73 | // Act 74 | ActionResult controllerResponse = await this._controller 75 | .PutAsync(requestObj, CancellationToken.None); 76 | 77 | // Assert 78 | Assert.IsNotNull(controllerResponse); 79 | Assert.IsInstanceOfType(controllerResponse, typeof(BadRequestObjectResult)); 80 | } 81 | 82 | /// 83 | /// 84 | /// returns when no user message is specified 85 | /// 86 | [TestMethod] 87 | public async Task PutAsync_BadRequestWithNoUserMessage() 88 | { 89 | // Arrange 90 | AreaMatch requestObj = new AreaMatch(); 91 | requestObj.Areas.Add(new Area 92 | { 93 | BeginTime = 0, 94 | EndTime = 1, 95 | Location = new Location 96 | { 97 | Latitude = 10.1234, 98 | Longitude = 10.1234 99 | }, 100 | RadiusMeters = 100 101 | }); 102 | 103 | // Act 104 | ActionResult controllerResponse = await this._controller 105 | .PutAsync(requestObj, CancellationToken.None); 106 | 107 | // Assert 108 | Assert.IsNotNull(controllerResponse); 109 | Assert.IsInstanceOfType(controllerResponse, typeof(BadRequestObjectResult)); 110 | } 111 | 112 | /// 113 | /// 114 | /// returns with valid input data 115 | /// 116 | [TestMethod] 117 | public async Task PutAsync_OkWithValidInputs() 118 | { 119 | // Arrange 120 | AreaMatch requestObj = new AreaMatch 121 | { 122 | UserMessage = "User message content" 123 | }; 124 | requestObj.Areas.Add(new Area 125 | { 126 | BeginTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(), 127 | EndTime = DateTimeOffset.UtcNow.AddHours(1).ToUnixTimeMilliseconds(), 128 | Location = new Location 129 | { 130 | Latitude = 10.1234, 131 | Longitude = 10.1234 132 | }, 133 | RadiusMeters = 100 134 | }); 135 | 136 | // Act 137 | ActionResult controllerResponse = await this._controller 138 | .PutAsync(requestObj, CancellationToken.None); 139 | 140 | // Assert 141 | Assert.IsNotNull(controllerResponse); 142 | Assert.IsInstanceOfType(controllerResponse, typeof(OkResult)); 143 | } 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /CovidSafe/CovidSafe.API.Tests/v20200505/Controllers/MessageControllers/AreaReportControllerTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | 5 | using AutoMapper; 6 | using CovidSafe.API.v20200505.Controllers.MessageControllers; 7 | using CovidSafe.API.v20200505.Protos; 8 | using CovidSafe.DAL.Repositories; 9 | using CovidSafe.DAL.Services; 10 | using Microsoft.AspNetCore.Http; 11 | using Microsoft.AspNetCore.Mvc; 12 | using Microsoft.VisualStudio.TestTools.UnitTesting; 13 | using Moq; 14 | 15 | namespace CovidSafe.API.v20200505.Tests.Controllers.MessageControllers 16 | { 17 | /// 18 | /// Unit tests for the class 19 | /// 20 | [TestClass] 21 | public class AnnounceControllerTests 22 | { 23 | /// 24 | /// Test instance 25 | /// 26 | private AreaReportController _controller; 27 | /// 28 | /// Mock instance 29 | /// 30 | private Mock _repo; 31 | /// 32 | /// instance 33 | /// 34 | private MessageService _service; 35 | 36 | /// 37 | /// Creates a new instance 38 | /// 39 | public AnnounceControllerTests() 40 | { 41 | // Configure repo mock 42 | this._repo = new Mock(); 43 | 44 | // Configure service 45 | this._service = new MessageService(this._repo.Object); 46 | 47 | // Create AutoMapper instance 48 | MapperConfiguration mapperConfig = new MapperConfiguration( 49 | opts => opts.AddProfile() 50 | ); 51 | IMapper mapper = mapperConfig.CreateMapper(); 52 | 53 | // Configure controller 54 | this._controller = new AreaReportController(mapper, this._service); 55 | this._controller.ControllerContext = new ControllerContext(); 56 | this._controller.ControllerContext.HttpContext = new DefaultHttpContext(); 57 | } 58 | 59 | /// 60 | /// 61 | /// returns when no objects are provided 62 | /// with request 63 | /// 64 | [TestMethod] 65 | public async Task PutAsync_BadRequestObjectWithNoAreas() 66 | { 67 | // Arrange 68 | AreaMatch requestObj = new AreaMatch 69 | { 70 | UserMessage = "This is a message" 71 | }; 72 | 73 | // Act 74 | ActionResult controllerResponse = await this._controller 75 | .PutAsync(requestObj, CancellationToken.None); 76 | 77 | // Assert 78 | Assert.IsNotNull(controllerResponse); 79 | Assert.IsInstanceOfType(controllerResponse, typeof(BadRequestObjectResult)); 80 | } 81 | 82 | /// 83 | /// 84 | /// returns when no user message is specified 85 | /// 86 | [TestMethod] 87 | public async Task PutAsync_BadRequestWithNoUserMessage() 88 | { 89 | // Arrange 90 | AreaMatch requestObj = new AreaMatch(); 91 | requestObj.Areas.Add(new Area 92 | { 93 | BeginTime = 0, 94 | EndTime = 1, 95 | Location = new Location 96 | { 97 | Latitude = 10.1234, 98 | Longitude = 10.1234 99 | }, 100 | RadiusMeters = 100 101 | }); 102 | 103 | // Act 104 | ActionResult controllerResponse = await this._controller 105 | .PutAsync(requestObj, CancellationToken.None); 106 | 107 | // Assert 108 | Assert.IsNotNull(controllerResponse); 109 | Assert.IsInstanceOfType(controllerResponse, typeof(BadRequestObjectResult)); 110 | } 111 | 112 | /// 113 | /// 114 | /// returns with valid input data 115 | /// 116 | [TestMethod] 117 | public async Task PutAsync_OkWithValidInputs() 118 | { 119 | // Arrange 120 | AreaMatch requestObj = new AreaMatch 121 | { 122 | UserMessage = "User message content" 123 | }; 124 | requestObj.Areas.Add(new Area 125 | { 126 | BeginTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(), 127 | EndTime = DateTimeOffset.UtcNow.AddHours(1).ToUnixTimeMilliseconds(), 128 | Location = new Location 129 | { 130 | Latitude = 10.1234, 131 | Longitude = 10.1234 132 | }, 133 | RadiusMeters = 100 134 | }); 135 | 136 | // Act 137 | ActionResult controllerResponse = await this._controller 138 | .PutAsync(requestObj, CancellationToken.None); 139 | 140 | // Assert 141 | Assert.IsNotNull(controllerResponse); 142 | Assert.IsInstanceOfType(controllerResponse, typeof(OkResult)); 143 | } 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /CovidSafe/CovidSafe.API.Tests/v20200611/Controllers/MessageControllers/AnnounceControllerTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | 5 | using AutoMapper; 6 | using CovidSafe.API.v20200611.Controllers.MessageControllers; 7 | using CovidSafe.API.v20200611.Protos; 8 | using CovidSafe.DAL.Repositories; 9 | using CovidSafe.DAL.Services; 10 | using Microsoft.AspNetCore.Http; 11 | using Microsoft.AspNetCore.Mvc; 12 | using Microsoft.VisualStudio.TestTools.UnitTesting; 13 | using Moq; 14 | 15 | namespace CovidSafe.API.v20200611.Tests.Controllers.MessageControllers 16 | { 17 | /// 18 | /// Unit tests for the class 19 | /// 20 | [TestClass] 21 | public class AnnounceControllerTests 22 | { 23 | /// 24 | /// Test instance 25 | /// 26 | private AnnounceController _controller; 27 | /// 28 | /// Mock instance 29 | /// 30 | private Mock _repo; 31 | /// 32 | /// instance 33 | /// 34 | private MessageService _service; 35 | 36 | /// 37 | /// Creates a new instance 38 | /// 39 | public AnnounceControllerTests() 40 | { 41 | // Configure repo mock 42 | this._repo = new Mock(); 43 | 44 | // Configure service 45 | this._service = new MessageService(this._repo.Object); 46 | 47 | // Create AutoMapper instance 48 | MapperConfiguration mapperConfig = new MapperConfiguration( 49 | opts => opts.AddProfile() 50 | ); 51 | IMapper mapper = mapperConfig.CreateMapper(); 52 | 53 | // Configure controller 54 | this._controller = new AnnounceController(mapper, this._service); 55 | this._controller.ControllerContext = new ControllerContext(); 56 | this._controller.ControllerContext.HttpContext = new DefaultHttpContext(); 57 | } 58 | 59 | /// 60 | /// 61 | /// returns when no objects are provided 62 | /// with request 63 | /// 64 | [TestMethod] 65 | public async Task PutAsync_BadRequestObjectWithNoAreas() 66 | { 67 | // Arrange 68 | NarrowcastMessage requestObj = new NarrowcastMessage 69 | { 70 | UserMessage = "This is a message" 71 | }; 72 | 73 | // Act 74 | ActionResult controllerResponse = await this._controller 75 | .PutAsync(requestObj, CancellationToken.None); 76 | 77 | // Assert 78 | Assert.IsNotNull(controllerResponse); 79 | Assert.IsInstanceOfType(controllerResponse, typeof(BadRequestObjectResult)); 80 | } 81 | 82 | /// 83 | /// 84 | /// returns when no user message is specified 85 | /// 86 | [TestMethod] 87 | public async Task PutAsync_BadRequestWithNoUserMessage() 88 | { 89 | // Arrange 90 | NarrowcastMessage requestObj = new NarrowcastMessage(); 91 | requestObj.Area = new Area 92 | { 93 | BeginTime = 0, 94 | EndTime = 1, 95 | Location = new Location 96 | { 97 | Latitude = 10.1234, 98 | Longitude = 10.1234 99 | }, 100 | RadiusMeters = 100 101 | }; 102 | 103 | // Act 104 | ActionResult controllerResponse = await this._controller 105 | .PutAsync(requestObj, CancellationToken.None); 106 | 107 | // Assert 108 | Assert.IsNotNull(controllerResponse); 109 | Assert.IsInstanceOfType(controllerResponse, typeof(BadRequestObjectResult)); 110 | } 111 | 112 | /// 113 | /// 114 | /// returns with valid input data 115 | /// 116 | [TestMethod] 117 | public async Task PutAsync_OkWithValidInputs() 118 | { 119 | // Arrange 120 | NarrowcastMessage requestObj = new NarrowcastMessage 121 | { 122 | UserMessage = "User message content" 123 | }; 124 | requestObj.Area = new Area 125 | { 126 | BeginTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(), 127 | EndTime = DateTimeOffset.UtcNow.AddHours(1).ToUnixTimeMilliseconds(), 128 | Location = new Location 129 | { 130 | Latitude = 10.1234, 131 | Longitude = 10.1234 132 | }, 133 | RadiusMeters = 100 134 | }; 135 | 136 | // Act 137 | ActionResult controllerResponse = await this._controller 138 | .PutAsync(requestObj, CancellationToken.None); 139 | 140 | // Assert 141 | Assert.IsNotNull(controllerResponse); 142 | Assert.IsInstanceOfType(controllerResponse, typeof(OkResult)); 143 | } 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /CovidSafe/CovidSafe.API/v20200415/Controllers/MessageController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | 7 | using AutoMapper; 8 | using CovidSafe.API.v20200415.Protos; 9 | using CovidSafe.DAL.Services; 10 | using CovidSafe.Entities.Messages; 11 | using CovidSafe.Entities.Validation; 12 | using Microsoft.AspNetCore.Http; 13 | using Microsoft.AspNetCore.Mvc; 14 | 15 | namespace CovidSafe.API.v20200415.Controllers 16 | { 17 | /// 18 | /// Handles CRUD operations 19 | /// 20 | [ApiController] 21 | [ApiVersion("2020-04-15", Deprecated = true)] 22 | [Route("api/[controller]")] 23 | public class MessageController : ControllerBase 24 | { 25 | /// 26 | /// AutoMapper instance for object resolution 27 | /// 28 | private readonly IMapper _map; 29 | /// 30 | /// service layer 31 | /// 32 | private readonly IMessageService _reportService; 33 | 34 | /// 35 | /// Creates a new instance 36 | /// 37 | /// AutoMapper instance 38 | /// service layer 39 | public MessageController(IMapper map, IMessageService reportService) 40 | { 41 | // Assign local values 42 | this._map = map; 43 | this._reportService = reportService; 44 | } 45 | 46 | /// 47 | /// Get objects matching the provided identifiers 48 | /// 49 | /// 50 | /// Sample request: 51 | /// 52 | /// POST /api/Message&api-version=2020-04-15 53 | /// { 54 | /// "RequestedQueries": [{ 55 | /// "messageId": "baa0ebe1-e6dd-447d-8d82-507644991e07", 56 | /// "messageTimestamp": 1586199635012 57 | /// }] 58 | /// } 59 | /// 60 | /// 61 | /// parameters 62 | /// Cancellation token (not required in API call) 63 | /// Successful request with results 64 | /// Malformed or invalid request provided 65 | /// Collection of objects matching request parameters 66 | [HttpPost] 67 | [Consumes("application/x-protobuf", "application/json")] 68 | [Produces("application/x-protobuf", "application/json")] 69 | [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] 70 | [ProducesResponseType(typeof(RequestValidationResult), StatusCodes.Status400BadRequest)] 71 | [ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)] 72 | public async Task>> PostAsync([FromBody] MessageRequest request, CancellationToken cancellationToken = default) 73 | { 74 | try 75 | { 76 | // Submit request 77 | IEnumerable reports = await this._reportService 78 | .GetByIdsAsync( 79 | request.RequestedQueries.Select(r => r.MessageId), 80 | cancellationToken 81 | ); 82 | 83 | // Map MatchMessage types 84 | List messages = new List(); 85 | 86 | foreach(MessageContainer report in reports) 87 | { 88 | MatchMessage result = this._map.Map(report); 89 | // Get BLEs 90 | BluetoothMatch match = new BluetoothMatch(); 91 | match.Seeds.AddRange( 92 | report.BluetoothSeeds.Select(s => this._map.Map(s)) 93 | ); 94 | // Add converted BLE match 95 | result.BluetoothMatches.Add(match); 96 | // Add converted MatchMessage 97 | messages.Add(result); 98 | } 99 | 100 | // Return as expected proto type 101 | return Ok(messages); 102 | } 103 | catch(RequestValidationFailedException ex) 104 | { 105 | // Only return validation results 106 | return BadRequest(ex.ValidationResult); 107 | } 108 | catch(ArgumentNullException) 109 | { 110 | return BadRequest(); 111 | } 112 | } 113 | 114 | /// 115 | /// Service status request endpoint, used mostly by Azure services to determine if 116 | /// an endpoint is alive 117 | /// 118 | /// 119 | /// Sample request: 120 | /// 121 | /// HEAD /api/Message 122 | /// 123 | /// 124 | /// Cancellation token 125 | /// Successful request 126 | [ApiExplorerSettings(IgnoreApi = true)] 127 | [HttpHead] 128 | [ProducesResponseType(StatusCodes.Status200OK)] 129 | [ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)] 130 | public Task HeadAsync(CancellationToken cancellationToken = default) 131 | { 132 | return Task.FromResult(Ok()); 133 | } 134 | } 135 | } -------------------------------------------------------------------------------- /CovidSafe/CovidSafe.API.Tests.Performance/WebTest.webtest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 |
31 |
32 | 33 | 34 | 35 | 36 | ewANAAoAIAAgACAAIAAiAFIAZQBxAHUAZQBzAHQAZQBkAFEAdQBlAHIAaQBlAHMAIgA6ACAAWwB7AA0ACgAgACAAIAAgACAAIAAgACAAIgBtAGUAcwBzAGEAZwBlAEkAZAAiADoAIAAiAGIAYQBhADAAZQBiAGUAMQAtAGUANgBkAGQALQA0ADQANwBkAC0AOABkADgAMgAtADUAMAA3ADYANAA0ADkAOQAxAGUAMAA3ACIALAANAAoAIAAgACAAIAAgACAAIAAgACIAbQBlAHMAcwBhAGcAZQBUAGkAbQBlAHMAdABhAG0AcAAiADoAIAAxADUAOAA2ADEAOQA5ADYAMwA1ADAAMQAyAA0ACgAgACAAIAAgAH0AXQANAAoAfQA= 37 | 38 | 39 | 40 |
41 |
42 | 43 | 44 | 45 | 46 | ewANAAoAIAAgACAAIAAiAHUAcwBlAHIATQBlAHMAcwBhAGcAZQAiADoAIAAiAE0AbwBuAGkAdABvAHIAIABzAHkAbQBwAHQAbwBtAHMAIABmAG8AcgAgAG8AbgBlACAAdwBlAGUAawAuACIALAANAAoAIAAgACAAIAAiAGEAcgBlAGEAIgA6ACAAewANAAoAIAAgACAAIAAgACAAIAAgACIAbABvAGMAYQB0AGkAbwBuACIAOgAgAHsADQAKACAAIAAgACAAIAAgACAAIAAgACAAIAAgACIAbABhAHQAaQB0AHUAZABlACIAOgAgADQAMAAuADcANQA2ADkALAANAAoAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIgBsAG8AbgBnAGkAdAB1AGQAZQAiADoAIAAtADcAMwAuADkAOAAyADgADQAKACAAIAAgACAAIAAgACAAIAB9ACwADQAKACAAIAAgACAAIAAgACAAIAAiAHIAYQBkAGkAdQBzAE0AZQB0AGUAcgBzACIAOgAgADEAMAAwACwADQAKACAAIAAgACAAIAAgACAAIAAiAGIAZQBnAGkAbgBUAGkAbQBlACIAOgAgADEANQA4ADYAMAA4ADMANQA5ADkALAANAAoAIAAgACAAIAAgACAAIAAgACIAZQBuAGQAVABpAG0AZQAiADoAIAAxADUAOAA2ADAAOAA1ADEAOAA5AA0ACgAgACAAIAAgAH0ADQAKAH0A 47 | 48 | 49 | -------------------------------------------------------------------------------- /CovidSafe/CovidSafe.API/v20200505/MappingProfiles.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | 4 | using AutoMapper; 5 | using CovidSafe.API.v20200505.Protos; 6 | using CovidSafe.Entities.Geospatial; 7 | using CovidSafe.Entities.Messages; 8 | using Microsoft.EntityFrameworkCore.Internal; 9 | 10 | namespace CovidSafe.API.v20200505 11 | { 12 | /// 13 | /// Maps proto types to their internal database representations 14 | /// 15 | public class MappingProfiles : Profile 16 | { 17 | /// 18 | /// Creates a new instance 19 | /// 20 | public MappingProfiles() 21 | { 22 | // Location -> Coordinates 23 | CreateMap() 24 | // Properties have the same name+type 25 | .ReverseMap(); 26 | 27 | // Region -> Region 28 | CreateMap() 29 | // Properties have the same name+type 30 | .ReverseMap(); 31 | 32 | // MessageInfo -> InfectionReportMetadata 33 | CreateMap() 34 | .ForMember( 35 | im => im.Id, 36 | op => op.MapFrom(mi => mi.MessageId) 37 | ) 38 | .ForMember( 39 | im => im.Timestamp, 40 | op => op.MapFrom(mi => mi.MessageTimestamp) 41 | ) 42 | .ReverseMap(); 43 | 44 | // IEnumerable -> MessageListResponse 45 | // This is a one-way response so no ReverseMap is necessary 46 | CreateMap, MessageListResponse>() 47 | .ForMember( 48 | mr => mr.MessageInfoes, 49 | op => op.MapFrom(im => im) 50 | ) 51 | .ForMember( 52 | mr => mr.MaxResponseTimestamp, 53 | op => op.MapFrom(im => im.Count() > 0 ? im.Max(o => o.Timestamp) : 0) 54 | ); 55 | 56 | // Area -> InfectionArea 57 | CreateMap() 58 | .ForMember( 59 | ia => ia.BeginTimestamp, 60 | op => op.MapFrom(a => a.BeginTime) 61 | ) 62 | .ForMember( 63 | ia => ia.EndTimestamp, 64 | op => op.MapFrom(a => a.EndTime) 65 | ) 66 | .ForMember( 67 | ia => ia.Location, 68 | op => op.MapFrom(a => a.Location) 69 | ) 70 | .ForMember( 71 | ia => ia.RadiusMeters, 72 | op => op.MapFrom(a => a.RadiusMeters) 73 | ) 74 | .ReverseMap(); 75 | 76 | // BlueToothSeed -> BluetoothSeedMessage 77 | CreateMap() 78 | .ForMember( 79 | bs => bs.BeginTimestamp, 80 | op => op.MapFrom(s => s.SequenceStartTime) 81 | ) 82 | .ForMember( 83 | bs => bs.EndTimestamp, 84 | op => op.MapFrom(s => s.SequenceEndTime) 85 | ) 86 | .ForMember( 87 | bs => bs.Seed, 88 | op => op.MapFrom(s => s.Seed) 89 | ) 90 | .ReverseMap(); 91 | 92 | // AreaMatch -> AreaReport 93 | CreateMap() 94 | .ForMember( 95 | ar => ar.Area, 96 | // v20200611 clarified a NarrowcastMessage should have only one Area 97 | op => op.MapFrom(am => am.Areas.FirstOrDefault()) 98 | ) 99 | .ForMember( 100 | ar => ar.UserMessage, 101 | op => op.MapFrom(am => am.UserMessage) 102 | ) 103 | .ReverseMap(); 104 | 105 | // SelfReportRequest -> InfectionReport 106 | // This is only a request object so no ReverseMap is necessary 107 | CreateMap() 108 | .ForMember( 109 | ir => ir.BluetoothSeeds, 110 | op => op.MapFrom(sr => sr.Seeds) 111 | ) 112 | // Currently no NarrowcastMessages in a SelfReportRequest 113 | .ForMember( 114 | ir => ir.Narrowcasts, 115 | op => op.Ignore() 116 | ) 117 | // Not specified by users 118 | .ForMember( 119 | ir => ir.BluetoothMatchMessage, 120 | op => op.Ignore() 121 | ) 122 | // Not specified by users 123 | .ForMember( 124 | ir => ir.BooleanExpression, 125 | op => op.Ignore() 126 | ); 127 | 128 | // MatchMessage -> InfectionReport 129 | CreateMap() 130 | .ForMember( 131 | ir => ir.Narrowcasts, 132 | op => op.MapFrom(mm => mm.AreaMatches) 133 | ) 134 | .ForMember( 135 | ir => ir.BluetoothSeeds, 136 | op => op.MapFrom(mm => mm.BluetoothSeeds) 137 | ) 138 | .ForMember( 139 | ir => ir.BooleanExpression, 140 | op => op.MapFrom(mm => mm.BoolExpression) 141 | ) 142 | .ForMember( 143 | // Not supported in v20200415 144 | ir => ir.BluetoothMatchMessage, 145 | op => op.Ignore() 146 | ) 147 | // Other properties have the same name+type 148 | .ReverseMap(); 149 | } 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /CovidSafe/CovidSafe.API/v20200415/MappingProfiles.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | 4 | using AutoMapper; 5 | using CovidSafe.API.v20200415.Protos; 6 | using CovidSafe.Entities.Geospatial; 7 | using CovidSafe.Entities.Messages; 8 | 9 | namespace CovidSafe.API.v20200415 10 | { 11 | /// 12 | /// Maps proto types to their internal database representations 13 | /// 14 | public class MappingProfiles : Profile 15 | { 16 | /// 17 | /// Creates a new instance 18 | /// 19 | public MappingProfiles() 20 | { 21 | // Location -> Coordinates 22 | CreateMap() 23 | // Properties have the same name+type 24 | .ReverseMap(); 25 | 26 | // Region -> Region 27 | CreateMap() 28 | // Properties have the same name+type 29 | .ReverseMap(); 30 | 31 | // MessageInfo -> InfectionReportMetadata 32 | CreateMap() 33 | .ForMember( 34 | im => im.Id, 35 | op => op.MapFrom(mi => mi.MessageId) 36 | ) 37 | .ForMember( 38 | im => im.Timestamp, 39 | op => op.MapFrom(mi => mi.MessageTimestamp) 40 | ) 41 | .ReverseMap(); 42 | 43 | // IEnumerable -> MessageListResponse 44 | // This is a one-way response so no ReverseMap is necessary 45 | CreateMap, MessageListResponse>() 46 | .ForMember( 47 | mr => mr.MessageInfoes, 48 | op => op.MapFrom(im => im) 49 | ) 50 | .ForMember( 51 | mr => mr.MaxResponseTimestamp, 52 | op => op.MapFrom(im => im.Count() > 0 ? im.Max(o => o.Timestamp) : 0) 53 | ); 54 | 55 | // Area -> InfectionArea 56 | CreateMap() 57 | .ForMember( 58 | ia => ia.BeginTimestamp, 59 | op => op.MapFrom(a => a.BeginTime) 60 | ) 61 | .ForMember( 62 | ia => ia.EndTimestamp, 63 | op => op.MapFrom(a => a.EndTime) 64 | ) 65 | .ForMember( 66 | ia => ia.Location, 67 | op => op.MapFrom(a => a.Location) 68 | ) 69 | .ForMember( 70 | ia => ia.RadiusMeters, 71 | op => op.MapFrom(a => a.RadiusMeters) 72 | ) 73 | .ReverseMap(); 74 | 75 | // BlueToothSeed -> BluetoothSeedMessage 76 | CreateMap() 77 | .ForMember( 78 | bs => bs.BeginTimestamp, 79 | op => op.MapFrom(s => s.SequenceStartTime) 80 | ) 81 | .ForMember( 82 | bs => bs.EndTimestamp, 83 | op => op.MapFrom(s => s.SequenceEndTime) 84 | ) 85 | .ForMember( 86 | bs => bs.Seed, 87 | op => op.MapFrom(s => s.Seed) 88 | ) 89 | .ReverseMap(); 90 | 91 | // AreaMatch -> AreaReport 92 | CreateMap() 93 | .ForMember( 94 | ar => ar.Area, 95 | // v20200611 clarified a NarrowcastMessage should have only one Area 96 | op => op.MapFrom(am => am.Areas.FirstOrDefault()) 97 | ) 98 | .ForMember( 99 | ar => ar.UserMessage, 100 | op => op.MapFrom(am => am.UserMessage) 101 | ) 102 | .ReverseMap(); 103 | 104 | // SelfReportRequest -> InfectionReport 105 | // This is only a request object so no ReverseMap is necessary 106 | CreateMap() 107 | .ForMember( 108 | ir => ir.BluetoothSeeds, 109 | op => op.MapFrom(sr => sr.Seeds) 110 | ) 111 | // Currently no Narrowcast message in a SelfReportRequest 112 | .ForMember( 113 | ir => ir.Narrowcasts, 114 | op => op.Ignore() 115 | ) 116 | // Not supported in v20200415 117 | .ForMember( 118 | ir => ir.BluetoothMatchMessage, 119 | op => op.Ignore() 120 | ) 121 | // Not specified by users 122 | .ForMember( 123 | ir => ir.BooleanExpression, 124 | op => op.Ignore() 125 | ); 126 | 127 | // List -> BluetoothMatch 128 | CreateMap, BluetoothMatch>() 129 | .ForMember( 130 | bm => bm.Seeds, 131 | op => op.MapFrom(bs => bs.Select(s => new BlueToothSeed 132 | { 133 | Seed = s.Seed, 134 | SequenceEndTime = s.EndTimestamp, 135 | SequenceStartTime = s.BeginTimestamp 136 | })) 137 | ) 138 | // No user messages in Bluetooth Matches 139 | .ForMember( 140 | bm => bm.UserMessage, 141 | op => op.Ignore() 142 | ); 143 | 144 | // InfectionReport -> MatchMessage 145 | CreateMap() 146 | .ForMember( 147 | mm => mm.AreaMatches, 148 | op => op.MapFrom(ir => ir.Narrowcasts) 149 | ) 150 | .ForMember( 151 | mm => mm.BoolExpression, 152 | op => op.MapFrom(ir => ir.BooleanExpression) 153 | ) 154 | // Ignore BLE matches because the conversion is too complex for AutoMapper 155 | // Do this in MessagesController 156 | .ForMember( 157 | mm => mm.BluetoothMatches, 158 | op => op.Ignore() 159 | ) 160 | .ReverseMap(); 161 | } 162 | } 163 | } -------------------------------------------------------------------------------- /CovidSafe/CovidSafe.API/v20200505/Controllers/MessageControllers/ListController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | 7 | using AutoMapper; 8 | using CovidSafe.API.v20200505.Protos; 9 | using CovidSafe.DAL.Helpers; 10 | using CovidSafe.DAL.Services; 11 | using CovidSafe.Entities.Messages; 12 | using CovidSafe.Entities.Validation; 13 | using Microsoft.AspNetCore.Http; 14 | using Microsoft.AspNetCore.Mvc; 15 | 16 | namespace CovidSafe.API.v20200505.Controllers.MessageControllers 17 | { 18 | /// 19 | /// Handles requests to list identifiers which are new to a client 20 | /// 21 | [ApiController] 22 | [ApiVersion("2020-05-05", Deprecated = true)] 23 | [Route("api/Messages/[controller]")] 24 | public class ListController : ControllerBase 25 | { 26 | /// 27 | /// AutoMapper instance for object resolution 28 | /// 29 | private readonly IMapper _map; 30 | /// 31 | /// service layer 32 | /// 33 | private readonly IMessageService _reportService; 34 | 35 | /// 36 | /// Creates a new instance 37 | /// 38 | /// AutoMapper instance 39 | /// service layer 40 | public ListController(IMapper map, IMessageService reportService) 41 | { 42 | // Assign local values 43 | this._map = map; 44 | this._reportService = reportService; 45 | } 46 | 47 | /// 48 | /// Get for a region, starting at a provided timestamp 49 | /// 50 | /// 51 | /// Sample request: 52 | /// 53 | /// GET /api/Messages/List?lat=74.12&lon=-39.12&precision=2&lastTimestamp=0&api-version=2020-05-05 54 | /// 55 | /// 56 | /// Latitude of desired 57 | /// Longitude of desired 58 | /// Precision of desired 59 | /// Latest timestamp on client device, in ms from UNIX epoch 60 | /// Cancellation token (not required in API call) 61 | /// Successful request with results 62 | /// Malformed or invalid request provided 63 | /// Collection of objects matching request parameters 64 | [HttpGet] 65 | [Produces("application/x-protobuf", "application/json")] 66 | [ProducesResponseType(typeof(MessageListResponse), StatusCodes.Status200OK)] 67 | [ProducesResponseType(typeof(ValidationResult), StatusCodes.Status400BadRequest)] 68 | [ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)] 69 | public async Task> GetAsync([Required] double lat, [Required] double lon, [Required] int precision, [Required] long lastTimestamp, CancellationToken cancellationToken = default) 70 | { 71 | try 72 | { 73 | // Pull queries matching parameters. Legacy precision is ignored 74 | var region =RegionHelper.CreateRegion(lat, lon); 75 | 76 | IEnumerable results = await this._reportService 77 | .GetLatestInfoAsync(region, lastTimestamp, cancellationToken); 78 | 79 | // Return using mapped proto object 80 | return Ok(this._map.Map(results)); 81 | } 82 | catch (RequestValidationFailedException ex) 83 | { 84 | return BadRequest(ex.ValidationResult); 85 | } 86 | catch (ArgumentNullException) 87 | { 88 | return BadRequest(); 89 | } 90 | } 91 | 92 | /// 93 | /// Get total size of objects for a based 94 | /// on the provided parameters when using application/x-protobuf 95 | /// 96 | /// 97 | /// Sample request: 98 | /// 99 | /// HEAD /Messages/List?lat=74.12&lon=-39.12&precision=2&lastTimestamp=0 100 | /// 101 | /// 102 | /// Latitude of desired 103 | /// Longitude of desired 104 | /// Precision of desired 105 | /// Latest timestamp on client device, in ms from UNIX epoch 106 | /// Cancellation token (not required in API call) 107 | /// Successful request 108 | /// Malformed or invalid request provided 109 | /// 110 | /// Total size of matching objects (via Content-Type header), in bytes, based 111 | /// on their size when converted to the Protobuf format 112 | /// 113 | [HttpHead] 114 | [ProducesResponseType(StatusCodes.Status200OK)] 115 | [ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)] 116 | public async Task HeadAsync([Required] double lat, [Required] double lon, [Required] int precision, [Required] long lastTimestamp, CancellationToken cancellationToken = default) 117 | { 118 | try 119 | { 120 | // Pull queries matching parameters 121 | var region = RegionHelper.CreateRegion(lat, lon); 122 | 123 | long size = await this._reportService 124 | .GetLatestRegionDataSizeAsync(region, lastTimestamp, cancellationToken); 125 | 126 | // Set Content-Length header with calculated size 127 | Response.ContentLength = size; 128 | 129 | return Ok(); 130 | } 131 | catch (RequestValidationFailedException ex) 132 | { 133 | return BadRequest(ex.ValidationResult); 134 | } 135 | catch (ArgumentNullException) 136 | { 137 | return BadRequest(); 138 | } 139 | } 140 | } 141 | } -------------------------------------------------------------------------------- /CovidSafe/CovidSafe.API/v20200415/Controllers/MessageControllers/ListController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | 7 | using AutoMapper; 8 | using CovidSafe.API.v20200415.Protos; 9 | using CovidSafe.DAL.Helpers; 10 | using CovidSafe.DAL.Services; 11 | using CovidSafe.Entities.Messages; 12 | using CovidSafe.Entities.Validation; 13 | using Microsoft.AspNetCore.Http; 14 | using Microsoft.AspNetCore.Mvc; 15 | 16 | namespace CovidSafe.API.v20200415.Controllers.MessageControllers 17 | { 18 | /// 19 | /// Handles requests to list identifiers which are new to a client 20 | /// 21 | [ApiController] 22 | [ApiVersion("2020-04-15", Deprecated = true)] 23 | [Route("api/Messages/[controller]")] 24 | public class ListController : ControllerBase 25 | { 26 | /// 27 | /// AutoMapper instance for object resolution 28 | /// 29 | private readonly IMapper _map; 30 | /// 31 | /// service layer 32 | /// 33 | private readonly IMessageService _reportService; 34 | 35 | /// 36 | /// Creates a new instance 37 | /// 38 | /// AutoMapper instance 39 | /// service layer 40 | public ListController(IMapper map, IMessageService reportService) 41 | { 42 | // Assign local values 43 | this._map = map; 44 | this._reportService = reportService; 45 | } 46 | 47 | /// 48 | /// Get for a region, starting at a provided timestamp 49 | /// 50 | /// 51 | /// Sample request: 52 | /// 53 | /// GET /api/Messages/List?lat=74.12&lon=-39.12&precision=2&lastTimestamp=0&api-version=2020-04-15 54 | /// 55 | /// 56 | /// Latitude of desired 57 | /// Longitude of desired 58 | /// Precision of desired 59 | /// Latest timestamp on client device, in ms from UNIX epoch 60 | /// Cancellation token (not required in API call) 61 | /// Successful request with results 62 | /// Malformed or invalid request provided 63 | /// Collection of objects matching request parameters 64 | [HttpGet] 65 | [Produces("application/x-protobuf", "application/json")] 66 | [ProducesResponseType(typeof(MessageListResponse), StatusCodes.Status200OK)] 67 | [ProducesResponseType(typeof(ValidationResult), StatusCodes.Status400BadRequest)] 68 | [ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)] 69 | public async Task> GetAsync([Required] double lat, [Required] double lon, [Required] int precision, [Required] long lastTimestamp, CancellationToken cancellationToken = default) 70 | { 71 | try 72 | { 73 | // Pull queries matching parameters, Legacy precision parameter is ignored 74 | var region = RegionHelper.CreateRegion(lat, lon); 75 | 76 | IEnumerable results = await this._reportService 77 | .GetLatestInfoAsync(region, lastTimestamp, cancellationToken); 78 | 79 | // Return using mapped proto object 80 | return Ok(this._map.Map(results)); 81 | } 82 | catch (RequestValidationFailedException ex) 83 | { 84 | return BadRequest(ex.ValidationResult); 85 | } 86 | catch (ArgumentNullException) 87 | { 88 | return BadRequest(); 89 | } 90 | } 91 | 92 | /// 93 | /// Get total size of objects for a based 94 | /// on the provided parameters when using application/x-protobuf 95 | /// 96 | /// 97 | /// Sample request: 98 | /// 99 | /// HEAD /Messages/List?lat=74.12&lon=-39.12&precision=2&lastTimestamp=0 100 | /// 101 | /// 102 | /// Latitude of desired 103 | /// Longitude of desired 104 | /// Precision of desired 105 | /// Latest timestamp on client device, in ms from UNIX epoch 106 | /// Cancellation token (not required in API call) 107 | /// Successful request 108 | /// Malformed or invalid request provided 109 | /// 110 | /// Total size of matching objects (via Content-Type header), in bytes, based 111 | /// on their size when converted to the Protobuf format 112 | /// 113 | [HttpHead] 114 | [ProducesResponseType(StatusCodes.Status200OK)] 115 | [ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)] 116 | public async Task HeadAsync([Required] double lat, [Required] double lon, [Required] int precision, [Required] long lastTimestamp, CancellationToken cancellationToken = default) 117 | { 118 | try 119 | { 120 | // Pull queries matching parameters. Legacy precision is ignored 121 | var region = RegionHelper.CreateRegion(lat, lon); 122 | 123 | long size = await this._reportService 124 | .GetLatestRegionDataSizeAsync(region, lastTimestamp, cancellationToken); 125 | 126 | // Set Content-Length header with calculated size 127 | Response.ContentLength = size; 128 | 129 | return Ok(); 130 | } 131 | catch (RequestValidationFailedException ex) 132 | { 133 | return BadRequest(ex.ValidationResult); 134 | } 135 | catch (ArgumentNullException) 136 | { 137 | return BadRequest(); 138 | } 139 | } 140 | } 141 | } -------------------------------------------------------------------------------- /CovidSafe/CovidSafe.API.Tests/v20200505/Controllers/MessageControllers/SeedReportControllerTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | 5 | using AutoMapper; 6 | using CovidSafe.API.v20200505.Controllers.MessageControllers; 7 | using CovidSafe.API.v20200505.Protos; 8 | using CovidSafe.DAL.Repositories; 9 | using CovidSafe.DAL.Services; 10 | using Microsoft.AspNetCore.Http; 11 | using Microsoft.AspNetCore.Mvc; 12 | using Microsoft.VisualStudio.TestTools.UnitTesting; 13 | using Moq; 14 | 15 | namespace CovidSafe.API.v20200505.Tests.Controllers.MessageControllers 16 | { 17 | /// 18 | /// Unit tests for the class 19 | /// 20 | [TestClass] 21 | public class SeedReportControllerTests 22 | { 23 | /// 24 | /// Test instance 25 | /// 26 | private SeedReportController _controller; 27 | /// 28 | /// Mock instance 29 | /// 30 | private Mock _repo; 31 | /// 32 | /// instance 33 | /// 34 | private MessageService _service; 35 | 36 | /// 37 | /// Creates a new instance 38 | /// 39 | public SeedReportControllerTests() 40 | { 41 | // Configure repo mock 42 | this._repo = new Mock(); 43 | 44 | // Configure service 45 | this._service = new MessageService(this._repo.Object); 46 | 47 | // Create AutoMapper instance 48 | MapperConfiguration mapperConfig = new MapperConfiguration( 49 | opts => opts.AddProfile() 50 | ); 51 | IMapper mapper = mapperConfig.CreateMapper(); 52 | 53 | // Configure controller 54 | this._controller = new SeedReportController(mapper, this._service); 55 | this._controller.ControllerContext = new ControllerContext(); 56 | this._controller.ControllerContext.HttpContext = new DefaultHttpContext(); 57 | } 58 | 59 | /// 60 | /// 61 | /// returns with invalid provided 62 | /// with request 63 | /// 64 | [TestMethod] 65 | public async Task PutAsync_BadRequestObjectWithInvalidSeed() 66 | { 67 | // Arrange 68 | SelfReportRequest requestObj = new SelfReportRequest 69 | { 70 | Region = new Region 71 | { 72 | LatitudePrefix = 10.1234, 73 | LongitudePrefix = 10.1234, 74 | Precision = 4 75 | } 76 | }; 77 | requestObj.Seeds.Add(new BlueToothSeed 78 | { 79 | Seed = "Invalid seed format!", 80 | SequenceEndTime = 1, 81 | SequenceStartTime = 0 82 | }); 83 | 84 | // Act 85 | ActionResult controllerResponse = await this._controller 86 | .PutAsync(requestObj, CancellationToken.None); 87 | 88 | // Assert 89 | Assert.IsNotNull(controllerResponse); 90 | Assert.IsInstanceOfType(controllerResponse, typeof(BadRequestObjectResult)); 91 | } 92 | 93 | /// 94 | /// 95 | /// returns with invalid provided 96 | /// with request 97 | /// 98 | [TestMethod] 99 | public async Task PutAsync_BadRequestWithNoRegion() 100 | { 101 | // Arrange 102 | SelfReportRequest requestObj = new SelfReportRequest(); 103 | requestObj.Seeds.Add(new BlueToothSeed 104 | { 105 | Seed = "00000000-0000-0000-0000-000000000000", 106 | SequenceEndTime = 1, 107 | SequenceStartTime = 0 108 | }); 109 | 110 | // Act 111 | ActionResult controllerResponse = await this._controller 112 | .PutAsync(requestObj, CancellationToken.None); 113 | 114 | // Assert 115 | Assert.IsNotNull(controllerResponse); 116 | Assert.IsInstanceOfType(controllerResponse, typeof(BadRequestResult)); 117 | } 118 | 119 | /// 120 | /// 121 | /// returns when no objects are provided 122 | /// with request 123 | /// 124 | [TestMethod] 125 | public async Task PutAsync_BadRequestWithNoSeeds() 126 | { 127 | // Arrange 128 | SelfReportRequest requestObj = new SelfReportRequest 129 | { 130 | Region = new Region 131 | { 132 | LatitudePrefix = 10.1234, 133 | LongitudePrefix = 10.1234, 134 | Precision = 4 135 | } 136 | }; 137 | 138 | // Act 139 | ActionResult controllerResponse = await this._controller 140 | .PutAsync(requestObj, CancellationToken.None); 141 | 142 | // Assert 143 | Assert.IsNotNull(controllerResponse); 144 | Assert.IsInstanceOfType(controllerResponse, typeof(BadRequestResult)); 145 | } 146 | 147 | /// 148 | /// 149 | /// returns with valid input data 150 | /// 151 | [TestMethod] 152 | public async Task PutAsync_OkWithValidInputs() 153 | { 154 | // Arrange 155 | SelfReportRequest requestObj = new SelfReportRequest 156 | { 157 | Region = new Region 158 | { 159 | LatitudePrefix = 10.1234, 160 | LongitudePrefix = 10.1234, 161 | Precision = 4 162 | } 163 | }; 164 | requestObj.Seeds.Add(new BlueToothSeed 165 | { 166 | SequenceEndTime = DateTimeOffset.UtcNow.AddHours(1).ToUnixTimeMilliseconds(), 167 | SequenceStartTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(), 168 | Seed = "00000000-0000-0000-0000-000000000001" 169 | }); 170 | 171 | // Act 172 | ActionResult controllerResponse = await this._controller 173 | .PutAsync(requestObj, CancellationToken.None); 174 | 175 | // Assert 176 | Assert.IsNotNull(controllerResponse); 177 | Assert.IsInstanceOfType(controllerResponse, typeof(OkResult)); 178 | } 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /CovidSafe/CovidSafe.API.Tests/v20200415/Controllers/MessageControllers/SeedReportControllerTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | 5 | using AutoMapper; 6 | using CovidSafe.API.v20200415.Controllers.MessageControllers; 7 | using CovidSafe.API.v20200415.Protos; 8 | using CovidSafe.DAL.Repositories; 9 | using CovidSafe.DAL.Services; 10 | using Microsoft.AspNetCore.Http; 11 | using Microsoft.AspNetCore.Mvc; 12 | using Microsoft.VisualStudio.TestTools.UnitTesting; 13 | using Moq; 14 | 15 | namespace CovidSafe.API.v20200415.Tests.Controllers.MessageControllers 16 | { 17 | /// 18 | /// Unit tests for the class 19 | /// 20 | [TestClass] 21 | public class SeedReportControllerTests 22 | { 23 | /// 24 | /// Test instance 25 | /// 26 | private SeedReportController _controller; 27 | /// 28 | /// Mock instance 29 | /// 30 | private Mock _repo; 31 | /// 32 | /// instance 33 | /// 34 | private MessageService _service; 35 | 36 | /// 37 | /// Creates a new instance 38 | /// 39 | public SeedReportControllerTests() 40 | { 41 | // Configure repo mock 42 | this._repo = new Mock(); 43 | 44 | // Configure service 45 | this._service = new MessageService(this._repo.Object); 46 | 47 | // Create AutoMapper instance 48 | MapperConfiguration mapperConfig = new MapperConfiguration( 49 | opts => opts.AddProfile() 50 | ); 51 | IMapper mapper = mapperConfig.CreateMapper(); 52 | 53 | // Configure controller 54 | this._controller = new SeedReportController(mapper, this._service); 55 | this._controller.ControllerContext = new ControllerContext(); 56 | this._controller.ControllerContext.HttpContext = new DefaultHttpContext(); 57 | } 58 | 59 | /// 60 | /// 61 | /// returns with invalid provided 62 | /// with request 63 | /// 64 | [TestMethod] 65 | public async Task PutAsync_BadRequestObjectWithInvalidSeed() 66 | { 67 | // Arrange 68 | SelfReportRequest requestObj = new SelfReportRequest 69 | { 70 | Region = new Region 71 | { 72 | LatitudePrefix = 10.1234, 73 | LongitudePrefix = 10.1234, 74 | Precision = 4 75 | } 76 | }; 77 | requestObj.Seeds.Add(new BlueToothSeed 78 | { 79 | Seed = "Invalid seed format!", 80 | SequenceEndTime = 1, 81 | SequenceStartTime = 0 82 | }); 83 | 84 | // Act 85 | ActionResult controllerResponse = await this._controller 86 | .PutAsync(requestObj, CancellationToken.None); 87 | 88 | // Assert 89 | Assert.IsNotNull(controllerResponse); 90 | Assert.IsInstanceOfType(controllerResponse, typeof(BadRequestObjectResult)); 91 | } 92 | 93 | /// 94 | /// 95 | /// returns with null provided 96 | /// with request 97 | /// 98 | [TestMethod] 99 | public async Task PutAsync_BadRequestWithNoRegion() 100 | { 101 | // Arrange 102 | SelfReportRequest requestObj = new SelfReportRequest(); 103 | requestObj.Seeds.Add(new BlueToothSeed 104 | { 105 | Seed = "00000000-0000-0000-0000-000000000001", 106 | SequenceEndTime = DateTimeOffset.UtcNow.AddHours(1).ToUnixTimeMilliseconds(), 107 | SequenceStartTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() 108 | }); 109 | 110 | // Act 111 | ActionResult controllerResponse = await this._controller 112 | .PutAsync(requestObj, CancellationToken.None); 113 | 114 | // Assert 115 | Assert.IsNotNull(controllerResponse); 116 | Assert.IsInstanceOfType(controllerResponse, typeof(BadRequestResult)); 117 | } 118 | 119 | /// 120 | /// 121 | /// returns when no objects are provided 122 | /// with request 123 | /// 124 | [TestMethod] 125 | public async Task PutAsync_BadRequestWithNoSeeds() 126 | { 127 | // Arrange 128 | SelfReportRequest requestObj = new SelfReportRequest 129 | { 130 | Region = new Region 131 | { 132 | LatitudePrefix = 10.1234, 133 | LongitudePrefix = 10.1234, 134 | Precision = 4 135 | } 136 | }; 137 | 138 | // Act 139 | ActionResult controllerResponse = await this._controller 140 | .PutAsync(requestObj, CancellationToken.None); 141 | 142 | // Assert 143 | Assert.IsNotNull(controllerResponse); 144 | Assert.IsInstanceOfType(controllerResponse, typeof(BadRequestResult)); 145 | } 146 | 147 | /// 148 | /// 149 | /// returns with valid input data 150 | /// 151 | [TestMethod] 152 | public async Task PutAsync_OkWithValidInputs() 153 | { 154 | // Arrange 155 | SelfReportRequest requestObj = new SelfReportRequest 156 | { 157 | Region = new Region 158 | { 159 | LatitudePrefix = 10.1234, 160 | LongitudePrefix = 10.1234, 161 | Precision = 4 162 | } 163 | }; 164 | requestObj.Seeds.Add(new BlueToothSeed 165 | { 166 | SequenceEndTime = DateTimeOffset.UtcNow.AddHours(1).ToUnixTimeMilliseconds(), 167 | SequenceStartTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(), 168 | Seed = "00000000-0000-0000-0000-000000000001" 169 | }); 170 | 171 | // Act 172 | ActionResult controllerResponse = await this._controller 173 | .PutAsync(requestObj, CancellationToken.None); 174 | 175 | // Assert 176 | Assert.IsNotNull(controllerResponse); 177 | Assert.IsInstanceOfType(controllerResponse, typeof(OkResult)); 178 | } 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /CovidSafe/CovidSafe.API/v20200611/Controllers/MessageControllers/ListController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | 7 | using AutoMapper; 8 | using CovidSafe.API.v20200611.Protos; 9 | using CovidSafe.DAL.Services; 10 | using CovidSafe.Entities.Messages; 11 | using CovidSafe.Entities.Validation; 12 | using Microsoft.AspNetCore.Http; 13 | using Microsoft.AspNetCore.Mvc; 14 | 15 | namespace CovidSafe.API.v20200611.Controllers.MessageControllers 16 | { 17 | /// 18 | /// Handles requests to list identifiers which are new to a client 19 | /// 20 | [ApiController] 21 | [ApiVersion("2020-06-11")] 22 | [Route("api/Messages/[controller]")] 23 | public class ListController : ControllerBase 24 | { 25 | /// 26 | /// AutoMapper instance for object resolution 27 | /// 28 | private readonly IMapper _map; 29 | /// 30 | /// service layer 31 | /// 32 | private readonly IMessageService _reportService; 33 | 34 | /// 35 | /// Creates a new instance 36 | /// 37 | /// AutoMapper instance 38 | /// service layer 39 | public ListController(IMapper map, IMessageService reportService) 40 | { 41 | // Assign local values 42 | this._map = map; 43 | this._reportService = reportService; 44 | } 45 | 46 | /// 47 | /// Get for a region, starting at a provided timestamp 48 | /// 49 | /// 50 | /// Sample request: 51 | /// 52 | /// GET /api/Messages/List?lat=74&lon=-39&precision=2&lastTimestamp=1591996565365&api-version=2020-06-11 53 | /// 54 | /// 55 | /// Latitude prefix (no decimals) of desired 56 | /// Longitude prefix (no decimals) of desired 57 | /// Precision of desired . 0 - whole globe, 8 - single degree precision 58 | /// Latest timestamp on client device, in ms from UNIX epoch 59 | /// Cancellation token (not required in API call) 60 | /// Successful request with results 61 | /// Malformed or invalid request provided 62 | /// Collection of objects matching request parameters 63 | [HttpGet] 64 | [Produces("application/x-protobuf", "application/json")] 65 | [ProducesResponseType(typeof(MessageListResponse), StatusCodes.Status200OK)] 66 | [ProducesResponseType(typeof(ValidationResult), StatusCodes.Status400BadRequest)] 67 | [ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)] 68 | public async Task> GetAsync([Required] int lat, [Required] int lon, [Required] int precision, [Required] long lastTimestamp, CancellationToken cancellationToken = default) 69 | { 70 | try 71 | { 72 | // Pull queries matching parameters 73 | var region = new Entities.Geospatial.Region 74 | { 75 | LatitudePrefix = lat, 76 | LongitudePrefix = lon, 77 | Precision = precision 78 | }; 79 | 80 | IEnumerable results = await this._reportService 81 | .GetLatestInfoAsync(region, lastTimestamp, cancellationToken); 82 | 83 | // Return using mapped proto object 84 | return Ok(this._map.Map(results)); 85 | } 86 | catch (RequestValidationFailedException ex) 87 | { 88 | return BadRequest(ex.ValidationResult); 89 | } 90 | catch (ArgumentNullException) 91 | { 92 | return BadRequest(); 93 | } 94 | } 95 | 96 | /// 97 | /// Get total size of objects for a based 98 | /// on the provided parameters when using application/x-protobuf 99 | /// 100 | /// 101 | /// Sample request: 102 | /// 103 | /// HEAD /Messages/List?lat=74&lon=-39&precision=2&lastTimestamp=1591996565365&api-version=2020-06-11 104 | /// 105 | /// 106 | /// Latitude prefix (no decimals) of desired 107 | /// Longitude prefix (no decimals) of desired 108 | /// Precision of desired . 0 - whole globe, 8 - single degree precision 109 | /// Latest timestamp on client device, in ms from UNIX epoch 110 | /// Cancellation token (not required in API call) 111 | /// Successful request 112 | /// Malformed or invalid request provided 113 | /// 114 | /// Total size of matching objects (via Content-Type header), in bytes, based 115 | /// on their size when converted to the Protobuf format 116 | /// 117 | [HttpHead] 118 | [ProducesResponseType(StatusCodes.Status200OK)] 119 | [ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)] 120 | public async Task HeadAsync([Required] int lat, [Required] int lon, [Required] int precision, [Required] long lastTimestamp, CancellationToken cancellationToken = default) 121 | { 122 | try 123 | { 124 | // Pull queries matching parameters 125 | var region = new Entities.Geospatial.Region 126 | { 127 | LatitudePrefix = lat, 128 | LongitudePrefix = lon, 129 | Precision = precision 130 | }; 131 | 132 | long size = await this._reportService 133 | .GetLatestRegionDataSizeAsync(region, lastTimestamp, cancellationToken); 134 | 135 | // Set Content-Length header with calculated size 136 | Response.ContentLength = size; 137 | 138 | return Ok(); 139 | } 140 | catch (RequestValidationFailedException ex) 141 | { 142 | return BadRequest(ex.ValidationResult); 143 | } 144 | catch (ArgumentNullException) 145 | { 146 | return BadRequest(); 147 | } 148 | } 149 | } 150 | } -------------------------------------------------------------------------------- /CovidSafe/CovidSafe.API/Startup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics.CodeAnalysis; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Reflection; 7 | 8 | using AutoMapper; 9 | using CovidSafe.API.Swagger; 10 | using CovidSafe.DAL.Repositories; 11 | using CovidSafe.DAL.Repositories.Cosmos; 12 | using CovidSafe.DAL.Repositories.Cosmos.Client; 13 | using CovidSafe.DAL.Services; 14 | using Microsoft.AspNetCore.Builder; 15 | using Microsoft.AspNetCore.Hosting; 16 | using Microsoft.AspNetCore.Mvc; 17 | using Microsoft.AspNetCore.Mvc.ApiExplorer; 18 | using Microsoft.AspNetCore.Mvc.Versioning; 19 | using Microsoft.Extensions.Configuration; 20 | using Microsoft.Extensions.DependencyInjection; 21 | using Microsoft.Extensions.Options; 22 | using Microsoft.Net.Http.Headers; 23 | using Microsoft.OpenApi.Models; 24 | using Swashbuckle.AspNetCore.SwaggerGen; 25 | using WebApiContrib.Core.Formatter.Protobuf; 26 | 27 | namespace CovidSafe.API 28 | { 29 | /// 30 | /// Service registration for the web application 31 | /// 32 | /// 33 | /// CS1591: Ignores missing documentation warnings. 34 | /// CodeCoverageExclusion: Required DI injections and core Startup procedures. 35 | /// 36 | #pragma warning disable CS1591 37 | [ExcludeFromCodeCoverage] 38 | public class Startup 39 | { 40 | /// 41 | /// Application configuration singleton 42 | /// 43 | public IConfiguration Configuration { get; } 44 | 45 | /// 46 | /// Creates a new instance 47 | /// 48 | /// Application configuration singleton 49 | public Startup(IConfiguration configuration) 50 | { 51 | Configuration = configuration; 52 | } 53 | 54 | // This method gets called by the runtime. Use this method to add services to the container. 55 | public void ConfigureServices(IServiceCollection services) 56 | { 57 | // Enable AppInsights 58 | services.AddApplicationInsightsTelemetry(); 59 | 60 | // Controller setup 61 | services 62 | .AddMvc( 63 | option => 64 | { 65 | // Use default ProtobufFormatterOptions 66 | ProtobufFormatterOptions formatterOptions = new ProtobufFormatterOptions(); 67 | option.InputFormatters.Insert(1, new ProtobufInputFormatter(formatterOptions)); 68 | option.OutputFormatters.Insert(1, new ProtobufOutputFormatter(formatterOptions)); 69 | option.FormatterMappings.SetMediaTypeMappingForFormat( 70 | "protobuf", 71 | MediaTypeHeaderValue.Parse("application/x-protobuf") 72 | ); 73 | } 74 | ) 75 | .SetCompatibilityVersion(CompatibilityVersion.Version_2_1); 76 | 77 | #region Database Configuration 78 | 79 | // Get configuration sections 80 | services.Configure(this.Configuration.GetSection("CosmosSchema")); 81 | 82 | // Create Cosmos Connection, based on connection string location 83 | if(!String.IsNullOrEmpty(this.Configuration.GetConnectionString("CosmosConnection"))) { 84 | services.AddTransient(cf => new CosmosConnectionFactory(this.Configuration.GetConnectionString("CosmosConnection"))); 85 | } 86 | else 87 | { 88 | // Attempt to pull from generic 'CosmosConnection' setting 89 | // Throws exception if not defined 90 | services.AddTransient(cf => new CosmosConnectionFactory(this.Configuration["CosmosConnection"])); 91 | } 92 | 93 | // Configure data repository implementations 94 | services.AddTransient(); 95 | services.AddSingleton(); 96 | 97 | #endregion 98 | 99 | // Add AutoMapper profiles 100 | services.AddAutoMapper( 101 | typeof(v20200415.MappingProfiles), 102 | typeof(v20200505.MappingProfiles), 103 | typeof(v20200611.MappingProfiles) 104 | ); 105 | 106 | // Configure service layer 107 | services.AddSingleton(); 108 | 109 | // Enable API versioning 110 | services.AddApiVersioning(o => 111 | { 112 | o.ApiVersionReader = new QueryStringApiVersionReader("api-version"); 113 | o.AssumeDefaultVersionWhenUnspecified = true; 114 | o.DefaultApiVersion = new ApiVersion( 115 | DateTime.Parse(this.Configuration["DefaultApiVersion"]) 116 | ); 117 | // Share supported API versions in headers 118 | o.ReportApiVersions = true; 119 | }); 120 | services.AddVersionedApiExplorer( 121 | options => 122 | { 123 | // Enable API version in URL 124 | options.SubstituteApiVersionInUrl = true; 125 | options.DefaultApiVersion = new ApiVersion( 126 | DateTime.Parse(this.Configuration["DefaultApiVersion"]) 127 | ); 128 | } 129 | ); 130 | 131 | // Add Swagger generator 132 | services.AddTransient, SwaggerConfigureOptions>(); 133 | services.AddSwaggerGen(c => 134 | { 135 | // Set the comments path for the Swagger JSON 136 | var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; 137 | var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile); 138 | c.IncludeXmlComments(xmlPath); 139 | }); 140 | } 141 | 142 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 143 | public void Configure(IApplicationBuilder app, IHostingEnvironment env, IApiVersionDescriptionProvider provider) 144 | { 145 | if (env.IsDevelopment()) 146 | { 147 | app.UseDeveloperExceptionPage(); 148 | } 149 | else 150 | { 151 | app.UseHsts(); 152 | } 153 | 154 | app.UseHttpsRedirection(); 155 | app.UseMvc(); 156 | 157 | // Add Swagger 158 | app.UseSwagger(c => 159 | { 160 | // Add API hosts 161 | c.PreSerializeFilters.Add((swagger, httpRequest) => 162 | { 163 | // Parse host list from configuration 164 | List servers = this.Configuration["SwaggerHosts"] 165 | .Split(';') 166 | .Select(s => new OpenApiServer 167 | { 168 | Url = s 169 | }) 170 | .ToList(); 171 | 172 | // Set servers (hosts) property 173 | swagger.Servers = servers; 174 | }); 175 | }); 176 | 177 | // Add SwaggerUI 178 | app.UseSwaggerUI(c => 179 | { 180 | // Enable UI for multiple API versions 181 | // Descending operator forces latest version to appear first 182 | foreach(ApiVersionDescription description in provider.ApiVersionDescriptions.OrderByDescending(x => x.ApiVersion)) 183 | { 184 | c.SwaggerEndpoint( 185 | $"/swagger/{description.GroupName}/swagger.json", 186 | description.GroupName.ToUpperInvariant() 187 | ); 188 | } 189 | }); 190 | } 191 | } 192 | #pragma warning restore CS1591 193 | } 194 | --------------------------------------------------------------------------------