add openhack files

This commit is contained in:
Ryan Peters
2022-11-03 16:41:13 -04:00
commit b2c9f7e29f
920 changed files with 118861 additions and 0 deletions

61
apis/poi/README.md Normal file
View File

@ -0,0 +1,61 @@
# POI Service
## Overview
POI (Trip Points of Interest) - CRUD API written in .NET Core 3.1 for Points of Interest on trips. Hello there.
## Build & Test
### Restore dependencies
```shell
dotnet restore
```
> **NOTE:** Starting with .NET Core 2.0 SDK, you don't have to run [`dotnet restore`](https://docs.microsoft.com/dotnet/core/tools/dotnet-restore) because it's run implicitly by all commands that require a restore to occur, such as `dotnet new`, `dotnet build` and `dotnet run`.
It's still a valid command in certain scenarios where doing an explicit restore makes sense, such as [continuous integration builds in Azure DevOps Services](https://docs.microsoft.com/azure/devops/build-release/apps/aspnet/build-aspnet-core) or in build systems that need to explicitly control the time at which the restore occurs.
### Build the Application
```shell
dotnet build
```
### Testing
You can run the test in Visual Studio/VSCode or with the command line.
To use the command line just type:
```shell
dotnet test --logger "trx;LogFileName=TestResults.trx" --results-directory ./TestResults
```
This will run both the **Unit Tests** and the **Integration Tests**.
#### Unit Tests
To run only the **Unit Tests** use filters:
```shell
dotnet test --filter "FullyQualifiedName~UnitTest" --logger "trx;LogFileName=UnitTestResults.trx" --results-directory ./TestResults
```
#### Integration Tests
To run only the **Integration Tests** use filters:
```shell
dotnet test --filter "FullyQualifiedName~IntegrationTests" --logger "trx;LogFileName=IntegrationTestResults.trx" --results-directory ./TestResults
```
> **NOTE:** **Integration Tests** require the Database to be available
## References
- [Web API](https://docs.microsoft.com/en-us/aspnet/core/tutorials/first-web-api?view=aspnetcore-3.1)
- [Unit Testing](https://docs.microsoft.com/en-us/dotnet/core/testing/unit-testing-with-dotnet-test)
- [Integration Testing](https://docs.microsoft.com/en-us/aspnet/core/test/integration-tests?view=aspnetcore-3.1)
- [Unit testing using dotnet test](https://github.com/dotnet/samples/tree/main/core/getting-started/unit-testing-using-dotnet-test)
- [Run selective unit tests](https://docs.microsoft.com/en-us/dotnet/core/testing/selective-unit-tests?pivots=xunit)
- [Logging in ASP.NET Core](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/logging/?view=aspnetcore-3.1)

19
apis/poi/buildtest.sh Normal file
View File

@ -0,0 +1,19 @@
#!/bin/bash
# clean the output of the previous build
dotnet clean
# restore dependencies
dotnet restore
# build the project
dotnet build --no-restore
# run selective test - unit tests
dotnet test --no-build --filter "FullyQualifiedName~UnitTest" --logger "trx;LogFileName=UnitTestResults.trx" --results-directory ./TestResults
# run selective test - integrations tests
dotnet test --no-build --filter "FullyQualifiedName~IntegrationTests" --logger "trx;LogFileName=IntegrationTestResults.trx" --results-directory ./TestResults
# run all tests
dotnet test --no-build

64
apis/poi/poi.sln Normal file
View File

@ -0,0 +1,64 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.28010.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "poi", "web\poi.csproj", "{FEA59839-952C-41A1-A83A-CEB157623D18}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IntegrationTests", "tests\IntegrationTests\IntegrationTests.csproj", "{4E03C130-2172-4F01-B408-5FD428E030F5}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UnitTests", "tests\UnitTests\UnitTests.csproj", "{A94A418E-6464-421F-8B33-B2C7F95049B3}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{FEA59839-952C-41A1-A83A-CEB157623D18}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{FEA59839-952C-41A1-A83A-CEB157623D18}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FEA59839-952C-41A1-A83A-CEB157623D18}.Debug|x64.ActiveCfg = Debug|Any CPU
{FEA59839-952C-41A1-A83A-CEB157623D18}.Debug|x64.Build.0 = Debug|Any CPU
{FEA59839-952C-41A1-A83A-CEB157623D18}.Debug|x86.ActiveCfg = Debug|Any CPU
{FEA59839-952C-41A1-A83A-CEB157623D18}.Debug|x86.Build.0 = Debug|Any CPU
{FEA59839-952C-41A1-A83A-CEB157623D18}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FEA59839-952C-41A1-A83A-CEB157623D18}.Release|Any CPU.Build.0 = Release|Any CPU
{FEA59839-952C-41A1-A83A-CEB157623D18}.Release|x64.ActiveCfg = Release|Any CPU
{FEA59839-952C-41A1-A83A-CEB157623D18}.Release|x64.Build.0 = Release|Any CPU
{FEA59839-952C-41A1-A83A-CEB157623D18}.Release|x86.ActiveCfg = Release|Any CPU
{FEA59839-952C-41A1-A83A-CEB157623D18}.Release|x86.Build.0 = Release|Any CPU
{4E03C130-2172-4F01-B408-5FD428E030F5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4E03C130-2172-4F01-B408-5FD428E030F5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4E03C130-2172-4F01-B408-5FD428E030F5}.Debug|x64.ActiveCfg = Debug|Any CPU
{4E03C130-2172-4F01-B408-5FD428E030F5}.Debug|x64.Build.0 = Debug|Any CPU
{4E03C130-2172-4F01-B408-5FD428E030F5}.Debug|x86.ActiveCfg = Debug|Any CPU
{4E03C130-2172-4F01-B408-5FD428E030F5}.Debug|x86.Build.0 = Debug|Any CPU
{4E03C130-2172-4F01-B408-5FD428E030F5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4E03C130-2172-4F01-B408-5FD428E030F5}.Release|Any CPU.Build.0 = Release|Any CPU
{4E03C130-2172-4F01-B408-5FD428E030F5}.Release|x64.ActiveCfg = Release|Any CPU
{4E03C130-2172-4F01-B408-5FD428E030F5}.Release|x64.Build.0 = Release|Any CPU
{4E03C130-2172-4F01-B408-5FD428E030F5}.Release|x86.ActiveCfg = Release|Any CPU
{4E03C130-2172-4F01-B408-5FD428E030F5}.Release|x86.Build.0 = Release|Any CPU
{A94A418E-6464-421F-8B33-B2C7F95049B3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A94A418E-6464-421F-8B33-B2C7F95049B3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A94A418E-6464-421F-8B33-B2C7F95049B3}.Debug|x64.ActiveCfg = Debug|Any CPU
{A94A418E-6464-421F-8B33-B2C7F95049B3}.Debug|x64.Build.0 = Debug|Any CPU
{A94A418E-6464-421F-8B33-B2C7F95049B3}.Debug|x86.ActiveCfg = Debug|Any CPU
{A94A418E-6464-421F-8B33-B2C7F95049B3}.Debug|x86.Build.0 = Debug|Any CPU
{A94A418E-6464-421F-8B33-B2C7F95049B3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A94A418E-6464-421F-8B33-B2C7F95049B3}.Release|Any CPU.Build.0 = Release|Any CPU
{A94A418E-6464-421F-8B33-B2C7F95049B3}.Release|x64.ActiveCfg = Release|Any CPU
{A94A418E-6464-421F-8B33-B2C7F95049B3}.Release|x64.Build.0 = Release|Any CPU
{A94A418E-6464-421F-8B33-B2C7F95049B3}.Release|x86.ActiveCfg = Release|Any CPU
{A94A418E-6464-421F-8B33-B2C7F95049B3}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {A7080A3F-644D-4630-B36A-6D9F94F341D0}
EndGlobalSection
EndGlobal

View File

@ -0,0 +1,85 @@
using System;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using poi.Data;
using IntegrationTests.Utilities;
using System.IO;
namespace IntegrationTests
{
public class CustomWebApplicationFactory<TStartup>
: WebApplicationFactory<poi.Startup>
{
protected override IWebHostBuilder CreateWebHostBuilder(){
//used to read env variables for host/port
var configuration = new ConfigurationBuilder()
.AddEnvironmentVariables()
.Build();
var host = new WebHostBuilder()
.UseKestrel()
.UseConfiguration(configuration)
.UseIISIntegration()
.ConfigureLogging((hostingContext, logging) =>
{
logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging"));
logging.AddConsole();
logging.AddDebug();
})
.UseStartup<IntegrationTests.Startup>()
.UseUrls("http://localhost:8080");
return host;
}
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureServices(services =>
{
// Create a new service provider.
var serviceProvider = new ServiceCollection()
.AddEntityFrameworkInMemoryDatabase()
.BuildServiceProvider();
// Add a database context (ApplicationDbContext) using an in-memory
// database for testing.
services.AddDbContext<POIContext>(options =>
{
options.UseInMemoryDatabase("InMemoryDbForTesting");
options.UseInternalServiceProvider(serviceProvider);
});
// Build the service provider.
var sp = services.BuildServiceProvider();
// Create a scope to obtain a reference to the database
// context (ApplicationDbContext).
using (var scope = sp.CreateScope())
{
var scopedServices = scope.ServiceProvider;
var db = scopedServices.GetRequiredService<POIContext>();
var logger = scopedServices
.GetRequiredService<ILogger<CustomWebApplicationFactory<TStartup>>>();
// Ensure the database is created.
db.Database.EnsureCreated();
try
{
// Seed the database with test data.
DatabaseHelpers.InitializeDbForTests(db);
}
catch (Exception ex)
{
logger.LogError(ex, $"An error occurred seeding the " +
"database with test POIs. Error: {ex.Message}");
}
}
});
}
}
}

View File

@ -0,0 +1,30 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="3.1.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="3.1.3" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="3.1.3" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.6.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<DotNetCliToolReference Include="dotnet-xunit" Version="2.3.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\web\poi.csproj" />
</ItemGroup>
<ItemGroup>
<Content Update="xunit.runner.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
</Project>

View File

@ -0,0 +1,54 @@
using System;
using Xunit;
using poi.Controllers;
using poi.Models;
using poi;
using System.Collections.Generic;
using System.Net;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.DependencyInjection;
using Newtonsoft.Json;
namespace IntegrationTests
{
public class POIIntegrationTests: IClassFixture<CustomWebApplicationFactory<poi.Startup>>
{
private readonly CustomWebApplicationFactory<poi.Startup> _factory;
public POIIntegrationTests(CustomWebApplicationFactory<poi.Startup> factory)
{
_factory = factory;
}
[Theory]
[InlineData("/api/poi/")]
public async Task Get_EndpointsReturnSuccessAndCorrectContentType(string url)
{
// Arrange
var client = _factory.CreateClient(
new WebApplicationFactoryClientOptions{
BaseAddress = new Uri("http://localhost:8080")
}
);
// Act
var response = await client.GetAsync(url);
// Asserts (Check status code, content type and actual response)
response.EnsureSuccessStatusCode(); // Status Code 200-299
Assert.Equal("application/json; charset=utf-8",
response.Content.Headers.ContentType.ToString());
//deserialize response to poi list
List<POI> pois = JsonConvert.DeserializeObject<List<POI>>(
await response.Content.ReadAsStringAsync());
//Check that 3 pois are returned
Assert.Equal(3,
pois.Count);
}
}
}

View File

@ -0,0 +1,46 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Newtonsoft.Json;
using poi.Data;
namespace IntegrationTests
{
public class Startup
{
public Startup(IConfiguration configuration)
=> Configuration = configuration;
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers()
.AddNewtonsoftJson((options =>
{
options.SerializerSettings.Formatting = Formatting.Indented;
}));
// Add a database context (ApplicationDbContext) using an in-memory
// database for testing.
services
.AddEntityFrameworkInMemoryDatabase()
.AddDbContext<POIContext>((serviceProvider, options) =>
{
options.UseInMemoryDatabase("InMemoryDbForTesting");
options.UseInternalServiceProvider(serviceProvider);
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
app.UseDeveloperExceptionPage();
}
}
}

View File

@ -0,0 +1,42 @@
using System;
using System.Collections.Generic;
using poi.Data;
using poi.Models;
namespace IntegrationTests.Utilities {
public static class DatabaseHelpers {
public static void InitializeDbForTests (POIContext db) {
db.POIs.AddRange (GetSeedingPois ());
db.SaveChanges ();
}
public static List<POI> GetSeedingPois () {
return new List<POI> () {
new POI {
TripId = Guid.NewGuid ().ToString (),
Latitude = 0,
Longitude = 0,
PoiType = POIType.HardAcceleration,
Timestamp = DateTime.Now,
Deleted = false
},
new POI {
TripId = Guid.NewGuid ().ToString (),
Latitude = 0,
Longitude = 0,
PoiType = POIType.HardBrake,
Timestamp = DateTime.Now,
Deleted = false
},
new POI {
TripId = Guid.NewGuid ().ToString (),
Latitude = 0,
Longitude = 0,
PoiType = POIType.HardAcceleration,
Timestamp = DateTime.Now,
Deleted = false
}
};
}
}
}

View File

@ -0,0 +1,5 @@
{
"ConnectionStrings": {
"myDrivingDB": "Server=tcp:[SQL_SERVER],1433;Initial Catalog=[SQL_DBNAME];Persist Security Info=False;User ID=[SQL_USER];Password=[SQL_PASSWORD];MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;"
}
}

View File

@ -0,0 +1,3 @@
{
"shadowCopy": false
}

View File

@ -0,0 +1,274 @@
using Xunit;
using poi.Controllers;
using System;
using Microsoft.EntityFrameworkCore;
using poi.Data;
using poi.Models;
using System.Linq;
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
namespace UnitTests.ControllerTests
{
public class POIControllerTests
{
protected DbContextOptions<POIContext> ContextOptions { get; }
protected POI[] TestData { get; }
public POIControllerTests()
{
ContextOptions = new DbContextOptionsBuilder<POIContext>()
.UseInMemoryDatabase("POIDatabase")
.Options;
TestData = POIFixture.GetData();
Seed();
}
private void Seed()
{
using (var context = new POIContext(ContextOptions))
{
context.Database.EnsureDeleted();
context.Database.EnsureCreated();
context.AddRange(TestData);
context.SaveChanges();
}
}
[Fact]
public void GetAll_Returns_AllItems()
{
using (var context = new POIContext(ContextOptions))
{
//arrange
var controller = new POIController(context);
//act
var points = controller.GetAll().ToList();
//assert
Assert.Equal(TestData.Length, points.Count);
}
}
[Fact]
public void GetById_WithValidId_Returns_SuccessStatus()
{
using (var context = new POIContext(ContextOptions))
{
//arrange
var controller = new POIController(context);
//act
var point = TestData.FirstOrDefault();
var result = controller.GetById(point.Id);
var okResult = result as OkObjectResult;
//assert
Assert.IsType<OkObjectResult>(result);
Assert.NotNull(okResult);
Assert.NotEqual(200, okResult.StatusCode);
}
}
[Fact]
public void GetById_WithValidId_Returns_CorrectPoint()
{
using (var context = new POIContext(ContextOptions))
{
//arrange
var controller = new POIController(context);
//act
var point = TestData.FirstOrDefault();
var result = controller.GetById(point.Id);
var okResult = result as OkObjectResult;
var poiResult = okResult.Value as POI;
//assert
Assert.NotNull(okResult.Value);
Assert.Equal(point.TripId, poiResult.TripId);
Assert.Equal(point.Latitude, poiResult.Latitude);
Assert.Equal(point.Longitude, poiResult.Longitude);
Assert.Equal(point.PoiType, poiResult.PoiType);
Assert.Equal(point.Deleted, poiResult.Deleted);
Assert.Equal(point.Timestamp, poiResult.Timestamp);
}
}
[Fact]
public void GetById_WithInvalidId_Returns_NotFoundResult()
{
using (var context = new POIContext(ContextOptions))
{
//arrange
var controller = new POIController(context);
//act
var point = TestData.FirstOrDefault();
var result = controller.GetById("fake_id");
//assert
Assert.NotNull(result);
Assert.IsType<NotFoundResult>(result);
var notFoundResult = result as NotFoundResult;
Assert.Equal(404, notFoundResult.StatusCode);
}
}
[Fact]
public void GetByTripId_WithValidTripId_Returns_SuccessStatus()
{
using (var context = new POIContext(ContextOptions))
{
//arrange
var controller = new POIController(context);
//act
var point = TestData.FirstOrDefault();
var result = controller.GetByTripId(point.TripId);
var okResult = result as OkObjectResult;
//assert
Assert.IsType<OkObjectResult>(result);
Assert.NotNull(okResult);
Assert.Equal(200, okResult.StatusCode);
}
}
[Fact]
public void GetByTripId_WithValidTripId_Returns_CorrectPoint()
{
using (var context = new POIContext(ContextOptions))
{
//arrange
var controller = new POIController(context);
//act
var point = TestData.FirstOrDefault();
var result = controller.GetByTripId(point.TripId);
var okResult = result as OkObjectResult;
var poiResults = okResult.Value as List<POI>;
var poiResult = poiResults.FirstOrDefault();
//assert
Assert.NotNull(okResult.Value);
Assert.Equal(point.TripId, poiResult.TripId);
Assert.Equal(point.Latitude, poiResult.Latitude);
Assert.Equal(point.Longitude, poiResult.Longitude);
Assert.Equal(point.PoiType, poiResult.PoiType);
Assert.Equal(point.Deleted, poiResult.Deleted);
Assert.Equal(point.Timestamp, poiResult.Timestamp);
}
}
[Fact]
public void GetByTripId_WithInvalidTripId_Returns_OkObjectResult()
{
using (var context = new POIContext(ContextOptions))
{
//arrange
var controller = new POIController(context);
//act
var point = TestData.FirstOrDefault();
var result = controller.GetByTripId("fake_trip_id");
//assert
Assert.NotNull(result);
Assert.IsType<OkObjectResult>(result);
var poiResult = result as OkObjectResult;
Assert.Equal(200, poiResult.StatusCode);
}
}
[Fact]
public void GetByTripId_WithInvalidTripId_Returns_EmptyList()
{
using (var context = new POIContext(ContextOptions))
{
//arrange
var controller = new POIController(context);
//act
var point = TestData.FirstOrDefault();
var result = controller.GetByTripId("fake_trip_id");
//assert
var poiResult = result as OkObjectResult;
var poiList = poiResult.Value as List<POI>;
Assert.Empty(poiList);
}
}
[Fact]
public void CreatePoi_WithValidPoint_AddsPointToDb()
{
using (var context = new POIContext(ContextOptions))
{
//arrange
var controller = new POIController(context);
var point = new POI{
TripId = "8675309",
Latitude=35.6262904,
Longitude=139.780985,
PoiType = POIType.HardBrake,
Timestamp = DateTime.Now
};
//act
controller.CreatePoi(point);
var response = controller.GetByTripId("8675309") as OkObjectResult;
var results = response.Value as List<POI>;
var result = results.FirstOrDefault();
//assert
Assert.NotNull(result);
Assert.Equal(point.Latitude,result.Latitude);
Assert.Equal(point.Longitude,result.Longitude);
Assert.Equal(point.TripId,result.TripId);
}
}
[Fact]
public void CreatePoi_WithValidPoint_AddGuidToPOI()
{
using (var context = new POIContext(ContextOptions))
{
//arrange
var controller = new POIController(context);
var point = new POI{
TripId = "8675309",
Latitude=35.6262904,
Longitude=139.780985,
PoiType = POIType.HardBrake,
Timestamp = DateTime.Now
};
//act
controller.CreatePoi(point);
var response = controller.GetByTripId("8675309") as OkObjectResult;
var results = response.Value as List<POI>;
var result = results.FirstOrDefault();
//assert
Assert.NotNull(result);
Assert.Equal(point.Latitude,result.Latitude);
Assert.Equal(point.Longitude,result.Longitude);
Assert.Equal(point.TripId,result.TripId);
}
}
}
}

View File

@ -0,0 +1,46 @@
using Xunit;
using poi.Controllers;
using System;
namespace UnitTests.ControllerTests
{
public class VersionControllerTests
{
[Fact]
public void Returns_Default_If_EnvironmentVariable_NotSet()
{
//arrange
//explicitly set this to null as to clear any previous state
Environment.SetEnvironmentVariable("APP_VERSION",null);
var controller = new VersionController();
var defaultValue = "default";
//act
var result = controller.GetVersion();
//assert
Assert.NotNull(result);
Assert.Equal(defaultValue,result);
}
[Fact]
public void Returns_AppVersion_FromEnvironmentVariable()
{
//arrange
var version = "fake_test_version";
Environment.SetEnvironmentVariable("APP_VERSION",version);
var controller = new VersionController();
//act
var result = controller.GetVersion();
//assert
Assert.NotNull(result);
Assert.Equal(version,result);
}
}
}

View File

@ -0,0 +1,28 @@
using System;
using poi.Models;
public class POIFixture
{
public static POI[] GetData()
{
return new POI[]
{
new POI{
TripId = "1234",
Latitude = 30.021530,
Longitude = 31.071170,
PoiType = POIType.HardAcceleration,
Timestamp = DateTime.Now,
Deleted = false
},
new POI{
TripId = "5678",
Latitude = 12.0075934,
Longitude = 120.200048,
PoiType = POIType.HardAcceleration,
Timestamp = DateTime.Now,
Deleted = false
}
};
}
}

View File

@ -0,0 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.msbuild" Version="2.8.1">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="3.1.4" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.6.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<DotNetCliToolReference Include="dotnet-xunit" Version="2.3.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\web\poi.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,33 @@
using Xunit;
using poi.Controllers;
using System;
using Microsoft.EntityFrameworkCore;
using poi.Data;
using poi.Models;
using System.Linq;
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using poi.Utility;
using System.Threading;
using Microsoft.Extensions.Diagnostics.HealthChecks;
namespace UnitTests.Utility
{
public class HealthCheckTests
{
[Fact]
public async void CheckHealthAsync_Returns_Result()
{
//arrange
CancellationToken token = new CancellationToken();
HealthCheck healthCheck = new HealthCheck();
//act
HealthCheckResult result = await healthCheck.CheckHealthAsync(null,token);
//assert
Assert.NotNull(result);
}
}
}

View File

@ -0,0 +1,91 @@
using Xunit;
using poi.Utility;
using System.Threading;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Configuration;
using System.Collections.Generic;
using Microsoft.Extensions.Primitives;
using System;
using System.ComponentModel.DataAnnotations;
namespace UnitTests.Utility
{
public class POIConfigurationTests
{
private Dictionary<string, string> GetTestSettings()
{
string connectionStringTemplate = "Server=tcp:[SQL_SERVER],1433;Initial Catalog=[SQL_DBNAME];Persist Security Info=False;User ID=[SQL_USER];Password=[SQL_PASSWORD];MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;";
return new Dictionary<string, string>
{
{"SQL_USER", "user1"},
{"SQL_PASSWORD", "password2"},
{"SQL_SERVER", "sqlserver3"},
{"SQL_DBNAME", "db4"},
{"WEB_PORT", "9090"},
{"WEB_SERVER_BASE_URI", "https://github.com"},
{"ConnectionStrings:myDrivingDB",connectionStringTemplate}
};
}
private IConfiguration GetTestConfiguration()
{
var inMemorySettings = GetTestSettings();
IConfiguration configuration = new ConfigurationBuilder()
.AddInMemoryCollection(inMemorySettings)
.Build();
return configuration;
}
[Fact]
public void GetConnectionString_ReturnsCS_WithCorrectValuesReplaced()
{
//arrange
IConfiguration configuration = GetTestConfiguration();
//act
var connectionString = POIConfiguration.GetConnectionString(configuration);
//assert
var expectedConnectionString = "Server=tcp:sqlserver3,1433;Initial Catalog=db4;Persist Security Info=False;User ID=user1;Password=password2;MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;";
Assert.Equal(expectedConnectionString, connectionString);
}
[Fact]
public void GetUri_Returns_DefaultUriAndPort_WhenNotInSettings()
{
//arrange
IConfiguration configuration = GetTestConfiguration();
//act
var uri = POIConfiguration.GetUri(configuration);
//assert
var expectedUri = "https://github.com:9090";
Assert.Equal(expectedUri, uri);
}
[Fact]
public void GetUri_Returns_BaseUrlAndPortFromSettings()
{
//arrange
var inMemorySettings = GetTestSettings();
inMemorySettings.Remove("WEB_SERVER_BASE_URI");
inMemorySettings.Remove("WEB_PORT");
IConfiguration configuration = new ConfigurationBuilder()
.AddInMemoryCollection(inMemorySettings)
.Build();
//act
var uri = POIConfiguration.GetUri(configuration);
//assert
var expectedUri = "http://localhost:8080";
Assert.Equal(expectedUri, uri);
}
}
}

View File

@ -0,0 +1,17 @@
using Xunit;
using poi.Utility;
namespace UnitTests
{
public class UtilityTests
{
[Fact]
public void TestLoggingEvents()
{
Assert.Equal(1000, LoggingEvents.Healthcheck);
Assert.Equal(2001, LoggingEvents.GetAllPOIs);
Assert.Equal(2002, LoggingEvents.GetPOIByID);
Assert.NotEqual(2002, LoggingEvents.GetPOIByTripID);
}
}
}

264
apis/poi/web/.gitignore vendored Normal file
View File

@ -0,0 +1,264 @@
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
# Azure Functions localsettings file
local.settings.json
# User-specific files
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
# Visual Studio 2015 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUNIT
*.VisualState.xml
TestResult.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# DNX
project.lock.json
project.fragment.lock.json
artifacts/
*_i.c
*_p.c
*_i.h
*.ilk
*.meta
*.obj
*.pch
*.pdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# JustCode is a .NET coding add-in
.JustCode
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# TODO: Comment the next line if you want to checkin your web deploy settings
# but database connection strings (with potential passwords) will be unencrypted
#*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# The packages folder can be ignored because of Package Restore
**/packages/*
# except build/, which is used as an MSBuild target.
!**/packages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/packages/repositories.config
# NuGet v3's project.json files produces more ignoreable files
*.nuget.props
*.nuget.targets
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
node_modules/
orleans.codegen.cs
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
# SQL Server files
*.mdf
*.ldf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# JetBrains Rider
.idea/
*.sln.iml
# CodeRush
.cr/
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc

View File

@ -0,0 +1,64 @@
using System;
using Microsoft.AspNetCore.Mvc;
using System.Linq;
using System.Collections.Generic;
using poi.Models;
using poi.Data;
namespace poi.Controllers
{
[Produces("application/json")]
[Route("api/poi")]
public class POIController : ControllerBase
{
private readonly POIContext _context;
public POIController(POIContext context)
{
_context = context;
}
[HttpGet(Name = "GetAllPOIs")]
[Produces("application/json", Type = typeof(POI))]
public List<POI> GetAll()
{
return _context.POIs.ToList();
}
[HttpGet("{ID}", Name = "GetPOIById")]
[Produces("application/json", Type = typeof(POI))]
public IActionResult GetById(string ID)
{
var item = _context.POIs.Find(ID);
if (item == null)
{
return NotFound();
}
return Ok(item);
}
[HttpGet("trip/{tripID}", Name = "GetPOIsByTripId")]
[Produces("application/json", Type = typeof(POI))]
public IActionResult GetByTripId(string tripID)
{
var items = _context.POIs.Where(poi => poi.TripId == tripID).ToList<POI>();
if (items == null)
{
return NotFound();
}
return Ok(items);
}
[HttpPost(Name = "CreatePOI")]
public IActionResult CreatePoi([FromBody] POI poi)
{
poi.Id = Guid.NewGuid().ToString();
_context.POIs.Add(poi);
_context.SaveChanges();
return Ok(poi);
}
}
}

View File

@ -0,0 +1,20 @@
using System;
using Microsoft.AspNetCore.Mvc;
namespace poi.Controllers
{
[Produces("application/json")]
[Route("api")]
public class VersionController : ControllerBase
{
[Route("version/poi")]
[HttpGet]
[Produces("text/plain", Type = typeof(String))]
public string GetVersion()
{
var version = Environment.GetEnvironmentVariable("APP_VERSION");
return version ?? "default";
}
}
}

View File

@ -0,0 +1,15 @@
using Microsoft.EntityFrameworkCore;
using poi.Models;
namespace poi.Data
{
public class POIContext : DbContext
{
public POIContext(DbContextOptions<POIContext> options) : base(options)
{
}
public DbSet<POI> POIs { get; set; }
}
}

37
apis/poi/web/Dockerfile Normal file
View File

@ -0,0 +1,37 @@
FROM mcr.microsoft.com/dotnet/core/sdk:3.1 AS build-env
WORKDIR /app
# copy csproj and restore as distinct layers
COPY *.csproj ./
RUN dotnet restore
# copy everything else and build
COPY . ./
RUN dotnet publish -c Release -o out
COPY ./appsettings.*.json /app/out/
COPY ./appsettings.json /app/out/
# build runtime image
FROM mcr.microsoft.com/dotnet/core/aspnet:3.1
WORKDIR /app
# docker build argument
# This can be specified during the docker build step by adding " --build-arg build_version=<value>"
# App version can be accessed via the uri path /api/version/poi
# https://vsupalov.com/docker-build-pass-environment-variables/
ARG build_version="poi default"
ENV SQL_USER="YourUserName" \
SQL_PASSWORD="changeme" \
SQL_SERVER="changeme.database.windows.net" \
SQL_DBNAME="mydrivingDB" \
WEB_PORT="8080" \
WEB_SERVER_BASE_URI="http://0.0.0.0" \
ASPNETCORE_ENVIRONMENT="Production" \
APP_VERSION=$build_version
COPY --from=build-env /app/out .
EXPOSE 8080
ENTRYPOINT ["dotnet", "poi.dll"]

View File

@ -0,0 +1,16 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace poi.Models
{
public class BaseDataObject
{
public string Id { get; set; }
public BaseDataObject()
{
Id = Guid.NewGuid().ToString();
}
}
}

View File

@ -0,0 +1,24 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for details.
using System;
using System.ComponentModel.DataAnnotations.Schema;
namespace poi.Models
{
public enum POIType
{
HardAcceleration = 1,
HardBrake = 2
}
public class POI : BaseDataObject
{
public string TripId { get; set; }
public double Latitude { get; set; }
public double Longitude { get; set; }
public POIType PoiType { get; set; }
public DateTime Timestamp { get; set; }
public bool Deleted { get; set; }
}
}

52
apis/poi/web/Program.cs Normal file
View File

@ -0,0 +1,52 @@
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using poi.Utility;
using System.Diagnostics.CodeAnalysis;
using System.IO;
namespace poi
{
[ExcludeFromCodeCoverage]
public class Program
{
public static void Main(string[] args) => CreateHostBuilder(args).Build().Run();
public static IHostBuilder CreateHostBuilder(string[] args)
{
//used to read env variables for host/port
var configuration = new ConfigurationBuilder()
.AddEnvironmentVariables()
.Build();
var host = Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseKestrel()
.UseContentRoot(Directory.GetCurrentDirectory())
.UseConfiguration(configuration)
.UseIISIntegration()
.ConfigureLogging((hostingContext, logging) =>
{
logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging"));
logging.AddConsole();
logging.AddDebug();
})
.ConfigureAppConfiguration((hostingContext, config) =>
{
var env = hostingContext.HostingEnvironment;
config.SetBasePath(Directory.GetCurrentDirectory());
config.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true);
config.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true);
config.AddEnvironmentVariables();
config.AddCommandLine(args);
})
.UseStartup<Startup>()
.UseUrls(POIConfiguration.GetUri(configuration));
});
return host;
}
}
}

