@atlashub/smartstack-cli 1.36.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.
Files changed (103) hide show
  1. package/config/mcp-defaults.json +62 -0
  2. package/dist/index.js +57 -4
  3. package/dist/index.js.map +1 -1
  4. package/dist/mcp-entry.mjs +16984 -0
  5. package/dist/mcp-entry.mjs.map +1 -0
  6. package/package.json +14 -5
  7. package/templates/agents/gitflow/start.md +5 -4
  8. package/templates/agents/mcp-healthcheck.md +15 -13
  9. package/templates/mcp-scaffolding/component.tsx.hbs +298 -0
  10. package/templates/mcp-scaffolding/controller.cs.hbs +184 -0
  11. package/templates/mcp-scaffolding/entity-extension.cs.hbs +231 -0
  12. package/templates/mcp-scaffolding/frontend/api-client.ts.hbs +116 -0
  13. package/templates/mcp-scaffolding/frontend/nav-routes.ts.hbs +133 -0
  14. package/templates/mcp-scaffolding/frontend/routes.tsx.hbs +134 -0
  15. package/templates/mcp-scaffolding/migrations/seed-roles.cs.hbs +261 -0
  16. package/templates/mcp-scaffolding/service-extension.cs.hbs +53 -0
  17. package/templates/mcp-scaffolding/tests/controller.test.cs.hbs +413 -0
  18. package/templates/mcp-scaffolding/tests/entity.test.cs.hbs +239 -0
  19. package/templates/mcp-scaffolding/tests/repository.test.cs.hbs +441 -0
  20. package/templates/mcp-scaffolding/tests/security.test.cs.hbs +442 -0
  21. package/templates/mcp-scaffolding/tests/service.test.cs.hbs +390 -0
  22. package/templates/mcp-scaffolding/tests/validator.test.cs.hbs +428 -0
  23. package/templates/ralph/README.md +3 -3
  24. package/templates/ralph/ralph.config.yaml +2 -2
  25. package/templates/skills/admin/SKILL.md +42 -0
  26. package/templates/skills/application/steps/step-01-navigation.md +226 -43
  27. package/templates/skills/application/steps/step-03-roles.md +160 -38
  28. package/templates/skills/application/steps/step-04-backend.md +109 -2
  29. package/templates/skills/application/templates-seed.md +200 -1
  30. package/templates/skills/business-analyse/_shared.md +24 -1
  31. package/templates/skills/business-analyse/questionnaire/01-context.md +4 -4
  32. package/templates/skills/business-analyse/questionnaire/02-stakeholders.md +3 -3
  33. package/templates/skills/business-analyse/questionnaire/03-scope.md +4 -4
  34. package/templates/skills/business-analyse/questionnaire/04-data.md +7 -7
  35. package/templates/skills/business-analyse/questionnaire/05-integrations.md +1 -1
  36. package/templates/skills/business-analyse/questionnaire/06-security.md +3 -3
  37. package/templates/skills/business-analyse/questionnaire/07-ui.md +1 -1
  38. package/templates/skills/business-analyse/questionnaire/08-performance.md +3 -3
  39. package/templates/skills/business-analyse/questionnaire/09-constraints.md +4 -4
  40. package/templates/skills/business-analyse/questionnaire/10-documentation.md +2 -2
  41. package/templates/skills/business-analyse/questionnaire/11-data-lifecycle.md +2 -2
  42. package/templates/skills/business-analyse/questionnaire/12-migration.md +1 -1
  43. package/templates/skills/business-analyse/questionnaire/13-cross-module.md +2 -2
  44. package/templates/skills/business-analyse/steps/step-01-discover.md +50 -25
  45. package/templates/skills/business-analyse/steps/step-05-handoff.md +133 -34
  46. package/templates/skills/cc-agent/SKILL.md +129 -0
  47. package/templates/skills/cc-agent/references/agent-frontmatter.md +213 -0
  48. package/templates/skills/cc-agent/references/permission-modes.md +102 -0
  49. package/templates/skills/cc-agent/references/tools-reference.md +144 -0
  50. package/templates/skills/cc-agent/steps/step-00-init.md +134 -0
  51. package/templates/skills/cc-agent/steps/step-01-design.md +186 -0
  52. package/templates/skills/cc-agent/steps/step-02-generate.md +204 -0
  53. package/templates/skills/cc-agent/steps/step-03-validate.md +130 -0
  54. package/templates/skills/cc-agent/templates/agent-categorized.md +67 -0
  55. package/templates/skills/cc-agent/templates/agent-standalone.md +56 -0
  56. package/templates/skills/cc-agent/templates/agent-with-skills.md +94 -0
  57. package/templates/skills/cc-audit/SKILL.md +108 -0
  58. package/templates/skills/cc-audit/references/agent-checklist.md +91 -0
  59. package/templates/skills/cc-audit/references/hook-checklist.md +110 -0
  60. package/templates/skills/cc-audit/references/skill-checklist.md +70 -0
  61. package/templates/skills/cc-audit/steps/step-00-init.md +98 -0
  62. package/templates/skills/cc-audit/steps/step-01-scan.md +142 -0
  63. package/templates/skills/cc-audit/steps/step-02-analyze.md +158 -0
  64. package/templates/skills/cc-audit/steps/step-03-report.md +142 -0
  65. package/templates/skills/cc-skill/SKILL.md +134 -0
  66. package/templates/skills/cc-skill/references/best-practices.md +167 -0
  67. package/templates/skills/cc-skill/references/frontmatter-reference.md +182 -0
  68. package/templates/skills/cc-skill/references/skill-patterns.md +199 -0
  69. package/templates/skills/cc-skill/steps/step-00-init.md +119 -0
  70. package/templates/skills/cc-skill/steps/step-01-design.md +199 -0
  71. package/templates/skills/cc-skill/steps/step-02-generate.md +145 -0
  72. package/templates/skills/cc-skill/steps/step-03-steps.md +151 -0
  73. package/templates/skills/cc-skill/steps/step-04-validate.md +124 -0
  74. package/templates/skills/cc-skill/templates/skill-forked.md +85 -0
  75. package/templates/skills/cc-skill/templates/skill-progressive.md +102 -0
  76. package/templates/skills/cc-skill/templates/skill-simple.md +75 -0
  77. package/templates/skills/cc-skill/templates/step-template.md +82 -0
  78. package/templates/skills/check-version/SKILL.md +6 -0
  79. package/templates/skills/debug/SKILL.md +4 -0
  80. package/templates/skills/documentation/SKILL.md +1 -0
  81. package/templates/skills/efcore/SKILL.md +5 -0
  82. package/templates/skills/efcore/steps/db/step-deploy.md +26 -5
  83. package/templates/skills/efcore/steps/shared/step-00-init.md +21 -7
  84. package/templates/skills/explore/SKILL.md +28 -32
  85. package/templates/skills/feature-full/SKILL.md +1 -0
  86. package/templates/skills/gitflow/SKILL.md +8 -0
  87. package/templates/skills/gitflow/steps/step-start.md +45 -10
  88. package/templates/skills/mcp/SKILL.md +38 -18
  89. package/templates/skills/quick-search/SKILL.md +8 -1
  90. package/templates/skills/ralph-loop/SKILL.md +1 -1
  91. package/templates/skills/ralph-loop/steps/step-00-init.md +8 -68
  92. package/templates/skills/ralph-loop/steps/step-04-check.md +1 -1
  93. package/templates/skills/refactor/SKILL.md +1 -0
  94. package/templates/skills/review-code/SKILL.md +7 -1
  95. package/templates/skills/ui-components/SKILL.md +31 -438
  96. package/templates/skills/ui-components/accessibility.md +170 -0
  97. package/templates/skills/ui-components/patterns/data-table.md +39 -0
  98. package/templates/skills/ui-components/patterns/entity-card.md +77 -0
  99. package/templates/skills/ui-components/patterns/grid-layout.md +91 -0
  100. package/templates/skills/ui-components/patterns/kanban.md +43 -0
  101. package/templates/skills/ui-components/style-guide.md +86 -0
  102. package/templates/skills/utils/SKILL.md +1 -0
  103. 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
+ }