@nakedev/go-scaffold 0.4.0 → 0.5.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 (192) hide show
  1. package/README.md +598 -306
  2. package/dist/commands/auth.js +65 -23
  3. package/dist/commands/check.js +281 -0
  4. package/dist/commands/config.js +50 -0
  5. package/dist/commands/create.js +33 -2
  6. package/dist/commands/generate.js +29 -3
  7. package/dist/commands/method.js +74 -63
  8. package/dist/commands/migration.js +2 -2
  9. package/dist/commands/observability.js +4 -53
  10. package/dist/commands/rbac.js +21 -10
  11. package/dist/commands/undo.js +11 -3
  12. package/dist/commands/worker.js +15 -5
  13. package/dist/index.js +198 -59
  14. package/dist/prompts/auth-wizard.js +40 -6
  15. package/dist/prompts/create-wizard.js +42 -1
  16. package/dist/prompts/generate-wizard.js +89 -9
  17. package/dist/templates/auth-manifest.js +50 -19
  18. package/dist/templates/create-manifest.js +8 -0
  19. package/dist/templates/module-manifest.js +84 -26
  20. package/dist/templates/rbac-manifest.js +16 -11
  21. package/dist/templates/worker-manifest.js +4 -1
  22. package/dist/types.js +8 -0
  23. package/dist/utils/auth-patcher.js +124 -33
  24. package/dist/utils/config.js +167 -4
  25. package/dist/utils/docs-patcher.js +68 -0
  26. package/dist/utils/hexagonal-method-patcher.js +334 -0
  27. package/dist/utils/main-patcher.js +32 -30
  28. package/dist/utils/marker-patch.js +7 -1
  29. package/dist/utils/module-location.js +17 -11
  30. package/dist/utils/module-profile.js +32 -0
  31. package/dist/utils/platform-patcher.js +56 -7
  32. package/dist/utils/rbac-patcher.js +89 -210
  33. package/package.json +7 -2
  34. package/templates/add/auth/cmd/seed/main.go.hbs +15 -3
  35. package/templates/add/auth/docs/login.yaml.hbs +11 -1
  36. package/templates/add/auth/docs/mfa-verify.yaml.hbs +19 -0
  37. package/templates/add/auth/docs/provider-exchange.yaml.hbs +40 -0
  38. package/templates/add/auth/docs/provider-login.yaml.hbs +31 -0
  39. package/templates/add/auth/docs/refresh.yaml.hbs +7 -0
  40. package/templates/add/auth/docs/register.yaml.hbs +7 -0
  41. package/templates/add/auth/docs/reset-password.yaml.hbs +1 -1
  42. package/templates/add/auth/docs/schemas.yaml.hbs +59 -1
  43. package/templates/add/auth/docs/users-me-mfa-confirm.yaml.hbs +19 -0
  44. package/templates/add/auth/docs/users-me-mfa-disable.yaml.hbs +15 -0
  45. package/templates/add/auth/docs/users-me-mfa-setup.yaml.hbs +14 -0
  46. package/templates/add/auth/docs/users-me-mfa.yaml.hbs +12 -0
  47. package/templates/add/auth/internal/app/user/adapters/inbound/http/browser_policy.go.hbs +98 -0
  48. package/templates/add/auth/internal/app/user/adapters/inbound/http/dto.go.hbs +159 -0
  49. package/templates/add/auth/internal/app/user/adapters/inbound/http/handler.go.hbs +228 -0
  50. package/templates/add/auth/internal/app/user/adapters/inbound/http/handler_local.go.hbs +76 -0
  51. package/templates/add/auth/internal/app/user/adapters/inbound/http/handler_mfa.go.hbs +83 -0
  52. package/templates/add/auth/internal/app/user/adapters/inbound/http/handler_oauth.go.hbs +70 -0
  53. package/templates/add/auth/internal/app/user/adapters/inbound/http/handler_recovery.go.hbs +49 -0
  54. package/templates/add/auth/internal/app/user/adapters/inbound/http/handler_test.go.hbs +311 -0
  55. package/templates/add/auth/internal/app/user/adapters/inbound/http/handler_user.go.hbs +41 -0
  56. package/templates/add/auth/internal/app/user/adapters/inbound/http/session_cookie.go.hbs +35 -0
  57. package/templates/add/auth/internal/app/user/adapters/outbound/password/bcrypt.go.hbs +35 -0
  58. package/templates/add/auth/internal/app/user/adapters/outbound/password/bcrypt_test.go.hbs +20 -0
  59. package/templates/add/auth/internal/app/user/adapters/outbound/postgres/mfa_store.go.hbs +129 -0
  60. package/templates/add/auth/internal/app/user/adapters/outbound/postgres/mfa_store_test.go.hbs +174 -0
  61. package/templates/add/auth/internal/app/user/adapters/outbound/postgres/model.go.hbs +84 -0
  62. package/templates/add/auth/internal/app/user/adapters/outbound/postgres/repository.go.hbs +211 -0
  63. package/templates/add/auth/internal/app/user/{repository_test.go.hbs → adapters/outbound/postgres/repository_test.go.hbs} +18 -19
  64. package/templates/add/auth/internal/app/user/adapters/outbound/postgres/tokenstore_pg.go.hbs +213 -0
  65. package/templates/add/auth/internal/app/user/adapters/outbound/postgres/tokenstore_pg_test.go.hbs +103 -0
  66. package/templates/add/auth/internal/app/user/adapters/outbound/postgres/tokenstore_recovery.go.hbs +84 -0
  67. package/templates/add/auth/internal/app/user/adapters/outbound/redis/tokenstore.go.hbs +228 -0
  68. package/templates/add/auth/internal/app/user/adapters/outbound/redis/tokenstore_test.go.hbs +196 -0
  69. package/templates/add/auth/internal/app/user/application/contracts.go.hbs +52 -0
  70. package/templates/add/auth/internal/app/user/application/dto.go.hbs +75 -0
  71. package/templates/add/auth/internal/app/user/application/errors.go.hbs +62 -0
  72. package/templates/add/auth/internal/app/user/application/external_login.go.hbs +198 -0
  73. package/templates/add/auth/internal/app/user/application/jwt.go.hbs +58 -0
  74. package/templates/add/auth/internal/app/user/application/local_auth.go.hbs +96 -0
  75. package/templates/add/auth/internal/app/user/application/mfa_service.go.hbs +449 -0
  76. package/templates/add/auth/internal/app/user/application/mfa_service_test.go.hbs +200 -0
  77. package/templates/add/auth/internal/app/user/application/oauth.go.hbs +132 -0
  78. package/templates/add/auth/internal/app/user/application/provider_test.go.hbs +285 -0
  79. package/templates/add/auth/internal/app/user/application/recovery.go.hbs +82 -0
  80. package/templates/add/auth/internal/app/user/application/recovery_service.go.hbs +112 -0
  81. package/templates/add/auth/internal/app/user/application/service.go.hbs +145 -0
  82. package/templates/add/auth/internal/app/user/application/service_test.go.hbs +891 -0
  83. package/templates/add/auth/internal/app/user/application/sessions.go.hbs +99 -0
  84. package/templates/add/auth/internal/app/user/application/tokenstore_ports.go.hbs +14 -0
  85. package/templates/add/auth/internal/app/user/application/user_query.go.hbs +65 -0
  86. package/templates/add/auth/internal/app/user/composition.go.hbs +168 -0
  87. package/templates/add/auth/internal/app/user/domain/entity.go.hbs +41 -0
  88. package/templates/add/auth/internal/app/user/domain/errors.go.hbs +32 -0
  89. package/templates/add/auth/internal/app/user/ports/password.go.hbs +9 -0
  90. package/templates/add/auth/internal/app/user/ports/repository.go.hbs +90 -0
  91. package/templates/add/auth/internal/platform/authprovider/google/google.go.hbs +389 -0
  92. package/templates/add/auth/internal/platform/authprovider/google/google_test.go.hbs +312 -0
  93. package/templates/add/auth/migrations/create_auth_tokens.up.sql.hbs +10 -5
  94. package/templates/add/auth/migrations/create_identities.up.sql.hbs +1 -1
  95. package/templates/add/auth/migrations/create_login_throttle.up.sql.hbs +1 -1
  96. package/templates/add/auth/migrations/create_mfa.down.sql.hbs +3 -0
  97. package/templates/add/auth/migrations/create_mfa.up.sql.hbs +29 -0
  98. package/templates/add/auth/migrations/create_users.up.sql.hbs +4 -3
  99. package/templates/add/rbac/internal/app/role/adapters/inbound/http/handler.go.hbs +142 -0
  100. package/templates/add/rbac/internal/app/role/adapters/inbound/http/handler_test.go.hbs +19 -0
  101. package/templates/add/rbac/internal/app/role/adapters/outbound/postgres/model.go.hbs +48 -0
  102. package/templates/add/rbac/internal/app/role/adapters/outbound/postgres/repository.go.hbs +127 -0
  103. package/templates/add/rbac/internal/app/role/{repository_test.go.hbs → adapters/outbound/postgres/repository_test.go.hbs} +8 -8
  104. package/templates/add/rbac/internal/app/role/application/dto.go.hbs +47 -0
  105. package/templates/add/rbac/internal/app/role/application/errors.go.hbs +19 -0
  106. package/templates/add/rbac/internal/app/role/application/service.go.hbs +157 -0
  107. package/templates/add/rbac/internal/app/role/{service_test.go.hbs → application/service_test.go.hbs} +26 -19
  108. package/templates/add/rbac/internal/app/role/composition.go.hbs +48 -0
  109. package/templates/add/rbac/internal/app/role/domain/entity.go.hbs +23 -0
  110. package/templates/add/rbac/internal/app/role/domain/errors.go.hbs +26 -0
  111. package/templates/add/rbac/internal/app/role/ports/repository.go.hbs +25 -0
  112. package/templates/add/rbac/migrations/add_roles.down.sql.hbs +3 -11
  113. package/templates/add/rbac/migrations/add_roles.up.sql.hbs +17 -6
  114. package/templates/add/worker/internal/platform/queue/river_test.go.hbs +84 -0
  115. package/templates/create/base/.claude/skills/go-scaffold/SKILL.md.hbs +358 -121
  116. package/templates/create/base/.env.example.hbs +0 -1
  117. package/templates/create/base/.golangci.yml.hbs +2 -2
  118. package/templates/create/base/AGENTS.md.hbs +279 -67
  119. package/templates/create/base/Makefile.hbs +2 -1
  120. package/templates/create/base/README.md.hbs +115 -32
  121. package/templates/create/base/cmd/api/wiring.go.hbs +13 -9
  122. package/templates/create/base/internal/composition/doc.go.hbs +7 -0
  123. package/templates/create/base/internal/platform/database/database.go.hbs +3 -3
  124. package/templates/create/base/internal/shared/apperror/apperror.go.hbs +15 -2
  125. package/templates/create/base/internal/shared/config/config.go.hbs +0 -8
  126. package/templates/create/base/internal/shared/middleware/cors_test.go.hbs +40 -0
  127. package/templates/create/base/internal/shared/middleware/error.go.hbs +15 -5
  128. package/templates/create/features/docs/architecture.md.hbs +92 -32
  129. package/templates/create/features/docs/patterns.md.hbs +137 -91
  130. package/templates/create/features/docs/techstack.md.hbs +18 -3
  131. package/templates/generate/module/hexagonal/adapters/inbound/http/dto.go.hbs +45 -0
  132. package/templates/generate/module/hexagonal/adapters/inbound/http/dto.minimal.go.hbs +28 -0
  133. package/templates/generate/module/hexagonal/adapters/inbound/http/handler.go.hbs +182 -0
  134. package/templates/generate/module/hexagonal/adapters/inbound/http/handler.minimal.go.hbs +83 -0
  135. package/templates/generate/module/hexagonal/adapters/inbound/http/handler_crud_test.go.hbs +18 -0
  136. package/templates/generate/module/hexagonal/adapters/inbound/http/handler_test.go.hbs +30 -0
  137. package/templates/generate/module/hexagonal/adapters/outbound/postgres/model.go.hbs +37 -0
  138. package/templates/generate/module/hexagonal/adapters/outbound/postgres/repository.go.hbs +95 -0
  139. package/templates/generate/module/{repository_test.go.hbs → hexagonal/adapters/outbound/postgres/repository_test.go.hbs} +8 -8
  140. package/templates/generate/module/hexagonal/application/commands.crud.go.hbs +54 -0
  141. package/templates/generate/module/hexagonal/application/commands.go.hbs +25 -0
  142. package/templates/generate/module/hexagonal/application/cqrs_test.go.hbs +66 -0
  143. package/templates/generate/module/hexagonal/application/dto.go.hbs +35 -0
  144. package/templates/generate/module/hexagonal/application/dto.minimal.go.hbs +25 -0
  145. package/templates/generate/module/hexagonal/application/queries.crud.go.hbs +33 -0
  146. package/templates/generate/module/hexagonal/application/queries.go.hbs +25 -0
  147. package/templates/generate/module/hexagonal/application/service.crud.go.hbs +73 -0
  148. package/templates/generate/module/hexagonal/application/service.go.hbs +29 -0
  149. package/templates/generate/module/hexagonal/application/service_test.go.hbs +62 -0
  150. package/templates/generate/module/hexagonal/composition.go.hbs +27 -0
  151. package/templates/generate/module/hexagonal/domain/entity.go.hbs +20 -0
  152. package/templates/generate/module/hexagonal/domain/errors.go.hbs +11 -0
  153. package/templates/generate/module/hexagonal/ports/repository.go.hbs +38 -0
  154. package/templates/generate/module/migration.up.sql.hbs +1 -1
  155. package/dist/utils/method-patcher.js +0 -357
  156. package/templates/add/auth/docs/google-callback.yaml.hbs +0 -22
  157. package/templates/add/auth/docs/google-login.yaml.hbs +0 -7
  158. package/templates/add/auth/internal/app/user/dto.go.hbs +0 -77
  159. package/templates/add/auth/internal/app/user/errors.go.hbs +0 -43
  160. package/templates/add/auth/internal/app/user/handler.go.hbs +0 -276
  161. package/templates/add/auth/internal/app/user/jwt.go.hbs +0 -108
  162. package/templates/add/auth/internal/app/user/model/authtoken.go.hbs +0 -39
  163. package/templates/add/auth/internal/app/user/model/identity.go.hbs +0 -31
  164. package/templates/add/auth/internal/app/user/model/loginthrottle.go.hbs +0 -26
  165. package/templates/add/auth/internal/app/user/model/user.go.hbs +0 -30
  166. package/templates/add/auth/internal/app/user/repository.go.hbs +0 -137
  167. package/templates/add/auth/internal/app/user/service.go.hbs +0 -531
  168. package/templates/add/auth/internal/app/user/service_test.go.hbs +0 -316
  169. package/templates/add/auth/internal/app/user/tokenstore.go.hbs +0 -30
  170. package/templates/add/auth/internal/app/user/tokenstore_pg.go.hbs +0 -144
  171. package/templates/add/auth/internal/app/user/tokenstore_redis.go.hbs +0 -147
  172. package/templates/add/rbac/internal/app/role/dto.go.hbs +0 -45
  173. package/templates/add/rbac/internal/app/role/errors.go.hbs +0 -39
  174. package/templates/add/rbac/internal/app/role/handler.go.hbs +0 -104
  175. package/templates/add/rbac/internal/app/role/model/permission.go.hbs +0 -12
  176. package/templates/add/rbac/internal/app/role/model/role.go.hbs +0 -22
  177. package/templates/add/rbac/internal/app/role/model/role_permission.go.hbs +0 -11
  178. package/templates/add/rbac/internal/app/role/repository.go.hbs +0 -97
  179. package/templates/add/rbac/internal/app/role/service.go.hbs +0 -217
  180. package/templates/generate/module/dto.go.hbs +0 -36
  181. package/templates/generate/module/errors.go.hbs +0 -33
  182. package/templates/generate/module/handler.go.hbs +0 -134
  183. package/templates/generate/module/handler_test.go.hbs +0 -174
  184. package/templates/generate/module/minimal/dto.go.hbs +0 -28
  185. package/templates/generate/module/minimal/handler.go.hbs +0 -48
  186. package/templates/generate/module/minimal/handler_test.go.hbs +0 -10
  187. package/templates/generate/module/minimal/service.go.hbs +0 -45
  188. package/templates/generate/module/minimal/service_test.go.hbs +0 -77
  189. package/templates/generate/module/model/model.go.hbs +0 -36
  190. package/templates/generate/module/repository.go.hbs +0 -103
  191. package/templates/generate/module/service.go.hbs +0 -108
  192. package/templates/generate/module/service_test.go.hbs +0 -161
