@atlashub/smartstack-cli 1.37.0 → 2.1.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/.documentation/agents.html +147 -40
- package/.documentation/apex.html +1 -1
- package/.documentation/business-analyse.html +3 -3
- package/.documentation/cli-commands.html +2 -2
- package/.documentation/commands.html +14 -14
- package/.documentation/efcore.html +14 -14
- package/.documentation/gitflow.html +12 -12
- package/.documentation/hooks.html +41 -3
- package/.documentation/index.html +1 -1
- package/.documentation/init.html +2 -2
- package/.documentation/installation.html +11 -11
- package/.documentation/js/app.js +1 -1
- package/.documentation/ralph-loop.html +1 -1
- package/.documentation/test-web.html +4 -4
- package/config/mcp-defaults.json +62 -0
- package/dist/index.js +58 -5
- package/dist/index.js.map +1 -1
- package/dist/mcp-entry.mjs +70010 -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 +79 -15
- 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-03-specify.md +63 -0
- package/templates/skills/business-analyse/steps/step-04-validate.md +23 -1
- package/templates/skills/business-analyse/steps/step-05-handoff.md +248 -66
- package/templates/skills/business-analyse/templates/tpl-handoff.md +99 -23
- 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/controller/templates.md +82 -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/references/zero-downtime-patterns.md +227 -0
- package/templates/skills/efcore/steps/db/step-deploy.md +26 -5
- package/templates/skills/efcore/steps/migration/step-03-validate.md +19 -0
- 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 +11 -3
- package/templates/skills/review-code/references/owasp-api-top10.md +243 -0
- package/templates/skills/review-code/references/security-checklist.md +86 -1
- package/templates/skills/review-code/references/smartstack-conventions.md +166 -0
- 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
- package/templates/skills/workflow/SKILL.md +27 -0
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
{{!-- SmartStack Service Test Template --}}
|
|
2
|
+
{{!-- Generates unit tests for application services following SmartStack conventions --}}
|
|
3
|
+
|
|
4
|
+
using FluentAssertions;
|
|
5
|
+
using Moq;
|
|
6
|
+
using Xunit;
|
|
7
|
+
using {{namespace}}.Application.Services;
|
|
8
|
+
using {{namespace}}.Application.Interfaces;
|
|
9
|
+
using {{namespace}}.Domain.Entities;
|
|
10
|
+
using {{namespace}}.Domain.Interfaces;
|
|
11
|
+
|
|
12
|
+
namespace {{namespace}}.Tests.Unit.Services;
|
|
13
|
+
|
|
14
|
+
/// <summary>
|
|
15
|
+
/// Unit tests for <see cref="{{name}}Service"/>.
|
|
16
|
+
/// Follows SmartStack testing conventions: {Method}_When{Condition}_Should{Result}
|
|
17
|
+
/// </summary>
|
|
18
|
+
public class {{name}}ServiceTests
|
|
19
|
+
{
|
|
20
|
+
private readonly Mock<I{{name}}Repository> _mockRepository;
|
|
21
|
+
private readonly Mock<IUnitOfWork> _mockUnitOfWork;
|
|
22
|
+
{{#unless isSystemEntity}}
|
|
23
|
+
private readonly Mock<ITenantContext> _mockTenantContext;
|
|
24
|
+
{{/unless}}
|
|
25
|
+
private readonly {{name}}Service _sut;
|
|
26
|
+
|
|
27
|
+
public {{name}}ServiceTests()
|
|
28
|
+
{
|
|
29
|
+
_mockRepository = new Mock<I{{name}}Repository>();
|
|
30
|
+
_mockUnitOfWork = new Mock<IUnitOfWork>();
|
|
31
|
+
{{#unless isSystemEntity}}
|
|
32
|
+
_mockTenantContext = new Mock<ITenantContext>();
|
|
33
|
+
_mockTenantContext.Setup(x => x.TenantId).Returns(Guid.NewGuid());
|
|
34
|
+
{{/unless}}
|
|
35
|
+
|
|
36
|
+
_sut = new {{name}}Service(
|
|
37
|
+
_mockRepository.Object,
|
|
38
|
+
_mockUnitOfWork.Object{{#unless isSystemEntity}},
|
|
39
|
+
_mockTenantContext.Object{{/unless}}
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
#region GetById Tests
|
|
44
|
+
|
|
45
|
+
[Fact]
|
|
46
|
+
public async Task GetByIdAsync_WhenExists_ShouldReturnEntity()
|
|
47
|
+
{
|
|
48
|
+
// Arrange
|
|
49
|
+
var id = Guid.NewGuid();
|
|
50
|
+
var entity = CreateValid{{name}}(id);
|
|
51
|
+
_mockRepository.Setup(x => x.GetByIdAsync(id, It.IsAny<CancellationToken>()))
|
|
52
|
+
.ReturnsAsync(entity);
|
|
53
|
+
|
|
54
|
+
// Act
|
|
55
|
+
var result = await _sut.GetByIdAsync(id);
|
|
56
|
+
|
|
57
|
+
// Assert
|
|
58
|
+
result.Should().NotBeNull();
|
|
59
|
+
result!.Id.Should().Be(id);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
[Fact]
|
|
63
|
+
public async Task GetByIdAsync_WhenNotExists_ShouldReturnNull()
|
|
64
|
+
{
|
|
65
|
+
// Arrange
|
|
66
|
+
var id = Guid.NewGuid();
|
|
67
|
+
_mockRepository.Setup(x => x.GetByIdAsync(id, It.IsAny<CancellationToken>()))
|
|
68
|
+
.ReturnsAsync(({{name}}?)null);
|
|
69
|
+
|
|
70
|
+
// Act
|
|
71
|
+
var result = await _sut.GetByIdAsync(id);
|
|
72
|
+
|
|
73
|
+
// Assert
|
|
74
|
+
result.Should().BeNull();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
#endregion
|
|
78
|
+
|
|
79
|
+
#region GetAll Tests
|
|
80
|
+
|
|
81
|
+
[Fact]
|
|
82
|
+
public async Task GetAllAsync_WhenEntitiesExist_ShouldReturnAll()
|
|
83
|
+
{
|
|
84
|
+
// Arrange
|
|
85
|
+
var entities = new List<{{name}}>
|
|
86
|
+
{
|
|
87
|
+
CreateValid{{name}}(),
|
|
88
|
+
CreateValid{{name}}()
|
|
89
|
+
};
|
|
90
|
+
_mockRepository.Setup(x => x.GetAllAsync(It.IsAny<CancellationToken>()))
|
|
91
|
+
.ReturnsAsync(entities);
|
|
92
|
+
|
|
93
|
+
// Act
|
|
94
|
+
var result = await _sut.GetAllAsync();
|
|
95
|
+
|
|
96
|
+
// Assert
|
|
97
|
+
result.Should().HaveCount(2);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
[Fact]
|
|
101
|
+
public async Task GetAllAsync_WhenNoEntities_ShouldReturnEmptyList()
|
|
102
|
+
{
|
|
103
|
+
// Arrange
|
|
104
|
+
_mockRepository.Setup(x => x.GetAllAsync(It.IsAny<CancellationToken>()))
|
|
105
|
+
.ReturnsAsync(new List<{{name}}>());
|
|
106
|
+
|
|
107
|
+
// Act
|
|
108
|
+
var result = await _sut.GetAllAsync();
|
|
109
|
+
|
|
110
|
+
// Assert
|
|
111
|
+
result.Should().BeEmpty();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
#endregion
|
|
115
|
+
|
|
116
|
+
#region Create Tests
|
|
117
|
+
|
|
118
|
+
[Fact]
|
|
119
|
+
public async Task CreateAsync_WhenValidData_ShouldCreateAndReturnEntity()
|
|
120
|
+
{
|
|
121
|
+
// Arrange
|
|
122
|
+
var request = new Create{{name}}Request
|
|
123
|
+
{
|
|
124
|
+
Code = "NEW-001",
|
|
125
|
+
// Add other required properties
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
_mockRepository.Setup(x => x.AddAsync(It.IsAny<{{name}}>(), It.IsAny<CancellationToken>()))
|
|
129
|
+
.Returns(Task.CompletedTask);
|
|
130
|
+
_mockUnitOfWork.Setup(x => x.SaveChangesAsync(It.IsAny<CancellationToken>()))
|
|
131
|
+
.ReturnsAsync(1);
|
|
132
|
+
|
|
133
|
+
// Act
|
|
134
|
+
var result = await _sut.CreateAsync(request);
|
|
135
|
+
|
|
136
|
+
// Assert
|
|
137
|
+
result.Should().NotBeNull();
|
|
138
|
+
result.Code.Should().Be(request.Code);
|
|
139
|
+
_mockRepository.Verify(x => x.AddAsync(It.IsAny<{{name}}>(), It.IsAny<CancellationToken>()), Times.Once);
|
|
140
|
+
_mockUnitOfWork.Verify(x => x.SaveChangesAsync(It.IsAny<CancellationToken>()), Times.Once);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
[Fact]
|
|
144
|
+
public async Task CreateAsync_WhenDuplicateCode_ShouldThrowException()
|
|
145
|
+
{
|
|
146
|
+
// Arrange
|
|
147
|
+
var request = new Create{{name}}Request { Code = "EXISTING-001" };
|
|
148
|
+
_mockRepository.Setup(x => x.ExistsByCodeAsync(request.Code, It.IsAny<CancellationToken>()))
|
|
149
|
+
.ReturnsAsync(true);
|
|
150
|
+
|
|
151
|
+
// Act
|
|
152
|
+
var act = () => _sut.CreateAsync(request);
|
|
153
|
+
|
|
154
|
+
// Assert
|
|
155
|
+
await act.Should().ThrowAsync<BusinessException>()
|
|
156
|
+
.WithMessage("*already exists*");
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
#endregion
|
|
160
|
+
|
|
161
|
+
#region Update Tests
|
|
162
|
+
|
|
163
|
+
[Fact]
|
|
164
|
+
public async Task UpdateAsync_WhenExists_ShouldUpdateEntity()
|
|
165
|
+
{
|
|
166
|
+
// Arrange
|
|
167
|
+
var id = Guid.NewGuid();
|
|
168
|
+
var entity = CreateValid{{name}}(id);
|
|
169
|
+
var request = new Update{{name}}Request
|
|
170
|
+
{
|
|
171
|
+
// Add properties to update
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
_mockRepository.Setup(x => x.GetByIdAsync(id, It.IsAny<CancellationToken>()))
|
|
175
|
+
.ReturnsAsync(entity);
|
|
176
|
+
_mockUnitOfWork.Setup(x => x.SaveChangesAsync(It.IsAny<CancellationToken>()))
|
|
177
|
+
.ReturnsAsync(1);
|
|
178
|
+
|
|
179
|
+
// Act
|
|
180
|
+
var result = await _sut.UpdateAsync(id, request);
|
|
181
|
+
|
|
182
|
+
// Assert
|
|
183
|
+
result.Should().NotBeNull();
|
|
184
|
+
_mockUnitOfWork.Verify(x => x.SaveChangesAsync(It.IsAny<CancellationToken>()), Times.Once);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
[Fact]
|
|
188
|
+
public async Task UpdateAsync_WhenNotExists_ShouldThrowException()
|
|
189
|
+
{
|
|
190
|
+
// Arrange
|
|
191
|
+
var id = Guid.NewGuid();
|
|
192
|
+
var request = new Update{{name}}Request();
|
|
193
|
+
_mockRepository.Setup(x => x.GetByIdAsync(id, It.IsAny<CancellationToken>()))
|
|
194
|
+
.ReturnsAsync(({{name}}?)null);
|
|
195
|
+
|
|
196
|
+
// Act
|
|
197
|
+
var act = () => _sut.UpdateAsync(id, request);
|
|
198
|
+
|
|
199
|
+
// Assert
|
|
200
|
+
await act.Should().ThrowAsync<NotFoundException>();
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
#endregion
|
|
204
|
+
|
|
205
|
+
#region Delete Tests
|
|
206
|
+
|
|
207
|
+
{{#if includeSoftDelete}}
|
|
208
|
+
[Fact]
|
|
209
|
+
public async Task DeleteAsync_WhenExists_ShouldSoftDelete()
|
|
210
|
+
{
|
|
211
|
+
// Arrange
|
|
212
|
+
var id = Guid.NewGuid();
|
|
213
|
+
var entity = CreateValid{{name}}(id);
|
|
214
|
+
_mockRepository.Setup(x => x.GetByIdAsync(id, It.IsAny<CancellationToken>()))
|
|
215
|
+
.ReturnsAsync(entity);
|
|
216
|
+
_mockUnitOfWork.Setup(x => x.SaveChangesAsync(It.IsAny<CancellationToken>()))
|
|
217
|
+
.ReturnsAsync(1);
|
|
218
|
+
|
|
219
|
+
// Act
|
|
220
|
+
await _sut.DeleteAsync(id);
|
|
221
|
+
|
|
222
|
+
// Assert
|
|
223
|
+
entity.IsDeleted.Should().BeTrue();
|
|
224
|
+
_mockUnitOfWork.Verify(x => x.SaveChangesAsync(It.IsAny<CancellationToken>()), Times.Once);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
[Fact]
|
|
228
|
+
public async Task DeleteAsync_WhenAlreadyDeleted_ShouldThrowException()
|
|
229
|
+
{
|
|
230
|
+
// Arrange
|
|
231
|
+
var id = Guid.NewGuid();
|
|
232
|
+
var entity = CreateValid{{name}}(id);
|
|
233
|
+
entity.SoftDelete("previous@example.com");
|
|
234
|
+
_mockRepository.Setup(x => x.GetByIdAsync(id, It.IsAny<CancellationToken>()))
|
|
235
|
+
.ReturnsAsync(entity);
|
|
236
|
+
|
|
237
|
+
// Act
|
|
238
|
+
var act = () => _sut.DeleteAsync(id);
|
|
239
|
+
|
|
240
|
+
// Assert
|
|
241
|
+
await act.Should().ThrowAsync<BusinessException>()
|
|
242
|
+
.WithMessage("*already deleted*");
|
|
243
|
+
}
|
|
244
|
+
{{else}}
|
|
245
|
+
[Fact]
|
|
246
|
+
public async Task DeleteAsync_WhenExists_ShouldDelete()
|
|
247
|
+
{
|
|
248
|
+
// Arrange
|
|
249
|
+
var id = Guid.NewGuid();
|
|
250
|
+
var entity = CreateValid{{name}}(id);
|
|
251
|
+
_mockRepository.Setup(x => x.GetByIdAsync(id, It.IsAny<CancellationToken>()))
|
|
252
|
+
.ReturnsAsync(entity);
|
|
253
|
+
_mockUnitOfWork.Setup(x => x.SaveChangesAsync(It.IsAny<CancellationToken>()))
|
|
254
|
+
.ReturnsAsync(1);
|
|
255
|
+
|
|
256
|
+
// Act
|
|
257
|
+
await _sut.DeleteAsync(id);
|
|
258
|
+
|
|
259
|
+
// Assert
|
|
260
|
+
_mockRepository.Verify(x => x.Remove(entity), Times.Once);
|
|
261
|
+
_mockUnitOfWork.Verify(x => x.SaveChangesAsync(It.IsAny<CancellationToken>()), Times.Once);
|
|
262
|
+
}
|
|
263
|
+
{{/if}}
|
|
264
|
+
|
|
265
|
+
[Fact]
|
|
266
|
+
public async Task DeleteAsync_WhenNotExists_ShouldThrowException()
|
|
267
|
+
{
|
|
268
|
+
// Arrange
|
|
269
|
+
var id = Guid.NewGuid();
|
|
270
|
+
_mockRepository.Setup(x => x.GetByIdAsync(id, It.IsAny<CancellationToken>()))
|
|
271
|
+
.ReturnsAsync(({{name}}?)null);
|
|
272
|
+
|
|
273
|
+
// Act
|
|
274
|
+
var act = () => _sut.DeleteAsync(id);
|
|
275
|
+
|
|
276
|
+
// Assert
|
|
277
|
+
await act.Should().ThrowAsync<NotFoundException>();
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
#endregion
|
|
281
|
+
|
|
282
|
+
{{#unless isSystemEntity}}
|
|
283
|
+
#region Tenant Isolation Tests
|
|
284
|
+
|
|
285
|
+
[Fact]
|
|
286
|
+
public async Task CreateAsync_ShouldUseCurrentTenantId()
|
|
287
|
+
{
|
|
288
|
+
// Arrange
|
|
289
|
+
var expectedTenantId = Guid.NewGuid();
|
|
290
|
+
_mockTenantContext.Setup(x => x.TenantId).Returns(expectedTenantId);
|
|
291
|
+
|
|
292
|
+
var request = new Create{{name}}Request { Code = "TEST" };
|
|
293
|
+
{{name}}? capturedEntity = null;
|
|
294
|
+
_mockRepository.Setup(x => x.AddAsync(It.IsAny<{{name}}>(), It.IsAny<CancellationToken>()))
|
|
295
|
+
.Callback<{{name}}, CancellationToken>((e, _) => capturedEntity = e)
|
|
296
|
+
.Returns(Task.CompletedTask);
|
|
297
|
+
_mockUnitOfWork.Setup(x => x.SaveChangesAsync(It.IsAny<CancellationToken>()))
|
|
298
|
+
.ReturnsAsync(1);
|
|
299
|
+
|
|
300
|
+
// Act
|
|
301
|
+
await _sut.CreateAsync(request);
|
|
302
|
+
|
|
303
|
+
// Assert
|
|
304
|
+
capturedEntity.Should().NotBeNull();
|
|
305
|
+
capturedEntity!.TenantId.Should().Be(expectedTenantId);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
[Fact]
|
|
309
|
+
public async Task GetByIdAsync_ShouldRespectTenantScope()
|
|
310
|
+
{
|
|
311
|
+
// Arrange
|
|
312
|
+
var id = Guid.NewGuid();
|
|
313
|
+
var tenantId = Guid.NewGuid();
|
|
314
|
+
_mockTenantContext.Setup(x => x.TenantId).Returns(tenantId);
|
|
315
|
+
|
|
316
|
+
// Act
|
|
317
|
+
await _sut.GetByIdAsync(id);
|
|
318
|
+
|
|
319
|
+
// Assert
|
|
320
|
+
_mockRepository.Verify(
|
|
321
|
+
x => x.GetByIdAsync(id, It.IsAny<CancellationToken>()),
|
|
322
|
+
Times.Once
|
|
323
|
+
);
|
|
324
|
+
// Repository should be configured to filter by tenant
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
#endregion
|
|
328
|
+
{{/unless}}
|
|
329
|
+
|
|
330
|
+
#region Error Handling Tests
|
|
331
|
+
|
|
332
|
+
[Fact]
|
|
333
|
+
public async Task CreateAsync_WhenRepositoryFails_ShouldThrowException()
|
|
334
|
+
{
|
|
335
|
+
// Arrange
|
|
336
|
+
var request = new Create{{name}}Request { Code = "TEST" };
|
|
337
|
+
_mockRepository.Setup(x => x.AddAsync(It.IsAny<{{name}}>(), It.IsAny<CancellationToken>()))
|
|
338
|
+
.ThrowsAsync(new Exception("Database error"));
|
|
339
|
+
|
|
340
|
+
// Act
|
|
341
|
+
var act = () => _sut.CreateAsync(request);
|
|
342
|
+
|
|
343
|
+
// Assert
|
|
344
|
+
await act.Should().ThrowAsync<Exception>()
|
|
345
|
+
.WithMessage("*Database error*");
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
[Fact]
|
|
349
|
+
public async Task UpdateAsync_WhenConcurrencyConflict_ShouldThrowException()
|
|
350
|
+
{
|
|
351
|
+
// Arrange
|
|
352
|
+
var id = Guid.NewGuid();
|
|
353
|
+
var entity = CreateValid{{name}}(id);
|
|
354
|
+
var request = new Update{{name}}Request();
|
|
355
|
+
|
|
356
|
+
_mockRepository.Setup(x => x.GetByIdAsync(id, It.IsAny<CancellationToken>()))
|
|
357
|
+
.ReturnsAsync(entity);
|
|
358
|
+
_mockUnitOfWork.Setup(x => x.SaveChangesAsync(It.IsAny<CancellationToken>()))
|
|
359
|
+
.ThrowsAsync(new DbUpdateConcurrencyException());
|
|
360
|
+
|
|
361
|
+
// Act
|
|
362
|
+
var act = () => _sut.UpdateAsync(id, request);
|
|
363
|
+
|
|
364
|
+
// Assert
|
|
365
|
+
await act.Should().ThrowAsync<ConcurrencyException>();
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
#endregion
|
|
369
|
+
|
|
370
|
+
#region Helper Methods
|
|
371
|
+
|
|
372
|
+
private static {{name}} CreateValid{{name}}(Guid? id = null)
|
|
373
|
+
{
|
|
374
|
+
{{#if isSystemEntity}}
|
|
375
|
+
var entity = {{name}}.Create("TEST-001", "test@example.com");
|
|
376
|
+
{{else}}
|
|
377
|
+
var entity = {{name}}.Create(Guid.NewGuid(), "TEST-001", "test@example.com");
|
|
378
|
+
{{/if}}
|
|
379
|
+
|
|
380
|
+
if (id.HasValue)
|
|
381
|
+
{
|
|
382
|
+
// Use reflection to set Id for testing
|
|
383
|
+
typeof({{name}}).GetProperty("Id")!.SetValue(entity, id.Value);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
return entity;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
#endregion
|
|
390
|
+
}
|