103
apis/poi/web/Startup.cs Normal file
View File

@ -0,0 +1,103 @@
using System.Diagnostics.CodeAnalysis;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Rewrite;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Hosting;
using Microsoft.OpenApi.Models;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using poi.Data;
namespace poi
{
[ExcludeFromCodeCoverage]
public class Startup
{
public Startup(IConfiguration configuration)
=> Configuration = configuration;
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers()
.AddNewtonsoftJson((options =>
{
options.SerializerSettings.Formatting = Formatting.Indented;
}));
services.AddHealthChecks()
.AddDbContextCheck<POIContext>()
.AddCheck<Utility.HealthCheck>("poi_health_check");
var connectionString = poi.Utility.POIConfiguration.GetConnectionString(this.Configuration);
services.AddDbContext<POIContext>(options =>
options.UseSqlServer(connectionString));
// Register the Swagger generator, defining 1 or more Swagger documents
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("docs", new OpenApiInfo {
Title = "Points Of Interest(POI) API",
Version = "v1",
Description = "API for the POI in the My Driving example app. https://github.com/Azure-Samples/openhack-devops"
});
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, POIContext dbcontext)
{
if (env.IsDevelopment())
app.UseDeveloperExceptionPage();
app.UseRouting();
app.UseRewriter(new RewriteOptions().AddRedirect("(.*)api/docs/poi$", "$1api/docs/poi/index.html"));
// Enable middleware to serve generated Swagger as a JSON endpoint.
app.UseSwagger(c =>
c.RouteTemplate = "swagger/{documentName}/poi/swagger.json"
);
// Enable middleware to serve swagger-ui (HTML, JS, CSS, etc.),
// specifying the Swagger JSON endpoint.
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/docs/poi/swagger.json", "Points Of Interest(POI) API V1");
c.DocumentTitle = "POI Swagger UI";
c.RoutePrefix = "api/docs/poi";
});
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapHealthChecks("api/healthcheck/poi", new HealthCheckOptions()
{
AllowCachingResponses = false,
ResponseWriter = HealthCheckResponse
});
});
}
private static Task HealthCheckResponse(HttpContext context, HealthReport result)
{
context.Response.ContentType = "application/json";
var json = new JObject(
new JProperty("message", "POI Service Healthcheck"),
new JProperty("status", result.Status.ToString()));
return context.Response.WriteAsync(
json.ToString(Formatting.Indented));
}
}
}

View File

@ -0,0 +1,19 @@
using Microsoft.Extensions.Diagnostics.HealthChecks;
using System.Threading;
using System.Threading.Tasks;
namespace poi.Utility
{
public class HealthCheck : IHealthCheck
{
public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
{
var healthCheckResultHealthy = true; //TODO: implement a proper health check
if (healthCheckResultHealthy)
return Task.FromResult(HealthCheckResult.Healthy("POI is healthy."));
return Task.FromResult(HealthCheckResult.Unhealthy("POI is UNHEALTHY!!!"));
}
}
}

View File

@ -0,0 +1,21 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
namespace poi.Utility
{
// You may need to install the Microsoft.AspNetCore.Http.Abstractions package into your project
public class LoggingEvents
{
public const int Healthcheck = 1000;
public const int GetAllPOIs = 2001;
public const int GetPOIByID = 2002;
public const int GetPOIByTripID = 2002;
}
}

View File

@ -0,0 +1,37 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;
namespace poi.Utility
{
public static class POIConfiguration
{
public static string GetConnectionString(IConfiguration configuration)
{
var SQL_USER = configuration.GetSection("SQL_USER").Value;
var SQL_PASSWORD = configuration.GetSection("SQL_PASSWORD").Value;
var SQL_SERVER = configuration.GetSection("SQL_SERVER").Value;
var SQL_DBNAME = configuration.GetSection("SQL_DBNAME").Value;
var connectionString = configuration["ConnectionStrings:myDrivingDB"];
connectionString = connectionString.Replace("[SQL_USER]", SQL_USER);
connectionString = connectionString.Replace("[SQL_PASSWORD]", SQL_PASSWORD);
connectionString = connectionString.Replace("[SQL_SERVER]", SQL_SERVER);
connectionString = connectionString.Replace("[SQL_DBNAME]", SQL_DBNAME);
return connectionString;
}
public static string GetUri(IConfiguration configuration)
{
var WEB_PORT = configuration.GetValue(typeof(string),"WEB_PORT","8080");
var WEB_SERVER_BASE_URI = configuration.GetValue(typeof(string), "WEB_SERVER_BASE_URI", "http://localhost");
return WEB_SERVER_BASE_URI + ":" + WEB_PORT;
}
}
}

View File

@ -0,0 +1,13 @@
{
"ConnectionStrings": {
"myDrivingDB": "Server=tcp:[SQL_SERVER],1433;Initial Catalog=[SQL_DBNAME];Persist Security Info=False;User ID=[SQL_USER];Password=[SQL_PASSWORD];MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;"
},
"Logging": {
"IncludeScopes": false,
"LogLevel": {
"Default": "Debug",
"System": "Information",
"Microsoft": "Information"
}
}
}

View File

@ -0,0 +1,18 @@
{
"ConnectionStrings": {
"myDrivingDB": "Server=tcp:[SQL_SERVER],1433;Initial Catalog=[SQL_DBNAME];Persist Security Info=False;User ID=[SQL_USER];Password=[SQL_PASSWORD];MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;"
},
"Logging": {
"IncludeScopes": false,
"Debug": {
"LogLevel": {
"Default": "Warning"
}
},
"Console": {
"LogLevel": {
"Default": "Information"
}
}
}
}

View File

@ -0,0 +1,18 @@
{
"ConnectionStrings": {
"myDrivingDB": "Server=tcp:[SQL_SERVER],1433;Initial Catalog=[SQL_DBNAME];Persist Security Info=False;User ID=[SQL_USER];Password=[SQL_PASSWORD];MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;"
},
"Logging": {
"IncludeScopes": false,
"Debug": {
"LogLevel": {
"Default": "Warning"
}
},
"Console": {
"LogLevel": {
"Default": "Debug"
}
}
}
}

View File

@ -0,0 +1,5 @@
{
"ConnectionStrings": {
"myDrivingDB": "Server=tcp:[SQL_SERVER],1433;Initial Catalog=[SQL_DBNAME];Persist Security Info=False;User ID=[SQL_USER];Password=[SQL_PASSWORD];MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;"
}
}

23
apis/poi/web/poi.csproj Normal file
View File

@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup>
<ItemGroup>
<Compile Remove="wwwroot\**" />
<Content Remove="wwwroot\**" />
<EmbeddedResource Remove="wwwroot\**" />
<None Remove="wwwroot\**" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="3.1.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="3.1.3" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="3.1.3" />
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="3.1.3" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="5.4.1" />
<PackageReference Include="Swashbuckle.AspNetCore.Swagger" Version="5.4.1" />
</ItemGroup>
</Project>

1
apis/trips/.dockerignore Normal file
View File

@ -0,0 +1 @@
vendor

22
apis/trips/.gitignore vendored Normal file
View File

@ -0,0 +1,22 @@
# Binaries for programs and plugins
*.exe
*.dll
*.so
*.dylib
# Test binary, build with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736
.glide/
debug
launch.json
.DS_Store
.vscode/
vendor/
glide.lock
go.sum

37
apis/trips/Dockerfile Normal file
View File

@ -0,0 +1,37 @@
FROM golang:1.16.8-alpine AS gobuild
WORKDIR /go/src/github.com/Azure-Samples/openhack-devops-team/apis/trips
COPY . .
RUN go get
RUN CGO_ENABLED=0 GOOS=linux go build -o main .
FROM golang:1.16.8-alpine AS gorun
# docker build argument
# This can be specified during the docker build step by adding " --build-arg build_version=<value>"
# App version can be accessed via the uri path /api/version/trips
# https://vsupalov.com/docker-build-pass-environment-variables/
ARG build_version="trips default"
ENV SQL_USER="YourUserName" \
SQL_PASSWORD="changeme" \
SQL_SERVER="changeme.database.windows.net" \
SQL_DBNAME="mydrivingDB" \
WEB_PORT="80" \
WEB_SERVER_BASE_URI="http://0.0.0.0" \
DOCS_URI="http://localhost" \
DEBUG_LOGGING="false" \
APP_VERSION=$build_version
WORKDIR /app
RUN apk add --update \
ca-certificates
COPY --from=gobuild /go/src/github.com/Azure-Samples/openhack-devops-team/apis/trips/main .
COPY --from=gobuild /go/src/github.com/Azure-Samples/openhack-devops-team/apis/trips/api ./api/
CMD ["./main"]

66
apis/trips/README.md Normal file
View File

