opticedge-cloud-utils 1.1.13 → 1.1.15

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.
@@ -1,29 +1,29 @@
1
- import { isValidWebhookSignature } from '../../src/tw/utils'
2
- import crypto from 'crypto'
3
-
4
- describe('isValidWebhookSignature', () => {
5
- const secret = 'test_secret'
6
- const body = '{"message":"hello"}'
7
-
8
- it('returns true for a valid signature', () => {
9
- const validSignature = crypto.createHmac('sha256', secret).update(body).digest('hex')
10
-
11
- expect(isValidWebhookSignature(secret, body, validSignature)).toBe(true)
12
- })
13
-
14
- it('returns false for an invalid signature', () => {
15
- const invalidSignature = 'invalidsignature123'
16
-
17
- expect(isValidWebhookSignature(secret, body, invalidSignature)).toBe(false)
18
- })
19
-
20
- it('returns false if body or secret is tampered', () => {
21
- const originalSignature = crypto.createHmac('sha256', secret).update(body).digest('hex')
22
-
23
- // wrong body
24
- expect(isValidWebhookSignature(secret, '{"message":"tampered"}', originalSignature)).toBe(false)
25
-
26
- // wrong secret
27
- expect(isValidWebhookSignature('wrong_secret', body, originalSignature)).toBe(false)
28
- })
29
- })
1
+ import { isValidWebhookSignature } from '../../src/tw/utils'
2
+ import crypto from 'crypto'
3
+
4
+ describe('isValidWebhookSignature', () => {
5
+ const secret = 'test_secret'
6
+ const body = '{"message":"hello"}'
7
+
8
+ it('returns true for a valid signature', () => {
9
+ const validSignature = crypto.createHmac('sha256', secret).update(body).digest('hex')
10
+
11
+ expect(isValidWebhookSignature(secret, body, validSignature)).toBe(true)
12
+ })
13
+
14
+ it('returns false for an invalid signature', () => {
15
+ const invalidSignature = 'invalidsignature123'
16
+
17
+ expect(isValidWebhookSignature(secret, body, invalidSignature)).toBe(false)
18
+ })
19
+
20
+ it('returns false if body or secret is tampered', () => {
21
+ const originalSignature = crypto.createHmac('sha256', secret).update(body).digest('hex')
22
+
23
+ // wrong body
24
+ expect(isValidWebhookSignature(secret, '{"message":"tampered"}', originalSignature)).toBe(false)
25
+
26
+ // wrong secret
27
+ expect(isValidWebhookSignature('wrong_secret', body, originalSignature)).toBe(false)
28
+ })
29
+ })
@@ -1,206 +1,206 @@
1
- /* eslint-disable @typescript-eslint/no-explicit-any */
2
- // tests/wallet.test.ts (or append to your existing test file)
3
- import axios from 'axios'
4
- import { pregenerateInAppWallet } from '../../src/tw/wallet'
5
- import * as secrets from '../../src/secrets'
6
-
7
- jest.mock('axios')
8
- jest.mock('../../src/secrets')
9
-
10
- const mockedAxios = axios as jest.Mocked<typeof axios>
11
- const mockedGetSecret = secrets.getSecret as jest.Mock
12
-
13
- const makeSuccessResponse = (data: any) => ({ data, status: 200 })
14
-
15
- // small helper to synthesize axios-like errors
16
- function makeAxiosError(message: string, status?: number, noResponse = false) {
17
- const err: any = new Error(message)
18
- err.isAxiosError = true
19
- if (!noResponse) {
20
- if (typeof status === 'number') {
21
- err.response = { status, data: { message: `mocked ${status}` } }
22
- } else {
23
- // no numeric status provided but simulate a response
24
- err.response = { status: 500, data: { message: 'mock' } }
25
- }
26
- } else {
27
- // explicitly simulate network error: no response property
28
- delete err.response
29
- }
30
- return err
31
- }
32
-
33
- describe('pregenerateInAppWallet with retry behavior', () => {
34
- const mockProjectId = 'test-project'
35
- const mockClientId = 'test-client-id'
36
- const mockSecretKeyName = 'test-key'
37
- const mockEmail = 'user@example.com'
38
- const mockWalletAddress = '0xabc123'
39
- const mockSecretValue = 'mock-secret-key'
40
-
41
- const realMathRandom = Math.random
42
- const realWarn = console.warn
43
- const realError = console.error
44
-
45
- beforeEach(() => {
46
- jest.clearAllMocks()
47
- // make retry jitter deterministic (so delay becomes 0)
48
- Math.random = jest.fn(() => 0)
49
-
50
- jest
51
- .spyOn(axios, 'isAxiosError')
52
- .mockImplementation((e: any) => Boolean(e && e.isAxiosError) as any)
53
- // silence console.warn/error in passing tests (but still allow spying)
54
- console.warn = jest.fn()
55
- console.error = jest.fn()
56
- })
57
-
58
- afterEach(() => {
59
- // restore
60
- Math.random = realMathRandom
61
- console.warn = realWarn
62
- console.error = realError
63
- })
64
-
65
- it('returns null and logs if secret is missing', async () => {
66
- // simulate secret manager returning nothing
67
- mockedGetSecret.mockResolvedValueOnce(undefined)
68
-
69
- const result = await pregenerateInAppWallet(
70
- mockProjectId,
71
- mockClientId,
72
- mockSecretKeyName,
73
- mockEmail
74
- )
75
-
76
- // should not call axios at all
77
- expect(mockedAxios.post).not.toHaveBeenCalled()
78
- expect(result).toBeNull()
79
-
80
- // console.error should be called with "Missing TW secret key"
81
- expect((console.error as jest.Mock).mock.calls.length).toBeGreaterThanOrEqual(1)
82
- expect((console.error as jest.Mock).mock.calls[0][0]).toEqual('Missing TW secret key')
83
- // second arg should contain the projectId and secretName
84
- expect((console.error as jest.Mock).mock.calls[0][1]).toEqual(
85
- expect.objectContaining({ projectId: mockProjectId, twSecretKeyName: mockSecretKeyName })
86
- )
87
- })
88
-
89
- it('retries on 500 then succeeds and returns address', async () => {
90
- mockedGetSecret.mockResolvedValue(mockSecretValue)
91
-
92
- // first call: server 500 -> rejected
93
- mockedAxios.post
94
- .mockRejectedValueOnce(makeAxiosError('server error', 500))
95
- // second call: success
96
- .mockResolvedValueOnce({
97
- data: {
98
- wallet: { address: mockWalletAddress }
99
- }
100
- } as any)
101
-
102
- const result = await pregenerateInAppWallet(
103
- mockProjectId,
104
- mockClientId,
105
- mockSecretKeyName,
106
- mockEmail
107
- )
108
-
109
- expect(mockedGetSecret).toHaveBeenCalledWith(mockProjectId, mockSecretKeyName)
110
- expect(mockedAxios.post).toHaveBeenCalledTimes(2)
111
- expect(result).toBe(mockWalletAddress)
112
- // onRetry should have been called (we log via console.warn in our version)
113
- expect((console.warn as jest.Mock).mock.calls.length).toBeGreaterThanOrEqual(1)
114
- })
115
-
116
- it('does NOT retry on 400 and returns null immediately', async () => {
117
- mockedGetSecret.mockResolvedValue(mockSecretValue)
118
- mockedAxios.post.mockRejectedValue(makeAxiosError('bad request', 400))
119
-
120
- const result = await pregenerateInAppWallet(
121
- mockProjectId,
122
- mockClientId,
123
- mockSecretKeyName,
124
- mockEmail
125
- )
126
-
127
- expect(mockedAxios.post).toHaveBeenCalledTimes(1)
128
- expect(result).toBeNull()
129
- // console.warn should not indicate retry
130
- expect((console.warn as jest.Mock).mock.calls.length).toBe(0)
131
- })
132
-
133
- it('retries on network/no-response errors and then succeeds', async () => {
134
- mockedGetSecret.mockResolvedValue(mockSecretValue)
135
-
136
- // First: network error (no response)
137
- mockedAxios.post
138
- .mockRejectedValueOnce(makeAxiosError('network down', undefined, true))
139
- .mockResolvedValueOnce({
140
- data: { wallet: { address: mockWalletAddress } }
141
- } as any)
142
-
143
- const result = await pregenerateInAppWallet(
144
- mockProjectId,
145
- mockClientId,
146
- mockSecretKeyName,
147
- mockEmail
148
- )
149
-
150
- expect(mockedAxios.post).toHaveBeenCalledTimes(2)
151
- expect(result).toBe(mockWalletAddress)
152
- })
153
-
154
- it('exhausts retries on repeated 500s and returns null', async () => {
155
- mockedGetSecret.mockResolvedValue(mockSecretValue)
156
-
157
- // Always 500 errors
158
- mockedAxios.post.mockRejectedValue(makeAxiosError('server error', 500))
159
-
160
- const result = await pregenerateInAppWallet(
161
- mockProjectId,
162
- mockClientId,
163
- mockSecretKeyName,
164
- mockEmail
165
- )
166
-
167
- // retry attempts are implementation-dependent (your retry opts). We assert >1 calls
168
- expect(mockedAxios.post.mock.calls.length).toBeGreaterThanOrEqual(1)
169
- // final result should be null when retries exhausted
170
- expect(result).toBeNull()
171
- // error should have been logged
172
- expect((console.error as jest.Mock).mock.calls.length).toBeGreaterThanOrEqual(1)
173
- })
174
-
175
- it('logs invalid wallet shape and returns null when wallet.address is missing (2xx response)', async () => {
176
- mockedGetSecret.mockResolvedValueOnce(mockSecretValue)
177
-
178
- // Simulate a 200 response that has wallet but no address
179
- mockedAxios.post.mockResolvedValueOnce(makeSuccessResponse({ wallet: {} }))
180
-
181
- const result = await pregenerateInAppWallet(
182
- mockProjectId,
183
- mockClientId,
184
- mockSecretKeyName,
185
- mockEmail
186
- )
187
-
188
- // axios was called once, but the function returns null (final failure)
189
- expect(mockedAxios.post).toHaveBeenCalledTimes(1)
190
- expect(result).toBeNull()
191
-
192
- // console.error should be called with the shape error message
193
- expect((console.error as jest.Mock).mock.calls.length).toBeGreaterThanOrEqual(1)
194
-
195
- // the first arg of the logged call should be the string we log in the implementation
196
- // (adjust if your message differs)
197
- const firstArg = (console.error as jest.Mock).mock.calls[0][0]
198
- expect(firstArg).toEqual(
199
- 'Invalid wallet response shape' /* or 'Invalid wallet response shape' */
200
- )
201
-
202
- // the second arg should contain status and data info
203
- const secondArg = (console.error as jest.Mock).mock.calls[0][1]
204
- expect(secondArg).toEqual(expect.objectContaining({ status: 200, data: { wallet: {} } }))
205
- })
206
- })
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ // tests/wallet.test.ts (or append to your existing test file)
3
+ import axios from 'axios'
4
+ import { pregenerateInAppWallet } from '../../src/tw/wallet'
5
+ import * as secrets from '../../src/secrets'
6
+
7
+ jest.mock('axios')
8
+ jest.mock('../../src/secrets')
9
+
10
+ const mockedAxios = axios as jest.Mocked<typeof axios>
11
+ const mockedGetSecret = secrets.getSecret as jest.Mock
12
+
13
+ const makeSuccessResponse = (data: any) => ({ data, status: 200 })
14
+
15
+ // small helper to synthesize axios-like errors
16
+ function makeAxiosError(message: string, status?: number, noResponse = false) {
17
+ const err: any = new Error(message)
18
+ err.isAxiosError = true
19
+ if (!noResponse) {
20
+ if (typeof status === 'number') {
21
+ err.response = { status, data: { message: `mocked ${status}` } }
22
+ } else {
23
+ // no numeric status provided but simulate a response
24
+ err.response = { status: 500, data: { message: 'mock' } }
25
+ }
26
+ } else {
27
+ // explicitly simulate network error: no response property
28
+ delete err.response
29
+ }
30
+ return err
31
+ }
32
+
33
+ describe('pregenerateInAppWallet with retry behavior', () => {
34
+ const mockProjectId = 'test-project'
35
+ const mockClientId = 'test-client-id'
36
+ const mockSecretKeyName = 'test-key'
37
+ const mockEmail = 'user@example.com'
38
+ const mockWalletAddress = '0xabc123'
39
+ const mockSecretValue = 'mock-secret-key'
40
+
41
+ const realMathRandom = Math.random
42
+ const realWarn = console.warn
43
+ const realError = console.error
44
+
45
+ beforeEach(() => {
46
+ jest.clearAllMocks()
47
+ // make retry jitter deterministic (so delay becomes 0)
48
+ Math.random = jest.fn(() => 0)
49
+
50
+ jest
51
+ .spyOn(axios, 'isAxiosError')
52
+ .mockImplementation((e: any) => Boolean(e && e.isAxiosError) as any)
53
+ // silence console.warn/error in passing tests (but still allow spying)
54
+ console.warn = jest.fn()
55
+ console.error = jest.fn()
56
+ })
57
+
58
+ afterEach(() => {
59
+ // restore
60
+ Math.random = realMathRandom
61
+ console.warn = realWarn
62
+ console.error = realError
63
+ })
64
+
65
+ it('returns null and logs if secret is missing', async () => {
66
+ // simulate secret manager returning nothing
67
+ mockedGetSecret.mockResolvedValueOnce(undefined)
68
+
69
+ const result = await pregenerateInAppWallet(
70
+ mockProjectId,
71
+ mockClientId,
72
+ mockSecretKeyName,
73
+ mockEmail
74
+ )
75
+
76
+ // should not call axios at all
77
+ expect(mockedAxios.post).not.toHaveBeenCalled()
78
+ expect(result).toBeNull()
79
+
80
+ // console.error should be called with "Missing TW secret key"
81
+ expect((console.error as jest.Mock).mock.calls.length).toBeGreaterThanOrEqual(1)
82
+ expect((console.error as jest.Mock).mock.calls[0][0]).toEqual('Missing TW secret key')
83
+ // second arg should contain the projectId and secretName
84
+ expect((console.error as jest.Mock).mock.calls[0][1]).toEqual(
85
+ expect.objectContaining({ projectId: mockProjectId, twSecretKeyName: mockSecretKeyName })
86
+ )
87
+ })
88
+
89
+ it('retries on 500 then succeeds and returns address', async () => {
90
+ mockedGetSecret.mockResolvedValue(mockSecretValue)
91
+
92
+ // first call: server 500 -> rejected
93
+ mockedAxios.post
94
+ .mockRejectedValueOnce(makeAxiosError('server error', 500))
95
+ // second call: success
96
+ .mockResolvedValueOnce({
97
+ data: {
98
+ wallet: { address: mockWalletAddress }
99
+ }
100
+ } as any)
101
+
102
+ const result = await pregenerateInAppWallet(
103
+ mockProjectId,
104
+ mockClientId,
105
+ mockSecretKeyName,
106
+ mockEmail
107
+ )
108
+
109
+ expect(mockedGetSecret).toHaveBeenCalledWith(mockProjectId, mockSecretKeyName)
110
+ expect(mockedAxios.post).toHaveBeenCalledTimes(2)
111
+ expect(result).toBe(mockWalletAddress)
112
+ // onRetry should have been called (we log via console.warn in our version)
113
+ expect((console.warn as jest.Mock).mock.calls.length).toBeGreaterThanOrEqual(1)
114
+ })
115
+
116
+ it('does NOT retry on 400 and returns null immediately', async () => {
117
+ mockedGetSecret.mockResolvedValue(mockSecretValue)
118
+ mockedAxios.post.mockRejectedValue(makeAxiosError('bad request', 400))
119
+
120
+ const result = await pregenerateInAppWallet(
121
+ mockProjectId,
122
+ mockClientId,
123
+ mockSecretKeyName,
124
+ mockEmail
125
+ )
126
+
127
+ expect(mockedAxios.post).toHaveBeenCalledTimes(1)
128
+ expect(result).toBeNull()
129
+ // console.warn should not indicate retry
130
+ expect((console.warn as jest.Mock).mock.calls.length).toBe(0)
131
+ })
132
+
133
+ it('retries on network/no-response errors and then succeeds', async () => {
134
+ mockedGetSecret.mockResolvedValue(mockSecretValue)
135
+
136
+ // First: network error (no response)
137
+ mockedAxios.post
138
+ .mockRejectedValueOnce(makeAxiosError('network down', undefined, true))
139
+ .mockResolvedValueOnce({
140
+ data: { wallet: { address: mockWalletAddress } }
141
+ } as any)
142
+
143
+ const result = await pregenerateInAppWallet(
144
+ mockProjectId,
145
+ mockClientId,
146
+ mockSecretKeyName,
147
+ mockEmail
148
+ )
149
+
150
+ expect(mockedAxios.post).toHaveBeenCalledTimes(2)
151
+ expect(result).toBe(mockWalletAddress)
152
+ })
153
+
154
+ it('exhausts retries on repeated 500s and returns null', async () => {
155
+ mockedGetSecret.mockResolvedValue(mockSecretValue)
156
+
157
+ // Always 500 errors
158
+ mockedAxios.post.mockRejectedValue(makeAxiosError('server error', 500))
159
+
160
+ const result = await pregenerateInAppWallet(
161
+ mockProjectId,
162
+ mockClientId,
163
+ mockSecretKeyName,
164
+ mockEmail
165
+ )
166
+
167
+ // retry attempts are implementation-dependent (your retry opts). We assert >1 calls
168
+ expect(mockedAxios.post.mock.calls.length).toBeGreaterThanOrEqual(1)
169
+ // final result should be null when retries exhausted
170
+ expect(result).toBeNull()
171
+ // error should have been logged
172
+ expect((console.error as jest.Mock).mock.calls.length).toBeGreaterThanOrEqual(1)
173
+ })
174
+
175
+ it('logs invalid wallet shape and returns null when wallet.address is missing (2xx response)', async () => {
176
+ mockedGetSecret.mockResolvedValueOnce(mockSecretValue)
177
+
178
+ // Simulate a 200 response that has wallet but no address
179
+ mockedAxios.post.mockResolvedValueOnce(makeSuccessResponse({ wallet: {} }))
180
+
181
+ const result = await pregenerateInAppWallet(
182
+ mockProjectId,
183
+ mockClientId,
184
+ mockSecretKeyName,
185
+ mockEmail
186
+ )
187
+
188
+ // axios was called once, but the function returns null (final failure)
189
+ expect(mockedAxios.post).toHaveBeenCalledTimes(1)
190
+ expect(result).toBeNull()
191
+
192
+ // console.error should be called with the shape error message
193
+ expect((console.error as jest.Mock).mock.calls.length).toBeGreaterThanOrEqual(1)
194
+
195
+ // the first arg of the logged call should be the string we log in the implementation
196
+ // (adjust if your message differs)
197
+ const firstArg = (console.error as jest.Mock).mock.calls[0][0]
198
+ expect(firstArg).toEqual(
199
+ 'Invalid wallet response shape' /* or 'Invalid wallet response shape' */
200
+ )
201
+
202
+ // the second arg should contain status and data info
203
+ const secondArg = (console.error as jest.Mock).mock.calls[0][1]
204
+ expect(secondArg).toEqual(expect.objectContaining({ status: 200, data: { wallet: {} } }))
205
+ })
206
+ })
@@ -1,37 +1,37 @@
1
- // isValidEmail.test.ts
2
- import { describe, it, expect } from '@jest/globals'
3
- import { isValidEmail } from '../src'
4
-
5
- describe('isValidEmail', () => {
6
- it('returns true for simple valid emails', () => {
7
- expect(isValidEmail('test@example.com')).toBe(true)
8
- expect(isValidEmail('user.name@domain.co')).toBe(true)
9
- expect(isValidEmail('user_name+tag@sub.domain.org')).toBe(true)
10
- })
11
-
12
- it('returns false for missing parts', () => {
13
- expect(isValidEmail('')).toBe(false)
14
- expect(isValidEmail('plainaddress')).toBe(false)
15
- expect(isValidEmail('@no-local-part.com')).toBe(false)
16
- expect(isValidEmail('username@')).toBe(false)
17
- expect(isValidEmail('user@domain')).toBe(false)
18
- expect(isValidEmail('username@.com')).toBe(false)
19
- })
20
-
21
- it('handles whitespace correctly', () => {
22
- expect(isValidEmail(' test@example.com ')).toBe(true) // trims input
23
- expect(isValidEmail('\nuser@domain.com\t')).toBe(true)
24
- })
25
-
26
- it('rejects invalid characters or formats', () => {
27
- expect(isValidEmail('user@@domain.com')).toBe(false)
28
- expect(isValidEmail('user@domain,com')).toBe(false)
29
- expect(isValidEmail('user@domain..com')).toBe(false)
30
- expect(isValidEmail('user@.domain.com')).toBe(false)
31
- expect(isValidEmail('user@domain com')).toBe(false)
32
- })
33
-
34
- it('accepts minimal valid domain structures', () => {
35
- expect(isValidEmail('x@y.z')).toBe(true) // still valid
36
- })
37
- })
1
+ // isValidEmail.test.ts
2
+ import { describe, it, expect } from '@jest/globals'
3
+ import { isValidEmail } from '../src'
4
+
5
+ describe('isValidEmail', () => {
6
+ it('returns true for simple valid emails', () => {
7
+ expect(isValidEmail('test@example.com')).toBe(true)
8
+ expect(isValidEmail('user.name@domain.co')).toBe(true)
9
+ expect(isValidEmail('user_name+tag@sub.domain.org')).toBe(true)
10
+ })
11
+
12
+ it('returns false for missing parts', () => {
13
+ expect(isValidEmail('')).toBe(false)
14
+ expect(isValidEmail('plainaddress')).toBe(false)
15
+ expect(isValidEmail('@no-local-part.com')).toBe(false)
16
+ expect(isValidEmail('username@')).toBe(false)
17
+ expect(isValidEmail('user@domain')).toBe(false)
18
+ expect(isValidEmail('username@.com')).toBe(false)
19
+ })
20
+
21
+ it('handles whitespace correctly', () => {
22
+ expect(isValidEmail(' test@example.com ')).toBe(true) // trims input
23
+ expect(isValidEmail('\nuser@domain.com\t')).toBe(true)
24
+ })
25
+
26
+ it('rejects invalid characters or formats', () => {
27
+ expect(isValidEmail('user@@domain.com')).toBe(false)
28
+ expect(isValidEmail('user@domain,com')).toBe(false)
29
+ expect(isValidEmail('user@domain..com')).toBe(false)
30
+ expect(isValidEmail('user@.domain.com')).toBe(false)
31
+ expect(isValidEmail('user@domain com')).toBe(false)
32
+ })
33
+
34
+ it('accepts minimal valid domain structures', () => {
35
+ expect(isValidEmail('x@y.z')).toBe(true) // still valid
36
+ })
37
+ })
package/tsconfig.json CHANGED
@@ -1,17 +1,17 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ES2020",
4
- "module": "CommonJS",
5
- "declaration": true,
6
- "outDir": "./dist",
7
- "strict": true,
8
- "esModuleInterop": true,
9
- "moduleResolution": "node",
10
- "skipLibCheck": true,
11
- "resolveJsonModule": true,
12
- "forceConsistentCasingInFileNames": true,
13
- "types": ["node", "jest"]
14
- },
15
- "include": ["src"],
16
- "exclude": ["node_modules", "dist"]
17
- }
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "CommonJS",
5
+ "declaration": true,
6
+ "outDir": "./dist",
7
+ "strict": true,
8
+ "esModuleInterop": true,
9
+ "moduleResolution": "node",
10
+ "skipLibCheck": true,
11
+ "resolveJsonModule": true,
12
+ "forceConsistentCasingInFileNames": true,
13
+ "types": ["node", "jest"]
14
+ },
15
+ "include": ["src"],
16
+ "exclude": ["node_modules", "dist"]
17
+ }