Compare commits
No commits in common. "53a0010084ac250247161cf95e5047e25b1764a4" and "23c602b07aadad9400c48b88eb4f76c49ccee750" have entirely different histories.
53a0010084
...
23c602b07a
3
.gitignore
vendored
3
.gitignore
vendored
@ -482,6 +482,3 @@ $RECYCLE.BIN/
|
|||||||
|
|
||||||
# Vim temporary swap files
|
# Vim temporary swap files
|
||||||
*.swp
|
*.swp
|
||||||
|
|
||||||
# VSCode Files
|
|
||||||
.vscode/*
|
|
||||||
@ -5,8 +5,6 @@ VisualStudioVersion = 17.0.31903.59
|
|||||||
MinimumVisualStudioVersion = 10.0.40219.1
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PTV-Street", "src\PTV-Street.csproj", "{B1AF2E92-D6AD-4999-8341-60CE9A728C3B}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PTV-Street", "src\PTV-Street.csproj", "{B1AF2E92-D6AD-4999-8341-60CE9A728C3B}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "tests", "tests\tests.csproj", "{FEB9535F-F5DF-43B1-B6B2-24D5D2102E78}"
|
|
||||||
EndProject
|
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
@ -20,9 +18,5 @@ Global
|
|||||||
{B1AF2E92-D6AD-4999-8341-60CE9A728C3B}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{B1AF2E92-D6AD-4999-8341-60CE9A728C3B}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{B1AF2E92-D6AD-4999-8341-60CE9A728C3B}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{B1AF2E92-D6AD-4999-8341-60CE9A728C3B}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{B1AF2E92-D6AD-4999-8341-60CE9A728C3B}.Release|Any CPU.Build.0 = Release|Any CPU
|
{B1AF2E92-D6AD-4999-8341-60CE9A728C3B}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
{FEB9535F-F5DF-43B1-B6B2-24D5D2102E78}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{FEB9535F-F5DF-43B1-B6B2-24D5D2102E78}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{FEB9535F-F5DF-43B1-B6B2-24D5D2102E78}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{FEB9535F-F5DF-43B1-B6B2-24D5D2102E78}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
EndGlobal
|
EndGlobal
|
||||||
|
|||||||
60
README.md
60
README.md
@ -1,68 +1,10 @@
|
|||||||
# PTV Street Ressource Task
|
|
||||||
|
|
||||||
1. Create a service in dotnet. It has a REST API that serves a resource 'street'. A street can be created and deleted. It has a name, a geometry and a capacity (= how many vehicles can use it within a minute).
|
1. Create a service in dotnet. It has a REST API that serves a resource 'street'. A street can be created and deleted. It has a name, a geometry and a capacity (= how many vehicles can use it within a minute).
|
||||||
2. The street data is stored in a Postgres database, and we use EF core to save data there.
|
2. The street data is stored in a Postgres database, and we use EF core to save data there.
|
||||||
3. Implement an endpoint to add a given single point to the geometry of an existing street on either the beginning or the end, whatever fits better.
|
3. Implement an endpoint to add a given single point to the geometry of an existing street on either the beginning or the end, whatever fits better.
|
||||||
- Bonus: Note that this endpoint has a strange behavior from a user perspective when there are race conditions. Please take care of that in the implementation.
|
- Bonus: Note that this endpoint has a strange behavior from a user perspective when there are race conditions. Please take care of that in the implementation.
|
||||||
4. Add a hidden feature flag to decide whether the operation (in bullet point 3.) is done on the database level, using PostGis, or withing the backend code, algorithmically.
|
4. Add a hidden feature flag to decide whether the operation (in bullet point 3.) is done on the database level, using PostGis, or withing the backend code, algorithmically.
|
||||||
5. Add a Docker file and a Kubernetes manifest, so we can deploy it as a service with 3 replicas.
|
5. Add a Docker file and a Kubernetes manifest, so we can deploy it as a service with 3 replicas.
|
||||||
6. Also create a docker compose file, so we can locally check everything.
|
6. Also create a docker compose file, so we can locally check everything.
|
||||||
|
|
||||||
Authentication and API documentation is not part of this task.
|
Authentication and API documentation is not part of this task.
|
||||||
|
|
||||||
## API
|
|
||||||
|
|
||||||
The following functions are supported:
|
|
||||||
|
|
||||||
- POST `/api/streets/`: Create new Street
|
|
||||||
|
|
||||||
Creates a new street when given the following format:
|
|
||||||
|
|
||||||
```JSON
|
|
||||||
{
|
|
||||||
"name": String,
|
|
||||||
"capacity": int,
|
|
||||||
"geometry": [
|
|
||||||
{
|
|
||||||
"x": int,
|
|
||||||
"y": int
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
- DELETE `/api/streets/<streetname>`: Delete a street
|
|
||||||
|
|
||||||
Deletes the street with the given name.
|
|
||||||
|
|
||||||
- GET `/api/streets/<streetname>`: Outputs a street
|
|
||||||
|
|
||||||
Returns the street with the given name in the following format:
|
|
||||||
|
|
||||||
```JSON
|
|
||||||
{
|
|
||||||
"name": String,
|
|
||||||
"capacity": int,
|
|
||||||
"geometry": [
|
|
||||||
{
|
|
||||||
"x": int,
|
|
||||||
"y": int
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
- PATCH `/api/streets/<streetname>`: Add a given point to a street
|
|
||||||
|
|
||||||
Adds a point to the end of the given street.
|
|
||||||
Optionally, one can specify the method used for this operation. This can be one of either `"Backend"`, `"Database"` or with `"PostGIS"`. If a different value is specified, an Error is returned.
|
|
||||||
|
|
||||||
```JSON
|
|
||||||
{
|
|
||||||
"point": {
|
|
||||||
"x": int,
|
|
||||||
"y": int
|
|
||||||
},
|
|
||||||
"method": "Backend" | "Database" | "PostGIS" // Optional, default is "Backend"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|||||||
@ -1,9 +0,0 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
|
|
||||||
public class StreetDbContext : DbContext
|
|
||||||
{
|
|
||||||
public StreetDbContext(DbContextOptions<StreetDbContext> options) : base(options)
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -4,5 +4,3 @@ var app = builder.Build();
|
|||||||
app.MapGet("/", () => "Hello World!");
|
app.MapGet("/", () => "Hello World!");
|
||||||
|
|
||||||
app.Run();
|
app.Run();
|
||||||
|
|
||||||
public partial class Program { }
|
|
||||||
|
|||||||
@ -1,201 +0,0 @@
|
|||||||
using System.Net.Http.Json;
|
|
||||||
using FluentAssertions;
|
|
||||||
|
|
||||||
public class APITests : IClassFixture<StreetWebApplicationFactory<Program>>
|
|
||||||
{
|
|
||||||
|
|
||||||
private readonly HttpClient client;
|
|
||||||
|
|
||||||
public APITests(StreetWebApplicationFactory<Program> factory)
|
|
||||||
{
|
|
||||||
client = factory.CreateClient();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task GET_Non_Existing_Street_Should_Return_NotFound()
|
|
||||||
{
|
|
||||||
var response = await client.GetAsync("/api/streets/Haid-und-Neu-Straße");
|
|
||||||
|
|
||||||
response.StatusCode.Should().Be(System.Net.HttpStatusCode.NotFound);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task GET_Street_Should_Return_POSTed_Street()
|
|
||||||
{
|
|
||||||
var street = new
|
|
||||||
{
|
|
||||||
name = "Kaiserstraße",
|
|
||||||
capacity = 100,
|
|
||||||
geometry = new[] { new { x = 10, y = 20 } }
|
|
||||||
};
|
|
||||||
|
|
||||||
var post_response = await client.PostAsJsonAsync("/api/streets/", street);
|
|
||||||
|
|
||||||
post_response.StatusCode.Should().Be(System.Net.HttpStatusCode.Created);
|
|
||||||
|
|
||||||
var get_response = await client.GetAsync("/api/streets/Kaiserstraße");
|
|
||||||
|
|
||||||
get_response.StatusCode.Should().Be(System.Net.HttpStatusCode.OK);
|
|
||||||
|
|
||||||
var responseContent = await get_response.Content.ReadFromJsonAsync<object>();
|
|
||||||
|
|
||||||
street.Should().BeEquivalentTo(responseContent);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task POST_Existing_Street_Should_Return_Conflict()
|
|
||||||
{
|
|
||||||
var street = new
|
|
||||||
{
|
|
||||||
name = "Adenauerring",
|
|
||||||
capacity = 100,
|
|
||||||
geometry = new[] { new { x = 10, y = 20 } }
|
|
||||||
};
|
|
||||||
|
|
||||||
await client.PostAsJsonAsync("/api/streets/", street);
|
|
||||||
|
|
||||||
var duplicate_street = new
|
|
||||||
{
|
|
||||||
name = "Adenauerring",
|
|
||||||
capacity = 200,
|
|
||||||
geometry = new[] { new { x = 30, y = 40 } }
|
|
||||||
};
|
|
||||||
|
|
||||||
var response = await client.PostAsJsonAsync("/api/streets", duplicate_street);
|
|
||||||
|
|
||||||
response.StatusCode.Should().Be(System.Net.HttpStatusCode.Conflict);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task DELETE_Non_Existing_Street_Should_Return_NotFound()
|
|
||||||
{
|
|
||||||
var response = await client.GetAsync("/api/streets/Englerstraße");
|
|
||||||
|
|
||||||
response.StatusCode.Should().Be(System.Net.HttpStatusCode.NoContent);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task DELETE_Existing_Street_Should_Delete_Street()
|
|
||||||
{
|
|
||||||
var street = new
|
|
||||||
{
|
|
||||||
name = "Moltkestraße",
|
|
||||||
capacity = 100,
|
|
||||||
geometry = new[] { new { x = 10, y = 20 } }
|
|
||||||
};
|
|
||||||
|
|
||||||
await client.PostAsJsonAsync("/api/streets/", street);
|
|
||||||
|
|
||||||
var delete_response = await client.DeleteAsync("/api/streets/Moltkestraße");
|
|
||||||
|
|
||||||
delete_response.StatusCode.Should().Be(System.Net.HttpStatusCode.NoContent);
|
|
||||||
|
|
||||||
var get_response = await client.GetAsync("/api/streets/Moltkestraße");
|
|
||||||
|
|
||||||
get_response.StatusCode.Should().Be(System.Net.HttpStatusCode.NotFound);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task PATCH_Non_Existing_Street_Should_Return_NotFound()
|
|
||||||
{
|
|
||||||
var update = new
|
|
||||||
{
|
|
||||||
Point = new { x = 10, y = 40 }
|
|
||||||
};
|
|
||||||
|
|
||||||
var response = await client.PatchAsJsonAsync("/api/streets/Ludwig-Erhard-Allee", update);
|
|
||||||
|
|
||||||
response.StatusCode.Should().Be(System.Net.HttpStatusCode.NotFound);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task PATCH_Existing_Street_Should_Update_Street()
|
|
||||||
{
|
|
||||||
var street = new
|
|
||||||
{
|
|
||||||
name = "Ettlingerstraße",
|
|
||||||
capacity = 50,
|
|
||||||
geometry = new[] { new { x = 5, y = 3 } }
|
|
||||||
};
|
|
||||||
|
|
||||||
await client.PostAsJsonAsync("/api/streets/", street);
|
|
||||||
|
|
||||||
var update_no_method = new
|
|
||||||
{
|
|
||||||
Point = new { x = 15, y = 30 }
|
|
||||||
};
|
|
||||||
|
|
||||||
var patch_response_no_method = await client.PatchAsJsonAsync("/api/streets/Ettlingerstraße", update_no_method);
|
|
||||||
|
|
||||||
patch_response_no_method.StatusCode.Should().Be(System.Net.HttpStatusCode.OK);
|
|
||||||
|
|
||||||
var get_response_no_method = await client.GetAsync("/api/streets/Ettlingerstraße");
|
|
||||||
|
|
||||||
get_response_no_method.StatusCode.Should().Be(System.Net.HttpStatusCode.OK);
|
|
||||||
|
|
||||||
var content_no_method = get_response_no_method.Content.ReadFromJsonAsync<object>();
|
|
||||||
|
|
||||||
var expected_content_no_method = new
|
|
||||||
{
|
|
||||||
name = "Ettlingerstraße",
|
|
||||||
capacity = 50,
|
|
||||||
geometry = new[] { new { x = 5, y = 3 }, new { x = 15, y = 30 } }
|
|
||||||
};
|
|
||||||
|
|
||||||
expected_content_no_method.Should().BeEquivalentTo(content_no_method);
|
|
||||||
|
|
||||||
// Test with "Backend" method
|
|
||||||
var update_backend = new
|
|
||||||
{
|
|
||||||
Point = new { x = 20, y = 35 },
|
|
||||||
Method = "Backend"
|
|
||||||
};
|
|
||||||
|
|
||||||
var patch_response_backend = await client.PatchAsJsonAsync("/api/streets/Ettlingerstraße", update_backend);
|
|
||||||
|
|
||||||
patch_response_backend.StatusCode.Should().Be(System.Net.HttpStatusCode.OK);
|
|
||||||
|
|
||||||
// Test with "PostGIS" method
|
|
||||||
var update_postgis = new
|
|
||||||
{
|
|
||||||
Point = new { x = 25, y = 40 },
|
|
||||||
Method = "PostGIS"
|
|
||||||
};
|
|
||||||
|
|
||||||
var patch_response_postgis = await client.PatchAsJsonAsync("/api/streets/Ettlingerstraße", update_postgis);
|
|
||||||
|
|
||||||
patch_response_postgis.StatusCode.Should().Be(System.Net.HttpStatusCode.OK);
|
|
||||||
|
|
||||||
// Test with "Database" method
|
|
||||||
var update_database = new
|
|
||||||
{
|
|
||||||
Point = new { x = 30, y = 45 },
|
|
||||||
Method = "Database"
|
|
||||||
};
|
|
||||||
|
|
||||||
var patch_response_database = await client.PatchAsJsonAsync("/api/streets/Ettlingerstraße", update_database);
|
|
||||||
|
|
||||||
patch_response_database.StatusCode.Should().Be(System.Net.HttpStatusCode.OK);
|
|
||||||
|
|
||||||
var get_response_final = await client.GetAsync("/api/streets/Ettlingerstraße");
|
|
||||||
|
|
||||||
get_response_final.StatusCode.Should().Be(System.Net.HttpStatusCode.OK);
|
|
||||||
|
|
||||||
var content_final = await get_response_final.Content.ReadFromJsonAsync<object>();
|
|
||||||
var expected_content_final = new
|
|
||||||
{
|
|
||||||
name = "Ettlingerstraße",
|
|
||||||
capacity = 50,
|
|
||||||
geometry = new[]
|
|
||||||
{
|
|
||||||
new { x = 5, y = 3 },
|
|
||||||
new { x = 15, y = 30 },
|
|
||||||
new { x = 20, y = 35 },
|
|
||||||
new { x = 25, y = 40 },
|
|
||||||
new { x = 30, y = 45 }
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
expected_content_final.Should().BeEquivalentTo(content_final);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,41 +0,0 @@
|
|||||||
using Microsoft.AspNetCore.Hosting;
|
|
||||||
using Microsoft.AspNetCore.Mvc.Testing;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using Testcontainers.PostgreSql;
|
|
||||||
|
|
||||||
public class StreetWebApplicationFactory<Program> : WebApplicationFactory<Program>, IAsyncLifetime where Program : class
|
|
||||||
{
|
|
||||||
private readonly PostgreSqlContainer dbTestContainer;
|
|
||||||
|
|
||||||
public StreetWebApplicationFactory()
|
|
||||||
{
|
|
||||||
dbTestContainer = new PostgreSqlBuilder().WithImage("postgis/postgis:17-3.5").Build();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void ConfigureWebHost(IWebHostBuilder builder)
|
|
||||||
{
|
|
||||||
builder.ConfigureServices(services =>
|
|
||||||
{
|
|
||||||
|
|
||||||
// Use the test database
|
|
||||||
services.AddDbContext<StreetDbContext>(options => options.UseNpgsql(dbTestContainer.GetConnectionString()));
|
|
||||||
|
|
||||||
// Ensure the database is created before running the tests
|
|
||||||
using var scope = services.BuildServiceProvider().CreateScope();
|
|
||||||
var db = scope.ServiceProvider.GetRequiredService<StreetDbContext>();
|
|
||||||
db.Database.EnsureCreated();
|
|
||||||
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task InitializeAsync()
|
|
||||||
{
|
|
||||||
await dbTestContainer.StartAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
async Task IAsyncLifetime.DisposeAsync()
|
|
||||||
{
|
|
||||||
await dbTestContainer.DisposeAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,33 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<TargetFramework>net9.0</TargetFramework>
|
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
<IsPackable>false</IsPackable>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="coverlet.collector" Version="6.0.2" />
|
|
||||||
<PackageReference Include="FluentAssertions" Version="8.2.0" />
|
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.3" />
|
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.3" />
|
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
|
||||||
<PackageReference Include="npgsql" Version="9.0.3" />
|
|
||||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
|
|
||||||
<PackageReference Include="Testcontainers" Version="4.3.0" />
|
|
||||||
<PackageReference Include="Testcontainers.PostgreSql" Version="4.3.0" />
|
|
||||||
<PackageReference Include="Testcontainers.Xunit" Version="4.3.0" />
|
|
||||||
<PackageReference Include="xunit" Version="2.9.3" />
|
|
||||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<Using Include="Xunit" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\src\PTV-Street.csproj" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
||||||
Loading…
x
Reference in New Issue
Block a user