Skip to content

Commit 4b5d3c3

Browse files
authored
Add k6 load testing suite and documentation (#14)
* Add k6 load testing suite and documentation - Introduced load testing scripts for various endpoints: - `dashboard-load.js`: Simulates concurrent users accessing the Executive Dashboard. - `department-crud-load.js`: Tests CRUD operations for the Department controller. - `notification-load.js`: Tests the Notifications endpoint for concurrent access. - `task-api-load.js`: Tests the Task API for creation, updates, and retrieval. - `reporting-smoke.js`: Smoke test for reporting endpoints. - Created comprehensive README.md in `Tests/load-tests/` directory detailing: - Installation instructions for k6. - Test file descriptions and usage. - Key metrics to monitor during tests. - Performance targets for various endpoints. - CI integration guidelines. - Added deployment and QA infrastructure documentation: - `DEPLOYMENT.md`: Outlines environment configuration, branch workflow, deployment procedures, and rollback instructions. - `QA-INFRASTRUCTURE-REPORT.md`: Comprehensive report on QA improvements, test coverage metrics, and future recommendations. * ci: format ReportGenerator installation and update commands for clarity * ci: update security workflows to check for private repository status * ci: update workflows to remove private repository condition for security scans * ci: update CodeQL and Gitleaks workflows to improve handling of private repositories and artifact uploads * ci: remove dependency review job to enhance compatibility with private repositories
1 parent 038305f commit 4b5d3c3

50 files changed

Lines changed: 7991 additions & 21 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci-tests-coverage.yml

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,44 @@ jobs:
4343
--configuration Release \
4444
--no-build \
4545
--verbosity normal \
46+
--settings Tests/coverlet.runsettings.xml \
4647
--collect:"XPlat Code Coverage" \
4748
--results-directory TestResults \
48-
--logger "trx;LogFileName=test-results.trx"
49+
--logger "trx;LogFileName=test-results.trx" \
50+
/p:ExcludeByFile="**/Migrations/*.cs%3b**/Areas/Identity/**/*.cs%3b**/*.g.cs%3b**/*.designer.cs"
51+
- name: Install ReportGenerator for HTML coverage
52+
if: always()
53+
run: |
54+
dotnet tool install -g dotnet-reportgenerator-globaltool || dotnet tool update -g dotnet-reportgenerator-globaltool
55+
echo "$HOME/.dotnet/tools" >> $GITHUB_PATH
56+
57+
- name: Generate HTML coverage report
58+
if: always()
59+
run: |
60+
reportgenerator \
61+
-reports:"TestResults/**/coverage.cobertura.xml" \
62+
-targetdir:"TestResults/coverage-html" \
63+
-reporttypes:"Html;HtmlSummary"
64+
4965
5066
- name: Upload test results and coverage
5167
if: always()
5268
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
5369
with:
5470
name: test-results-and-coverage
5571
path: TestResults
72+
73+
- name: Upload HTML coverage report
74+
if: always()
75+
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
76+
with:
77+
name: coverage-html-report
78+
path: TestResults/coverage-html
79+
80+
- name: Generate test & coverage summary
81+
if: always()
82+
run: |
83+
echo "### Test Results" >> $GITHUB_STEP_SUMMARY
84+
echo "- **Framework**: xUnit (FluentAssertions, Moq)" >> $GITHUB_STEP_SUMMARY
85+
echo "- **Coverage Tool**: Coverlet" >> $GITHUB_STEP_SUMMARY
86+
echo "- **Excluded**: Migrations, Identity scaffolding, generated code" >> $GITHUB_STEP_SUMMARY

.github/workflows/ci.yml

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
name: CI – Build & Test
2+
3+
on:
4+
push:
5+
branches: [main, dev]
6+
pull_request:
7+
branches: [main, dev]
8+
9+
permissions:
10+
contents: read
11+
12+
jobs:
13+
build-and-test:
14+
runs-on: ubuntu-latest
15+
timeout-minutes: 20
16+
17+
steps:
18+
- name: Checkout
19+
uses: actions/checkout@v4
20+
21+
- name: Setup .NET 8
22+
uses: actions/setup-dotnet@v4
23+
with:
24+
dotnet-version: 8.0.x
25+
26+
- name: Cache NuGet packages
27+
uses: actions/cache@v4
28+
with:
29+
path: ~/.nuget/packages
30+
key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj') }}
31+
restore-keys: ${{ runner.os }}-nuget-
32+
33+
- name: Restore
34+
run: dotnet restore CompanyManagementSystem.sln
35+
36+
- name: Build
37+
run: dotnet build CompanyManagementSystem.sln --no-restore --configuration Release
38+
39+
- name: Test with coverage
40+
run: |
41+
dotnet test Tests/Tests.csproj \
42+
--no-build \
43+
--configuration Release \
44+
--logger "trx;LogFileName=test-results.trx" \
45+
--settings Tests/coverlet.runsettings.xml \
46+
--collect:"XPlat Code Coverage" \
47+
--results-directory ./TestResults \
48+
/p:ExcludeByFile="**/Migrations/*.cs%3b**/Areas/Identity/**/*.cs%3b**/*.g.cs%3b**/*.designer.cs"
49+
50+
- name: Upload test results
51+
if: always()
52+
uses: actions/upload-artifact@v4
53+
with:
54+
name: test-results
55+
path: ./TestResults/**/*.trx
56+
57+
- name: Upload coverage report
58+
if: always()
59+
uses: actions/upload-artifact@v4
60+
with:
61+
name: coverage-report
62+
path: ./TestResults/**/coverage.cobertura.xml

.github/workflows/codeql-analysis.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ permissions:
1717
jobs:
1818
analyze:
1919
name: Analyze C# Code
20+
# Removed: if: github.event.repository.private == false
21+
# That condition caused the job to be permanently skipped on private repos.
2022
runs-on: ubuntu-latest
2123
timeout-minutes: 30
2224

@@ -54,3 +56,11 @@ jobs:
5456
uses: github/codeql-action/analyze@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6
5557
with:
5658
category: "/language:${{ matrix.language }}"
59+
# FIX: Disable SARIF upload to GitHub's code-scanning API.
60+
# This repo is private without GitHub Advanced Security, so the upload
61+
# endpoint returns "Code scanning is not enabled" and fails the job.
62+
# The scan itself completes successfully — disabling upload lets the
63+
# job pass while preserving full analysis coverage.
64+
# To re-enable: remove this line and enable Code Scanning at
65+
# Settings → Code security → Code scanning (requires GHAS).
66+
upload: false

.github/workflows/gitleaks.yml

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@ on:
1111

1212
permissions:
1313
contents: read
14+
# FIX: gitleaks-action v2 calls GET /repos/{owner}/{repo}/pulls/{n}/commits
15+
# on pull_request events to determine which commits to scan. Without this
16+
# permission the API returns HTTP 403 "Resource not accessible by integration"
17+
# crashing the action before any scan runs.
18+
pull-requests: read
1419

1520
jobs:
1621
gitleaks:
@@ -23,14 +28,30 @@ jobs:
2328
fetch-depth: 0
2429

2530
- name: Run Gitleaks
31+
id: gitleaks
2632
uses: gitleaks/gitleaks-action@ff98106e4c7b2bc287b24eaf42907196329070c7 # v2.3.9
2733
env:
2834
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
35+
# GITLEAKS_CONFIG points to your custom rules file at repo root.
2936
GITLEAKS_CONFIG: .gitleaks.toml
37+
# FIX: gitleaks-action v2 auto-generates a SARIF file internally and
38+
# handles its own artifact upload when GITLEAKS_ENABLE_UPLOAD_ARTIFACT=true
39+
# (the default). The `with.args` parameter does not exist in this action
40+
# and was silently ignored — that is why results.sarif was never written
41+
# to disk, causing the upload step to fail with "file not found".
42+
# Uploading SARIF to the code-scanning API also requires GitHub Advanced
43+
# Security (GHAS), which is not available on private free-plan repos.
44+
# We therefore disable the built-in artifact upload and instead capture
45+
# the report ourselves below using actions/upload-artifact (no GHAS needed).
46+
GITLEAKS_ENABLE_UPLOAD_ARTIFACT: false
3047

31-
- name: Upload Gitleaks SARIF report
32-
if: always()
33-
uses: github/codeql-action/upload-sarif@v4
48+
- name: Upload Gitleaks report as workflow artifact
49+
# Run even when gitleaks finds secrets (exit code 1) so the report is
50+
# always available for review — but skip when the step was cancelled.
51+
if: always() && steps.gitleaks.outcome != 'cancelled'
52+
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
3453
with:
35-
sarif_file: results.sarif
36-
continue-on-error: true
54+
name: gitleaks-report
55+
# gitleaks-action v2 writes the SARIF to this fixed path
56+
path: results.sarif
57+
if-no-files-found: ignore

.github/workflows/security-scans.yml

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,12 @@ permissions:
1212
contents: read
1313

1414
jobs:
15-
dependency-review:
16-
name: Dependency Review
17-
if: github.event_name == 'pull_request'
18-
runs-on: ubuntu-latest
19-
timeout-minutes: 10
20-
steps:
21-
- name: Checkout
22-
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
23-
- name: Dependency Review
24-
uses: actions/dependency-review-action@2031cfc080254a8a887f58cffee85186f0e49e48 # v4.9.0
25-
with:
26-
fail-on-severity: moderate
15+
# NOTE: actions/dependency-review-action has been intentionally removed.
16+
# It hard-requires GitHub Advanced Security (GHAS) on private repositories —
17+
# the Dependency Graph being enabled is necessary but not sufficient.
18+
# GHAS is only available on GitHub Enterprise plans.
19+
# Equivalent coverage is provided by dotnet-vulnerability-scan below,
20+
# which uses `dotnet list --vulnerable` and needs no special GitHub features.
2721

2822
dotnet-vulnerability-scan:
2923
name: .NET Vulnerability Audit
@@ -71,4 +65,4 @@ jobs:
7165
echo "::warning::Deprecated NuGet packages detected. Consider updating."
7266
else
7367
echo "No deprecated packages found."
74-
fi
68+
fi

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
# Visual Studio / .NET
33
# =========================
44
.vs/
5+
.vscode/
56
bin/
67
obj/
78
[Bb]uild/

ERP.PL/ERP.PL.csproj

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
1-
<Project Sdk="Microsoft.NET.Sdk.Web">
1+
<Project Sdk="Microsoft.NET.Sdk.Web">
22

33
<PropertyGroup>
44
<TargetFramework>net8.0</TargetFramework>
55
<Nullable>enable</Nullable>
66
<ImplicitUsings>enable</ImplicitUsings>
7+
<UserSecretsId>0b0d445a-714e-4a6e-8191-649508c06636</UserSecretsId>
78
</PropertyGroup>
89

910
<ItemGroup>
10-
<PackageReference Include="AutoMapper" Version="16.1.0" />
11+
<PackageReference Include="AutoMapper" Version="16.1.1" />
1112
<PackageReference Include="Microsoft.AspNetCore.Identity.UI" Version="8.0.24" />
1213
<PackageReference Include="Microsoft.Data.SqlClient" Version="6.1.4" />
1314
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.24">

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66
[![EF Core](https://img.shields.io/badge/EF%20Core-8.0-512BD4)](https://learn.microsoft.com/en-us/ef/core/)
77
[![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
88
[![Live Demo](https://img.shields.io/badge/Live%20Demo-companyflow.runasp.net-brightgreen?logo=microsoftazure)](https://companyflow.runasp.net/)
9+
[![Tests](https://img.shields.io/badge/tests-400+-brightgreen?logo=xunit)](Tests)
10+
[![Coverage](https://img.shields.io/badge/coverage-~70%25-yellowgreen)]()
11+
[![Unit Tests](https://img.shields.io/badge/unit%20tests-389%2B-blue)]()
912

1013
> **Live Demo:** [https://companyflow.runasp.net](https://companyflow.runasp.net/)
1114

Tests.UI/DepartmentTests.cs

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
using Microsoft.Playwright;
2+
using NUnit.Framework;
3+
using FluentAssertions;
4+
5+
namespace Tests.UI;
6+
7+
/// <summary>
8+
/// End-to-end tests for the Departments module.
9+
/// Logs in as CEO, creates a department, verifies it appears in the table,
10+
/// then edits and deletes it.
11+
/// </summary>
12+
[TestFixture]
13+
[Category("UI")]
14+
public class DepartmentTests : PlaywrightFixture
15+
{
16+
private static readonly string CeoEmail = Environment.GetEnvironmentVariable("CEO_EMAIL") ?? "ceo@companyflow.com";
17+
private static readonly string CeoPassword = Environment.GetEnvironmentVariable("CEO_PASSWORD") ?? "Admin@123456!";
18+
19+
[SetUp]
20+
public async Task SetUp()
21+
{
22+
await LoginAsAsync(Page, CeoEmail, CeoPassword);
23+
}
24+
25+
// ── Navigate to Departments ──
26+
27+
[Test]
28+
public async Task Department_Index_PageLoads()
29+
{
30+
await Page.GotoAsync($"{BaseUrl}/Department");
31+
await Page.WaitForLoadStateAsync();
32+
33+
var title = await Page.TitleAsync();
34+
title.Should().NotBeNullOrEmpty();
35+
// Verify we are on departments page (not redirected to login)
36+
Page.Url.Should().Contain("/Department");
37+
}
38+
39+
// ── Create Department ──
40+
41+
[Test]
42+
public async Task Department_Create_ValidData_AppearsInList()
43+
{
44+
var uniqueCode = $"TEST_{DateTime.Now:mmss}";
45+
var deptName = $"UI Test Dept {DateTime.Now:mmss}";
46+
47+
await Page.GotoAsync($"{BaseUrl}/Department/Create");
48+
await Page.WaitForLoadStateAsync();
49+
50+
// Fill form
51+
await Page.FillAsync("input[name='DepartmentCode']", uniqueCode);
52+
await Page.FillAsync("input[name='DepartmentName']", deptName);
53+
await Page.ClickAsync("button[type='submit']");
54+
55+
// Should redirect to index
56+
await Page.WaitForURLAsync(url => url.Contains("/Department") && !url.Contains("/Create"),
57+
new PageWaitForURLOptions { Timeout = 10_000 });
58+
59+
// Verify the new department appears in the list
60+
var pageContent = await Page.ContentAsync();
61+
pageContent.Should().Contain(deptName);
62+
}
63+
64+
// ── Create with duplicate code returns validation error ──
65+
66+
[Test]
67+
public async Task Department_Create_DuplicateCode_ShowsError()
68+
{
69+
await Page.GotoAsync($"{BaseUrl}/Department/Create");
70+
await Page.WaitForLoadStateAsync();
71+
72+
// Use a code that is very likely to already exist or use an obvious test one
73+
await Page.FillAsync("input[name='DepartmentCode']", "INVALID CODE!@#");
74+
await Page.FillAsync("input[name='DepartmentName']", "Test Department");
75+
await Page.ClickAsync("button[type='submit']");
76+
77+
// Should remain on create page when validation fails
78+
await Page.WaitForLoadStateAsync();
79+
// Either stays on create page or shows error
80+
var isOnPage = Page.Url.Contains("/Department");
81+
isOnPage.Should().BeTrue();
82+
}
83+
}

0 commit comments

Comments
 (0)