Skip to content

Commit 65cafea

Browse files
committed
add unit tests for token discovery with retry (#1)
1 parent 69ae01e commit 65cafea

File tree

3 files changed

+210
-14
lines changed

3 files changed

+210
-14
lines changed

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
"@chatie/tsconfig": "^0.16.2",
4545
"@grpc/grpc-js": "^1.3.6",
4646
"@types/uuid": "^8.3.1",
47+
"nock": "^13.1.1",
4748
"pkg-jq": "^0.2.11",
4849
"shx": "^0.3.3",
4950
"tstest": "^0.4.10"
@@ -60,6 +61,7 @@
6061
"dependencies": {
6162
"brolog": "^1.12.4",
6263
"cmd-ts": "^0.7.0",
64+
"cockatiel": "^2.0.2",
6365
"uuid": "^8.3.2"
6466
}
6567
}

src/wechaty-token.spec.ts

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
#!/usr/bin/env ts-node
2+
3+
import {
4+
test,
5+
} from 'tstest'
6+
7+
import nock from 'nock'
8+
9+
import { WechatyToken } from './wechaty-token'
10+
11+
test('WechatyToken.discover() resolved', async t => {
12+
const TOKEN = '__token__'
13+
const EXPECTED_ADDRESS = {
14+
host: '1.2.3.4',
15+
port: 5678,
16+
}
17+
18+
const scope = nock('https://api.chatie.io')
19+
.get(`/v0/hosties/${TOKEN}`)
20+
.reply(200, EXPECTED_ADDRESS)
21+
22+
const wechatyToken = new WechatyToken()
23+
const address = await wechatyToken.discover(TOKEN)
24+
25+
// console.info(address)
26+
t.deepEqual(address, EXPECTED_ADDRESS, 'should get address')
27+
scope.done()
28+
})
29+
30+
test('WechatyToken.discover() with custom authority', async t => {
31+
const AUTHORITY = 'authority.com'
32+
const TOKEN = '__token__'
33+
const EXPECTED_ADDRESS = {
34+
host: '1.2.3.4',
35+
port: 5678,
36+
}
37+
38+
const scope = nock(`https://${AUTHORITY}`)
39+
.get(`/v0/hosties/${TOKEN}`)
40+
.reply(200, EXPECTED_ADDRESS)
41+
42+
const wechatyToken = new WechatyToken(AUTHORITY)
43+
const address = await wechatyToken.discover(TOKEN)
44+
45+
// console.info(address)
46+
t.deepEqual(address, EXPECTED_ADDRESS, `should get address from authority ${AUTHORITY}`)
47+
scope.done()
48+
})
49+
50+
test('WechatyToken.discover() retry succeed: HTTP/5XX <= 3', async t => {
51+
const TOKEN = '__token__'
52+
const EXPECTED_ADDRESS = {
53+
host: '1.2.3.4',
54+
port: 5678,
55+
}
56+
57+
const scope500 = nock('https://api.chatie.io')
58+
.get(`/v0/hosties/${TOKEN}`)
59+
.reply(500)
60+
const scope501 = nock('https://api.chatie.io')
61+
.get(`/v0/hosties/${TOKEN}`)
62+
.reply(501)
63+
const scope502 = nock('https://api.chatie.io')
64+
.get(`/v0/hosties/${TOKEN}`)
65+
.reply(502)
66+
67+
const scope200 = nock('https://api.chatie.io')
68+
.get(`/v0/hosties/${TOKEN}`)
69+
.reply(200, EXPECTED_ADDRESS)
70+
71+
const wechatyToken = new WechatyToken()
72+
const address = await wechatyToken.discover(TOKEN)
73+
74+
// console.info(address)
75+
t.deepEqual(address, EXPECTED_ADDRESS, 'should get address')
76+
scope200.done()
77+
scope500.done()
78+
scope501.done()
79+
scope502.done()
80+
})
81+
82+
test('WechatyToken.discover() retry failed: too many HTTP/500 (>3)', async t => {
83+
const TOKEN = '__token__'
84+
const EXPECTED_ADDRESS = {
85+
host: '1.2.3.4',
86+
port: 5678,
87+
}
88+
89+
const scope500 = nock('https://api.chatie.io')
90+
.get(`/v0/hosties/${TOKEN}`)
91+
.reply(500)
92+
const scope501 = nock('https://api.chatie.io')
93+
.get(`/v0/hosties/${TOKEN}`)
94+
.reply(501)
95+
const scope502 = nock('https://api.chatie.io')
96+
.get(`/v0/hosties/${TOKEN}`)
97+
.reply(502)
98+
const scope503 = nock('https://api.chatie.io')
99+
.get(`/v0/hosties/${TOKEN}`)
100+
.reply(503)
101+
102+
const scope200 = nock('https://api.chatie.io')
103+
.get(`/v0/hosties/${TOKEN}`)
104+
.reply(200, EXPECTED_ADDRESS)
105+
106+
const wechatyToken = new WechatyToken()
107+
const address = await wechatyToken.discover(TOKEN)
108+
109+
// console.info(address)
110+
t.equal(address, undefined, 'should not get address')
111+
112+
t.equal(scope200.isDone(), false, 'should not call the 4th times')
113+
/**
114+
* https://github.com/nock/nock/issues/1495#issuecomment-791079068
115+
*/
116+
;(scope200 as any).interceptors.forEach(nock.removeInterceptor)
117+
118+
scope500.done()
119+
scope501.done()
120+
scope502.done()
121+
scope503.done()
122+
})
123+
124+
test('WechatyToken.discover() 404', async t => {
125+
const TOKEN = '__token__'
126+
127+
const scope = nock('https://api.chatie.io')
128+
.get(`/v0/hosties/${TOKEN}`)
129+
.reply(404)
130+
131+
const wechatyToken = new WechatyToken()
132+
const address = await wechatyToken.discover(TOKEN)
133+
134+
// console.info(address)
135+
t.equal(address, undefined, 'should get undefined for 404 NotFound')
136+
scope.done()
137+
})

