@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.
Files changed (80) 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/isolated/useUpgradeCheck.isolated.d.ts +2 -0
  28. package/dist/isolated/useUpgradeCheck.isolated.d.ts.map +1 -0
  29. package/dist/isolated/useUpgradeCheck.isolated.js +135 -0
  30. package/dist/isolated/useUpgradeCheck.isolated.js.map +1 -0
  31. package/dist/mongooseSlice.test.d.ts +2 -0
  32. package/dist/mongooseSlice.test.d.ts.map +1 -0
  33. package/dist/mongooseSlice.test.js +39 -0
  34. package/dist/mongooseSlice.test.js.map +1 -0
  35. package/dist/tagGenerator.test.d.ts +2 -0
  36. package/dist/tagGenerator.test.d.ts.map +1 -0
  37. package/dist/tagGenerator.test.js +96 -0
  38. package/dist/tagGenerator.test.js.map +1 -0
  39. package/dist/testPreload.test.d.ts +2 -0
  40. package/dist/testPreload.test.d.ts.map +1 -0
  41. package/dist/testPreload.test.js +27 -0
  42. package/dist/testPreload.test.js.map +1 -0
  43. package/dist/useFeatureFlags.d.ts +25 -1
  44. package/dist/useFeatureFlags.d.ts.map +1 -1
  45. package/dist/useFeatureFlags.js +18 -16
  46. package/dist/useFeatureFlags.js.map +1 -1
  47. package/dist/useFeatureFlags.test.d.ts +2 -0
  48. package/dist/useFeatureFlags.test.d.ts.map +1 -0
  49. package/dist/useFeatureFlags.test.js +162 -0
  50. package/dist/useFeatureFlags.test.js.map +1 -0
  51. package/dist/useUpgradeCheck.d.ts +2 -0
  52. package/dist/useUpgradeCheck.d.ts.map +1 -1
  53. package/dist/useUpgradeCheck.js +39 -46
  54. package/dist/useUpgradeCheck.js.map +1 -1
  55. package/dist/useUpgradeCheck.test.d.ts +2 -0
  56. package/dist/useUpgradeCheck.test.d.ts.map +1 -0
  57. package/dist/useUpgradeCheck.test.js +326 -0
  58. package/dist/useUpgradeCheck.test.js.map +1 -0
  59. package/package.json +6 -3
  60. package/src/authSlice.test.ts +79 -0
  61. package/src/authSliceNative.test.ts +187 -0
  62. package/src/betterAuthClient.test.ts +176 -0
  63. package/src/betterAuthClient.ts +6 -3
  64. package/src/betterAuthSlice.test.ts +67 -0
  65. package/src/buildNumber.test.ts +120 -0
  66. package/src/constants.test.ts +193 -154
  67. package/src/constants.ts +72 -70
  68. package/src/isolated/useUpgradeCheck.isolated.ts +175 -0
  69. package/src/mongooseSlice.test.ts +46 -0
  70. package/src/tagGenerator.test.ts +109 -0
  71. package/src/testPreload.test.ts +30 -0
  72. package/src/useFeatureFlags.test.ts +209 -0
  73. package/src/useFeatureFlags.ts +44 -5
  74. package/src/useUpgradeCheck.test.ts +408 -0
  75. package/src/useUpgradeCheck.ts +41 -48
  76. package/dist/test-preload.d.ts +0 -2
  77. package/dist/test-preload.d.ts.map +0 -1
  78. package/dist/test-preload.js +0 -24
  79. package/dist/test-preload.js.map +0 -1
  80. package/src/test-preload.ts +0 -28
