Skip to content

Commit a4636e6

Browse files
Merge pull request #5 from SculptTechProject/tests/fix/init
Add integration tests and testing infrastructure setup
2 parents e93faef + 2ddfd8b commit a4636e6

File tree

6 files changed

+237
-107
lines changed

6 files changed

+237
-107
lines changed

Program.cs

Lines changed: 114 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -6,124 +6,131 @@
66
using sparkly_server.Services.Auth;
77
using sparkly_server.Services.Users;
88
using sparkly_server.Services.UserServices;
9-
using System.Text;
10-
using Scalar.AspNetCore;
119
using sparkly_server.Services.Projects;
10+
using Scalar.AspNetCore;
11+
using System.Text;
12+
13+
var builder = WebApplication.CreateBuilder(args);
1214

13-
namespace sparkly_server;
15+
// JWT configuration
16+
var jwtKey = builder.Configuration["SPARKLY_JWT_KEY"]
17+
?? Environment.GetEnvironmentVariable("SPARKLY_JWT_KEY");
1418

15-
public class Program
19+
if (string.IsNullOrWhiteSpace(jwtKey))
1620
{
17-
public static void Main(string[] args)
21+
if (builder.Environment.IsDevelopment() || builder.Environment.IsEnvironment("Testing"))
1822
{
19-
var builder = WebApplication.CreateBuilder(args);
20-
21-
var jwtKey = builder.Configuration["SPARKLY_JWT_KEY"]
22-
?? Environment.GetEnvironmentVariable("SPARKLY_JWT_KEY");
23+
// Dev / Testing fallback key only for local usage
24+
jwtKey = "dev-only-jwt-key-change-me";
25+
}
26+
else
27+
{
28+
throw new Exception("JWT key missing");
29+
}
30+
}
2331

24-
if (string.IsNullOrWhiteSpace(jwtKey))
25-
{
26-
if (builder.Environment.IsDevelopment())
27-
{
28-
jwtKey = "dev-only-jwt-key-change-me";
29-
}
30-
else
31-
{
32-
throw new Exception("JWT key missing");
33-
}
34-
}
35-
36-
37-
var jwtIssuer = builder.Configuration["SPARKLY_JWT_ISSUER"]
38-
?? Environment.GetEnvironmentVariable("SPARKLY_JWT_ISSUER")
39-
?? "sparkly";
40-
41-
var jwtAudience = builder.Configuration["SPARKLY_JWT_AUDIENCE"]
42-
?? Environment.GetEnvironmentVariable("SPARKLY_JWT_AUDIENCE")
43-
?? "sparkly-api";
44-
45-
builder.Services.AddHttpContextAccessor();
46-
47-
builder.Services.AddAuthorization(options =>
48-
{
49-
options.AddPolicy("AdminOnly", policy =>
50-
policy.RequireRole(Roles.Admin));
51-
});
52-
53-
// Services
54-
builder.Services.AddScoped<IUserRepository, UserRepository>();
55-
builder.Services.AddScoped<IUserService, UserService>();
56-
builder.Services.AddScoped<IJwtProvider, JwtProvider>();
57-
builder.Services.AddScoped<IAuthService, AuthService>();
58-
builder.Services.AddScoped<ICurrentUser, CurrentUser>();
59-
builder.Services.AddScoped<IProjectRepository, ProjectRepository>();
60-
builder.Services.AddScoped<IProjectService, ProjectService>();
61-
62-
var connectionString = builder.Configuration.GetConnectionString("Default")
63-
?? Environment.GetEnvironmentVariable("ConnectionStrings__Default")
64-
?? throw new Exception("Connection string 'Default' not found.");
65-
66-
builder.Services.AddDbContext<AppDbContext>(options =>
67-
{
68-
options.UseNpgsql(connectionString);
69-
});
32+
var jwtIssuer = builder.Configuration["SPARKLY_JWT_ISSUER"]
33+
?? Environment.GetEnvironmentVariable("SPARKLY_JWT_ISSUER")
34+
?? "sparkly";
7035

