Skip to content

Commit c4bfa47

Browse files
authored
Merge pull request #8 from carterscode/chore/openssf-passing-tier
chore: CONTRIBUTING, xUnit tests, warnings-as-errors (OpenSSF Passing groundwork)
2 parents 14acaaf + ba2815e commit c4bfa47

8 files changed

Lines changed: 349 additions & 1 deletion

File tree

.github/workflows/build.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,6 @@ jobs:
2323

2424
- name: Build (Release)
2525
run: dotnet build GamerGuardian.sln -c Release --no-restore
26+
27+
- name: Test
28+
run: dotnet test GamerGuardian.sln -c Release --no-build --verbosity normal

CONTRIBUTING.md

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
# Contributing to GamerGuardian
2+
3+
Thanks for your interest. This document describes how to set up, contribute, and what reviewers look for.
4+
5+
## Quick start for contributors
6+
7+
```powershell
8+
git clone https://github.com/carterscode/GamerGuardian.git
9+
cd GamerGuardian
10+
dotnet build
11+
dotnet test
12+
src\GamerGuardian\bin\Debug\net8.0-windows10.0.22000.0\GamerGuardian.exe --show-settings
13+
```
14+
15+
For the installer build and CI workflow details, see [docs/wiki/Build-from-source.md](https://github.com/carterscode/GamerGuardian/blob/main/docs/wiki/Build-from-source.md).
16+
17+
## Branching and pull requests
18+
19+
`main` is protected — you cannot push to it directly. The flow:
20+
21+
1. Branch off `main`: `git checkout -b feat/something-descriptive`. Use `feat/`, `fix/`, `chore/`, `ci/`, `docs/` prefixes.
22+
2. Commit your changes with descriptive messages (see *Commit messages* below).
23+
3. Push the branch: `git push -u origin feat/something-descriptive`.
24+
4. Open a pull request: `gh pr create --base main`.
25+
5. CI runs automatically — `build`, `Analyze (csharp)`, `Analyze (actions)`. All three must pass before merge.
26+
6. Self-merge once green: `gh pr merge --merge --delete-branch` (no required approvals for the solo-dev workflow).
27+
28+
## Commit messages
29+
30+
Conventional Commits format. The first line is `<type>: <imperative summary>` under 72 chars.
31+
32+
Common types:
33+
- `feat:` — new functionality (new monitor, UI feature, CLI flag)
34+
- `fix:` — bug fix
35+
- `chore:` — maintenance, refactors with no behavior change
36+
- `ci:` — workflow / build pipeline changes
37+
- `docs:` — wiki, README, comments
38+
- `perf:` — performance improvements
39+
- `ui:` — UI/UX changes
40+
41+
Multi-line bodies are encouraged for non-trivial changes — explain *why*, not *what*. Example:
42+
43+
```
44+
fix(services): stop UAC spam when Windows reverts a service change
45+
46+
Symptom: enabling auto-apply on a service Windows refuses to actually
47+
disable (DoSvc / Delivery Optimization is the trigger case) caused a
48+
UAC prompt every 30 s forever.
49+
50+
MonitorService now backs off auto-apply for a setting whose verify
51+
failed for 15 minutes. Drift still surfaces as a notification.
52+
```
53+
54+
## Code style
55+
56+
- Follow the existing patterns. The codebase is small and consistent.
57+
- `<Nullable>enable</Nullable>` is on. Don't introduce `?` types if you can avoid them.
58+
- `<TreatWarningsAsErrors>true</TreatWarningsAsErrors>` is on. Build warnings break CI.
59+
- Default to no comments. Only comment the *why* when the *what* is obvious from the code. See examples in `Monitors/HagsMonitor.cs` for the conventional level of commenting.
60+
- C# expression-bodied members and pattern-matching are encouraged where they read naturally.
61+
- Don't introduce abstractions speculatively. Three similar lines is better than a premature framework.
62+
63+
## Adding a new monitor
64+
65+
The canonical example is `src/GamerGuardian/Monitors/HagsMonitor.cs` — about 30 lines.
66+
67+
A new monitor needs:
68+
69+
1. A class implementing `IMonitoredSetting` in `src/GamerGuardian/Monitors/`.
70+
2. Registration in `App.xaml.cs` in the `_allMonitors` array.
71+
3. A row in `SettingsWindow.xaml.cs` `LoadGlobals` (or the equivalent for your tab).
72+
4. A `MechanismFor` and `VerifyCommandFor` entry in `src/GamerGuardian/Services/SettingDocs.cs`.
73+
5. **A test** in `tests/GamerGuardian.Tests/` (see *Tests* below).
74+
75+
If the new monitor writes to `HKLM`, route it through `ElevatedRegistry` so it shares the existing UAC-prompt behavior.
76+
77+
## Adding a new Windows service to the catalog
78+
79+
For the `Windows services` tab, just append to `ServiceCatalog.All` in `src/GamerGuardian/Services/ServiceCatalog.cs`. No code change required elsewhere — `WindowsServiceMonitor` is registered once per catalog entry by `App.xaml.cs`.
80+
81+
If the service is one Windows actively protects (re-enables via `WaaSMedicSvc` etc.), set `RecommendedTarget: ServiceTargetState.Manual` rather than `Disabled`, or omit it from the catalog entirely. See `docs/wiki/Architecture-rationale.md` for the WU-protection background.
82+
83+
## Tests
84+
85+
We use xUnit. The test project lives at `tests/GamerGuardian.Tests/`.
86+
87+
Run all tests:
88+
89+
```powershell
90+
dotnet test
91+
```
92+
93+
CI runs the same on every PR.
94+
95+
### Test policy
96+
97+
When you add or change behavior:
98+
99+
- **Pure logic** (catalogs, mappings, parsers, lookup tables) — add a unit test covering the new behavior.
100+
- **Native API wrappers** (anything in `Native/` or `WindowsServiceController`) — add a "doesn't throw on bad input" test if practical. Full coverage isn't expected since these wrap Windows APIs that aren't easily mockable.
101+
- **UI** — manual verification on a dev-build artifact is the current standard. UI test automation is on the roadmap.
102+
- **Bug fixes** — add a regression test if the bug is reproducible from a unit test.
103+
104+
The general rule: it's fine to merge without a test if the change can't be reasonably unit-tested (a UI tweak, a workflow change, a doc update). It's not fine to merge without a test if the change touches a class that *is* unit-tested already.
105+
106+
## Reporting issues and requesting features
107+
108+
- **Bug reports / feature requests:** [GitHub Issues](https://github.com/carterscode/GamerGuardian/issues). Search first; include `--test` output and your `changes.log` if relevant.
109+
- **Security vulnerabilities:** see [SECURITY.md](SECURITY.md). **Do not** open a public issue.
110+
- **Questions:** also fine in Issues; tag with `question`.
111+
112+
## What reviewers look for
113+
114+
- The change is scoped to one concern.
115+
- New behavior has a test if reasonably testable.
116+
- No new compiler warnings.
117+
- Commit messages explain *why*.
118+
- No secrets in the diff (GitHub push protection will catch most, but double-check).
119+
- Touched files have consistent style with the surrounding code.
120+
- For new dependencies: justified, well-maintained, license-compatible (MIT-friendly).
121+
122+
## License
123+
124+
By contributing you agree your contributions are licensed under the [MIT License](LICENSE), the same license the project uses.

GamerGuardian.sln

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
1-
Microsoft Visual Studio Solution File, Format Version 12.00
1+
Microsoft Visual Studio Solution File, Format Version 12.00
22
# Visual Studio Version 17
33
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GamerGuardian", "src\GamerGuardian\GamerGuardian.csproj", "{8A1E0001-0001-0001-0001-000000000001}"
44
EndProject
5+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{6C24C775-257A-472D-A077-0A1AE0BEB2EB}"
6+
EndProject
7+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GamerGuardian.Tests", "tests\GamerGuardian.Tests\GamerGuardian.Tests.csproj", "{7C117441-4E51-4330-AECC-C2343F5C2CC3}"
8+
EndProject
59
Global
610
GlobalSection(SolutionConfigurationPlatforms) = preSolution
711
Debug|Any CPU = Debug|Any CPU
@@ -12,5 +16,12 @@ Global
1216
{8A1E0001-0001-0001-0001-000000000001}.Debug|Any CPU.Build.0 = Debug|Any CPU
1317
{8A1E0001-0001-0001-0001-000000000001}.Release|Any CPU.ActiveCfg = Release|Any CPU
1418
{8A1E0001-0001-0001-0001-000000000001}.Release|Any CPU.Build.0 = Release|Any CPU
19+
{7C117441-4E51-4330-AECC-C2343F5C2CC3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
20+
{7C117441-4E51-4330-AECC-C2343F5C2CC3}.Debug|Any CPU.Build.0 = Debug|Any CPU
21+
{7C117441-4E51-4330-AECC-C2343F5C2CC3}.Release|Any CPU.ActiveCfg = Release|Any CPU
22+
{7C117441-4E51-4330-AECC-C2343F5C2CC3}.Release|Any CPU.Build.0 = Release|Any CPU
23+
EndGlobalSection
24+
GlobalSection(NestedProjects) = preSolution
25+
{7C117441-4E51-4330-AECC-C2343F5C2CC3} = {6C24C775-257A-472D-A077-0A1AE0BEB2EB}
1526
EndGlobalSection
1627
EndGlobal

src/GamerGuardian/GamerGuardian.csproj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@
2727
same commit produces byte-identical output across runs. -->
2828
<Deterministic>true</Deterministic>
2929
<DebugType>portable</DebugType>
30+
31+
<!-- Warnings break the build. Keeps regressions from sneaking in. -->
32+
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
3033
</PropertyGroup>
3134

3235
<PropertyGroup Condition="'$(GITHUB_ACTIONS)' == 'true'">
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
<PropertyGroup>
3+
<TargetFramework>net8.0-windows10.0.22000.0</TargetFramework>
4+
<Nullable>enable</Nullable>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<UseWPF>true</UseWPF>
7+
<SupportedOSPlatformVersion>10.0.22000.0</SupportedOSPlatformVersion>
8+
<IsPackable>false</IsPackable>
9+
<IsTestProject>true</IsTestProject>
10+
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
11+
</PropertyGroup>
12+
13+
<ItemGroup>
14+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
15+
<PackageReference Include="xunit" Version="2.9.2" />
16+
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
17+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
18+
<PrivateAssets>all</PrivateAssets>
19+
</PackageReference>
20+
</ItemGroup>
21+
22+
<ItemGroup>
23+
<ProjectReference Include="..\..\src\GamerGuardian\GamerGuardian.csproj" />
24+
</ItemGroup>
25+
</Project>
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
using GamerGuardian.Models;
2+
using GamerGuardian.Services;
3+
using Xunit;
4+
5+
namespace GamerGuardian.Tests;
6+
7+
public class ServiceCatalogTests
8+
{
9+
[Fact]
10+
public void All_ContainsServices()
11+
{
12+
Assert.NotEmpty(ServiceCatalog.All);
13+
}
14+
15+
[Fact]
16+
public void All_HasNoDuplicateServiceNames()
17+
{
18+
var names = ServiceCatalog.All.Select(d => d.Name).ToList();
19+
Assert.Equal(names.Count, names.Distinct(StringComparer.OrdinalIgnoreCase).Count());
20+
}
21+
22+
[Fact]
23+
public void All_EveryEntryHasNonEmptyDisplayNameAndDescription()
24+
{
25+
foreach (var def in ServiceCatalog.All)
26+
{
27+
Assert.False(string.IsNullOrWhiteSpace(def.Name), $"empty Name for an entry");
28+
Assert.False(string.IsNullOrWhiteSpace(def.DisplayName), $"empty DisplayName for {def.Name}");
29+
Assert.False(string.IsNullOrWhiteSpace(def.Description), $"empty Description for {def.Name}");
30+
}
31+
}
32+
33+
[Fact]
34+
public void All_DefaultStartTypeIsKnown()
35+
{
36+
foreach (var def in ServiceCatalog.All)
37+
{
38+
Assert.NotEqual(ServiceStartType.Unknown, def.DefaultStartType);
39+
}
40+
}
41+
42+
[Fact]
43+
public void RecommendedTarget_NeverDefault()
44+
{
45+
// RecommendedTarget == Default would mean "the preset moves it to where it
46+
// already is" which is meaningless. The convention is: leave RecommendedTarget
47+
// null for services that aren't in the preset, otherwise specify Manual or Disabled.
48+
foreach (var def in ServiceCatalog.All)
49+
{
50+
if (def.RecommendedTarget.HasValue)
51+
{
52+
Assert.NotEqual(ServiceTargetState.Default, def.RecommendedTarget.Value);
53+
}
54+
}
55+
}
56+
57+
[Theory]
58+
[InlineData("DiagTrack")]
59+
[InlineData("MapsBroker")]
60+
[InlineData("Fax")]
61+
[InlineData("Spooler")]
62+
[InlineData("DoSvc")]
63+
[InlineData("iphlpsvc")]
64+
public void All_IncludesExpectedServices(string serviceName)
65+
{
66+
Assert.Contains(ServiceCatalog.All, d =>
67+
d.Name.Equals(serviceName, StringComparison.OrdinalIgnoreCase));
68+
}
69+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
using GamerGuardian.Services;
2+
using Xunit;
3+
4+
namespace GamerGuardian.Tests;
5+
6+
public class SettingDocsTests
7+
{
8+
[Theory]
9+
[InlineData("hags")]
10+
[InlineData("memintegrity")]
11+
[InlineData("gamemode")]
12+
[InlineData("gamedvr")]
13+
[InlineData("mouseaccel")]
14+
[InlineData("fso")]
15+
[InlineData("vrr")]
16+
[InlineData("sysresponse")]
17+
[InlineData("netthrottle")]
18+
[InlineData("usbsuspend")]
19+
[InlineData("gamestask")]
20+
[InlineData("powerplan")]
21+
public void MechanismFor_KnownIds_ReturnsNonEmpty(string id)
22+
{
23+
var mech = SettingDocs.MechanismFor(id);
24+
Assert.False(string.IsNullOrWhiteSpace(mech), $"no Mechanism for {id}");
25+
Assert.NotEqual("(unknown)", mech);
26+
}
27+
28+
[Theory]
29+
[InlineData("hags")]
30+
[InlineData("memintegrity")]
31+
[InlineData("gamemode")]
32+
[InlineData("powerplan")]
33+
public void VerifyCommandFor_KnownIds_ReturnsNonEmpty(string id)
34+
{
35+
var cmd = SettingDocs.VerifyCommandFor(id);
36+
Assert.False(string.IsNullOrWhiteSpace(cmd), $"no Verify command for {id}");
37+
}
38+
39+
[Fact]
40+
public void MechanismFor_DisplayPrefixIds_RecognizesAllThree()
41+
{
42+
Assert.NotEqual("(unknown)", SettingDocs.MechanismFor("hdr:DISPLAY1"));
43+
Assert.NotEqual("(unknown)", SettingDocs.MechanismFor("refresh:DISPLAY1"));
44+
Assert.NotEqual("(unknown)", SettingDocs.MechanismFor("resolution:DISPLAY1"));
45+
}
46+
47+
[Fact]
48+
public void MechanismFor_ServicePrefix_IncludesServiceName()
49+
{
50+
var mech = SettingDocs.MechanismFor("service:diagtrack");
51+
Assert.Contains("diagtrack", mech, StringComparison.OrdinalIgnoreCase);
52+
}
53+
54+
[Fact]
55+
public void VerifyCommandFor_ServicePrefix_IncludesScQc()
56+
{
57+
var cmd = SettingDocs.VerifyCommandFor("service:diagtrack");
58+
Assert.Contains("sc qc", cmd);
59+
Assert.Contains("diagtrack", cmd, StringComparison.OrdinalIgnoreCase);
60+
}
61+
62+
[Fact]
63+
public void MechanismFor_UnknownId_ReturnsUnknownMarker()
64+
{
65+
Assert.Equal("(unknown)", SettingDocs.MechanismFor("definitely_not_a_real_setting"));
66+
}
67+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
using GamerGuardian.Models;
2+
using GamerGuardian.Services;
3+
using Xunit;
4+
5+
namespace GamerGuardian.Tests;
6+
7+
public class WindowsServiceControllerTests
8+
{
9+
private const string DefinitelyNotAService = "GamerGuardianFakeServiceForTests";
10+
11+
[Fact]
12+
public void Exists_NonexistentService_ReturnsFalse()
13+
{
14+
Assert.False(WindowsServiceController.Exists(DefinitelyNotAService));
15+
}
16+
17+
[Fact]
18+
public void ReadStartType_NonexistentService_ReturnsUnknown()
19+
{
20+
// Should not throw; should return Unknown so callers can treat it as "skip".
21+
Assert.Equal(ServiceStartType.Unknown, WindowsServiceController.ReadStartType(DefinitelyNotAService));
22+
}
23+
24+
[Fact]
25+
public void ReadStatus_NonexistentService_ReturnsNull()
26+
{
27+
Assert.Null(WindowsServiceController.ReadStatus(DefinitelyNotAService));
28+
}
29+
30+
// EventLog is a service that exists on every supported Windows install and
31+
// boots automatically. Reading its registry start type should succeed and
32+
// never return Unknown. We don't assert the specific value because Microsoft
33+
// has changed it over time (Auto vs AutoDelayed).
34+
[Fact]
35+
public void ReadStartType_EventLog_ReturnsKnown()
36+
{
37+
var start = WindowsServiceController.ReadStartType("EventLog");
38+
Assert.NotEqual(ServiceStartType.Unknown, start);
39+
}
40+
41+
[Fact]
42+
public void Exists_EventLog_ReturnsTrue()
43+
{
44+
Assert.True(WindowsServiceController.Exists("EventLog"));
45+
}
46+
}

0 commit comments

Comments
 (0)