@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,413 @@
|
|
|
1
|
+
{{!-- SmartStack Controller Test Template --}}
|
|
2
|
+
{{!-- Generates integration tests for API controllers following SmartStack conventions --}}
|
|
3
|
+
|
|
4
|
+
using FluentAssertions;
|
|
5
|
+
using Microsoft.AspNetCore.Mvc.Testing;
|
|
6
|
+
using Microsoft.Extensions.DependencyInjection;
|
|
7
|
+
using Moq;
|
|
8
|
+
using System.Net;
|
|
9
|
+
using System.Net.Http.Json;
|
|
10
|
+
using Xunit;
|
|
11
|
+
using {{namespace}}.Api;
|
|
12
|
+
using {{namespace}}.Api.Controllers;
|
|
13
|
+
using {{namespace}}.Application.DTOs;
|
|
14
|
+
using {{namespace}}.Application.Interfaces;
|
|
15
|
+
using {{namespace}}.Domain.Entities;
|
|
16
|
+
|
|
17
|
+
namespace {{namespace}}.Tests.Integration.Controllers;
|
|
18
|
+
|
|
19
|
+
/// <summary>
|
|
20
|
+
/// Integration tests for <see cref="{{name}}Controller"/>.
|
|
21
|
+
/// Follows SmartStack testing conventions: {Method}_When{Condition}_Should{Result}
|
|
22
|
+
/// </summary>
|
|
23
|
+
public class {{name}}ControllerTests : IClassFixture<WebApplicationFactory<Program>>
|
|
24
|
+
{
|
|
25
|
+
private readonly WebApplicationFactory<Program> _factory;
|
|
26
|
+
private readonly HttpClient _client;
|
|
27
|
+
private readonly Mock<I{{name}}Service> _mockService;
|
|
28
|
+
|
|
29
|
+
public {{name}}ControllerTests(WebApplicationFactory<Program> factory)
|
|
30
|
+
{
|
|
31
|
+
_mockService = new Mock<I{{name}}Service>();
|
|
32
|
+
|
|
33
|
+
_factory = factory.WithWebHostBuilder(builder =>
|
|
34
|
+
{
|
|
35
|
+
builder.ConfigureServices(services =>
|
|
36
|
+
{
|
|
37
|
+
// Remove the real service and add mock
|
|
38
|
+
var descriptor = services.SingleOrDefault(
|
|
39
|
+
d => d.ServiceType == typeof(I{{name}}Service));
|
|
40
|
+
if (descriptor != null)
|
|
41
|
+
services.Remove(descriptor);
|
|
42
|
+
|
|
43
|
+
services.AddScoped(_ => _mockService.Object);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
_client = _factory.CreateClient();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
#region GET Tests
|
|
51
|
+
|
|
52
|
+
[Fact]
|
|
53
|
+
public async Task GetAll_WhenCalled_ShouldReturn200WithList()
|
|
54
|
+
{
|
|
55
|
+
// Arrange
|
|
56
|
+
var items = new List<{{name}}Response>
|
|
57
|
+
{
|
|
58
|
+
new() { Id = Guid.NewGuid(), Code = "TEST-001" },
|
|
59
|
+
new() { Id = Guid.NewGuid(), Code = "TEST-002" }
|
|
60
|
+
};
|
|
61
|
+
_mockService.Setup(x => x.GetAllAsync(It.IsAny<CancellationToken>()))
|
|
62
|
+
.ReturnsAsync(items);
|
|
63
|
+
|
|
64
|
+
// Act
|
|
65
|
+
var response = await _client.GetAsync("/api/{{lowerName}}");
|
|
66
|
+
|
|
67
|
+
// Assert
|
|
68
|
+
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
|
69
|
+
var result = await response.Content.ReadFromJsonAsync<List<{{name}}Response>>();
|
|
70
|
+
result.Should().HaveCount(2);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
[Fact]
|
|
74
|
+
public async Task GetById_WhenExists_ShouldReturn200()
|
|
75
|
+
{
|
|
76
|
+
// Arrange
|
|
77
|
+
var id = Guid.NewGuid();
|
|
78
|
+
var item = new {{name}}Response { Id = id, Code = "TEST-001" };
|
|
79
|
+
_mockService.Setup(x => x.GetByIdAsync(id, It.IsAny<CancellationToken>()))
|
|
80
|
+
.ReturnsAsync(item);
|
|
81
|
+
|
|
82
|
+
// Act
|
|
83
|
+
var response = await _client.GetAsync($"/api/{{lowerName}}/{id}");
|
|
84
|
+
|
|
85
|
+
// Assert
|
|
86
|
+
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
|
87
|
+
var result = await response.Content.ReadFromJsonAsync<{{name}}Response>();
|
|
88
|
+
result!.Id.Should().Be(id);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
[Fact]
|
|
92
|
+
public async Task GetById_WhenNotExists_ShouldReturn404()
|
|
93
|
+
{
|
|
94
|
+
// Arrange
|
|
95
|
+
var id = Guid.NewGuid();
|
|
96
|
+
_mockService.Setup(x => x.GetByIdAsync(id, It.IsAny<CancellationToken>()))
|
|
97
|
+
.ReturnsAsync(({{name}}Response?)null);
|
|
98
|
+
|
|
99
|
+
// Act
|
|
100
|
+
var response = await _client.GetAsync($"/api/{{lowerName}}/{id}");
|
|
101
|
+
|
|
102
|
+
// Assert
|
|
103
|
+
response.StatusCode.Should().Be(HttpStatusCode.NotFound);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
#endregion
|
|
107
|
+
|
|
108
|
+
#region POST Tests
|
|
109
|
+
|
|
110
|
+
[Fact]
|
|
111
|
+
public async Task Create_WhenValidData_ShouldReturn201()
|
|
112
|
+
{
|
|
113
|
+
// Arrange
|
|
114
|
+
var request = new Create{{name}}Request { Code = "NEW-001" };
|
|
115
|
+
var created = new {{name}}Response { Id = Guid.NewGuid(), Code = request.Code };
|
|
116
|
+
_mockService.Setup(x => x.CreateAsync(It.IsAny<Create{{name}}Request>(), It.IsAny<CancellationToken>()))
|
|
117
|
+
.ReturnsAsync(created);
|
|
118
|
+
|
|
119
|
+
// Act
|
|
120
|
+
var response = await _client.PostAsJsonAsync("/api/{{lowerName}}", request);
|
|
121
|
+
|
|
122
|
+
// Assert
|
|
123
|
+
response.StatusCode.Should().Be(HttpStatusCode.Created);
|
|
124
|
+
response.Headers.Location.Should().NotBeNull();
|
|
125
|
+
var result = await response.Content.ReadFromJsonAsync<{{name}}Response>();
|
|
126
|
+
result!.Code.Should().Be(request.Code);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
[Fact]
|
|
130
|
+
public async Task Create_WhenInvalidData_ShouldReturn400()
|
|
131
|
+
{
|
|
132
|
+
// Arrange
|
|
133
|
+
var request = new Create{{name}}Request { Code = "" }; // Invalid
|
|
134
|
+
|
|
135
|
+
// Act
|
|
136
|
+
var response = await _client.PostAsJsonAsync("/api/{{lowerName}}", request);
|
|
137
|
+
|
|
138
|
+
// Assert
|
|
139
|
+
response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
[Fact]
|
|
143
|
+
public async Task Create_WhenDuplicate_ShouldReturn409()
|
|
144
|
+
{
|
|
145
|
+
// Arrange
|
|
146
|
+
var request = new Create{{name}}Request { Code = "EXISTING" };
|
|
147
|
+
_mockService.Setup(x => x.CreateAsync(It.IsAny<Create{{name}}Request>(), It.IsAny<CancellationToken>()))
|
|
148
|
+
.ThrowsAsync(new BusinessException("Code already exists"));
|
|
149
|
+
|
|
150
|
+
// Act
|
|
151
|
+
var response = await _client.PostAsJsonAsync("/api/{{lowerName}}", request);
|
|
152
|
+
|
|
153
|
+
// Assert
|
|
154
|
+
response.StatusCode.Should().Be(HttpStatusCode.Conflict);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
#endregion
|
|
158
|
+
|
|
159
|
+
#region PUT Tests
|
|
160
|
+
|
|
161
|
+
[Fact]
|
|
162
|
+
public async Task Update_WhenValidData_ShouldReturn200()
|
|
163
|
+
{
|
|
164
|
+
// Arrange
|
|
165
|
+
var id = Guid.NewGuid();
|
|
166
|
+
var request = new Update{{name}}Request { /* properties */ };
|
|
167
|
+
var updated = new {{name}}Response { Id = id, Code = "UPDATED" };
|
|
168
|
+
_mockService.Setup(x => x.UpdateAsync(id, It.IsAny<Update{{name}}Request>(), It.IsAny<CancellationToken>()))
|
|
169
|
+
.ReturnsAsync(updated);
|
|
170
|
+
|
|
171
|
+
// Act
|
|
172
|
+
var response = await _client.PutAsJsonAsync($"/api/{{lowerName}}/{id}", request);
|
|
173
|
+
|
|
174
|
+
// Assert
|
|
175
|
+
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
[Fact]
|
|
179
|
+
public async Task Update_WhenNotExists_ShouldReturn404()
|
|
180
|
+
{
|
|
181
|
+
// Arrange
|
|
182
|
+
var id = Guid.NewGuid();
|
|
183
|
+
var request = new Update{{name}}Request();
|
|
184
|
+
_mockService.Setup(x => x.UpdateAsync(id, It.IsAny<Update{{name}}Request>(), It.IsAny<CancellationToken>()))
|
|
185
|
+
.ThrowsAsync(new NotFoundException($"{{name}} {id} not found"));
|
|
186
|
+
|
|
187
|
+
// Act
|
|
188
|
+
var response = await _client.PutAsJsonAsync($"/api/{{lowerName}}/{id}", request);
|
|
189
|
+
|
|
190
|
+
// Assert
|
|
191
|
+
response.StatusCode.Should().Be(HttpStatusCode.NotFound);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
[Fact]
|
|
195
|
+
public async Task Update_WhenConcurrencyConflict_ShouldReturn409()
|
|
196
|
+
{
|
|
197
|
+
// Arrange
|
|
198
|
+
var id = Guid.NewGuid();
|
|
199
|
+
var request = new Update{{name}}Request();
|
|
200
|
+
_mockService.Setup(x => x.UpdateAsync(id, It.IsAny<Update{{name}}Request>(), It.IsAny<CancellationToken>()))
|
|
201
|
+
.ThrowsAsync(new ConcurrencyException("Concurrency conflict"));
|
|
202
|
+
|
|
203
|
+
// Act
|
|
204
|
+
var response = await _client.PutAsJsonAsync($"/api/{{lowerName}}/{id}", request);
|
|
205
|
+
|
|
206
|
+
// Assert
|
|
207
|
+
response.StatusCode.Should().Be(HttpStatusCode.Conflict);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
#endregion
|
|
211
|
+
|
|
212
|
+
#region DELETE Tests
|
|
213
|
+
|
|
214
|
+
[Fact]
|
|
215
|
+
public async Task Delete_WhenExists_ShouldReturn204()
|
|
216
|
+
{
|
|
217
|
+
// Arrange
|
|
218
|
+
var id = Guid.NewGuid();
|
|
219
|
+
_mockService.Setup(x => x.DeleteAsync(id, It.IsAny<CancellationToken>()))
|
|
220
|
+
.Returns(Task.CompletedTask);
|
|
221
|
+
|
|
222
|
+
// Act
|
|
223
|
+
var response = await _client.DeleteAsync($"/api/{{lowerName}}/{id}");
|
|
224
|
+
|
|
225
|
+
// Assert
|
|
226
|
+
response.StatusCode.Should().Be(HttpStatusCode.NoContent);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
[Fact]
|
|
230
|
+
public async Task Delete_WhenNotExists_ShouldReturn404()
|
|
231
|
+
{
|
|
232
|
+
// Arrange
|
|
233
|
+
var id = Guid.NewGuid();
|
|
234
|
+
_mockService.Setup(x => x.DeleteAsync(id, It.IsAny<CancellationToken>()))
|
|
235
|
+
.ThrowsAsync(new NotFoundException($"{{name}} {id} not found"));
|
|
236
|
+
|
|
237
|
+
// Act
|
|
238
|
+
var response = await _client.DeleteAsync($"/api/{{lowerName}}/{id}");
|
|
239
|
+
|
|
240
|
+
// Assert
|
|
241
|
+
response.StatusCode.Should().Be(HttpStatusCode.NotFound);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
#endregion
|
|
245
|
+
|
|
246
|
+
{{#if includeAuthorization}}
|
|
247
|
+
#region Authorization Tests
|
|
248
|
+
|
|
249
|
+
[Fact]
|
|
250
|
+
public async Task GetAll_WhenUnauthorized_ShouldReturn401()
|
|
251
|
+
{
|
|
252
|
+
// Arrange
|
|
253
|
+
var unauthenticatedClient = _factory.CreateClient();
|
|
254
|
+
// Don't add auth header
|
|
255
|
+
|
|
256
|
+
// Act
|
|
257
|
+
var response = await unauthenticatedClient.GetAsync("/api/{{lowerName}}");
|
|
258
|
+
|
|
259
|
+
// Assert
|
|
260
|
+
response.StatusCode.Should().Be(HttpStatusCode.Unauthorized);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
[Fact]
|
|
264
|
+
public async Task Create_WhenForbidden_ShouldReturn403()
|
|
265
|
+
{
|
|
266
|
+
// Arrange - User without create permission
|
|
267
|
+
var request = new Create{{name}}Request { Code = "TEST" };
|
|
268
|
+
|
|
269
|
+
// Act
|
|
270
|
+
var response = await _client.PostAsJsonAsync("/api/{{lowerName}}", request);
|
|
271
|
+
|
|
272
|
+
// Assert
|
|
273
|
+
response.StatusCode.Should().Be(HttpStatusCode.Forbidden);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
[Fact]
|
|
277
|
+
public async Task Delete_WhenNotAdmin_ShouldReturn403()
|
|
278
|
+
{
|
|
279
|
+
// Arrange
|
|
280
|
+
var id = Guid.NewGuid();
|
|
281
|
+
|
|
282
|
+
// Act
|
|
283
|
+
var response = await _client.DeleteAsync($"/api/{{lowerName}}/{id}");
|
|
284
|
+
|
|
285
|
+
// Assert
|
|
286
|
+
response.StatusCode.Should().Be(HttpStatusCode.Forbidden);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
#endregion
|
|
290
|
+
{{/if}}
|
|
291
|
+
|
|
292
|
+
{{#unless isSystemEntity}}
|
|
293
|
+
#region Tenant Isolation Tests
|
|
294
|
+
|
|
295
|
+
[Fact]
|
|
296
|
+
public async Task GetAll_ShouldOnlyReturnCurrentTenantData()
|
|
297
|
+
{
|
|
298
|
+
// Arrange
|
|
299
|
+
var tenantAId = Guid.NewGuid();
|
|
300
|
+
var tenantAItems = new List<{{name}}Response>
|
|
301
|
+
{
|
|
302
|
+
new() { Id = Guid.NewGuid(), TenantId = tenantAId, Code = "A-001" }
|
|
303
|
+
};
|
|
304
|
+
_mockService.Setup(x => x.GetAllAsync(It.IsAny<CancellationToken>()))
|
|
305
|
+
.ReturnsAsync(tenantAItems);
|
|
306
|
+
|
|
307
|
+
// Act
|
|
308
|
+
var response = await _client.GetAsync("/api/{{lowerName}}");
|
|
309
|
+
|
|
310
|
+
// Assert
|
|
311
|
+
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
|
312
|
+
var result = await response.Content.ReadFromJsonAsync<List<{{name}}Response>>();
|
|
313
|
+
result.Should().OnlyContain(x => x.TenantId == tenantAId);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
[Fact]
|
|
317
|
+
public async Task GetById_WhenDifferentTenant_ShouldReturn404()
|
|
318
|
+
{
|
|
319
|
+
// Arrange
|
|
320
|
+
var id = Guid.NewGuid();
|
|
321
|
+
// Entity belongs to different tenant
|
|
322
|
+
_mockService.Setup(x => x.GetByIdAsync(id, It.IsAny<CancellationToken>()))
|
|
323
|
+
.ReturnsAsync(({{name}}Response?)null);
|
|
324
|
+
|
|
325
|
+
// Act
|
|
326
|
+
var response = await _client.GetAsync($"/api/{{lowerName}}/{id}");
|
|
327
|
+
|
|
328
|
+
// Assert
|
|
329
|
+
response.StatusCode.Should().Be(HttpStatusCode.NotFound);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
#endregion
|
|
333
|
+
{{/unless}}
|
|
334
|
+
|
|
335
|
+
#region Validation Tests
|
|
336
|
+
|
|
337
|
+
[Theory]
|
|
338
|
+
[InlineData(null)]
|
|
339
|
+
[InlineData("")]
|
|
340
|
+
[InlineData(" ")]
|
|
341
|
+
public async Task Create_WhenInvalidCode_ShouldReturn400(string? invalidCode)
|
|
342
|
+
{
|
|
343
|
+
// Arrange
|
|
344
|
+
var request = new Create{{name}}Request { Code = invalidCode! };
|
|
345
|
+
|
|
346
|
+
// Act
|
|
347
|
+
var response = await _client.PostAsJsonAsync("/api/{{lowerName}}", request);
|
|
348
|
+
|
|
349
|
+
// Assert
|
|
350
|
+
response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
[Fact]
|
|
354
|
+
public async Task Create_WhenCodeTooLong_ShouldReturn400()
|
|
355
|
+
{
|
|
356
|
+
// Arrange
|
|
357
|
+
var request = new Create{{name}}Request
|
|
358
|
+
{
|
|
359
|
+
Code = new string('A', 256) // Exceeds max length
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
// Act
|
|
363
|
+
var response = await _client.PostAsJsonAsync("/api/{{lowerName}}", request);
|
|
364
|
+
|
|
365
|
+
// Assert
|
|
366
|
+
response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
#endregion
|
|
370
|
+
|
|
371
|
+
#region Pagination Tests
|
|
372
|
+
|
|
373
|
+
[Fact]
|
|
374
|
+
public async Task GetAll_WithPagination_ShouldReturnPagedResults()
|
|
375
|
+
{
|
|
376
|
+
// Arrange
|
|
377
|
+
var items = Enumerable.Range(1, 100)
|
|
378
|
+
.Select(i => new {{name}}Response { Id = Guid.NewGuid(), Code = $"TEST-{i:D3}" })
|
|
379
|
+
.ToList();
|
|
380
|
+
_mockService.Setup(x => x.GetAllAsync(It.IsAny<CancellationToken>()))
|
|
381
|
+
.ReturnsAsync(items.Take(10).ToList());
|
|
382
|
+
|
|
383
|
+
// Act
|
|
384
|
+
var response = await _client.GetAsync("/api/{{lowerName}}?page=1&pageSize=10");
|
|
385
|
+
|
|
386
|
+
// Assert
|
|
387
|
+
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
|
388
|
+
var result = await response.Content.ReadFromJsonAsync<List<{{name}}Response>>();
|
|
389
|
+
result.Should().HaveCount(10);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
#endregion
|
|
393
|
+
|
|
394
|
+
#region Error Response Format Tests
|
|
395
|
+
|
|
396
|
+
[Fact]
|
|
397
|
+
public async Task Create_WhenValidationFails_ShouldReturnProblemDetails()
|
|
398
|
+
{
|
|
399
|
+
// Arrange
|
|
400
|
+
var request = new Create{{name}}Request { Code = "" };
|
|
401
|
+
|
|
402
|
+
// Act
|
|
403
|
+
var response = await _client.PostAsJsonAsync("/api/{{lowerName}}", request);
|
|
404
|
+
|
|
405
|
+
// Assert
|
|
406
|
+
response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
|
|
407
|
+
var problemDetails = await response.Content.ReadFromJsonAsync<ProblemDetails>();
|
|
408
|
+
problemDetails.Should().NotBeNull();
|
|
409
|
+
problemDetails!.Status.Should().Be(400);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
#endregion
|
|
413
|
+
}
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
{{!-- SmartStack Entity Test Template --}}
|
|
2
|
+
{{!-- Generates unit tests for domain entities following SmartStack conventions --}}
|
|
3
|
+
|
|
4
|
+
using FluentAssertions;
|
|
5
|
+
using Xunit;
|
|
6
|
+
using {{namespace}}.Domain.Entities;
|
|
7
|
+
|
|
8
|
+
namespace {{namespace}}.Tests.Unit.Domain;
|
|
9
|
+
|
|
10
|
+
/// <summary>
|
|
11
|
+
/// Unit tests for <see cref="{{name}}"/> entity.
|
|
12
|
+
/// Follows SmartStack testing conventions: {Method}_When{Condition}_Should{Result}
|
|
13
|
+
/// </summary>
|
|
14
|
+
public class {{name}}Tests
|
|
15
|
+
{
|
|
16
|
+
#region Factory Method Tests
|
|
17
|
+
|
|
18
|
+
[Fact]
|
|
19
|
+
public void Create_WhenValidData_ShouldCreateEntity()
|
|
20
|
+
{
|
|
21
|
+
// Arrange
|
|
22
|
+
{{#unless isSystemEntity}}
|
|
23
|
+
var tenantId = Guid.NewGuid();
|
|
24
|
+
{{/unless}}
|
|
25
|
+
var code = "TEST-001";
|
|
26
|
+
var createdBy = "test@example.com";
|
|
27
|
+
|
|
28
|
+
// Act
|
|
29
|
+
var result = {{name}}.Create({{#unless isSystemEntity}}tenantId, {{/unless}}code, createdBy);
|
|
30
|
+
|
|
31
|
+
// Assert
|
|
32
|
+
result.Should().NotBeNull();
|
|
33
|
+
result.Code.Should().Be(code);
|
|
34
|
+
{{#unless isSystemEntity}}
|
|
35
|
+
result.TenantId.Should().Be(tenantId);
|
|
36
|
+
{{/unless}}
|
|
37
|
+
result.CreatedBy.Should().Be(createdBy);
|
|
38
|
+
result.CreatedAt.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(1));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
{{#unless isSystemEntity}}
|
|
42
|
+
[Fact]
|
|
43
|
+
public void Create_WhenEmptyTenantId_ShouldThrowException()
|
|
44
|
+
{
|
|
45
|
+
// Arrange
|
|
46
|
+
var tenantId = Guid.Empty;
|
|
47
|
+
var code = "TEST-001";
|
|
48
|
+
|
|
49
|
+
// Act
|
|
50
|
+
var act = () => {{name}}.Create(tenantId, code);
|
|
51
|
+
|
|
52
|
+
// Assert
|
|
53
|
+
act.Should().Throw<ArgumentException>()
|
|
54
|
+
.WithMessage("*TenantId*");
|
|
55
|
+
}
|
|
56
|
+
{{/unless}}
|
|
57
|
+
|
|
58
|
+
[Theory]
|
|
59
|
+
[InlineData(null)]
|
|
60
|
+
[InlineData("")]
|
|
61
|
+
[InlineData(" ")]
|
|
62
|
+
public void Create_WhenInvalidCode_ShouldThrowException(string? invalidCode)
|
|
63
|
+
{
|
|
64
|
+
// Arrange
|
|
65
|
+
{{#unless isSystemEntity}}
|
|
66
|
+
var tenantId = Guid.NewGuid();
|
|
67
|
+
{{/unless}}
|
|
68
|
+
|
|
69
|
+
// Act
|
|
70
|
+
var act = () => {{name}}.Create({{#unless isSystemEntity}}tenantId, {{/unless}}invalidCode!);
|
|
71
|
+
|
|
72
|
+
// Assert
|
|
73
|
+
act.Should().Throw<ArgumentException>();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
#endregion
|
|
77
|
+
|
|
78
|
+
#region Update Tests
|
|
79
|
+
|
|
80
|
+
[Fact]
|
|
81
|
+
public void Update_WhenValidData_ShouldUpdateEntity()
|
|
82
|
+
{
|
|
83
|
+
// Arrange
|
|
84
|
+
var entity = CreateValid{{name}}();
|
|
85
|
+
var updatedBy = "updater@example.com";
|
|
86
|
+
|
|
87
|
+
// Act
|
|
88
|
+
entity.Update(updatedBy);
|
|
89
|
+
|
|
90
|
+
// Assert
|
|
91
|
+
entity.UpdatedBy.Should().Be(updatedBy);
|
|
92
|
+
entity.UpdatedAt.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(1));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
#endregion
|
|
96
|
+
|
|
97
|
+
{{#if includeSoftDelete}}
|
|
98
|
+
#region Soft Delete Tests
|
|
99
|
+
|
|
100
|
+
[Fact]
|
|
101
|
+
public void SoftDelete_WhenNotDeleted_ShouldMarkAsDeleted()
|
|
102
|
+
{
|
|
103
|
+
// Arrange
|
|
104
|
+
var entity = CreateValid{{name}}();
|
|
105
|
+
var deletedBy = "deleter@example.com";
|
|
106
|
+
|
|
107
|
+
// Act
|
|
108
|
+
entity.SoftDelete(deletedBy);
|
|
109
|
+
|
|
110
|
+
// Assert
|
|
111
|
+
entity.IsDeleted.Should().BeTrue();
|
|
112
|
+
entity.DeletedBy.Should().Be(deletedBy);
|
|
113
|
+
entity.DeletedAt.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(1));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
[Fact]
|
|
117
|
+
public void SoftDelete_WhenAlreadyDeleted_ShouldThrowException()
|
|
118
|
+
{
|
|
119
|
+
// Arrange
|
|
120
|
+
var entity = CreateValid{{name}}();
|
|
121
|
+
entity.SoftDelete("first@example.com");
|
|
122
|
+
|
|
123
|
+
// Act
|
|
124
|
+
var act = () => entity.SoftDelete("second@example.com");
|
|
125
|
+
|
|
126
|
+
// Assert
|
|
127
|
+
act.Should().Throw<InvalidOperationException>()
|
|
128
|
+
.WithMessage("*already deleted*");
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
[Fact]
|
|
132
|
+
public void Restore_WhenDeleted_ShouldRestoreEntity()
|
|
133
|
+
{
|
|
134
|
+
// Arrange
|
|
135
|
+
var entity = CreateValid{{name}}();
|
|
136
|
+
entity.SoftDelete("deleter@example.com");
|
|
137
|
+
var restoredBy = "restorer@example.com";
|
|
138
|
+
|
|
139
|
+
// Act
|
|
140
|
+
entity.Restore(restoredBy);
|
|
141
|
+
|
|
142
|
+
// Assert
|
|
143
|
+
entity.IsDeleted.Should().BeFalse();
|
|
144
|
+
entity.DeletedBy.Should().BeNull();
|
|
145
|
+
entity.DeletedAt.Should().BeNull();
|
|
146
|
+
entity.UpdatedBy.Should().Be(restoredBy);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
#endregion
|
|
150
|
+
{{/if}}
|
|
151
|
+
|
|
152
|
+
{{#if includeAudit}}
|
|
153
|
+
#region Audit Trail Tests
|
|
154
|
+
|
|
155
|
+
[Fact]
|
|
156
|
+
public void Create_ShouldSetAuditFields()
|
|
157
|
+
{
|
|
158
|
+
// Arrange
|
|
159
|
+
{{#unless isSystemEntity}}
|
|
160
|
+
var tenantId = Guid.NewGuid();
|
|
161
|
+
{{/unless}}
|
|
162
|
+
var createdBy = "creator@example.com";
|
|
163
|
+
|
|
164
|
+
// Act
|
|
165
|
+
var entity = {{name}}.Create({{#unless isSystemEntity}}tenantId, {{/unless}}"TEST", createdBy);
|
|
166
|
+
|
|
167
|
+
// Assert
|
|
168
|
+
entity.CreatedAt.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(1));
|
|
169
|
+
entity.CreatedBy.Should().Be(createdBy);
|
|
170
|
+
entity.UpdatedAt.Should().BeNull();
|
|
171
|
+
entity.UpdatedBy.Should().BeNull();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
[Fact]
|
|
175
|
+
public void Update_ShouldSetUpdateAuditFields()
|
|
176
|
+
{
|
|
177
|
+
// Arrange
|
|
178
|
+
var entity = CreateValid{{name}}();
|
|
179
|
+
var originalCreatedAt = entity.CreatedAt;
|
|
180
|
+
var originalCreatedBy = entity.CreatedBy;
|
|
181
|
+
var updatedBy = "updater@example.com";
|
|
182
|
+
|
|
183
|
+
// Act
|
|
184
|
+
entity.Update(updatedBy);
|
|
185
|
+
|
|
186
|
+
// Assert
|
|
187
|
+
entity.CreatedAt.Should().Be(originalCreatedAt);
|
|
188
|
+
entity.CreatedBy.Should().Be(originalCreatedBy);
|
|
189
|
+
entity.UpdatedAt.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(1));
|
|
190
|
+
entity.UpdatedBy.Should().Be(updatedBy);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
#endregion
|
|
194
|
+
{{/if}}
|
|
195
|
+
|
|
196
|
+
{{#unless isSystemEntity}}
|
|
197
|
+
#region Tenant Isolation Tests
|
|
198
|
+
|
|
199
|
+
[Fact]
|
|
200
|
+
public void TenantId_ShouldBeImmutable()
|
|
201
|
+
{
|
|
202
|
+
// Arrange
|
|
203
|
+
var tenantId = Guid.NewGuid();
|
|
204
|
+
var entity = {{name}}.Create(tenantId, "TEST");
|
|
205
|
+
|
|
206
|
+
// Assert - TenantId should not have a public setter
|
|
207
|
+
typeof({{name}}).GetProperty("TenantId")!
|
|
208
|
+
.SetMethod.Should().BeNull("TenantId should be immutable");
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
[Fact]
|
|
212
|
+
public void Create_WhenValidTenantId_ShouldStoreTenantId()
|
|
213
|
+
{
|
|
214
|
+
// Arrange
|
|
215
|
+
var tenantId = Guid.NewGuid();
|
|
216
|
+
|
|
217
|
+
// Act
|
|
218
|
+
var entity = {{name}}.Create(tenantId, "TEST");
|
|
219
|
+
|
|
220
|
+
// Assert
|
|
221
|
+
entity.TenantId.Should().Be(tenantId);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
#endregion
|
|
225
|
+
{{/unless}}
|
|
226
|
+
|
|
227
|
+
#region Helper Methods
|
|
228
|
+
|
|
229
|
+
private static {{name}} CreateValid{{name}}()
|
|
230
|
+
{
|
|
231
|
+
{{#if isSystemEntity}}
|
|
232
|
+
return {{name}}.Create("TEST-001", "test@example.com");
|
|
233
|
+
{{else}}
|
|
234
|
+
return {{name}}.Create(Guid.NewGuid(), "TEST-001", "test@example.com");
|
|
235
|
+
{{/if}}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
#endregion
|
|
239
|
+
}
|