71-
builder.Services.AddControllers();
36+
var jwtAudience = builder.Configuration["SPARKLY_JWT_AUDIENCE"]
37+
?? Environment.GetEnvironmentVariable("SPARKLY_JWT_AUDIENCE")
38+
?? "sparkly-api";
7239

73-
builder.Services.AddOpenApi();
74-
75-
builder.Services.AddCors(options =>
76-
{
77-
options.AddPolicy("FrontendDev", policy =>
78-
{
79-
policy
80-
.WithOrigins("http://localhost:4200")
81-
.AllowAnyHeader()
82-
.AllowAnyMethod()
83-
.AllowCredentials();
84-
});
85-
});
86-
87-
builder.Services
88-
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
89-
.AddJwtBearer(options =>
90-
{
91-
options.TokenValidationParameters = new TokenValidationParameters
92-
{
93-
ValidateIssuer = false,
94-
ValidateAudience = false,
95-
ValidateLifetime = true,
96-
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtKey)),
97-
ValidateIssuerSigningKey = true,
98-
ClockSkew = TimeSpan.FromMinutes(1),
99-
};
100-
});
101-
102-
var app = builder.Build();
103-
104-
// Configure the HTTP request pipeline.
105-
if (app.Environment.IsDevelopment())
106-
{
107-
app.MapOpenApi();
108-
109-
app.MapScalarApiReference();
110-
}
40+
// Common services
41+
builder.Services.AddHttpContextAccessor();
42+
43+
builder.Services.AddAuthorization(options =>
44+
{
45+
options.AddPolicy("AdminOnly", policy =>
46+
policy.RequireRole(Roles.Admin));
47+
});
48+
49+
// Domain / app services
50+
builder.Services.AddScoped<IUserRepository, UserRepository>();
51+
builder.Services.AddScoped<IUserService, UserService>();
52+
builder.Services.AddScoped<IJwtProvider, JwtProvider>();
53+
builder.Services.AddScoped<IAuthService, AuthService>();
54+
builder.Services.AddScoped<ICurrentUser, CurrentUser>();
55+
builder.Services.AddScoped<IProjectRepository, ProjectRepository>();
56+
builder.Services.AddScoped<IProjectService, ProjectService>();
57+
58+
// Database
59+
if (!builder.Environment.IsEnvironment("Testing"))
60+
{
61+
var connectionString = builder.Configuration.GetConnectionString("Default")
62+
?? Environment.GetEnvironmentVariable("ConnectionStrings__Default")
63+
?? throw new Exception("Connection string 'Default' not found.");
11164

112-
app.UseHttpsRedirection();
65+
builder.Services.AddDbContext<AppDbContext>(options =>
66+
{
67+
options.UseNpgsql(connectionString);
68+
});
69+
}
11370

114-
app.UseAuthentication();
115-
app.UseAuthorization();
116-
117-
app.UseCors("FrontendDev");
71+
builder.Services.AddControllers();
11872

119-
app.MapControllers();
73+
// OpenAPI / Scalar
74+
builder.Services.AddOpenApi();
12075

121-
using (var scope = app.Services.CreateScope())
76+
// CORS
77+
builder.Services.AddCors(options =>
78+
{
79+
options.AddPolicy("FrontendDev", policy =>
80+
{
81+
policy
82+
.WithOrigins("http://localhost:4200")
83+
.AllowAnyHeader()
84+
.AllowAnyMethod()
85+
.AllowCredentials();
86+
});
87+
});
88+
89+
// Authentication
90+
builder.Services
91+
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
92+
.AddJwtBearer(options =>
93+
{
94+
options.TokenValidationParameters = new TokenValidationParameters
12295
{
123-
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
124-
db.Database.Migrate();
125-
}
126-
127-
app.Run();
128-
}
96+
ValidateIssuer = false,
97+
ValidateAudience = false,
98+
ValidateLifetime = true,
99+
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtKey)),
100+
ValidateIssuerSigningKey = true,
101+
ClockSkew = TimeSpan.FromMinutes(1),
102+
};
103+
});
104+
105+
var app = builder.Build();
106+
107+
// Pipeline
108+
if (app.Environment.IsDevelopment())
109+
{
110+
app.MapOpenApi();
111+
app.MapScalarApiReference();
129112
}
113+
114+
// Run migrations only outside Testing
115+
if (!app.Environment.IsEnvironment("Testing"))
116+
{
117+
using var scope = app.Services.CreateScope();
118+
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
119+
db.Database.Migrate();
120+
}
121+
122+
app.UseHttpsRedirection();
123+
124+
app.UseAuthentication();
125+
app.UseAuthorization();
126+
127+
app.UseCors("FrontendDev");
128+
129+
app.MapControllers();
130+
131+
// Simple health endpoint for tests and monitoring
132+
app.MapGet("/healthz", () => Results.Ok(new { status = "ok" }));
133+
134+
app.Run();
135+
136+
public partial class Program;