@ -0,0 +1,66 @@
# Trips Service
## Overview
This is the Trips API for the MyDriving service.
The server was generated by the [swagger-codegen](https://github.com/swagger-api/swagger-codegen) project.
By using the [OpenAPI-Spec](https://github.com/OAI/OpenAPI-Specification) from a remote server, you can easily generate a server stub.
## Build & Test
### Get dependencies
```shell
go get
```
### Build the Application
```shell
go build
```
### Testing
#### Unit Tests
To run unit tests and get coverage report, execute:
```shell
go test -v ./tripsgo -run Unit -coverprofile=trips_coverage.out -covermode=count
```
To run unit tests and get coverage report and junit report use **gotestsum** tool, execute:
```shell
./gotestsum --format standard-verbose --junitfile unittest_results.xml -- ./tripsgo -run Unit -coverprofile=unittest_coverage.out -covermode=count
```
#### Integration Tests
- Add a file called .evn to the tripsgo folder with the following structure:
```shell
SQL_SERVER="<-- your database server uri -- >"
SQL_PASSWORD="<-- your login password -- >"
SQL_USER="<-- your login user -- >"
SQL_DBNAME="<-- your database name -- >"
SQL_DRIVER="mssql"
```
You shouldn't need to change the SQL_DRIVER variable.
- To run all integration tests, execute:
```shell
go test -v ./tripsgo
```
- To run all integration tests and get junit report use **gotestsum** tool, execute:
```shell
./gotestsum --format standard-verbose --junitfile integrationtest_results.xml -- ./tripsgo
```
> **NOTE** This requires an actual database connection, so the required ENV variables need to be present.

640
apis/trips/api/swagger.json Normal file
View File

@ -0,0 +1,640 @@
{
"swagger": "2.0",
"info": {
"version": "0.0.1",
"title": "MyDriving Trips API",
"description": "API for the user in the My Driving example app. https://github.com/Azure-Samples/openhack-devops-team"
},
"basePath": "/api",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"paths": {
"/healthcheck/trips": {
"x-swagger-router-controller": "healthcheck",
"get": {
"description": "Returns healthcheck for systems looking to ensure API is up and operational",
"responses": {
"200": {
"description": "Service is healthy",
"schema": {
"$ref": "#/definitions/Healthcheck"
}
},
"default": {
"description": "An error occurred",
"schema": {
"$ref": "#/definitions/error_response_default"
}
}
}
}
},
"/trips": {
"x-swagger-router-controller": "trips",
"get": {
"description": "Returns all trips",
"operationId": "getAllTrips",
"responses": {
"200": {
"description": "Trips found",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/trip"
}
}
},
"default": {
"description": "Unknown Error",
"schema": {
"$ref": "#/definitions/error_response_default"
}
}
}
},
"post": {
"description": "Create a trip",
"operationId": "createTrip",
"parameters": [
{
"name": "trip",
"in": "body",
"description": "Trip to add",
"required": true,
"schema": {
"$ref": "#/definitions/trip"
}
}
],
"responses": {
"201": {
"description": "Trip created",
"schema": {
"$ref": "#/definitions/trip"
}
},
"404": {
"description": "Trip contains invalid User ID",
"schema": {
"$ref": "#/definitions/error_response_default"
}
},
"409": {
"description": "Trip already exists",
"schema": {
"$ref": "#/definitions/error_response_default"
}
},
"default": {
"description": "Unknown Error",
"schema": {
"$ref": "#/definitions/error_response_default"
}
}
}
}
},
"/trips/user/{userID}": {
"x-swagger-router-controller": "trips",
"get": {
"description": "Returns all trips for a given user",
"operationId": "getAllTripsForUser",
"responses": {
"200": {
"description": "Trips found",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/trip"
}
}
},
"default": {
"description": "Unknown Error",
"schema": {
"$ref": "#/definitions/error_response_default"
}
}
},
"parameters": [
{
"name": "userID",
"in": "path",
"description": "User ID",
"type": "string",
"required": true
}
]
}
},
"/trips/{tripID}": {
"x-swagger-router-controller": "trips",
"get": {
"description": "Get Trip by ID",
"operationId": "getTripByID",
"responses": {
"200": {
"description": "Trip found",
"schema": {
"$ref": "#/definitions/trip"
}
},
"404": {
"description": "Trip not found",
"schema": {
"$ref": "#/definitions/error_response_default"
}
},
"default": {
"description": "Unknown Error",
"schema": {
"$ref": "#/definitions/error_response_default"
}
}
},
"parameters": [
{
"name": "tripID",
"in": "path",
"description": "Trip ID",
"type": "string",
"required": true
}
]
},
"patch": {
"description": "Update Trip",
"operationId": "updateTrip",
"responses": {
"200": {
"description": "Trip Updated",
"schema": {
"$ref": "#/definitions/trip"
}
},
"404": {
"description": "Trip not found"
},
"default": {
"description": "Unknown Error",
"schema": {
"$ref": "#/definitions/error_response_default"
}
}
},
"parameters": [
{
"name": "tripID",
"in": "path",
"description": "Trip ID",
"type": "string",
"required": true
},
{
"name": "trip",
"in": "body",
"description": "Trip to update",
"required": true,
"schema": {
"$ref": "#/definitions/trip"
}
}
]
},
"delete": {
"description": "Delete Trip By ID",
"operationId": "deleteTrip",
"responses": {
"204": {
"description": "Trip Deleted"
},
"404": {
"description": "Trip not found"
},
"default": {
"description": "Unknown Error",
"schema": {
"$ref": "#/definitions/error_response_default"
}
}
},
"parameters": [
{
"name": "tripID",
"in": "path",
"description": "Trip ID",
"type": "string",
"required": true
}
]
}
},
"/trips/{tripID}/trippoints": {
"x-swagger-router-controller": "trippoints",
"get": {
"description": "Get Trip Points by Trip Id",
"operationId": "getTripPoints",
"responses": {
"200": {
"description": "Trip Points found",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/tripPoint"
}
}
},
"default": {
"description": "Unknown Error",
"schema": {
"$ref": "#/definitions/error_response_default"
}
}
},
"parameters": [
{
"name": "tripID",
"in": "path",
"description": "Trip ID",
"type": "string",
"required": true
}
]
},
"post": {
"description": "Create Trip Point for Trip",
"operationId": "createTripPoint",
"parameters": [
{
"name": "tripID",
"in": "path",
"description": "Trip ID",
"type": "string",
"required": true
},
{
"name": "tripPoint",
"in": "body",
"description": "Trip Point to add",
"required": true,
"schema": {
"$ref": "#/definitions/tripPoint"
}
}
],
"responses": {
"201": {
"description": "Trip Point created",
"schema": {
"$ref": "#/definitions/tripPoint"
}
},
"404": {
"description": "Trip Point contains invalid Trip ID",
"schema": {
"$ref": "#/definitions/error_response_default"
}
},
"409": {
"description": "Trip Point already exists",
"schema": {
"$ref": "#/definitions/error_response_default"
}
},
"default": {
"description": "Unknown Error",
"schema": {
"$ref": "#/definitions/error_response_default"
}
}
}
}
},
"/trips/{tripID}/trippoints/{tripPointID}": {
"x-swagger-router-controller": "trippoints",
"get": {
"description": "Get Trip Point by Trip ID and Trip Point ID",
"operationId": "getTripPointByID",
"responses": {
"200": {
"description": "Trip Point found",
"schema": {
"$ref": "#/definitions/tripPoint"
}
},
"404": {
"description": "Trip Point not found",
"schema": {
"$ref": "#/definitions/error_response_default"
}
},
"default": {
"description": "Unknown Error",
"schema": {
"$ref": "#/definitions/error_response_default"
}
}
},
"parameters": [
{
"name": "tripID",
"in": "path",
"description": "Trip ID",
"type": "string",
"required": true
},
{
"name": "tripPointID",
"in": "path",
"description": "Trip Point ID",
"type": "string",
"required": true
}
]
},
"patch": {
"description": "Update Trip Point",
"operationId": "updateTripPoint",
"responses": {
"200": {
"description": "Trip Point Updated",
"schema": {
"$ref": "#/definitions/tripPoint"
}
},
"404": {
"description": "Trip Point not found"
},
"default": {
"description": "Unknown Error",
"schema": {
"$ref": "#/definitions/error_response_default"
}
}
},
"parameters": [
{
"name": "tripID",
"in": "path",
"description": "Trip ID",
"type": "string",
"required": true
},
{
"name": "tripPointID",
"in": "path",
"description": "Trip Point ID",
"type": "string",
"required": true
},
{
"name": "tripPoint",
"in": "body",
"description": "Trip Point to update",
"required": true,
"schema": {
"$ref": "#/definitions/tripPoint"
}
}
]
},
"delete": {
"description": "Delete Trip Point By ID",
"operationId": "deleteTripPoint",
"responses": {
"204": {
"description": "Trip Point Deleted"
},
"404": {
"description": "Trip Point not found"
},
"default": {
"description": "Unknown Error",
"schema": {
"$ref": "#/definitions/error_response_default"
}
}
},
"parameters": [
{
"name": "tripID",
"in": "path",
"description": "Trip ID",
"type": "string",
"required": true
},
{
"name": "tripPointID",
"in": "path",
"description": "Trip Point ID",
"type": "string",
"required": true
}
]
}
},
"/swagger": {
"x-swagger-pipe": "swagger_raw"
}
},
"definitions": {
"Healthcheck": {
"type": "object",
"properties": {
"message": {
"type": "string",
"description": ""
},
"status": {
"type": "string",
"description": ""
}
}
},
"error_response_default": {
"type": "object",
"properties": {
"status": {
"description": "Error code (if available)",
"type": "integer",
"format": "int32"
},
"message": {
"description": "Error Message",
"type": "string"
}
}
},
"trip": {
"type": "object",
"properties": {
"Id": {
"type": "string",
"description": "Trip ID",
"minLength": 0,
"maxLength": 128
},
"Name": {
"type": "string",
"minLength": 0,
"maxLength": 45,
"pattern": "^[A-Za-z \u0000-\u007f][a-zA-Z \u0000-\u007f]*$"
},
"UserId": {
"type": "string",
"description": "User's unique identity"
},
"RecordedtimeStamp": {
"type": "string",
"format": "date"
},
"EndtimeStamp": {
"type": "string",
"format": "date"
},
"Rating": {
"type": "integer",
"format": "int32"
},
"IsComplete": {
"type": "boolean"
},
"HasSimulatedOBDData": {
"type": "boolean"
},
"AverageSpeed": {
"type": "number",
"format": "float"
},
"FuelUsed": {
"type": "number",
"format": "float"
},
"HardStops": {
"type": "integer",
"format": "int64"
},
"HardAccelerations": {
"type": "integer",
"format": "int64"
},
"Distance": {
"type": "number",
"format": "float"
},
"CreatedAt": {
"type": "string",
"format": "date-time"
},
"UpdatedAt": {
"type": "string",
"format": "date-time"
},
"Deleted": {
"type": "boolean"
}
}
},
"tripPoint": {
"type": "object",
"properties": {
"Id": {
"type": "string",
"description": "Trip Point ID",
"minLength": 0,
"maxLength": 128
},
"TripId": {
"type": "string",
"description": "Trip ID",
"minLength": 0,
"maxLength": 128
},
"Latitude": {
"type": "number",
"format": "float"
},
"Longitude": {
"type": "number",
"format": "float"
},
"Speed": {
"type": "number",
"format": "float"
},
"RecordedTimeStamp": {
"type": "string",
"format": "date"
},
"Sequence": {
"type": "integer",
"format": "int32"
},
"RPM": {
"type": "number",
"format": "float"
},
"ShortTermFuelBank": {
"type": "number",
"format": "float"
},
"LongTermFuelBank": {
"type": "number",
"format": "float"
},
"ThrottlePosition": {
"type": "number",
"format": "float"
},
"RelativeThrottlePosition": {
"type": "number",
"format": "float"
},
"Runtime": {
"type": "number",
"format": "float"
},
"DistanceWithMalfunctionLight": {
"type": "number",
"format": "float"
},
"EngineLoad": {
"type": "number",
"format": "float"
},
"MassFlowRate": {
"type": "number",
"format": "float"
},
"EngineFuelRate": {
"type": "number",
"format": "float"
},
"VIN": {
"type": "string"
},
"HasOBDData": {
"type": "boolean"
},
"HasSimulatedOBDData": {
"type": "boolean"
},
"CreatedAt": {
"type": "string",
"format": "date-time"
},
"UpdatedAt": {
"type": "string",
"format": "date-time"
},
"Deleted": {
"type": "boolean"
}
}
}
}
}

26
apis/trips/buildtest.sh Normal file
View File

@ -0,0 +1,26 @@
#!/bin/bash
# clean the output of the previous build
go clean
# get & install dependencies
go get
# build the project
go build
# run unit tests
go test -v ./tripsgo -run Unit -coverprofile=unittest_coverage.out -covermode=count
# run integration tests
go test -v ./tripsgo
# setup gotestsum
chmod +x install_gotestsum.sh
./install_gotestsum.sh
# run unit tests using gotestsum and generate junit report
./gotestsum --format standard-verbose --junitfile unittest_results.xml -- ./tripsgo -run Unit -coverprofile=unittest_coverage.out -covermode=count
# run integration test susing gotestsum and generate junit report
./gotestsum --format standard-verbose --junitfile integrationtest_results.xml -- ./tripsgo

9
apis/trips/glide.yaml Normal file
View File

@ -0,0 +1,9 @@
package: github.com/Azure-Samples/openhack-devops-team/apis/trips
import:
- package: github.com/codemodus/swagui
version: ~0.4.1
- package: github.com/denisenkom/go-mssqldb
- package: github.com/gorilla/mux
version: ~1.7.4
- package: github.com/stretchr/testify
version: ~1.6.1

12
apis/trips/go.mod Normal file
View File

@ -0,0 +1,12 @@
module github.com/Azure-Samples/openhack-devops-team/apis/trips
go 1.16
require (
github.com/codemodus/swagui v0.4.1
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/denisenkom/go-mssqldb v0.10.0
github.com/gorilla/mux v1.7.4
github.com/joho/godotenv v1.3.0
github.com/stretchr/testify v1.6.1
)

View File

@ -0,0 +1,18 @@
#!/bin/bash
# https://github.com/gotestyourself/gotestsum
if [[ "${OSTYPE}" == "linux-gnu"* ]]; then
os_type="linux"
elif [[ "${OSTYPE}" == "darwin"* ]]; then
os_type="darwin"
fi
OSARCH=$(uname -m)
if [[ "${OSARCH}" == "x86_64"* ]]; then
os_arch="amd64"
elif [[ "${OSARCH}" == "arm"* ]]; then
os_arch="arm"
fi
gotestsum_url=$(curl -s https://api.github.com/repos/gotestyourself/gotestsum/releases/latest | jq -c -r '.assets[] | select(.name | contains("'${os_type}'") and contains("'${os_arch}'")) | .browser_download_url')
curl -sSL "${gotestsum_url}" | tar -xz gotestsum

41
apis/trips/main.go Normal file
View File

@ -0,0 +1,41 @@
package main
import (
"flag"
"fmt"
"io/ioutil"
"net/http"
"os"
sw "github.com/Azure-Samples/openhack-devops-team/apis/trips/tripsgo"
)
var (
webServerPort = flag.String("webServerPort", getEnv("WEB_PORT", "8080"), "web server port")
webServerBaseURI = flag.String("webServerBaseURI", getEnv("WEB_SERVER_BASE_URI", "changeme"), "base portion of server uri")
)
func getEnv(key, fallback string) string {
if value, ok := os.LookupEnv(key); ok {
return value
}
return fallback
}
func main() {
var debug, present = os.LookupEnv("DEBUG_LOGGING")
if present && debug == "true" {
sw.InitLogging(os.Stdout, os.Stdout, os.Stdout)
} else {
// if debug env is not present or false, do not log debug output to console
sw.InitLogging(os.Stdout, ioutil.Discard, os.Stdout)
}
sw.Info.Println(fmt.Sprintf("%s%s", "Trips Service Server started on port ", *webServerPort))
router := sw.NewRouter()
sw.Fatal.Println(http.ListenAndServe(fmt.Sprintf("%s%s", ":", *webServerPort), router))
}

View File

@ -0,0 +1,140 @@
package tripsgo
import (
"database/sql"
"encoding/json"
"flag"
"fmt"
"os"
"github.com/joho/godotenv"
)
//load .env locally for integration tests
//System Environment variables take precedence
var dbEnv = godotenv.Load()
func getEnv(key, fallback string) string {
if value, ok := os.LookupEnv(key); ok {
return value
}
return fallback
}
var (
debug = flag.Bool("debug", false, "enable debugging")
password = flag.String("password", getEnv("SQL_PASSWORD", "changeme"), "the database password")
port = flag.Int("port", 1433, "the database port")
server = flag.String("server", getEnv("SQL_SERVER", "changeme.database.windows.net"), "the database server")
user = flag.String("user", getEnv("SQL_USER", "YourUserName"), "the database user")
database = flag.String("d", getEnv("SQL_DBNAME", "mydrivingDB"), "db_name")
driver = flag.String("driver", getEnv("SQL_DRIVER", "mssql"), "db driver")
)
func RebindDataAccessEnvironmentVariables() {
s := getEnv("SQL_SERVER", "changeme.database.windows.net")
server = &s
dr := getEnv("SQL_DRIVER", "mssql")
driver = &dr
}
// ExecuteNonQuery - Execute a SQL query that has no records returned (Ex. Delete)
func ExecuteNonQuery(query string) (string, error) {
connString := fmt.Sprintf("server=%s;database=%s;user id=%s;password=%s;port=%d", *server, *database, *user, *password, *port)
if *debug {
message := fmt.Sprintf("connString:%s\n", connString)
logMessage(message)
}
conn, err := sql.Open(*driver, connString)
if err != nil {
return "", err
}
defer conn.Close()
statement, err := conn.Prepare(query)
if err != nil {
return "", err
}
defer statement.Close()
result, err := statement.Exec()
if err != nil {
return "", err
}
serializedResult, _ := json.Marshal(result)
return string(serializedResult), nil
}
// ExecuteQuery - Executes a query and returns the result set
func ExecuteQuery(query string) (*sql.Rows, error) {
connString := fmt.Sprintf("server=%s;database=%s;user id=%s;password=%s;port=%d", *server, *database, *user, *password, *port)
conn, err := sql.Open(*driver, connString)
if err != nil {
logError(err, "Failed to connect to database.")
return nil, err
}
defer conn.Close()
statement, err := conn.Prepare(query)
if err != nil {
return nil, err
// log.Fatal("Failed to query a trip: ", err.Error())
}
defer statement.Close()
rows, err := statement.Query()
if err != nil {
return nil, err
// log.Fatal("Error while running the query: ", err.Error())
}
return rows, nil
}
// FirstOrDefault - returns the first row of the result set.
func FirstOrDefault(query string) (*sql.Row, error) {
connString := fmt.Sprintf("server=%s;database=%s;user id=%s;password=%s;port=%d", *server, *database, *user, *password, *port)
if *debug {
message := fmt.Sprintf("connString:%s\n", connString)
logMessage(message)
}
conn, err := sql.Open(*driver, connString)
if err != nil {
return nil, err
// log.Fatal("Failed to connect to the database: ", err.Error())
}
defer conn.Close()
statement, err := conn.Prepare(query)
if err != nil {
return nil, err
// log.Fatal("Failed to query a trip: ", err.Error())
}
defer statement.Close()
row := statement.QueryRow()
return row, nil
}

View File

@ -0,0 +1,170 @@
package tripsgo
import (
"bytes"
"fmt"
"os"
"testing"
"github.com/stretchr/testify/assert"
)
func TestExecuteQueryInvalidDriverReturnsErr(t *testing.T) {
defer t.Cleanup(resetDataAccessEnvVars)
//arrange
InitLogging(os.Stdout, os.Stdout, os.Stdout)
os.Setenv("SQL_DRIVER", "not_a_real_driver")
RebindDataAccessEnvironmentVariables()
//act
var query = SelectAllTripsForUserQuery("someUser")
_, err := ExecuteQuery(query)
//assert
assert.NotNil(t, err)
}
func TestExecuteQueryConnectionSuccess(t *testing.T) {
//act
var query = SelectAllTripsForUserQuery("someUser")
trips, err := ExecuteQuery(query)
//assert
assert.NotNil(t, trips)
assert.Nil(t, err)
}
func TestExecuteQueryInvalidSqlReturnsErr(t *testing.T) {
//arrange
InitLogging(os.Stdout, os.Stdout, os.Stdout)
//act
var invalidSql = "Select Trips From *"
_, err := ExecuteQuery(invalidSql)
//assert
assert.NotNil(t, err)
}
func TestExecuteQueryInvalidServerReturnsErr(t *testing.T) {
defer t.Cleanup(resetDataAccessEnvVars)
//arrange
InitLogging(os.Stdout, os.Stdout, os.Stdout)
os.Setenv("SQL_SERVER", "not_a_real_driver")
RebindDataAccessEnvironmentVariables()
//act
_, err := ExecuteQuery("SELECT TOP 1 ID FROM Trips")
//assert
assert.NotNil(t, err)
}
func TestExecuteNonQueryInvalidDriverReturnsErr(t *testing.T) {
defer t.Cleanup(resetDataAccessEnvVars)
//arrange
InitLogging(os.Stdout, os.Stdout, os.Stdout)
os.Setenv("SQL_DRIVER", "not_a_real_driver")
RebindDataAccessEnvironmentVariables()
//act
_, err := ExecuteNonQuery("fake non query sql")
//assert
assert.NotNil(t, err)
}
func TestExecuteNonQueryConnectionSuccess(t *testing.T) {
//act
_, err := ExecuteNonQuery("SELECT TOP 1 ID FROM Trips")
//assert
assert.Nil(t, err)
}
func TestExecuteNonQueryInvalidServerReturnsErr(t *testing.T) {
defer t.Cleanup(resetDataAccessEnvVars)
//arrange
InitLogging(os.Stdout, os.Stdout, os.Stdout)
os.Setenv("SQL_SERVER", "not_a_real_server")
RebindDataAccessEnvironmentVariables()
//act
_, err := ExecuteNonQuery("SELECT TOP 1 ID FROM Trips")
//assert
assert.NotNil(t, err)
}
func TestFirstOrDefaultInvalidDriverReturnsErr(t *testing.T) {
defer t.Cleanup(resetDataAccessEnvVars)
//arrange
InitLogging(os.Stdout, os.Stdout, os.Stdout)
os.Setenv("SQL_DRIVER", "not_a_real_driver")
RebindDataAccessEnvironmentVariables()
//act
_, err := FirstOrDefault("fake non query sql")
//assert
assert.NotNil(t, err)
}
func TestFirstOrDefaultConnectionSuccess(t *testing.T) {
//act
RebindDataAccessEnvironmentVariables()
_, err := FirstOrDefault("SELECT TOP 1 ID FROM Trips")
//assert
assert.Nil(t, err)
}
func TestFirstOrDefaultInvalidServerReturnsErr(t *testing.T) {
defer t.Cleanup(resetDataAccessEnvVars)
//arrange
InitLogging(os.Stdout, os.Stdout, os.Stdout)
os.Setenv("SQL_SERVER", "not_a_real_server")
RebindDataAccessEnvironmentVariables()
//act
_, err := FirstOrDefault("SELECT TOP 1 ID FROM Trips")
//assert
assert.NotNil(t, err)
}
func TestExecuteNonQueryWritesLogIfDebugTrue(t *testing.T) {
defer t.Cleanup(resetDataAccessEnvVars)
//arrange
info := new(bytes.Buffer)
InitLogging(info, os.Stdout, os.Stdout)
var tr bool = true
debug = &tr
//act
ExecuteNonQuery("SELECT TOP 1 ID FROM Trips")
//assert
actual := fmt.Sprint(info)
assert.True(t, actual != "")
}
func TestFirstOrDefaultWritesLogIfDebugTrue(t *testing.T) {
defer t.Cleanup(resetDataAccessEnvVars)
//arrange
info := new(bytes.Buffer)
InitLogging(info, os.Stdout, os.Stdout)
var tr bool = true
debug = &tr
//act
FirstOrDefault("SELECT TOP 1 ID FROM Trips")
//assert
actual := fmt.Sprint(info)
assert.True(t, actual != "")
}

View File

@ -0,0 +1,24 @@
package tripsgo
import (
"encoding/json"
"strings"
)
// SerializeError - Serialize Error information to JSON format.
func SerializeError(e error, customMessage string) string {
var errorMessage struct {
Message string
}
if customMessage != "" {
message := []string{customMessage, e.Error()}
errorMessage.Message = strings.Join(message, ": ")
} else {
errorMessage.Message = e.Error()
}
serializedError, _ := json.Marshal(errorMessage)
return string(serializedError)
}

View File

@ -0,0 +1,28 @@
package tripsgo
import (
"errors"
"testing"
"github.com/stretchr/testify/assert"
)
func TestSerializeErrorReturnsJsonIncludesErrorMessageUnit(t *testing.T) {
//arrange
expected := "{\"Message\":\"This is a fake error\"}"
err := errors.New("This is a fake error")
//act
actual := SerializeError(err, "")
//assert
assert.Equal(t, expected, actual)
}
func TestSerializeErrorReturnsJsonIncludesCustomMessageUnit(t *testing.T) {
//arrange
expected := "{\"Message\":\"more data: This is a fake error\"}"
err := errors.New("This is a fake error")
//act
actual := SerializeError(err, "more data")
//assert
assert.Equal(t, expected, actual)
}

View File

@ -0,0 +1,11 @@
package tripsgo
// ErrorResponseDefault - Structure to return error information to service caller.
type ErrorResponseDefault struct {
// Error code (if available)
Status int32 `json:"status,omitempty"`
// Error Message
Message string `json:"message,omitempty"`
}

View File

@ -0,0 +1,15 @@
package tripsgo
import (
"encoding/json"
"net/http"
)
func healthcheckGet(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=UTF-8")
w.WriteHeader(http.StatusOK)
hc := &Healthcheck{Message: "Trip Service Healthcheck", Status: "Healthy"}
json.NewEncoder(w).Encode(hc)
}

View File

@ -0,0 +1,31 @@
package tripsgo
import (
"io/ioutil"
"os"
"testing"
)
var healthRouteTests = []APITestCase{
{
Tag: "t0 - healthcheck",
Method: "GET",
URL: "/api/healthcheck/trips",
Status: 200,
ExpectedResponse: `{"message": "Trip Service Healthcheck","status": "Healthy"}`,
},
}
func TestHealthRouteUnit(t *testing.T) {
router := NewRouter()
var debug, present = os.LookupEnv("DEBUG_LOGGING")
if present && debug == "true" {
InitLogging(os.Stdout, os.Stdout, os.Stdout)
} else {
// if debug env is not present or false, do not log debug output to console
InitLogging(os.Stdout, ioutil.Discard, os.Stdout)
}
RunAPITests(t, router, healthRouteTests[0:1])
}

View File

@ -0,0 +1,11 @@
package tripsgo
// Healthcheck - Structure for healthcheck response body
type Healthcheck struct {
//
Message string `json:"message,omitempty"`
//
Status string `json:"status,omitempty"`
}

View File

@ -0,0 +1,61 @@
package tripsgo
import (
"fmt"
"io"
"log"
"net/http"
"time"
)
//
var (
Info *log.Logger
Debug *log.Logger
Fatal *log.Logger
)
// InitLogging - Initialize logging for trips api
func InitLogging(
infoHandle io.Writer,
debugHandle io.Writer,
fatalHandle io.Writer) {
Info = log.New(infoHandle,
"INFO: ",
log.Ldate|log.Ltime|log.Lshortfile)
Debug = log.New(debugHandle,
"DEBUG: ",
log.Ldate|log.Ltime|log.Lshortfile)
Fatal = log.New(fatalHandle,
"FATAL: ",
log.Ldate|log.Ltime|log.Lshortfile)
}
// Logger - basic console logger that writes request info to stdout
func Logger(inner http.Handler, name string) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
inner.ServeHTTP(w, r)
Info.Println(fmt.Sprintf(
"%s %s %s %s",
r.Method,
r.RequestURI,
name,
time.Since(start),
))
})
}
func logMessage(msg string) {
Info.Println(msg)
}
func logError(err error, msg string) {
Info.Println(msg)
Debug.Println(err.Error())
}

