@ossy/users 3.2.0 → 3.4.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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ossy/users",
3
3
  "description": "User domain - aggregate, events, and validators for the Ossy user model",
4
- "version": "3.2.0",
4
+ "version": "3.4.0",
5
5
  "private": false,
6
6
  "type": "module",
7
7
  "main": "./src/index.js",
@@ -36,7 +36,7 @@
36
36
  "dependencies": {
37
37
  "@ossy/config": "^3.0.9",
38
38
  "@ossy/observability": "^3.0.9",
39
- "@ossy/schema": "^3.0.9",
39
+ "@ossy/schema": "^3.4.0",
40
40
  "nanoid": "^5.1.11"
41
41
  },
42
42
  "devDependencies": {
@@ -48,5 +48,5 @@
48
48
  "/src",
49
49
  "README.md"
50
50
  ],
51
- "gitHead": "b045f1fd7f2fe00c776b9ea96f76083b93fd8556"
51
+ "gitHead": "d36b69444268d172bc3e8e1dc77e67afc63f8650"
52
52
  }
@@ -1,39 +1,214 @@
1
1
  import { describe, expect, it } from '@jest/globals'
2
2
  import { User } from './user.aggregate.js'
3
3
  import { UsersEvents } from './users.events.js'
4
+ import { UserSchema } from './schema-ids.js'
4
5
 