sparkly-server.csproj

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,37 @@
66
<ImplicitUsings>enable</ImplicitUsings>
77
<RootNamespace>sparkly_server</RootNamespace>
88
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
9+
<IsTestProject>false</IsTestProject>
10+
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
11+
<GenerateTargetFrameworkAttribute>false</GenerateTargetFrameworkAttribute>
912
</PropertyGroup>
1013

1114
<ItemGroup>
15+
<PackageReference Include="coverlet.collector" Version="6.0.0">
16+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
17+
<PrivateAssets>all</PrivateAssets>
18+
</PackageReference>
1219
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.11" />
20+
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.11" />
1321
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.3" />
1422
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.11" />
1523
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.11">
1624
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
1725
<PrivateAssets>all</PrivateAssets>
1826
</PackageReference>
27+
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="9.0.11" />
1928
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.11" />
2029
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.11">
2130
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
2231
<PrivateAssets>all</PrivateAssets>
2332
</PackageReference>
2433
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
2534
<PackageReference Include="Scalar.AspNetCore" Version="2.10.3" />
35+
<PackageReference Include="xunit" Version="2.9.3" />
36+
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.1">
37+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
38+
<PrivateAssets>all</PrivateAssets>
39+
</PackageReference>
2640
</ItemGroup>
2741

2842
<ItemGroup>