src/wechaty-token.ts

Lines changed: 71 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@ import {
1212
VERSION,
1313
} from './version'
1414

15+
import {
16+
Policy,
17+
RetryPolicy,
18+
} from 'cockatiel'
19+
1520
interface PuppetServiceAddress {
1621
host: string,
1722
port: number,
@@ -34,6 +39,14 @@ class WechatyToken {
3439

3540
public authority: string
3641

42+
/**
43+
* Create a retry policy that'll try whatever function we execute 3
44+
* times with a randomized exponential backoff.
45+
*
46+
* https://github.com/connor4312/cockatiel#policyretry
47+
*/
48+
private retry: RetryPolicy
49+
3750
constructor (
3851
authority?: string,
3952
) {
@@ -43,22 +56,43 @@ class WechatyToken {
4356
log.silly('WechatyToken', 'constructor() authority not set, use the default value "%s"', DEFAULT_AUTHORITY)
4457
}
4558
this.authority = authority || DEFAULT_AUTHORITY
46-
}
4759

48-
async discover (token: string): Promise<undefined | PuppetServiceAddress> {
49-
log.verbose('WechatyToken', 'discover(%s)', token)
60+
this.retry = Policy
61+
.handleAll()
62+
.retry()
63+
.attempts(3)
64+
.exponential()
5065

51-
const url = `https://${this.authority}/v0/hosties/${token}`
66+
this.initRetry()
67+
}
68+
69+
private initRetry () {
70+
this.retry.onRetry(reason => log.silly('WechatyToken',
71+
'constructor() this.retry.onRetry() reason: "%s"',
72+
JSON.stringify(reason),
73+
))
74+
this.retry.onSuccess(({ duration }) => log.silly('WechatyToken',
75+
'initRetry() onSuccess(): retry call ran in %s ms',
76+
duration,
77+
))
78+
}
5279

53-
const jsonStr = await new Promise<undefined | string>((resolve, reject) => {
80+
private discoverApi (url: string): Promise<undefined | string> {
81+
return new Promise<undefined | string>((resolve, reject) => {
5482
const httpClient = /^https:\/\//.test(url) ? https : http
5583
httpClient.get(url, function (res) {
56-
/**
57-
* Token service discovery fail: not exist
58-
*/
59-
if (/^4/.test(String(res.statusCode))) {
60-
resolve(undefined) // 4XX NotFound
61-
return
84+
if (/^4/.test('' + res.statusCode)) {
85+
/**
86+
* 4XX Not Found: Token service discovery fail: not exist
87+
*/
88+
return resolve(undefined) // 4XX NotFound
89+
} else if (!/^2/.test(String(res.statusCode))) {
90+
/**
91+
* Non-2XX: unknown error
92+
*/
93+
const e = new Error(`Unknown error: HTTP/${res.statusCode}`)
94+
log.warn('WechatyToken', 'discoverApi() httpClient.get() %s', e.message)
95+
return reject(e)
6296
}
6397

6498
let body = ''
@@ -74,17 +108,37 @@ class WechatyToken {
74108
reject(new Error(`WechatyToken discover() endpoint<${url}> rejection: ${e}`))
75109
})
76110
})
111+
}
112+
113+
async discover (token: string): Promise<undefined | PuppetServiceAddress> {
114+
log.verbose('WechatyToken', 'discover(%s)', token)
115+
116+
const url = `https://${this.authority}/v0/hosties/${token}`
117+
118+
let jsonStr: undefined | string
119+
120+
try {
121+
jsonStr = await this.retry.execute(
122+
() => this.discoverApi(url)
123+
)
124+
} catch (e) {
125+
log.warn('WechatyToken', 'discover() retry.execute(discoverApi) fail: %s', e.message)
126+
// console.error(e)
127+
return undefined
128+
}
77129

78130
/**
79-
* Token service discovery: Not Found
80-
*/
131+
* Token service discovery: Not Found
132+
*/
81133
if (!jsonStr) {
82134
return undefined
83135
}
84136

85137
try {
86138
const result = JSON.parse(jsonStr) as PuppetServiceAddress
87-
return result
139+
if (result.host && result.port) {
140+
return result
141+
}
88142

89143
} catch (e) {
90144
console.error([
@@ -94,8 +148,11 @@ class WechatyToken {
94148
jsonStr,
95149
'----- jsonStr END -----',
96150
].join('\n'))
151+
console.error(e)
152+
return undefined
97153
}
98154

155+
log.warn('WechatyToken', 'discover() address is malformed: "%s"', jsonStr)
99156
return undefined
100157
}
101158

0 commit comments

Comments
 (0)