@terreno/rtk 0.25.0 → 0.26.0
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/.ai/guidelines/core.md +2 -0
- package/dist/authSlice.d.ts.map +1 -1
- package/dist/authSlice.js +1 -1
- package/dist/authSlice.js.map +1 -1
- package/dist/authSlice.test.js +11 -7
- package/dist/authSlice.test.js.map +1 -1
- package/dist/constants.d.ts +7 -1
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js.map +1 -1
- package/dist/isolated/constants.test.js +53 -75
- package/dist/isolated/constants.test.js.map +1 -1
- package/dist/mongooseSlice.test.js +0 -1
- package/dist/mongooseSlice.test.js.map +1 -1
- package/dist/sync.test.d.ts +2 -0
- package/dist/sync.test.d.ts.map +1 -0
- package/dist/sync.test.js +298 -0
- package/dist/sync.test.js.map +1 -0
- package/package.json +2 -2
- package/src/authSlice.test.ts +20 -12
- package/src/authSlice.ts +1 -1
- package/src/constants.ts +7 -2
- package/src/isolated/constants.test.ts +81 -78
- package/src/mongooseSlice.test.ts +1 -2
- package/src/sync.test.ts +391 -0
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import { describe, expect, it, mock } from "bun:test";
|
|
2
|
+
import { configureStore } from "@reduxjs/toolkit";
|
|
3
|
+
import { act, renderHook } from "@testing-library/react-native";
|
|
4
|
+
import React from "react";
|
|
5
|
+
import { Provider } from "react-redux";
|
|
6
|
+
import { useSyncConnection } from "./sync";
|
|
7
|
+
const REDUCER_PATH = "testApi";
|
|
8
|
+
const createFakeSocket = (connected) => {
|
|
9
|
+
const handlers = {};
|
|
10
|
+
return {
|
|
11
|
+
connected,
|
|
12
|
+
emit: mock(() => { }),
|
|
13
|
+
off: mock((event, cb) => {
|
|
14
|
+
handlers[event] = (handlers[event] ?? []).filter((h) => h !== cb);
|
|
15
|
+
}),
|
|
16
|
+
on: mock((event, cb) => {
|
|
17
|
+
handlers[event] = handlers[event] ?? [];
|
|
18
|
+
handlers[event].push(cb);
|
|
19
|
+
}),
|
|
20
|
+
trigger: (event, arg) => {
|
|
21
|
+
for (const cb of handlers[event] ?? []) {
|
|
22
|
+
cb(arg);
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
const createApi = (queries) => {
|
|
28
|
+
const dataByEndpoint = {};
|
|
29
|
+
if (queries) {
|
|
30
|
+
for (const key of Object.keys(queries)) {
|
|
31
|
+
const entry = queries[key];
|
|
32
|
+
if (entry.data) {
|
|
33
|
+
dataByEndpoint[entry.endpointName] = entry.data;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const invalidateTags = mock((tags) => ({ payload: tags, type: "api/invalidateTags" }));
|
|
38
|
+
const updateQueryData = mock((endpointName, _args, recipe) => {
|
|
39
|
+
const draft = dataByEndpoint[endpointName];
|
|
40
|
+
if (draft) {
|
|
41
|
+
recipe(draft);
|
|
42
|
+
}
|
|
43
|
+
return { type: "api/updateQueryData" };
|
|
44
|
+
});
|
|
45
|
+
const api = {
|
|
46
|
+
reducerPath: REDUCER_PATH,
|
|
47
|
+
util: { invalidateTags, updateQueryData },
|
|
48
|
+
};
|
|
49
|
+
const store = configureStore({
|
|
50
|
+
middleware: (getDefaultMiddleware) => getDefaultMiddleware({ immutableCheck: false, serializableCheck: false }),
|
|
51
|
+
reducer: {
|
|
52
|
+
[REDUCER_PATH]: (state = { queries: queries ?? undefined }) => state,
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
return { api, invalidateTags, store, updateQueryData };
|
|
56
|
+
};
|
|
57
|
+
const renderSync = (socket,
|
|
58
|
+
// biome-ignore lint/suspicious/noExplicitAny: test-only structural RTK Query api stub
|
|
59
|
+
api, store, options = {}) => {
|
|
60
|
+
const Wrapper = ({ children }) => React.createElement(Provider, { children, store });
|
|
61
|
+
return renderHook(() => useSyncConnection({
|
|
62
|
+
api,
|
|
63
|
+
debug: options.debug ?? true,
|
|
64
|
+
socket: socket,
|
|
65
|
+
tagTypes: options.tagTypes ?? ["todos"],
|
|
66
|
+
}), { wrapper: Wrapper });
|
|
67
|
+
};
|
|
68
|
+
const syncEvent = (overrides) => ({
|
|
69
|
+
collection: "todos",
|
|
70
|
+
id: "1",
|
|
71
|
+
method: "update",
|
|
72
|
+
...overrides,
|
|
73
|
+
});
|
|
74
|
+
describe("useSyncConnection", () => {
|
|
75
|
+
it("does nothing when there is no socket", () => {
|
|
76
|
+
const { store, api } = createApi(null);
|
|
77
|
+
const { result } = renderSync(null, api, store);
|
|
78
|
+
expect(result.current).toBeUndefined();
|
|
79
|
+
});
|
|
80
|
+
it("subscribes to model rooms when the socket is already connected", () => {
|
|
81
|
+
const socket = createFakeSocket(true);
|
|
82
|
+
const { store, api } = createApi({});
|
|
83
|
+
const { unmount } = renderSync(socket, api, store, { tagTypes: ["todos", "users"] });
|
|
84
|
+
expect(socket.emit).toHaveBeenCalledWith("subscribe:model", "todos");
|
|
85
|
+
expect(socket.emit).toHaveBeenCalledWith("subscribe:model", "users");
|
|
86
|
+
expect(socket.on).toHaveBeenCalledWith("connect", expect.any(Function));
|
|
87
|
+
expect(socket.on).toHaveBeenCalledWith("sync", expect.any(Function));
|
|
88
|
+
unmount();
|
|
89
|
+
expect(socket.emit).toHaveBeenCalledWith("unsubscribe:model", "todos");
|
|
90
|
+
expect(socket.off).toHaveBeenCalledWith("sync", expect.any(Function));
|
|
91
|
+
});
|
|
92
|
+
it("subscribes on the connect event when not initially connected", () => {
|
|
93
|
+
const socket = createFakeSocket(false);
|
|
94
|
+
const { store, api } = createApi({});
|
|
95
|
+
const { unmount } = renderSync(socket, api, store);
|
|
96
|
+
expect(socket.emit).not.toHaveBeenCalledWith("subscribe:model", "todos");
|
|
97
|
+
act(() => {
|
|
98
|
+
socket.trigger("connect");
|
|
99
|
+
});
|
|
100
|
+
expect(socket.emit).toHaveBeenCalledWith("subscribe:model", "todos");
|
|
101
|
+
// Not connected on cleanup, so no unsubscribe emit.
|
|
102
|
+
socket.emit.mockClear();
|
|
103
|
+
unmount();
|
|
104
|
+
expect(socket.emit).not.toHaveBeenCalledWith("unsubscribe:model", "todos");
|
|
105
|
+
});
|
|
106
|
+
it("ignores events for collections that are not tracked", () => {
|
|
107
|
+
const socket = createFakeSocket(true);
|
|
108
|
+
const { store, api, invalidateTags } = createApi({});
|
|
109
|
+
renderSync(socket, api, store);
|
|
110
|
+
act(() => {
|
|
111
|
+
socket.trigger("sync", syncEvent({ collection: "other", method: "create" }));
|
|
112
|
+
});
|
|
113
|
+
expect(invalidateTags).not.toHaveBeenCalled();
|
|
114
|
+
});
|
|
115
|
+
it("invalidates tags for create events", () => {
|
|
116
|
+
const socket = createFakeSocket(true);
|
|
117
|
+
const { store, api, invalidateTags } = createApi({});
|
|
118
|
+
renderSync(socket, api, store);
|
|
119
|
+
act(() => {
|
|
120
|
+
socket.trigger("sync", syncEvent({ method: "create" }));
|
|
121
|
+
});
|
|
122
|
+
expect(invalidateTags).toHaveBeenCalledWith(["todos"]);
|
|
123
|
+
});
|
|
124
|
+
it("invalidates tags for update events without data", () => {
|
|
125
|
+
const socket = createFakeSocket(true);
|
|
126
|
+
const { store, api, invalidateTags } = createApi({});
|
|
127
|
+
renderSync(socket, api, store);
|
|
128
|
+
act(() => {
|
|
129
|
+
socket.trigger("sync", syncEvent({ data: undefined, method: "update" }));
|
|
130
|
+
});
|
|
131
|
+
expect(invalidateTags).toHaveBeenCalledWith(["todos"]);
|
|
132
|
+
});
|
|
133
|
+
it("invalidates tags for update events when there are no cached queries", () => {
|
|
134
|
+
const socket = createFakeSocket(true);
|
|
135
|
+
const { store, api, invalidateTags } = createApi(null);
|
|
136
|
+
renderSync(socket, api, store);
|
|
137
|
+
act(() => {
|
|
138
|
+
socket.trigger("sync", syncEvent({ data: { name: "new" }, method: "update" }));
|
|
139
|
+
});
|
|
140
|
+
expect(invalidateTags).toHaveBeenCalledWith(["todos"]);
|
|
141
|
+
});
|
|
142
|
+
it("patches a matching entity inside a cached list query on update", () => {
|
|
143
|
+
const socket = createFakeSocket(true);
|
|
144
|
+
const queries = {
|
|
145
|
+
"getTodos()": {
|
|
146
|
+
data: { data: [{ _id: "1", name: "old", updated: "2026-01-01T00:00:00.000Z" }], total: 1 },
|
|
147
|
+
endpointName: "getTodos",
|
|
148
|
+
originalArgs: undefined,
|
|
149
|
+
status: "fulfilled",
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
const { store, api, updateQueryData, invalidateTags } = createApi(queries);
|
|
153
|
+
renderSync(socket, api, store);
|
|
154
|
+
act(() => {
|
|
155
|
+
socket.trigger("sync", syncEvent({ data: { name: "new", updated: "2026-02-01T00:00:00.000Z" }, method: "update" }));
|
|
156
|
+
});
|
|
157
|
+
expect(updateQueryData).toHaveBeenCalled();
|
|
158
|
+
expect(queries["getTodos()"].data.data[0].name).toBe("new");
|
|
159
|
+
expect(invalidateTags).not.toHaveBeenCalled();
|
|
160
|
+
});
|
|
161
|
+
it("skips stale updates in a cached list query", () => {
|
|
162
|
+
const socket = createFakeSocket(true);
|
|
163
|
+
const queries = {
|
|
164
|
+
"getTodos()": {
|
|
165
|
+
data: { data: [{ _id: "1", name: "current", updated: "2026-03-01T00:00:00.000Z" }], total: 1 },
|
|
166
|
+
endpointName: "getTodos",
|
|
167
|
+
originalArgs: undefined,
|
|
168
|
+
status: "fulfilled",
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
const { store, api } = createApi(queries);
|
|
172
|
+
renderSync(socket, api, store);
|
|
173
|
+
act(() => {
|
|
174
|
+
socket.trigger("sync", syncEvent({ data: { name: "stale", updated: "2026-01-01T00:00:00.000Z" }, method: "update" }));
|
|
175
|
+
});
|
|
176
|
+
expect(queries["getTodos()"].data.data[0].name).toBe("current");
|
|
177
|
+
});
|
|
178
|
+
it("patches a matching single-entity cached query on update", () => {
|
|
179
|
+
const socket = createFakeSocket(true);
|
|
180
|
+
const queries = {
|
|
181
|
+
"getTodoById(1)": {
|
|
182
|
+
data: { _id: "1", name: "old", updated: "2026-01-01T00:00:00.000Z" },
|
|
183
|
+
endpointName: "getTodoById",
|
|
184
|
+
originalArgs: { id: "1" },
|
|
185
|
+
status: "fulfilled",
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
const { store, api, updateQueryData } = createApi(queries);
|
|
189
|
+
renderSync(socket, api, store);
|
|
190
|
+
act(() => {
|
|
191
|
+
socket.trigger("sync", syncEvent({ data: { name: "new", updated: "2026-02-01T00:00:00.000Z" }, method: "update" }));
|
|
192
|
+
});
|
|
193
|
+
expect(updateQueryData).toHaveBeenCalled();
|
|
194
|
+
expect(queries["getTodoById(1)"].data.name).toBe("new");
|
|
195
|
+
});
|
|
196
|
+
it("skips stale updates for a single-entity cached query", () => {
|
|
197
|
+
const socket = createFakeSocket(true);
|
|
198
|
+
const queries = {
|
|
199
|
+
"getTodoById(1)": {
|
|
200
|
+
data: { _id: "1", name: "current", updated: "2026-03-01T00:00:00.000Z" },
|
|
201
|
+
endpointName: "getTodoById",
|
|
202
|
+
originalArgs: { id: "1" },
|
|
203
|
+
status: "fulfilled",
|
|
204
|
+
},
|
|
205
|
+
};
|
|
206
|
+
const { store, api, invalidateTags } = createApi(queries);
|
|
207
|
+
renderSync(socket, api, store);
|
|
208
|
+
act(() => {
|
|
209
|
+
socket.trigger("sync", syncEvent({ data: { name: "stale", updated: "2026-01-01T00:00:00.000Z" }, method: "update" }));
|
|
210
|
+
});
|
|
211
|
+
expect(queries["getTodoById(1)"].data.name).toBe("current");
|
|
212
|
+
expect(invalidateTags).toHaveBeenCalledWith(["todos"]);
|
|
213
|
+
});
|
|
214
|
+
it("invalidates tags on update when no cached query matches", () => {
|
|
215
|
+
const socket = createFakeSocket(true);
|
|
216
|
+
const queries = {
|
|
217
|
+
"getOthers()": {
|
|
218
|
+
data: { data: [{ _id: "999", name: "unrelated" }] },
|
|
219
|
+
endpointName: "getOthers",
|
|
220
|
+
originalArgs: undefined,
|
|
221
|
+
status: "fulfilled",
|
|
222
|
+
},
|
|
223
|
+
"pending()": {
|
|
224
|
+
data: { data: [{ _id: "1" }] },
|
|
225
|
+
endpointName: "pending",
|
|
226
|
+
originalArgs: undefined,
|
|
227
|
+
status: "pending",
|
|
228
|
+
},
|
|
229
|
+
};
|
|
230
|
+
const { store, api, invalidateTags } = createApi(queries);
|
|
231
|
+
renderSync(socket, api, store);
|
|
232
|
+
act(() => {
|
|
233
|
+
socket.trigger("sync", syncEvent({ data: { name: "new" }, method: "update" }));
|
|
234
|
+
});
|
|
235
|
+
expect(invalidateTags).toHaveBeenCalledWith(["todos"]);
|
|
236
|
+
});
|
|
237
|
+
it("removes a deleted entity from cached list queries and decrements total", () => {
|
|
238
|
+
const socket = createFakeSocket(true);
|
|
239
|
+
const queries = {
|
|
240
|
+
"getTodos()": {
|
|
241
|
+
data: {
|
|
242
|
+
data: [
|
|
243
|
+
{ _id: "1", name: "a" },
|
|
244
|
+
{ _id: "2", name: "b" },
|
|
245
|
+
],
|
|
246
|
+
total: 2,
|
|
247
|
+
},
|
|
248
|
+
endpointName: "getTodos",
|
|
249
|
+
originalArgs: undefined,
|
|
250
|
+
status: "fulfilled",
|
|
251
|
+
},
|
|
252
|
+
};
|
|
253
|
+
const { store, api, updateQueryData } = createApi(queries);
|
|
254
|
+
renderSync(socket, api, store);
|
|
255
|
+
act(() => {
|
|
256
|
+
socket.trigger("sync", syncEvent({ method: "delete" }));
|
|
257
|
+
});
|
|
258
|
+
expect(updateQueryData).toHaveBeenCalled();
|
|
259
|
+
expect(queries["getTodos()"].data.data).toHaveLength(1);
|
|
260
|
+
expect(queries["getTodos()"].data.total).toBe(1);
|
|
261
|
+
});
|
|
262
|
+
it("invalidates tags on delete when there are no cached queries", () => {
|
|
263
|
+
const socket = createFakeSocket(true);
|
|
264
|
+
const { store, api, invalidateTags } = createApi(null);
|
|
265
|
+
renderSync(socket, api, store);
|
|
266
|
+
act(() => {
|
|
267
|
+
socket.trigger("sync", syncEvent({ method: "delete" }));
|
|
268
|
+
});
|
|
269
|
+
expect(invalidateTags).toHaveBeenCalledWith(["todos"]);
|
|
270
|
+
});
|
|
271
|
+
it("invalidates tags on delete when no cached query matches", () => {
|
|
272
|
+
const socket = createFakeSocket(true);
|
|
273
|
+
const queries = {
|
|
274
|
+
"getOthers()": {
|
|
275
|
+
data: { data: [{ _id: "999" }] },
|
|
276
|
+
endpointName: "getOthers",
|
|
277
|
+
originalArgs: undefined,
|
|
278
|
+
status: "fulfilled",
|
|
279
|
+
},
|
|
280
|
+
};
|
|
281
|
+
const { store, api, invalidateTags } = createApi(queries);
|
|
282
|
+
renderSync(socket, api, store);
|
|
283
|
+
act(() => {
|
|
284
|
+
socket.trigger("sync", syncEvent({ method: "delete" }));
|
|
285
|
+
});
|
|
286
|
+
expect(invalidateTags).toHaveBeenCalledWith(["todos"]);
|
|
287
|
+
});
|
|
288
|
+
it("does not log sync details when debug is disabled", () => {
|
|
289
|
+
const socket = createFakeSocket(true);
|
|
290
|
+
const { store, api, invalidateTags } = createApi({});
|
|
291
|
+
renderSync(socket, api, store, { debug: false });
|
|
292
|
+
act(() => {
|
|
293
|
+
socket.trigger("sync", syncEvent({ method: "create" }));
|
|
294
|
+
});
|
|
295
|
+
expect(invalidateTags).toHaveBeenCalledWith(["todos"]);
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
//# sourceMappingURL=sync.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sync.test.js","sourceRoot":"","sources":["../src/sync.test.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"@openfeature/core": "^1.11.0",
|
|
5
5
|
"@react-native-async-storage/async-storage": "2.2.0",
|
|
6
6
|
"@reduxjs/toolkit": "^2.11.1",
|
|
7
|
-
"@terreno/ui": "0.
|
|
7
|
+
"@terreno/ui": "0.26.0",
|
|
8
8
|
"async-mutex": "^0.5.0",
|
|
9
9
|
"axios": "^1.13.2",
|
|
10
10
|
"axios-retry": "^4.5.0",
|
|
@@ -73,5 +73,5 @@
|
|
|
73
73
|
"test:coverage": "bun run ../scripts/check-coverage.ts"
|
|
74
74
|
},
|
|
75
75
|
"types": "dist/index.d.ts",
|
|
76
|
-
"version": "0.
|
|
76
|
+
"version": "0.26.0"
|
|
77
77
|
}
|
package/src/authSlice.test.ts
CHANGED
|
@@ -4,6 +4,7 @@ import AsyncStorage from "@react-native-async-storage/async-storage";
|
|
|
4
4
|
import {configureStore} from "@reduxjs/toolkit";
|
|
5
5
|
import {createApi, fetchBaseQuery} from "@reduxjs/toolkit/query/react";
|
|
6
6
|
import {renderHook} from "@testing-library/react-native";
|
|
7
|
+
import {assert} from "chai";
|
|
7
8
|
import React from "react";
|
|
8
9
|
import {Provider} from "react-redux";
|
|
9
10
|
|
|
@@ -18,6 +19,7 @@ import {
|
|
|
18
19
|
useSelectCurrentUserId,
|
|
19
20
|
useSelectIsAuthenticating,
|
|
20
21
|
} from "./authSlice";
|
|
22
|
+
import type {RootState} from "./constants";
|
|
21
23
|
|
|
22
24
|
// Create a real RTK Query API with the endpoints that generateAuthSlice expects
|
|
23
25
|
const api = createApi({
|
|
@@ -41,7 +43,9 @@ const api = createApi({
|
|
|
41
43
|
});
|
|
42
44
|
|
|
43
45
|
const createTestStore = () => {
|
|
44
|
-
const {authReducer, middleware, ...rest} = generateAuthSlice(
|
|
46
|
+
const {authReducer, middleware, ...rest} = generateAuthSlice(
|
|
47
|
+
api as unknown as Parameters<typeof generateAuthSlice>[0]
|
|
48
|
+
);
|
|
45
49
|
|
|
46
50
|
return {
|
|
47
51
|
...rest,
|
|
@@ -281,26 +285,30 @@ describe("generateAuthSlice", () => {
|
|
|
281
285
|
|
|
282
286
|
describe("selectors", () => {
|
|
283
287
|
it("selectCurrentUserId returns userId", () => {
|
|
284
|
-
const state = {auth: {userId: "user-123"}}
|
|
285
|
-
|
|
288
|
+
const state: RootState = {auth: {lastTokenRefreshTimestamp: null, userId: "user-123"}};
|
|
289
|
+
assert.equal(selectCurrentUserId(state), "user-123");
|
|
286
290
|
});
|
|
287
291
|
|
|
288
292
|
it("selectCurrentUserId returns undefined when no auth state", () => {
|
|
289
|
-
|
|
293
|
+
assert.isUndefined(selectCurrentUserId({}));
|
|
290
294
|
});
|
|
291
295
|
|
|
292
296
|
it("selectIsAuthenticating returns isAuthenticating", () => {
|
|
293
|
-
const state = {auth: {isAuthenticating: true}}
|
|
294
|
-
|
|
297
|
+
const state: RootState = {auth: {isAuthenticating: true, lastTokenRefreshTimestamp: null}};
|
|
298
|
+
assert.isTrue(selectIsAuthenticating(state));
|
|
295
299
|
});
|
|
296
300
|
|
|
297
301
|
it("selectIsAuthenticating defaults to false", () => {
|
|
298
|
-
|
|
302
|
+
assert.isFalse(selectIsAuthenticating({}));
|
|
299
303
|
});
|
|
300
304
|
|
|
301
305
|
it("selectLastTokenRefreshTimestamp returns timestamp", () => {
|
|
302
|
-
const state = {auth: {lastTokenRefreshTimestamp: 12345}}
|
|
303
|
-
|
|
306
|
+
const state: RootState = {auth: {lastTokenRefreshTimestamp: 12345}};
|
|
307
|
+
assert.equal(selectLastTokenRefreshTimestamp(state), 12345);
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it("selectLastTokenRefreshTimestamp defaults to null", () => {
|
|
311
|
+
assert.isNull(selectLastTokenRefreshTimestamp({}));
|
|
304
312
|
});
|
|
305
313
|
});
|
|
306
314
|
|
|
@@ -321,9 +329,9 @@ describe("EmailLoginRequest type", () => {
|
|
|
321
329
|
describe("generateProfileEndpoints", () => {
|
|
322
330
|
it("builds endpoint query payloads", () => {
|
|
323
331
|
const builder = {
|
|
324
|
-
mutation: (config:
|
|
325
|
-
};
|
|
326
|
-
const endpoints = generateProfileEndpoints(builder
|
|
332
|
+
mutation: (config: unknown) => config,
|
|
333
|
+
} as unknown as Parameters<typeof generateProfileEndpoints>[0];
|
|
334
|
+
const endpoints = generateProfileEndpoints(builder, "todos");
|
|
327
335
|
const createEmailUserQuery = endpoints.createEmailUser.query;
|
|
328
336
|
const emailLoginQuery = endpoints.emailLogin.query;
|
|
329
337
|
const emailSignUpQuery = endpoints.emailSignUp.query;
|
package/src/authSlice.ts
CHANGED
|
@@ -298,7 +298,7 @@ export const generateAuthSlice = (api: Api<any, any, any, any, any>) => {
|
|
|
298
298
|
|
|
299
299
|
export const selectCurrentUserId = (state: RootState): string | undefined => state.auth?.userId;
|
|
300
300
|
export const selectLastTokenRefreshTimestamp = (state: RootState): number | null =>
|
|
301
|
-
state.auth?.lastTokenRefreshTimestamp;
|
|
301
|
+
state.auth?.lastTokenRefreshTimestamp ?? null;
|
|
302
302
|
export const selectIsAuthenticating = (state: RootState): boolean =>
|
|
303
303
|
state.auth?.isAuthenticating ?? false;
|
|
304
304
|
|
package/src/constants.ts
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import Constants from "expo-constants";
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
export interface RootState {
|
|
4
|
+
auth?: {
|
|
5
|
+
isAuthenticating?: boolean;
|
|
6
|
+
lastTokenRefreshTimestamp: number | null;
|
|
7
|
+
userId?: string;
|
|
8
|
+
};
|
|
9
|
+
}
|
|
5
10
|
export const LOGOUT_ACTION_TYPE = "auth/logout";
|
|
6
11
|
export const TOKEN_REFRESHED_SUCCESS = "auth/tokenRefreshedSuccess";
|
|
7
12
|
|
|
@@ -1,92 +1,95 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Isolated tests for constants.ts paths that require mock.module.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* The module is loaded in a subprocess so mock.module does not replace
|
|
5
|
+
* expo-constants for the rest of the package test process.
|
|
6
6
|
*/
|
|
7
|
-
import {describe,
|
|
7
|
+
import {describe, it} from "bun:test";
|
|
8
|
+
import {assert} from "chai";
|
|
9
|
+
|
|
10
|
+
interface ConstantsModuleResult {
|
|
11
|
+
authDebug: boolean;
|
|
12
|
+
debugCalls: unknown[][];
|
|
13
|
+
errorCalls: unknown[][];
|
|
14
|
+
infoCalls: unknown[][];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const runConstantsModule = async (
|
|
18
|
+
expoConstants: Record<string, unknown>
|
|
19
|
+
): Promise<ConstantsModuleResult> => {
|
|
20
|
+
const constantsUrl = new URL("../constants.ts", import.meta.url).href;
|
|
21
|
+
const script = `
|
|
22
|
+
import {mock} from "bun:test";
|
|
23
|
+
const output = console.log.bind(console);
|
|
24
|
+
const debugCalls = [];
|
|
25
|
+
const errorCalls = [];
|
|
26
|
+
const infoCalls = [];
|
|
27
|
+
console.debug = (...args) => debugCalls.push(args);
|
|
28
|
+
console.error = (...args) => errorCalls.push(args);
|
|
29
|
+
console.info = (...args) => infoCalls.push(args);
|
|
30
|
+
mock.module("expo-constants", () => ({default: ${JSON.stringify(expoConstants)}}));
|
|
31
|
+
const loaded = await import(${JSON.stringify(constantsUrl)});
|
|
32
|
+
loaded.logAuth("hello");
|
|
33
|
+
loaded.logSocket(undefined, "ws on");
|
|
34
|
+
output(JSON.stringify({
|
|
35
|
+
authDebug: loaded.AUTH_DEBUG,
|
|
36
|
+
debugCalls,
|
|
37
|
+
errorCalls,
|
|
38
|
+
infoCalls,
|
|
39
|
+
}));
|
|
40
|
+
`;
|
|
41
|
+
const child = Bun.spawn([process.execPath, "-e", script], {
|
|
42
|
+
cwd: import.meta.dir,
|
|
43
|
+
stderr: "pipe",
|
|
44
|
+
stdout: "pipe",
|
|
45
|
+
});
|
|
46
|
+
const [exitCode, stdout, stderr] = await Promise.all([
|
|
47
|
+
child.exited,
|
|
48
|
+
new Response(child.stdout).text(),
|
|
49
|
+
new Response(child.stderr).text(),
|
|
50
|
+
]);
|
|
51
|
+
|
|
52
|
+
assert.equal(exitCode, 0, stderr);
|
|
53
|
+
return JSON.parse(stdout.trim()) as ConstantsModuleResult;
|
|
54
|
+
};
|
|
8
55
|
|
|
9
56
|
describe("expo tunnel warning", () => {
|
|
10
57
|
it("warns when expoGoConfig.debuggerHost contains exp.direct", async () => {
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
}));
|
|
22
|
-
try {
|
|
23
|
-
const testId = `${Date.now()}-${Math.random()}`;
|
|
24
|
-
await import(`../constants?case=${testId}`);
|
|
25
|
-
const warning = errorCalls.find((args) =>
|
|
26
|
-
args.some((v) => typeof v === "string" && v.includes("Expo Tunnel is not currently"))
|
|
27
|
-
);
|
|
28
|
-
expect(warning).toBeDefined();
|
|
29
|
-
} finally {
|
|
30
|
-
console.error = originalError;
|
|
31
|
-
mock.module("expo-constants", () => ({default: {expoConfig: {extra: {}}}}));
|
|
32
|
-
}
|
|
58
|
+
const result = await runConstantsModule({
|
|
59
|
+
expoConfig: {extra: {}},
|
|
60
|
+
expoGoConfig: {debuggerHost: "abc.exp.direct"},
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
assert.isTrue(
|
|
64
|
+
result.errorCalls.some((args) =>
|
|
65
|
+
args.some((value) => String(value).includes("Expo Tunnel is not currently"))
|
|
66
|
+
)
|
|
67
|
+
);
|
|
33
68
|
});
|
|
34
69
|
});
|
|
35
70
|
|
|
36
71
|
describe("AUTH_DEBUG enabled path", () => {
|
|
37
|
-
it("logs debug messages
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
const originalInfo = console.info;
|
|
42
|
-
console.debug = (...args: unknown[]): void => {
|
|
43
|
-
debugCalls.push(args);
|
|
44
|
-
};
|
|
45
|
-
console.info = (...args: unknown[]): void => {
|
|
46
|
-
infoCalls.push(args);
|
|
47
|
-
};
|
|
48
|
-
mock.module("expo-constants", () => ({
|
|
49
|
-
default: {expoConfig: {extra: {AUTH_DEBUG: "true", WEBSOCKETS_DEBUG: "true"}}},
|
|
50
|
-
}));
|
|
51
|
-
try {
|
|
52
|
-
const testId = `${Date.now()}-${Math.random()}`;
|
|
53
|
-
const loaded = (await import(`../constants?case=${testId}`)) as typeof import("../constants");
|
|
54
|
-
expect(loaded.AUTH_DEBUG).toBe(true);
|
|
55
|
-
const preLength = debugCalls.length;
|
|
56
|
-
loaded.logAuth("hello");
|
|
57
|
-
expect(debugCalls.length).toBe(preLength + 1);
|
|
58
|
-
loaded.logSocket(undefined, "ws on");
|
|
59
|
-
expect(infoCalls.some((args) => args[0] === "[websocket]" && args[1] === "ws on")).toBe(true);
|
|
60
|
-
} finally {
|
|
61
|
-
console.debug = originalDebug;
|
|
62
|
-
console.info = originalInfo;
|
|
63
|
-
mock.module("expo-constants", () => ({default: {expoConfig: {extra: {}}}}));
|
|
64
|
-
}
|
|
65
|
-
});
|
|
72
|
+
it("logs auth and websocket debug messages when enabled", async () => {
|
|
73
|
+
const result = await runConstantsModule({
|
|
74
|
+
expoConfig: {extra: {AUTH_DEBUG: "true", WEBSOCKETS_DEBUG: "true"}},
|
|
75
|
+
});
|
|
66
76
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
);
|
|
85
|
-
expect(authMsg).toBeDefined();
|
|
86
|
-
expect(wsMsg).toBeDefined();
|
|
87
|
-
} finally {
|
|
88
|
-
console.debug = originalDebug;
|
|
89
|
-
mock.module("expo-constants", () => ({default: {expoConfig: {extra: {}}}}));
|
|
90
|
-
}
|
|
77
|
+
assert.isTrue(result.authDebug);
|
|
78
|
+
assert.isTrue(
|
|
79
|
+
result.debugCalls.some((args) =>
|
|
80
|
+
args.some((value) => String(value).includes("AUTH_DEBUG is enabled"))
|
|
81
|
+
)
|
|
82
|
+
);
|
|
83
|
+
assert.isTrue(
|
|
84
|
+
result.debugCalls.some((args) =>
|
|
85
|
+
args.some((value) => String(value).includes("WEBSOCKETS_DEBUG is enabled"))
|
|
86
|
+
)
|
|
87
|
+
);
|
|
88
|
+
assert.isTrue(
|
|
89
|
+
result.debugCalls.some((args) => args.some((value) => String(value).includes("hello")))
|
|
90
|
+
);
|
|
91
|
+
assert.isTrue(
|
|
92
|
+
result.infoCalls.some((args) => args[0] === "[websocket]" && args[1] === "ws on")
|
|
93
|
+
);
|
|
91
94
|
});
|
|
92
95
|
});
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
// biome-ignore-all lint/suspicious/noExplicitAny: test mock typing
|
|
2
1
|
import {describe, expect, it} from "bun:test";
|
|
3
2
|
|
|
4
3
|
import {type ListResponse, populateId} from "./mongooseSlice";
|
|
@@ -39,7 +38,7 @@ describe("populateId", () => {
|
|
|
39
38
|
|
|
40
39
|
it("handles sparse arrays without throwing", () => {
|
|
41
40
|
const sparse: ListResponse<{_id: string}> = {
|
|
42
|
-
data: [undefined as
|
|
41
|
+
data: [undefined as unknown as {_id: string}, {_id: "found"}],
|
|
43
42
|
};
|
|
44
43
|
expect(populateId("found", sparse)).toEqual({_id: "found"});
|
|
45
44
|
});
|