sparkly-server.sln

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,44 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
77
compose.yaml = compose.yaml
88
EndProjectSection
99
EndProject
10+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "sparkly-server.test", "sparkly-server.test\sparkly-server.test.csproj", "{E26AB9F3-8A34-4AB8-A503-F0B851823527}"
11+
EndProject
1012
Global
1113
GlobalSection(SolutionConfigurationPlatforms) = preSolution
1214
Debug|Any CPU = Debug|Any CPU
15+
Debug|x64 = Debug|x64
16+
Debug|x86 = Debug|x86
1317
Release|Any CPU = Release|Any CPU
18+
Release|x64 = Release|x64
19+
Release|x86 = Release|x86
1420
EndGlobalSection
1521
GlobalSection(ProjectConfigurationPlatforms) = postSolution
1622
{B04A64D4-FEE1-4614-8BCD-B3B4C5B8E20A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
1723
{B04A64D4-FEE1-4614-8BCD-B3B4C5B8E20A}.Debug|Any CPU.Build.0 = Debug|Any CPU
24+
{B04A64D4-FEE1-4614-8BCD-B3B4C5B8E20A}.Debug|x64.ActiveCfg = Debug|Any CPU
25+
{B04A64D4-FEE1-4614-8BCD-B3B4C5B8E20A}.Debug|x64.Build.0 = Debug|Any CPU
26+
{B04A64D4-FEE1-4614-8BCD-B3B4C5B8E20A}.Debug|x86.ActiveCfg = Debug|Any CPU
27+
{B04A64D4-FEE1-4614-8BCD-B3B4C5B8E20A}.Debug|x86.Build.0 = Debug|Any CPU
1828
{B04A64D4-FEE1-4614-8BCD-B3B4C5B8E20A}.Release|Any CPU.ActiveCfg = Release|Any CPU
1929
{B04A64D4-FEE1-4614-8BCD-B3B4C5B8E20A}.Release|Any CPU.Build.0 = Release|Any CPU
30+
{B04A64D4-FEE1-4614-8BCD-B3B4C5B8E20A}.Release|x64.ActiveCfg = Release|Any CPU
31+
{B04A64D4-FEE1-4614-8BCD-B3B4C5B8E20A}.Release|x64.Build.0 = Release|Any CPU
32+
{B04A64D4-FEE1-4614-8BCD-B3B4C5B8E20A}.Release|x86.ActiveCfg = Release|Any CPU
33+
{B04A64D4-FEE1-4614-8BCD-B3B4C5B8E20A}.Release|x86.Build.0 = Release|Any CPU
34+
{E26AB9F3-8A34-4AB8-A503-F0B851823527}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
35+
{E26AB9F3-8A34-4AB8-A503-F0B851823527}.Debug|Any CPU.Build.0 = Debug|Any CPU
36+
{E26AB9F3-8A34-4AB8-A503-F0B851823527}.Debug|x64.ActiveCfg = Debug|Any CPU
37+
{E26AB9F3-8A34-4AB8-A503-F0B851823527}.Debug|x64.Build.0 = Debug|Any CPU
38+
{E26AB9F3-8A34-4AB8-A503-F0B851823527}.Debug|x86.ActiveCfg = Debug|Any CPU
39+
{E26AB9F3-8A34-4AB8-A503-F0B851823527}.Debug|x86.Build.0 = Debug|Any CPU
40+
{E26AB9F3-8A34-4AB8-A503-F0B851823527}.Release|Any CPU.ActiveCfg = Release|Any CPU
41+
{E26AB9F3-8A34-4AB8-A503-F0B851823527}.Release|Any CPU.Build.0 = Release|Any CPU
42+
{E26AB9F3-8A34-4AB8-A503-F0B851823527}.Release|x64.ActiveCfg = Release|Any CPU
43+
{E26AB9F3-8A34-4AB8-A503-F0B851823527}.Release|x64.Build.0 = Release|Any CPU
44+
{E26AB9F3-8A34-4AB8-A503-F0B851823527}.Release|x86.ActiveCfg = Release|Any CPU
45+
{E26AB9F3-8A34-4AB8-A503-F0B851823527}.Release|x86.Build.0 = Release|Any CPU
46+
EndGlobalSection
47+
GlobalSection(SolutionProperties) = preSolution
48+
HideSolutionNode = FALSE
2049
EndGlobalSection
2150
EndGlobal

sparkly-server.test/HealthzTest.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
using System.Net;
2+
3+
namespace sparkly_server.test;
4+
5+
public class HealthzTest : IClassFixture<TestWebApplicationFactory>
6+
{
7+
private readonly HttpClient _client;
8+
9+
public HealthzTest(TestWebApplicationFactory factory)
10+
{
11+
_client = factory.CreateClient();
12+
}
13+
14+
[Fact]
15+
public async Task Healthz_ReturnsOk()
16+
{
17+
var response = await _client.GetAsync("/healthz");
18+
19+
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
20+
}
21+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
using Microsoft.AspNetCore.Hosting;
2+
using Microsoft.AspNetCore.Mvc.Testing;
3+
using Microsoft.EntityFrameworkCore;
4+
using Microsoft.Extensions.DependencyInjection;
5+
using sparkly_server.Infrastructure;
6+
7+
namespace sparkly_server.test;
8+
9+
public class TestWebApplicationFactory : WebApplicationFactory<Program>
10+
{
11+
protected override void ConfigureWebHost(IWebHostBuilder builder)
12+
{
13+
builder.UseEnvironment("Testing");
14+
15+
builder.ConfigureServices(services =>
16+
{
17+
var descriptor = services.SingleOrDefault(
18+
d => d.ServiceType == typeof(DbContextOptions<AppDbContext>));
19+
20+
if (descriptor is not null)
21+
{
22+
services.Remove(descriptor);
23+
}
24+
25+
services.AddDbContext<AppDbContext>(options =>
26+
{
27+
options.UseInMemoryDatabase("sparkly-tests");
28+
});
29+
});
30+
}
31+
}

0 commit comments

Comments
 (0)