@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.
- 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/application/steps/step-01-navigation.md +226 -43
- package/templates/skills/application/steps/step-03-roles.md +160 -38
- package/templates/skills/application/steps/step-04-backend.md +109 -2
- package/templates/skills/application/templates-seed.md +200 -1
- 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,441 @@
|
|
|
1
|
+
{{!-- SmartStack Repository Test Template --}}
|
|
2
|
+
{{!-- Generates integration tests for repositories following SmartStack conventions --}}
|
|
3
|
+
|
|
4
|
+
using FluentAssertions;
|
|
5
|
+
using Microsoft.EntityFrameworkCore;
|
|
6
|
+
using Xunit;
|
|
7
|
+
using {{namespace}}.Domain.Entities;
|
|
8
|
+
using {{namespace}}.Infrastructure.Data;
|
|
9
|
+
using {{namespace}}.Infrastructure.Repositories;
|
|
10
|
+
|
|
11
|
+
namespace {{namespace}}.Tests.Integration.Repositories;
|
|
12
|
+
|
|
13
|
+
/// <summary>
|
|
14
|
+
/// Integration tests for <see cref="{{name}}Repository"/>.
|
|
15
|
+
/// Uses in-memory database for isolation.
|
|
16
|
+
/// Follows SmartStack testing conventions: {Method}_When{Condition}_Should{Result}
|
|
17
|
+
/// </summary>
|
|
18
|
+
public class {{name}}RepositoryTests : IDisposable
|
|
19
|
+
{
|
|
20
|
+
private readonly ApplicationDbContext _context;
|
|
21
|
+
private readonly {{name}}Repository _sut;
|
|
22
|
+
{{#unless isSystemEntity}}
|
|
23
|
+
private readonly Guid _tenantId = Guid.NewGuid();
|
|
24
|
+
{{/unless}}
|
|
25
|
+
|
|
26
|
+
public {{name}}RepositoryTests()
|
|
27
|
+
{
|
|
28
|
+
var options = new DbContextOptionsBuilder<ApplicationDbContext>()
|
|
29
|
+
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
|
|
30
|
+
.Options;
|
|
31
|
+
|
|
32
|
+
_context = new ApplicationDbContext(options);
|
|
33
|
+
_sut = new {{name}}Repository(_context);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
public void Dispose()
|
|
37
|
+
{
|
|
38
|
+
_context.Dispose();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
#region GetByIdAsync Tests
|
|
42
|
+
|
|
43
|
+
[Fact]
|
|
44
|
+
public async Task GetByIdAsync_WhenExists_ShouldReturnEntity()
|
|
45
|
+
{
|
|
46
|
+
// Arrange
|
|
47
|
+
var entity = CreateAndSave{{name}}();
|
|
48
|
+
|
|
49
|
+
// Act
|
|
50
|
+
var result = await _sut.GetByIdAsync(entity.Id);
|
|
51
|
+
|
|
52
|
+
// Assert
|
|
53
|
+
result.Should().NotBeNull();
|
|
54
|
+
result!.Id.Should().Be(entity.Id);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
[Fact]
|
|
58
|
+
public async Task GetByIdAsync_WhenNotExists_ShouldReturnNull()
|
|
59
|
+
{
|
|
60
|
+
// Arrange
|
|
61
|
+
var nonExistentId = Guid.NewGuid();
|
|
62
|
+
|
|
63
|
+
// Act
|
|
64
|
+
var result = await _sut.GetByIdAsync(nonExistentId);
|
|
65
|
+
|
|
66
|
+
// Assert
|
|
67
|
+
result.Should().BeNull();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
{{#if includeSoftDelete}}
|
|
71
|
+
[Fact]
|
|
72
|
+
public async Task GetByIdAsync_WhenSoftDeleted_ShouldReturnNull()
|
|
73
|
+
{
|
|
74
|
+
// Arrange
|
|
75
|
+
var entity = CreateAndSave{{name}}();
|
|
76
|
+
entity.SoftDelete("deleter@example.com");
|
|
77
|
+
await _context.SaveChangesAsync();
|
|
78
|
+
|
|
79
|
+
// Act
|
|
80
|
+
var result = await _sut.GetByIdAsync(entity.Id);
|
|
81
|
+
|
|
82
|
+
// Assert
|
|
83
|
+
result.Should().BeNull();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
[Fact]
|
|
87
|
+
public async Task GetByIdAsync_WhenSoftDeletedAndIncludeDeleted_ShouldReturnEntity()
|
|
88
|
+
{
|
|
89
|
+
// Arrange
|
|
90
|
+
var entity = CreateAndSave{{name}}();
|
|
91
|
+
entity.SoftDelete("deleter@example.com");
|
|
92
|
+
await _context.SaveChangesAsync();
|
|
93
|
+
|
|
94
|
+
// Act
|
|
95
|
+
var result = await _sut.GetByIdAsync(entity.Id, includeDeleted: true);
|
|
96
|
+
|
|
97
|
+
// Assert
|
|
98
|
+
result.Should().NotBeNull();
|
|
99
|
+
result!.IsDeleted.Should().BeTrue();
|
|
100
|
+
}
|
|
101
|
+
{{/if}}
|
|
102
|
+
|
|
103
|
+
#endregion
|
|
104
|
+
|
|
105
|
+
#region GetAllAsync Tests
|
|
106
|
+
|
|
107
|
+
[Fact]
|
|
108
|
+
public async Task GetAllAsync_WhenEntitiesExist_ShouldReturnAll()
|
|
109
|
+
{
|
|
110
|
+
// Arrange
|
|
111
|
+
CreateAndSave{{name}}("CODE-001");
|
|
112
|
+
CreateAndSave{{name}}("CODE-002");
|
|
113
|
+
CreateAndSave{{name}}("CODE-003");
|
|
114
|
+
|
|
115
|
+
// Act
|
|
116
|
+
var result = await _sut.GetAllAsync();
|
|
117
|
+
|
|
118
|
+
// Assert
|
|
119
|
+
result.Should().HaveCount(3);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
[Fact]
|
|
123
|
+
public async Task GetAllAsync_WhenEmpty_ShouldReturnEmptyList()
|
|
124
|
+
{
|
|
125
|
+
// Act
|
|
126
|
+
var result = await _sut.GetAllAsync();
|
|
127
|
+
|
|
128
|
+
// Assert
|
|
129
|
+
result.Should().BeEmpty();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
{{#if includeSoftDelete}}
|
|
133
|
+
[Fact]
|
|
134
|
+
public async Task GetAllAsync_ShouldExcludeSoftDeleted()
|
|
135
|
+
{
|
|
136
|
+
// Arrange
|
|
137
|
+
CreateAndSave{{name}}("ACTIVE-001");
|
|
138
|
+
var deleted = CreateAndSave{{name}}("DELETED-001");
|
|
139
|
+
deleted.SoftDelete("deleter@example.com");
|
|
140
|
+
await _context.SaveChangesAsync();
|
|
141
|
+
|
|
142
|
+
// Act
|
|
143
|
+
var result = await _sut.GetAllAsync();
|
|
144
|
+
|
|
145
|
+
// Assert
|
|
146
|
+
result.Should().HaveCount(1);
|
|
147
|
+
result.Should().OnlyContain(x => !x.IsDeleted);
|
|
148
|
+
}
|
|
149
|
+
{{/if}}
|
|
150
|
+
|
|
151
|
+
#endregion
|
|
152
|
+
|
|
153
|
+
#region AddAsync Tests
|
|
154
|
+
|
|
155
|
+
[Fact]
|
|
156
|
+
public async Task AddAsync_WhenValidEntity_ShouldPersist()
|
|
157
|
+
{
|
|
158
|
+
// Arrange
|
|
159
|
+
{{#if isSystemEntity}}
|
|
160
|
+
var entity = {{name}}.Create("NEW-001", "test@example.com");
|
|
161
|
+
{{else}}
|
|
162
|
+
var entity = {{name}}.Create(_tenantId, "NEW-001", "test@example.com");
|
|
163
|
+
{{/if}}
|
|
164
|
+
|
|
165
|
+
// Act
|
|
166
|
+
await _sut.AddAsync(entity);
|
|
167
|
+
await _context.SaveChangesAsync();
|
|
168
|
+
|
|
169
|
+
// Assert
|
|
170
|
+
var persisted = await _context.Set<{{name}}>().FindAsync(entity.Id);
|
|
171
|
+
persisted.Should().NotBeNull();
|
|
172
|
+
persisted!.Code.Should().Be("NEW-001");
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
[Fact]
|
|
176
|
+
public async Task AddAsync_ShouldGenerateId()
|
|
177
|
+
{
|
|
178
|
+
// Arrange
|
|
179
|
+
{{#if isSystemEntity}}
|
|
180
|
+
var entity = {{name}}.Create("TEST", "test@example.com");
|
|
181
|
+
{{else}}
|
|
182
|
+
var entity = {{name}}.Create(_tenantId, "TEST", "test@example.com");
|
|
183
|
+
{{/if}}
|
|
184
|
+
|
|
185
|
+
// Act
|
|
186
|
+
await _sut.AddAsync(entity);
|
|
187
|
+
await _context.SaveChangesAsync();
|
|
188
|
+
|
|
189
|
+
// Assert
|
|
190
|
+
entity.Id.Should().NotBe(Guid.Empty);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
#endregion
|
|
194
|
+
|
|
195
|
+
#region Update Tests
|
|
196
|
+
|
|
197
|
+
[Fact]
|
|
198
|
+
public async Task Update_WhenEntityModified_ShouldPersistChanges()
|
|
199
|
+
{
|
|
200
|
+
// Arrange
|
|
201
|
+
var entity = CreateAndSave{{name}}("ORIGINAL");
|
|
202
|
+
entity.Update("updater@example.com");
|
|
203
|
+
|
|
204
|
+
// Act
|
|
205
|
+
await _context.SaveChangesAsync();
|
|
206
|
+
|
|
207
|
+
// Assert
|
|
208
|
+
var updated = await _context.Set<{{name}}>().FindAsync(entity.Id);
|
|
209
|
+
updated!.UpdatedBy.Should().Be("updater@example.com");
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
#endregion
|
|
213
|
+
|
|
214
|
+
#region Remove Tests
|
|
215
|
+
|
|
216
|
+
[Fact]
|
|
217
|
+
public async Task Remove_WhenCalled_ShouldDeleteEntity()
|
|
218
|
+
{
|
|
219
|
+
// Arrange
|
|
220
|
+
var entity = CreateAndSave{{name}}();
|
|
221
|
+
var entityId = entity.Id;
|
|
222
|
+
|
|
223
|
+
// Act
|
|
224
|
+
_sut.Remove(entity);
|
|
225
|
+
await _context.SaveChangesAsync();
|
|
226
|
+
|
|
227
|
+
// Assert
|
|
228
|
+
var deleted = await _context.Set<{{name}}>().FindAsync(entityId);
|
|
229
|
+
deleted.Should().BeNull();
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
#endregion
|
|
233
|
+
|
|
234
|
+
#region ExistsByCodeAsync Tests
|
|
235
|
+
|
|
236
|
+
[Fact]
|
|
237
|
+
public async Task ExistsByCodeAsync_WhenExists_ShouldReturnTrue()
|
|
238
|
+
{
|
|
239
|
+
// Arrange
|
|
240
|
+
CreateAndSave{{name}}("EXISTING");
|
|
241
|
+
|
|
242
|
+
// Act
|
|
243
|
+
var result = await _sut.ExistsByCodeAsync("EXISTING");
|
|
244
|
+
|
|
245
|
+
// Assert
|
|
246
|
+
result.Should().BeTrue();
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
[Fact]
|
|
250
|
+
public async Task ExistsByCodeAsync_WhenNotExists_ShouldReturnFalse()
|
|
251
|
+
{
|
|
252
|
+
// Act
|
|
253
|
+
var result = await _sut.ExistsByCodeAsync("NON-EXISTENT");
|
|
254
|
+
|
|
255
|
+
// Assert
|
|
256
|
+
result.Should().BeFalse();
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
{{#if includeSoftDelete}}
|
|
260
|
+
[Fact]
|
|
261
|
+
public async Task ExistsByCodeAsync_WhenSoftDeleted_ShouldReturnFalse()
|
|
262
|
+
{
|
|
263
|
+
// Arrange
|
|
264
|
+
var entity = CreateAndSave{{name}}("DELETED");
|
|
265
|
+
entity.SoftDelete("deleter@example.com");
|
|
266
|
+
await _context.SaveChangesAsync();
|
|
267
|
+
|
|
268
|
+
// Act
|
|
269
|
+
var result = await _sut.ExistsByCodeAsync("DELETED");
|
|
270
|
+
|
|
271
|
+
// Assert
|
|
272
|
+
result.Should().BeFalse();
|
|
273
|
+
}
|
|
274
|
+
{{/if}}
|
|
275
|
+
|
|
276
|
+
#endregion
|
|
277
|
+
|
|
278
|
+
{{#unless isSystemEntity}}
|
|
279
|
+
#region Tenant Isolation Tests
|
|
280
|
+
|
|
281
|
+
[Fact]
|
|
282
|
+
public async Task GetAllAsync_ShouldOnlyReturnCurrentTenantEntities()
|
|
283
|
+
{
|
|
284
|
+
// Arrange
|
|
285
|
+
var tenantA = Guid.NewGuid();
|
|
286
|
+
var tenantB = Guid.NewGuid();
|
|
287
|
+
|
|
288
|
+
CreateAndSave{{name}}("A-001", tenantA);
|
|
289
|
+
CreateAndSave{{name}}("A-002", tenantA);
|
|
290
|
+
CreateAndSave{{name}}("B-001", tenantB);
|
|
291
|
+
|
|
292
|
+
// Create a tenant-scoped repository for tenant A
|
|
293
|
+
var tenantARepo = new {{name}}Repository(_context, tenantA);
|
|
294
|
+
|
|
295
|
+
// Act
|
|
296
|
+
var result = await tenantARepo.GetAllAsync();
|
|
297
|
+
|
|
298
|
+
// Assert
|
|
299
|
+
result.Should().HaveCount(2);
|
|
300
|
+
result.Should().OnlyContain(x => x.TenantId == tenantA);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
[Fact]
|
|
304
|
+
public async Task GetByIdAsync_WhenDifferentTenant_ShouldReturnNull()
|
|
305
|
+
{
|
|
306
|
+
// Arrange
|
|
307
|
+
var tenantA = Guid.NewGuid();
|
|
308
|
+
var tenantB = Guid.NewGuid();
|
|
309
|
+
var entity = CreateAndSave{{name}}("TEST", tenantA);
|
|
310
|
+
|
|
311
|
+
var tenantBRepo = new {{name}}Repository(_context, tenantB);
|
|
312
|
+
|
|
313
|
+
// Act
|
|
314
|
+
var result = await tenantBRepo.GetByIdAsync(entity.Id);
|
|
315
|
+
|
|
316
|
+
// Assert
|
|
317
|
+
result.Should().BeNull("entity belongs to different tenant");
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
[Fact]
|
|
321
|
+
public async Task AddAsync_ShouldEnforceTenantId()
|
|
322
|
+
{
|
|
323
|
+
// Arrange
|
|
324
|
+
var entity = {{name}}.Create(_tenantId, "TEST", "test@example.com");
|
|
325
|
+
|
|
326
|
+
// Act
|
|
327
|
+
await _sut.AddAsync(entity);
|
|
328
|
+
await _context.SaveChangesAsync();
|
|
329
|
+
|
|
330
|
+
// Assert
|
|
331
|
+
var persisted = await _context.Set<{{name}}>().FindAsync(entity.Id);
|
|
332
|
+
persisted!.TenantId.Should().Be(_tenantId);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
#endregion
|
|
336
|
+
{{/unless}}
|
|
337
|
+
|
|
338
|
+
#region Query Tests
|
|
339
|
+
|
|
340
|
+
[Fact]
|
|
341
|
+
public async Task FindByConditionAsync_ShouldFilterCorrectly()
|
|
342
|
+
{
|
|
343
|
+
// Arrange
|
|
344
|
+
CreateAndSave{{name}}("ALPHA");
|
|
345
|
+
CreateAndSave{{name}}("BETA");
|
|
346
|
+
CreateAndSave{{name}}("ALPHA-2");
|
|
347
|
+
|
|
348
|
+
// Act
|
|
349
|
+
var result = await _sut.FindByConditionAsync(x => x.Code.StartsWith("ALPHA"));
|
|
350
|
+
|
|
351
|
+
// Assert
|
|
352
|
+
result.Should().HaveCount(2);
|
|
353
|
+
result.Should().OnlyContain(x => x.Code.StartsWith("ALPHA"));
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
[Fact]
|
|
357
|
+
public async Task CountAsync_ShouldReturnCorrectCount()
|
|
358
|
+
{
|
|
359
|
+
// Arrange
|
|
360
|
+
CreateAndSave{{name}}("ONE");
|
|
361
|
+
CreateAndSave{{name}}("TWO");
|
|
362
|
+
CreateAndSave{{name}}("THREE");
|
|
363
|
+
|
|
364
|
+
// Act
|
|
365
|
+
var result = await _sut.CountAsync();
|
|
366
|
+
|
|
367
|
+
// Assert
|
|
368
|
+
result.Should().Be(3);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
#endregion
|
|
372
|
+
|
|
373
|
+
#region Pagination Tests
|
|
374
|
+
|
|
375
|
+
[Fact]
|
|
376
|
+
public async Task GetPagedAsync_ShouldReturnCorrectPage()
|
|
377
|
+
{
|
|
378
|
+
// Arrange
|
|
379
|
+
for (int i = 1; i <= 25; i++)
|
|
380
|
+
{
|
|
381
|
+
CreateAndSave{{name}}($"CODE-{i:D3}");
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Act
|
|
385
|
+
var result = await _sut.GetPagedAsync(page: 2, pageSize: 10);
|
|
386
|
+
|
|
387
|
+
// Assert
|
|
388
|
+
result.Items.Should().HaveCount(10);
|
|
389
|
+
result.TotalCount.Should().Be(25);
|
|
390
|
+
result.TotalPages.Should().Be(3);
|
|
391
|
+
result.CurrentPage.Should().Be(2);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
#endregion
|
|
395
|
+
|
|
396
|
+
#region Concurrency Tests
|
|
397
|
+
|
|
398
|
+
[Fact]
|
|
399
|
+
public async Task Update_WhenConcurrentModification_ShouldThrowException()
|
|
400
|
+
{
|
|
401
|
+
// Arrange
|
|
402
|
+
var entity = CreateAndSave{{name}}();
|
|
403
|
+
|
|
404
|
+
// Simulate concurrent modification
|
|
405
|
+
var concurrentContext = new ApplicationDbContext(
|
|
406
|
+
new DbContextOptionsBuilder<ApplicationDbContext>()
|
|
407
|
+
.UseInMemoryDatabase(_context.Database.GetDbConnection().Database)
|
|
408
|
+
.Options
|
|
409
|
+
);
|
|
410
|
+
var concurrentEntity = await concurrentContext.Set<{{name}}>().FindAsync(entity.Id);
|
|
411
|
+
concurrentEntity!.Update("concurrent@example.com");
|
|
412
|
+
await concurrentContext.SaveChangesAsync();
|
|
413
|
+
|
|
414
|
+
// Act
|
|
415
|
+
entity.Update("original@example.com");
|
|
416
|
+
var act = () => _context.SaveChangesAsync();
|
|
417
|
+
|
|
418
|
+
// Assert
|
|
419
|
+
await act.Should().ThrowAsync<DbUpdateConcurrencyException>();
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
#endregion
|
|
423
|
+
|
|
424
|
+
#region Helper Methods
|
|
425
|
+
|
|
426
|
+
private {{name}} CreateAndSave{{name}}(string code = "TEST-001"{{#unless isSystemEntity}}, Guid? tenantId = null{{/unless}})
|
|
427
|
+
{
|
|
428
|
+
{{#if isSystemEntity}}
|
|
429
|
+
var entity = {{name}}.Create(code, "test@example.com");
|
|
430
|
+
{{else}}
|
|
431
|
+
var entity = {{name}}.Create(tenantId ?? _tenantId, code, "test@example.com");
|
|
432
|
+
{{/if}}
|
|
433
|
+
|
|
434
|
+
_context.Set<{{name}}>().Add(entity);
|
|
435
|
+
_context.SaveChanges();
|
|
436
|
+
|
|
437
|
+
return entity;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
#endregion
|
|
441
|
+
}
|