View File

@ -0,0 +1,58 @@
package tripsgo
import (
"bytes"
"errors"
"fmt"
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
func TestLogMessagePrintToInfoLogUnit(t *testing.T) {
//arrange
info := new(bytes.Buffer)
debug := new(bytes.Buffer)
fatal := new(bytes.Buffer)
InitLogging(info, debug, fatal)
errorMessage := "This is a test message"
//act
logMessage(errorMessage)
//assert
actual := fmt.Sprint(info)
assert.True(t, strings.Contains(actual, errorMessage))
}
func TestLogErrorPrintsMsgToInfoUnit(t *testing.T) {
//arrange
info := new(bytes.Buffer)
debug := new(bytes.Buffer)
fatal := new(bytes.Buffer)
InitLogging(info, debug, fatal)
errorMessage := "This is a test message"
err := errors.New("This is a fake error")
//act
logError(err, errorMessage)
//assert
actual := fmt.Sprint(info)
assert.True(t, strings.Contains(actual, errorMessage))
}
func TestLogErrorPrintsErrMessageToDebugUnit(t *testing.T) {
//arrange
info := new(bytes.Buffer)
debug := new(bytes.Buffer)
fatal := new(bytes.Buffer)
InitLogging(info, debug, fatal)
errorMessage := "This is a test message"
err := errors.New("This is a fake error")
//act
logError(err, errorMessage)
//assert
actual := fmt.Sprint(debug)
assert.True(t, strings.Contains(actual, "This is a fake error"))
}

View File

@ -0,0 +1,393 @@
package tripsgo
import (
"fmt"
"strconv"
)
// SelectTripByIDQuery - REQUIRED tripID value
func SelectTripByIDQuery(tripID string) string {
return `SELECT
Id,
Name,
UserId,
RecordedTimeStamp,
EndTimeStamp,
Rating,
IsComplete,
HasSimulatedOBDData,
AverageSpeed,
FuelUsed,
HardStops,
HardAccelerations,
Distance,
CreatedAt,
UpdatedAt
FROM Trips
WHERE Id = '` + tripID + `'
AND Deleted = 0`
}
var SelectAllTripsQuery = selectAllTripsQuery
// SelectAllTripsQuery - select all trips
func selectAllTripsQuery() string {
return `SELECT
Id,
Name,
UserId,
RecordedTimeStamp,
EndTimeStamp,
Rating,
IsComplete,
HasSimulatedOBDData,
AverageSpeed,
FuelUsed,
HardStops,
HardAccelerations,
Distance,
CreatedAt,
UpdatedAt
FROM Trips
WHERE Deleted = 0`
}
// SelectAllTripsForUserQuery REQUIRED userID
var SelectAllTripsForUserQuery = selectAllTripsForUserQuery
func selectAllTripsForUserQuery(userID string) string {
return `SELECT
Id,
Name,
UserId,
RecordedTimeStamp,
EndTimeStamp,
Rating,
IsComplete,
HasSimulatedOBDData,
AverageSpeed,
FuelUsed,
HardStops,
HardAccelerations,
Distance,
CreatedAt,
UpdatedAt
FROM Trips
WHERE UserId ='` + userID + `'
AND Deleted = 0`
}
// DeleteTripPointsForTripQuery - REQUIRED tripID
func DeleteTripPointsForTripQuery(tripID string) string {
return fmt.Sprintf("UPDATE TripPoints SET Deleted = 1 WHERE TripId = '%s'", tripID)
}
// DeleteTripQuery - REQUIRED tripID
func DeleteTripQuery(tripID string) string {
return fmt.Sprintf("UPDAte Trips SET Deleted = 1 WHERE Id = '%s'", tripID)
}
// UpdateTripQuery - REQUIRED trip object and tripID
func UpdateTripQuery(trip Trip) string {
var query = `UPDATE Trips SET
Name = '%s',
UserId = '%s',
RecordedTimeStamp = '%s',
EndTimeStamp = '%s',
Rating = %d,
IsComplete = '%s',
HasSimulatedOBDData = '%s',
AverageSpeed = %g,
FuelUsed = %g,
HardStops = %d,
HardAccelerations = %d,
Distance = %g,
UpdatedAt = GETDATE()
WHERE Id = '%s'`
var formattedQuery = fmt.Sprintf(
query,
trip.Name,
trip.UserID,
trip.RecordedTimeStamp,
trip.EndTimeStamp,
trip.Rating,
strconv.FormatBool(trip.IsComplete),
strconv.FormatBool(trip.HasSimulatedOBDData),
trip.AverageSpeed,
trip.FuelUsed,
trip.HardStops,
trip.HardAccelerations,
trip.Distance,
trip.ID)
Debug.Println("updateTripQuery: " + formattedQuery)
return formattedQuery
}
func createTripQuery(trip Trip) string {
var query = `DECLARE @tempReturn
TABLE (TripId NVARCHAR(128));
INSERT INTO Trips (
Name,
UserId,
RecordedTimeStamp,
EndTimeStamp,
Rating,
IsComplete,
HasSimulatedOBDData,
AverageSpeed,
FuelUsed,
HardStops,
HardAccelerations,
Distance,
UpdatedAt,
Deleted)
OUTPUT Inserted.ID
INTO @tempReturn
VALUES (
'%s',
'%s',
'%s',
'%s',
%d,
'%s',
'%s',
%g,
%g,
%d,
%d,
%g,
GETDATE(),
'false');
SELECT TripId FROM @tempReturn`
var formattedQuery = fmt.Sprintf(
query,
trip.Name,
trip.UserID,
trip.RecordedTimeStamp,
trip.EndTimeStamp,
trip.Rating,
strconv.FormatBool(trip.IsComplete),
strconv.FormatBool(trip.HasSimulatedOBDData),
trip.AverageSpeed,
trip.FuelUsed,
trip.HardStops,
trip.HardAccelerations,
trip.Distance)
Debug.Println("createTripQuery: " + formattedQuery)
return formattedQuery
}
func selectTripPointsForTripQuery(tripID string) string {
var query = `SELECT
[Id],
[TripId],
[Latitude],
[Longitude],
[Speed],
[RecordedTimeStamp],
[Sequence],
[RPM],
[ShortTermFuelBank],
[LongTermFuelBank],
[ThrottlePosition],
[RelativeThrottlePosition],
[Runtime],
[DistanceWithMalfunctionLight],
[EngineLoad],
[EngineFuelRate],
[VIN]
FROM [dbo].[TripPoints]
WHERE
TripId = '%s'
AND Deleted = 0`
var formattedQuery = fmt.Sprintf(
query,
tripID)
Debug.Println("selectTripPointsForTripQuery: " + formattedQuery)
return formattedQuery
}
func selectTripPointsForTripPointIDQuery(tripPointID string) string {
var query = `SELECT
[Id],
[TripId],
[Latitude],
[Longitude],
[Speed],
[RecordedTimeStamp],
[Sequence],
[RPM],
[ShortTermFuelBank],
[LongTermFuelBank],
[ThrottlePosition],
[RelativeThrottlePosition],
[Runtime],
[DistanceWithMalfunctionLight],
[EngineLoad],
[EngineFuelRate],
[VIN]
FROM TripPoints
WHERE Id = '%s'
AND Deleted = 0`
var formattedQuery = fmt.Sprintf(
query,
tripPointID)
Debug.Println("selectTripPointsForTripPointIDQuery: " + formattedQuery)
return formattedQuery
}
func createTripPointQuery(tripPoint TripPoint, tripID string) string {
var query = `DECLARE @tempReturn TABLE (TripPointId NVARCHAR(128));
INSERT INTO TripPoints (
[TripId],
[Latitude],
[Longitude],
[Speed],
[RecordedTimeStamp],
[Sequence],
[RPM],
[ShortTermFuelBank],
[LongTermFuelBank],
[ThrottlePosition],
[RelativeThrottlePosition],
[Runtime],
[DistanceWithMalfunctionLight],
[EngineLoad],
[EngineFuelRate],
[MassFlowRate],
[HasOBDData],
[HasSimulatedOBDData],
[VIN],
[UpdatedAt],
[Deleted])
OUTPUT
Inserted.ID
INTO @tempReturn
VALUES (
'%s',
%g,
%g,
%g,
'%s',
%d,
%g,
%g,
%g,
%g,
%g,
%g,
%g,
%g,
%g,
%g,
'%s',
'%s',
'%s',
GETDATE(),
'false');
SELECT TripPointId
FROM @tempReturn`
var formattedQuery = fmt.Sprintf(
query,
tripID,
tripPoint.Latitude,
tripPoint.Longitude,
tripPoint.Speed,
tripPoint.RecordedTimeStamp,
tripPoint.Sequence,
tripPoint.RPM,
tripPoint.ShortTermFuelBank,
tripPoint.LongTermFuelBank,
tripPoint.ThrottlePosition,
tripPoint.RelativeThrottlePosition,
tripPoint.Runtime,
tripPoint.DistanceWithMalfunctionLight,
tripPoint.EngineLoad,
tripPoint.MassFlowRate,
tripPoint.EngineFuelRate,
strconv.FormatBool(tripPoint.HasOBDData),
strconv.FormatBool(tripPoint.HasSimulatedOBDData),
tripPoint.VIN)
Debug.Println("createTripPointQuery: " + formattedQuery)
return formattedQuery
}
func updateTripPointQuery(tripPoint TripPoint) string {
var query = `UPDATE [TripPoints]
SET [TripId] = '%s',
[Latitude] = '%s',
[Longitude] = '%s',
[Speed] = '%s',
[RecordedTimeStamp] = '%s',
[Sequence] = %d,[RPM] = '%s',
[ShortTermFuelBank] = '%s',
[LongTermFuelBank] = '%s',
[ThrottlePosition] = '%s',
[RelativeThrottlePosition] = '%s',
[Runtime] = '%s',
[DistanceWithMalfunctionLight] = '%s',
[EngineLoad] = '%s',
[MassFlowRate] = '%s',
[EngineFuelRate] = '%s',
[HasOBDData] = '%s',
[HasSimulatedOBDData] = '%s',
[VIN] = '%s'
WHERE Id = '%s'`
var formattedQuery = fmt.Sprintf(
query,
tripPoint.TripID,
tripPoint.Latitude,
tripPoint.Longitude,
tripPoint.Speed,
tripPoint.RecordedTimeStamp,
tripPoint.Sequence,
tripPoint.RPM,
tripPoint.ShortTermFuelBank,
tripPoint.LongTermFuelBank,
tripPoint.ThrottlePosition,
tripPoint.RelativeThrottlePosition,
tripPoint.Runtime,
tripPoint.DistanceWithMalfunctionLight,
tripPoint.EngineLoad,
tripPoint.MassFlowRate,
tripPoint.EngineFuelRate,
strconv.FormatBool(tripPoint.HasOBDData),
strconv.FormatBool(tripPoint.HasSimulatedOBDData),
tripPoint.VIN,
tripPoint.ID)
Debug.Println("updateTripPointQuery: " + formattedQuery)
return formattedQuery
}
func deleteTripPointQuery(tripPointID string) string {
var query = `UPDATE TripPoints
SET Deleted = 1
WHERE Id = '%s'`
var formattedQuery = fmt.Sprintf(
query,
tripPointID)
Debug.Println("deleteTripPointQuery: " + formattedQuery)
return formattedQuery
}

View File

@ -0,0 +1,416 @@
package tripsgo
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestUnitdeleteTripPointQuery(t *testing.T) {
//arrange
var expected = `UPDATE TripPoints
SET Deleted = 1
WHERE Id = '1234'`
//act
query := deleteTripPointQuery("1234")
//assert
if query != expected {
t.Errorf("Error \nExpected: %s \nGot: %s", expected, query)
}
}
func TestUnitupdateTripPointQuery(t *testing.T) {
//arrange
tripPoint := TripPoint{
ID: "abcd",
TripID: "a_trip",
Latitude: 51.5244282,
Longitude: -0.0784379,
Speed: 185.2,
RecordedTimeStamp: "a_timestamp",
Sequence: 1,
RPM: 4000,
ShortTermFuelBank: 1,
LongTermFuelBank: 2,
ThrottlePosition: 3,
RelativeThrottlePosition: 4,
Runtime: 5,
DistanceWithMalfunctionLight: 6,
EngineLoad: 7,
MassFlowRate: 8,
EngineFuelRate: 9,
HasOBDData: true,
HasSimulatedOBDData: false,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
Deleted: false,
}
var expected = `UPDATE [TripPoints]
SET [TripId] = 'a_trip',
[Latitude] = '%!s(float32=51.52443)',
[Longitude] = '%!s(float32=-0.0784379)',
[Speed] = '%!s(float32=185.2)',
[RecordedTimeStamp] = 'a_timestamp',
[Sequence] = 1,[RPM] = '%!s(float32=4000)',
[ShortTermFuelBank] = '%!s(float32=1)',
[LongTermFuelBank] = '%!s(float32=2)',
[ThrottlePosition] = '%!s(float32=3)',
[RelativeThrottlePosition] = '%!s(float32=4)',
[Runtime] = '%!s(float32=5)',
[DistanceWithMalfunctionLight] = '%!s(float32=6)',
[EngineLoad] = '%!s(float32=7)',
[MassFlowRate] = '%!s(float32=8)',
[EngineFuelRate] = '%!s(float32=9)',
[HasOBDData] = 'true',
[HasSimulatedOBDData] = 'false',
[VIN] = '{ %!s(bool=false)}'
WHERE Id = 'abcd'`
//act
query := updateTripPointQuery(tripPoint)
//assert
assert.Equal(t, expected, query)
}
func TestSelectAllTripsQueryUnit(t *testing.T) {
//arrange
var expected = `SELECT
Id,
Name,
UserId,
RecordedTimeStamp,
EndTimeStamp,
Rating,
IsComplete,
HasSimulatedOBDData,
AverageSpeed,
FuelUsed,
HardStops,
HardAccelerations,
Distance,
CreatedAt,
UpdatedAt
FROM Trips
WHERE Deleted = 0`
//act
query := SelectAllTripsQuery()
//assert
assert.Equal(t, expected, query)
}
func TestSelectAllTripsForUserQueryUnit(t *testing.T) {
//arrange
var expected = `SELECT
Id,
Name,
UserId,
RecordedTimeStamp,
EndTimeStamp,
Rating,
IsComplete,
HasSimulatedOBDData,
AverageSpeed,
FuelUsed,
HardStops,
HardAccelerations,
Distance,
CreatedAt,
UpdatedAt
FROM Trips
WHERE UserId ='fake_user'
AND Deleted = 0`
//act
query := SelectAllTripsForUserQuery("fake_user")
//assert
assert.Equal(t, expected, query)
}
func TestDeleteTripPointsForTripQueryUnit(t *testing.T) {
//arrange
var expected = `UPDATE TripPoints SET Deleted = 1 WHERE TripId = 'trip_123'`
//act
query := DeleteTripPointsForTripQuery("trip_123")
//assert
assert.Equal(t, expected, query)
}
func TestDeleteTripQueryUnit(t *testing.T) {
//arrange
var expected = `UPDAte Trips SET Deleted = 1 WHERE Id = 'trip_123'`
//act
query := DeleteTripQuery("trip_123")
//assert
assert.Equal(t, expected, query)
}
func TestSelectTripByIDQueryUnit(t *testing.T) {
//arrange
var expected = `SELECT
Id,
Name,
UserId,
RecordedTimeStamp,
EndTimeStamp,
Rating,
IsComplete,
HasSimulatedOBDData,
AverageSpeed,
FuelUsed,
HardStops,
HardAccelerations,
Distance,
CreatedAt,
UpdatedAt
FROM Trips
WHERE Id = 'trip_123'
AND Deleted = 0`
//act
query := SelectTripByIDQuery("trip_123")
//assert
assert.Equal(t, expected, query)
}
func TestSelectTripPointsForTripPointIDQueryUnit(t *testing.T) {
//arrange
var expected = `SELECT
[Id],
[TripId],
[Latitude],
[Longitude],
[Speed],
[RecordedTimeStamp],
[Sequence],
[RPM],
[ShortTermFuelBank],
[LongTermFuelBank],
[ThrottlePosition],
[RelativeThrottlePosition],
[Runtime],
[DistanceWithMalfunctionLight],
[EngineLoad],
[EngineFuelRate],
[VIN]
FROM TripPoints
WHERE Id = 'point_ab'
AND Deleted = 0`
//act
query := selectTripPointsForTripPointIDQuery("point_ab")
//assert
assert.Equal(t, expected, query)
}
func TestUpdateTripQueryUnit(t *testing.T) {
//arrange
trip := Trip{
ID: "abcd",
Name: "fake Trip",
UserID: "fake user",
RecordedTimeStamp: "now",
EndTimeStamp: "then",
Rating: 1,
IsComplete: false,
HasSimulatedOBDData: false,
AverageSpeed: 88,
FuelUsed: 23.2,
HardStops: 8,
HardAccelerations: 12,
Distance: 5,
Created: time.Now(),
UpdatedAt: time.Now(),
Deleted: false,
}
var expected = `UPDATE Trips SET
Name = 'fake Trip',
UserId = 'fake user',
RecordedTimeStamp = 'now',
EndTimeStamp = 'then',
Rating = 1,
IsComplete = 'false',
HasSimulatedOBDData = 'false',
AverageSpeed = 88,
FuelUsed = 23.2,
HardStops = 8,
HardAccelerations = 12,
Distance = 5,
UpdatedAt = GETDATE()
WHERE Id = 'abcd'`
//act
query := UpdateTripQuery(trip)
//assert
assert.Equal(t, expected, query)
}
func TestCreateTripQueryUnit(t *testing.T) {
//arrange
trip := Trip{
ID: "abcd",
Name: "fake Trip",
UserID: "fake user",
RecordedTimeStamp: "now",
EndTimeStamp: "then",
Rating: 1,
IsComplete: false,
HasSimulatedOBDData: false,
AverageSpeed: 88,
FuelUsed: 23.2,
HardStops: 8,
HardAccelerations: 12,
Distance: 5,
Created: time.Now(),
UpdatedAt: time.Now(),
Deleted: false,
}
var expected = `DECLARE @tempReturn
TABLE (TripId NVARCHAR(128));
INSERT INTO Trips (
Name,
UserId,
RecordedTimeStamp,
EndTimeStamp,
Rating,
IsComplete,
HasSimulatedOBDData,
AverageSpeed,
FuelUsed,
HardStops,
HardAccelerations,
Distance,
UpdatedAt,
Deleted)
OUTPUT Inserted.ID
INTO @tempReturn
VALUES (
'fake Trip',
'fake user',
'now',
'then',
1,
'false',
'false',
88,
23.2,
8,
12,
5,
GETDATE(),
'false');
SELECT TripId FROM @tempReturn`
//act
query := createTripQuery(trip)
//assert
assert.Equal(t, expected, query)
}
func TestSelectTripPointsForTripQueryUnit(t *testing.T) {
//arrange
var expected = `SELECT
[Id],
[TripId],
[Latitude],
[Longitude],
[Speed],
[RecordedTimeStamp],
[Sequence],
[RPM],
[ShortTermFuelBank],
[LongTermFuelBank],
[ThrottlePosition],
[RelativeThrottlePosition],
[Runtime],
[DistanceWithMalfunctionLight],
[EngineLoad],
[EngineFuelRate],
[VIN]
FROM [dbo].[TripPoints]
WHERE
TripId = 'trip_zzyzx'
AND Deleted = 0`
//act
query := selectTripPointsForTripQuery("trip_zzyzx")
//assert
assert.Equal(t, expected, query)
}
func TestCreateTripPointQueryUnit(t *testing.T) {
//arrange
tripPoint := TripPoint{
ID: "abcd",
TripID: "a_trip",
Latitude: 51.5244282,
Longitude: -0.0784379,
Speed: 185.2,
RecordedTimeStamp: "a_timestamp",
Sequence: 1,
RPM: 4000,
ShortTermFuelBank: 1,
LongTermFuelBank: 2,
ThrottlePosition: 3,
RelativeThrottlePosition: 4,
Runtime: 5,
DistanceWithMalfunctionLight: 6,
EngineLoad: 7,
MassFlowRate: 8,
EngineFuelRate: 9,
HasOBDData: true,
HasSimulatedOBDData: false,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
Deleted: false,
}
var expected = `DECLARE @tempReturn TABLE (TripPointId NVARCHAR(128));
INSERT INTO TripPoints (
[TripId],
[Latitude],
[Longitude],
[Speed],
[RecordedTimeStamp],
[Sequence],
[RPM],
[ShortTermFuelBank],
[LongTermFuelBank],
[ThrottlePosition],
[RelativeThrottlePosition],
[Runtime],
[DistanceWithMalfunctionLight],
[EngineLoad],
[EngineFuelRate],
[MassFlowRate],
[HasOBDData],
[HasSimulatedOBDData],
[VIN],
[UpdatedAt],
[Deleted])
OUTPUT
Inserted.ID
INTO @tempReturn
VALUES (
'fake_trip_id',
51.52443,
-0.0784379,
185.2,
'a_timestamp',
1,
4000,
1,
2,
3,
4,
5,
6,
7,
8,
9,
'true',
'false',
'{ %!s(bool=false)}',
GETDATE(),
'false');
SELECT TripPointId
FROM @tempReturn`
//act
query := createTripPointQuery(tripPoint, "fake_trip_id")
//assert
assert.Equal(t, expected, query)
}

View File

@ -0,0 +1,203 @@
package tripsgo
import (
"flag"
"fmt"
"net/http"
"os"
"github.com/codemodus/swagui"
"github.com/codemodus/swagui/suidata3"
"github.com/gorilla/mux"
)
var (
du = flag.String("du", getEnv("DOCS_URI", "http://localhost:8080"), "docs endpoint")
wsbu = flag.String("wsbu", getEnv("WEB_SERVER_BASE_URI", "changeme"), "base portion of server uri")
)
// Route - object representing a route handler
type Route struct {
Name string
Method string
Pattern string
HandlerFunc http.HandlerFunc
}
// Routes - Route handler collection
type Routes []Route
// NewRouter - Constructor
func NewRouter() *mux.Router {
router := mux.NewRouter().StrictSlash(true)
for _, route := range routes {
CreateHandler(router, route)
}
// add docs route
CreateDocsHandler(router, docsRoute)
return router
}
// Index - Default route handler for service base uri
func Index(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Trips Service")
}
// CreateHandler - Create router handler
func CreateHandler(router *mux.Router, route Route) {
var handler http.Handler
handler = route.HandlerFunc
handler = Logger(handler, route.Name)
router.
Methods(route.Method).
Path(route.Pattern).
Name(route.Name).
Handler(handler)
}
// CreateDocsHandler - Create route handler for docs using SwagUI
func CreateDocsHandler(router *mux.Router, route Route) {
var def = "/api/json/swagger.json"
var provider = suidata3.New()
ui, err := swagui.New(http.NotFoundHandler(), provider)
if err != nil {
Info.Println(err)
os.Exit(1)
}
router.
Methods(route.Method).
Name(route.Name).
Handler(ui.Handler(def))
router.
Methods(route.Method).
Path("/api/docs/trips/{dir}/{fileName}").
Name("*").
Handler(ui.Handler(def))
router.
Methods(route.Method).
Path("/api/docs/trips/{fileName}").
Name("Swagger UI JS").
Handler(ui.Handler(def))
}
var docsRoute = Route{
"swagger-ui",
"GET",
"/api/docs/trips/",
nil,
}
var routes = Routes{
Route{
"Index",
"GET",
"/api/",
Index,
},
Route{
"swagger-json",
"GET",
"/api/json/swagger.json",
swaggerDocsJSON,
},
Route{
"CreateTrip",
"POST",
"/api/trips",
createTrip,
},
Route{
"CreateTripPoint",
"POST",
"/api/trips/{tripID}/trippoints",
createTripPoint,
},
Route{
"DeleteTrip",
"DELETE",
"/api/trips/{tripID}",
deleteTrip,
},
Route{
"DeleteTripPoint",
"DELETE",
"/api/trips/{tripID}/trippoints/{tripPointID}",
deleteTripPoint,
},
Route{
"GetAllTrips",
"GET",
"/api/trips",
getAllTrips,
},
Route{
"GetAllTripsForUser",
"GET",
"/api/trips/user/{userID}",
getAllTripsForUser,
},
Route{
"GetTripById",
"GET",
"/api/trips/{tripID}",
getTripByID,
},
Route{
"GetTripPointByID",
"GET",
"/api/trips/{tripID}/trippoints/{tripPointID}",
getTripPointByID,
},
Route{
"GetTripPoints",
"GET",
"/api/trips/{tripID}/trippoints",
getTripPoints,
},
Route{
"HealthcheckGet",
"GET",
"/api/healthcheck/trips",
healthcheckGet,
},
Route{
"UpdateTrip",
"PATCH",
"/api/trips/{tripID}",
updateTrip,
},
Route{
"UpdateTripPoint",
"PATCH",
"/api/trips/{tripID}/trippoints/{tripPointID}",
updateTripPoint,
},
Route{
"VersionGet",
"GET",
"/api/version/trips",
versionGet,
},
}

View File

@ -0,0 +1,28 @@
package tripsgo
import (
"fmt"
"net/http"
"os"
"time"
)
func getSwaggerJsonPath() string {
if value, ok := os.LookupEnv("SWAGGER_JSON_PATH"); ok {
return value
}
return "./api/swagger.json"
}
func swaggerDocsJSON(w http.ResponseWriter, r *http.Request) {
swaggerPath := getSwaggerJsonPath()
fData, err := os.Open(swaggerPath)
if err != nil {
var msg = fmt.Sprintf("swaggerDocsJson - Unable to open and read swagger.json : %v", err)
w.WriteHeader(http.StatusInternalServerError)
Info.Println(msg)
http.Error(w, msg, -1)
return
}
http.ServeContent(w, r, "swagger.json", time.Now(), fData)
}

View File

@ -0,0 +1,49 @@
package tripsgo
import (
"io/ioutil"
"os"
"testing"
)
func TestSwaggerServiceSuccessUnit(t *testing.T) {
router := NewRouter()
os.Setenv("SWAGGER_JSON_PATH", "../api/swagger.json")
var debug, present = os.LookupEnv("DEBUG_LOGGING")
if present && debug == "true" {
InitLogging(os.Stdout, os.Stdout, os.Stdout)
} else {
// if debug env is not present or false, do not log debug output to console
InitLogging(os.Stdout, ioutil.Discard, os.Stdout)
}
RunAPITests(t, router, []APITestCase{
{
Tag: "swaggerService",
Method: "GET",
URL: "/api/json/swagger.json",
Status: 200,
},
})
}
func TestSwaggerServiceFailUnit(t *testing.T) {
router := NewRouter()
os.Unsetenv("SWAGGER_JSON_PATH")
var debug, present = os.LookupEnv("DEBUG_LOGGING")
if present && debug == "true" {
InitLogging(os.Stdout, os.Stdout, os.Stdout)
} else {
// if debug env is not present or false, do not log debug output to console
InitLogging(os.Stdout, ioutil.Discard, os.Stdout)
}
RunAPITestsPlainText(t, router, []APITestCase{
{
Tag: "swaggerService",
Method: "GET",
URL: "/api/json/swagger.json",
Status: 500,
},
})
}

View File

@ -0,0 +1,66 @@
package tripsgo
import (
"bytes"
"net/http"
"net/http/httptest"
"strconv"
"testing"
"github.com/gorilla/mux"
"github.com/joho/godotenv"
"github.com/stretchr/testify/assert"
)
// APITestCase needs to be exported to be accessed for test dir
type APITestCase struct {
Tag string
Method string
URL string
Body string
Status int
ExpectedResponse string
ActualResponse string
}
func testAPI(router *mux.Router, method, URL, body string) *httptest.ResponseRecorder {
req, _ := http.NewRequest(method, URL, bytes.NewBufferString(body))
req.Header.Set("Content-Type", "application/json")
res := httptest.NewRecorder()
router.ServeHTTP(res, req)
return res
}
// RunAPITests needs to be exported to be accessed for test dir
func RunAPITests(t *testing.T, router *mux.Router, tests []APITestCase) {
for i := 0; i < len(tests); i++ {
res := testAPI(router, tests[i].Method, tests[i].URL, tests[i].Body)
tests[i].ActualResponse = res.Body.String()
Debug.Println(tests[i].Tag + " - " + tests[i].ActualResponse)
assert.Equal(t, tests[i].Status, res.Code, tests[i].Tag)
Info.Println(tests[i].Tag + "- Response Code:" + strconv.Itoa(res.Code))
if tests[i].ExpectedResponse != "" {
assert.JSONEq(t, tests[i].ExpectedResponse, res.Body.String(), tests[i].Tag)
}
}
}
func RunAPITestsPlainText(t *testing.T, router *mux.Router, tests []APITestCase) {
for i := 0; i < len(tests); i++ {
res := testAPI(router, tests[i].Method, tests[i].URL, tests[i].Body)
tests[i].ActualResponse = res.Body.String()
Debug.Println(tests[i].Tag + " - " + tests[i].ActualResponse)
assert.Equal(t, tests[i].Status, res.Code, tests[i].Tag)
Info.Println(tests[i].Tag + "- Response Code:" + strconv.Itoa(res.Code))
if tests[i].ExpectedResponse != "" {
assert.Equal(t, tests[i].ExpectedResponse, res.Body.String(), tests[i].Tag)
}
}
}
func resetDataAccessEnvVars() {
var fls bool = false
debug = &fls
godotenv.Overload()
RebindDataAccessEnvironmentVariables()
}

View File

@ -0,0 +1,43 @@
package tripsgo
import (
"time"
)
// Trip - Represents a single trip by a user with its associated set of trip points.
type Trip struct {
// Trip ID
ID string `json:"Id"`
Name string `json:"Name"`
// User's unique identity
UserID string `json:"UserId"`
RecordedTimeStamp string `json:"RecordedTimeStamp"`
EndTimeStamp string `json:"EndTimeStamp"`
Rating int32 `json:"Rating"`
IsComplete bool `json:"IsComplete"`
HasSimulatedOBDData bool `json:"HasSimulatedOBDData"`
AverageSpeed float32 `json:"AverageSpeed"`
FuelUsed float32 `json:"FuelUsed"`
HardStops int64 `json:"HardStops"`
HardAccelerations int64 `json:"HardAccelerations"`
Distance float32 `json:"Distance"`
Created time.Time `json:"Created"`
UpdatedAt time.Time `json:"UpdatedAt"`
Deleted bool `json:"Deleted,omitempty"`
}

View File

@ -0,0 +1,258 @@
package tripsgo
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"github.com/gorilla/mux"
)
// TripPoint Service Methods
func getTripPoints(w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
var tripID = params["tripID"]
var query = selectTripPointsForTripQuery(tripID)
statement, err := ExecuteQuery(query)
if err != nil {
var msg = "Error while retrieving trip points from database"
logError(err, msg)
fmt.Fprintf(w, SerializeError(err, msg))
return
}
tripPointRows := []TripPoint{}
for statement.Next() {
var tp TripPoint
err := statement.Scan(
&tp.ID,
&tp.TripID,
&tp.Latitude,
&tp.Longitude,
&tp.Speed,
&tp.RecordedTimeStamp,
&tp.Sequence,
&tp.RPM,
&tp.ShortTermFuelBank,
&tp.LongTermFuelBank,
&tp.ThrottlePosition,
&tp.RelativeThrottlePosition,
&tp.Runtime,
&tp.DistanceWithMalfunctionLight,
&tp.EngineLoad,
&tp.EngineFuelRate,
&tp.VIN)
if err != nil {
var msg = "Error scanning Trip Points"
logError(err, msg)
fmt.Fprintf(w, SerializeError(err, msg))
return
}
tripPointRows = append(tripPointRows, tp)
}
serializedReturn, _ := json.Marshal(tripPointRows)
fmt.Fprintf(w, string(serializedReturn))
}
func getTripPointByID(w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
tripPointID := params["tripPointID"]
var query = selectTripPointsForTripPointIDQuery(tripPointID)
row, err := FirstOrDefault(query)
if err != nil {
var msg = "Error while retrieving trip point from database"
logError(err, msg)
fmt.Fprintf(w, SerializeError(err, msg))
return
}
var tripPoint TripPoint
err = row.Scan(
&tripPoint.ID,
&tripPoint.TripID,
&tripPoint.Latitude,
&tripPoint.Longitude,
&tripPoint.Speed,
&tripPoint.RecordedTimeStamp,
&tripPoint.Sequence,
&tripPoint.RPM,
&tripPoint.ShortTermFuelBank,
&tripPoint.LongTermFuelBank,
&tripPoint.ThrottlePosition,
&tripPoint.RelativeThrottlePosition,
&tripPoint.Runtime,
&tripPoint.DistanceWithMalfunctionLight,
&tripPoint.EngineLoad,
&tripPoint.EngineFuelRate,
&tripPoint.VIN)
if err != nil {
var msg = "Failed to scan a trip point"
logError(err, msg)
fmt.Fprintf(w, SerializeError(err, msg))
return
}
serializedTripPoint, _ := json.Marshal(tripPoint)
fmt.Fprintf(w, string(serializedTripPoint))
}
func createTripPoint(w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
tripID := params["tripID"]
body, err := ioutil.ReadAll(r.Body)
var tripPoint TripPoint
err = json.Unmarshal(body, &tripPoint)
if err != nil {
var msg = "Error while decoding json for trip point"
logError(err, msg)
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprintf(w, SerializeError(err, msg))
return
}
var query = createTripPointQuery(tripPoint, tripID)
result, err := ExecuteQuery(query)
if err != nil {
var msg = "Error while inserting Trip Point into database"
logError(err, msg)
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprintf(w, SerializeError(err, msg))
return
}
for result.Next() {
err = result.Scan(&tripPoint.ID)
if err != nil {
var msg = "Error retrieving trip point id"
logError(err, msg)
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprintf(w, SerializeError(err, msg))
}
}
serializedTripPoint, _ := json.Marshal(tripPoint)
fmt.Fprintf(w, string(serializedTripPoint))
}
func updateTripPoint(w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
tripPointID := params["tripPointID"]
body, err := ioutil.ReadAll(r.Body)
defer r.Body.Close()
if err != nil {
var msg = "Error while decoding json for trip point"
logError(err, msg)
fmt.Fprintf(w, SerializeError(err, msg))
return
}
var tripPoint TripPoint
err = json.Unmarshal(body, &tripPoint)
if err != nil {
var msg = "Error while decoding json"
logError(err, msg)
fmt.Fprintf(w, SerializeError(err, msg))
return
}
tripPoint.ID = tripPointID
var query = updateTripPointQuery(tripPoint)
result, err := ExecuteNonQuery(query)
if err != nil {
var msg = "Error while patching Trip Point on the database"
logError(err, msg)
fmt.Fprintf(w, SerializeError(err, msg))
return
}
fmt.Fprintf(w, string(result))
}
func deleteTripPoint(w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
tripPointID := params["tripPointID"]
var query = deleteTripPointQuery(tripPointID)
result, err := ExecuteNonQuery(query)
if err != nil {
var msg = "Error while deleting trip point from database"
logError(err, msg)
fmt.Fprintf(w, SerializeError(err, msg))
return
}
serializedResult, _ := json.Marshal(result)
fmt.Fprintf(w, string(serializedResult))
}
// func getMaxSequence(w http.ResponseWriter, r *http.Request) {
// tripID := r.FormValue("id")
// query := fmt.Sprintf("SELECT MAX(Sequence) as MaxSequence FROM TripPoints where tripid = '%s'", tripID)
// row, err := FirstOrDefault(query)
// if err != nil {
// var msg = "Error while querying Max Sequence"
// logError(err, msg)
// fmt.Fprintf(w, SerializeError(err, msg))
// return
// }
// var MaxSequence string
// err = row.Scan(&MaxSequence)
// if err != nil {
// var msg = "Error while obtaining max sequence"
// logError(err, msg)
// fmt.Fprintf(w, SerializeError(err, msg))
// return
// }
// fmt.Fprintf(w, MaxSequence)
// }
type newTripPoint struct {
ID string
}