5
- describe('User.View email change', () => {
6
- it('sets pendingEmail on EmailChangeRequested and updates email on EmailUpdated', () => {
7
- const created = UsersEvents.Created({
8
- email: 'old@example.com',
6
+ const NOW = 1_700_000_000_000
7
+
8
+ function createdEvent (overrides = {}) {
9
+ return {
10
+ ...UsersEvents.Created({
11
+ email: 'ada@example.com',
9
12
  firstName: 'Ada',
10
13
  lastName: 'Lovelace',
11
14
  userId: 'user-1',
15
+ }),
16
+ created: NOW,
17
+ ...overrides,
18
+ }
19
+ }
20
+
21
+ function withMeta (event, overrides = {}) {
22
+ return {
23
+ ...event,
24
+ created: NOW + 1000,
25
+ ...overrides,
26
+ }
27
+ }
28
+
29
+ describe('User.View', () => {
30
+
31
+ describe('empty event list', () => {
32
+ it('returns the savedState unchanged when no events are given', () => {
33
+ expect(User.View([], { id: 'user-1' })).toEqual({ id: 'user-1' })
34
+ })
35
+
36
+ it('uses {} as the default savedState', () => {
37
+ expect(User.View([])).toEqual({})
38
+ })
39
+ })
40
+
41
+ describe('Created event', () => {
42
+ it('sets identity and profile fields from the event', () => {
43
+ const result = User.View([createdEvent()])
44
+ expect(result).toMatchObject({
45
+ id: 'user-1',
46
+ type: 'User',
47
+ created: NOW,
48
+ email: 'ada@example.com',
49
+ firstName: 'Ada',
50
+ lastName: 'Lovelace',
51
+ workspaces: [],
52
+ })
53
+ })
54
+
55
+ it('preserves existing workspaces from savedState', () => {
56
+ const result = User.View([createdEvent()], { workspaces: ['ws-1'] })
57
+ expect(result.workspaces).toEqual(['ws-1'])
58
+ })
59
+
60
+ it('uses the user schema type on Created events', () => {
61
+ expect(createdEvent().type).toBe(UserSchema.user)
62
+ })
63
+ })
64
+
65
+ describe('SignInVerified', () => {
66
+ it('sets verifiedAt from the first verification event', () => {
67
+ const result = User.View([
68
+ createdEvent(),
69
+ withMeta(UsersEvents.SignInVerified({
70
+ createdBy: 'user-1',
71
+ token: 'tok',
72
+ }), { created: NOW + 2000 }),
73
+ ])
74
+ expect(result.verifiedAt).toBe(NOW + 2000)
75
+ })
76
+
77
+ it('keeps the earliest verifiedAt when verified again', () => {
78
+ const result = User.View([
79
+ createdEvent(),
80
+ withMeta(UsersEvents.SignInVerified({
81
+ createdBy: 'user-1',
82
+ token: 'tok-1',
83
+ }), { created: NOW + 2000 }),
84
+ withMeta(UsersEvents.SignInVerified({
85
+ createdBy: 'user-1',
86
+ token: 'tok-2',
87
+ }), { created: NOW + 3000 }),
88
+ ])
89
+ expect(result.verifiedAt).toBe(NOW + 2000)
12
90
  })
13
- const requested = {
14
- ...UsersEvents.EmailChangeRequested({
91
+ })
92
+
93
+ describe('NameUpdated', () => {
94
+ it('updates first and last name from the payload', () => {
95
+ const result = User.View([
96
+ createdEvent(),
97
+ withMeta(UsersEvents.NameUpdated({
98
+ firstName: 'Augusta',
99
+ lastName: 'Byron',
100
+ createdBy: 'user-1',
101
+ })),
102
+ ])
103
+ expect(result.firstName).toBe('Augusta')
104
+ expect(result.lastName).toBe('Byron')
105
+ })
106
+
107
+ it('keeps previous names when payload fields are empty', () => {
108
+ const result = User.View([
109
+ createdEvent(),
110
+ withMeta(UsersEvents.NameUpdated({
111
+ firstName: '',
112
+ lastName: '',
113
+ createdBy: 'user-1',
114
+ })),
115
+ ])
116
+ expect(result.firstName).toBe('Ada')
117
+ expect(result.lastName).toBe('Lovelace')
118
+ })
119
+ })
120
+
121
+ describe('email change', () => {
122
+ it('sets pendingEmail on EmailChangeRequested and updates email on EmailUpdated', () => {
123
+ const requested = withMeta(UsersEvents.EmailChangeRequested({
15
124
  email: 'new@example.com',
16
125
  createdBy: 'user-1',
17
- }),
18
- resourceId: 'user-1',
19
- created: Date.now(),
20
- }
21
- const updated = {
22
- ...UsersEvents.EmailUpdated({
126
+ }), { resourceId: 'user-1', created: NOW + 1 })
127
+ const updated = withMeta(UsersEvents.EmailUpdated({
23
128
  email: 'new@example.com',
24
- previousEmail: 'old@example.com',
129
+ previousEmail: 'ada@example.com',
130
+ createdBy: 'user-1',
131
+ }), { resourceId: 'user-1', created: NOW + 2 })
132
+
133
+ const afterRequest = User.View([createdEvent(), requested])
134
+ expect(afterRequest.email).toBe('ada@example.com')
135
+ expect(afterRequest.pendingEmail).toBe('new@example.com')
136
+
137
+ const afterConfirm = User.View([createdEvent(), requested, updated])
138
+ expect(afterConfirm.email).toBe('new@example.com')
139
+ expect(afterConfirm.pendingEmail).toBeUndefined()
140
+ })
141
+ })
142
+
143
+ describe('WorkspaceJoined and WorkspaceLeft', () => {
144
+ it('adds a workspace id without duplicates', () => {
145
+ const joined = withMeta(UsersEvents.WorkspaceJoined({
146
+ workspaceId: 'ws-1',
147
+ createdBy: 'user-1',
148
+ }))
149
+ const result = User.View([createdEvent(), joined, joined])
150
+ expect(result.workspaces).toEqual(['ws-1'])
151
+ })
152
+
153
+ it('removes a workspace id on WorkspaceLeft', () => {
154
+ const result = User.View([
155
+ createdEvent(),
156
+ withMeta(UsersEvents.WorkspaceJoined({
157
+ workspaceId: 'ws-1',
158
+ createdBy: 'user-1',
159
+ })),
160
+ withMeta(UsersEvents.WorkspaceJoined({
161
+ workspaceId: 'ws-2',
162
+ createdBy: 'user-1',
163
+ }), { created: NOW + 2000 }),
164
+ withMeta(UsersEvents.WorkspaceLeft({
165
+ workspaceId: 'ws-1',
166
+ createdBy: 'user-1',
167
+ }), { created: NOW + 3000 }),
168
+ ])
169
+ expect(result.workspaces).toEqual(['ws-2'])
170
+ })
171
+ })
172
+
173
+ describe('unknown event type', () => {
174
+ it('ignores unknown events and returns state unchanged', () => {
175
+ const unknown = { event: 'SomethingElse', payload: {} }
176
+ expect(User.View([createdEvent(), unknown]).email).toBe('ada@example.com')
177
+ })
178
+ })
179
+ })
180
+
181
+ describe('User.UserForWorkspace', () => {
182
+ it('projects the workspace-facing user fields', () => {
183
+ const projection = User.UserForWorkspace([createdEvent()])
184
+ expect(projection).toEqual({
185
+ id: 'user-1',
186
+ firstName: 'Ada',
187
+ lastName: 'Lovelace',
188
+ email: 'ada@example.com',
189
+ type: 'User',
190
+ })
191
+ })
192
+ })
193
+
194
+ describe('User.History', () => {
195
+ it('returns event types newest first', () => {
196
+ const history = User.History([
197
+ { ...createdEvent(), created: NOW },
198
+ withMeta(UsersEvents.NameUpdated({
199
+ firstName: 'Augusta',
200
+ lastName: 'Byron',
201
+ createdBy: 'user-1',
202
+ }), { created: NOW + 5000 }),
203
+ withMeta(UsersEvents.SignInVerified({
25
204
  createdBy: 'user-1',
26
- }),
27
- resourceId: 'user-1',
28
- created: Date.now() + 1,
29
- }
30
-
31
- const afterRequest = User.View([created, requested])
32
- expect(afterRequest.email).toBe('old@example.com')
33
- expect(afterRequest.pendingEmail).toBe('new@example.com')
34
-
35
- const afterConfirm = User.View([created, requested, updated])
36
- expect(afterConfirm.email).toBe('new@example.com')
37
- expect(afterConfirm.pendingEmail).toBeUndefined()
205
+ token: 'tok',
206
+ }), { created: NOW + 2000 }),
207
+ ])
208
+ expect(history).toEqual([
209
+ { type: 'NameUpdated', created: NOW + 5000 },
210
+ { type: 'SignInVerified', created: NOW + 2000 },
211
+ { type: 'Created', created: NOW },
212
+ ])
38
213
  })
39
214
  })