@terreno/rtk 0.10.0 → 0.11.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.
Files changed (65) hide show
  1. package/dist/authSlice.test.js +68 -0
  2. package/dist/authSlice.test.js.map +1 -1
  3. package/dist/authSliceNative.test.d.ts +2 -0
  4. package/dist/authSliceNative.test.d.ts.map +1 -0
  5. package/dist/authSliceNative.test.js +167 -0
  6. package/dist/authSliceNative.test.js.map +1 -0
  7. package/dist/betterAuthClient.d.ts +16 -0
  8. package/dist/betterAuthClient.d.ts.map +1 -1
  9. package/dist/betterAuthClient.js +5 -2
  10. package/dist/betterAuthClient.js.map +1 -1
  11. package/dist/betterAuthClient.test.d.ts +2 -0
  12. package/dist/betterAuthClient.test.d.ts.map +1 -0
  13. package/dist/betterAuthClient.test.js +151 -0
  14. package/dist/betterAuthClient.test.js.map +1 -0
  15. package/dist/betterAuthSlice.test.js +54 -1
  16. package/dist/betterAuthSlice.test.js.map +1 -1
  17. package/dist/buildNumber.test.d.ts +2 -0
  18. package/dist/buildNumber.test.d.ts.map +1 -0
  19. package/dist/buildNumber.test.js +95 -0
  20. package/dist/buildNumber.test.js.map +1 -0
  21. package/dist/constants.d.ts +27 -3
  22. package/dist/constants.d.ts.map +1 -1
  23. package/dist/constants.js +45 -56
  24. package/dist/constants.js.map +1 -1
  25. package/dist/constants.test.js +174 -123
  26. package/dist/constants.test.js.map +1 -1
  27. package/dist/mongooseSlice.test.d.ts +2 -0
  28. package/dist/mongooseSlice.test.d.ts.map +1 -0
  29. package/dist/mongooseSlice.test.js +39 -0
  30. package/dist/mongooseSlice.test.js.map +1 -0
  31. package/dist/tagGenerator.test.d.ts +2 -0
  32. package/dist/tagGenerator.test.d.ts.map +1 -0
  33. package/dist/tagGenerator.test.js +96 -0
  34. package/dist/tagGenerator.test.js.map +1 -0
  35. package/dist/testPreload.test.d.ts +2 -0
  36. package/dist/testPreload.test.d.ts.map +1 -0
  37. package/dist/testPreload.test.js +27 -0
  38. package/dist/testPreload.test.js.map +1 -0
  39. package/dist/useFeatureFlags.d.ts +25 -1
  40. package/dist/useFeatureFlags.d.ts.map +1 -1
  41. package/dist/useFeatureFlags.js +18 -16
  42. package/dist/useFeatureFlags.js.map +1 -1
  43. package/dist/useFeatureFlags.test.d.ts +2 -0
  44. package/dist/useFeatureFlags.test.d.ts.map +1 -0
  45. package/dist/useFeatureFlags.test.js +162 -0
  46. package/dist/useFeatureFlags.test.js.map +1 -0
  47. package/package.json +6 -3
  48. package/src/authSlice.test.ts +79 -0
  49. package/src/authSliceNative.test.ts +187 -0
  50. package/src/betterAuthClient.test.ts +176 -0
  51. package/src/betterAuthClient.ts +6 -3
  52. package/src/betterAuthSlice.test.ts +67 -0
  53. package/src/buildNumber.test.ts +120 -0
  54. package/src/constants.test.ts +193 -154
  55. package/src/constants.ts +72 -70
  56. package/src/mongooseSlice.test.ts +46 -0
  57. package/src/tagGenerator.test.ts +109 -0
  58. package/src/testPreload.test.ts +30 -0
  59. package/src/useFeatureFlags.test.ts +209 -0
  60. package/src/useFeatureFlags.ts +44 -5
  61. package/dist/test-preload.d.ts +0 -2
  62. package/dist/test-preload.d.ts.map +0 -1
  63. package/dist/test-preload.js +0 -24
  64. package/dist/test-preload.js.map +0 -1
  65. package/src/test-preload.ts +0 -28
