forked from OurHike/OurHike
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathoutboxSync.test.ts
More file actions
180 lines (144 loc) · 6 KB
/
Copy pathoutboxSync.test.ts
File metadata and controls
180 lines (144 loc) · 6 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
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { renderHook, waitFor } from '@testing-library/react'
import { syncOutbox, useOutboxSync } from './outboxSync'
import { flushOutbox } from './outbox'
import { accessToken, sendReport, permanentFailureReason } from './api'
// #231's other half: the outbox had queued correctly since it was written and
// nothing had ever emptied it.
//
// The property most worth pinning is the overlap guard. `flushOutbox` reads
// the whole queue, sends every item, then writes back the failures - so two
// calls that overlap both read the same queue and file every report twice.
// Coming back into signal is precisely when that happens, because the `online`
// event and a screen mounting can land in the same tick.
vi.mock('./outbox', () => ({ flushOutbox: vi.fn() }))
vi.mock('./api', () => ({
accessToken: vi.fn(),
sendReport: vi.fn(),
permanentFailureReason: vi.fn(),
API_CONFIGURED: true,
}))
const mockedFlush = vi.mocked(flushOutbox)
const mockedToken = vi.mocked(accessToken)
beforeEach(() => {
mockedToken.mockResolvedValue('a-real-token')
mockedFlush.mockResolvedValue({ sent: 1, failed: 0, stuck: 0 })
})
afterEach(() => {
vi.clearAllMocks()
})
describe('syncOutbox', () => {
it('flushes with the real sender, and the real failure classifier', async () => {
// Both arguments matter. Without the classifier every failure looks
// retryable, which is the state #243 is about: a report the server will
// never accept sitting in the queue saying "waiting to send" forever.
await syncOutbox()
expect(mockedFlush).toHaveBeenCalledWith(sendReport, permanentFailureReason)
})
it('does not try when signed out - the queue waits for an account', async () => {
mockedToken.mockResolvedValue(null)
expect(await syncOutbox()).toBeNull()
expect(mockedFlush).not.toHaveBeenCalled()
})
it('reports a flush that ran, so a caller can record a real sync time', async () => {
mockedFlush.mockResolvedValue({ sent: 2, failed: 1, stuck: 0 })
expect(await syncOutbox()).toEqual({ sent: 2, failed: 1, stuck: 0 })
})
it('never rejects, even when the flush itself throws', async () => {
// Background work behind a map someone is navigating by. An unhandled
// rejection here would surface as an error over the trail.
mockedFlush.mockRejectedValue(new Error('storage is gone'))
expect(await syncOutbox()).toBeNull()
})
it('does not overlap two flushes, which would file every report twice', async () => {
let release: (value: {
sent: number
failed: number
stuck: number
}) => void = () => {}
mockedFlush.mockReturnValue(
new Promise((resolve) => {
release = resolve
}),
)
// Both started before either can finish - the exact race the guard exists
// for, not two calls one after another.
const first = syncOutbox()
const second = syncOutbox()
release({ sent: 1, failed: 0, stuck: 0 })
await Promise.all([first, second])
expect(mockedFlush).toHaveBeenCalledTimes(1)
})
it('can flush again once the first one has finished', async () => {
// The guard must not latch: coming back into signal a second time has to
// send whatever was written in between.
await syncOutbox()
await syncOutbox()
expect(mockedFlush).toHaveBeenCalledTimes(2)
})
})
describe('useOutboxSync', () => {
it('does nothing while there is no signal', async () => {
const onSynced = vi.fn()
renderHook(() => useOutboxSync(false, onSynced))
// Waiting on the mock rather than asserting immediately: a promise that
// had been scheduled would resolve after this line, and a bare assertion
// would pass whether or not the guard works.
await waitFor(() => expect(mockedToken).not.toHaveBeenCalled())
expect(mockedFlush).not.toHaveBeenCalled()
expect(onSynced).not.toHaveBeenCalled()
})
it('flushes once there is', async () => {
const onSynced = vi.fn()
renderHook(() => useOutboxSync(true, onSynced))
await waitFor(() => expect(mockedFlush).toHaveBeenCalled())
})
it('reports the result back', async () => {
const onSynced = vi.fn()
mockedFlush.mockResolvedValue({ sent: 3, failed: 0, stuck: 0 })
renderHook(() => useOutboxSync(true, onSynced))
await waitFor(() =>
expect(onSynced).toHaveBeenCalledWith({ sent: 3, failed: 0, stuck: 0 }),
)
})
it('flushes when signal arrives, not only when it was there all along', async () => {
const onSynced = vi.fn()
const { rerender } = renderHook(({ enabled }) => useOutboxSync(enabled, onSynced), {
initialProps: { enabled: false },
})
await waitFor(() => expect(mockedFlush).not.toHaveBeenCalled())
rerender({ enabled: true })
await waitFor(() => expect(mockedFlush).toHaveBeenCalledTimes(1))
})
it('says nothing when it could not even try', async () => {
mockedToken.mockResolvedValue(null)
const onSynced = vi.fn()
renderHook(() => useOutboxSync(true, onSynced))
// "Could not try" must not read as "synced": the status strip would say
// "just now" on a device that has never reached the server.
await waitFor(() => expect(mockedToken).toHaveBeenCalled())
expect(onSynced).not.toHaveBeenCalled()
})
})
describe('a build with no backend configured', () => {
afterEach(() => {
vi.resetModules()
})
it('does not even ask for a token', async () => {
// Re-mocked and re-imported rather than asserting on the module-level mock
// above, which is fixed at true. Asserting `API_CONFIGURED === true` there
// would be a test that cannot fail (#175): it would check the mock, not
// the guard.
vi.resetModules()
vi.doMock('./api', () => ({
accessToken: mockedToken,
sendReport,
permanentFailureReason,
API_CONFIGURED: false,
}))
const { syncOutbox: unconfiguredSync } = await import('./outboxSync')
expect(await unconfiguredSync()).toBeNull()
expect(mockedToken).not.toHaveBeenCalled()
expect(mockedFlush).not.toHaveBeenCalled()
})
})