@pbvision/cloud-run-service 0.0.46
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/.dockerignore +10 -0
- package/.env +9 -0
- package/.eslintrc.json +51 -0
- package/.gcloudignore +9 -0
- package/.github/workflows/check-pr.yml +33 -0
- package/.github/workflows/publish-to-github-npm.yml +36 -0
- package/.vscode/extensions.json +9 -0
- package/.vscode/settings.json +39 -0
- package/Dockerfile +18 -0
- package/LICENSE +201 -0
- package/README.md +49 -0
- package/babel.config.cjs +12 -0
- package/docs/cloudbuild/preview.example.yaml +76 -0
- package/docs/cloudbuild/release.example.yaml +74 -0
- package/jest.config.json +24 -0
- package/package.json +69 -0
- package/src/analytics.js +164 -0
- package/src/app.js +31 -0
- package/src/call-service-api.js +25 -0
- package/src/index.js +17 -0
- package/src/main.js +92 -0
- package/src/placeholder.js +71 -0
- package/src/port.js +2 -0
- package/src/tasks.js +117 -0
- package/src/utils.js +44 -0
- package/test/base-test.js +17 -0
- package/test/main.js +4 -0
- package/test/unit-test-analytics.js +280 -0
- package/test/unit-test-call-service-api.js +96 -0
- package/test/unit-test-placeholder.js +10 -0
- package/test/unit-test-tasks.js +133 -0
- package/test/unit-test-utils.js +44 -0
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import crypto from 'node:crypto'
|
|
2
|
+
|
|
3
|
+
import { mixpanelToken } from '../src/analytics.js'
|
|
4
|
+
|
|
5
|
+
import { AppTest, runTests } from './base-test.js'
|
|
6
|
+
|
|
7
|
+
class AnalyticsTest extends AppTest {
|
|
8
|
+
async beforeEach () {
|
|
9
|
+
await super.beforeEach()
|
|
10
|
+
// mock using node-fetch to request the mixpanel APIs
|
|
11
|
+
this.fetchMock.mockResp(1)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async testMixpanelAPIFailure () {
|
|
15
|
+
this.fetchMock.mockResp(0)
|
|
16
|
+
await this.sendBasicEvent(null, 551)
|
|
17
|
+
|
|
18
|
+
this.fetchMock.mockResp('', 500)
|
|
19
|
+
await this.sendBasicEvent(null, 551)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async testNoAnalyticsLogged () {
|
|
23
|
+
await this.app.post('/analytics')
|
|
24
|
+
.send({})
|
|
25
|
+
.expect(200)
|
|
26
|
+
expect(this.fetchMock).not.toHaveBeenCalled()
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async testSendingEvents () {
|
|
30
|
+
await this.sendBasicEvent()
|
|
31
|
+
expect(this.fetchMock).toHaveBeenCalledWith(
|
|
32
|
+
'https://api.mixpanel.com/track',
|
|
33
|
+
expect.objectContaining({
|
|
34
|
+
headers: { accept: 'text/plain', 'content-type': 'application/json' },
|
|
35
|
+
method: 'POST',
|
|
36
|
+
compress: true
|
|
37
|
+
}))
|
|
38
|
+
const calls = this.fetchMock.mock.calls
|
|
39
|
+
expect(calls.length).toBe(1)
|
|
40
|
+
const actualBody = JSON.parse(calls[0][1].body)
|
|
41
|
+
expect(actualBody).toEqual([{
|
|
42
|
+
event: 'some event',
|
|
43
|
+
properties: expect.objectContaining({
|
|
44
|
+
cool: 1,
|
|
45
|
+
hi: 'world',
|
|
46
|
+
$user_id: 'some uid',
|
|
47
|
+
token: mixpanelToken
|
|
48
|
+
})
|
|
49
|
+
}])
|
|
50
|
+
const props = actualBody[0].properties
|
|
51
|
+
expect(typeof props.ip).toBe('string')
|
|
52
|
+
expect(new Date().getTime() - props.time).toBeLessThan(5000)
|
|
53
|
+
expect(props.$insert_id).toMatch(/[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}/)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async sendBasicEvent (userAgent, expRespCode = 200) {
|
|
57
|
+
const req = this.app.post('/analytics')
|
|
58
|
+
if (userAgent) {
|
|
59
|
+
req.set('User-Agent', userAgent)
|
|
60
|
+
}
|
|
61
|
+
await req.send({
|
|
62
|
+
eventCalls: [
|
|
63
|
+
['some uid', 'some event', { cool: 1, hi: 'world' }]
|
|
64
|
+
]
|
|
65
|
+
}).expect(expRespCode)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async testSendInBatches () {
|
|
69
|
+
await this.app.post('/analytics')
|
|
70
|
+
.send({
|
|
71
|
+
eventCalls: [
|
|
72
|
+
['xyz', 'some event', { cool: 1, hi: 'world' }]
|
|
73
|
+
],
|
|
74
|
+
moreEventCalls: [
|
|
75
|
+
['xyz', 'another event', { x: 2 }]
|
|
76
|
+
]
|
|
77
|
+
}).expect(200)
|
|
78
|
+
const calls = this.fetchMock.mock.calls
|
|
79
|
+
expect(calls.length).toBe(2)
|
|
80
|
+
const actualBody = JSON.parse(calls[0][1].body)
|
|
81
|
+
expect(actualBody).toEqual([{
|
|
82
|
+
event: 'some event',
|
|
83
|
+
properties: expect.objectContaining({
|
|
84
|
+
cool: 1,
|
|
85
|
+
hi: 'world',
|
|
86
|
+
$user_id: 'xyz',
|
|
87
|
+
token: mixpanelToken
|
|
88
|
+
})
|
|
89
|
+
}])
|
|
90
|
+
const actualBody2 = JSON.parse(calls[1][1].body)
|
|
91
|
+
expect(actualBody2).toEqual([{
|
|
92
|
+
event: 'another event',
|
|
93
|
+
properties: expect.objectContaining({
|
|
94
|
+
x: 2,
|
|
95
|
+
$user_id: 'xyz',
|
|
96
|
+
token: mixpanelToken
|
|
97
|
+
})
|
|
98
|
+
}])
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async testDeviceId () {
|
|
102
|
+
await this.app.post('/analytics')
|
|
103
|
+
.send({
|
|
104
|
+
eventCalls: [
|
|
105
|
+
['$device:xyz', 'some event', { cool: 1, hi: 'world' }]
|
|
106
|
+
]
|
|
107
|
+
}).expect(200)
|
|
108
|
+
const calls = this.fetchMock.mock.calls
|
|
109
|
+
expect(calls.length).toBe(1)
|
|
110
|
+
const actualBody = JSON.parse(calls[0][1].body)
|
|
111
|
+
expect(actualBody).toEqual([{
|
|
112
|
+
event: 'some event',
|
|
113
|
+
properties: expect.objectContaining({
|
|
114
|
+
cool: 1,
|
|
115
|
+
hi: 'world',
|
|
116
|
+
distinct_id: '$device:xyz',
|
|
117
|
+
$device_id: 'xyz',
|
|
118
|
+
token: mixpanelToken
|
|
119
|
+
})
|
|
120
|
+
}])
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async testDeviceIdWithUserId () {
|
|
124
|
+
await this.app.post('/analytics')
|
|
125
|
+
.send({
|
|
126
|
+
eventCalls: [
|
|
127
|
+
['some uid', 'some event', { cool: 1, hi: 'world' }, '$device:xyz']
|
|
128
|
+
]
|
|
129
|
+
}).expect(200)
|
|
130
|
+
const calls = this.fetchMock.mock.calls
|
|
131
|
+
expect(calls.length).toBe(1)
|
|
132
|
+
const actualBody = JSON.parse(calls[0][1].body)
|
|
133
|
+
expect(actualBody).toEqual([{
|
|
134
|
+
event: 'some event',
|
|
135
|
+
properties: expect.objectContaining({
|
|
136
|
+
cool: 1,
|
|
137
|
+
hi: 'world',
|
|
138
|
+
$user_id: 'some uid',
|
|
139
|
+
distinct_id: 'some uid',
|
|
140
|
+
$device_id: 'xyz',
|
|
141
|
+
token: mixpanelToken
|
|
142
|
+
})
|
|
143
|
+
}])
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async testCustomInsertionId () {
|
|
147
|
+
await this.app.post('/analytics')
|
|
148
|
+
.send({
|
|
149
|
+
eventCalls: [
|
|
150
|
+
['some uid', 'some event', { cool: 1, hi: 'world' }, null, 'xx']
|
|
151
|
+
]
|
|
152
|
+
}).expect(200)
|
|
153
|
+
const calls = this.fetchMock.mock.calls
|
|
154
|
+
expect(calls.length).toBe(1)
|
|
155
|
+
const actualBody = JSON.parse(calls[0][1].body)
|
|
156
|
+
expect(actualBody).toEqual([{
|
|
157
|
+
event: 'some event',
|
|
158
|
+
properties: expect.objectContaining({
|
|
159
|
+
cool: 1,
|
|
160
|
+
hi: 'world',
|
|
161
|
+
$user_id: 'some uid',
|
|
162
|
+
token: mixpanelToken,
|
|
163
|
+
$insert_id: crypto.createHash('md5').update('xx').digest('hex')
|
|
164
|
+
})
|
|
165
|
+
}])
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async testOmittingEventProperties () {
|
|
169
|
+
await this.app.post('/analytics')
|
|
170
|
+
.send({
|
|
171
|
+
eventCalls: [
|
|
172
|
+
['some uid', 'some event']
|
|
173
|
+
]
|
|
174
|
+
}).expect(200)
|
|
175
|
+
const calls = this.fetchMock.mock.calls
|
|
176
|
+
expect(calls.length).toBe(1)
|
|
177
|
+
const actualBody = JSON.parse(calls[0][1].body)
|
|
178
|
+
expect(actualBody).toEqual([{
|
|
179
|
+
event: 'some event',
|
|
180
|
+
properties: expect.objectContaining({
|
|
181
|
+
$user_id: 'some uid',
|
|
182
|
+
token: mixpanelToken
|
|
183
|
+
})
|
|
184
|
+
}])
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async testUserAgent () {
|
|
188
|
+
await this.sendBasicEvent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36')
|
|
189
|
+
const calls = this.fetchMock.mock.calls
|
|
190
|
+
expect(calls.length).toBe(1)
|
|
191
|
+
const actualBody = JSON.parse(calls[0][1].body)
|
|
192
|
+
const props = actualBody[0].properties
|
|
193
|
+
expect(props.$browser).toBe('Chrome 122.0.0.0')
|
|
194
|
+
expect(props.$device).toBe(undefined)
|
|
195
|
+
expect(props.$os).toBe('Windows 10')
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async testUserAgentWithDevice () {
|
|
199
|
+
await this.sendBasicEvent('Mozilla/5.0 (PlayBook; U; RIM Tablet OS 1.0.0; en-US) AppleWebKit/534.11 (KHTML, like Gecko) Version/7.1.0.7 Safari/534.11')
|
|
200
|
+
const calls = this.fetchMock.mock.calls
|
|
201
|
+
expect(calls.length).toBe(1)
|
|
202
|
+
const actualBody = JSON.parse(calls[0][1].body)
|
|
203
|
+
const props = actualBody[0].properties
|
|
204
|
+
expect(props.$browser).toBe('Safari 7.1.0.7')
|
|
205
|
+
expect(props.$device).toBe('RIM PlayBook tablet')
|
|
206
|
+
expect(props.$os).toBe('RIM Tablet OS 1.0.0')
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async sendUserProfileUpdate (profileUpdates) {
|
|
210
|
+
const req = this.app.post('/analytics')
|
|
211
|
+
await req.send({ profileUpdates }).expect(200)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async testUserProfileUpdates () {
|
|
215
|
+
const uids = ['uid0', 'uid1']
|
|
216
|
+
await this.sendUserProfileUpdate([
|
|
217
|
+
[uids[0], 'p1', true],
|
|
218
|
+
[uids[0], 'p2', 5],
|
|
219
|
+
[uids[1], 'p2', 6],
|
|
220
|
+
[uids[1], 'p1', 'nope', '$set'],
|
|
221
|
+
[uids[1], 'p1', 'cool'],
|
|
222
|
+
[uids[1], 'p1', 'once', '$set_once'],
|
|
223
|
+
[uids[0], 'p1', false],
|
|
224
|
+
[uids[0], 'p3', 'hi']
|
|
225
|
+
])
|
|
226
|
+
const calls = this.fetchMock.mock.calls
|
|
227
|
+
expect(calls.length).toBe(2)
|
|
228
|
+
const setURL = 'https://api.mixpanel.com/engage#profile-set'
|
|
229
|
+
const setOnceURL = 'https://api.mixpanel.com/engage#profile-set-once'
|
|
230
|
+
expect(this.fetchMock).toHaveBeenCalledWith(
|
|
231
|
+
setURL,
|
|
232
|
+
expect.objectContaining({
|
|
233
|
+
headers: { accept: 'text/plain', 'content-type': 'application/json' },
|
|
234
|
+
method: 'POST',
|
|
235
|
+
compress: true
|
|
236
|
+
}))
|
|
237
|
+
expect(this.fetchMock).toHaveBeenCalledWith(
|
|
238
|
+
setOnceURL,
|
|
239
|
+
expect.objectContaining({
|
|
240
|
+
headers: { accept: 'text/plain', 'content-type': 'application/json' },
|
|
241
|
+
method: 'POST',
|
|
242
|
+
compress: true
|
|
243
|
+
}))
|
|
244
|
+
// sort $set first, then $set_once (just so we have a consistent order for
|
|
245
|
+
// the test to check)
|
|
246
|
+
calls.sort((a, b) => a[0].localeCompare(b[0]))
|
|
247
|
+
|
|
248
|
+
// check $set updates
|
|
249
|
+
const actualSetUpdates = JSON.parse(calls[0][1].body)
|
|
250
|
+
actualSetUpdates.sort((a, b) => a.$distinct_id.localeCompare(b.$distinct_id))
|
|
251
|
+
const expectedSets = [
|
|
252
|
+
{ p1: false, p2: 5, p3: 'hi' },
|
|
253
|
+
{ p1: 'cool', p2: 6 }
|
|
254
|
+
]
|
|
255
|
+
for (let i = 0; i < actualSetUpdates.length; i++) {
|
|
256
|
+
const actualArgs = actualSetUpdates[i]
|
|
257
|
+
expect(actualArgs).toEqual({
|
|
258
|
+
$token: mixpanelToken,
|
|
259
|
+
$distinct_id: uids[i],
|
|
260
|
+
$set: expectedSets[i]
|
|
261
|
+
})
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// check $set_once updates
|
|
265
|
+
const actualSetOnceUpdates = JSON.parse(calls[1][1].body)
|
|
266
|
+
const expectedSetOnces = [
|
|
267
|
+
{ p1: 'once' }
|
|
268
|
+
]
|
|
269
|
+
for (let i = 0; i < actualSetOnceUpdates.length; i++) {
|
|
270
|
+
const actualArgs = actualSetOnceUpdates[i]
|
|
271
|
+
expect(actualArgs).toEqual({
|
|
272
|
+
$token: mixpanelToken,
|
|
273
|
+
$distinct_id: uids[1],
|
|
274
|
+
$set_once: expectedSetOnces[i]
|
|
275
|
+
})
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
runTests(AnalyticsTest)
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { jest } from '@jest/globals'
|
|
2
|
+
import { GoogleAuth } from 'google-auth-library'
|
|
3
|
+
|
|
4
|
+
import { AppTest, runTests } from './base-test.js'
|
|
5
|
+
|
|
6
|
+
class TestCallServiceAPI extends AppTest {
|
|
7
|
+
async beforeAll () {
|
|
8
|
+
await super.beforeAll()
|
|
9
|
+
|
|
10
|
+
// mock GoogleAuth because we can't actually get a token during testing
|
|
11
|
+
this.fetchIdToken = jest.fn().mockReturnValue('fake-token')
|
|
12
|
+
jest.spyOn(GoogleAuth.prototype, 'getIdTokenClient').mockImplementation(() => ({
|
|
13
|
+
idTokenProvider: {
|
|
14
|
+
fetchIdToken: this.fetchIdToken
|
|
15
|
+
}
|
|
16
|
+
}))
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async beforeEach () {
|
|
20
|
+
await super.beforeEach()
|
|
21
|
+
// mock using node-fetch to request an API
|
|
22
|
+
this.fetchMock.mockResp()
|
|
23
|
+
this.fetchIdToken.mockClear()
|
|
24
|
+
GoogleAuth.prototype.getIdTokenClient.mockClear()
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async afterAll () {
|
|
28
|
+
await super.afterAll()
|
|
29
|
+
jest.restoreAllMocks()
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async check (args, respBody = '', expCode = 200, expPort = 8080) {
|
|
33
|
+
const result = await this.app.post('/callService').send(args).expect(200)
|
|
34
|
+
const resp = result.body
|
|
35
|
+
expect(resp.code).toBe(expCode)
|
|
36
|
+
expect(resp.body).toEqual(respBody)
|
|
37
|
+
|
|
38
|
+
const shouldHaveToken = args.isServiceInternal ?? true
|
|
39
|
+
const expHeaders = args.headers ?? {}
|
|
40
|
+
if (shouldHaveToken) {
|
|
41
|
+
expHeaders.Authorization = 'Bearer fake-token'
|
|
42
|
+
}
|
|
43
|
+
expect(this.fetchMock).toHaveBeenCalledWith(
|
|
44
|
+
`http://localhost:${expPort}${args.path}`, {
|
|
45
|
+
body: args.body,
|
|
46
|
+
headers: expHeaders,
|
|
47
|
+
method: args.method ?? 'POST',
|
|
48
|
+
compress: true
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
// make sure Google Auth was called with appropriate arguments (or not, if
|
|
52
|
+
// this wasn't an internal call)
|
|
53
|
+
if (shouldHaveToken) {
|
|
54
|
+
const targetAudience = `http://localhost:${expPort}/`
|
|
55
|
+
expect(GoogleAuth.prototype.getIdTokenClient).toHaveBeenCalledWith(targetAudience)
|
|
56
|
+
expect(this.fetchIdToken).toHaveBeenCalledWith(targetAudience)
|
|
57
|
+
} else {
|
|
58
|
+
expect(GoogleAuth.prototype.getIdTokenClient).not.toHaveBeenCalled()
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async testServiceCallingItsOwnInternalAPI () {
|
|
63
|
+
await this.check({
|
|
64
|
+
service: process.env.SERVICE,
|
|
65
|
+
path: '/someAPI',
|
|
66
|
+
isServiceInternal: true
|
|
67
|
+
})
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async testServiceCallingItsOwnPublicAPI () {
|
|
71
|
+
await this.check({
|
|
72
|
+
service: process.env.SERVICE,
|
|
73
|
+
path: '/someAPI',
|
|
74
|
+
isServiceInternal: false
|
|
75
|
+
})
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async testServiceCallingAnotherOneOfItsAPIs () {
|
|
79
|
+
this.fetchMock.mockResp({ x: 3 })
|
|
80
|
+
await this.check({
|
|
81
|
+
service: process.env.SERVICE,
|
|
82
|
+
path: '/x/y/z'
|
|
83
|
+
}, JSON.stringify({ x: 3 }))
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async testServiceCallingAnotherService () {
|
|
87
|
+
process.env.LOCAL_SERVICE_PORT_MAP = JSON.stringify({ notMe: 9999 })
|
|
88
|
+
this.fetchMock.mockResp('test resp', 222)
|
|
89
|
+
await this.check({
|
|
90
|
+
service: 'notMe',
|
|
91
|
+
path: '/x'
|
|
92
|
+
}, 'test resp', 222, 9999)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
runTests(TestCallServiceAPI)
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import crypto from 'node:crypto'
|
|
2
|
+
|
|
3
|
+
import { CloudTasksClient } from '@google-cloud/tasks'
|
|
4
|
+
import { expect, jest } from '@jest/globals'
|
|
5
|
+
|
|
6
|
+
import { enqueueCloudTask } from '../src/tasks.js'
|
|
7
|
+
|
|
8
|
+
import { BaseTest, runTests } from './base-test.js'
|
|
9
|
+
|
|
10
|
+
class TestTasks extends BaseTest {
|
|
11
|
+
async beforeAll () {
|
|
12
|
+
await super.beforeAll()
|
|
13
|
+
|
|
14
|
+
process.env.LOCAL_SERVICE_PORT_MAP = JSON.stringify({ internal: 8888 })
|
|
15
|
+
|
|
16
|
+
// mock GoogleAuth because we can't actually get a token during testing
|
|
17
|
+
this.createTaskReturnValue = new Promise(resolve => resolve())
|
|
18
|
+
jest.spyOn(CloudTasksClient.prototype, 'createTask').mockImplementation(
|
|
19
|
+
() => this.createTaskReturnValue)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async beforeEach () {
|
|
23
|
+
await super.beforeEach()
|
|
24
|
+
CloudTasksClient.prototype.createTask.mockClear()
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async afterAll () {
|
|
28
|
+
await super.afterAll()
|
|
29
|
+
jest.restoreAllMocks()
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async check (args, taskExpectations = {}, expRejectionMsg = null) {
|
|
33
|
+
const promise = enqueueCloudTask({
|
|
34
|
+
queue: 'test-queue',
|
|
35
|
+
...args
|
|
36
|
+
})
|
|
37
|
+
if (expRejectionMsg) {
|
|
38
|
+
await expect(promise).rejects.toThrow(expRejectionMsg)
|
|
39
|
+
} else {
|
|
40
|
+
await promise
|
|
41
|
+
}
|
|
42
|
+
expect(CloudTasksClient.prototype.createTask).toHaveBeenCalledWith({
|
|
43
|
+
// assuming values for project (localhost-emulator) and region (us-central1) and
|
|
44
|
+
// service (tbd)
|
|
45
|
+
parent: 'projects/localhost-emulator/locations/us-central1/queues/test-queue',
|
|
46
|
+
task: {
|
|
47
|
+
httpRequest: {
|
|
48
|
+
body: Buffer.from(JSON.stringify(args.payload)).toString('base64'),
|
|
49
|
+
headers: { 'Content-Type': 'application/json' },
|
|
50
|
+
httpMethod: 'POST',
|
|
51
|
+
oidcToken: {
|
|
52
|
+
serviceAccountEmail: 'cr-tbd@localhost-emulator.iam.gserviceaccount.com'
|
|
53
|
+
},
|
|
54
|
+
url: 'http://localhost:8888/test_queue'
|
|
55
|
+
},
|
|
56
|
+
...taskExpectations
|
|
57
|
+
}
|
|
58
|
+
})
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async testEnqueueTask () {
|
|
62
|
+
await this.check({ payload: { x: 3 } })
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async testEnqueueTaskWithDelay () {
|
|
66
|
+
const now = new Date().getTime() / 1000
|
|
67
|
+
await this.check(
|
|
68
|
+
{ payload: { x: 3 }, delaySecs: 10 },
|
|
69
|
+
{ scheduleTime: { seconds: expect.closeTo(now + 10, -0.7) } })
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async testEnqueueTaskWithSchedule () {
|
|
73
|
+
const target = 100 + Math.floor(new Date().getTime() / 1000)
|
|
74
|
+
await this.check(
|
|
75
|
+
{ payload: { x: 3 }, scheduledEpoch: target },
|
|
76
|
+
{ scheduleTime: { seconds: target } })
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async testEnqueueTaskWithName () {
|
|
80
|
+
await this.check(
|
|
81
|
+
{ name: 'x', payload: { x: 3 } },
|
|
82
|
+
{ name: 'projects/localhost-emulator/locations/us-central1/queues/test-queue/tasks/x' })
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async testEnqueueTaskWithHashName () {
|
|
86
|
+
const partsToCheck = [
|
|
87
|
+
[], ['x'], ['x', 'yyy', '']
|
|
88
|
+
]
|
|
89
|
+
for (const hashNameParts of partsToCheck) {
|
|
90
|
+
const expName = crypto.createHash('md5').update(
|
|
91
|
+
['test-queue'].concat(hashNameParts).join('|')).digest('hex')
|
|
92
|
+
await this.check(
|
|
93
|
+
{ hashNameParts, payload: { x: 3 } },
|
|
94
|
+
{ name: 'projects/localhost-emulator/locations/us-central1/queues/test-queue/tasks/' + expName })
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async testEnqueueTaskWithNameThatWasRecentlyUsedNOTOkay () {
|
|
99
|
+
this.createTaskReturnValue = new Promise((resolve, reject) => {
|
|
100
|
+
const err = new Error('6 ALREADY_EXISTS and other random details')
|
|
101
|
+
err.code = 6
|
|
102
|
+
reject(err)
|
|
103
|
+
})
|
|
104
|
+
await this.check(
|
|
105
|
+
{ name: 'x', payload: { x: 3 }, ignoreNameAlreadyUsedError: true },
|
|
106
|
+
{ name: 'projects/localhost-emulator/locations/us-central1/queues/test-queue/tasks/x' })
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async testEnqueueTaskWithNameThatWasRecentlyUsedAndThatsOkay () {
|
|
110
|
+
this.createTaskReturnValue = new Promise((resolve, reject) => {
|
|
111
|
+
const err = new Error('6 ALREADY_EXISTS and other random details')
|
|
112
|
+
err.code = 6
|
|
113
|
+
reject(err)
|
|
114
|
+
})
|
|
115
|
+
await this.check(
|
|
116
|
+
{ name: 'x', payload: { x: 3 } },
|
|
117
|
+
{ name: 'projects/localhost-emulator/locations/us-central1/queues/test-queue/tasks/x' },
|
|
118
|
+
'task name already used recently: x')
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async testEnqueueTaskThrowsUnknownExceptions () {
|
|
122
|
+
this.createTaskReturnValue = new Promise((resolve, reject) => {
|
|
123
|
+
const err = new Error('ALREADY_EXISTS but not in right format')
|
|
124
|
+
reject(err)
|
|
125
|
+
})
|
|
126
|
+
await this.check(
|
|
127
|
+
{ name: 'x', payload: { x: 3 } },
|
|
128
|
+
{ name: 'projects/localhost-emulator/locations/us-central1/queues/test-queue/tasks/x' },
|
|
129
|
+
'not in right format')
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
runTests(TestTasks)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { jest } from '@jest/globals'
|
|
2
|
+
|
|
3
|
+
import { BaseTest, runTests } from '../node_modules/@pbvision/fastify-firestore-service/test/base-test.js'
|
|
4
|
+
import { port } from '../src/port.js'
|
|
5
|
+
import { getServiceHost, isCloud, isDev, isLocalhost, isProd, usingEmulator } from '../src/utils.js'
|
|
6
|
+
|
|
7
|
+
const ORIG_ENV = process.env
|
|
8
|
+
|
|
9
|
+
class TestUtils extends BaseTest {
|
|
10
|
+
beforeEach () {
|
|
11
|
+
jest.resetModules()
|
|
12
|
+
process.env = { ...ORIG_ENV }
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
afterEach () {
|
|
16
|
+
process.env = ORIG_ENV
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
testEnv () {
|
|
20
|
+
expect(process.env.GIT_HASH).toBe(undefined)
|
|
21
|
+
expect(process.env.K_REVISION).toBe('unittest')
|
|
22
|
+
expect(process.env.NODE_ENV).toBe('localhost')
|
|
23
|
+
expect(isLocalhost).toBe(true)
|
|
24
|
+
expect(isCloud).toBe(false)
|
|
25
|
+
expect(isProd).toBe(false)
|
|
26
|
+
expect(isDev).toBe(false)
|
|
27
|
+
expect(usingEmulator).toBe(true)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
testGetServiceHost () {
|
|
31
|
+
expect(getServiceHost(process.env.SERVICE)).toBe(`localhost:${port}`)
|
|
32
|
+
expect(() => getServiceHost('unknown')).toThrow()
|
|
33
|
+
process.env.LOCAL_SERVICE_PORT_MAP = JSON.stringify({ unknown: 8088 })
|
|
34
|
+
expect(getServiceHost('unknown')).toBe('localhost:8088')
|
|
35
|
+
expect(() => getServiceHost('actually_unknown')).toThrow()
|
|
36
|
+
|
|
37
|
+
process.env.NODE_ENV = 'dev'
|
|
38
|
+
const testSuffix = '-tbd-uc.a.run.app'
|
|
39
|
+
process.env.CLOUD_RUN_HOSTNAME_SUFFIX = testSuffix
|
|
40
|
+
expect(getServiceHost('unknown')).toBe('unknown' + testSuffix)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
runTests(TestUtils)
|