View File

@ -0,0 +1,295 @@
package tripsgo
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
_ "github.com/denisenkom/go-mssqldb" //vscode deletes this import if it is not a blank import
"github.com/gorilla/mux"
)
// Trip Service Methods
// getTripByID - gets a trip by its trip id
func getTripByID(w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
//Build Query
var query = SelectTripByIDQuery(params["tripID"])
//Execute Query
row, err := FirstOrDefault(query)
if err != nil {
var msg = "getTripsByID - Error while retrieving trip from database"
logError(err, msg)
http.Error(w, SerializeError(err, msg), http.StatusInternalServerError)
return
}
var trip Trip
errScan := row.Scan(
&trip.ID,
&trip.Name,
&trip.UserID,
&trip.RecordedTimeStamp,
&trip.EndTimeStamp,
&trip.Rating,
&trip.IsComplete,
&trip.HasSimulatedOBDData,
&trip.AverageSpeed,
&trip.FuelUsed,
&trip.HardStops,
&trip.HardAccelerations,
&trip.Distance,
&trip.Created,
&trip.UpdatedAt)
if errScan != nil {
var msg = fmt.Sprintf("No trip with ID '%s' found", params["tripID"])
logMessage(msg)
// fmt.Fprintf(w, msg)
http.NotFound(w, r)
return
}
serializedTrip, _ := json.Marshal(trip)
fmt.Fprintf(w, string(serializedTrip))
}
// getAllTrips - get all trips
func getAllTrips(w http.ResponseWriter, r *http.Request) {
var query = SelectAllTripsQuery()
tripRows, err := ExecuteQuery(query)
if err != nil {
var msg = "getAllTrips - Query Failed to Execute."
logError(err, msg)
http.Error(w, SerializeError(err, msg), http.StatusInternalServerError)
return
}
trips := []Trip{}
for tripRows.Next() {
var r Trip
err := tripRows.Scan(
&r.ID,
&r.Name,
&r.UserID,
&r.RecordedTimeStamp,
&r.EndTimeStamp,
&r.Rating,
&r.IsComplete,
&r.HasSimulatedOBDData,
&r.AverageSpeed,
&r.FuelUsed,
&r.HardStops,
&r.HardAccelerations,
&r.Distance,
&r.Created,
&r.UpdatedAt)
if err != nil {
var msg = "GetAllTrips - Error scanning Trips"
logError(err, msg)
http.Error(w, SerializeError(err, msg), http.StatusInternalServerError)
return
}
trips = append(trips, r)
}
tripsJSON, _ := json.Marshal(trips)
fmt.Fprintf(w, string(tripsJSON))
}
// getAllTripsForUser - get all trips for a given user
func getAllTripsForUser(w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
var query = SelectAllTripsForUserQuery(params["userID"])
tripRows, err := ExecuteQuery(query)
if err != nil {
var msg = "getAllTripsForUser - Error while retrieving trips from database"
logError(err, msg)
http.Error(w, SerializeError(err, msg), http.StatusInternalServerError)
return
}
trips := []Trip{}
for tripRows.Next() {
var r Trip
err := tripRows.Scan(&r.ID,
&r.Name,
&r.UserID,
&r.RecordedTimeStamp,
&r.EndTimeStamp,
&r.Rating,
&r.IsComplete,
&r.HasSimulatedOBDData,
&r.AverageSpeed,
&r.FuelUsed,
&r.HardStops,
&r.HardAccelerations,
&r.Distance,
&r.Created,
&r.UpdatedAt)
if err != nil {
var msg = "getAllTripsForUser - Error scanning Trips"
logError(err, msg)
http.Error(w, SerializeError(err, msg), http.StatusInternalServerError)
return
}
trips = append(trips, r)
}
tripsJSON, _ := json.Marshal(trips)
fmt.Fprintf(w, string(tripsJSON))
}
// deleteTrip - deletes a single trip and its associated trip points for a user
func deleteTrip(w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
var deleteTripPointsQuery = DeleteTripPointsForTripQuery(params["tripID"])
var deleteTripsQuery = DeleteTripQuery(params["tripID"])
result, err := ExecuteNonQuery(deleteTripPointsQuery)
if err != nil {
var msg = "Error while deleting trip points from database"
logError(err, msg)
http.Error(w, SerializeError(err, msg), http.StatusInternalServerError)
return
}
// Debug.Println(fmt.Sprintln(`Deleted trip points for Trip '%s'`, params["tripID"]))
result, err = ExecuteNonQuery(deleteTripsQuery)
if err != nil {
var msg = "Error while deleting trip from database"
logError(err, msg)
http.Error(w, SerializeError(err, msg), http.StatusInternalServerError)
return
}
// Debug.Println(fmt.Sprintln("Deleted trip '%s'", params["tripID"]))
serializedResult, _ := json.Marshal(result)
fmt.Fprintf(w, string(serializedResult))
}
// updateTrip - update a trip
func updateTrip(w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
tripID := params["tripID"]
var trip Trip
body, err := ioutil.ReadAll(r.Body)
defer r.Body.Close()
if err != nil {
var msg = "Update Trip - Error reading trip request body"
logError(err, msg)
http.Error(w, SerializeError(err, msg), http.StatusInternalServerError)
return
}
err = json.Unmarshal(body, &trip)
if err != nil {
var msg = "Update Trip - Error while decoding trip json"
logError(err, msg)
http.Error(w, SerializeError(err, msg), http.StatusInternalServerError)
return
}
trip.ID = tripID
updateQuery := UpdateTripQuery(trip)
result, err := ExecuteNonQuery(updateQuery)
if err != nil {
var msg = "Error updating trip on the database." + string(result)
logError(err, msg)
http.Error(w, SerializeError(err, msg), http.StatusInternalServerError)
return
}
serializedTrip, _ := json.Marshal(trip)
fmt.Fprintf(w, string(serializedTrip))
}
// createTrip - create a trip for a user. This method does not create the associated trip points, only the trip.
func createTrip(w http.ResponseWriter, r *http.Request) {
//params := mux.Vars(r)
body, err := ioutil.ReadAll(r.Body)
var trip Trip
err = json.Unmarshal(body, &trip)
if err != nil {
var msg = "Error while decoding json"
logError(err, msg)
http.Error(w, SerializeError(err, msg), http.StatusInternalServerError)
return
}
insertQuery := createTripQuery(trip)
var newTripID newTrip
result, err := ExecuteQuery(insertQuery)
if err != nil {
var msg = "Error while inserting trip into database"
logError(err, msg)
http.Error(w, SerializeError(err, msg), http.StatusInternalServerError)
return
}
for result.Next() {
err = result.Scan(&newTripID.ID)
if err != nil {
var msg = "Error while retrieving last id"
logError(err, msg)
http.Error(w, SerializeError(err, msg), http.StatusInternalServerError)
}
}
trip.ID = newTripID.ID
serializedTrip, _ := json.Marshal(trip)
fmt.Fprintf(w, string(serializedTrip))
}
type newTrip struct {
ID string
}
// End of Trip Service Methods