@@ -1,22 +1,24 @@
1
1
  import { useCallback, useEffect, useMemo, useRef } from "react";
2
2
  /**
3
- * Creates feature flag accessors from an RTK Query API instance.
3
+ * Normalizes the legacy-compatible `basePathOrOptions` argument into a
4
+ * `{basePath, skip}` pair with defaults applied.
4
5
  *
5
- * Injects a `GET {basePath}/evaluate` endpoint into the API and returns
6
- * accessors for reading flag values. Fetches once on mount and caches via
7
- * RTK Query. Both `api` and `basePath` should be stable references.
8
- *
9
- * @example
10
- * ```typescript
11
- * const { getFlag, getVariant } = useFeatureFlags(terrenoApi);
12
- *
13
- * const showNewCheckout = getFlag("new-checkout-flow"); // true | false
14
- * const variant = getVariant("checkout-experiment"); // "control" | "variant-a" | null
15
- * ```
6
+ * - `undefined` -> `{basePath: "/feature-flags", skip: false}`
7
+ * - `string` -> `{basePath: <string>, skip: false}` (legacy form)
8
+ * - `object` -> `{basePath: opts.basePath ?? "/feature-flags", skip: opts.skip ?? false}`
16
9
  */
17
- export const useFeatureFlags = (
10
+ export const resolveFeatureFlagsOptions = (basePathOrOptions) => {
11
+ const { basePath = "/feature-flags", skip = false } = typeof basePathOrOptions === "string"
12
+ ? { basePath: basePathOrOptions, skip: false }
13
+ : (basePathOrOptions ?? {});
14
+ return { basePath, skip };
15
+ };
16
+ // Overloaded signature preserves backwards compatibility with callers that
17
+ // pass a string basePath as the second argument.
18
+ export function useFeatureFlags(
18
19
  // biome-ignore lint/suspicious/noExplicitAny: RTK Query API generic typing is intentionally flexible here.
19
- api, basePath = "/feature-flags") => {
20
+ api, basePathOrOptions) {
21
+ const { basePath, skip } = resolveFeatureFlagsOptions(basePathOrOptions);
20
22
  const enhancedApi = useMemo(() => api.injectEndpoints({
21
23
  endpoints: (builder) => ({
22
24
  evaluateFeatureFlags: builder.query({
@@ -30,7 +32,7 @@ api, basePath = "/feature-flags") => {
30
32
  }), [api, basePath]);
31
33
  // biome-ignore lint/suspicious/noExplicitAny: Endpoint hook is injected dynamically by RTK Query.
32
34
  const useEvaluateQuery = enhancedApi.useEvaluateFeatureFlagsQuery;
33
- const { data, isLoading, error, refetch } = useEvaluateQuery();
35
+ const { data, isLoading, error, refetch } = useEvaluateQuery(undefined, { skip });
34
36
  const evaluateStartedAtRef = useRef(null);
35
37
  const flags = data ?? {};
36
38
  // Log when evaluate request enters loading state so client timing can be measured.
@@ -82,5 +84,5 @@ api, basePath = "/feature-flags") => {
82
84
  return null;
83
85
  }, [flags]);
84
86
  return { error, flags, getFlag, getVariant, isLoading, refetch };
85
- };
87
+ }
86
88
  //# sourceMappingURL=useFeatureFlags.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"useFeatureFlags.js","sourceRoot":"","sources":["../src/useFeatureFlags.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"useFeatureFlags.js","sourceRoot":"","sources":["../src/useFeatureFlags.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=useFeatureFlags.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useFeatureFlags.test.d.ts","sourceRoot":"","sources":["../src/useFeatureFlags.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,162 @@
1
+ import { afterEach, beforeEach, describe, expect, it, mock } from "bun:test";
2
+ import { renderHook } from "@testing-library/react-native";
3
+ import { resolveFeatureFlagsOptions, useFeatureFlags } from "./useFeatureFlags";
4
+ describe("resolveFeatureFlagsOptions", () => {
5
+ it("applies defaults when no argument is provided", () => {
6
+ expect(resolveFeatureFlagsOptions()).toEqual({
7
+ basePath: "/feature-flags",
8
+ skip: false,
9
+ });
10
+ });
11
+ it("applies defaults when an empty options object is provided", () => {
12
+ expect(resolveFeatureFlagsOptions({})).toEqual({
13
+ basePath: "/feature-flags",
14
+ skip: false,
15
+ });
16
+ });
17
+ it("treats a string argument as a legacy basePath with skip=false", () => {
18
+ expect(resolveFeatureFlagsOptions("/custom-path")).toEqual({
19
+ basePath: "/custom-path",
20
+ skip: false,
21
+ });
22
+ });
23
+ it("preserves an empty string basePath when passed as a legacy string argument", () => {
24
+ expect(resolveFeatureFlagsOptions("")).toEqual({
25
+ basePath: "",
26
+ skip: false,
27
+ });
28
+ });
29
+ it("uses options.basePath when provided", () => {
30
+ expect(resolveFeatureFlagsOptions({ basePath: "/flags" })).toEqual({
31
+ basePath: "/flags",
32
+ skip: false,
33
+ });
34
+ });
35
+ it("uses options.skip when provided", () => {
36
+ expect(resolveFeatureFlagsOptions({ skip: true })).toEqual({
37
+ basePath: "/feature-flags",
38
+ skip: true,
39
+ });
40
+ });
41
+ it("uses both options.basePath and options.skip when provided together", () => {
42
+ expect(resolveFeatureFlagsOptions({ basePath: "/flags", skip: true })).toEqual({
43
+ basePath: "/flags",
44
+ skip: true,
45
+ });
46
+ });
47
+ it("does not let a legacy string basePath override skip to true", () => {
48
+ expect(resolveFeatureFlagsOptions("/custom-path").skip).toBe(false);
49
+ });
50
+ });
51
+ const buildApi = (queryResult) => {
52
+ const refetch = mock(() => { });
53
+ const capturedQueryBuilder = [];
54
+ const useEvaluateFeatureFlagsQuery = mock(() => ({
55
+ data: queryResult.data,
56
+ error: queryResult.error,
57
+ isLoading: queryResult.isLoading ?? false,
58
+ refetch,
59
+ }));
60
+ const api = {
61
+ injectEndpoints: mock((opts) => {
62
+ const builder = {
63
+ query: (def) => {
64
+ capturedQueryBuilder.push(def.query());
65
+ return { useQuery: useEvaluateFeatureFlagsQuery };
66
+ },
67
+ };
68
+ opts.endpoints(builder);
69
+ return { useEvaluateFeatureFlagsQuery };
70
+ }),
71
+ };
72
+ return { api, capturedQueryBuilder, refetch, useEvaluateFeatureFlagsQuery };
73
+ };
74
+ describe("useFeatureFlags hook", () => {
75
+ const debugCalls = [];
76
+ const originalDebug = console.debug;
77
+ beforeEach(() => {
78
+ debugCalls.length = 0;
79
+ console.debug = (...args) => {
80
+ debugCalls.push(args);
81
+ };
82
+ });
83
+ afterEach(() => {
84
+ console.debug = originalDebug;
85
+ });
86
+ it("builds the evaluate endpoint with the default basePath", () => {
87
+ const { api, capturedQueryBuilder } = buildApi({ data: { foo: true } });
88
+ renderHook(() => useFeatureFlags(api));
89
+ expect(capturedQueryBuilder[0]).toEqual({
90
+ method: "GET",
91
+ url: "/feature-flags/evaluate",
92
+ });
93
+ });
94
+ it("builds the evaluate endpoint with a custom basePath from legacy string argument", () => {
95
+ const { api, capturedQueryBuilder } = buildApi({ data: { foo: true } });
96
+ renderHook(() => useFeatureFlags(api, "/flags"));
97
+ expect(capturedQueryBuilder[0]).toEqual({ method: "GET", url: "/flags/evaluate" });
98
+ });
99
+ it("returns flag accessors that read boolean and string values", () => {
100
+ const { api } = buildApi({
101
+ data: { booleanOff: false, booleanOn: true, variantFlag: "variant-a" },
102
+ });
103
+ const { result } = renderHook(() => useFeatureFlags(api));
104
+ expect(result.current.getFlag("booleanOn")).toBe(true);
105
+ expect(result.current.getFlag("booleanOff")).toBe(false);
106
+ expect(result.current.getFlag("variantFlag")).toBe(false);
107
+ expect(result.current.getVariant("variantFlag")).toBe("variant-a");
108
+ expect(result.current.getVariant("booleanOn")).toBeNull();
109
+ expect(result.current.getVariant("missing")).toBeNull();
110
+ });
111
+ it("returns an empty flags map when no data is available", () => {
112
+ const { api } = buildApi({ isLoading: true });
113
+ const { result } = renderHook(() => useFeatureFlags(api));
114
+ expect(result.current.flags).toEqual({});
115
+ expect(result.current.isLoading).toBe(true);
116
+ });
117
+ it("exposes refetch from the underlying query hook", () => {
118
+ const { api, refetch } = buildApi({ data: { foo: true } });
119
+ const { result } = renderHook(() => useFeatureFlags(api));
120
+ result.current.refetch();
121
+ expect(refetch).toHaveBeenCalled();
122
+ });
123
+ it("logs evaluate request started when loading begins without prior data", () => {
124
+ const { api } = buildApi({ isLoading: true });
125
+ renderHook(() => useFeatureFlags(api));
126
+ const started = debugCalls.find((args) => args[0] === "[feature-flags] evaluate request started");
127
+ expect(started).toBeDefined();
128
+ });
129
+ it("logs evaluate request completed when data resolves after a loading phase", () => {
130
+ const api = buildApi({ isLoading: true });
131
+ const { rerender } = renderHook(({ queryResult }) => {
132
+ api.useEvaluateFeatureFlagsQuery.mockImplementationOnce(() => ({
133
+ data: queryResult.data,
134
+ error: queryResult.error,
135
+ isLoading: queryResult.isLoading ?? false,
136
+ refetch: api.refetch,
137
+ }));
138
+ return useFeatureFlags(api.api);
139
+ }, { initialProps: { queryResult: { isLoading: true } } });
140
+ rerender({ queryResult: { data: { alpha: true }, isLoading: false } });
141
+ const completed = debugCalls.find((args) => args[0] === "[feature-flags] evaluate request completed");
142
+ const started = debugCalls.find((args) => args[0] === "[feature-flags] evaluate request started");
143
+ expect(started).toBeDefined();
144
+ expect(completed).toBeDefined();
145
+ });
146
+ it("logs evaluate request failed when error is returned after loading", () => {
147
+ const api = buildApi({ isLoading: true });
148
+ const { rerender } = renderHook(({ queryResult }) => {
149
+ api.useEvaluateFeatureFlagsQuery.mockImplementationOnce(() => ({
150
+ data: queryResult.data,
151
+ error: queryResult.error,
152
+ isLoading: queryResult.isLoading ?? false,
153
+ refetch: api.refetch,
154
+ }));
155
+ return useFeatureFlags(api.api);
156
+ }, { initialProps: { queryResult: { isLoading: true } } });
157
+ rerender({ queryResult: { error: new Error("boom"), isLoading: false } });
158
+ const failed = debugCalls.find((args) => args[0] === "[feature-flags] evaluate request failed");
159
+ expect(failed).toBeDefined();
160
+ });
161
+ });
162
+ //# sourceMappingURL=useFeatureFlags.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useFeatureFlags.test.js","sourceRoot":"","sources":["../src/useFeatureFlags.test.ts"],"names":[],"mappings":"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"}
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.10.0",
6
+ "@terreno/ui": "0.11.0",
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.10.0"
70
+ "version": "0.11.0"
68
71
  }
@@ -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
+ });