@atlashub/smartstack-cli 1.37.0 → 2.0.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/config/mcp-defaults.json +62 -0
- package/dist/index.js +57 -4
- package/dist/index.js.map +1 -1
- package/dist/mcp-entry.mjs +16984 -0
- package/dist/mcp-entry.mjs.map +1 -0
- package/package.json +14 -5
- package/templates/agents/gitflow/start.md +5 -4
- package/templates/agents/mcp-healthcheck.md +15 -13
- package/templates/mcp-scaffolding/component.tsx.hbs +298 -0
- package/templates/mcp-scaffolding/controller.cs.hbs +184 -0
- package/templates/mcp-scaffolding/entity-extension.cs.hbs +231 -0
- package/templates/mcp-scaffolding/frontend/api-client.ts.hbs +116 -0
- package/templates/mcp-scaffolding/frontend/nav-routes.ts.hbs +133 -0
- package/templates/mcp-scaffolding/frontend/routes.tsx.hbs +134 -0
- package/templates/mcp-scaffolding/migrations/seed-roles.cs.hbs +261 -0
- package/templates/mcp-scaffolding/service-extension.cs.hbs +53 -0
- package/templates/mcp-scaffolding/tests/controller.test.cs.hbs +413 -0
- package/templates/mcp-scaffolding/tests/entity.test.cs.hbs +239 -0
- package/templates/mcp-scaffolding/tests/repository.test.cs.hbs +441 -0
- package/templates/mcp-scaffolding/tests/security.test.cs.hbs +442 -0
- package/templates/mcp-scaffolding/tests/service.test.cs.hbs +390 -0
- package/templates/mcp-scaffolding/tests/validator.test.cs.hbs +428 -0
- package/templates/ralph/README.md +3 -3
- package/templates/ralph/ralph.config.yaml +2 -2
- package/templates/skills/admin/SKILL.md +42 -0
- package/templates/skills/business-analyse/_shared.md +24 -1
- package/templates/skills/business-analyse/questionnaire/01-context.md +4 -4
- package/templates/skills/business-analyse/questionnaire/02-stakeholders.md +3 -3
- package/templates/skills/business-analyse/questionnaire/03-scope.md +4 -4
- package/templates/skills/business-analyse/questionnaire/04-data.md +7 -7
- package/templates/skills/business-analyse/questionnaire/05-integrations.md +1 -1
- package/templates/skills/business-analyse/questionnaire/06-security.md +3 -3
- package/templates/skills/business-analyse/questionnaire/07-ui.md +1 -1
- package/templates/skills/business-analyse/questionnaire/08-performance.md +3 -3
- package/templates/skills/business-analyse/questionnaire/09-constraints.md +4 -4
- package/templates/skills/business-analyse/questionnaire/10-documentation.md +2 -2
- package/templates/skills/business-analyse/questionnaire/11-data-lifecycle.md +2 -2
- package/templates/skills/business-analyse/questionnaire/12-migration.md +1 -1
- package/templates/skills/business-analyse/questionnaire/13-cross-module.md +2 -2
- package/templates/skills/business-analyse/steps/step-01-discover.md +50 -25
- package/templates/skills/business-analyse/steps/step-05-handoff.md +133 -34
- package/templates/skills/cc-agent/SKILL.md +129 -0
- package/templates/skills/cc-agent/references/agent-frontmatter.md +213 -0
- package/templates/skills/cc-agent/references/permission-modes.md +102 -0
- package/templates/skills/cc-agent/references/tools-reference.md +144 -0
- package/templates/skills/cc-agent/steps/step-00-init.md +134 -0
- package/templates/skills/cc-agent/steps/step-01-design.md +186 -0
- package/templates/skills/cc-agent/steps/step-02-generate.md +204 -0
- package/templates/skills/cc-agent/steps/step-03-validate.md +130 -0
- package/templates/skills/cc-agent/templates/agent-categorized.md +67 -0
- package/templates/skills/cc-agent/templates/agent-standalone.md +56 -0
- package/templates/skills/cc-agent/templates/agent-with-skills.md +94 -0
- package/templates/skills/cc-audit/SKILL.md +108 -0
- package/templates/skills/cc-audit/references/agent-checklist.md +91 -0
- package/templates/skills/cc-audit/references/hook-checklist.md +110 -0
- package/templates/skills/cc-audit/references/skill-checklist.md +70 -0
- package/templates/skills/cc-audit/steps/step-00-init.md +98 -0
- package/templates/skills/cc-audit/steps/step-01-scan.md +142 -0
- package/templates/skills/cc-audit/steps/step-02-analyze.md +158 -0
- package/templates/skills/cc-audit/steps/step-03-report.md +142 -0
- package/templates/skills/cc-skill/SKILL.md +134 -0
- package/templates/skills/cc-skill/references/best-practices.md +167 -0
- package/templates/skills/cc-skill/references/frontmatter-reference.md +182 -0
- package/templates/skills/cc-skill/references/skill-patterns.md +199 -0
- package/templates/skills/cc-skill/steps/step-00-init.md +119 -0
- package/templates/skills/cc-skill/steps/step-01-design.md +199 -0
- package/templates/skills/cc-skill/steps/step-02-generate.md +145 -0
- package/templates/skills/cc-skill/steps/step-03-steps.md +151 -0
- package/templates/skills/cc-skill/steps/step-04-validate.md +124 -0
- package/templates/skills/cc-skill/templates/skill-forked.md +85 -0
- package/templates/skills/cc-skill/templates/skill-progressive.md +102 -0
- package/templates/skills/cc-skill/templates/skill-simple.md +75 -0
- package/templates/skills/cc-skill/templates/step-template.md +82 -0
- package/templates/skills/check-version/SKILL.md +6 -0
- package/templates/skills/debug/SKILL.md +4 -0
- package/templates/skills/documentation/SKILL.md +1 -0
- package/templates/skills/efcore/SKILL.md +5 -0
- package/templates/skills/efcore/steps/db/step-deploy.md +26 -5
- package/templates/skills/efcore/steps/shared/step-00-init.md +21 -7
- package/templates/skills/explore/SKILL.md +28 -32
- package/templates/skills/feature-full/SKILL.md +1 -0
- package/templates/skills/gitflow/SKILL.md +8 -0
- package/templates/skills/gitflow/steps/step-start.md +45 -10
- package/templates/skills/mcp/SKILL.md +38 -18
- package/templates/skills/quick-search/SKILL.md +8 -1
- package/templates/skills/ralph-loop/SKILL.md +1 -1
- package/templates/skills/ralph-loop/steps/step-00-init.md +8 -68
- package/templates/skills/ralph-loop/steps/step-04-check.md +1 -1
- package/templates/skills/refactor/SKILL.md +1 -0
- package/templates/skills/review-code/SKILL.md +7 -1
- package/templates/skills/ui-components/SKILL.md +31 -438
- package/templates/skills/ui-components/accessibility.md +170 -0
- package/templates/skills/ui-components/patterns/data-table.md +39 -0
- package/templates/skills/ui-components/patterns/entity-card.md +77 -0
- package/templates/skills/ui-components/patterns/grid-layout.md +91 -0
- package/templates/skills/ui-components/patterns/kanban.md +43 -0
- package/templates/skills/ui-components/style-guide.md +86 -0
- package/templates/skills/utils/SKILL.md +1 -0
- package/templates/skills/validate/SKILL.md +1 -0
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
{{!-- SmartStack Security Test Template --}}
|
|
2
|
+
{{!-- Generates security-focused tests for entities, services, and controllers --}}
|
|
3
|
+
|
|
4
|
+
using FluentAssertions;
|
|
5
|
+
using Microsoft.AspNetCore.Mvc.Testing;
|
|
6
|
+
using System.Net;
|
|
7
|
+
using System.Net.Http.Json;
|
|
8
|
+
using Xunit;
|
|
9
|
+
using {{namespace}}.Api;
|
|
10
|
+
using {{namespace}}.Domain.Entities;
|
|
11
|
+
|
|
12
|
+
namespace {{namespace}}.Tests.Security;
|
|
13
|
+
|
|
14
|
+
/// <summary>
|
|
15
|
+
/// Security tests for {{name}}.
|
|
16
|
+
/// Covers: Authentication, Authorization, Input Validation, Tenant Isolation, Data Protection
|
|
17
|
+
/// Follows SmartStack testing conventions: {Method}_When{Condition}_Should{Result}
|
|
18
|
+
/// </summary>
|
|
19
|
+
public class {{name}}SecurityTests : IClassFixture<WebApplicationFactory<Program>>
|
|
20
|
+
{
|
|
21
|
+
private readonly HttpClient _client;
|
|
22
|
+
private readonly HttpClient _unauthenticatedClient;
|
|
23
|
+
|
|
24
|
+
public {{name}}SecurityTests(WebApplicationFactory<Program> factory)
|
|
25
|
+
{
|
|
26
|
+
_client = factory.CreateClient();
|
|
27
|
+
_unauthenticatedClient = factory.CreateClient();
|
|
28
|
+
// Note: _client should have auth headers, _unauthenticatedClient should not
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
#region Authentication Tests
|
|
32
|
+
|
|
33
|
+
[Fact]
|
|
34
|
+
public async Task GetAll_WhenNotAuthenticated_ShouldReturn401()
|
|
35
|
+
{
|
|
36
|
+
// Act
|
|
37
|
+
var response = await _unauthenticatedClient.GetAsync("/api/{{lowerName}}");
|
|
38
|
+
|
|
39
|
+
// Assert
|
|
40
|
+
response.StatusCode.Should().Be(HttpStatusCode.Unauthorized);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
[Fact]
|
|
44
|
+
public async Task Create_WhenNotAuthenticated_ShouldReturn401()
|
|
45
|
+
{
|
|
46
|
+
// Arrange
|
|
47
|
+
var request = new { Code = "TEST" };
|
|
48
|
+
|
|
49
|
+
// Act
|
|
50
|
+
var response = await _unauthenticatedClient.PostAsJsonAsync("/api/{{lowerName}}", request);
|
|
51
|
+
|
|
52
|
+
// Assert
|
|
53
|
+
response.StatusCode.Should().Be(HttpStatusCode.Unauthorized);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
[Fact]
|
|
57
|
+
public async Task Delete_WhenNotAuthenticated_ShouldReturn401()
|
|
58
|
+
{
|
|
59
|
+
// Act
|
|
60
|
+
var response = await _unauthenticatedClient.DeleteAsync($"/api/{{lowerName}}/{Guid.NewGuid()}");
|
|
61
|
+
|
|
62
|
+
// Assert
|
|
63
|
+
response.StatusCode.Should().Be(HttpStatusCode.Unauthorized);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
[Fact]
|
|
67
|
+
public async Task Api_WhenTokenExpired_ShouldReturn401()
|
|
68
|
+
{
|
|
69
|
+
// Arrange
|
|
70
|
+
var expiredTokenClient = CreateClientWithExpiredToken();
|
|
71
|
+
|
|
72
|
+
// Act
|
|
73
|
+
var response = await expiredTokenClient.GetAsync("/api/{{lowerName}}");
|
|
74
|
+
|
|
75
|
+
// Assert
|
|
76
|
+
response.StatusCode.Should().Be(HttpStatusCode.Unauthorized);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
[Fact]
|
|
80
|
+
public async Task Api_WhenTokenMalformed_ShouldReturn401()
|
|
81
|
+
{
|
|
82
|
+
// Arrange
|
|
83
|
+
var malformedTokenClient = CreateClientWithMalformedToken();
|
|
84
|
+
|
|
85
|
+
// Act
|
|
86
|
+
var response = await malformedTokenClient.GetAsync("/api/{{lowerName}}");
|
|
87
|
+
|
|
88
|
+
// Assert
|
|
89
|
+
response.StatusCode.Should().Be(HttpStatusCode.Unauthorized);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
#endregion
|
|
93
|
+
|
|
94
|
+
#region Authorization Tests
|
|
95
|
+
|
|
96
|
+
[Fact]
|
|
97
|
+
public async Task Create_WhenUserLacksPermission_ShouldReturn403()
|
|
98
|
+
{
|
|
99
|
+
// Arrange
|
|
100
|
+
var readOnlyClient = CreateClientWithReadOnlyPermissions();
|
|
101
|
+
var request = new { Code = "TEST" };
|
|
102
|
+
|
|
103
|
+
// Act
|
|
104
|
+
var response = await readOnlyClient.PostAsJsonAsync("/api/{{lowerName}}", request);
|
|
105
|
+
|
|
106
|
+
// Assert
|
|
107
|
+
response.StatusCode.Should().Be(HttpStatusCode.Forbidden);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
[Fact]
|
|
111
|
+
public async Task Delete_WhenUserLacksAdminRole_ShouldReturn403()
|
|
112
|
+
{
|
|
113
|
+
// Arrange
|
|
114
|
+
var regularUserClient = CreateClientWithRegularUserRole();
|
|
115
|
+
|
|
116
|
+
// Act
|
|
117
|
+
var response = await regularUserClient.DeleteAsync($"/api/{{lowerName}}/{Guid.NewGuid()}");
|
|
118
|
+
|
|
119
|
+
// Assert
|
|
120
|
+
response.StatusCode.Should().Be(HttpStatusCode.Forbidden);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
[Fact]
|
|
124
|
+
public async Task Update_WhenUserCannotModifyOthersData_ShouldReturn403()
|
|
125
|
+
{
|
|
126
|
+
// Arrange
|
|
127
|
+
var otherUserId = Guid.NewGuid();
|
|
128
|
+
var request = new { Name = "Hacked" };
|
|
129
|
+
|
|
130
|
+
// Act
|
|
131
|
+
var response = await _client.PutAsJsonAsync($"/api/{{lowerName}}/{otherUserId}", request);
|
|
132
|
+
|
|
133
|
+
// Assert
|
|
134
|
+
// Should be 403 if trying to modify another user's data
|
|
135
|
+
response.StatusCode.Should().BeOneOf(HttpStatusCode.Forbidden, HttpStatusCode.NotFound);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
#endregion
|
|
139
|
+
|
|
140
|
+
{{#unless isSystemEntity}}
|
|
141
|
+
#region Tenant Isolation Tests
|
|
142
|
+
|
|
143
|
+
[Fact]
|
|
144
|
+
public async Task GetById_WhenAccessingOtherTenantData_ShouldReturn404()
|
|
145
|
+
{
|
|
146
|
+
// Arrange
|
|
147
|
+
var otherTenantEntityId = Guid.NewGuid(); // ID from different tenant
|
|
148
|
+
|
|
149
|
+
// Act
|
|
150
|
+
var response = await _client.GetAsync($"/api/{{lowerName}}/{otherTenantEntityId}");
|
|
151
|
+
|
|
152
|
+
// Assert
|
|
153
|
+
response.StatusCode.Should().Be(HttpStatusCode.NotFound,
|
|
154
|
+
"accessing other tenant's data should appear as if it doesn't exist");
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
[Fact]
|
|
158
|
+
public async Task Update_WhenTargetingOtherTenantData_ShouldReturn404()
|
|
159
|
+
{
|
|
160
|
+
// Arrange
|
|
161
|
+
var otherTenantEntityId = Guid.NewGuid();
|
|
162
|
+
var request = new { Name = "Hacked" };
|
|
163
|
+
|
|
164
|
+
// Act
|
|
165
|
+
var response = await _client.PutAsJsonAsync($"/api/{{lowerName}}/{otherTenantEntityId}", request);
|
|
166
|
+
|
|
167
|
+
// Assert
|
|
168
|
+
response.StatusCode.Should().Be(HttpStatusCode.NotFound);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
[Fact]
|
|
172
|
+
public async Task Delete_WhenTargetingOtherTenantData_ShouldReturn404()
|
|
173
|
+
{
|
|
174
|
+
// Arrange
|
|
175
|
+
var otherTenantEntityId = Guid.NewGuid();
|
|
176
|
+
|
|
177
|
+
// Act
|
|
178
|
+
var response = await _client.DeleteAsync($"/api/{{lowerName}}/{otherTenantEntityId}");
|
|
179
|
+
|
|
180
|
+
// Assert
|
|
181
|
+
response.StatusCode.Should().Be(HttpStatusCode.NotFound);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
[Fact]
|
|
185
|
+
public async Task Create_ShouldNotAllowTenantIdInRequest()
|
|
186
|
+
{
|
|
187
|
+
// Arrange - Try to create with a different tenant ID
|
|
188
|
+
var maliciousRequest = new
|
|
189
|
+
{
|
|
190
|
+
Code = "TEST",
|
|
191
|
+
TenantId = Guid.NewGuid() // Attempting to specify tenant
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
// Act
|
|
195
|
+
var response = await _client.PostAsJsonAsync("/api/{{lowerName}}", maliciousRequest);
|
|
196
|
+
|
|
197
|
+
// Assert
|
|
198
|
+
// Should either ignore the TenantId or return 400
|
|
199
|
+
response.StatusCode.Should().BeOneOf(HttpStatusCode.Created, HttpStatusCode.BadRequest);
|
|
200
|
+
|
|
201
|
+
if (response.StatusCode == HttpStatusCode.Created)
|
|
202
|
+
{
|
|
203
|
+
// If created, verify tenant was not the one in request
|
|
204
|
+
var created = await response.Content.ReadFromJsonAsync<{{name}}Response>();
|
|
205
|
+
created!.TenantId.Should().NotBe(maliciousRequest.TenantId,
|
|
206
|
+
"server should assign tenant based on auth context, not request");
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
#endregion
|
|
211
|
+
{{/unless}}
|
|
212
|
+
|
|
213
|
+
#region Input Validation / Injection Prevention Tests
|
|
214
|
+
|
|
215
|
+
[Theory]
|
|
216
|
+
[InlineData("<script>alert('xss')</script>")]
|
|
217
|
+
[InlineData("<img src=x onerror=alert('xss')>")]
|
|
218
|
+
[InlineData("javascript:alert('xss')")]
|
|
219
|
+
public async Task Create_WhenXssAttempt_ShouldSanitizeOrReject(string xssPayload)
|
|
220
|
+
{
|
|
221
|
+
// Arrange
|
|
222
|
+
var request = new { Code = "TEST", Name = xssPayload };
|
|
223
|
+
|
|
224
|
+
// Act
|
|
225
|
+
var response = await _client.PostAsJsonAsync("/api/{{lowerName}}", request);
|
|
226
|
+
|
|
227
|
+
// Assert
|
|
228
|
+
response.StatusCode.Should().BeOneOf(HttpStatusCode.BadRequest, HttpStatusCode.Created);
|
|
229
|
+
|
|
230
|
+
if (response.StatusCode == HttpStatusCode.Created)
|
|
231
|
+
{
|
|
232
|
+
var created = await response.Content.ReadFromJsonAsync<{{name}}Response>();
|
|
233
|
+
created!.Name.Should().NotContain("<script>", "XSS should be sanitized");
|
|
234
|
+
created.Name.Should().NotContain("javascript:", "XSS should be sanitized");
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
[Theory]
|
|
239
|
+
[InlineData("'; DROP TABLE {{name}}s; --")]
|
|
240
|
+
[InlineData("1; DELETE FROM {{name}}s WHERE 1=1; --")]
|
|
241
|
+
[InlineData("1 OR 1=1")]
|
|
242
|
+
public async Task Create_WhenSqlInjectionAttempt_ShouldPrevent(string sqlPayload)
|
|
243
|
+
{
|
|
244
|
+
// Arrange
|
|
245
|
+
var request = new { Code = sqlPayload };
|
|
246
|
+
|
|
247
|
+
// Act
|
|
248
|
+
var response = await _client.PostAsJsonAsync("/api/{{lowerName}}", request);
|
|
249
|
+
|
|
250
|
+
// Assert
|
|
251
|
+
// Should be rejected by validation or handled safely by parameterized queries
|
|
252
|
+
response.StatusCode.Should().BeOneOf(HttpStatusCode.BadRequest, HttpStatusCode.Created);
|
|
253
|
+
|
|
254
|
+
// Verify database wasn't affected
|
|
255
|
+
var allResponse = await _client.GetAsync("/api/{{lowerName}}");
|
|
256
|
+
allResponse.StatusCode.Should().Be(HttpStatusCode.OK);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
[Theory]
|
|
260
|
+
[InlineData("{{'{{'}}constructor{{'}}'}}")]
|
|
261
|
+
[InlineData("{{'{{'}}__proto__{{'}}'}}")]
|
|
262
|
+
[InlineData("{\"$type\":\"System.Diagnostics.Process\"}")]
|
|
263
|
+
public async Task Create_WhenPrototypePollutionAttempt_ShouldPrevent(string payload)
|
|
264
|
+
{
|
|
265
|
+
// Arrange
|
|
266
|
+
var request = new { Code = "TEST", Description = payload };
|
|
267
|
+
|
|
268
|
+
// Act
|
|
269
|
+
var response = await _client.PostAsJsonAsync("/api/{{lowerName}}", request);
|
|
270
|
+
|
|
271
|
+
// Assert
|
|
272
|
+
response.StatusCode.Should().BeOneOf(HttpStatusCode.BadRequest, HttpStatusCode.Created);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
[Theory]
|
|
276
|
+
[InlineData("../../../etc/passwd")]
|
|
277
|
+
[InlineData("..\\..\\..\\windows\\system32")]
|
|
278
|
+
[InlineData("file:///etc/passwd")]
|
|
279
|
+
public async Task Create_WhenPathTraversalAttempt_ShouldPrevent(string pathPayload)
|
|
280
|
+
{
|
|
281
|
+
// Arrange
|
|
282
|
+
var request = new { Code = "TEST", FilePath = pathPayload };
|
|
283
|
+
|
|
284
|
+
// Act
|
|
285
|
+
var response = await _client.PostAsJsonAsync("/api/{{lowerName}}", request);
|
|
286
|
+
|
|
287
|
+
// Assert
|
|
288
|
+
response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
#endregion
|
|
292
|
+
|
|
293
|
+
#region IDOR (Insecure Direct Object Reference) Tests
|
|
294
|
+
|
|
295
|
+
[Fact]
|
|
296
|
+
public async Task GetById_WhenGuessingIds_ShouldNotLeakData()
|
|
297
|
+
{
|
|
298
|
+
// Arrange - Try sequential IDs
|
|
299
|
+
var guessedIds = Enumerable.Range(1, 10).Select(i => Guid.NewGuid());
|
|
300
|
+
|
|
301
|
+
foreach (var id in guessedIds)
|
|
302
|
+
{
|
|
303
|
+
// Act
|
|
304
|
+
var response = await _client.GetAsync($"/api/{{lowerName}}/{id}");
|
|
305
|
+
|
|
306
|
+
// Assert
|
|
307
|
+
response.StatusCode.Should().BeOneOf(
|
|
308
|
+
HttpStatusCode.NotFound,
|
|
309
|
+
HttpStatusCode.OK, // Only if it's user's own data
|
|
310
|
+
"should not leak existence of other users' data"
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
#endregion
|
|
316
|
+
|
|
317
|
+
#region Rate Limiting Tests
|
|
318
|
+
|
|
319
|
+
[Fact]
|
|
320
|
+
public async Task Api_WhenExcessiveRequests_ShouldReturn429()
|
|
321
|
+
{
|
|
322
|
+
// Arrange
|
|
323
|
+
var tasks = Enumerable.Range(1, 100)
|
|
324
|
+
.Select(_ => _client.GetAsync("/api/{{lowerName}}"));
|
|
325
|
+
|
|
326
|
+
// Act
|
|
327
|
+
var responses = await Task.WhenAll(tasks);
|
|
328
|
+
|
|
329
|
+
// Assert
|
|
330
|
+
responses.Should().Contain(r => r.StatusCode == HttpStatusCode.TooManyRequests,
|
|
331
|
+
"rate limiting should be enforced");
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
#endregion
|
|
335
|
+
|
|
336
|
+
#region Sensitive Data Exposure Tests
|
|
337
|
+
|
|
338
|
+
[Fact]
|
|
339
|
+
public async Task GetById_ShouldNotExposeInternalFields()
|
|
340
|
+
{
|
|
341
|
+
// Arrange
|
|
342
|
+
var id = Guid.NewGuid();
|
|
343
|
+
|
|
344
|
+
// Act
|
|
345
|
+
var response = await _client.GetAsync($"/api/{{lowerName}}/{id}");
|
|
346
|
+
|
|
347
|
+
if (response.StatusCode == HttpStatusCode.OK)
|
|
348
|
+
{
|
|
349
|
+
var content = await response.Content.ReadAsStringAsync();
|
|
350
|
+
|
|
351
|
+
// Assert
|
|
352
|
+
content.Should().NotContain("password", StringComparison.OrdinalIgnoreCase);
|
|
353
|
+
content.Should().NotContain("secret", StringComparison.OrdinalIgnoreCase);
|
|
354
|
+
content.Should().NotContain("connectionString", StringComparison.OrdinalIgnoreCase);
|
|
355
|
+
content.Should().NotContain("apiKey", StringComparison.OrdinalIgnoreCase);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
[Fact]
|
|
360
|
+
public async Task ErrorResponse_ShouldNotExposeStackTrace()
|
|
361
|
+
{
|
|
362
|
+
// Arrange - Trigger an error
|
|
363
|
+
var request = new { Code = new string('A', 10000) }; // Very long to potentially cause error
|
|
364
|
+
|
|
365
|
+
// Act
|
|
366
|
+
var response = await _client.PostAsJsonAsync("/api/{{lowerName}}", request);
|
|
367
|
+
|
|
368
|
+
if (!response.IsSuccessStatusCode)
|
|
369
|
+
{
|
|
370
|
+
var content = await response.Content.ReadAsStringAsync();
|
|
371
|
+
|
|
372
|
+
// Assert
|
|
373
|
+
content.Should().NotContain("at ", "stack trace should not be exposed");
|
|
374
|
+
content.Should().NotContain("Exception", "exception details should not be exposed");
|
|
375
|
+
content.Should().NotContain(".cs:line", "source file info should not be exposed");
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
#endregion
|
|
380
|
+
|
|
381
|
+
#region CORS Tests
|
|
382
|
+
|
|
383
|
+
[Fact]
|
|
384
|
+
public async Task Api_ShouldHaveProperCorsHeaders()
|
|
385
|
+
{
|
|
386
|
+
// Act
|
|
387
|
+
var response = await _client.GetAsync("/api/{{lowerName}}");
|
|
388
|
+
|
|
389
|
+
// Assert
|
|
390
|
+
var corsHeader = response.Headers.GetValues("Access-Control-Allow-Origin").FirstOrDefault();
|
|
391
|
+
corsHeader.Should().NotBe("*", "CORS should not allow all origins in production");
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
#endregion
|
|
395
|
+
|
|
396
|
+
#region Security Headers Tests
|
|
397
|
+
|
|
398
|
+
[Fact]
|
|
399
|
+
public async Task Api_ShouldHaveSecurityHeaders()
|
|
400
|
+
{
|
|
401
|
+
// Act
|
|
402
|
+
var response = await _client.GetAsync("/api/{{lowerName}}");
|
|
403
|
+
|
|
404
|
+
// Assert
|
|
405
|
+
response.Headers.Should().ContainKey("X-Content-Type-Options");
|
|
406
|
+
response.Headers.Should().ContainKey("X-Frame-Options");
|
|
407
|
+
response.Headers.Should().ContainKey("X-XSS-Protection");
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
#endregion
|
|
411
|
+
|
|
412
|
+
#region Helper Methods
|
|
413
|
+
|
|
414
|
+
private HttpClient CreateClientWithExpiredToken()
|
|
415
|
+
{
|
|
416
|
+
// Implementation depends on your auth setup
|
|
417
|
+
var client = new HttpClient();
|
|
418
|
+
client.DefaultRequestHeaders.Add("Authorization", "Bearer expired.token.here");
|
|
419
|
+
return client;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
private HttpClient CreateClientWithMalformedToken()
|
|
423
|
+
{
|
|
424
|
+
var client = new HttpClient();
|
|
425
|
+
client.DefaultRequestHeaders.Add("Authorization", "Bearer not-a-valid-jwt");
|
|
426
|
+
return client;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
private HttpClient CreateClientWithReadOnlyPermissions()
|
|
430
|
+
{
|
|
431
|
+
// Implementation depends on your auth setup
|
|
432
|
+
return _client; // Placeholder
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
private HttpClient CreateClientWithRegularUserRole()
|
|
436
|
+
{
|
|
437
|
+
// Implementation depends on your auth setup
|
|
438
|
+
return _client; // Placeholder
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
#endregion
|
|
442
|
+
}
|