@torus-engineering/tas-kit 1.11.1 → 1.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.tas/README.md +334 -334
- package/{.claude → .tas/_platform/claude-code}/settings.json +0 -12
- package/{.claude → .tas/_platform}/hooks/code-quality.js +1 -1
- package/{.claude → .tas/_platform}/hooks/session-end.js +20 -25
- package/{.claude → .tas}/commands/ado-create.md +5 -4
- package/{.claude → .tas}/commands/ado-delete.md +5 -4
- package/{.claude → .tas}/commands/ado-update.md +5 -4
- package/{.claude → .tas}/commands/tas-adr.md +3 -3
- package/{.claude → .tas}/commands/tas-apitest-plan.md +2 -2
- package/{.claude → .tas}/commands/tas-apitest.md +4 -4
- package/{.claude → .tas}/commands/tas-bug.md +6 -6
- package/{.claude → .tas}/commands/tas-design.md +3 -3
- package/{.claude → .tas}/commands/tas-dev.md +11 -14
- package/{.claude → .tas}/commands/tas-epic.md +3 -3
- package/{.claude → .tas}/commands/tas-feature.md +4 -4
- package/{.claude → .tas}/commands/tas-fix.md +5 -5
- package/{.claude → .tas}/commands/tas-init.md +1 -1
- package/{.claude → .tas}/commands/tas-plan.md +198 -198
- package/{.claude → .tas}/commands/tas-prd.md +3 -3
- package/{.claude → .tas}/commands/tas-review.md +17 -15
- package/{.claude → .tas}/commands/tas-sad.md +3 -3
- package/{.claude → .tas}/commands/tas-security.md +4 -4
- package/{.claude → .tas}/commands/tas-story.md +3 -3
- package/.tas/platforms.json +5 -0
- package/.tas/project-status-example.yaml +17 -17
- package/{.claude/skills/ado-integration/SKILL.md → .tas/rules/ado-integration.md} +5 -15
- package/{.claude/skills/api-design/SKILL.md → .tas/rules/common/api-design.md} +517 -530
- package/{.claude → .tas}/rules/common/code-review.md +30 -6
- package/{.claude/rules/common/post-review-agent.md → .tas/rules/common/post-implementation-review.md} +51 -49
- package/{.claude → .tas}/rules/common/project-status.md +80 -80
- package/{.claude → .tas}/rules/common/stack-detection.md +29 -29
- package/.tas/{checklists → rules/common}/story-done.md +12 -5
- package/{.claude/skills/tas-tdd/SKILL.md → .tas/rules/common/tdd.md} +4 -38
- package/{.claude → .tas}/rules/common/testing.md +3 -8
- package/{.claude → .tas}/rules/common/token-logging.md +36 -27
- package/{.claude → .tas}/rules/csharp/api-testing.md +171 -171
- package/{.claude → .tas}/rules/csharp/coding-style.md +0 -2
- package/{.claude → .tas}/rules/csharp/security.md +10 -0
- package/{.claude → .tas}/rules/python/coding-style.md +0 -2
- package/{.claude → .tas}/rules/typescript/coding-style.md +0 -2
- package/.tas/rules/typescript/patterns.md +142 -0
- package/.tas/rules/typescript/security.md +88 -0
- package/{.claude → .tas}/rules/typescript/testing.md +0 -4
- package/{.claude → .tas}/rules/web/coding-style.md +0 -2
- package/.tas/tas-example.yaml +125 -126
- package/.tas/templates/ADR.md +47 -47
- package/.tas/templates/Bug.md +67 -67
- package/.tas/templates/Design-Spec.md +36 -36
- package/.tas/templates/Epic.md +46 -46
- package/.tas/templates/Feature.md +1 -1
- package/.tas/templates/Security-Report.md +27 -27
- package/.tas/tools/tas-ado-readme.md +169 -169
- package/.tas/tools/tas-ado.py +621 -621
- package/README.md +334 -334
- package/bin/cli.js +91 -73
- package/lib/adapters/antigravity.js +131 -0
- package/lib/adapters/claude-code.js +35 -0
- package/lib/adapters/codex.js +157 -0
- package/lib/adapters/cursor.js +80 -0
- package/lib/adapters/index.js +20 -0
- package/lib/adapters/utils.js +81 -0
- package/lib/deleted-files.json +99 -0
- package/lib/install.js +543 -327
- package/package.json +5 -4
- package/.claude/agents/code-reviewer.md +0 -41
- package/.claude/agents/e2e-runner.md +0 -61
- package/.claude/agents/planner.md +0 -82
- package/.claude/agents/tdd-guide.md +0 -84
- package/.claude/commands/tas-verify.md +0 -51
- package/.claude/rules/typescript/patterns.md +0 -62
- package/.claude/rules/typescript/security.md +0 -28
- package/.claude/settings.local.json +0 -38
- package/.claude/skills/ai-regression-testing/SKILL.md +0 -364
- package/.claude/skills/architecture-decision-records/SKILL.md +0 -184
- package/.claude/skills/benchmark/SKILL.md +0 -98
- package/.claude/skills/browser-qa/SKILL.md +0 -92
- package/.claude/skills/canary-watch/SKILL.md +0 -104
- package/.claude/skills/js-backend-patterns/SKILL.md +0 -603
- package/.claude/skills/tas-conventions/SKILL.md +0 -65
- package/.claude/skills/tas-implementation-complete/SKILL.md +0 -100
- package/.claude/skills/token-logger/SKILL.md +0 -19
- package/.tas/checklists/code-review.md +0 -29
- package/.tas/checklists/security.md +0 -21
- /package/{.claude → .tas}/agents/architect.md +0 -0
- /package/{.claude → .tas}/agents/aws-reviewer.md +0 -0
- /package/{.claude → .tas}/agents/build-resolver.md +0 -0
- /package/{.claude → .tas}/agents/code-explorer.md +0 -0
- /package/{.claude → .tas}/agents/csharp-reviewer.md +0 -0
- /package/{.claude → .tas}/agents/database-reviewer.md +0 -0
- /package/{.claude → .tas}/agents/doc-updater.md +0 -0
- /package/{.claude → .tas}/agents/python-reviewer.md +0 -0
- /package/{.claude → .tas}/agents/security-reviewer.md +0 -0
- /package/{.claude → .tas}/agents/typescript-reviewer.md +0 -0
- /package/{.claude → .tas}/commands/ado-get.md +0 -0
- /package/{.claude → .tas}/commands/ado-status.md +0 -0
- /package/{.claude → .tas}/commands/tas-brainstorm.md +0 -0
- /package/{.claude → .tas}/commands/tas-e2e-mobile.md +0 -0
- /package/{.claude → .tas}/commands/tas-e2e-web.md +0 -0
- /package/{.claude → .tas}/commands/tas-e2e.md +0 -0
- /package/{.claude → .tas}/commands/tas-functest-mobile.md +0 -0
- /package/{.claude → .tas}/commands/tas-functest-web.md +0 -0
- /package/{.claude → .tas}/commands/tas-functest.md +0 -0
- /package/{.claude → .tas}/commands/tas-spec.md +0 -0
- /package/{.claude → .tas}/commands/tas-status.md +0 -0
- /package/{.claude → .tas}/rules/.gitkeep +0 -0
- /package/{.claude → .tas}/rules/common/hooks.md +0 -0
- /package/{.claude → .tas}/rules/common/patterns.md +0 -0
- /package/{.claude → .tas}/rules/common/security.md +0 -0
- /package/{.claude → .tas}/rules/csharp/hooks.md +0 -0
- /package/{.claude → .tas}/rules/csharp/patterns.md +0 -0
- /package/{.claude → .tas}/rules/csharp/testing.md +0 -0
- /package/{.claude → .tas}/rules/python/hooks.md +0 -0
- /package/{.claude → .tas}/rules/python/patterns.md +0 -0
- /package/{.claude → .tas}/rules/python/security.md +0 -0
- /package/{.claude → .tas}/rules/python/testing.md +0 -0
- /package/{.claude → .tas}/rules/typescript/hooks.md +0 -0
- /package/{.claude → .tas}/rules/web/design-quality.md +0 -0
- /package/{.claude → .tas}/rules/web/hooks.md +0 -0
- /package/{.claude → .tas}/rules/web/patterns.md +0 -0
- /package/{.claude → .tas}/rules/web/performance.md +0 -0
- /package/{.claude → .tas}/rules/web/security.md +0 -0
- /package/{.claude → .tas}/rules/web/testing.md +0 -0
- /package/{CLAUDE-Example.md → .tas/templates/AGENTS.md} +0 -0
|
@@ -1,171 +1,171 @@
|
|
|
1
|
-
---
|
|
2
|
-
paths:
|
|
3
|
-
- "**/*.cs"
|
|
4
|
-
- "**/ApiTests/**"
|
|
5
|
-
- "**/Api.Tests/**"
|
|
6
|
-
---
|
|
7
|
-
|
|
8
|
-
# C# API Automation Testing
|
|
9
|
-
|
|
10
|
-
> Used by `/tas-
|
|
11
|
-
|
|
12
|
-
## Tech Stack
|
|
13
|
-
|
|
14
|
-
| Component | Choice |
|
|
15
|
-
|---|---|
|
|
16
|
-
| Framework | xUnit (default) — match project if already using MSTest/NUnit |
|
|
17
|
-
| Assertions | FluentAssertions |
|
|
18
|
-
| HTTP | System.Net.Http.HttpClient |
|
|
19
|
-
| Config | Microsoft.Extensions.Configuration + JSON/EnvVars |
|
|
20
|
-
|
|
21
|
-
```xml
|
|
22
|
-
<!-- ApiTests.csproj packages -->
|
|
23
|
-
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.*" />
|
|
24
|
-
<PackageReference Include="xunit" Version="2.*" />
|
|
25
|
-
<PackageReference Include="xunit.runner.visualstudio" Version="2.*" />
|
|
26
|
-
<PackageReference Include="FluentAssertions" Version="6.*" />
|
|
27
|
-
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.*" />
|
|
28
|
-
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.*" />
|
|
29
|
-
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="8.*" />
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
## Project Structure
|
|
33
|
-
|
|
34
|
-
```
|
|
35
|
-
tests/ApiTests/
|
|
36
|
-
appsettings.json # base (no real secrets)
|
|
37
|
-
appsettings.Test.json # Test env override
|
|
38
|
-
appsettings.Staging.json # Staging env override
|
|
39
|
-
.gitignore # appsettings.*.local.json
|
|
40
|
-
Shared/
|
|
41
|
-
ApiTestSettings.cs
|
|
42
|
-
TestBase.cs
|
|
43
|
-
v1/
|
|
44
|
-
UsersApiTests.cs
|
|
45
|
-
v2/ # APPEND-ONLY: don't modify v1
|
|
46
|
-
UsersApiTests.cs
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
## Config
|
|
50
|
-
|
|
51
|
-
```json
|
|
52
|
-
// appsettings.json
|
|
53
|
-
{
|
|
54
|
-
"ApiTest": {
|
|
55
|
-
"BaseUrl": "https://localhost:5001",
|
|
56
|
-
"TimeoutSeconds": 30,
|
|
57
|
-
"Auth": { "Type": "Bearer", "TokenEndpoint": "/api/auth/token", "Username": "", "Password": "" }
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
```csharp
|
|
63
|
-
// ApiTestSettings.cs
|
|
64
|
-
public sealed class ApiTestSettings
|
|
65
|
-
{
|
|
66
|
-
public required string BaseUrl { get; init; }
|
|
67
|
-
public int TimeoutSeconds { get; init; } = 30;
|
|
68
|
-
public required AuthSettings Auth { get; init; }
|
|
69
|
-
}
|
|
70
|
-
public sealed class AuthSettings
|
|
71
|
-
{
|
|
72
|
-
public string Type { get; init; } = "Bearer";
|
|
73
|
-
public string TokenEndpoint { get; init; } = "/api/auth/token";
|
|
74
|
-
public string Username { get; init; } = "";
|
|
75
|
-
public string Password { get; init; } = "";
|
|
76
|
-
}
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
```csharp
|
|
80
|
-
// TestBase.cs
|
|
81
|
-
public abstract class TestBase : IAsyncLifetime
|
|
82
|
-
{
|
|
83
|
-
protected HttpClient Client { get; private set; } = null!;
|
|
84
|
-
protected ApiTestSettings Settings { get; private set; } = null!;
|
|
85
|
-
|
|
86
|
-
public async Task InitializeAsync()
|
|
87
|
-
{
|
|
88
|
-
var env = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Test";
|
|
89
|
-
Settings = new ConfigurationBuilder()
|
|
90
|
-
.AddJsonFile("appsettings.json")
|
|
91
|
-
.AddJsonFile($"appsettings.{env}.json", optional: true)
|
|
92
|
-
.AddEnvironmentVariables("APITEST_")
|
|
93
|
-
.Build()
|
|
94
|
-
.GetRequiredSection("ApiTest").Get<ApiTestSettings>()!;
|
|
95
|
-
|
|
96
|
-
Client = new HttpClient { BaseAddress = new Uri(Settings.BaseUrl),
|
|
97
|
-
Timeout = TimeSpan.FromSeconds(Settings.TimeoutSeconds) };
|
|
98
|
-
|
|
99
|
-
if (!string.IsNullOrEmpty(Settings.Auth.Username))
|
|
100
|
-
await AuthenticateAsync();
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
public Task DisposeAsync() { Client.Dispose(); return Task.CompletedTask; }
|
|
104
|
-
|
|
105
|
-
protected async Task AuthenticateAsync(string? username = null, string? password = null)
|
|
106
|
-
{
|
|
107
|
-
var res = await Client.PostAsJsonAsync(Settings.Auth.TokenEndpoint,
|
|
108
|
-
new { username = username ?? Settings.Auth.Username, password = password ?? Settings.Auth.Password });
|
|
109
|
-
res.EnsureSuccessStatusCode();
|
|
110
|
-
var token = (await res.Content.ReadFromJsonAsync<TokenResponse>())!.AccessToken;
|
|
111
|
-
Client.DefaultRequestHeaders.Authorization =
|
|
112
|
-
new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
private sealed record TokenResponse(string AccessToken);
|
|
116
|
-
}
|
|
117
|
-
```
|
|
118
|
-
|
|
119
|
-
## Test Class Header (required)
|
|
120
|
-
|
|
121
|
-
```csharp
|
|
122
|
-
// ============================================================
|
|
123
|
-
// {Resource} API Tests — v{N}
|
|
124
|
-
// Spec: {spec-file} | Generated: {YYYY-MM-DD} | Story: {ID}
|
|
125
|
-
// APPEND-ONLY: don't modify existing methods.
|
|
126
|
-
// ============================================================
|
|
127
|
-
namespace ApiTests.V{N};
|
|
128
|
-
public sealed class {Resource}ApiTests : TestBase
|
|
129
|
-
{
|
|
130
|
-
// Inline DTOs — don't import from production code
|
|
131
|
-
private sealed record {Resource}Dto(Guid Id, string Name);
|
|
132
|
-
private sealed record ListResponse<T>(IReadOnlyList<T> Data, int Total);
|
|
133
|
-
}
|
|
134
|
-
```
|
|
135
|
-
|
|
136
|
-
## Test Naming
|
|
137
|
-
|
|
138
|
-
```
|
|
139
|
-
{HttpMethod}_{Resource}_Returns{Status}_When{Condition}
|
|
140
|
-
```
|
|
141
|
-
|
|
142
|
-
Example: `GetById_User_Returns200_WhenExists`, `Create_Order_Returns422_WhenEmailInvalid`
|
|
143
|
-
|
|
144
|
-
AC test: comment `// AC: {text}` right below XML doc.
|
|
145
|
-
|
|
146
|
-
## XML Doc (required on each test)
|
|
147
|
-
|
|
148
|
-
```csharp
|
|
149
|
-
/// <summary>Verify {METHOD} {path} → {status} when {condition}. Spec: {ref}</summary>
|
|
150
|
-
[Fact]
|
|
151
|
-
public async Task ...
|
|
152
|
-
```
|
|
153
|
-
|
|
154
|
-
## Coverage Matrix
|
|
155
|
-
|
|
156
|
-
| Condition | Status | When |
|
|
157
|
-
|---|---|---|
|
|
158
|
-
| Valid, authenticated | 2xx | Always |
|
|
159
|
-
| No token | 401 | Endpoint requires auth |
|
|
160
|
-
| Insufficient permission | 403 | RBAC / ownership |
|
|
161
|
-
| `{id}` doesn't exist | 404 | Has path param |
|
|
162
|
-
| Required field missing/invalid | 400/422 | Has request body |
|
|
163
|
-
| Business rule (from AC) | 4xx | Story has corresponding AC |
|
|
164
|
-
|
|
165
|
-
## CI/CD Env Vars
|
|
166
|
-
|
|
167
|
-
```
|
|
168
|
-
ASPNETCORE_ENVIRONMENT=Test
|
|
169
|
-
APITEST__AUTH__USERNAME=... # double __ for nested key
|
|
170
|
-
APITEST__AUTH__PASSWORD=...
|
|
171
|
-
```
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- "**/*.cs"
|
|
4
|
+
- "**/ApiTests/**"
|
|
5
|
+
- "**/Api.Tests/**"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# C# API Automation Testing
|
|
9
|
+
|
|
10
|
+
> Used by `/tas-apitest`. Extends [csharp/testing.md](./testing.md).
|
|
11
|
+
|
|
12
|
+
## Tech Stack
|
|
13
|
+
|
|
14
|
+
| Component | Choice |
|
|
15
|
+
|---|---|
|
|
16
|
+
| Framework | xUnit (default) — match project if already using MSTest/NUnit |
|
|
17
|
+
| Assertions | FluentAssertions |
|
|
18
|
+
| HTTP | System.Net.Http.HttpClient |
|
|
19
|
+
| Config | Microsoft.Extensions.Configuration + JSON/EnvVars |
|
|
20
|
+
|
|
21
|
+
```xml
|
|
22
|
+
<!-- ApiTests.csproj packages -->
|
|
23
|
+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.*" />
|
|
24
|
+
<PackageReference Include="xunit" Version="2.*" />
|
|
25
|
+
<PackageReference Include="xunit.runner.visualstudio" Version="2.*" />
|
|
26
|
+
<PackageReference Include="FluentAssertions" Version="6.*" />
|
|
27
|
+
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.*" />
|
|
28
|
+
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.*" />
|
|
29
|
+
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="8.*" />
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Project Structure
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
tests/ApiTests/
|
|
36
|
+
appsettings.json # base (no real secrets)
|
|
37
|
+
appsettings.Test.json # Test env override
|
|
38
|
+
appsettings.Staging.json # Staging env override
|
|
39
|
+
.gitignore # appsettings.*.local.json
|
|
40
|
+
Shared/
|
|
41
|
+
ApiTestSettings.cs
|
|
42
|
+
TestBase.cs
|
|
43
|
+
v1/
|
|
44
|
+
UsersApiTests.cs
|
|
45
|
+
v2/ # APPEND-ONLY: don't modify v1
|
|
46
|
+
UsersApiTests.cs
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Config
|
|
50
|
+
|
|
51
|
+
```json
|
|
52
|
+
// appsettings.json
|
|
53
|
+
{
|
|
54
|
+
"ApiTest": {
|
|
55
|
+
"BaseUrl": "https://localhost:5001",
|
|
56
|
+
"TimeoutSeconds": 30,
|
|
57
|
+
"Auth": { "Type": "Bearer", "TokenEndpoint": "/api/auth/token", "Username": "", "Password": "" }
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
```csharp
|
|
63
|
+
// ApiTestSettings.cs
|
|
64
|
+
public sealed class ApiTestSettings
|
|
65
|
+
{
|
|
66
|
+
public required string BaseUrl { get; init; }
|
|
67
|
+
public int TimeoutSeconds { get; init; } = 30;
|
|
68
|
+
public required AuthSettings Auth { get; init; }
|
|
69
|
+
}
|
|
70
|
+
public sealed class AuthSettings
|
|
71
|
+
{
|
|
72
|
+
public string Type { get; init; } = "Bearer";
|
|
73
|
+
public string TokenEndpoint { get; init; } = "/api/auth/token";
|
|
74
|
+
public string Username { get; init; } = "";
|
|
75
|
+
public string Password { get; init; } = "";
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
```csharp
|
|
80
|
+
// TestBase.cs
|
|
81
|
+
public abstract class TestBase : IAsyncLifetime
|
|
82
|
+
{
|
|
83
|
+
protected HttpClient Client { get; private set; } = null!;
|
|
84
|
+
protected ApiTestSettings Settings { get; private set; } = null!;
|
|
85
|
+
|
|
86
|
+
public async Task InitializeAsync()
|
|
87
|
+
{
|
|
88
|
+
var env = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Test";
|
|
89
|
+
Settings = new ConfigurationBuilder()
|
|
90
|
+
.AddJsonFile("appsettings.json")
|
|
91
|
+
.AddJsonFile($"appsettings.{env}.json", optional: true)
|
|
92
|
+
.AddEnvironmentVariables("APITEST_")
|
|
93
|
+
.Build()
|
|
94
|
+
.GetRequiredSection("ApiTest").Get<ApiTestSettings>()!;
|
|
95
|
+
|
|
96
|
+
Client = new HttpClient { BaseAddress = new Uri(Settings.BaseUrl),
|
|
97
|
+
Timeout = TimeSpan.FromSeconds(Settings.TimeoutSeconds) };
|
|
98
|
+
|
|
99
|
+
if (!string.IsNullOrEmpty(Settings.Auth.Username))
|
|
100
|
+
await AuthenticateAsync();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
public Task DisposeAsync() { Client.Dispose(); return Task.CompletedTask; }
|
|
104
|
+
|
|
105
|
+
protected async Task AuthenticateAsync(string? username = null, string? password = null)
|
|
106
|
+
{
|
|
107
|
+
var res = await Client.PostAsJsonAsync(Settings.Auth.TokenEndpoint,
|
|
108
|
+
new { username = username ?? Settings.Auth.Username, password = password ?? Settings.Auth.Password });
|
|
109
|
+
res.EnsureSuccessStatusCode();
|
|
110
|
+
var token = (await res.Content.ReadFromJsonAsync<TokenResponse>())!.AccessToken;
|
|
111
|
+
Client.DefaultRequestHeaders.Authorization =
|
|
112
|
+
new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
private sealed record TokenResponse(string AccessToken);
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Test Class Header (required)
|
|
120
|
+
|
|
121
|
+
```csharp
|
|
122
|
+
// ============================================================
|
|
123
|
+
// {Resource} API Tests — v{N}
|
|
124
|
+
// Spec: {spec-file} | Generated: {YYYY-MM-DD} | Story: {ID}
|
|
125
|
+
// APPEND-ONLY: don't modify existing methods.
|
|
126
|
+
// ============================================================
|
|
127
|
+
namespace ApiTests.V{N};
|
|
128
|
+
public sealed class {Resource}ApiTests : TestBase
|
|
129
|
+
{
|
|
130
|
+
// Inline DTOs — don't import from production code
|
|
131
|
+
private sealed record {Resource}Dto(Guid Id, string Name);
|
|
132
|
+
private sealed record ListResponse<T>(IReadOnlyList<T> Data, int Total);
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Test Naming
|
|
137
|
+
|
|
138
|
+
```
|
|
139
|
+
{HttpMethod}_{Resource}_Returns{Status}_When{Condition}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Example: `GetById_User_Returns200_WhenExists`, `Create_Order_Returns422_WhenEmailInvalid`
|
|
143
|
+
|
|
144
|
+
AC test: comment `// AC: {text}` right below XML doc.
|
|
145
|
+
|
|
146
|
+
## XML Doc (required on each test)
|
|
147
|
+
|
|
148
|
+
```csharp
|
|
149
|
+
/// <summary>Verify {METHOD} {path} → {status} when {condition}. Spec: {ref}</summary>
|
|
150
|
+
[Fact]
|
|
151
|
+
public async Task ...
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Coverage Matrix
|
|
155
|
+
|
|
156
|
+
| Condition | Status | When |
|
|
157
|
+
|---|---|---|
|
|
158
|
+
| Valid, authenticated | 2xx | Always |
|
|
159
|
+
| No token | 401 | Endpoint requires auth |
|
|
160
|
+
| Insufficient permission | 403 | RBAC / ownership |
|
|
161
|
+
| `{id}` doesn't exist | 404 | Has path param |
|
|
162
|
+
| Required field missing/invalid | 400/422 | Has request body |
|
|
163
|
+
| Business rule (from AC) | 4xx | Story has corresponding AC |
|
|
164
|
+
|
|
165
|
+
## CI/CD Env Vars
|
|
166
|
+
|
|
167
|
+
```
|
|
168
|
+
ASPNETCORE_ENVIRONMENT=Test
|
|
169
|
+
APITEST__AUTH__USERNAME=... # double __ for nested key
|
|
170
|
+
APITEST__AUTH__PASSWORD=...
|
|
171
|
+
```
|
|
@@ -53,6 +53,16 @@ await connection.QueryAsync<Order>(sql, new { customerId });
|
|
|
53
53
|
- Log detailed exceptions with structured context server-side
|
|
54
54
|
- Do not expose stack traces, SQL text, or filesystem paths in API responses
|
|
55
55
|
|
|
56
|
+
## Web / API Hardening
|
|
57
|
+
|
|
58
|
+
- Enforce HTTPS in production (`app.UseHttpsRedirection()`)
|
|
59
|
+
- Enable HSTS (`app.UseHsts()`)
|
|
60
|
+
- Add security headers: `X-Content-Type-Options: nosniff`, `X-Frame-Options: DENY`, `Content-Security-Policy`
|
|
61
|
+
- CORS policy must be restrictive — list allowed origins explicitly, never `AllowAnyOrigin()` in production
|
|
62
|
+
- Anti-forgery token required for state-changing form posts (`[ValidateAntiForgeryToken]`)
|
|
63
|
+
- File upload validation: check size limit, allowed MIME types, sanitize file name, scan for malware before persisting
|
|
64
|
+
- Encrypt PII at rest (column-level encryption, Always Encrypted, or transparent data encryption)
|
|
65
|
+
|
|
56
66
|
## References
|
|
57
67
|
|
|
58
68
|
See skill: `security-review` for broader application security review checklists.
|
|
@@ -7,8 +7,6 @@ paths:
|
|
|
7
7
|
---
|
|
8
8
|
# TypeScript/JavaScript Coding Style
|
|
9
9
|
|
|
10
|
-
> This file extends [common/coding-style.md](../common/coding-style.md) with TypeScript/JavaScript specific content.
|
|
11
|
-
|
|
12
10
|
## Types and Interfaces
|
|
13
11
|
|
|
14
12
|
Use types to make public APIs, shared models, and component props explicit, readable, and reusable.
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- "**/*.ts"
|
|
4
|
+
- "**/*.tsx"
|
|
5
|
+
- "**/*.js"
|
|
6
|
+
- "**/*.jsx"
|
|
7
|
+
---
|
|
8
|
+
# TypeScript/JavaScript Patterns
|
|
9
|
+
|
|
10
|
+
> This file extends [common/patterns.md](../common/patterns.md) with TypeScript/JavaScript specific content.
|
|
11
|
+
|
|
12
|
+
## API Response Format
|
|
13
|
+
|
|
14
|
+
```typescript
|
|
15
|
+
interface ApiResponse<T> {
|
|
16
|
+
success: boolean
|
|
17
|
+
data?: T
|
|
18
|
+
error?: string
|
|
19
|
+
meta?: {
|
|
20
|
+
total: number
|
|
21
|
+
page: number
|
|
22
|
+
limit: number
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Custom Hooks Pattern
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
export function useDebounce<T>(value: T, delay: number): T {
|
|
31
|
+
const [debouncedValue, setDebouncedValue] = useState<T>(value)
|
|
32
|
+
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
const handler = setTimeout(() => setDebouncedValue(value), delay)
|
|
35
|
+
return () => clearTimeout(handler)
|
|
36
|
+
}, [value, delay])
|
|
37
|
+
|
|
38
|
+
return debouncedValue
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Repository Pattern
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
interface Repository<T> {
|
|
46
|
+
findAll(filters?: Filters): Promise<T[]>
|
|
47
|
+
findById(id: string): Promise<T | null>
|
|
48
|
+
create(data: CreateDto): Promise<T>
|
|
49
|
+
update(id: string, data: UpdateDto): Promise<T>
|
|
50
|
+
delete(id: string): Promise<void>
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Performance Anti-Patterns
|
|
55
|
+
|
|
56
|
+
Avoid these in Node.js backends:
|
|
57
|
+
|
|
58
|
+
- **Sequential awaits that could be parallel**: use `Promise.all()` when calls are independent
|
|
59
|
+
- **CPU-blocking on event loop**: offload heavy computation to worker threads
|
|
60
|
+
- **Missing connection pooling**: never create a new DB connection per request — use pooled clients
|
|
61
|
+
- **ORM N+1**: missing `include`/`select` in Prisma/Sequelize — use eager loading or DataLoader for GraphQL
|
|
62
|
+
- **Large file reads without streaming**: avoid `fs.readFile` on large files — use streams instead
|
|
63
|
+
|
|
64
|
+
## N+1 Query Prevention (Batch Fetch)
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
// BAD: N+1 queries
|
|
68
|
+
const markets = await getMarkets()
|
|
69
|
+
for (const market of markets) {
|
|
70
|
+
market.creator = await getUser(market.creator_id) // N queries
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// GOOD: Batch fetch — 2 queries total
|
|
74
|
+
const markets = await getMarkets()
|
|
75
|
+
const creatorIds = markets.map(m => m.creator_id)
|
|
76
|
+
const creators = await getUsers(creatorIds)
|
|
77
|
+
const creatorMap = new Map(creators.map(c => [c.id, c]))
|
|
78
|
+
markets.forEach(m => { m.creator = creatorMap.get(m.creator_id) })
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Centralized Error Handler
|
|
82
|
+
|
|
83
|
+
Custom `ApiError` class + single error mapper for API routes.
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
class ApiError extends Error {
|
|
87
|
+
constructor(public statusCode: number, message: string, public isOperational = true) {
|
|
88
|
+
super(message)
|
|
89
|
+
Object.setPrototypeOf(this, ApiError.prototype)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function errorHandler(error: unknown): Response {
|
|
94
|
+
if (error instanceof ApiError) {
|
|
95
|
+
return NextResponse.json({ error: error.message }, { status: error.statusCode })
|
|
96
|
+
}
|
|
97
|
+
if (error instanceof z.ZodError) {
|
|
98
|
+
return NextResponse.json({ error: 'Validation failed', details: error.errors }, { status: 400 })
|
|
99
|
+
}
|
|
100
|
+
console.error('Unexpected error:', error)
|
|
101
|
+
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Cache-Aside Pattern
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
async function getMarketWithCache(id: string): Promise<Market> {
|
|
109
|
+
const cacheKey = `market:${id}`
|
|
110
|
+
const cached = await redis.get(cacheKey)
|
|
111
|
+
if (cached) return JSON.parse(cached)
|
|
112
|
+
|
|
113
|
+
const market = await db.markets.findUnique({ where: { id } })
|
|
114
|
+
if (!market) throw new ApiError(404, 'Market not found')
|
|
115
|
+
|
|
116
|
+
await redis.setex(cacheKey, 300, JSON.stringify(market)) // 5 min TTL
|
|
117
|
+
return market
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Invalidate on writes: `await redis.del(\`market:\${id}\`)` after update/delete.
|
|
122
|
+
|
|
123
|
+
## Retry with Exponential Backoff
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
async function fetchWithRetry<T>(fn: () => Promise<T>, maxRetries = 3): Promise<T> {
|
|
127
|
+
let lastError: Error
|
|
128
|
+
for (let i = 0; i < maxRetries; i++) {
|
|
129
|
+
try {
|
|
130
|
+
return await fn()
|
|
131
|
+
} catch (error) {
|
|
132
|
+
lastError = error as Error
|
|
133
|
+
if (i < maxRetries - 1) {
|
|
134
|
+
await new Promise(r => setTimeout(r, Math.pow(2, i) * 1000)) // 1s, 2s, 4s
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
throw lastError!
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Use for transient failures (network, upstream API). Do NOT retry on 4xx client errors.
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- "**/*.ts"
|
|
4
|
+
- "**/*.tsx"
|
|
5
|
+
- "**/*.js"
|
|
6
|
+
- "**/*.jsx"
|
|
7
|
+
---
|
|
8
|
+
# TypeScript/JavaScript Security
|
|
9
|
+
|
|
10
|
+
> This file extends [common/security.md](../common/security.md) with TypeScript/JavaScript specific content.
|
|
11
|
+
|
|
12
|
+
## Secret Management
|
|
13
|
+
|
|
14
|
+
```typescript
|
|
15
|
+
// NEVER: Hardcoded secrets
|
|
16
|
+
const apiKey = "sk-proj-xxxxx"
|
|
17
|
+
|
|
18
|
+
// ALWAYS: Environment variables
|
|
19
|
+
const apiKey = process.env.OPENAI_API_KEY
|
|
20
|
+
|
|
21
|
+
if (!apiKey) {
|
|
22
|
+
throw new Error('OPENAI_API_KEY not configured')
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## JWT Token Validation
|
|
27
|
+
|
|
28
|
+
```typescript
|
|
29
|
+
import jwt from 'jsonwebtoken'
|
|
30
|
+
|
|
31
|
+
interface JWTPayload {
|
|
32
|
+
userId: string
|
|
33
|
+
email: string
|
|
34
|
+
role: 'admin' | 'user'
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function verifyToken(token: string): JWTPayload {
|
|
38
|
+
try {
|
|
39
|
+
return jwt.verify(token, process.env.JWT_SECRET!) as JWTPayload
|
|
40
|
+
} catch {
|
|
41
|
+
throw new ApiError(401, 'Invalid token')
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function requireAuth(request: Request): Promise<JWTPayload> {
|
|
46
|
+
const token = request.headers.get('authorization')?.replace('Bearer ', '')
|
|
47
|
+
if (!token) throw new ApiError(401, 'Missing authorization token')
|
|
48
|
+
return verifyToken(token)
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Always verify `JWT_SECRET` is set at boot. Never log tokens. Use short TTL (15min) + refresh tokens.
|
|
53
|
+
|
|
54
|
+
## Role-Based Access Control (RBAC)
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
type Permission = 'read' | 'write' | 'delete' | 'admin'
|
|
58
|
+
|
|
59
|
+
const rolePermissions: Record<User['role'], Permission[]> = {
|
|
60
|
+
admin: ['read', 'write', 'delete', 'admin'],
|
|
61
|
+
moderator: ['read', 'write', 'delete'],
|
|
62
|
+
user: ['read', 'write'],
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function hasPermission(user: User, permission: Permission): boolean {
|
|
66
|
+
return rolePermissions[user.role].includes(permission)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function requirePermission(permission: Permission) {
|
|
70
|
+
return (handler: (req: Request, user: User) => Promise<Response>) =>
|
|
71
|
+
async (req: Request) => {
|
|
72
|
+
const user = await requireAuth(req)
|
|
73
|
+
if (!hasPermission(user, permission)) throw new ApiError(403, 'Insufficient permissions')
|
|
74
|
+
return handler(req, user)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Usage
|
|
79
|
+
export const DELETE = requirePermission('delete')(async (req, user) => {
|
|
80
|
+
// handler with verified permission
|
|
81
|
+
})
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Check authorization at the route level (not inside business logic). Resource-level ownership checks (e.g., `if (resource.userId !== user.id)`) belong in service layer.
|
|
85
|
+
|
|
86
|
+
## Agent Support
|
|
87
|
+
|
|
88
|
+
- Use **security-reviewer** skill for comprehensive security audits
|