-
-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathqueue-approval-handler.test.ts
More file actions
218 lines (186 loc) · 6.84 KB
/
Copy pathqueue-approval-handler.test.ts
File metadata and controls
218 lines (186 loc) · 6.84 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
/* eslint-disable test/no-import-node-test */
import type {
ApprovalUser,
QueueApprovalDependencies,
} from '#root/bot/features/admin/queue-approval-handler'
import assert from 'node:assert/strict'
import test from 'node:test'
import { buildQueueApproval } from '#root/bot/features/admin/queue-approval-handler'
interface Recorder {
deps: QueueApprovalDependencies
mintCalls: number
pinCalls: number
markMintedCalls: Array<{ userId: number, nftUrl: string }>
releaseCalls: number[]
reworkCalls: number[]
notifiedApproved: number[]
notifiedDeclined: number[]
referralRewards: number[]
errors: string[]
}
interface Overrides {
// claimForMint behaviour: by default true once then false (simulating CAS).
claimForMint?: () => Promise<boolean>
mintThrows?: boolean
pinThrows?: boolean
rewardThrows?: boolean
}
function makeDeps(overrides: Overrides = {}): Recorder {
let claimsTaken = 0
const markMintedCalls: Recorder['markMintedCalls'] = []
const releaseCalls: number[] = []
const reworkCalls: number[] = []
const notifiedApproved: number[] = []
const notifiedDeclined: number[] = []
const referralRewards: number[] = []
const errors: string[] = []
const rec: Partial<Recorder> = {
mintCalls: 0,
pinCalls: 0,
markMintedCalls,
releaseCalls,
reworkCalls,
notifiedApproved,
notifiedDeclined,
referralRewards,
errors,
}
const deps: QueueApprovalDependencies = {
claimForMint:
overrides.claimForMint
?? (async () => {
// First caller wins; subsequent callers lose (atomic CAS emulation).
claimsTaken += 1
return claimsTaken === 1
}),
releaseClaim: async (userId) => {
releaseCalls.push(userId)
},
pinToIpfs: async () => {
rec.pinCalls! += 1
if (overrides.pinThrows) throw new Error('pin down')
return { imageHash: 'imgHash', jsonHash: 'jsonHash' }
},
mintOnChain: async () => {
rec.mintCalls! += 1
if (overrides.mintThrows) throw new Error('chain down')
return 'https://getgems.io/nft/xyz'
},
markMinted: async (userId, nftUrl) => {
markMintedCalls.push({ userId, nftUrl })
},
setRework: async (userId) => {
reworkCalls.push(userId)
},
notifyApproved: async (user) => {
notifiedApproved.push(user.id)
},
notifyDeclined: async (user) => {
notifiedDeclined.push(user.id)
},
rewardReferrer: async (user) => {
if (overrides.rewardThrows) throw new Error('referrer gone')
referralRewards.push(user.id)
},
logError: (m) => errors.push(m),
}
rec.deps = deps
return rec as Recorder
}
function submittedUser(overrides: Partial<ApprovalUser> = {}): ApprovalUser {
return {
id: 1001,
name: 'alice',
wallet: 'EQC_wallet',
votes: 1_000n,
minted: false,
image: '/data/alice/alice_0.png',
nftDescription: 'a hero',
...overrides,
}
}
// APPROVE — happy path
test('approve pins, mints on-chain, flips minted, rewards the referrer, and notifies', async () => {
const rec = makeDeps()
const { approve } = buildQueueApproval(rec.deps)
const result = await approve(submittedUser())
assert.deepEqual(result, { ok: true, nftUrl: 'https://getgems.io/nft/xyz' })
assert.equal(rec.mintCalls, 1)
assert.deepEqual(rec.markMintedCalls, [
{ userId: 1001, nftUrl: 'https://getgems.io/nft/xyz' },
])
assert.deepEqual(rec.referralRewards, [1001], 'referrer rewarded once')
assert.deepEqual(rec.notifiedApproved, [1001])
assert.equal(rec.releaseCalls.length, 0, 'no release on success')
})
test('a referral-reward failure never fails the approved mint', async () => {
const rec = makeDeps({ rewardThrows: true })
const { approve } = buildQueueApproval(rec.deps)
const result = await approve(submittedUser())
assert.equal(result.ok, true, 'mint stays approved')
assert.deepEqual(rec.notifiedApproved, [1001], 'user still notified')
assert.equal(rec.errors.length, 1)
assert.match(rec.errors[0], /Referral reward failed/)
})
// APPROVE — re-entrancy: double approve mints exactly once
test('approve is idempotent — a second approve does NOT mint again', async () => {
const rec = makeDeps() // claimForMint: true once, then false
const { approve } = buildQueueApproval(rec.deps)
const user = submittedUser()
const first = await approve(user)
const second = await approve(user)
assert.equal(first.ok, true)
assert.equal(second.ok, false)
assert.equal(rec.mintCalls, 1, 'mint seam called EXACTLY once on double approve')
assert.equal(rec.markMintedCalls.length, 1)
})
// APPROVE — illegal transitions
test('approve refuses an already-minted user and never mints', async () => {
const rec = makeDeps()
const { approve } = buildQueueApproval(rec.deps)
const result = await approve(submittedUser({ minted: true }))
assert.deepEqual(result, { ok: false, reason: 'already-minted' })
assert.equal(rec.mintCalls, 0)
})
test('approve refuses a user with no bound wallet', async () => {
const rec = makeDeps()
const { approve } = buildQueueApproval(rec.deps)
const result = await approve(submittedUser({ wallet: undefined }))
assert.deepEqual(result, { ok: false, reason: 'no-wallet' })
assert.equal(rec.mintCalls, 0)
})
test('approve refuses a user without a draft', async () => {
const rec = makeDeps()
const { approve } = buildQueueApproval(rec.deps)
const result = await approve(submittedUser({ image: undefined }))
assert.deepEqual(result, { ok: false, reason: 'no-draft' })
assert.equal(rec.mintCalls, 0)
})
// APPROVE — failure releases the claim (no NFT delivered)
test('approve releases the claim when the on-chain mint throws', async () => {
const rec = makeDeps({ mintThrows: true })
const { approve } = buildQueueApproval(rec.deps)
const result = await approve(submittedUser())
assert.equal(result.ok, false)
assert.deepEqual(rec.releaseCalls, [1001], 'claim released for retry')
assert.equal(rec.markMintedCalls.length, 0, 'never flips minted on mint failure')
assert.equal(rec.referralRewards.length, 0, 'no reward without a mint')
})
// DECLINE — sets Rework, never mints
test('decline sets Rework and does NOT mint', async () => {
const rec = makeDeps()
const { decline } = buildQueueApproval(rec.deps)
const result = await decline(submittedUser())
assert.deepEqual(result, { ok: true })
assert.deepEqual(rec.reworkCalls, [1001])
assert.deepEqual(rec.notifiedDeclined, [1001])
assert.equal(rec.mintCalls, 0, 'decline never mints')
assert.equal(rec.referralRewards.length, 0, 'decline never rewards')
})
test('decline refuses an already-minted user', async () => {
const rec = makeDeps()
const { decline } = buildQueueApproval(rec.deps)
const result = await decline(submittedUser({ minted: true }))
assert.deepEqual(result, { ok: false, reason: 'already-minted' })
assert.deepEqual(rec.reworkCalls, [], 'no Rework transition for minted user')
})