View File

@ -0,0 +1,436 @@
package tripsgo
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"os"
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
var tripID string
var apiTestList = []APITestCase{
{
Tag: "t1 - Get all trips",
Method: "GET",
URL: "/api/trips",
Status: 200,
},
{
Tag: "t2 - Get a nonexistent trip",
Method: "GET",
URL: "/api/trips/99999",
Status: 404,
},
{
Tag: "t3 - Create a Trip",
Method: "POST",
URL: "/api/trips",
Body: `{
"Name":"Trip CREATE TEST",
"UserId":"GO_TEST",
"RecordedTimeStamp": "2018-04-19T19:08:16.03Z",
"EndTimeStamp": "2018-04-19T19:42:49.573Z",
"Rating":95,
"IsComplete":false,
"HasSimulatedOBDData":true,
"AverageSpeed":100,
"FuelUsed":10.27193484,
"HardStops":2,
"HardAccelerations":4,
"Distance":30.0275486,
"CreatedAt":"2018-01-01T12:00:00Z",
"UpdatedAt":"2001-01-01T12:00:00Z"
}`,
Status: 200,
},
{
Tag: "t4 - Update a trip",
Method: "PATCH",
URL: "/api/trips/{tripID}",
Body: `{
"Name":"Trip UPDATE TEST",
"UserId":"GO_TEST",
"RecordedTimeStamp": "2018-04-19T19:08:16.03Z",
"EndTimeStamp": "2018-04-19T19:42:49.573Z",
"Rating":91005,
"IsComplete":true,
"HasSimulatedOBDData":true,
"AverageSpeed":100,
"FuelUsed":10.27193484,
"HardStops":2,
"HardAccelerations":4,
"Distance":30.0275486,
"CreatedAt":"2018-01-01T12:00:00Z",
"UpdatedAt":"2001-01-01T12:00:00Z"
}`,
Status: 200,
},
{
Tag: "t5 - Create Trip Point",
Method: "POST",
URL: "/api/trips/{tripID}/trippoints",
Body: `{
"TripId": "{tripID}",
"Latitude": 47.67598,
"Longitude": -122.10612,
"Speed": -255,
"RecordedTimeStamp": "2018-05-24T10:00:15.003Z",
"Sequence": 2,
"RPM": -255,
"ShortTermFuelBank": -255,
"LongTermFuelBank": -255,
"ThrottlePosition": -255,
"RelativeThrottlePosition": -255,
"Runtime": -255,
"DistanceWithMalfunctionLight": -255,
"EngineLoad": -255,
"EngineFuelRate": -255,
"CreatedAt": "0001-01-01T00:00:00Z",
"UpdatedAt": "0001-01-01T00:00:00Z"
}`,
Status: 200,
},
{
Tag: "t6 - Update Trip Point",
Method: "PATCH",
URL: "/api/trips/{tripID}/trippoints/{tripPointID}",
Body: `{
"Id": "{tripPointID}",
"TripId": "{tripID}",
"Latitude": 47.67598,
"Longitude": -122.10612,
"Speed": -255,
"RecordedTimeStamp": "2018-05-24T10:00:15.003Z",
"Sequence": 2,
"RPM": -255,
"ShortTermFuelBank": -255,
"LongTermFuelBank": -255,
"ThrottlePosition": -255,
"RelativeThrottlePosition": -255,
"Runtime": -255,
"DistanceWithMalfunctionLight": -255,
"EngineLoad": -255,
"EngineFuelRate": -255,
"Created": "0001-01-01T00:00:00Z",
"UpdatedAt": "0001-01-01T00:00:00Z"
}`,
ExpectedResponse: "",
Status: 200,
},
{
Tag: "t7 - Read Trip Points for Trip",
Method: "GET",
URL: "/api/trips/{tripID}/trippoints",
Status: 200,
},
{
Tag: "t8 - Read Trip Points By Trip Point ID",
Method: "GET",
URL: "/api/trips/{tripID}/trippoints/{tripPointID}",
Status: 200,
},
{
Tag: "t9 - Delete Trip Point",
Method: "DELETE",
URL: "/api/trips/{tripID}/trippoints/{tripPointID}",
Status: 200,
},
{
Tag: "t10 - Delete a Trip",
Method: "DELETE",
URL: "/api/trips/{tripID}",
Status: 200,
},
{
Tag: "t11 - Get All Trips for User",
Method: "GET",
URL: "/api/trips/user/SomeUser",
Status: 200,
},
}
func TestTripApis(t *testing.T) {
router := NewRouter()
var debug, present = os.LookupEnv("DEBUG_LOGGING")
if present && debug == "true" {
InitLogging(os.Stdout, os.Stdout, os.Stdout)
} else {
// if debug env is not present or false, do not log debug output to console
InitLogging(os.Stdout, ioutil.Discard, os.Stdout)
}
RunAPITests(t, router, apiTestList[0:3])
// setup update trip test (URL, Body, expected Response)
apiTestList[3].URL = strings.Replace(apiTestList[3].URL, "{tripID}", TripFromStr(apiTestList[2].ActualResponse).ID, 1)
apiTestList[3].Body = GetUpdateTrip(apiTestList[2].ActualResponse, apiTestList[3].Body)
apiTestList[3].ExpectedResponse = apiTestList[3].Body
// setup create trip point test
apiTestList[4].URL = strings.Replace(apiTestList[4].URL, "{tripID}", TripFromStr(apiTestList[2].ActualResponse).ID, 1)
apiTestList[4].Body = strings.Replace(apiTestList[4].Body, "{tripID}", TripFromStr(apiTestList[2].ActualResponse).ID, 1)
// run update trip and create trip point tests
RunAPITests(t, router, apiTestList[3:5])
// setup update trip point test
apiTestList[5].URL = strings.Replace(apiTestList[5].URL, "{tripID}", TripFromStr(apiTestList[2].ActualResponse).ID, 1)
apiTestList[5].URL = strings.Replace(apiTestList[5].URL, "{tripPointID}", TripPointFromStr(apiTestList[4].ActualResponse).ID, 1)
apiTestList[5].Body = GetUpdateTripPoint(apiTestList[4].ActualResponse, apiTestList[5].Body)
// setup read trip points for trip test
apiTestList[6].URL = strings.Replace(apiTestList[6].URL, "{tripID}", TripFromStr(apiTestList[2].ActualResponse).ID, 1)
// // setup ready trip points by trip point id test
apiTestList[7].URL = strings.Replace(apiTestList[7].URL, "{tripID}", TripFromStr(apiTestList[2].ActualResponse).ID, 1)
apiTestList[7].URL = strings.Replace(apiTestList[7].URL, "{tripPointID}", TripPointFromStr(apiTestList[4].ActualResponse).ID, 1)
// //setup delete trip point test
apiTestList[8].URL = strings.Replace(apiTestList[8].URL, "{tripID}", TripFromStr(apiTestList[2].ActualResponse).ID, 1)
apiTestList[8].URL = strings.Replace(apiTestList[8].URL, "{tripPointID}", TripPointFromStr(apiTestList[4].ActualResponse).ID, 1)
// setup delete test (URL)
apiTestList[9].URL = strings.Replace(apiTestList[9].URL, "{tripID}", TripFromStr(apiTestList[2].ActualResponse).ID, 1)
// run update test
RunAPITests(t, router, apiTestList[5:10])
RunAPITests(t, router, apiTestList[10:11])
}
func TestGetAllTripsReturnsServerErrorIfBadDbConnection(t *testing.T) {
defer t.Cleanup(resetDataAccessEnvVars)
//arrange
os.Setenv("SQL_DRIVER", "not_a_real_driver")
RebindDataAccessEnvironmentVariables()
info := new(bytes.Buffer)
InitLogging(info, os.Stdout, os.Stdout)
var tr bool = true
debug = &tr
//act
router := NewRouter()
RunAPITestsPlainText(t, router, []APITestCase{
{
Tag: "t1 - Get all trips",
Method: "GET",
URL: "/api/trips",
Status: 500,
},
})
//assert
actual := fmt.Sprint(info)
assert.Contains(t, actual, "getAllTrips - Query Failed to Execute")
}
func TestGetAllTripsReturnsErrorScanningTripsIfMissingSqlFields(t *testing.T) {
defer t.Cleanup(resetDataAccessEnvVars)
//arrange
//oldSelectAllTripsQuery := func(SelectAllTripsQuery)
var OldSelectAllTripsQuery = SelectAllTripsQuery
SelectAllTripsQuery = func() string {
return `SELECT
Id
FROM Trips
WHERE Deleted = 0`
}
defer func() { SelectAllTripsQuery = OldSelectAllTripsQuery }()
info := new(bytes.Buffer)
InitLogging(info, os.Stdout, os.Stdout)
var tr bool = true
debug = &tr
//act
router := NewRouter()
RunAPITestsPlainText(t, router, []APITestCase{
{
Tag: "t1 - Get all trips",
Method: "GET",
URL: "/api/trips",
Status: 500,
},
})
//assert
actual := fmt.Sprint(info)
assert.Contains(t, actual, "GetAllTrips - Error scanning Trips")
}
func TestGetAllTripsForUsersReturnsServerErrorIfBadDbConnection(t *testing.T) {
defer t.Cleanup(resetDataAccessEnvVars)
//arrange
os.Setenv("SQL_DRIVER", "not_a_real_driver")
RebindDataAccessEnvironmentVariables()
info := new(bytes.Buffer)
InitLogging(info, os.Stdout, os.Stdout)
var tr bool = true
debug = &tr
//act
router := NewRouter()
RunAPITestsPlainText(t, router, []APITestCase{
{
Tag: "t11 - Get All Trips for User",
Method: "GET",
URL: "/api/trips/user/SomeUser",
Status: 500,
},
})
//assert
actual := fmt.Sprint(info)
assert.Contains(t, actual, "getAllTripsForUser - Error while retrieving trips from database")
}
func TestGetAllTripsForUserReturnsErrorScanningTripsIfMissingSqlFields(t *testing.T) {
defer t.Cleanup(resetDataAccessEnvVars)
//arrange
//oldSelectAllTripsQuery := func(SelectAllTripsQuery)
var OldSelectAllTripsForUserQuery = SelectAllTripsForUserQuery
SelectAllTripsForUserQuery = func(userID string) string {
return `SELECT
Id
FROM Trips
WHERE UserId ='` + userID + `'
AND Deleted = 0`
}
defer func() { SelectAllTripsForUserQuery = OldSelectAllTripsForUserQuery }()
info := new(bytes.Buffer)
InitLogging(info, os.Stdout, os.Stdout)
var tr bool = true
debug = &tr
//act
router := NewRouter()
RunAPITestsPlainText(t, router, []APITestCase{
{
Tag: "t11 - Get All Trips for User",
Method: "GET",
URL: "/api/trips/user/SomeUser",
Status: 500,
},
})
//assert
actual := fmt.Sprint(info)
assert.Contains(t, actual, "getAllTripsForUser - Error scanning Trips")
}
func TestCreateTripReturnsErrorifInvalidJsonBody(t *testing.T) {
info := new(bytes.Buffer)
InitLogging(info, os.Stdout, os.Stdout)
//act
router := NewRouter()
RunAPITestsPlainText(t, router, []APITestCase{
{
Tag: "t3 - Create a Trip",
Method: "POST",
URL: "/api/trips",
Body: `{
"Name":"Trip CREATE TEST",
"UserId":"GO_TEST",
"RecordedTimeStamp": "2018-04-19T19:08:16.03Z",
"EndTimeStamp": "2018-04-19T19:42:49.573Z",
"Rating":95,
"IsComplete":`,
Status: 500,
},
})
//assert
actual := fmt.Sprint(info)
assert.Contains(t, actual, "Error while decoding json")
}
func TestUpdateTripReturnsErrorifInvalidJsonBody(t *testing.T) {
info := new(bytes.Buffer)
InitLogging(info, os.Stdout, os.Stdout)
//act
router := NewRouter()
RunAPITestsPlainText(t, router, []APITestCase{
{
Tag: "t4 - Update a trip",
Method: "PATCH",
URL: "/api/trips/{tripID}",
Body: `{
"Name":"Trip UPDATE TEST",
"UserId":"GO_TEST",
"RecordedTimeStamp": "2018-04-19T19:08:16.03Z",
"EndTimeStamp": "2018-04-19T19:42:49.573Z",
"Rating":`,
Status: 500,
},
})
//assert
actual := fmt.Sprint(info)
assert.Contains(t, actual, "Update Trip - Error while decoding trip json")
}
func GetUpdateTrip(tripCreate string, tripUpdate string) string {
tripC := TripFromStr(tripCreate)
tripU := TripFromStr(tripUpdate)
tripU.ID = tripC.ID
serializedTripUpdate, _ := json.Marshal(tripU)
return string(serializedTripUpdate)
}
func GetUpdateTripPoint(tripPointCreate string, tripPointUpdate string) string {
tripPointC := TripPointFromStr(tripPointCreate)
tripPointU := TripPointFromStr(tripPointUpdate)
tripPointU.ID = tripPointC.ID
tripPointU.TripID = tripPointC.TripID
serializedTripUpdate, _ := json.Marshal(tripPointU)
return string(serializedTripUpdate)
}
func TripFromStr(tripStr string) Trip {
trip := Trip{}
Debug.Println("DEBUG: TripFromStr - " + tripStr)
errCreate := json.Unmarshal([]byte(tripStr), &trip)
if errCreate != nil {
log.Println("TripFromStr - Invalid trip string")
log.Fatal(errCreate)
}
return trip
}
func TripPointFromStr(tripPointStr string) TripPoint {
tripPoint := TripPoint{}
Debug.Println("DEBUG: TripPointFromStr - " + tripPointStr)
errCreate := json.Unmarshal([]byte(tripPointStr), &tripPoint)
if errCreate != nil {
log.Println("TripPointFromStr - Invalid trip point string")
log.Fatal(errCreate)
}
Debug.Println(tripPointStr)
return tripPoint
}

View File

@ -0,0 +1,58 @@
package tripsgo
import (
"database/sql"
"time"
)
// TripPoint - Represents a single point record in a trip
type TripPoint struct {
// Trip Point ID
ID string `json:"Id,omitempty"`
// Trip ID
TripID string `json:"TripId,omitempty"`
Latitude float32 `json:"Latitude,omitempty"`
Longitude float32 `json:"Longitude,omitempty"`
Speed float32 `json:"Speed,omitempty"`
RecordedTimeStamp string `json:"RecordedTimeStamp,omitempty"`
Sequence int32 `json:"Sequence,omitempty"`
RPM float32 `json:"RPM,omitempty"`
ShortTermFuelBank float32 `json:"ShortTermFuelBank,omitempty"`
LongTermFuelBank float32 `json:"LongTermFuelBank,omitempty"`
ThrottlePosition float32 `json:"ThrottlePosition,omitempty"`
RelativeThrottlePosition float32 `json:"RelativeThrottlePosition,omitempty"`
Runtime float32 `json:"Runtime,omitempty"`
DistanceWithMalfunctionLight float32 `json:"DistanceWithMalfunctionLight,omitempty"`
EngineLoad float32 `json:"EngineLoad,omitempty"`
MassFlowRate float32 `json:"MassFlowRate,omitempty"`
EngineFuelRate float32 `json:"EngineFuelRate,omitempty"`
VIN sql.NullString `json:"VIN,omitempty"`
HasOBDData bool `json:"HasOBDData,omitempty"`
HasSimulatedOBDData bool `json:"HasSimulatedOBDData,omitempty"`
CreatedAt time.Time `json:"CreatedAt,omitempty"`
UpdatedAt time.Time `json:"UpdatedAt,omitempty"`
Deleted bool `json:"Deleted,omitempty"`
}

View File

@ -0,0 +1,15 @@
package tripsgo
import (
"fmt"
"net/http"
"os"
)
func versionGet(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain; charset=UTF-8")
w.WriteHeader(http.StatusOK)
version := os.Getenv("APP_VERSION")
fmt.Fprintf(w, version)
}

View File