@@ -0,0 +1,176 @@
1
+ import {afterAll, afterEach, beforeEach, describe, expect, it, mock} from "bun:test";
2
+
3
+ type ExpoOptions = {
4
+ scheme?: string;
5
+ storagePrefix?: string;
6
+ storage?: {
7
+ getItem: (key: string) => Promise<string | null>;
8
+ setItem: (key: string, value: string) => Promise<void>;
9
+ removeItem: (key: string) => Promise<void>;
10
+ };
11
+ };
12
+
13
+ const captured: {expo: ExpoOptions | null; auth: Record<string, unknown> | null} = {
14
+ auth: null,
15
+ expo: null,
16
+ };
17
+
18
+ mock.module("@better-auth/expo/client", () => ({
19
+ expoClient: (options: ExpoOptions) => {
20
+ captured.expo = options;
21
+ return {name: "expo-plugin"};
22
+ },
23
+ }));
24
+
25
+ mock.module("better-auth/react", () => ({
26
+ createAuthClient: (config: Record<string, unknown>) => {
27
+ captured.auth = config;
28
+ return {clientId: "mock-client", config};
29
+ },
30
+ }));
31
+
32
+ const secureCalls = {
33
+ delete: [] as string[],
34
+ get: [] as string[],
35
+ set: [] as Array<[string, string]>,
36
+ };
37
+ const resetSecureCalls = (): void => {
38
+ secureCalls.delete = [];
39
+ secureCalls.get = [];
40
+ secureCalls.set = [];
41
+ };
42
+
43
+ mock.module("expo-secure-store", () => ({
44
+ deleteItemAsync: async (key: string): Promise<void> => {
45
+ secureCalls.delete.push(key);
46
+ },
47
+ getItemAsync: async (key: string): Promise<string | null> => {
48
+ secureCalls.get.push(key);
49
+ return `secure-${key}`;
50
+ },
51
+ setItemAsync: async (key: string, value: string): Promise<void> => {
52
+ secureCalls.set.push([key, value]);
53
+ },
54
+ }));
55
+
56
+ const AsyncStorage = (await import("@react-native-async-storage/async-storage")).default;
57
+ const {createBetterAuthClient, createStorageAdapter} = await import("./betterAuthClient");
58
+
59
+ describe("createStorageAdapter (web)", () => {
60
+ const globalWithWindow = globalThis as {window?: unknown};
61
+ const originalGet = AsyncStorage.getItem;
62
+ const originalSet = AsyncStorage.setItem;
63
+ const originalRemove = AsyncStorage.removeItem;
64
+ const calls = {get: [] as string[], remove: [] as string[], set: [] as Array<[string, string]>};
65
+ let originalWindow: unknown;
66
+
67
+ beforeEach(() => {
68
+ originalWindow = globalWithWindow.window;
69
+ calls.get = [];
70
+ calls.remove = [];
71
+ calls.set = [];
72
+ AsyncStorage.getItem = async (key: string): Promise<string | null> => {
73
+ calls.get.push(key);
74
+ return `async-${key}`;
75
+ };
76
+ AsyncStorage.setItem = async (key: string, value: string): Promise<void> => {
77
+ calls.set.push([key, value]);
78
+ };
79
+ AsyncStorage.removeItem = async (key: string): Promise<void> => {
80
+ calls.remove.push(key);
81
+ };
82
+ });
83
+
84
+ afterEach(() => {
85
+ AsyncStorage.getItem = originalGet;
86
+ AsyncStorage.setItem = originalSet;
87
+ AsyncStorage.removeItem = originalRemove;
88
+ if (typeof originalWindow === "undefined") {
89
+ delete globalWithWindow.window;
90
+ } else {
91
+ globalWithWindow.window = originalWindow;
92
+ }
93
+ });
94
+
95
+ it("reads, writes, and removes via AsyncStorage when window exists", async () => {
96
+ globalWithWindow.window = {};
97
+ const adapter = createStorageAdapter(true);
98
+ await expect(adapter.getItem("k")).resolves.toBe("async-k");
99
+ await adapter.setItem("a", "b");
100
+ await adapter.removeItem?.("c");
101
+ expect(calls.get).toEqual(["k"]);
102
+ expect(calls.set).toEqual([["a", "b"]]);
103
+ expect(calls.remove).toEqual(["c"]);
104
+ });
105
+
106
+ it("returns null/void without touching AsyncStorage in SSR (no window)", async () => {
107
+ delete globalWithWindow.window;
108
+ const adapter = createStorageAdapter(true);
109
+ await expect(adapter.getItem("k")).resolves.toBeNull();
110
+ await expect(adapter.setItem("a", "b")).resolves.toBeUndefined();
111
+ await expect(adapter.removeItem?.("c")).resolves.toBeUndefined();
112
+ expect(calls.get).toEqual([]);
113
+ expect(calls.set).toEqual([]);
114
+ expect(calls.remove).toEqual([]);
115
+ });
116
+ });
117
+
118
+ describe("createStorageAdapter (native)", () => {
119
+ beforeEach(() => {
120
+ resetSecureCalls();
121
+ });
122
+
123
+ it("routes reads, writes, and deletes through SecureStore", async () => {
124
+ const adapter = createStorageAdapter(false);
125
+ await expect(adapter.getItem("auth")).resolves.toBe("secure-auth");
126
+ await adapter.setItem("auth", "token");
127
+ await adapter.removeItem?.("auth");
128
+ expect(secureCalls.get).toEqual(["auth"]);
129
+ expect(secureCalls.set).toEqual([["auth", "token"]]);
130
+ expect(secureCalls.delete).toEqual(["auth"]);
131
+ });
132
+ });
133
+
134
+ describe("createBetterAuthClient", () => {
135
+ beforeEach(() => {
136
+ captured.auth = null;
137
+ captured.expo = null;
138
+ });
139
+
140
+ afterAll(() => {
141
+ // Restore the test-preload mocks so later test files aren't polluted.
142
+ mock.module("@better-auth/expo/client", () => ({
143
+ expoClient: () => ({name: "expo-plugin"}),
144
+ }));
145
+ mock.module("better-auth/react", () => ({
146
+ createAuthClient: () => ({}),
147
+ }));
148
+ mock.module("expo-secure-store", () => ({
149
+ deleteItemAsync: async () => {},
150
+ getItemAsync: async () => null,
151
+ setItemAsync: async () => {},
152
+ }));
153
+ });
154
+
155
+ it("passes baseURL and scheme through to the Better Auth client", () => {
156
+ const client = createBetterAuthClient({
157
+ baseURL: "http://localhost:3000",
158
+ scheme: "terreno",
159
+ });
160
+ expect(captured.auth).not.toBeNull();
161
+ expect(captured.auth?.baseURL).toBe("http://localhost:3000");
162
+ expect(captured.expo?.scheme).toBe("terreno");
163
+ expect(captured.expo?.storagePrefix).toBe("terreno");
164
+ expect(captured.expo?.storage).toBeDefined();
165
+ expect(client).toBeDefined();
166
+ });
167
+
168
+ it("uses a custom storagePrefix when provided", () => {
169
+ createBetterAuthClient({
170
+ baseURL: "http://localhost:3000",
171
+ scheme: "terreno",
172
+ storagePrefix: "custom",
173
+ });
174
+ expect(captured.expo?.storagePrefix).toBe("custom");
175
+ });
176
+ });
@@ -24,7 +24,7 @@ export type {
24
24
  /**
25
25
  * Storage adapter interface matching what Better Auth expects.
26
26
  */
27
- interface StorageAdapter {
27
+ export interface StorageAdapter {
28
28
  setItem: (key: string, value: string) => void | Promise<void>;
29
29
  getItem: (key: string) => string | null | Promise<string | null>;
30
30
  removeItem?: (key: string) => void | Promise<void>;
@@ -33,9 +33,12 @@ interface StorageAdapter {
33
33
  /**
34
34
  * Async storage adapter for Better Auth that works on both web and native.
35
35
  * Uses SecureStore on native platforms and AsyncStorage on web.
36
+ *
37
+ * `isWeb` is exposed as a parameter so the adapter can be unit tested
38
+ * without having to re-load the module for each platform.
36
39
  */
37
- const createStorageAdapter = (): StorageAdapter => {
38
- if (IsWeb) {
40
+ export const createStorageAdapter = (isWeb: boolean = IsWeb): StorageAdapter => {
41
+ if (isWeb) {
39
42
  return {
40
43
  getItem: (key: string): Promise<string | null> => {
41
44
  if (typeof window !== "undefined") {
@@ -6,6 +6,7 @@ import {
6
6
  selectBetterAuthError,
7
7
  selectBetterAuthIsAuthenticated,
8
8
  selectBetterAuthIsLoading,
9
+ selectBetterAuthState,
9
10
  selectBetterAuthUser,
10
11
  selectBetterAuthUserId,
11
12
  } from "./betterAuthSlice";
@@ -302,6 +303,66 @@ describe("generateBetterAuthSlice", () => {
302
303
  expect(mockAuthClient.signOut).toHaveBeenCalledTimes(1);
303
304
  });
304
305
 
306
+ it("logs and swallows signOut errors triggered by the slice logout action", async () => {
307
+ mockAuthClient.signOut = mock(() =>
308
+ Promise.reject(new Error("slice-signout-fail"))
309
+ ) as unknown as typeof mockAuthClient.signOut;
310
+ const originalError = console.error;
311
+ const errorCalls: unknown[][] = [];
312
+ console.error = (...args: unknown[]): void => {
313
+ errorCalls.push(args);
314
+ };
315
+ // biome-ignore lint/suspicious/noExplicitAny: Mock client type
316
+ const betterAuthSlice = generateBetterAuthSlice({authClient: mockAuthClient as any});
317
+ const store = configureStore({
318
+ middleware: (getDefaultMiddleware) =>
319
+ getDefaultMiddleware().concat(...betterAuthSlice.middleware),
320
+ reducer: {betterAuth: betterAuthSlice.reducer},
321
+ });
322
+
323
+ try {
324
+ store.dispatch(betterAuthSlice.actions.logout());
325
+ await Promise.resolve();
326
+ await new Promise((resolve) => setTimeout(resolve, 0));
327
+ const logged = errorCalls.find((args) =>
328
+ args.some((v) => typeof v === "string" && v.includes("Error signing out"))
329
+ );
330
+ expect(logged).toBeDefined();
331
+ } finally {
332
+ console.error = originalError;
333
+ }
334
+ });
335
+
336
+ it("logs and swallows signOut errors triggered by the global auth/logout action", async () => {
337
+ mockAuthClient.signOut = mock(() =>
338
+ Promise.reject(new Error("global-signout-fail"))
339
+ ) as unknown as typeof mockAuthClient.signOut;
340
+ const originalError = console.error;
341
+ const errorCalls: unknown[][] = [];
342
+ console.error = (...args: unknown[]): void => {
343
+ errorCalls.push(args);
344
+ };
345
+ // biome-ignore lint/suspicious/noExplicitAny: Mock client type
346
+ const betterAuthSlice = generateBetterAuthSlice({authClient: mockAuthClient as any});
347
+ const store = configureStore({
348
+ middleware: (getDefaultMiddleware) =>
349
+ getDefaultMiddleware().concat(...betterAuthSlice.middleware),
350
+ reducer: {betterAuth: betterAuthSlice.reducer},
351
+ });
352
+
353
+ try {
354
+ store.dispatch({type: "auth/logout"});
355
+ await Promise.resolve();
356
+ await new Promise((resolve) => setTimeout(resolve, 0));
357
+ const logged = errorCalls.find((args) =>
358
+ args.some((v) => typeof v === "string" && v.includes("Error signing out"))
359
+ );
360
+ expect(logged).toBeDefined();
361
+ } finally {
362
+ console.error = originalError;
363
+ }
364
+ });
365
+
305
366
  it("global auth/logout action signs out and clears session", async () => {
306
367
  // biome-ignore lint/suspicious/noExplicitAny: Mock client type
307
368
  const betterAuthSlice = generateBetterAuthSlice({authClient: mockAuthClient as any});
@@ -397,6 +458,12 @@ describe("Better Auth selectors", () => {
397
458
  expect(selectBetterAuthIsLoading(emptyState)).toBe(false);
398
459
  expect(selectBetterAuthError(emptyState)).toBeNull();
399
460
  });
461
+
462
+ it("selectBetterAuthState returns the raw betterAuth slice of state", () => {
463
+ expect(selectBetterAuthState(createMockState({userId: "user-zzz"}))?.userId).toBe("user-zzz");
464
+ // biome-ignore lint/suspicious/noExplicitAny: Test empty state
465
+ expect(selectBetterAuthState({} as any)).toBeUndefined();
466
+ });
400
467
  });
401
468
 
402
469
  describe("BetterAuthUser interface", () => {
@@ -0,0 +1,120 @@
1
+ import {afterAll, beforeEach, describe, expect, it} from "bun:test";
2
+
3
+ import {coerceBuildNumber, resolveBuildNumber} from "./buildNumber";
4
+
5
+ describe("coerceBuildNumber", () => {
6
+ it("returns undefined for undefined", () => {
7
+ expect(coerceBuildNumber(undefined)).toBeUndefined();
8
+ });
9
+
10
+ it("returns undefined for null", () => {
11
+ expect(coerceBuildNumber(null)).toBeUndefined();
12
+ });
13
+
14
+ it("returns the number for a valid positive integer", () => {
15
+ expect(coerceBuildNumber(42)).toBe(42);
16
+ });
17
+
18
+ it("returns the number for zero", () => {
19
+ expect(coerceBuildNumber(0)).toBe(0);
20
+ });
21
+
22
+ it("returns undefined for NaN", () => {
23
+ expect(coerceBuildNumber(Number.NaN)).toBeUndefined();
24
+ });
25
+
26
+ it("returns undefined for Infinity", () => {
27
+ expect(coerceBuildNumber(Number.POSITIVE_INFINITY)).toBeUndefined();
28
+ });
29
+
30
+ it("parses a valid numeric string", () => {
31
+ expect(coerceBuildNumber("123")).toBe(123);
32
+ });
33
+
34
+ it("returns undefined for a non-numeric string", () => {
35
+ expect(coerceBuildNumber("abc")).toBeUndefined();
36
+ });
37
+
38
+ it("returns the number for a negative integer", () => {
39
+ expect(coerceBuildNumber(-5)).toBe(-5);
40
+ });
41
+
42
+ it("parses a string with leading zeros", () => {
43
+ expect(coerceBuildNumber("007")).toBe(7);
44
+ });
45
+ });
46
+
47
+ describe("resolveBuildNumber", () => {
48
+ const ORIGINAL_ENV = process.env;
49
+
50
+ beforeEach(() => {
51
+ process.env = {...ORIGINAL_ENV};
52
+ delete process.env.EXPO_PUBLIC_BUILD_NUMBER;
53
+ });
54
+
55
+ afterAll(() => {
56
+ process.env = ORIGINAL_ENV;
57
+ });
58
+
59
+ it("returns override when provided", () => {
60
+ expect(resolveBuildNumber({override: 99})).toBe(99);
61
+ });
62
+
63
+ it("returns configValue when override is not provided", () => {
64
+ expect(resolveBuildNumber({configValue: 50})).toBe(50);
65
+ });
66
+
67
+ it("returns env var when override and configValue are not provided", () => {
68
+ process.env.EXPO_PUBLIC_BUILD_NUMBER = "200";
69
+ expect(resolveBuildNumber()).toBe(200);
70
+ });
71
+
72
+ it("uses custom envVar name", () => {
73
+ process.env.CUSTOM_BUILD = "77";
74
+ expect(resolveBuildNumber({envVar: "CUSTOM_BUILD"})).toBe(77);
75
+ });
76
+
77
+ it("prefers override over configValue", () => {
78
+ expect(resolveBuildNumber({configValue: 10, override: 20})).toBe(20);
79
+ });
80
+
81
+ it("prefers configValue over env var", () => {
82
+ process.env.EXPO_PUBLIC_BUILD_NUMBER = "300";
83
+ expect(resolveBuildNumber({configValue: 15})).toBe(15);
84
+ });
85
+
86
+ it("falls back to git rev-list count when nothing else is set", () => {
87
+ const result = resolveBuildNumber();
88
+ // In a git repo, this should return a positive integer
89
+ expect(typeof result).toBe("number");
90
+ expect(result).toBeGreaterThan(0);
91
+ });
92
+
93
+ it("skips invalid env var values and falls through to git", () => {
94
+ process.env.EXPO_PUBLIC_BUILD_NUMBER = "not-a-number";
95
+ // Should fall through past the env var to git
96
+ const result = resolveBuildNumber();
97
+ expect(typeof result).toBe("number");
98
+ expect(result).toBeGreaterThan(0);
99
+ });
100
+
101
+ it("skips undefined override and uses configValue", () => {
102
+ expect(resolveBuildNumber({configValue: 42, override: undefined})).toBe(42);
103
+ });
104
+
105
+ it("skips invalid configValue and uses env var", () => {
106
+ process.env.EXPO_PUBLIC_BUILD_NUMBER = "88";
107
+ expect(resolveBuildNumber({configValue: "bad"})).toBe(88);
108
+ });
109
+
110
+ it("returns a number with default options", () => {
111
+ const result = resolveBuildNumber();
112
+ expect(result).toBeDefined();
113
+ expect(typeof result).toBe("number");
114
+ });
115
+
116
+ it("prefers override over env var", () => {
117
+ process.env.EXPO_PUBLIC_BUILD_NUMBER = "999";
118
+ expect(resolveBuildNumber({override: 1})).toBe(1);
119
+ });
120
+ });