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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .config/dotnet-tools.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"version": 1,
"isRoot": true,
"tools": {
"dotnet-ef": {
"version": "10.0.10",
"commands": [
"dotnet-ef"
],
"rollForward": false
}
}
}
32 changes: 32 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
name: CI

on:
push:
branches: [master]
pull_request:
branches: [master]

permissions:
contents: read

jobs:
build-and-test:
runs-on: ubuntu-latest

steps:
- name: Check out repository
uses: actions/checkout@v4

- name: Set up .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 10.0.x

- name: Restore
run: dotnet restore PaletteCoreAPI.sln

- name: Build
run: dotnet build PaletteCoreAPI.sln --configuration Release --no-restore

- name: Test
run: dotnet test PaletteCoreAPI.sln --configuration Release --no-build
54 changes: 54 additions & 0 deletions PaletteCoreAPI.Tests/ApiSecurityTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
using System.Net;
using PaletteCoreAPI.Contracts;

namespace PaletteCoreAPI.Tests;

public sealed class ApiSecurityTests : IClassFixture<PaletteApiFactory>
{
private readonly PaletteApiFactory _factory;

public ApiSecurityTests(PaletteApiFactory factory)
{
_factory = factory;
}

[Fact]
public async Task Health_endpoint_is_public()
{
using var client = _factory.CreateClient(new()
{
BaseAddress = new Uri("https://localhost")
});

var response = await client.GetAsync("/health");

Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}

[Theory]
[InlineData("/api/posts")]
[InlineData("/api/users")]
[InlineData("/api/apikeys")]
public async Task Domain_endpoints_require_authentication(string path)
{
using var client = _factory.CreateClient(new()
{
BaseAddress = new Uri("https://localhost")
});

var response = await client.GetAsync(path);

Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
}

[Fact]
public void Public_contracts_do_not_expose_passwords_or_stored_api_keys()
{
var userProperties = typeof(UserDto).GetProperties().Select(item => item.Name);
var keyProperties = typeof(ApiKeyDto).GetProperties().Select(item => item.Name);

Assert.DoesNotContain("Password", userProperties);
Assert.DoesNotContain("UserKey", keyProperties);
Assert.DoesNotContain("ApiKey", keyProperties);
}
}
1 change: 1 addition & 0 deletions PaletteCoreAPI.Tests/GlobalUsings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
global using Xunit;
35 changes: 35 additions & 0 deletions PaletteCoreAPI.Tests/PaletteApiFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using PaletteCoreAPI.Models;

namespace PaletteCoreAPI.Tests;

public sealed class PaletteApiFactory : WebApplicationFactory<Program>
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.UseEnvironment("Testing");
builder.ConfigureAppConfiguration((_, configuration) =>
{
configuration.AddInMemoryCollection(new Dictionary<string, string?>
{
["ConnectionStrings:Database"] =
"Server=localhost;Database=PaletteTests;Integrated Security=True;",
["Authentication:Audience"] = "palette-api",
["Authentication:Authority"] = "https://identity.example.test"
});
});

builder.ConfigureServices(services =>
{
services.RemoveAll<DbContextOptions<PaletteContext>>();
services.RemoveAll<PaletteContext>();
services.AddDbContext<PaletteContext>(options =>
options.UseInMemoryDatabase($"palette-tests-{Guid.NewGuid()}"));
});
}
}
30 changes: 30 additions & 0 deletions PaletteCoreAPI.Tests/PaletteCoreAPI.Tests.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="coverlet.collector" Version="10.0.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.10" />
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="10.0.10" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.8.1" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\PaletteCoreAPI\PaletteCoreAPI.csproj" />
</ItemGroup>

