@terreno/rtk 0.10.0 → 0.11.1
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/dist/authSlice.test.js +68 -0
- package/dist/authSlice.test.js.map +1 -1
- package/dist/authSliceNative.test.d.ts +2 -0
- package/dist/authSliceNative.test.d.ts.map +1 -0
- package/dist/authSliceNative.test.js +167 -0
- package/dist/authSliceNative.test.js.map +1 -0
- package/dist/betterAuthClient.d.ts +16 -0
- package/dist/betterAuthClient.d.ts.map +1 -1
- package/dist/betterAuthClient.js +5 -2
- package/dist/betterAuthClient.js.map +1 -1
- package/dist/betterAuthClient.test.d.ts +2 -0
- package/dist/betterAuthClient.test.d.ts.map +1 -0
- package/dist/betterAuthClient.test.js +151 -0
- package/dist/betterAuthClient.test.js.map +1 -0
- package/dist/betterAuthSlice.test.js +54 -1
- package/dist/betterAuthSlice.test.js.map +1 -1
- package/dist/buildNumber.test.d.ts +2 -0
- package/dist/buildNumber.test.d.ts.map +1 -0
- package/dist/buildNumber.test.js +95 -0
- package/dist/buildNumber.test.js.map +1 -0
- package/dist/constants.d.ts +27 -3
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +45 -56
- package/dist/constants.js.map +1 -1
- package/dist/constants.test.js +174 -123
- package/dist/constants.test.js.map +1 -1
- package/dist/isolated/useUpgradeCheck.isolated.d.ts +2 -0
- package/dist/isolated/useUpgradeCheck.isolated.d.ts.map +1 -0
- package/dist/isolated/useUpgradeCheck.isolated.js +135 -0
- package/dist/isolated/useUpgradeCheck.isolated.js.map +1 -0
- package/dist/mongooseSlice.test.d.ts +2 -0
- package/dist/mongooseSlice.test.d.ts.map +1 -0
- package/dist/mongooseSlice.test.js +39 -0
- package/dist/mongooseSlice.test.js.map +1 -0
- package/dist/tagGenerator.test.d.ts +2 -0
- package/dist/tagGenerator.test.d.ts.map +1 -0
- package/dist/tagGenerator.test.js +96 -0
- package/dist/tagGenerator.test.js.map +1 -0
- package/dist/testPreload.test.d.ts +2 -0
- package/dist/testPreload.test.d.ts.map +1 -0
- package/dist/testPreload.test.js +27 -0
- package/dist/testPreload.test.js.map +1 -0
- package/dist/useFeatureFlags.d.ts +25 -1
- package/dist/useFeatureFlags.d.ts.map +1 -1
- package/dist/useFeatureFlags.js +18 -16
- package/dist/useFeatureFlags.js.map +1 -1
- package/dist/useFeatureFlags.test.d.ts +2 -0
- package/dist/useFeatureFlags.test.d.ts.map +1 -0
- package/dist/useFeatureFlags.test.js +162 -0
- package/dist/useFeatureFlags.test.js.map +1 -0
- package/dist/useUpgradeCheck.d.ts +2 -0
- package/dist/useUpgradeCheck.d.ts.map +1 -1
- package/dist/useUpgradeCheck.js +39 -46
- package/dist/useUpgradeCheck.js.map +1 -1
- package/dist/useUpgradeCheck.test.d.ts +2 -0
- package/dist/useUpgradeCheck.test.d.ts.map +1 -0
- package/dist/useUpgradeCheck.test.js +326 -0
- package/dist/useUpgradeCheck.test.js.map +1 -0
- package/package.json +6 -3
- package/src/authSlice.test.ts +79 -0
- package/src/authSliceNative.test.ts +187 -0
- package/src/betterAuthClient.test.ts +176 -0
- package/src/betterAuthClient.ts +6 -3
- package/src/betterAuthSlice.test.ts +67 -0
- package/src/buildNumber.test.ts +120 -0
- package/src/constants.test.ts +193 -154
- package/src/constants.ts +72 -70
- package/src/isolated/useUpgradeCheck.isolated.ts +175 -0
- package/src/mongooseSlice.test.ts +46 -0
- package/src/tagGenerator.test.ts +109 -0
- package/src/testPreload.test.ts +30 -0
- package/src/useFeatureFlags.test.ts +209 -0
- package/src/useFeatureFlags.ts +44 -5
- package/src/useUpgradeCheck.test.ts +408 -0
- package/src/useUpgradeCheck.ts +41 -48
- package/dist/test-preload.d.ts +0 -2
- package/dist/test-preload.d.ts.map +0 -1
- package/dist/test-preload.js +0 -24
- package/dist/test-preload.js.map +0 -1
- package/src/test-preload.ts +0 -28
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, mock } from "bun:test";
|
|
2
|
+
import { act, renderHook, waitFor } from "@testing-library/react-native";
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Mutable refs that tests can tweak between runs
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
let mockBuildNumber = 42;
|
|
7
|
+
const mockUnwrap = mock(() => Promise.resolve({ status: "ok" }));
|
|
8
|
+
const mockTrigger = mock((..._args) => ({ unwrap: mockUnwrap }));
|
|
9
|
+
mock.module("./emptyApi", () => ({
|
|
10
|
+
useLazyGetVersionCheckQuery: () => [mockTrigger],
|
|
11
|
+
}));
|
|
12
|
+
// IsWeb is always true in tests (Platform.OS mocked as "web" in preload).
|
|
13
|
+
// We cannot change it per-test because bun snapshots module exports.
|
|
14
|
+
mock.module("./platform", () => ({
|
|
15
|
+
IsWeb: true,
|
|
16
|
+
}));
|
|
17
|
+
// AppState mock — capture listeners so tests can simulate transitions
|
|
18
|
+
let appStateListeners = [];
|
|
19
|
+
const mockRemove = mock(() => { });
|
|
20
|
+
const mockAddEventListener = mock((_event, handler) => {
|
|
21
|
+
appStateListeners.push(handler);
|
|
22
|
+
return { remove: mockRemove };
|
|
23
|
+
});
|
|
24
|
+
const mockOpenURL = mock(() => Promise.resolve(true));
|
|
25
|
+
mock.module("react-native", () => ({
|
|
26
|
+
AppState: {
|
|
27
|
+
addEventListener: mockAddEventListener,
|
|
28
|
+
currentState: "active",
|
|
29
|
+
},
|
|
30
|
+
Linking: { openURL: mockOpenURL },
|
|
31
|
+
Platform: { OS: "web" },
|
|
32
|
+
StyleSheet: { create: (s) => s },
|
|
33
|
+
}));
|
|
34
|
+
mock.module("expo-constants", () => ({
|
|
35
|
+
default: {
|
|
36
|
+
get expoConfig() {
|
|
37
|
+
return { extra: { buildNumber: mockBuildNumber } };
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
}));
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
// window.location.reload mock
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
const mockReload = mock(() => { });
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
// setInterval / clearInterval mocks (bun has no fake timers)
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
const originalSetInterval = globalThis.setInterval;
|
|
49
|
+
const originalClearInterval = globalThis.clearInterval;
|
|
50
|
+
let capturedIntervalMs;
|
|
51
|
+
let intervalIdCounter = 0;
|
|
52
|
+
const mockSetInterval = mock((_cb, ms) => {
|
|
53
|
+
capturedIntervalMs = ms;
|
|
54
|
+
return ++intervalIdCounter;
|
|
55
|
+
});
|
|
56
|
+
const mockClearInterval = mock((_id) => { });
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
// console capture
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
const debugCalls = [];
|
|
61
|
+
const originalDebug = console.debug;
|
|
62
|
+
const originalWarn = console.warn;
|
|
63
|
+
// Now import the hook (after all mock.module calls which are hoisted)
|
|
64
|
+
import { useUpgradeCheck } from "./useUpgradeCheck";
|
|
65
|
+
// Helper: flush microtasks (lets .then() chains resolve)
|
|
66
|
+
const flushPromises = () => new Promise((r) => setTimeout(r, 0));
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
// Setup / teardown
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
beforeEach(() => {
|
|
71
|
+
// Reset mutable refs
|
|
72
|
+
mockBuildNumber = 42;
|
|
73
|
+
appStateListeners = [];
|
|
74
|
+
// Clear call history and set default implementations
|
|
75
|
+
mockUnwrap.mockClear();
|
|
76
|
+
mockUnwrap.mockImplementation(() => Promise.resolve({ status: "ok" }));
|
|
77
|
+
mockTrigger.mockClear();
|
|
78
|
+
mockTrigger.mockImplementation((..._args) => ({ unwrap: mockUnwrap }));
|
|
79
|
+
mockRemove.mockClear();
|
|
80
|
+
mockAddEventListener.mockClear();
|
|
81
|
+
mockAddEventListener.mockImplementation((_event, handler) => {
|
|
82
|
+
appStateListeners.push(handler);
|
|
83
|
+
return { remove: mockRemove };
|
|
84
|
+
});
|
|
85
|
+
mockOpenURL.mockClear();
|
|
86
|
+
mockOpenURL.mockImplementation(() => Promise.resolve(true));
|
|
87
|
+
mockReload.mockClear();
|
|
88
|
+
// Mock window.location.reload
|
|
89
|
+
Object.defineProperty(globalThis, "window", {
|
|
90
|
+
configurable: true,
|
|
91
|
+
value: { location: { reload: mockReload } },
|
|
92
|
+
writable: true,
|
|
93
|
+
});
|
|
94
|
+
// Mock timers
|
|
95
|
+
globalThis.setInterval = mockSetInterval;
|
|
96
|
+
globalThis.clearInterval = mockClearInterval;
|
|
97
|
+
mockSetInterval.mockClear();
|
|
98
|
+
mockSetInterval.mockImplementation((_cb, ms) => {
|
|
99
|
+
capturedIntervalMs = ms;
|
|
100
|
+
return ++intervalIdCounter;
|
|
101
|
+
});
|
|
102
|
+
mockClearInterval.mockClear();
|
|
103
|
+
capturedIntervalMs = undefined;
|
|
104
|
+
// Console capture
|
|
105
|
+
debugCalls.length = 0;
|
|
106
|
+
console.debug = (...args) => {
|
|
107
|
+
debugCalls.push(args);
|
|
108
|
+
};
|
|
109
|
+
console.warn = () => { };
|
|
110
|
+
});
|
|
111
|
+
afterEach(() => {
|
|
112
|
+
console.debug = originalDebug;
|
|
113
|
+
console.warn = originalWarn;
|
|
114
|
+
globalThis.setInterval = originalSetInterval;
|
|
115
|
+
globalThis.clearInterval = originalClearInterval;
|
|
116
|
+
});
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
// Tests
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
describe("useUpgradeCheck", () => {
|
|
121
|
+
describe("mount behavior", () => {
|
|
122
|
+
it("returns default state before check resolves", () => {
|
|
123
|
+
// Make unwrap hang so state stays at defaults
|
|
124
|
+
mockUnwrap.mockImplementation(() => new Promise(() => { }));
|
|
125
|
+
const { result } = renderHook(() => useUpgradeCheck());
|
|
126
|
+
expect(result.current.isRequired).toBe(false);
|
|
127
|
+
expect(result.current.isWarning).toBe(false);
|
|
128
|
+
expect(result.current.requiredMessage).toBeUndefined();
|
|
129
|
+
expect(result.current.warningMessage).toBeUndefined();
|
|
130
|
+
expect(result.current.warningCheckCount).toBe(0);
|
|
131
|
+
});
|
|
132
|
+
it("skips check when buildNumber is undefined", () => {
|
|
133
|
+
mockBuildNumber = undefined;
|
|
134
|
+
renderHook(() => useUpgradeCheck());
|
|
135
|
+
expect(mockTrigger).not.toHaveBeenCalled();
|
|
136
|
+
});
|
|
137
|
+
it("triggers version check on mount with correct params", () => {
|
|
138
|
+
renderHook(() => useUpgradeCheck());
|
|
139
|
+
expect(mockTrigger).toHaveBeenCalledWith({ platform: "web", version: 42 });
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
describe("status handling", () => {
|
|
143
|
+
it("sets isRequired and requiredMessage when status is 'required'", async () => {
|
|
144
|
+
mockUnwrap.mockImplementation(() => Promise.resolve({ message: "Please update now", status: "required" }));
|
|
145
|
+
const { result } = renderHook(() => useUpgradeCheck());
|
|
146
|
+
await act(async () => {
|
|
147
|
+
await flushPromises();
|
|
148
|
+
});
|
|
149
|
+
await waitFor(() => {
|
|
150
|
+
expect(result.current.isRequired).toBe(true);
|
|
151
|
+
expect(result.current.requiredMessage).toBe("Please update now");
|
|
152
|
+
expect(result.current.isWarning).toBe(false);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
it("sets isWarning, warningMessage, and increments warningCheckCount when status is 'warning'", async () => {
|
|
156
|
+
mockUnwrap.mockImplementation(() => Promise.resolve({ message: "New version available", status: "warning" }));
|
|
157
|
+
const { result } = renderHook(() => useUpgradeCheck());
|
|
158
|
+
await act(async () => {
|
|
159
|
+
await flushPromises();
|
|
160
|
+
});
|
|
161
|
+
await waitFor(() => {
|
|
162
|
+
expect(result.current.isWarning).toBe(true);
|
|
163
|
+
expect(result.current.warningMessage).toBe("New version available");
|
|
164
|
+
expect(result.current.warningCheckCount).toBe(1);
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
it("clears required/warning state when status is 'ok' after previous required", async () => {
|
|
168
|
+
// Start with "required"
|
|
169
|
+
mockUnwrap.mockImplementation(() => Promise.resolve({ message: "Update", status: "required" }));
|
|
170
|
+
const { result } = renderHook(() => useUpgradeCheck({ recheckOnForeground: true }));
|
|
171
|
+
await act(async () => {
|
|
172
|
+
await flushPromises();
|
|
173
|
+
});
|
|
174
|
+
await waitFor(() => {
|
|
175
|
+
expect(result.current.isRequired).toBe(true);
|
|
176
|
+
});
|
|
177
|
+
// Switch to "ok" and trigger via foreground recheck (more reliable than interval mock)
|
|
178
|
+
mockUnwrap.mockImplementation(() => Promise.resolve({ status: "ok" }));
|
|
179
|
+
await act(async () => {
|
|
180
|
+
// Simulate background → active transition to trigger runCheck
|
|
181
|
+
appStateListeners.forEach((listener) => {
|
|
182
|
+
listener("background");
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
await act(async () => {
|
|
186
|
+
appStateListeners.forEach((listener) => {
|
|
187
|
+
listener("active");
|
|
188
|
+
});
|
|
189
|
+
await flushPromises();
|
|
190
|
+
});
|
|
191
|
+
await waitFor(() => {
|
|
192
|
+
expect(result.current.isRequired).toBe(false);
|
|
193
|
+
expect(result.current.isWarning).toBe(false);
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
it("stores updateUrl from response", async () => {
|
|
197
|
+
mockUnwrap.mockImplementation(() => Promise.resolve({
|
|
198
|
+
message: "Update available",
|
|
199
|
+
status: "warning",
|
|
200
|
+
updateUrl: "https://example.com/update",
|
|
201
|
+
}));
|
|
202
|
+
const { result } = renderHook(() => useUpgradeCheck());
|
|
203
|
+
await act(async () => {
|
|
204
|
+
await flushPromises();
|
|
205
|
+
});
|
|
206
|
+
await waitFor(() => {
|
|
207
|
+
expect(result.current.isWarning).toBe(true);
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
describe("canUpdate", () => {
|
|
212
|
+
it("is true on web regardless of updateUrl", () => {
|
|
213
|
+
// IsWeb is always true in test env (Platform.OS: "web")
|
|
214
|
+
const { result } = renderHook(() => useUpgradeCheck());
|
|
215
|
+
expect(result.current.canUpdate).toBe(true);
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
describe("onUpdate", () => {
|
|
219
|
+
it("calls window.location.reload on web", () => {
|
|
220
|
+
const { result } = renderHook(() => useUpgradeCheck());
|
|
221
|
+
act(() => {
|
|
222
|
+
result.current.onUpdate();
|
|
223
|
+
});
|
|
224
|
+
expect(mockReload).toHaveBeenCalled();
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
describe("error handling", () => {
|
|
228
|
+
it("handles check failure gracefully", async () => {
|
|
229
|
+
mockUnwrap.mockImplementation(() => Promise.reject(new Error("Network error")));
|
|
230
|
+
const { result } = renderHook(() => useUpgradeCheck());
|
|
231
|
+
await act(async () => {
|
|
232
|
+
await flushPromises();
|
|
233
|
+
});
|
|
234
|
+
await waitFor(() => {
|
|
235
|
+
const failLog = debugCalls.find((args) => typeof args[0] === "string" && args[0].includes("Version check failed"));
|
|
236
|
+
expect(failLog).toBeDefined();
|
|
237
|
+
});
|
|
238
|
+
// State should remain at defaults
|
|
239
|
+
expect(result.current.isRequired).toBe(false);
|
|
240
|
+
expect(result.current.isWarning).toBe(false);
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
describe("polling", () => {
|
|
244
|
+
it("sets up interval when pollingIntervalMs is provided", () => {
|
|
245
|
+
renderHook(() => useUpgradeCheck({ pollingIntervalMs: 60000 }));
|
|
246
|
+
expect(mockSetInterval).toHaveBeenCalled();
|
|
247
|
+
expect(capturedIntervalMs).toBe(60000);
|
|
248
|
+
});
|
|
249
|
+
it("does not set up interval when pollingIntervalMs is omitted", () => {
|
|
250
|
+
renderHook(() => useUpgradeCheck());
|
|
251
|
+
expect(mockSetInterval).not.toHaveBeenCalled();
|
|
252
|
+
});
|
|
253
|
+
it("cleans up interval on unmount", () => {
|
|
254
|
+
const { unmount } = renderHook(() => useUpgradeCheck({ pollingIntervalMs: 60000 }));
|
|
255
|
+
unmount();
|
|
256
|
+
expect(mockClearInterval).toHaveBeenCalled();
|
|
257
|
+
});
|
|
258
|
+
it("increments warningCheckCount on each warning poll", async () => {
|
|
259
|
+
mockUnwrap.mockImplementation(() => Promise.resolve({ message: "Update", status: "warning" }));
|
|
260
|
+
const { result } = renderHook(() => useUpgradeCheck({ recheckOnForeground: true }));
|
|
261
|
+
await act(async () => {
|
|
262
|
+
await flushPromises();
|
|
263
|
+
});
|
|
264
|
+
await waitFor(() => {
|
|
265
|
+
expect(result.current.warningCheckCount).toBe(1);
|
|
266
|
+
});
|
|
267
|
+
// Simulate a foreground return to trigger another check
|
|
268
|
+
await act(async () => {
|
|
269
|
+
appStateListeners.forEach((listener) => {
|
|
270
|
+
listener("background");
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
await act(async () => {
|
|
274
|
+
appStateListeners.forEach((listener) => {
|
|
275
|
+
listener("active");
|
|
276
|
+
});
|
|
277
|
+
await flushPromises();
|
|
278
|
+
});
|
|
279
|
+
await waitFor(() => {
|
|
280
|
+
expect(result.current.warningCheckCount).toBe(2);
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
describe("foreground recheck", () => {
|
|
285
|
+
it("sets up AppState listener when recheckOnForeground is true", () => {
|
|
286
|
+
renderHook(() => useUpgradeCheck({ recheckOnForeground: true }));
|
|
287
|
+
expect(mockAddEventListener).toHaveBeenCalledWith("change", expect.any(Function));
|
|
288
|
+
});
|
|
289
|
+
it("does not set up AppState listener by default", () => {
|
|
290
|
+
renderHook(() => useUpgradeCheck());
|
|
291
|
+
expect(mockAddEventListener).not.toHaveBeenCalled();
|
|
292
|
+
});
|
|
293
|
+
it("triggers check on background to active transition", async () => {
|
|
294
|
+
renderHook(() => useUpgradeCheck({ recheckOnForeground: true }));
|
|
295
|
+
const initialCallCount = mockTrigger.mock.calls.length;
|
|
296
|
+
// Simulate going to background, then back to active
|
|
297
|
+
act(() => {
|
|
298
|
+
appStateListeners.forEach((listener) => {
|
|
299
|
+
listener("background");
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
act(() => {
|
|
303
|
+
appStateListeners.forEach((listener) => {
|
|
304
|
+
listener("active");
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
expect(mockTrigger.mock.calls.length).toBeGreaterThan(initialCallCount);
|
|
308
|
+
});
|
|
309
|
+
it("does not trigger extra check on active to active transition", () => {
|
|
310
|
+
renderHook(() => useUpgradeCheck({ recheckOnForeground: true }));
|
|
311
|
+
const initialCallCount = mockTrigger.mock.calls.length;
|
|
312
|
+
act(() => {
|
|
313
|
+
appStateListeners.forEach((listener) => {
|
|
314
|
+
listener("active");
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
expect(mockTrigger.mock.calls.length).toBe(initialCallCount);
|
|
318
|
+
});
|
|
319
|
+
it("cleans up AppState listener on unmount", () => {
|
|
320
|
+
const { unmount } = renderHook(() => useUpgradeCheck({ recheckOnForeground: true }));
|
|
321
|
+
unmount();
|
|
322
|
+
expect(mockRemove).toHaveBeenCalled();
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
});
|
|
326
|
+
//# sourceMappingURL=useUpgradeCheck.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useUpgradeCheck.test.js","sourceRoot":"","sources":["../src/useUpgradeCheck.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,SAAS,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,IAAI,EAAC,MAAM,UAAU,CAAC;AAC3E,OAAO,EAAC,GAAG,EAAE,UAAU,EAAE,OAAO,EAAC,MAAM,+BAA+B,CAAC;AAEvE,8EAA8E;AAC9E,iDAAiD;AACjD,8EAA8E;AAC9E,IAAI,eAAe,GAAuB,EAAE,CAAC;AAQ7C,MAAM,UAAU,GAAG,IAAI,CAAC,GAAsC,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC,EAAC,MAAM,EAAE,IAAI,EAAC,CAAC,CAAC,CAAC;AAClG,MAAM,WAAW,GAAG,IAAI,CAAC,CAAC,GAAG,KAAgB,EAAE,EAAE,CAAC,CAAC,EAAC,MAAM,EAAE,UAAU,EAAC,CAAC,CAAC,CAAC;AAE1E,IAAI,CAAC,MAAM,CAAC,YAAY,EAAE,GAAG,EAAE,CAAC,CAAC;IAC/B,2BAA2B,EAAE,GAAG,EAAE,CAAC,CAAC,WAAW,CAAC;CACjD,CAAC,CAAC,CAAC;AAEJ,0EAA0E;AAC1E,qEAAqE;AACrE,IAAI,CAAC,MAAM,CAAC,YAAY,EAAE,GAAG,EAAE,CAAC,CAAC;IAC/B,KAAK,EAAE,IAAI;CACZ,CAAC,CAAC,CAAC;AAEJ,sEAAsE;AACtE,IAAI,iBAAiB,GAAmC,EAAE,CAAC;AAC3D,MAAM,UAAU,GAAG,IAAI,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;AAClC,MAAM,oBAAoB,GAAG,IAAI,CAAC,CAAC,MAAc,EAAE,OAAgC,EAAE,EAAE;IACrF,iBAAiB,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IAChC,OAAO,EAAC,MAAM,EAAE,UAAU,EAAC,CAAC;AAC9B,CAAC,CAAC,CAAC;AAEH,MAAM,WAAW,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;AAEtD,IAAI,CAAC,MAAM,CAAC,cAAc,EAAE,GAAG,EAAE,CAAC,CAAC;IACjC,QAAQ,EAAE;QACR,gBAAgB,EAAE,oBAAoB;QACtC,YAAY,EAAE,QAAQ;KACvB;IACD,OAAO,EAAE,EAAC,OAAO,EAAE,WAAW,EAAC;IAC/B,QAAQ,EAAE,EAAC,EAAE,EAAE,KAAK,EAAC;IACrB,UAAU,EAAE,EAAC,MAAM,EAAE,CAAC,CAAU,EAAE,EAAE,CAAC,CAAC,EAAC;CACxC,CAAC,CAAC,CAAC;AAEJ,IAAI,CAAC,MAAM,CAAC,gBAAgB,EAAE,GAAG,EAAE,CAAC,CAAC;IACnC,OAAO,EAAE;QACP,IAAI,UAAU;YACZ,OAAO,EAAC,KAAK,EAAE,EAAC,WAAW,EAAE,eAAe,EAAC,EAAC,CAAC;QACjD,CAAC;KACF;CACF,CAAC,CAAC,CAAC;AAEJ,8EAA8E;AAC9E,8BAA8B;AAC9B,8EAA8E;AAC9E,MAAM,UAAU,GAAG,IAAI,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;AAElC,8EAA8E;AAC9E,6DAA6D;AAC7D,8EAA8E;AAC9E,MAAM,mBAAmB,GAAG,UAAU,CAAC,WAAW,CAAC;AACnD,MAAM,qBAAqB,GAAG,UAAU,CAAC,aAAa,CAAC;AACvD,IAAI,kBAAsC,CAAC;AAC3C,IAAI,iBAAiB,GAAG,CAAC,CAAC;AAC1B,MAAM,eAAe,GAAG,IAAI,CAAC,CAAC,GAAe,EAAE,EAAU,EAAE,EAAE;IAC3D,kBAAkB,GAAG,EAAE,CAAC;IACxB,OAAO,EAAE,iBAA8D,CAAC;AAC1E,CAAC,CAAC,CAAC;AACH,MAAM,iBAAiB,GAAG,IAAI,CAAC,CAAC,GAAY,EAAE,EAAE,GAAE,CAAC,CAAC,CAAC;AAErD,8EAA8E;AAC9E,kBAAkB;AAClB,8EAA8E;AAC9E,MAAM,UAAU,GAAgB,EAAE,CAAC;AACnC,MAAM,aAAa,GAAG,OAAO,CAAC,KAAK,CAAC;AACpC,MAAM,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;AAElC,sEAAsE;AACtE,OAAO,EAAC,eAAe,EAAC,MAAM,mBAAmB,CAAC;AAElD,yDAAyD;AACzD,MAAM,aAAa,GAAG,GAAkB,EAAE,CAAC,IAAI,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;AAEhF,8EAA8E;AAC9E,mBAAmB;AACnB,8EAA8E;AAC9E,UAAU,CAAC,GAAG,EAAE;IACd,qBAAqB;IACrB,eAAe,GAAG,EAAE,CAAC;IACrB,iBAAiB,GAAG,EAAE,CAAC;IAEvB,qDAAqD;IACrD,UAAU,CAAC,SAAS,EAAE,CAAC;IACvB,UAAU,CAAC,kBAAkB,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC,EAAC,MAAM,EAAE,IAAa,EAAC,CAAC,CAAC,CAAC;IAC9E,WAAW,CAAC,SAAS,EAAE,CAAC;IACxB,WAAW,CAAC,kBAAkB,CAAC,CAAC,GAAG,KAAgB,EAAE,EAAE,CAAC,CAAC,EAAC,MAAM,EAAE,UAAU,EAAC,CAAC,CAAC,CAAC;IAChF,UAAU,CAAC,SAAS,EAAE,CAAC;IACvB,oBAAoB,CAAC,SAAS,EAAE,CAAC;IACjC,oBAAoB,CAAC,kBAAkB,CAAC,CAAC,MAAc,EAAE,OAAgC,EAAE,EAAE;QAC3F,iBAAiB,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAChC,OAAO,EAAC,MAAM,EAAE,UAAU,EAAC,CAAC;IAC9B,CAAC,CAAC,CAAC;IACH,WAAW,CAAC,SAAS,EAAE,CAAC;IACxB,WAAW,CAAC,kBAAkB,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;IAC5D,UAAU,CAAC,SAAS,EAAE,CAAC;IAEvB,8BAA8B;IAC9B,MAAM,CAAC,cAAc,CAAC,UAAU,EAAE,QAAQ,EAAE;QAC1C,YAAY,EAAE,IAAI;QAClB,KAAK,EAAE,EAAC,QAAQ,EAAE,EAAC,MAAM,EAAE,UAAU,EAAC,EAAC;QACvC,QAAQ,EAAE,IAAI;KACf,CAAC,CAAC;IAEH,cAAc;IACd,UAAU,CAAC,WAAW,GAAG,eAAgD,CAAC;IAC1E,UAAU,CAAC,aAAa,GAAG,iBAAoD,CAAC;IAChF,eAAe,CAAC,SAAS,EAAE,CAAC;IAC5B,eAAe,CAAC,kBAAkB,CAAC,CAAC,GAAe,EAAE,EAAU,EAAE,EAAE;QACjE,kBAAkB,GAAG,EAAE,CAAC;QACxB,OAAO,EAAE,iBAA8D,CAAC;IAC1E,CAAC,CAAC,CAAC;IACH,iBAAiB,CAAC,SAAS,EAAE,CAAC;IAC9B,kBAAkB,GAAG,SAAS,CAAC;IAE/B,kBAAkB;IAClB,UAAU,CAAC,MAAM,GAAG,CAAC,CAAC;IACtB,OAAO,CAAC,KAAK,GAAG,CAAC,GAAG,IAAe,EAAQ,EAAE;QAC3C,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACxB,CAAC,CAAC;IACF,OAAO,CAAC,IAAI,GAAG,GAAS,EAAE,GAAE,CAAC,CAAC;AAChC,CAAC,CAAC,CAAC;AAEH,SAAS,CAAC,GAAG,EAAE;IACb,OAAO,CAAC,KAAK,GAAG,aAAa,CAAC;IAC9B,OAAO,CAAC,IAAI,GAAG,YAAY,CAAC;IAC5B,UAAU,CAAC,WAAW,GAAG,mBAAmB,CAAC;IAC7C,UAAU,CAAC,aAAa,GAAG,qBAAqB,CAAC;AACnD,CAAC,CAAC,CAAC;AAEH,8EAA8E;AAC9E,QAAQ;AACR,8EAA8E;AAC9E,QAAQ,CAAC,iBAAiB,EAAE,GAAG,EAAE;IAC/B,QAAQ,CAAC,gBAAgB,EAAE,GAAG,EAAE;QAC9B,EAAE,CAAC,6CAA6C,EAAE,GAAG,EAAE;YACrD,8CAA8C;YAC9C,UAAU,CAAC,kBAAkB,CAAC,GAAG,EAAE,CAAC,IAAI,OAAO,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC,CAAC;YAC3D,MAAM,EAAC,MAAM,EAAC,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,eAAe,EAAE,CAAC,CAAC;YAErD,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YAC9C,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YAC7C,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,eAAe,CAAC,CAAC,aAAa,EAAE,CAAC;YACvD,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,cAAc,CAAC,CAAC,aAAa,EAAE,CAAC;YACtD,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,iBAAiB,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QACnD,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,2CAA2C,EAAE,GAAG,EAAE;YACnD,eAAe,GAAG,SAAS,CAAC;YAC5B,UAAU,CAAC,GAAG,EAAE,CAAC,eAAe,EAAE,CAAC,CAAC;YACpC,MAAM,CAAC,WAAW,CAAC,CAAC,GAAG,CAAC,gBAAgB,EAAE,CAAC;QAC7C,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,qDAAqD,EAAE,GAAG,EAAE;YAC7D,UAAU,CAAC,GAAG,EAAE,CAAC,eAAe,EAAE,CAAC,CAAC;YACpC,MAAM,CAAC,WAAW,CAAC,CAAC,oBAAoB,CAAC,EAAC,QAAQ,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE,EAAC,CAAC,CAAC;QAC3E,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,QAAQ,CAAC,iBAAiB,EAAE,GAAG,EAAE;QAC/B,EAAE,CAAC,+DAA+D,EAAE,KAAK,IAAI,EAAE;YAC7E,UAAU,CAAC,kBAAkB,CAAC,GAAG,EAAE,CACjC,OAAO,CAAC,OAAO,CAAC,EAAC,OAAO,EAAE,mBAAmB,EAAE,MAAM,EAAE,UAAmB,EAAC,CAAC,CAC7E,CAAC;YACF,MAAM,EAAC,MAAM,EAAC,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,eAAe,EAAE,CAAC,CAAC;YAErD,MAAM,GAAG,CAAC,KAAK,IAAI,EAAE;gBACnB,MAAM,aAAa,EAAE,CAAC;YACxB,CAAC,CAAC,CAAC;YAEH,MAAM,OAAO,CAAC,GAAG,EAAE;gBACjB,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;gBAC7C,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,eAAe,CAAC,CAAC,IAAI,CAAC,mBAAmB,CAAC,CAAC;gBACjE,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YAC/C,CAAC,CAAC,CAAC;QACL,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,2FAA2F,EAAE,KAAK,IAAI,EAAE;YACzG,UAAU,CAAC,kBAAkB,CAAC,GAAG,EAAE,CACjC,OAAO,CAAC,OAAO,CAAC,EAAC,OAAO,EAAE,uBAAuB,EAAE,MAAM,EAAE,SAAkB,EAAC,CAAC,CAChF,CAAC;YACF,MAAM,EAAC,MAAM,EAAC,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,eAAe,EAAE,CAAC,CAAC;YAErD,MAAM,GAAG,CAAC,KAAK,IAAI,EAAE;gBACnB,MAAM,aAAa,EAAE,CAAC;YACxB,CAAC,CAAC,CAAC;YAEH,MAAM,OAAO,CAAC,GAAG,EAAE;gBACjB,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;gBAC5C,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,cAAc,CAAC,CAAC,IAAI,CAAC,uBAAuB,CAAC,CAAC;gBACpE,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,iBAAiB,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YACnD,CAAC,CAAC,CAAC;QACL,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,2EAA2E,EAAE,KAAK,IAAI,EAAE;YACzF,wBAAwB;YACxB,UAAU,CAAC,kBAAkB,CAAC,GAAG,EAAE,CACjC,OAAO,CAAC,OAAO,CAAC,EAAC,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,UAAmB,EAAC,CAAC,CAClE,CAAC;YACF,MAAM,EAAC,MAAM,EAAC,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,eAAe,CAAC,EAAC,mBAAmB,EAAE,IAAI,EAAC,CAAC,CAAC,CAAC;YAEhF,MAAM,GAAG,CAAC,KAAK,IAAI,EAAE;gBACnB,MAAM,aAAa,EAAE,CAAC;YACxB,CAAC,CAAC,CAAC;YAEH,MAAM,OAAO,CAAC,GAAG,EAAE;gBACjB,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YAC/C,CAAC,CAAC,CAAC;YAEH,uFAAuF;YACvF,UAAU,CAAC,kBAAkB,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC,EAAC,MAAM,EAAE,IAAa,EAAC,CAAC,CAAC,CAAC;YAE9E,MAAM,GAAG,CAAC,KAAK,IAAI,EAAE;gBACnB,8DAA8D;gBAC9D,iBAAiB,CAAC,OAAO,CAAC,CAAC,QAAQ,EAAE,EAAE;oBACrC,QAAQ,CAAC,YAAY,CAAC,CAAC;gBACzB,CAAC,CAAC,CAAC;YACL,CAAC,CAAC,CAAC;YACH,MAAM,GAAG,CAAC,KAAK,IAAI,EAAE;gBACnB,iBAAiB,CAAC,OAAO,CAAC,CAAC,QAAQ,EAAE,EAAE;oBACrC,QAAQ,CAAC,QAAQ,CAAC,CAAC;gBACrB,CAAC,CAAC,CAAC;gBACH,MAAM,aAAa,EAAE,CAAC;YACxB,CAAC,CAAC,CAAC;YAEH,MAAM,OAAO,CAAC,GAAG,EAAE;gBACjB,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;gBAC9C,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YAC/C,CAAC,CAAC,CAAC;QACL,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,gCAAgC,EAAE,KAAK,IAAI,EAAE;YAC9C,UAAU,CAAC,kBAAkB,CAAC,GAAG,EAAE,CACjC,OAAO,CAAC,OAAO,CAAC;gBACd,OAAO,EAAE,kBAAkB;gBAC3B,MAAM,EAAE,SAAkB;gBAC1B,SAAS,EAAE,4BAA4B;aACxC,CAAC,CACH,CAAC;YACF,MAAM,EAAC,MAAM,EAAC,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,eAAe,EAAE,CAAC,CAAC;YAErD,MAAM,GAAG,CAAC,KAAK,IAAI,EAAE;gBACnB,MAAM,aAAa,EAAE,CAAC;YACxB,CAAC,CAAC,CAAC;YAEH,MAAM,OAAO,CAAC,GAAG,EAAE;gBACjB,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YAC9C,CAAC,CAAC,CAAC;QACL,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,QAAQ,CAAC,WAAW,EAAE,GAAG,EAAE;QACzB,EAAE,CAAC,wCAAwC,EAAE,GAAG,EAAE;YAChD,wDAAwD;YACxD,MAAM,EAAC,MAAM,EAAC,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,eAAe,EAAE,CAAC,CAAC;YACrD,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC9C,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,QAAQ,CAAC,UAAU,EAAE,GAAG,EAAE;QACxB,EAAE,CAAC,qCAAqC,EAAE,GAAG,EAAE;YAC7C,MAAM,EAAC,MAAM,EAAC,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,eAAe,EAAE,CAAC,CAAC;YAErD,GAAG,CAAC,GAAG,EAAE;gBACP,MAAM,CAAC,OAAO,CAAC,QAAQ,EAAE,CAAC;YAC5B,CAAC,CAAC,CAAC;YAEH,MAAM,CAAC,UAAU,CAAC,CAAC,gBAAgB,EAAE,CAAC;QACxC,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,QAAQ,CAAC,gBAAgB,EAAE,GAAG,EAAE;QAC9B,EAAE,CAAC,kCAAkC,EAAE,KAAK,IAAI,EAAE;YAChD,UAAU,CAAC,kBAAkB,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC;YAChF,MAAM,EAAC,MAAM,EAAC,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,eAAe,EAAE,CAAC,CAAC;YAErD,MAAM,GAAG,CAAC,KAAK,IAAI,EAAE;gBACnB,MAAM,aAAa,EAAE,CAAC;YACxB,CAAC,CAAC,CAAC;YAEH,MAAM,OAAO,CAAC,GAAG,EAAE;gBACjB,MAAM,OAAO,GAAG,UAAU,CAAC,IAAI,CAC7B,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,IAAI,CAAC,CAAC,CAAC,KAAK,QAAQ,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,sBAAsB,CAAC,CAClF,CAAC;gBACF,MAAM,CAAC,OAAO,CAAC,CAAC,WAAW,EAAE,CAAC;YAChC,CAAC,CAAC,CAAC;YAEH,kCAAkC;YAClC,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YAC9C,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC/C,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,QAAQ,CAAC,SAAS,EAAE,GAAG,EAAE;QACvB,EAAE,CAAC,qDAAqD,EAAE,GAAG,EAAE;YAC7D,UAAU,CAAC,GAAG,EAAE,CAAC,eAAe,CAAC,EAAC,iBAAiB,EAAE,KAAM,EAAC,CAAC,CAAC,CAAC;YAC/D,MAAM,CAAC,eAAe,CAAC,CAAC,gBAAgB,EAAE,CAAC;YAC3C,MAAM,CAAC,kBAAkB,CAAC,CAAC,IAAI,CAAC,KAAM,CAAC,CAAC;QAC1C,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,4DAA4D,EAAE,GAAG,EAAE;YACpE,UAAU,CAAC,GAAG,EAAE,CAAC,eAAe,EAAE,CAAC,CAAC;YACpC,MAAM,CAAC,eAAe,CAAC,CAAC,GAAG,CAAC,gBAAgB,EAAE,CAAC;QACjD,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,+BAA+B,EAAE,GAAG,EAAE;YACvC,MAAM,EAAC,OAAO,EAAC,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,eAAe,CAAC,EAAC,iBAAiB,EAAE,KAAM,EAAC,CAAC,CAAC,CAAC;YACjF,OAAO,EAAE,CAAC;YACV,MAAM,CAAC,iBAAiB,CAAC,CAAC,gBAAgB,EAAE,CAAC;QAC/C,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,mDAAmD,EAAE,KAAK,IAAI,EAAE;YACjE,UAAU,CAAC,kBAAkB,CAAC,GAAG,EAAE,CACjC,OAAO,CAAC,OAAO,CAAC,EAAC,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAkB,EAAC,CAAC,CACjE,CAAC;YACF,MAAM,EAAC,MAAM,EAAC,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,eAAe,CAAC,EAAC,mBAAmB,EAAE,IAAI,EAAC,CAAC,CAAC,CAAC;YAEhF,MAAM,GAAG,CAAC,KAAK,IAAI,EAAE;gBACnB,MAAM,aAAa,EAAE,CAAC;YACxB,CAAC,CAAC,CAAC;YAEH,MAAM,OAAO,CAAC,GAAG,EAAE;gBACjB,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,iBAAiB,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YACnD,CAAC,CAAC,CAAC;YAEH,wDAAwD;YACxD,MAAM,GAAG,CAAC,KAAK,IAAI,EAAE;gBACnB,iBAAiB,CAAC,OAAO,CAAC,CAAC,QAAQ,EAAE,EAAE;oBACrC,QAAQ,CAAC,YAAY,CAAC,CAAC;gBACzB,CAAC,CAAC,CAAC;YACL,CAAC,CAAC,CAAC;YACH,MAAM,GAAG,CAAC,KAAK,IAAI,EAAE;gBACnB,iBAAiB,CAAC,OAAO,CAAC,CAAC,QAAQ,EAAE,EAAE;oBACrC,QAAQ,CAAC,QAAQ,CAAC,CAAC;gBACrB,CAAC,CAAC,CAAC;gBACH,MAAM,aAAa,EAAE,CAAC;YACxB,CAAC,CAAC,CAAC;YAEH,MAAM,OAAO,CAAC,GAAG,EAAE;gBACjB,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,iBAAiB,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YACnD,CAAC,CAAC,CAAC;QACL,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,QAAQ,CAAC,oBAAoB,EAAE,GAAG,EAAE;QAClC,EAAE,CAAC,4DAA4D,EAAE,GAAG,EAAE;YACpE,UAAU,CAAC,GAAG,EAAE,CAAC,eAAe,CAAC,EAAC,mBAAmB,EAAE,IAAI,EAAC,CAAC,CAAC,CAAC;YAC/D,MAAM,CAAC,oBAAoB,CAAC,CAAC,oBAAoB,CAAC,QAAQ,EAAE,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC;QACpF,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,8CAA8C,EAAE,GAAG,EAAE;YACtD,UAAU,CAAC,GAAG,EAAE,CAAC,eAAe,EAAE,CAAC,CAAC;YACpC,MAAM,CAAC,oBAAoB,CAAC,CAAC,GAAG,CAAC,gBAAgB,EAAE,CAAC;QACtD,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,mDAAmD,EAAE,KAAK,IAAI,EAAE;YACjE,UAAU,CAAC,GAAG,EAAE,CAAC,eAAe,CAAC,EAAC,mBAAmB,EAAE,IAAI,EAAC,CAAC,CAAC,CAAC;YAE/D,MAAM,gBAAgB,GAAG,WAAW,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC;YAEvD,oDAAoD;YACpD,GAAG,CAAC,GAAG,EAAE;gBACP,iBAAiB,CAAC,OAAO,CAAC,CAAC,QAAQ,EAAE,EAAE;oBACrC,QAAQ,CAAC,YAAY,CAAC,CAAC;gBACzB,CAAC,CAAC,CAAC;YACL,CAAC,CAAC,CAAC;YACH,GAAG,CAAC,GAAG,EAAE;gBACP,iBAAiB,CAAC,OAAO,CAAC,CAAC,QAAQ,EAAE,EAAE;oBACrC,QAAQ,CAAC,QAAQ,CAAC,CAAC;gBACrB,CAAC,CAAC,CAAC;YACL,CAAC,CAAC,CAAC;YAEH,MAAM,CAAC,WAAW,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,eAAe,CAAC,gBAAgB,CAAC,CAAC;QAC1E,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,6DAA6D,EAAE,GAAG,EAAE;YACrE,UAAU,CAAC,GAAG,EAAE,CAAC,eAAe,CAAC,EAAC,mBAAmB,EAAE,IAAI,EAAC,CAAC,CAAC,CAAC;YAE/D,MAAM,gBAAgB,GAAG,WAAW,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC;YAEvD,GAAG,CAAC,GAAG,EAAE;gBACP,iBAAiB,CAAC,OAAO,CAAC,CAAC,QAAQ,EAAE,EAAE;oBACrC,QAAQ,CAAC,QAAQ,CAAC,CAAC;gBACrB,CAAC,CAAC,CAAC;YACL,CAAC,CAAC,CAAC;YAEH,MAAM,CAAC,WAAW,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAC;QAC/D,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,wCAAwC,EAAE,GAAG,EAAE;YAChD,MAAM,EAAC,OAAO,EAAC,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,eAAe,CAAC,EAAC,mBAAmB,EAAE,IAAI,EAAC,CAAC,CAAC,CAAC;YACjF,OAAO,EAAE,CAAC;YACV,MAAM,CAAC,UAAU,CAAC,CAAC,gBAAgB,EAAE,CAAC;QACxC,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"@better-auth/expo": "^1.2.8",
|
|
4
4
|
"@react-native-async-storage/async-storage": "2.2.0",
|
|
5
5
|
"@reduxjs/toolkit": "^2.11.1",
|
|
6
|
-
"@terreno/ui": "0.
|
|
6
|
+
"@terreno/ui": "0.11.1",
|
|
7
7
|
"async-mutex": "^0.5.0",
|
|
8
8
|
"axios": "^1.13.2",
|
|
9
9
|
"axios-retry": "^4.5.0",
|
|
@@ -22,12 +22,14 @@
|
|
|
22
22
|
"description": "Redux Toolkit Query utilities for @terreno/api backends",
|
|
23
23
|
"devDependencies": {
|
|
24
24
|
"@biomejs/biome": "^2.3.6",
|
|
25
|
+
"@testing-library/react-native": "^13.2.0",
|
|
25
26
|
"@types/bun": "^1.2.4",
|
|
26
27
|
"@types/luxon": "^3.7.1",
|
|
27
28
|
"@types/node": "^25.0.3",
|
|
28
29
|
"@types/qs": "^6.14.0",
|
|
29
30
|
"@types/react": "~19.1.10",
|
|
30
31
|
"react": "19.1.0",
|
|
32
|
+
"react-test-renderer": "19.1.0",
|
|
31
33
|
"typescript": "~5.9.2"
|
|
32
34
|
},
|
|
33
35
|
"exports": {
|
|
@@ -61,8 +63,9 @@
|
|
|
61
63
|
"lint:fix": "biome check --write .",
|
|
62
64
|
"lint:unsafefix": "biome check --write --unsafe .",
|
|
63
65
|
"test": "bun test",
|
|
64
|
-
"test:ci": "bun test"
|
|
66
|
+
"test:ci": "bun test",
|
|
67
|
+
"test:coverage": "bun run ../scripts/check-coverage.ts"
|
|
65
68
|
},
|
|
66
69
|
"types": "dist/index.d.ts",
|
|
67
|
-
"version": "0.
|
|
70
|
+
"version": "0.11.1"
|
|
68
71
|
}
|
package/src/authSlice.test.ts
CHANGED
|
@@ -449,6 +449,85 @@ describe("listener middleware side effects", () => {
|
|
|
449
449
|
}
|
|
450
450
|
});
|
|
451
451
|
|
|
452
|
+
it("re-throws and logs when AsyncStorage.setItem fails on web login", async () => {
|
|
453
|
+
const {store} = createTestStore();
|
|
454
|
+
const originalSetItem = AsyncStorage.setItem;
|
|
455
|
+
const originalConsoleError = console.error;
|
|
456
|
+
const globalWithWindow = globalThis as {window?: unknown};
|
|
457
|
+
const originalWindow = globalWithWindow.window;
|
|
458
|
+
const errorCalls: unknown[][] = [];
|
|
459
|
+
|
|
460
|
+
console.error = (...args: unknown[]): void => {
|
|
461
|
+
errorCalls.push(args);
|
|
462
|
+
};
|
|
463
|
+
AsyncStorage.setItem = async (): Promise<void> => {
|
|
464
|
+
throw new Error("storage quota exceeded");
|
|
465
|
+
};
|
|
466
|
+
globalWithWindow.window = {};
|
|
467
|
+
|
|
468
|
+
try {
|
|
469
|
+
store.dispatch({
|
|
470
|
+
meta: {
|
|
471
|
+
arg: {endpointName: "emailLogin", type: "mutation"},
|
|
472
|
+
requestId: "listener-login-error",
|
|
473
|
+
},
|
|
474
|
+
payload: {refreshToken: "refresh-token", token: "auth-token", userId: "user-err"},
|
|
475
|
+
type: "terreno-rtk/executeMutation/fulfilled",
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
await flushAsyncListeners();
|
|
479
|
+
|
|
480
|
+
const loggedErrorMessage = errorCalls.find((args) =>
|
|
481
|
+
args.some(
|
|
482
|
+
(value) => typeof value === "string" && value.includes("Error setting auth token")
|
|
483
|
+
)
|
|
484
|
+
);
|
|
485
|
+
expect(loggedErrorMessage).toBeDefined();
|
|
486
|
+
} finally {
|
|
487
|
+
AsyncStorage.setItem = originalSetItem;
|
|
488
|
+
console.error = originalConsoleError;
|
|
489
|
+
if (typeof originalWindow === "undefined") {
|
|
490
|
+
delete globalWithWindow.window;
|
|
491
|
+
} else {
|
|
492
|
+
globalWithWindow.window = originalWindow;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
it("skips storing auth tokens when window is undefined (SSR context)", async () => {
|
|
498
|
+
const {store} = createTestStore();
|
|
499
|
+
const setItemCalls: Array<[string, string]> = [];
|
|
500
|
+
const originalSetItem = AsyncStorage.setItem;
|
|
501
|
+
const globalWithWindow = globalThis as {window?: unknown};
|
|
502
|
+
const originalWindow = globalWithWindow.window;
|
|
503
|
+
|
|
504
|
+
AsyncStorage.setItem = async (key: string, value: string): Promise<void> => {
|
|
505
|
+
setItemCalls.push([key, value]);
|
|
506
|
+
};
|
|
507
|
+
delete globalWithWindow.window;
|
|
508
|
+
|
|
509
|
+
try {
|
|
510
|
+
store.dispatch({
|
|
511
|
+
meta: {
|
|
512
|
+
arg: {endpointName: "emailSignUp", type: "mutation"},
|
|
513
|
+
requestId: "listener-ssr-1",
|
|
514
|
+
},
|
|
515
|
+
payload: {refreshToken: "r", token: "t", userId: "user-ssr"},
|
|
516
|
+
type: "terreno-rtk/executeMutation/fulfilled",
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
await flushAsyncListeners();
|
|
520
|
+
|
|
521
|
+
expect(setItemCalls).toEqual([]);
|
|
522
|
+
expect(store.getState().auth.userId).toBe("user-ssr");
|
|
523
|
+
} finally {
|
|
524
|
+
AsyncStorage.setItem = originalSetItem;
|
|
525
|
+
if (typeof originalWindow !== "undefined") {
|
|
526
|
+
globalWithWindow.window = originalWindow;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
});
|
|
530
|
+
|
|
452
531
|
it("removes tokens from AsyncStorage on web logout when window exists", async () => {
|
|
453
532
|
const {store, authSlice} = createTestStore();
|
|
454
533
|
const removeItemCalls: string[] = [];
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import {afterAll, afterEach, beforeEach, describe, expect, it, mock} from "bun:test";
|
|
2
|
+
|
|
3
|
+
mock.module("react-native", () => ({
|
|
4
|
+
Platform: {OS: "ios"},
|
|
5
|
+
StyleSheet: {create: (s: unknown) => s},
|
|
6
|
+
}));
|
|
7
|
+
|
|
8
|
+
// Force IsWeb=false regardless of whether ./platform was already imported
|
|
9
|
+
// elsewhere in the test run. `mock.module` is hoisted in bun, so this takes
|
|
10
|
+
// effect before the dynamic imports below.
|
|
11
|
+
mock.module("./platform", () => ({IsWeb: false}));
|
|
12
|
+
|
|
13
|
+
const secureCalls = {
|
|
14
|
+
delete: [] as string[],
|
|
15
|
+
get: [] as string[],
|
|
16
|
+
set: [] as Array<[string, string]>,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
mock.module("expo-secure-store", () => ({
|
|
20
|
+
deleteItemAsync: async (key: string): Promise<void> => {
|
|
21
|
+
secureCalls.delete.push(key);
|
|
22
|
+
},
|
|
23
|
+
getItemAsync: async (key: string): Promise<string | null> => {
|
|
24
|
+
secureCalls.get.push(key);
|
|
25
|
+
return null;
|
|
26
|
+
},
|
|
27
|
+
setItemAsync: async (key: string, value: string): Promise<void> => {
|
|
28
|
+
secureCalls.set.push([key, value]);
|
|
29
|
+
},
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
const auth = await import("./authSlice");
|
|
33
|
+
|
|
34
|
+
const {configureStore} = await import("@reduxjs/toolkit");
|
|
35
|
+
const {createApi, fetchBaseQuery} = await import("@reduxjs/toolkit/query/react");
|
|
36
|
+
|
|
37
|
+
const api = createApi({
|
|
38
|
+
baseQuery: fetchBaseQuery({baseUrl: "/"}),
|
|
39
|
+
endpoints: (builder) => ({
|
|
40
|
+
emailLogin: builder.mutation({
|
|
41
|
+
query: (body: {email: string; password: string}) => ({
|
|
42
|
+
body,
|
|
43
|
+
method: "POST",
|
|
44
|
+
url: "auth/login",
|
|
45
|
+
}),
|
|
46
|
+
}),
|
|
47
|
+
emailSignUp: builder.mutation({
|
|
48
|
+
query: (body: {email: string; password: string}) => ({
|
|
49
|
+
body,
|
|
50
|
+
method: "POST",
|
|
51
|
+
url: "auth/signup",
|
|
52
|
+
}),
|
|
53
|
+
}),
|
|
54
|
+
googleLogin: builder.mutation({
|
|
55
|
+
query: (body: {idToken: string}) => ({body, method: "POST", url: "auth/google"}),
|
|
56
|
+
}),
|
|
57
|
+
}),
|
|
58
|
+
reducerPath: "terreno-rtk",
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const createTestStore = () => {
|
|
62
|
+
const {authReducer, middleware, authSlice} = auth.generateAuthSlice(
|
|
63
|
+
// biome-ignore lint/suspicious/noExplicitAny: Test mock
|
|
64
|
+
api as any
|
|
65
|
+
);
|
|
66
|
+
const store = configureStore({
|
|
67
|
+
middleware: (getDefault) =>
|
|
68
|
+
getDefault({serializableCheck: false}).concat(api.middleware, ...middleware),
|
|
69
|
+
reducer: {
|
|
70
|
+
[api.reducerPath]: api.reducer,
|
|
71
|
+
auth: authReducer,
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
return {authSlice, store};
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const flushAsyncListeners = async (): Promise<void> => {
|
|
78
|
+
await Promise.resolve();
|
|
79
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
describe("native listener middleware side effects", () => {
|
|
83
|
+
const originalDebug = console.debug;
|
|
84
|
+
const originalError = console.error;
|
|
85
|
+
const debugCalls: unknown[][] = [];
|
|
86
|
+
const errorCalls: unknown[][] = [];
|
|
87
|
+
|
|
88
|
+
beforeEach(() => {
|
|
89
|
+
debugCalls.length = 0;
|
|
90
|
+
errorCalls.length = 0;
|
|
91
|
+
secureCalls.delete = [];
|
|
92
|
+
secureCalls.get = [];
|
|
93
|
+
secureCalls.set = [];
|
|
94
|
+
console.debug = (...args: unknown[]): void => {
|
|
95
|
+
debugCalls.push(args);
|
|
96
|
+
};
|
|
97
|
+
console.error = (...args: unknown[]): void => {
|
|
98
|
+
errorCalls.push(args);
|
|
99
|
+
};
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
afterEach(() => {
|
|
103
|
+
console.debug = originalDebug;
|
|
104
|
+
console.error = originalError;
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
afterAll(() => {
|
|
108
|
+
// Restore mocks to the values the rest of the suite expects.
|
|
109
|
+
mock.module("react-native", () => ({
|
|
110
|
+
Platform: {OS: "web"},
|
|
111
|
+
StyleSheet: {create: (s: unknown) => s},
|
|
112
|
+
}));
|
|
113
|
+
mock.module("./platform", () => ({IsWeb: true}));
|
|
114
|
+
mock.module("expo-secure-store", () => ({
|
|
115
|
+
deleteItemAsync: async () => {},
|
|
116
|
+
getItemAsync: async () => null,
|
|
117
|
+
setItemAsync: async () => {},
|
|
118
|
+
}));
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("stores tokens in SecureStore on native login", async () => {
|
|
122
|
+
const {store} = createTestStore();
|
|
123
|
+
store.dispatch({
|
|
124
|
+
meta: {arg: {endpointName: "emailLogin", type: "mutation"}, requestId: "native-login-1"},
|
|
125
|
+
payload: {refreshToken: "native-refresh", token: "native-auth", userId: "native-user"},
|
|
126
|
+
type: "terreno-rtk/executeMutation/fulfilled",
|
|
127
|
+
});
|
|
128
|
+
await flushAsyncListeners();
|
|
129
|
+
expect(secureCalls.set).toEqual([
|
|
130
|
+
["AUTH_TOKEN", "native-auth"],
|
|
131
|
+
["REFRESH_TOKEN", "native-refresh"],
|
|
132
|
+
]);
|
|
133
|
+
expect(store.getState().auth.userId).toBe("native-user");
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("logs and rethrows when SecureStore fails on native login", async () => {
|
|
137
|
+
mock.module("expo-secure-store", () => ({
|
|
138
|
+
deleteItemAsync: async (key: string): Promise<void> => {
|
|
139
|
+
secureCalls.delete.push(key);
|
|
140
|
+
},
|
|
141
|
+
getItemAsync: async (): Promise<string | null> => null,
|
|
142
|
+
setItemAsync: async (): Promise<void> => {
|
|
143
|
+
throw new Error("secure-store-fail");
|
|
144
|
+
},
|
|
145
|
+
}));
|
|
146
|
+
const {store} = createTestStore();
|
|
147
|
+
store.dispatch({
|
|
148
|
+
meta: {arg: {endpointName: "emailLogin", type: "mutation"}, requestId: "native-login-err"},
|
|
149
|
+
payload: {refreshToken: "r", token: "t", userId: "u"},
|
|
150
|
+
type: "terreno-rtk/executeMutation/fulfilled",
|
|
151
|
+
});
|
|
152
|
+
await flushAsyncListeners();
|
|
153
|
+
const found = errorCalls.find((args) =>
|
|
154
|
+
args.some((v) => typeof v === "string" && v.includes("Error setting auth token"))
|
|
155
|
+
);
|
|
156
|
+
expect(found).toBeDefined();
|
|
157
|
+
// Reset setItemAsync back so other tests aren't affected.
|
|
158
|
+
mock.module("expo-secure-store", () => ({
|
|
159
|
+
deleteItemAsync: async (key: string): Promise<void> => {
|
|
160
|
+
secureCalls.delete.push(key);
|
|
161
|
+
},
|
|
162
|
+
getItemAsync: async (): Promise<string | null> => null,
|
|
163
|
+
setItemAsync: async (key: string, value: string): Promise<void> => {
|
|
164
|
+
secureCalls.set.push([key, value]);
|
|
165
|
+
},
|
|
166
|
+
}));
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("removes tokens from SecureStore on native logout", async () => {
|
|
170
|
+
const {store, authSlice} = createTestStore();
|
|
171
|
+
store.dispatch(authSlice.actions.logout());
|
|
172
|
+
await flushAsyncListeners();
|
|
173
|
+
expect(secureCalls.delete).toEqual(["AUTH_TOKEN", "REFRESH_TOKEN"]);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("warns when native login response is missing a token", async () => {
|
|
177
|
+
const {store} = createTestStore();
|
|
178
|
+
store.dispatch({
|
|
179
|
+
meta: {arg: {endpointName: "emailLogin", type: "mutation"}, requestId: "native-login-2"},
|
|
180
|
+
payload: {refreshToken: "r", token: null, userId: "u-missing"},
|
|
181
|
+
type: "terreno-rtk/executeMutation/fulfilled",
|
|
182
|
+
});
|
|
183
|
+
await flushAsyncListeners();
|
|
184
|
+
// Nothing should have been written to SecureStore since the outer token check filters.
|
|
185
|
+
expect(secureCalls.set).toEqual([]);
|
|
186
|
+
});
|
|
187
|
+
});
|