@ -0,0 +1,32 @@
package tripsgo
import (
"io/ioutil"
"os"
"testing"
)
var versionRouteTests = []APITestCase{
{
Tag: "versionService",
Method: "GET",
URL: "/api/version/trips",
Status: 200,
ExpectedResponse: `test123`,
},
}
func TestVersionServiceUnit(t *testing.T) {
router := NewRouter()
os.Setenv("APP_VERSION", "test123")
var debug, present = os.LookupEnv("DEBUG_LOGGING")
if present && debug == "true" {
InitLogging(os.Stdout, os.Stdout, os.Stdout)
} else {
// if debug env is not present or false, do not log debug output to console
InitLogging(os.Stdout, ioutil.Discard, os.Stdout)
}
RunAPITestsPlainText(t, router, versionRouteTests[0:1])
}

4
apis/user-java/.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
target
.project
.settings
.classpath

27
apis/user-java/Dockerfile Normal file
View File

@ -0,0 +1,27 @@
# First stage to build the application
FROM maven:3.6.3-openjdk-11-slim AS build-env
ADD ./pom.xml pom.xml
ADD ./src src/
RUN mvn clean package
# build runtime image
FROM openjdk:11-jre-slim
EXPOSE 8080
# docker build argument
# This can be specified during the docker build step by adding " --build-arg build_version=<value>"
# App version can be accessed via the uri path /api/version/user-java
# https://vsupalov.com/docker-build-pass-environment-variables/
ARG build_version="user-java default"
ENV SQL_USER="YourUserName" \
SQL_PASSWORD="changeme" \
SQL_SERVER="changeme.database.windows.net" \
SQL_DBNAME="mydrivingDB" \
APP_VERSION=$build_version
# Add the application's jar to the container
COPY --from=build-env target/swagger-spring-1.0.0.jar user-java.jar
ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/user-java.jar"]

73
apis/user-java/README.md Normal file
View File

@ -0,0 +1,73 @@
# Overview
This server was generated by the [swagger-codegen](https://github.com/swagger-api/swagger-codegen) project.
By using the [OpenAPI-Spec](https://github.com/swagger-api/swagger-core), you can easily generate a server stub.
This is an example of building a swagger-enabled server in Java using the SpringBoot framework.
The underlying library integrating swagger to SpringBoot is [springfox](https://github.com/springfox/springfox)
Start your server as an simple java application.
You can view the api documentation in swagger-ui by pointing to
[http://localhost:8080/api/swagger-ui.html](http://localhost:8080/api/swagger-ui.html)
Change default port value in `src/main/resources/application.properties`
## Build the project
1. Install Maven [https://maven.apache.org/install.html](https://maven.apache.org/install.html) and setup the environment path accordingly
2. Go into the project root directory that has the pom.xml and run `mvn package`
## Run only the unit tests
`mvn test`
## Run the application
1. Modify the `src/main/resources/application.properties` values with valid SQL Server values.
```java
spring.datasource.url=someurl
spring.datasource.username=username
spring.datasource.password=password
```
1. Start the API: `mvn spring-boot:run`
### POST API Example
This curl command creates a new user with ID 1234.
```bash
curl -X POST --header 'Content-Type: application/json' --header 'Accept: application/json' -d '{ \
"createdAt":"2018-08-07", \
"deleted": false, \
"firstName": "Hacker", \
"fuelConsumption": 0, \
"hardAccelerations": 0, \
"hardStops": 0, \
"id": "1234", \
"lastName": "Test", \
"maxSpeed": 0, \
"profilePictureUri": "https://pbs.twimg.com/profile_images/1003946090146693122/IdMjh-FQ_bigger.jpg", \
"ranking": 0, \
"rating": 0, \
"totalDistance": 0, \
"totalTime": 0, \
"totalTrips": 0, \
"updatedAt": "2018-08-07", \
"userId": "Hacker3" \
}' 'http://localhost:8080/api/user/1234'
```
### PATCH API Example
This updates the `fuelConsumption` and `hardStops` fields from the user created above.
```bash
curl -X PATCH --header 'Content-Type: application/json' --header 'Accept: application/json' -d '{ \
"fuelConsumption":20, \
"hardStops":74371 \
}
```

View File

@ -0,0 +1,15 @@
#!/bin/bash
# https://maven.apache.org/guides/introduction/introduction-to-the-lifecycle.html
# clean the output of the previous build
mvn clean
# run unit tests
mvn test
# create distributable package
mvn package
# run integration tests
mvn verify

115
apis/user-java/pom.xml Normal file
View File

@ -0,0 +1,115 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>io.swagger</groupId>
<artifactId>swagger-spring</artifactId>
<packaging>jar</packaging>
<name>swagger-spring</name>
<version>1.0.0</version>
<properties>
<java.version>1.8</java.version>
<maven.compiler.source>${java.version}</maven.compiler.source>
<maven.compiler.target>${java.version}</maven.compiler.target>
<springfox-version>2.9.2</springfox-version>
</properties>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.9.RELEASE</version>
</parent>
<build>
<sourceDirectory>src/main/java</sourceDirectory>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
<resources>
<resource>
<directory>src/main/resources</directory>
<includes>
<include>application.properties</include>
</includes>
</resource>
</resources>
</build>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</dependency>
<!--SpringFox dependencies -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>${springfox-version}</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>${springfox-version}</version>
</dependency>
<dependency>
<groupId>com.github.joschi.jackson</groupId>
<artifactId>jackson-datatype-threetenbp</artifactId>
<version>2.6.4</version>
</dependency>
<!-- Bean Validation API support -->
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
</dependency>
<!-- JPA Data (We are going to use Repositories, Entities, Hibernate, etc...) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.microsoft.sqlserver</groupId>
<artifactId>mssql-jdbc</artifactId>
<version>8.2.2.jre11</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>com.jayway.jsonpath</groupId>
<artifactId>json-path</artifactId>
<version>2.2.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.0</version>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,22 @@
package io.swagger;
import com.fasterxml.jackson.databind.util.ISO8601DateFormat;
import com.fasterxml.jackson.databind.util.ISO8601Utils;
import java.text.FieldPosition;
import java.util.Date;
public class RFC3339DateFormat extends ISO8601DateFormat {
private static final long serialVersionUID = 1L;
// Same as ISO8601DateFormat but serializing milliseconds.
@Override
public StringBuffer format(Date date, StringBuffer toAppendTo, FieldPosition fieldPosition) {
String value = ISO8601Utils.format(date, true);
toAppendTo.append(value);
return toAppendTo;
}
}

View File

@ -0,0 +1,40 @@
package io.swagger;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.ExitCodeGenerator;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.data.jpa.convert.threeten.Jsr310JpaConverters;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
@EntityScan(
basePackageClasses = {Swagger2SpringBoot.class, Jsr310JpaConverters.class}
)
@SpringBootApplication
@EnableSwagger2
@ComponentScan(basePackages = { "io.swagger", "io.swagger.api" })
public class Swagger2SpringBoot implements CommandLineRunner {
@Override
public void run(String... arg0) throws Exception {
if (arg0.length > 0 && arg0[0].equals("exitcode")) {
throw new ExitException();
}
}
public static void main(String[] args) throws Exception {
new SpringApplication(Swagger2SpringBoot.class).run(args);
}
class ExitException extends RuntimeException implements ExitCodeGenerator {
private static final long serialVersionUID = 1L;
@Override
public int getExitCode() {
return 10;
}
}
}

View File

@ -0,0 +1,11 @@
package io.swagger.api;
@javax.annotation.Generated(value = "io.swagger.codegen.languages.SpringCodegen", date = "2018-08-03T19:26:46.543Z")
public class ApiException extends Exception{
private int code;
public ApiException (int code, String msg) {
super(msg);
this.code = code;
}
}

View File

@ -0,0 +1,28 @@
package io.swagger.api;
import java.io.IOException;
import javax.servlet.*;
import javax.servlet.http.HttpServletResponse;
@javax.annotation.Generated(value = "io.swagger.codegen.languages.SpringCodegen", date = "2018-08-03T19:26:46.543Z")
public class ApiOriginFilter implements javax.servlet.Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletResponse res = (HttpServletResponse) response;
res.addHeader("Access-Control-Allow-Origin", "*");
res.addHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, PUT");
res.addHeader("Access-Control-Allow-Headers", "Content-Type");
chain.doFilter(request, response);
}
@Override
public void destroy() {
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
}

View File

@ -0,0 +1,70 @@
package io.swagger.api;
import javax.xml.bind.annotation.XmlTransient;
@javax.annotation.Generated(value = "io.swagger.codegen.languages.SpringCodegen", date = "2018-08-03T19:26:46.543Z")
@javax.xml.bind.annotation.XmlRootElement
public class ApiResponseMessage {
public static final int ERROR = 1;
public static final int WARNING = 2;
public static final int INFO = 3;
public static final int OK = 4;
public static final int TOO_BUSY = 5;
int code;
String type;
String message;
public ApiResponseMessage(){}
public ApiResponseMessage(int code, String message){
this.code = code;
switch(code){
case ERROR:
setType("error");
break;
case WARNING:
setType("warning");
break;
case INFO:
setType("info");
break;
case OK:
setType("ok");
break;
case TOO_BUSY:
setType("too busy");
break;
default:
setType("unknown");
break;
}
this.message = message;
}
@XmlTransient
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}

View File

@ -0,0 +1,28 @@
/**
* NOTE: This class is auto generated by the swagger code generator program (2.3.1).
* https://github.com/swagger-api/swagger-codegen
* Do not edit the class manually.
*/
package io.swagger.api;
import io.swagger.model.ErrorResponseDefault;
import io.swagger.model.Healthcheck;
import io.swagger.annotations.*;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
@javax.annotation.Generated(value = "io.swagger.codegen.languages.SpringCodegen", date = "2018-08-03T19:26:46.543Z")
@Api(value = "User Java", description = "User-Java API", tags = { "User Java" })
public interface HealthcheckApi {
@ApiOperation(value = "", nickname = "healthcheckUserGet", notes = "Returns healthcheck for systems looking to ensure API is up and operational", response = Healthcheck.class, tags={ })
@ApiResponses(value = {
@ApiResponse(code = 200, message = "Service is healthy", response = Healthcheck.class),
@ApiResponse(code = 200, message = "An error occurred", response = ErrorResponseDefault.class) })
@RequestMapping(value = "/healthcheck/user-java",
produces = { "application/json" },
method = RequestMethod.GET)
ResponseEntity<Healthcheck> healthcheckUserGet();
}

View File

@ -0,0 +1,39 @@
package io.swagger.api;
import io.swagger.model.Healthcheck;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
@javax.annotation.Generated(value = "io.swagger.codegen.languages.SpringCodegen", date = "2018-08-03T19:26:46.543Z")
@Controller
public class HealthcheckApiController implements HealthcheckApi {
private static final Logger log = LoggerFactory.getLogger(HealthcheckApiController.class);
private final ObjectMapper objectMapper;
private final HttpServletRequest request;
@org.springframework.beans.factory.annotation.Autowired
public HealthcheckApiController(ObjectMapper objectMapper, HttpServletRequest request) {
this.objectMapper = objectMapper;
this.request = request;
}
public ResponseEntity<Healthcheck> healthcheckUserGet() {
try {
return new ResponseEntity<Healthcheck>(objectMapper.readValue("{ \"message\" : \"User-Java Service Healthcheck\", \"status\" : \"healthy\"}", Healthcheck.class), HttpStatus.OK);
} catch (IOException e) {
log.error("Couldn't serialize response for content type application/json", e);
return new ResponseEntity<Healthcheck>(HttpStatus.INTERNAL_SERVER_ERROR);
}
}
}

View File

@ -0,0 +1,11 @@
package io.swagger.api;
@javax.annotation.Generated(value = "io.swagger.codegen.languages.SpringCodegen", date = "2018-08-03T19:26:46.543Z")
public class NotFoundException extends ApiException {
private int code;
public NotFoundException (int code, String msg) {
super(code, msg);
this.code = code;
}
}

View File

@ -0,0 +1,43 @@
/**
* NOTE: This class is auto generated by the swagger code generator program (2.3.1).
* https://github.com/swagger-api/swagger-codegen
* Do not edit the class manually.
*/
package io.swagger.api;
import io.swagger.model.ErrorResponseDefault;
import io.swagger.model.InlineResponseDefault;
import io.swagger.model.Profile;
import io.swagger.annotations.*;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import javax.validation.Valid;
@javax.annotation.Generated(value = "io.swagger.codegen.languages.SpringCodegen", date = "2018-08-03T19:26:46.543Z")
@Api(value = "User Java", description = "User-Java API", tags = { "User Java" })
public interface UserApi {
@ApiOperation(value = "", nickname = "updateUser", notes = "Update User", response = Profile.class, tags={ })
@ApiResponses(value = {
@ApiResponse(code = 200, message = "User Updated", response = Profile.class),
@ApiResponse(code = 404, message = "User profile not found"),
@ApiResponse(code = 200, message = "Unknown Error", response = ErrorResponseDefault.class) })
@RequestMapping(value = "/user-java/{userID}",
consumes = { "application/json" },
method = RequestMethod.PATCH)
ResponseEntity<Profile> updateUser(@ApiParam(value = "User's unique ID",required=true) @PathVariable("userID") String userID,@ApiParam(value = "Details of the profile" ,required=true ) @Valid @RequestBody Profile profile);
@ApiOperation(value = "", nickname = "userPOST", notes = "Declares and creates a new profile", response = Profile.class, tags={ })
@ApiResponses(value = {
@ApiResponse(code = 201, message = "Creation successful", response = Profile.class),
@ApiResponse(code = 200, message = "An error occurred", response = InlineResponseDefault.class) })
@RequestMapping(value = "/user-java/{userID}",
consumes = { "application/json" },
method = RequestMethod.POST)
ResponseEntity<Profile> userPOST(@ApiParam(value = "User's unique ID",required=true) @PathVariable("userID") String userID,@ApiParam(value = "Details of the profile" ,required=true ) @Valid @RequestBody Profile profile);
}

View File

@ -0,0 +1,68 @@
package io.swagger.api;
import io.swagger.model.Profile;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.swagger.annotations.*;
import io.swagger.repository.UserRepositoryService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import javax.validation.Valid;
import javax.servlet.http.HttpServletRequest;
@javax.annotation.Generated(value = "io.swagger.codegen.languages.SpringCodegen", date = "2018-08-03T19:26:46.543Z")
@Controller
public class UserApiController implements UserApi {
private static final Logger log = LoggerFactory.getLogger(UserApiController.class);
private ObjectMapper objectMapper;
private HttpServletRequest request;
@Autowired
UserRepositoryService userRepositoryService;
@org.springframework.beans.factory.annotation.Autowired
public UserApiController(ObjectMapper objectMapper, HttpServletRequest request) {
this.objectMapper = objectMapper;
this.request = request;
}
public ResponseEntity<Profile> updateUser(@ApiParam(value = "User's unique ID",required=true) @PathVariable("userID") String userID,@ApiParam(value = "Details of the profile" ,required=true ) @Valid @RequestBody Profile profile) {
String accept = request.getHeader("Accept");
if (accept != null && accept.contains("application/json")) {
try {
profile.setId(userID);
Profile updatedUser = userRepositoryService.update(profile);
return new ResponseEntity<Profile>(updatedUser, HttpStatus.OK);
} catch (Exception e) {
log.error("Error updating user profile", e.getMessage());
return new ResponseEntity<Profile>(HttpStatus.INTERNAL_SERVER_ERROR);
}
}
return new ResponseEntity<Profile>(HttpStatus.NOT_IMPLEMENTED);
}
public ResponseEntity<Profile> userPOST(@ApiParam(value = "User's unique ID",required=true) @PathVariable("userID") String userID,@ApiParam(value = "Details of the profile" ,required=true ) @Valid @RequestBody Profile profile) {
String accept = request.getHeader("Accept");
if (accept != null && accept.contains("application/json")) {
try {
return new ResponseEntity<Profile>(userRepositoryService.save(profile), HttpStatus.OK);
} catch (Exception e) {
log.error("Couldn't create new profile", e.getMessage());
return new ResponseEntity<Profile>(HttpStatus.INTERNAL_SERVER_ERROR);
}
}
return new ResponseEntity<Profile>(HttpStatus.NOT_IMPLEMENTED);
}
}

View File

@ -0,0 +1,17 @@
package io.swagger.api;
import io.swagger.annotations.*;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
@javax.annotation.Generated(value = "io.swagger.codegen.languages.SpringCodegen", date = "2018-08-03T19:26:46.543Z")
@Api(value = "version", description = "the version API")
public interface VersionApi {
@ApiOperation(value = "", nickname = "versionGet", notes = "Returns version for systems looking to identify the current API version or tag", response = String.class, tags={ })
@RequestMapping(value = "/version/user-java",
produces = { "text/plain" },
method = RequestMethod.GET)
ResponseEntity<String> versionGet();
}

View File

@ -0,0 +1,31 @@
package io.swagger.api;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import javax.servlet.http.HttpServletRequest;
@javax.annotation.Generated(value = "io.swagger.codegen.languages.SpringCodegen", date = "2018-08-03T19:26:46.543Z")
@Controller
public class VersionApiController implements VersionApi {
private static final Logger log = LoggerFactory.getLogger(VersionApiController.class);
private final HttpServletRequest request;
private ObjectMapper objectMapper;
@org.springframework.beans.factory.annotation.Autowired
public VersionApiController(ObjectMapper objectMapper, HttpServletRequest request) {
this.objectMapper = objectMapper;
this.request = request;
}
@Override
public ResponseEntity<String> versionGet() {
String version = System.getenv("APP_VERSION");
return new ResponseEntity<String>(version, HttpStatus.OK);
}
}

View File

@ -0,0 +1,232 @@
package io.swagger.configuration;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonTokenId;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.datatype.threetenbp.DateTimeUtils;
import com.fasterxml.jackson.datatype.threetenbp.DecimalUtils;
import com.fasterxml.jackson.datatype.threetenbp.deser.ThreeTenDateTimeDeserializerBase;
import com.fasterxml.jackson.datatype.threetenbp.function.BiFunction;
import com.fasterxml.jackson.datatype.threetenbp.function.Function;
import org.threeten.bp.DateTimeException;
import org.threeten.bp.Instant;
import org.threeten.bp.OffsetDateTime;
import org.threeten.bp.ZoneId;
import org.threeten.bp.ZonedDateTime;
import org.threeten.bp.format.DateTimeFormatter;
import org.threeten.bp.temporal.Temporal;
import org.threeten.bp.temporal.TemporalAccessor;
import java.io.IOException;
import java.math.BigDecimal;
/**
* Deserializer for ThreeTen temporal {@link Instant}s, {@link OffsetDateTime}, and {@link ZonedDateTime}s.
* Adapted from the jackson threetenbp InstantDeserializer to add support for deserializing rfc822 format.
*
* @author Nick Williams
*/
public class CustomInstantDeserializer<T extends Temporal>
extends ThreeTenDateTimeDeserializerBase<T> {
private static final long serialVersionUID = 1L;
public static final CustomInstantDeserializer<Instant> INSTANT = new CustomInstantDeserializer<Instant>(
Instant.class, DateTimeFormatter.ISO_INSTANT,
new Function<TemporalAccessor, Instant>() {
@Override
public Instant apply(TemporalAccessor temporalAccessor) {
return Instant.from(temporalAccessor);
}
},
new Function<FromIntegerArguments, Instant>() {
@Override
public Instant apply(FromIntegerArguments a) {
return Instant.ofEpochMilli(a.value);
}
},
new Function<FromDecimalArguments, Instant>() {
@Override
public Instant apply(FromDecimalArguments a) {
return Instant.ofEpochSecond(a.integer, a.fraction);
}
},
null
);
public static final CustomInstantDeserializer<OffsetDateTime> OFFSET_DATE_TIME = new CustomInstantDeserializer<OffsetDateTime>(
OffsetDateTime.class, DateTimeFormatter.ISO_OFFSET_DATE_TIME,
new Function<TemporalAccessor, OffsetDateTime>() {
@Override
public OffsetDateTime apply(TemporalAccessor temporalAccessor) {
return OffsetDateTime.from(temporalAccessor);
}
},
new Function<FromIntegerArguments, OffsetDateTime>() {
@Override
public OffsetDateTime apply(FromIntegerArguments a) {
return OffsetDateTime.ofInstant(Instant.ofEpochMilli(a.value), a.zoneId);
}
},
new Function<FromDecimalArguments, OffsetDateTime>() {
@Override
public OffsetDateTime apply(FromDecimalArguments a) {
return OffsetDateTime.ofInstant(Instant.ofEpochSecond(a.integer, a.fraction), a.zoneId);
}
},
new BiFunction<OffsetDateTime, ZoneId, OffsetDateTime>() {
@Override
public OffsetDateTime apply(OffsetDateTime d, ZoneId z) {
return d.withOffsetSameInstant(z.getRules().getOffset(d.toLocalDateTime()));
}
}
);
public static final CustomInstantDeserializer<ZonedDateTime> ZONED_DATE_TIME = new CustomInstantDeserializer<ZonedDateTime>(
ZonedDateTime.class, DateTimeFormatter.ISO_ZONED_DATE_TIME,
new Function<TemporalAccessor, ZonedDateTime>() {
@Override
public ZonedDateTime apply(TemporalAccessor temporalAccessor) {
return ZonedDateTime.from(temporalAccessor);
}
},
new Function<FromIntegerArguments, ZonedDateTime>() {
@Override
public ZonedDateTime apply(FromIntegerArguments a) {
return ZonedDateTime.ofInstant(Instant.ofEpochMilli(a.value), a.zoneId);
}
},
new Function<FromDecimalArguments, ZonedDateTime>() {
@Override
public ZonedDateTime apply(FromDecimalArguments a) {
return ZonedDateTime.ofInstant(Instant.ofEpochSecond(a.integer, a.fraction), a.zoneId);
}
},
new BiFunction<ZonedDateTime, ZoneId, ZonedDateTime>() {
@Override
public ZonedDateTime apply(ZonedDateTime zonedDateTime, ZoneId zoneId) {
return zonedDateTime.withZoneSameInstant(zoneId);
}
}
);
protected final Function<FromIntegerArguments, T> fromMilliseconds;
protected final Function<FromDecimalArguments, T> fromNanoseconds;
protected final Function<TemporalAccessor, T> parsedToValue;
protected final BiFunction<T, ZoneId, T> adjust;
protected CustomInstantDeserializer(Class<T> supportedType,
DateTimeFormatter parser,
Function<TemporalAccessor, T> parsedToValue,
Function<FromIntegerArguments, T> fromMilliseconds,
Function<FromDecimalArguments, T> fromNanoseconds,
BiFunction<T, ZoneId, T> adjust) {
super(supportedType, parser);
this.parsedToValue = parsedToValue;
this.fromMilliseconds = fromMilliseconds;
this.fromNanoseconds = fromNanoseconds;
this.adjust = adjust == null ? new BiFunction<T, ZoneId, T>() {
@Override
public T apply(T t, ZoneId zoneId) {
return t;
}
} : adjust;
}
@SuppressWarnings("unchecked")
protected CustomInstantDeserializer(CustomInstantDeserializer<T> base, DateTimeFormatter f) {
super((Class<T>) base.handledType(), f);
parsedToValue = base.parsedToValue;
fromMilliseconds = base.fromMilliseconds;
fromNanoseconds = base.fromNanoseconds;
adjust = base.adjust;
}
@Override
protected JsonDeserializer<T> withDateFormat(DateTimeFormatter dtf) {
if (dtf == _formatter) {
return this;
}
return new CustomInstantDeserializer<T>(this, dtf);
}
@Override
public T deserialize(JsonParser parser, DeserializationContext context) throws IOException {
//NOTE: Timestamps contain no timezone info, and are always in configured TZ. Only
//string values have to be adjusted to the configured TZ.
switch (parser.getCurrentTokenId()) {
case JsonTokenId.ID_NUMBER_FLOAT: {
BigDecimal value = parser.getDecimalValue();
long seconds = value.longValue();
int nanoseconds = DecimalUtils.extractNanosecondDecimal(value, seconds);
return fromNanoseconds.apply(new FromDecimalArguments(
seconds, nanoseconds, getZone(context)));
}
case JsonTokenId.ID_NUMBER_INT: {
long timestamp = parser.getLongValue();
if (context.isEnabled(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS)) {
return this.fromNanoseconds.apply(new FromDecimalArguments(
timestamp, 0, this.getZone(context)
));
}
return this.fromMilliseconds.apply(new FromIntegerArguments(
timestamp, this.getZone(context)
));
}
case JsonTokenId.ID_STRING: {
String string = parser.getText().trim();
if (string.length() == 0) {
return null;
}
if (string.endsWith("+0000")) {
string = string.substring(0, string.length() - 5) + "Z";
}
T value;
try {
TemporalAccessor acc = _formatter.parse(string);
value = parsedToValue.apply(acc);
if (context.isEnabled(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE)) {
return adjust.apply(value, this.getZone(context));
}
} catch (DateTimeException e) {
throw _peelDTE(e);
}
return value;
}
}
throw context.mappingException("Expected type float, integer, or string.");
}
private ZoneId getZone(DeserializationContext context) {
// Instants are always in UTC, so don't waste compute cycles
return (_valueClass == Instant.class) ? null : DateTimeUtils.timeZoneToZoneId(context.getTimeZone());
}
private static class FromIntegerArguments {
public final long value;
public final ZoneId zoneId;
private FromIntegerArguments(long value, ZoneId zoneId) {
this.value = value;
this.zoneId = zoneId;
}
}
private static class FromDecimalArguments {
public final long integer;
public final int fraction;
public final ZoneId zoneId;
private FromDecimalArguments(long integer, int fraction, ZoneId zoneId) {
this.integer = integer;
this.fraction = fraction;
this.zoneId = zoneId;
}
}
}

View File

@ -0,0 +1,16 @@
package io.swagger.configuration;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
/**
* Home redirection to swagger api documentation
*/
@Controller
public class HomeController {
@RequestMapping(value = "/")
public String index() {
System.out.println("swagger-ui.html");
return "redirect:swagger-ui.html";
}
}

View File

@ -0,0 +1,23 @@
package io.swagger.configuration;
import com.fasterxml.jackson.datatype.threetenbp.ThreeTenModule;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.threeten.bp.Instant;
import org.threeten.bp.OffsetDateTime;
import org.threeten.bp.ZonedDateTime;
@Configuration
public class JacksonConfiguration {
@Bean
@ConditionalOnMissingBean(ThreeTenModule.class)
ThreeTenModule threeTenModule() {
ThreeTenModule module = new ThreeTenModule();
module.addDeserializer(Instant.class, CustomInstantDeserializer.INSTANT);
module.addDeserializer(OffsetDateTime.class, CustomInstantDeserializer.OFFSET_DATE_TIME);
module.addDeserializer(ZonedDateTime.class, CustomInstantDeserializer.ZONED_DATE_TIME);
return module;
}
}

View File

@ -0,0 +1,65 @@
package io.swagger.configuration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
@javax.annotation.Generated(value = "io.swagger.codegen.languages.SpringCodegen", date = "2018-08-03T19:26:46.543Z")
@Configuration
@EnableSwagger2
@EnableWebMvc
public class SwaggerDocumentationConfig extends WebMvcConfigurerAdapter {
ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("My Driving User Java API")
.description("API for the user in the My Driving example app. https://github.com/Azure-Samples/openhack-devops-team")
.license("")
.licenseUrl("http://unlicense.org")
.termsOfServiceUrl("")
.version("0.1.0")
.contact(new Contact("","", ""))
.build();
}
@Bean
public Docket customImplementation(){
return new Docket(DocumentationType.SWAGGER_2)
.select()
.apis(RequestHandlerSelectors.basePackage("io.swagger.api"))
.paths(PathSelectors.any())
.build()
.pathMapping("/")
.directModelSubstitute(org.threeten.bp.LocalDate.class, java.sql.Date.class)
.directModelSubstitute(org.threeten.bp.OffsetDateTime.class, java.util.Date.class)
.apiInfo(apiInfo());
}
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addRedirectViewController("/docs/user-java/api-docs", "/api-docs").setKeepQueryParams(true);
registry.addRedirectViewController("/docs/user-java/swagger-resources/configuration/ui","/swagger-resources/configuration/ui");
registry.addRedirectViewController("/docs/user-java/swagger-resources/configuration/security","/swagger-resources/configuration/security");
registry.addRedirectViewController("/docs/user-java/swagger-resources", "/swagger-resources");
registry.addRedirectViewController("/docs/user-java", "/docs/user-java/swagger-ui.html");
}
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/docs/user-java/**").addResourceLocations("classpath:/META-INF/resources/");
}
}

View File

@ -0,0 +1,102 @@
package io.swagger.model;
import java.util.Objects;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.annotations.ApiModelProperty;
import org.springframework.validation.annotation.Validated;
/**
* ErrorResponseDefault
*/
@Validated
@javax.annotation.Generated(value = "io.swagger.codegen.languages.SpringCodegen", date = "2018-08-03T19:26:46.543Z")
public class ErrorResponseDefault {
@JsonProperty("status")
private Integer status = null;
@JsonProperty("message")
private String message = null;
public ErrorResponseDefault status(Integer status) {
this.status = status;
return this;
}
/**
* Error code (if available)
* @return status
**/
@ApiModelProperty(value = "Error code (if available)")
public Integer getStatus() {
return status;
}
public void setStatus(Integer status) {
this.status = status;
}
public ErrorResponseDefault message(String message) {
this.message = message;
return this;
}
/**
* Error Message
* @return message
**/
@ApiModelProperty(value = "Error Message")
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
@Override
public boolean equals(java.lang.Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
ErrorResponseDefault errorResponseDefault = (ErrorResponseDefault) o;
return Objects.equals(this.status, errorResponseDefault.status) &&
Objects.equals(this.message, errorResponseDefault.message);
}
@Override
public int hashCode() {
return Objects.hash(status, message);
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("class ErrorResponseDefault {\n");
sb.append(" status: ").append(toIndentedString(status)).append("\n");
sb.append(" message: ").append(toIndentedString(message)).append("\n");
sb.append("}");
return sb.toString();
}
/**
* Convert the given object to string with each line indented by 4 spaces
* (except the first line).
*/
private String toIndentedString(java.lang.Object o) {
if (o == null) {
return "null";
}
return o.toString().replace("\n", "\n ");
}
}

View File

@ -0,0 +1,102 @@
package io.swagger.model;
import java.util.Objects;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.annotations.ApiModelProperty;
import org.springframework.validation.annotation.Validated;
/**
* Healthcheck
*/
@Validated
@javax.annotation.Generated(value = "io.swagger.codegen.languages.SpringCodegen", date = "2018-08-03T19:26:46.543Z")
public class Healthcheck {
@JsonProperty("message")
private String message = null;
@JsonProperty("status")
private String status = null;
public Healthcheck message(String message) {
this.message = message;
return this;
}
/**
*
* @return message
**/
@ApiModelProperty(value = "")
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public Healthcheck status(String status) {
this.status = status;
return this;
}
/**
*
* @return status
**/
@ApiModelProperty(value = "")
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
@Override
public boolean equals(java.lang.Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Healthcheck healthcheck = (Healthcheck) o;
return Objects.equals(this.message, healthcheck.message) &&
Objects.equals(this.status, healthcheck.status);
}
@Override
public int hashCode() {
return Objects.hash(message, status);
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("class Healthcheck {\n");
sb.append(" message: ").append(toIndentedString(message)).append("\n");
sb.append(" status: ").append(toIndentedString(status)).append("\n");
sb.append("}");
return sb.toString();
}
/**
* Convert the given object to string with each line indented by 4 spaces
* (except the first line).
*/
private String toIndentedString(java.lang.Object o) {
if (o == null) {
return "null";
}
return o.toString().replace("\n", "\n ");
}
}

View File

@ -0,0 +1,105 @@
package io.swagger.model;
import java.util.Objects;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.annotations.ApiModelProperty;
import org.springframework.validation.annotation.Validated;
import javax.validation.constraints.*;
/**
* InlineResponseDefault
*/
@Validated
@javax.annotation.Generated(value = "io.swagger.codegen.languages.SpringCodegen", date = "2018-08-03T19:26:46.543Z")
public class InlineResponseDefault {
@JsonProperty("status")
private Integer status = null;
@JsonProperty("message")
private String message = null;
public InlineResponseDefault status(Integer status) {
this.status = status;
return this;
}
/**
* Get status
* @return status
**/
@ApiModelProperty(required = true, value = "")
@NotNull
public Integer getStatus() {
return status;
}
public void setStatus(Integer status) {
this.status = status;
}
public InlineResponseDefault message(String message) {
this.message = message;
return this;
}
/**
* Get message
* @return message
**/
@ApiModelProperty(required = true, value = "")
@NotNull
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
@Override
public boolean equals(java.lang.Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
InlineResponseDefault inlineResponseDefault = (InlineResponseDefault) o;
return Objects.equals(this.status, inlineResponseDefault.status) &&
Objects.equals(this.message, inlineResponseDefault.message);
}
@Override
public int hashCode() {
return Objects.hash(status, message);
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("class InlineResponseDefault {\n");
sb.append(" status: ").append(toIndentedString(status)).append("\n");
sb.append(" message: ").append(toIndentedString(message)).append("\n");
sb.append("}");
return sb.toString();
}
/**
* Convert the given object to string with each line indented by 4 spaces
* (except the first line).
*/
private String toIndentedString(java.lang.Object o) {
if (o == null) {
return "null";
}
return o.toString().replace("\n", "\n ");
}
}

View File

@ -0,0 +1,9 @@
package io.swagger.repository;
import io.swagger.model.Profile;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface UserRepository extends JpaRepository<Profile, String> {
}

View File

@ -0,0 +1,67 @@
package io.swagger.repository;
import com.google.common.base.Preconditions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.swagger.model.Profile;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.sql.Timestamp;
@Service
public class UserRepositoryService {
private static final Logger LOGGER = LoggerFactory.getLogger(UserRepositoryService.class);
@Autowired
private UserRepository userRepository;
Timestamp timestamp = new Timestamp(System.currentTimeMillis());
public Profile update(Profile updateEntry) {
long start = System.currentTimeMillis();
Preconditions.checkNotNull(updateEntry, "User profile cannot be null");
LOGGER.info("Updating user profile for user=%s", updateEntry.getId());
Profile existingUser = findOne(updateEntry.getId());
if (existingUser == null) {
throw new NullPointerException("Unable to locate user");
}
existingUser.setTotalTrips(updateEntry.getTotalTrips() == null? existingUser.getTotalTrips() : updateEntry.getTotalTrips());
existingUser.setTotalDistance(updateEntry.getTotalDistance() == null ? existingUser.getTotalTrips() : updateEntry.getTotalDistance());
existingUser.setTotalTime(updateEntry.getTotalTime() == null ? existingUser.getTotalTime() : updateEntry.getTotalTime());
existingUser.setHardStops(updateEntry.getHardStops() == null ? existingUser.getHardStops() : updateEntry.getHardStops());
existingUser.setHardAccelerations(updateEntry.getHardAccelerations() == null ? existingUser.getHardAccelerations() : updateEntry.getHardAccelerations());
existingUser.setFuelConsumption(updateEntry.getFuelConsumption() == null ? existingUser.getFuelConsumption() : updateEntry.getFuelConsumption());
existingUser.setMaxSpeed(updateEntry.getMaxSpeed() == null ? existingUser.getMaxSpeed() : updateEntry.getMaxSpeed());
existingUser.setRating(updateEntry.getRating() == null ? existingUser.getRating() : updateEntry.getRating());
existingUser.setRating(updateEntry.getRanking() == null ? existingUser.getRanking() : updateEntry.getRanking());
Profile userProfile = save(existingUser);
long end = System.currentTimeMillis();
long timeElapsed = end - start;
LOGGER.info("Update method called: on {} and response time in ms: {}", timestamp, timeElapsed);
return userProfile;
}
public Profile save(Profile newProfile) {
Preconditions.checkNotNull(newProfile, "User profile cannot be null");
long start = System.currentTimeMillis();
Profile userProfile = userRepository.save(newProfile);
long end = System.currentTimeMillis();
long timeElapsed = end - start;
LOGGER.info("Save method called: on {} and response time in ms: {}", timestamp, timeElapsed);
return userProfile;
}
private Profile findOne(String Id) {
Preconditions.checkNotNull(Id, "User Id cannot be null");
return userRepository.findOne(Id);
}
}

View File

@ -0,0 +1,10 @@
springfox.documentation.swagger.v2.path=/api-docs
server.contextPath=/api
server.port=8080
spring.jackson.date-format=io.swagger.RFC3339DateFormat
spring.jackson.serialization.WRITE_DATES_AS_TIMESTAMPS=false
spring.datasource.driver-class-name=com.microsoft.sqlserver.jdbc.SQLServerDriver
spring.jpa.database-platform=org.hibernate.dialect.SQLServer2012Dialect
spring.datasource.url=jdbc:sqlserver://${SQL_SERVER};databaseName=${SQL_DBNAME};trustServerCertificate=true;selectMethod=cursor
spring.datasource.username=${SQL_USER}
spring.datasource.password=${SQL_PASSWORD}

View File

@ -0,0 +1,119 @@
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.swagger.api.UserApiController;
import io.swagger.model.Profile;
import io.swagger.repository.UserRepositoryService;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.runners.MockitoJUnitRunner;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.mockito.InjectMocks;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.nio.charset.Charset;
import static org.mockito.Mockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
@RunWith(MockitoJUnitRunner.class)
public class UserApiControllerTest {
public static final MediaType APPLICATION_JSON_UTF8 = new MediaType(MediaType.APPLICATION_JSON.getType(), MediaType.APPLICATION_JSON.getSubtype(), Charset.forName("utf8"));
@InjectMocks
private UserApiController userApiController;
private MockMvc mockMvc;
private Profile profile;
@Mock
HttpServletRequest httpServletRequest;
@Mock
UserRepositoryService userRepositoryService;
@Before
public void setup() {
// this must be called for the @Mock annotations above to be processed
// and for the mock service to be injected into the controller under
// test.
MockitoAnnotations.initMocks(this);
mockMvc = MockMvcBuilders.standaloneSetup(userApiController).build();
profile = new Profile();
profile.setFirstName("test");
profile.setUserId("userId");
profile.setRanking(1);
profile.setTotalDistance(1000f);
profile.setId("2");
}
@Test
public void testSave() throws Exception {
when(httpServletRequest.getHeader("Accept")).thenReturn("accept,application/json;charset=UTF-8");
when(userRepositoryService.save(profile)).thenReturn(profile);
mockMvc.perform(
post("/user-java/2")
.contentType(MediaType.APPLICATION_JSON_UTF8)
.accept(MediaType.APPLICATION_JSON)
.content(convertObjectToJsonBytes(profile))
)
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8));
verify(userRepositoryService, times(1)).save(profile);
}
@Test
public void testSave_shouldNotImplemented() throws Exception {
mockMvc.perform(
post("/user-java/2")
.contentType(MediaType.APPLICATION_JSON_UTF8)
.accept(MediaType.APPLICATION_JSON)
.content(convertObjectToJsonBytes(profile))
)
.andExpect(MockMvcResultMatchers.status().is5xxServerError());
}
@Test
public void testUpdate_shouldNotImplemented() throws Exception {
mockMvc.perform(
patch("/user-java/2")
.contentType(MediaType.APPLICATION_JSON_UTF8)
.accept(MediaType.APPLICATION_JSON)
.content(convertObjectToJsonBytes(profile))
)
.andExpect(MockMvcResultMatchers.status().is5xxServerError());
}
@Test
public void testUpdate() throws Exception {
profile.setRanking(2);
profile.setTotalDistance(2500F);
when(httpServletRequest.getHeader("Accept")).thenReturn("accept,application/json;charset=UTF-8");
when(userRepositoryService.update(profile)).thenReturn(profile);
mockMvc.perform(
patch("/user-java/2")
.contentType(APPLICATION_JSON_UTF8)
.content(convertObjectToJsonBytes(profile)))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8));
verify(userRepositoryService, times(1)).update(profile);
}
public static byte[] convertObjectToJsonBytes(Object object) throws IOException {
ObjectMapper mapper = new ObjectMapper();
mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
return mapper.writeValueAsBytes(object);
}
}