</Project>
6 changes: 6 additions & 0 deletions PaletteCoreAPI.sln
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ VisualStudioVersion = 16.0.30523.141
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PaletteCoreAPI", "PaletteCoreAPI\PaletteCoreAPI.csproj", "{70B0EE8F-B4B8-40C1-B5F6-2300523FECCE}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PaletteCoreAPI.Tests", "PaletteCoreAPI.Tests\PaletteCoreAPI.Tests.csproj", "{B045F530-D77D-4F82-8A25-18BB81369F22}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -15,6 +17,10 @@ Global
{70B0EE8F-B4B8-40C1-B5F6-2300523FECCE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{70B0EE8F-B4B8-40C1-B5F6-2300523FECCE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{70B0EE8F-B4B8-40C1-B5F6-2300523FECCE}.Release|Any CPU.Build.0 = Release|Any CPU
{B045F530-D77D-4F82-8A25-18BB81369F22}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B045F530-D77D-4F82-8A25-18BB81369F22}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B045F530-D77D-4F82-8A25-18BB81369F22}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B045F530-D77D-4F82-8A25-18BB81369F22}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
123 changes: 123 additions & 0 deletions PaletteCoreAPI/Contracts/ResourceDtos.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
using System.ComponentModel.DataAnnotations;

namespace PaletteCoreAPI.Contracts;

public sealed record ApiKeyDto(
int ApiKeyId,
int UserId,
string Role,
DateTime CreatedAt);

public sealed record ApiKeyCreateDto(
[property: Range(1, int.MaxValue)] int UserId,
[property: Required, MaxLength(50)] string Role);

public sealed record ApiKeyCreatedDto(
int ApiKeyId,
int UserId,
string Role,
DateTime CreatedAt,
Guid ApiKey);

public sealed record CommentDto(
int CommentId,
int CommenterId,
int PostId,
string Text,
DateTime CommentedAt);

public sealed record CommentWriteDto(
[property: Range(1, int.MaxValue)] int CommenterId,
[property: Range(1, int.MaxValue)] int PostId,
[property: Required, MaxLength(50)] string Text);

public sealed record FavouriteDto(
int FavouriteId,
int PostId,
int UserId,
DateTime CreatedAt);

public sealed record FavouriteWriteDto(
[property: Range(1, int.MaxValue)] int PostId,
[property: Range(1, int.MaxValue)] int UserId);

public sealed record FollowingDto(
int FollowingRelationshipId,
int UserId,
int FollowingId,
DateTime FollowedAt);

public sealed record FollowingWriteDto(
[property: Range(1, int.MaxValue)] int UserId,
[property: Range(1, int.MaxValue)] int FollowingId);

public sealed record PostColorDto(
int PostColorId,
int PostId,
int ColorHex1,
int ColorHex2,
int ColorHex3);

public sealed record PostColorWriteDto(
[property: Range(1, int.MaxValue)] int PostId,
int ColorHex1,
int ColorHex2,
int ColorHex3);

public sealed record PostTagDto(
int PostTagId,
int PostId,
int TagId);

public sealed record PostTagWriteDto(
[property: Range(1, int.MaxValue)] int PostId,
[property: Range(1, int.MaxValue)] int TagId);

public sealed record PostDto(
int PostId,
int UserId,
string Title,
string Contents,
DateTime PublishedAt);

public sealed record PostWriteDto(
[property: Range(1, int.MaxValue)] int UserId,
[property: Required, MaxLength(50)] string Title,
[property: Required, MaxLength(50)] string Contents);

public sealed record TagDto(
int TagId,
string Name);

public sealed record TagWriteDto(
[property: Required, MaxLength(50)] string Name);

public sealed record UserProfileDto(
int UserProfileId,
int UserId,
string FirstName,
string LastName,
string Email,
string About,
bool HasProfilePhoto);

public sealed record UserProfileWriteDto(
[property: Range(1, int.MaxValue)] int UserId,
[property: Required, MaxLength(50)] string FirstName,
[property: Required, MaxLength(50)] string LastName,
[property: Required, EmailAddress, MaxLength(50)] string Email,
[property: Required, MaxLength(150)] string About,
byte[]? ProfilePhoto);

public sealed record UserDto(
int UserId,
string Username,
DateTime RegisteredAt);

public sealed record UserCreateDto(
[property: Required, MaxLength(50)] string Username,
[property: Required, MinLength(12), MaxLength(128)] string Password);

public sealed record UserUpdateDto(
[property: Required, MaxLength(50)] string Username,
[property: MinLength(12), MaxLength(128)] string? Password);
Loading
Loading