@@ -1,108 +0,0 @@
1
- package {{pkg}}
2
-
3
- import (
4
- "context"
5
- "errors"
6
-
7
- "{{goModule}}/internal/app/{{modulePath}}/model"
8
- "{{goModule}}/internal/shared/apperror"
9
- "{{goModule}}/internal/shared/dberr"
10
- "{{goModule}}/internal/shared/id"
11
-
12
- "github.com/google/uuid"
13
- "gorm.io/gorm"
14
- )
15
-
16
- // repository = what the service needs from the data layer (declared on the consumer side, so it can be mocked in tests)
17
- type repository interface {
18
- Create(ctx context.Context, m *model.{{pascalName}}) error
19
- FindAll(ctx context.Context, limit, offset int) ([]model.{{pascalName}}, error)
20
- FindByID(ctx context.Context, id uuid.UUID) (*model.{{pascalName}}, error)
21
- Update(ctx context.Context, m *model.{{pascalName}}) error
22
- Delete(ctx context.Context, id uuid.UUID) error
23
- // go-scaffold:repository-interface
24
- }
25
-
26
- // Service = business logic for {{pkg}} (rules live here, it knows nothing about HTTP)
27
- // input shape validation happens in the handler (binding tags)
28
- type Service struct {
29
- repo repository
30
- }
31
-
32
- func NewService(repo repository) *Service {
33
- return &Service{repo: repo}
34
- }
35
-
36
- func (s *Service) Create(ctx context.Context, in createInput) (*model.{{pascalName}}, error) {
37
- // TODO: set real fields from in
38
- _ = in
39
- m := &model.{{pascalName}}{ID: id.New()}
40
- if err := s.repo.Create(ctx, m); err != nil {
41
- if dberr.IsDuplicate(err) {
42
- return nil, errConflict()
43
- }
44
- return nil, apperror.NewInternal()
45
- }
46
- return m, nil
47
- }
48
-
49
- func (s *Service) List(ctx context.Context, limit, offset int) ([]model.{{pascalName}}, error) {
50
- items, err := s.repo.FindAll(ctx, limit, offset)
51
- if err != nil {
52
- return nil, apperror.NewInternal()
53
- }
54
- return items, nil
55
- }
56
-
57
- func (s *Service) Get(ctx context.Context, id uuid.UUID) (*model.{{pascalName}}, error) {
58
- m, err := s.repo.FindByID(ctx, id)
59
- if err != nil {
60
- return nil, wrapFindErr(err)
61
- }
62
- return m, nil
63
- }
64
-
65
- func (s *Service) Update(ctx context.Context, id uuid.UUID, in updateInput) (*model.{{pascalName}}, error) {
66
- m, err := s.repo.FindByID(ctx, id)
67
- if err != nil {
68
- return nil, wrapFindErr(err)
69
- }
70
- // TODO: apply real fields from in before saving
71
- // The version the client read comes from the request, not from the row we
72
- // just loaded — comparing the row against itself would always succeed and
73
- // defeat the check.
74
- m.Version = in.Version
75
- if err := s.repo.Update(ctx, m); err != nil {
76
- if errors.Is(err, ErrStaleVersion) {
77
- return nil, errStale()
78
- }
79
- if dberr.IsDuplicate(err) {
80
- return nil, errConflict()
81
- }
82
- return nil, apperror.NewInternal()
83
- }
84
- return m, nil
85
- }
86
-
87
- func (s *Service) Delete(ctx context.Context, id uuid.UUID) error {
88
- if err := s.repo.Delete(ctx, id); err != nil {
89
- if errors.Is(err, gorm.ErrRecordNotFound) {
90
- return errNotFound()
91
- }
92
- // deleting a {{pkg}} that's still referenced elsewhere → FK RESTRICT trips = 409, not 500
93
- if dberr.IsForeignKey(err) {
94
- return errHasReferences()
95
- }
96
- return apperror.NewInternal()
97
- }
98
- return nil
99
- }
100
-
101
- func wrapFindErr(err error) error {
102
- if errors.Is(err, gorm.ErrRecordNotFound) {
103
- return errNotFound()
104
- }
105
- return apperror.NewInternal()
106
- }
107
-
108
- // go-scaffold:service-methods
@@ -1,161 +0,0 @@
1
- package {{pkg}}
2
-
3
- import (
4
- "context"
5
- "errors"
6
- "testing"
7
-
8
- "{{goModule}}/internal/app/{{modulePath}}/model"
9
- "{{goModule}}/internal/shared/apperror"
10
-
11
- "github.com/google/uuid"
12
- "gorm.io/gorm"
13
- )
14
-
15
- // repositoryStub exposes one function per dependency operation. Tests configure
16
- // only the calls they expect; an unexpected call panics instead of silently
17
- // returning a shared zero value/error that can hide broken orchestration.
18
- type repositoryStub struct {
19
- createFn func(context.Context, *model.{{pascalName}}) error
20
- findAllFn func(context.Context, int, int) ([]model.{{pascalName}}, error)
21
- findByIDFn func(context.Context, uuid.UUID) (*model.{{pascalName}}, error)
22
- updateFn func(context.Context, *model.{{pascalName}}) error
23
- deleteFn func(context.Context, uuid.UUID) error
24
- // go-scaffold:repository-stub-fields
25
- }
26
-
27
- func (s *repositoryStub) Create(ctx context.Context, m *model.{{pascalName}}) error {
28
- if s.createFn == nil {
29
- panic("unexpected repository.Create call")
30
- }
31
- return s.createFn(ctx, m)
32
- }
33
-
34
- func (s *repositoryStub) FindAll(ctx context.Context, limit, offset int) ([]model.{{pascalName}}, error) {
35
- if s.findAllFn == nil {
36
- panic("unexpected repository.FindAll call")
37
- }
38
- return s.findAllFn(ctx, limit, offset)
39
- }
40
-
41
- func (s *repositoryStub) FindByID(ctx context.Context, id uuid.UUID) (*model.{{pascalName}}, error) {
42
- if s.findByIDFn == nil {
43
- panic("unexpected repository.FindByID call")
44
- }
45
- return s.findByIDFn(ctx, id)
46
- }
47
-
48
- func (s *repositoryStub) Update(ctx context.Context, m *model.{{pascalName}}) error {
49
- if s.updateFn == nil {
50
- panic("unexpected repository.Update call")
51
- }
52
- return s.updateFn(ctx, m)
53
- }
54
-
55
- func (s *repositoryStub) Delete(ctx context.Context, id uuid.UUID) error {
56
- if s.deleteFn == nil {
57
- panic("unexpected repository.Delete call")
58
- }
59
- return s.deleteFn(ctx, id)
60
- }
61
-
62
- // go-scaffold:repository-stub-methods
63
-
64
- func status(t *testing.T, err error) int {
65
- t.Helper()
66
- var appErr *apperror.AppError
67
- if !errors.As(err, &appErr) {
68
- t.Fatalf("expected *apperror.AppError, got %T: %v", err, err)
69
- }
70
- return appErr.HTTPStatus
71
- }
72
-
73
- func TestService_Create_Duplicate(t *testing.T) {
74
- repo := &repositoryStub{
75
- createFn: func(context.Context, *model.{{pascalName}}) error {
76
- return gorm.ErrDuplicatedKey
77
- },
78
- }
79
- svc := NewService(repo)
80
-
81
- _, err := svc.Create(context.Background(), createInput{})
82
-
83
- if got := status(t, err); got != 409 {
84
- t.Fatalf("want 409 conflict, got %d", got)
85
- }
86
- }
87
-
88
- func TestService_Get_NotFound(t *testing.T) {
89
- repo := &repositoryStub{
90
- findByIDFn: func(context.Context, uuid.UUID) (*model.{{pascalName}}, error) {
91
- return nil, gorm.ErrRecordNotFound
92
- },
93
- }
94
- svc := NewService(repo)
95
-
96
- _, err := svc.Get(context.Background(), uuid.New())
97
-
98
- if got := status(t, err); got != 404 {
99
- t.Fatalf("want 404, got %d", got)
100
- }
101
- }
102
-
103
- func TestService_List_DBError_Becomes500(t *testing.T) {
104
- repo := &repositoryStub{
105
- findAllFn: func(context.Context, int, int) ([]model.{{pascalName}}, error) {
106
- return nil, errors.New("connection refused")
107
- },
108
- }
109
- svc := NewService(repo)
110
-
111
- _, err := svc.List(context.Background(), 20, 0)
112
-
113
- if got := status(t, err); got != 500 {
114
- t.Fatalf("want 500, got %d", got)
115
- }
116
- }
117
-
118
- // The check that stops one user's save from silently erasing another's: the
119
- // repository reports the row moved on, the client must see 409 rather than a
120
- // 500 or, worse, a success.
121
- func TestService_Update_StaleVersion(t *testing.T) {
122
- repo := &repositoryStub{
123
- findByIDFn: func(_ context.Context, id uuid.UUID) (*model.{{pascalName}}, error) {
124
- return &model.{{pascalName}}{ID: id, Version: 7}, nil
125
- },
126
- updateFn: func(context.Context, *model.{{pascalName}}) error {
127
- return ErrStaleVersion
128
- },
129
- }
130
- svc := NewService(repo)
131
-
132
- _, err := svc.Update(context.Background(), uuid.New(), updateInput{Version: 3})
133
-
134
- if got := status(t, err); got != 409 {
135
- t.Fatalf("want 409 stale, got %d", got)
136
- }
137
- }
138
-
139
- // The version compared against the database has to be the one the client sent,
140
- // not the one just read back — comparing the row against itself would always
141
- // match and the guard would never fire.
142
- func TestService_Update_SendsClientVersion(t *testing.T) {
143
- var seen int
144
- repo := &repositoryStub{
145
- findByIDFn: func(_ context.Context, id uuid.UUID) (*model.{{pascalName}}, error) {
146
- return &model.{{pascalName}}{ID: id, Version: 7}, nil
147
- },
148
- updateFn: func(_ context.Context, m *model.{{pascalName}}) error {
149
- seen = m.Version
150
- return nil
151
- },
152
- }
153
- svc := NewService(repo)
154
-
155
- if _, err := svc.Update(context.Background(), uuid.New(), updateInput{Version: 3}); err != nil {
156
- t.Fatalf("update: %v", err)
157
- }
158
- if seen != 3 {
159
- t.Fatalf("want the client's version 3 to reach the repository, got %d", seen)
160
- }
161
- }