@open-mercato/enterprise 0.4.5-develop-2e9903a57a → 0.4.5-develop-754ef4d2f0

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 (32) hide show
  1. package/package.json +4 -4
  2. package/dist/modules/record_locks/__integration__/TC-LOCK-001.spec.js +0 -73
  3. package/dist/modules/record_locks/__integration__/TC-LOCK-001.spec.js.map +0 -7
  4. package/dist/modules/record_locks/__integration__/TC-LOCK-002.spec.js +0 -114
  5. package/dist/modules/record_locks/__integration__/TC-LOCK-002.spec.js.map +0 -7
  6. package/dist/modules/record_locks/__integration__/TC-LOCK-003.spec.js +0 -119
  7. package/dist/modules/record_locks/__integration__/TC-LOCK-003.spec.js.map +0 -7
  8. package/dist/modules/record_locks/__integration__/TC-LOCK-004.spec.js +0 -119
  9. package/dist/modules/record_locks/__integration__/TC-LOCK-004.spec.js.map +0 -7
  10. package/dist/modules/record_locks/__integration__/TC-LOCK-005.spec.js +0 -90
  11. package/dist/modules/record_locks/__integration__/TC-LOCK-005.spec.js.map +0 -7
  12. package/dist/modules/record_locks/__integration__/TC-LOCK-006.spec.js +0 -90
  13. package/dist/modules/record_locks/__integration__/TC-LOCK-006.spec.js.map +0 -7
  14. package/dist/modules/record_locks/__integration__/TC-LOCK-007.spec.js +0 -211
  15. package/dist/modules/record_locks/__integration__/TC-LOCK-007.spec.js.map +0 -7
  16. package/dist/modules/record_locks/__integration__/helpers/recordLocks.js +0 -219
  17. package/dist/modules/record_locks/__integration__/helpers/recordLocks.js.map +0 -7
  18. package/src/modules/record_locks/__integration__/TC-LOCK-001.spec.ts +0 -84
  19. package/src/modules/record_locks/__integration__/TC-LOCK-002.spec.ts +0 -129
  20. package/src/modules/record_locks/__integration__/TC-LOCK-003.spec.ts +0 -136
  21. package/src/modules/record_locks/__integration__/TC-LOCK-004.spec.ts +0 -136
  22. package/src/modules/record_locks/__integration__/TC-LOCK-005.spec.ts +0 -106
  23. package/src/modules/record_locks/__integration__/TC-LOCK-006.spec.ts +0 -113
  24. package/src/modules/record_locks/__integration__/TC-LOCK-007.spec.ts +0 -251
  25. package/src/modules/record_locks/__integration__/helpers/recordLocks.ts +0 -366
  26. package/src/modules/record_locks/__tests__/config.test.ts +0 -21
  27. package/src/modules/record_locks/__tests__/crudMutationGuardService.test.ts +0 -106
  28. package/src/modules/record_locks/__tests__/recordLockService.test.ts +0 -1226
  29. package/src/modules/record_locks/__tests__/recordLockWidgetHeaders.test.ts +0 -127
  30. package/src/modules/record_locks/api/__tests__/acquire.route.test.ts +0 -175
  31. package/src/modules/record_locks/api/__tests__/release.route.test.ts +0 -135
  32. package/src/modules/record_locks/api/__tests__/settings.route.test.ts +0 -85
