Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 37 additions & 3 deletions src/lib/store/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@ import { configureStore } from '@reduxjs/toolkit';
import authReducer from './slices/auth';
import walletReducer from './slices/wallet';
import uiReducer from './slices/ui';
import type { AuthState } from './slices/auth';

// All private-key fields stored in the auth slice.
const AUTH_KEY_FIELDS = ['ownerKey', 'activeKey', 'postingKey', 'memoKey', 'privateKey'] as const;
type AuthKeyField = (typeof AUTH_KEY_FIELDS)[number];

export const makeStore = () =>
configureStore({
Expand All @@ -13,11 +18,40 @@ export const makeStore = () =>
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: {
// Ignore non-serializable values (like private keys)
ignoredActions: ['auth/setPrivateKey'],
ignoredPaths: ['auth.privateKey'],
// auth/setCredentials carries private key strings; list all key paths
// so the middleware never logs or warns about them.
ignoredActions: ['auth/setCredentials'],
ignoredPaths: AUTH_KEY_FIELDS.map((k) => `auth.${k}`),
},
}),
devTools:
process.env.NODE_ENV === 'production'
? // Completely disabled in production — no DevTools extension should
// ever be able to inspect the store on a real user's session.
false
: {
// In development DevTools remain fully functional for every slice
// except that live key values are replaced before they reach the
// extension, so a developer's own keys are never shown in plain text.
stateSanitizer: (state) => {
const s = state as unknown as { auth: AuthState; [k: string]: unknown };
const sanitizedAuth: AuthState = { ...s.auth };
for (const field of AUTH_KEY_FIELDS) {
if (sanitizedAuth[field] != null)
(sanitizedAuth as Record<AuthKeyField, string | null>)[field] = '[REDACTED]';
}
return { ...s, auth: sanitizedAuth } as typeof state;
},
actionSanitizer: (action) => {
if (action.type !== 'auth/setCredentials') return action;
const a = action as unknown as { type: string; payload: Partial<Record<AuthKeyField, string | null>> };
const payload = { ...a.payload };
for (const field of AUTH_KEY_FIELDS) {
if (payload[field] != null) payload[field] = '[REDACTED]';
}
return { ...action, payload } as typeof action;
},
},
});

export type AppStore = ReturnType<typeof makeStore>;
Expand Down
30 changes: 26 additions & 4 deletions tests/unit/ui-slice.test.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
/**
* UI Redux slice unit tests
*
* Only covers behavior that isn't a one-line `state.x = payload` setter:
* - initial state shape (catches accidental defaults change)
* - toggleSidebar (only reducer with computed next state)
* Covers initial state shape and all reducers to maintain branch coverage threshold.
* toggleSidebar is the only reducer with computed (non-trivial) next state.
*/

import { describe, it, expect } from 'vitest';
import uiReducer, { toggleSidebar } from '@/lib/store/slices/ui';
import uiReducer, {
toggleSidebar,
setSidebarOpen,
setTheme,
setLocale,
} from '@/lib/store/slices/ui';

describe('UI Slice', () => {
it('seeds the default UI state', () => {
Expand All @@ -24,4 +28,22 @@ describe('UI Slice', () => {
expect(closed.sidebarOpen).toBe(false);
expect(uiReducer(closed, toggleSidebar()).sidebarOpen).toBe(true);
});

it('setSidebarOpen sets sidebarOpen to the payload value', () => {
const state = uiReducer(undefined, { type: 'init' });
expect(uiReducer(state, setSidebarOpen(false)).sidebarOpen).toBe(false);
expect(uiReducer(state, setSidebarOpen(true)).sidebarOpen).toBe(true);
});

it('setTheme updates the theme', () => {
const state = uiReducer(undefined, { type: 'init' });
expect(uiReducer(state, setTheme('dark')).theme).toBe('dark');
expect(uiReducer(state, setTheme('light')).theme).toBe('light');
});

it('setLocale updates the locale', () => {
const state = uiReducer(undefined, { type: 'init' });
expect(uiReducer(state, setLocale('zh')).locale).toBe('zh');
expect(uiReducer(state, setLocale('en')).locale).toBe('en');
});
});
11 changes: 11 additions & 0 deletions tests/unit/wallet-slice.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@
*
* Only covers reducers with side effects beyond a single field assignment:
* - setBalance: writes balance AND clears loading + error
* - setLoading: writes loading without touching balance/error
* - setError: writes error AND clears loading
* - clearWallet: full reset
*/

import { describe, it, expect } from 'vitest';
import walletReducer, {
setBalance,
setLoading,
setError,
clearWallet,
} from '@/lib/store/slices/wallet';
Expand All @@ -29,6 +31,15 @@ describe('Wallet Slice', () => {
expect(next).toEqual({ balance: sampleBalance, loading: false, error: null });
});

it('setLoading toggles loading without touching balance or error', () => {
const seeded = { balance: sampleBalance, loading: false, error: 'x' };
const loading = walletReducer(seeded, setLoading(true));
expect(loading.loading).toBe(true);
expect(loading.balance).toEqual(sampleBalance);
expect(loading.error).toBe('x');
expect(walletReducer(loading, setLoading(false)).loading).toBe(false);
});

it('setError stores the message and clears loading', () => {
const next = walletReducer(
{ balance: null, loading: true, error: null },
Expand Down
Loading