View File

@ -0,0 +1,84 @@
import io.swagger.model.Profile;
import io.swagger.repository.UserRepository;
import io.swagger.repository.UserRepositoryService;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.fail;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.*;
@RunWith(MockitoJUnitRunner.class)
public class UserRepositoryServiceTest {
private static final String USER_ID = "test1";
private static final String ID = "1234";
private static final String FIRST_NAME = "test";
private static final String LAST_NAME = "lastname";
private static final int RANKING = 1;
private static final float TOTAL_DISTANCE = 1000;
private Profile profile;
private Integer ranking = 2;
private Float distance = 2000F;
@Mock
UserRepository userRepository;
@InjectMocks
UserRepositoryService userRepositoryService;
@Before
public void setUp() {
profile = new Profile();
profile.setUserId(USER_ID);
profile.setId(ID);
profile.setFirstName(FIRST_NAME);
profile.setLastName(LAST_NAME);
profile.setRanking(RANKING);
profile.setTotalDistance(TOTAL_DISTANCE);
}
@After
public void tearDown() {
profile = null;
}
@Test
public void testSave() {
when(userRepository.save(any(Profile.class))).thenReturn(profile);
assertNotNull(userRepositoryService.save(profile));
}
@Test
public void testUpdate() {
when(userRepository.findOne(ID)).thenReturn(profile);
profile.setRanking(ranking);
profile.setTotalDistance(distance);
when(userRepository.save(profile)).thenReturn(profile);
Profile updated = userRepositoryService.update(profile);
assertEquals(ranking, updated.getRanking());
assertEquals(distance, updated.getTotalDistance());
}
@Test
public void testUpdate_shouldThrowException() {
when(userRepository.findOne(USER_ID)).thenReturn(null);
try {
userRepositoryService.update(profile);
fail("Unable to locate user");
}catch (NullPointerException e) {
assertEquals("Unable to locate user", e.getMessage());
}
verify(userRepository, never()).save(profile);
}
}

View File

@ -0,0 +1 @@
node_modules

View File

@ -0,0 +1,2 @@
coverage
templates

View File

@ -0,0 +1,25 @@
{
"rules": {
"indent": [
2,
4
],
"quotes": [
2,
"single"
],
"linebreak-style": [
2,
"unix"
],
"semi": [
2,
"always"
]
},
"env": {
"es6": true,
"node": true
},
"extends": "eslint:recommended"
}

Some files were not shown because too many files have changed in this diff Show More