@@ -1,127 +0,0 @@
1
- import widget from '../widgets/injection/record-locking/widget'
2
- import {
3
- clearRecordLockFormState,
4
- getRecordLockFormState,
5
- setRecordLockFormState,
6
- } from '@open-mercato/enterprise/modules/record_locks/lib/clientLockStore'
7
- import { validateBeforeSave } from '../widgets/injection/record-locking/widget.client'
8
-
9
- jest.mock('../widgets/injection/record-locking/widget.client', () => ({
10
- __esModule: true,
11
- default: () => null,
12
- validateBeforeSave: jest.fn(),
13
- }))
14
-
15
- const mockedValidateBeforeSave = validateBeforeSave as jest.MockedFunction<typeof validateBeforeSave>
16
-
17
- describe('record lock widget resolution headers', () => {
18
- const formId = 'record-lock:test-form'
19
- const conflictId = 'a0000000-0000-4000-8000-000000000001'
20
-
21
- beforeEach(() => {
22
- clearRecordLockFormState(formId)
23
- mockedValidateBeforeSave.mockReset()
24
- mockedValidateBeforeSave.mockResolvedValue({ ok: true })
25
- })
26
-
27
- test('blocks save when resolution intent is not armed', async () => {
28
- setRecordLockFormState(formId, {
29
- formId,
30
- resourceKind: 'customers.deal',
31
- resourceId: 'b0000000-0000-4000-8000-000000000001',
32
- conflict: {
33
- id: conflictId,
34
- resourceKind: 'customers.deal',
35
- resourceId: 'b0000000-0000-4000-8000-000000000001',
36
- baseActionLogId: null,
37
- incomingActionLogId: null,
38
- allowIncomingOverride: true,
39
- canOverrideIncoming: true,
40
- resolutionOptions: ['accept_mine'],
41
- changes: [],
42
- },
43
- pendingConflictId: conflictId,
44
- pendingResolution: 'accept_mine',
45
- pendingResolutionArmed: false,
46
- })
47
-
48
- const result = await widget.eventHandlers.onBeforeSave({}, { formId } as any)
49
- expect(result.ok).toBe(false)
50
- expect(mockedValidateBeforeSave).not.toHaveBeenCalled()
51
- })
52
-
53
- test('does not call validate before save while conflict is unresolved', async () => {
54
- setRecordLockFormState(formId, {
55
- formId,
56
- resourceKind: 'customers.deal',
57
- resourceId: 'b0000000-0000-4000-8000-000000000001',
58
- conflict: {
59
- id: conflictId,
60
- resourceKind: 'customers.deal',
61
- resourceId: 'b0000000-0000-4000-8000-000000000001',
62
- baseActionLogId: null,
63
- incomingActionLogId: null,
64
- allowIncomingOverride: true,
65
- canOverrideIncoming: true,
66
- resolutionOptions: ['accept_mine'],
67
- changes: [],
68
- },
69
- pendingConflictId: conflictId,
70
- pendingResolution: 'normal',
71
- pendingResolutionArmed: false,
72
- })
73
-
74
- const result = await widget.eventHandlers.onBeforeSave({}, { formId } as any)
75
- expect(result.ok).toBe(false)
76
- expect(mockedValidateBeforeSave).not.toHaveBeenCalled()
77
- })
78
-
79
- test('sends resolution header once and disarms it immediately', async () => {
80
- setRecordLockFormState(formId, {
81
- formId,
82
- resourceKind: 'customers.deal',
83
- resourceId: 'b0000000-0000-4000-8000-000000000001',
84
- conflict: {
85
- id: conflictId,
86
- resourceKind: 'customers.deal',
87
- resourceId: 'b0000000-0000-4000-8000-000000000001',
88
- baseActionLogId: null,
89
- incomingActionLogId: null,
90
- allowIncomingOverride: true,
91
- canOverrideIncoming: true,
92
- resolutionOptions: ['accept_mine'],
93
- changes: [],
94
- },
95
- pendingConflictId: conflictId,
96
- pendingResolution: 'accept_mine',
97
- pendingResolutionArmed: true,
98
- })
99
-
100
- const first = await widget.eventHandlers.onBeforeSave({}, { formId } as any)
101
- expect(first.ok).toBe(true)
102
- if (!first.ok) throw new Error('Expected successful first result')
103
- expect(first.requestHeaders?.['x-om-record-lock-resolution']).toBe('accept_mine')
104
-
105
- const consumedState = getRecordLockFormState(formId)
106
- expect(consumedState?.pendingResolution).toBe('normal')
107
- expect(consumedState?.pendingResolutionArmed).toBe(false)
108
-
109
- const second = await widget.eventHandlers.onBeforeSave({}, { formId } as any)
110
- expect(second.ok).toBe(false)
111
- })
112
-
113
- test('blocks save when record was deleted by another user', async () => {
114
- setRecordLockFormState(formId, {
115
- formId,
116
- resourceKind: 'customers.deal',
117
- resourceId: 'b0000000-0000-4000-8000-000000000001',
118
- recordDeleted: true,
119
- })
120
-
121
- const result = await widget.eventHandlers.onBeforeSave({}, { formId } as any)
122
- expect(result.ok).toBe(false)
123
- if (result.ok) throw new Error('Expected blocked save result')
124
- expect(result.message).toContain('deleted')
125
- expect(mockedValidateBeforeSave).not.toHaveBeenCalled()
126
- })
127
- })
@@ -1,175 +0,0 @@
1
- import { POST } from '@open-mercato/enterprise/modules/record_locks/api/acquire/route'
2
- import { resolveRecordLocksApiContext, resolveRequestIp } from '@open-mercato/enterprise/modules/record_locks/api/utils'
3
-
4
- jest.mock('@open-mercato/enterprise/modules/record_locks/api/utils', () => ({
5
- resolveRecordLocksApiContext: jest.fn(),
6
- resolveRequestIp: jest.fn(() => '127.0.0.1'),
7
- }))
8
-
9
- function makeContext(overrides: Record<string, unknown> = {}) {
10
- const emInstance = {
11
- fork: () => ({
12
- findOne: async () => null,
13
- }),
14
- }
15
- const rbacServiceInstance = {
16
- userHasAllFeatures: jest.fn().mockResolvedValue(true),
17
- }
18
-
19
- return {
20
- auth: {
21
- sub: '10000000-0000-4000-8000-000000000001',
22
- tenantId: '20000000-0000-4000-8000-000000000001',
23
- },
24
- organizationId: '30000000-0000-4000-8000-000000000001',
25
- container: {
26
- resolve: (key: string) => {
27
- if (key === 'em') return emInstance
28
- if (key === 'rbacService') return rbacServiceInstance
29
- return null
30
- },
31
- },
32
- ...overrides,
33
- }
34
- }
35
-
36
- function makeRequest(body: unknown) {
37
- return new Request('http://localhost/api/record_locks/acquire', {
38
- method: 'POST',
39
- headers: { 'content-type': 'application/json' },
40
- body: JSON.stringify(body),
41
- })
42
- }
43
-
44
- describe('record_locks acquire route', () => {
45
- beforeEach(() => {
46
- jest.clearAllMocks()
47
- })
48
-
49
- test('returns 400 for invalid payload', async () => {
50
- ;(resolveRecordLocksApiContext as jest.Mock).mockResolvedValue(makeContext({
51
- recordLockService: { acquire: jest.fn() },
52
- }))
53
-
54
- const response = await POST(makeRequest({}))
55
- expect(response.status).toBe(400)
56
- })
57
-
58
- test('returns lock error payload when service reports lock', async () => {
59
- const acquire = jest.fn().mockResolvedValue({
60
- ok: false,
61
- status: 423,
62
- error: 'Record is currently locked by another user',
63
- code: 'record_locked',
64
- allowForceUnlock: false,
65
- lock: null,
66
- })
67
-
68
- ;(resolveRecordLocksApiContext as jest.Mock).mockResolvedValue(makeContext({
69
- recordLockService: { acquire },
70
- }))
71
-
72
- const response = await POST(
73
- makeRequest({
74
- resourceKind: 'sales.quote',
75
- resourceId: '40000000-0000-4000-8000-000000000001',
76
- }),
77
- )
78
-
79
- expect(response.status).toBe(423)
80
- const body = await response.json()
81
- expect(body).toMatchObject({
82
- code: 'record_locked',
83
- error: 'Record is currently locked by another user',
84
- allowForceUnlock: false,
85
- })
86
- expect(resolveRequestIp).toHaveBeenCalled()
87
- })
88
-
89
- test('returns successful acquire response', async () => {
90
- const acquire = jest.fn().mockResolvedValue({
91
- ok: true,
92
- enabled: true,
93
- resourceEnabled: true,
94
- strategy: 'optimistic',
95
- allowForceUnlock: true,
96
- heartbeatSeconds: 30,
97
- acquired: true,
98
- latestActionLogId: null,
99
- lock: null,
100
- })
101
-
102
- ;(resolveRecordLocksApiContext as jest.Mock).mockResolvedValue(makeContext({
103
- recordLockService: { acquire },
104
- }))
105
-
106
- const response = await POST(
107
- makeRequest({
108
- resourceKind: 'sales.quote',
109
- resourceId: '40000000-0000-4000-8000-000000000001',
110
- }),
111
- )
112
-
113
- expect(response.status).toBe(200)
114
- const body = await response.json()
115
- expect(body).toMatchObject({
116
- ok: true,
117
- resourceEnabled: true,
118
- strategy: 'optimistic',
119
- allowForceUnlock: true,
120
- })
121
- })
122
-
123
- test('hides force unlock when user lacks force-release feature', async () => {
124
- const acquire = jest.fn().mockResolvedValue({
125
- ok: true,
126
- enabled: true,
127
- resourceEnabled: true,
128
- strategy: 'optimistic',
129
- allowForceUnlock: true,
130
- heartbeatSeconds: 30,
131
- acquired: false,
132
- latestActionLogId: null,
133
- lock: null,
134
- })
135
- const rbacService = {
136
- userHasAllFeatures: jest.fn().mockResolvedValue(false),
137
- }
138
-
139
- ;(resolveRecordLocksApiContext as jest.Mock).mockResolvedValue(makeContext({
140
- recordLockService: { acquire },
141
- container: {
142
- resolve: (key: string) => {
143
- if (key === 'rbacService') return rbacService
144
- if (key === 'em') {
145
- return {
146
- fork: () => ({
147
- findOne: async () => null,
148
- }),
149
- }
150
- }
151
- return null
152
- },
153
- },
154
- }))
155
-
156
- const response = await POST(
157
- makeRequest({
158
- resourceKind: 'sales.quote',
159
- resourceId: '40000000-0000-4000-8000-000000000001',
160
- }),
161
- )
162
-
163
- expect(response.status).toBe(200)
164
- const body = await response.json()
165
- expect(body.allowForceUnlock).toBe(false)
166
- expect(rbacService.userHasAllFeatures).toHaveBeenCalledWith(
167
- '10000000-0000-4000-8000-000000000001',
168
- ['record_locks.force_release'],
169
- {
170
- tenantId: '20000000-0000-4000-8000-000000000001',
171
- organizationId: '30000000-0000-4000-8000-000000000001',
172
- },
173
- )
174
- })
175
- })
@@ -1,135 +0,0 @@
1
- import { POST } from '@open-mercato/enterprise/modules/record_locks/api/release/route'
2
- import { resolveRecordLocksApiContext } from '@open-mercato/enterprise/modules/record_locks/api/utils'
3
-
4
- jest.mock('@open-mercato/enterprise/modules/record_locks/api/utils', () => ({
5
- resolveRecordLocksApiContext: jest.fn(),
6
- }))
7
-
8
- function makeRequest(body: unknown) {
9
- return new Request('http://localhost/api/record_locks/release', {
10
- method: 'POST',
11
- headers: { 'content-type': 'application/json' },
12
- body: JSON.stringify(body),
13
- })
14
- }
15
-
16
- describe('record_locks release route', () => {
17
- beforeEach(() => {
18
- jest.clearAllMocks()
19
- })
20
-
21
- test('returns 400 when reason=conflict_resolved but conflict payload is missing', async () => {
22
- ;(resolveRecordLocksApiContext as jest.Mock).mockResolvedValue({
23
- auth: {
24
- sub: '10000000-0000-4000-8000-000000000001',
25
- tenantId: '20000000-0000-4000-8000-000000000001',
26
- },
27
- organizationId: '30000000-0000-4000-8000-000000000001',
28
- recordLockService: { release: jest.fn() },
29
- })
30
-
31
- const response = await POST(
32
- makeRequest({
33
- resourceKind: 'sales.quote',
34
- resourceId: '40000000-0000-4000-8000-000000000001',
35
- token: '50000000-0000-4000-8000-000000000001',
36
- reason: 'conflict_resolved',
37
- }),
38
- )
39
-
40
- expect(response.status).toBe(400)
41
- })
42
-
43
- test('passes explicit conflict resolution payload to service and returns result', async () => {
44
- const release = jest.fn().mockResolvedValue({
45
- ok: true,
46
- released: true,
47
- conflictResolved: true,
48
- })
49
-
50
- ;(resolveRecordLocksApiContext as jest.Mock).mockResolvedValue({
51
- auth: {
52
- sub: '10000000-0000-4000-8000-000000000001',
53
- tenantId: '20000000-0000-4000-8000-000000000001',
54
- },
55
- organizationId: '30000000-0000-4000-8000-000000000001',
56
- recordLockService: { release },
57
- })
58
-
59
- const response = await POST(
60
- makeRequest({
61
- resourceKind: 'sales.quote',
62
- resourceId: '40000000-0000-4000-8000-000000000001',
63
- token: '50000000-0000-4000-8000-000000000001',
64
- reason: 'conflict_resolved',
65
- conflictId: '60000000-0000-4000-8000-000000000001',
66
- resolution: 'accept_incoming',
67
- }),
68
- )
69
-
70
- expect(response.status).toBe(200)
71
- const body = await response.json()
72
- expect(body).toEqual({
73
- ok: true,
74
- released: true,
75
- conflictResolved: true,
76
- })
77
- expect(release).toHaveBeenCalledWith({
78
- token: '50000000-0000-4000-8000-000000000001',
79
- resourceKind: 'sales.quote',
80
- resourceId: '40000000-0000-4000-8000-000000000001',
81
- reason: 'conflict_resolved',
82
- conflictId: '60000000-0000-4000-8000-000000000001',
83
- resolution: 'accept_incoming',
84
- tenantId: '20000000-0000-4000-8000-000000000001',
85
- organizationId: '30000000-0000-4000-8000-000000000001',
86
- userId: '10000000-0000-4000-8000-000000000001',
87
- })
88
- })
89
-
90
- test('accepts conflict_resolved payload without token and passes it to service', async () => {
91
- const release = jest.fn().mockResolvedValue({
92
- ok: true,
93
- released: false,
94
- conflictResolved: true,
95
- })
96
-
97
- ;(resolveRecordLocksApiContext as jest.Mock).mockResolvedValue({
98
- auth: {
99
- sub: '10000000-0000-4000-8000-000000000001',
100
- tenantId: '20000000-0000-4000-8000-000000000001',
101
- },
102
- organizationId: '30000000-0000-4000-8000-000000000001',
103
- recordLockService: { release },
104
- })
105
-
106
- const response = await POST(
107
- makeRequest({
108
- resourceKind: 'sales.quote',
109
- resourceId: '40000000-0000-4000-8000-000000000001',
110
- reason: 'conflict_resolved',
111
- conflictId: '60000000-0000-4000-8000-000000000001',
112
- resolution: 'accept_incoming',
113
- }),
114
- )
115
-
116
- expect(response.status).toBe(200)
117
- const body = await response.json()
118
- expect(body).toEqual({
119
- ok: true,
120
- released: false,
121
- conflictResolved: true,
122
- })
123
- expect(release).toHaveBeenCalledWith({
124
- token: undefined,
125
- resourceKind: 'sales.quote',
126
- resourceId: '40000000-0000-4000-8000-000000000001',
127
- reason: 'conflict_resolved',
128
- conflictId: '60000000-0000-4000-8000-000000000001',
129
- resolution: 'accept_incoming',
130
- tenantId: '20000000-0000-4000-8000-000000000001',
131
- organizationId: '30000000-0000-4000-8000-000000000001',
132
- userId: '10000000-0000-4000-8000-000000000001',
133
- })
134
- })
135
- })
@@ -1,85 +0,0 @@
1
- import { GET, POST } from '@open-mercato/enterprise/modules/record_locks/api/settings/route'
2
- import { resolveRecordLocksApiContext } from '@open-mercato/enterprise/modules/record_locks/api/utils'
3
-
4
- jest.mock('@open-mercato/enterprise/modules/record_locks/api/utils', () => ({
5
- resolveRecordLocksApiContext: jest.fn(),
6
- }))
7
-
8
- function makeRequest(body: unknown) {
9
- return new Request('http://localhost/api/record_locks/settings', {
10
- method: 'POST',
11
- headers: { 'content-type': 'application/json' },
12
- body: JSON.stringify(body),
13
- })
14
- }
15
-
16
- describe('record_locks settings route', () => {
17
- beforeEach(() => {
18
- jest.clearAllMocks()
19
- })
20
-
21
- test('GET returns current settings', async () => {
22
- const settings = {
23
- enabled: false,
24
- strategy: 'optimistic',
25
- timeoutSeconds: 300,
26
- heartbeatSeconds: 30,
27
- enabledResources: [],
28
- allowForceUnlock: true,
29
- allowIncomingOverride: true,
30
- notifyOnConflict: true,
31
- }
32
-
33
- ;(resolveRecordLocksApiContext as jest.Mock).mockResolvedValue({
34
- recordLockService: {
35
- getSettings: jest.fn().mockResolvedValue(settings),
36
- },
37
- })
38
-
39
- const response = await GET(new Request('http://localhost/api/record_locks/settings'))
40
- expect(response.status).toBe(200)
41
- const body = await response.json()
42
- expect(body).toEqual({ settings })
43
- })
44
-
45
- test('POST returns 400 when payload is invalid', async () => {
46
- ;(resolveRecordLocksApiContext as jest.Mock).mockResolvedValue({
47
- recordLockService: { saveSettings: jest.fn() },
48
- })
49
-
50
- const response = await POST(makeRequest({ enabled: 'yes' }))
51
- expect(response.status).toBe(400)
52
- })
53
-
54
- test('POST saves and returns settings', async () => {
55
- const settings = {
56
- enabled: true,
57
- strategy: 'pessimistic',
58
- timeoutSeconds: 600,
59
- heartbeatSeconds: 30,
60
- enabledResources: ['sales.quote'],
61
- allowForceUnlock: true,
62
- allowIncomingOverride: true,
63
- notifyOnConflict: true,
64
- }
65
-
66
- const saveSettings = jest.fn().mockResolvedValue(settings)
67
- ;(resolveRecordLocksApiContext as jest.Mock).mockResolvedValue({
68
- recordLockService: { saveSettings },
69
- })
70
-
71
- const response = await POST(makeRequest(settings))
72
- expect(response.status).toBe(200)
73
- const body = await response.json()
74
- expect(body).toEqual({ settings })
75
- expect(saveSettings).toHaveBeenCalledWith(expect.objectContaining({
76
- enabled: true,
77
- strategy: 'pessimistic',
78
- timeoutSeconds: 600,
79
- heartbeatSeconds: 30,
80
- enabledResources: ['sales.quote'],
81
- allowForceUnlock: true,
82
- notifyOnConflict: true,
83
- }))
84
- })
85
- })