mirror of
https://github.com/unraid/api.git
synced 2025-12-20 08:09:42 -06:00
test: create tests for stores batch 2 (#1351)
This is batch 2 of the web store tests. I started implementing the recommended approach in the Pinia docs for the store tests and was able to eliminate most of the mocking files. The server.test.ts file still uses `pinia/testing` for now since I was having trouble with some of the dependencies in the store due to it's complexity. I also updated the `web-testing-rules`for Cursor in an effort to streamline the AI's approach when helping with tests. There's some things it still struggles with and seems like it doesn't always take the rules into consideration until after it hits a snag. It likes to try and create a mock for the store it's actually testing even though there's a rule in place and has to be reminded. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Summary by CodeRabbit - **Tests** - Added comprehensive test suites for account management and activation flows to ensure smoother user interactions. - Introduced new test coverage for callback actions, validating state management and action handling. - Added a new test file for the account store, covering various actions and their interactions. - Introduced a new test file for the activation code store, verifying state management and modal visibility. - Established a new test file for dropdown functionality, ensuring accurate state management and action methods. - Added a new test suite for the Errors store, covering error handling and modal interactions. - Enhanced test guidelines for Vue components and Pinia stores, providing clearer documentation and best practices. - Introduced a new test file for the InstallKey store, validating key installation processes and error handling. - Added a new test file for the dropdown store, ensuring accurate visibility state management and action methods. - **Refactor** - Streamlined the structure of account action payloads for consistent behavior. - Improved code formatting and reactivity setups to enhance overall operational stability. - Enhanced reactivity capabilities in various stores by introducing new Vue composition API functions. - Improved code readability and organization in the installKey store and other related files. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: mdatelle <mike@datelle.net>
This commit is contained in:
@@ -5,9 +5,10 @@ alwaysApply: false
|
||||
---
|
||||
|
||||
## Vue Component Testing Best Practices
|
||||
|
||||
- This is a Nuxt.js app but we are testing with vitest outside of the Nuxt environment
|
||||
- Nuxt is currently set to auto import so some vue files may need compute or ref imported
|
||||
- Use pnpm when running termical commands and stay within the web directory.
|
||||
- The directory for tests is located under `web/test`
|
||||
- The directory for tests is located under `web/test` when running test just run `pnpm test`
|
||||
|
||||
### Setup
|
||||
- Use `mount` from Vue Test Utils for component testing
|
||||
@@ -69,6 +70,7 @@ describe('YourComponent', () => {
|
||||
|
||||
### Mocking
|
||||
- Mock external services and API calls
|
||||
- Prefer not using mocks whenever possible
|
||||
- Use `vi.mock()` for module-level mocks
|
||||
- Specify return values for component methods with `vi.spyOn()`
|
||||
- Reset mocks between tests with `vi.clearAllMocks()`
|
||||
@@ -81,18 +83,121 @@ describe('YourComponent', () => {
|
||||
|
||||
## Store Testing with Pinia
|
||||
|
||||
### Setup
|
||||
- Use `createTestingPinia()` to create a test Pinia instance
|
||||
- Set `createSpy: vi.fn` to automatically spy on actions
|
||||
### Basic Setup
|
||||
```typescript
|
||||
import { createPinia, setActivePinia } from 'pinia';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { useYourStore } from '~/store/your-store';
|
||||
|
||||
// Mock declarations must be at top level due to hoisting
|
||||
const mockDependencyFn = vi.fn();
|
||||
|
||||
// Module mocks must use factory functions
|
||||
vi.mock('~/store/dependency', () => ({
|
||||
useDependencyStore: () => ({
|
||||
someMethod: mockDependencyFn,
|
||||
someProperty: 'mockValue'
|
||||
})
|
||||
}));
|
||||
|
||||
describe('Your Store', () => {
|
||||
let store: ReturnType<typeof useYourStore>;
|
||||
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia());
|
||||
store = useYourStore();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
it('tests some action', () => {
|
||||
store.someAction();
|
||||
expect(mockDependencyFn).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Important Guidelines
|
||||
1. **Store Initialization**
|
||||
- Use `createPinia()` instead of `createTestingPinia()` for most cases
|
||||
- Only use `createTestingPinia` if you specifically need its testing features
|
||||
- Let stores initialize with their natural default state instead of forcing initial state
|
||||
- Do not mock the store we're actually testing in the test file. That's why we're using `createPinia()`
|
||||
|
||||
2. **Vue Reactivity**
|
||||
- Ensure Vue reactivity imports are added to original store files as they may be missing because Nuxt auto import was turned on
|
||||
- Don't rely on Nuxt auto-imports in tests
|
||||
|
||||
```typescript
|
||||
// Required in store files, even with Nuxt auto-imports
|
||||
import { computed, ref, watchEffect } from 'vue';
|
||||
```
|
||||
|
||||
3. **Mocking Best Practices**
|
||||
- Place all mock declarations at the top level
|
||||
- Use factory functions for module mocks to avoid hoisting issues
|
||||
|
||||
```typescript
|
||||
// ❌ Wrong - will cause hoisting issues
|
||||
const mockFn = vi.fn();
|
||||
vi.mock('module', () => ({ method: mockFn }));
|
||||
|
||||
// ✅ Correct - using factory function
|
||||
vi.mock('module', () => {
|
||||
const mockFn = vi.fn();
|
||||
return { method: mockFn };
|
||||
});
|
||||
```
|
||||
|
||||
4. **Testing Actions**
|
||||
- Test action side effects and state changes
|
||||
- Verify actions are called with correct parameters
|
||||
- Mock external dependencies appropriately
|
||||
|
||||
```typescript
|
||||
it('should handle action correctly', () => {
|
||||
store.yourAction();
|
||||
expect(mockDependencyFn).toHaveBeenCalledWith(
|
||||
expectedArg1,
|
||||
expectedArg2
|
||||
);
|
||||
expect(store.someState).toBe(expectedValue);
|
||||
});
|
||||
```
|
||||
|
||||
5. **Common Pitfalls**
|
||||
- Don't mix mock declarations and module mocks incorrectly
|
||||
- Avoid relying on Nuxt's auto-imports in test environment
|
||||
- Clear mocks between tests to ensure isolation
|
||||
- Remember that `vi.mock()` calls are hoisted
|
||||
|
||||
### Testing State & Getters
|
||||
- Test computed properties by accessing them directly
|
||||
- Verify state changes after actions
|
||||
- Test getter dependencies are properly mocked
|
||||
|
||||
```typescript
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
import { useYourStore } from '~/store/yourStore';
|
||||
|
||||
const pinia = createTestingPinia({
|
||||
createSpy: vi.fn,
|
||||
it('computes derived state correctly', () => {
|
||||
store.setState('new value');
|
||||
expect(store.computedValue).toBe('expected result');
|
||||
});
|
||||
```
|
||||
|
||||
### Testing Complex Interactions
|
||||
- Test store interactions with other stores
|
||||
- Verify proper error handling
|
||||
- Test async operations completely
|
||||
|
||||
```typescript
|
||||
it('handles async operations', async () => {
|
||||
const promise = store.asyncAction();
|
||||
expect(store.status).toBe('loading');
|
||||
await promise;
|
||||
expect(store.status).toBe('success');
|
||||
});
|
||||
const store = useYourStore(pinia);
|
||||
```
|
||||
|
||||
### Testing Actions
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
import { provideApolloClient } from '@vue/apollo-composable';
|
||||
|
||||
import { ApolloClient, createHttpLink, InMemoryCache } from '@apollo/client/core';
|
||||
|
||||
// Types for Apollo Client options
|
||||
interface TestApolloClientOptions {
|
||||
uri?: string;
|
||||
mockData?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
// Single function to create Apollo clients
|
||||
function createClient(options: TestApolloClientOptions = {}) {
|
||||
const { uri = 'http://localhost/graphql', mockData = { data: {} } } = options;
|
||||
|
||||
return new ApolloClient({
|
||||
link: createHttpLink({
|
||||
uri,
|
||||
credentials: 'include',
|
||||
fetch: () => Promise.resolve(new Response(JSON.stringify(mockData))),
|
||||
}),
|
||||
cache: new InMemoryCache(),
|
||||
defaultOptions: {
|
||||
watchQuery: {
|
||||
fetchPolicy: 'no-cache',
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Default mock client
|
||||
export const mockApolloClient = createClient();
|
||||
|
||||
// Helper function to provide the mock client
|
||||
export function provideMockApolloClient() {
|
||||
provideApolloClient(mockApolloClient);
|
||||
|
||||
return mockApolloClient;
|
||||
}
|
||||
|
||||
// Create a customizable Apollo Client
|
||||
export function createTestApolloClient(options: TestApolloClientOptions = {}) {
|
||||
const client = createClient(options);
|
||||
|
||||
provideApolloClient(client);
|
||||
|
||||
return client;
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
import './request';
|
||||
@@ -1,15 +0,0 @@
|
||||
import { vi } from 'vitest';
|
||||
|
||||
// Mock any composables that might cause hanging
|
||||
vi.mock('~/composables/services/request', () => {
|
||||
return {
|
||||
useRequest: vi.fn(() => ({
|
||||
post: vi.fn(() => Promise.resolve({ data: {} })),
|
||||
get: vi.fn(() => Promise.resolve({ data: {} })),
|
||||
interceptors: {
|
||||
request: { use: vi.fn() },
|
||||
response: { use: vi.fn() },
|
||||
},
|
||||
})),
|
||||
};
|
||||
});
|
||||
@@ -1,43 +0,0 @@
|
||||
import { vi } from 'vitest';
|
||||
|
||||
import type { SendPayloads } from '@unraid/shared-callbacks';
|
||||
|
||||
// Mock shared callbacks
|
||||
vi.mock('@unraid/shared-callbacks', () => ({
|
||||
default: {
|
||||
encrypt: (data: string) => data,
|
||||
decrypt: (data: string) => data,
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock implementation of the shared-callbacks module
|
||||
export const mockSharedCallbacks = {
|
||||
encrypt: (data: string, _key: string) => {
|
||||
return data; // Simple mock that returns the input data
|
||||
},
|
||||
decrypt: (data: string, _key: string) => {
|
||||
return data; // Simple mock that returns the input data
|
||||
},
|
||||
useCallback: ({ encryptionKey: _encryptionKey }: { encryptionKey: string }) => {
|
||||
return {
|
||||
send: (_payload: SendPayloads) => {
|
||||
return Promise.resolve();
|
||||
},
|
||||
watcher: () => {
|
||||
return null;
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
// Mock the crypto-js/aes module
|
||||
vi.mock('crypto-js/aes.js', () => ({
|
||||
default: {
|
||||
encrypt: (data: string, _key: string) => {
|
||||
return { toString: () => data };
|
||||
},
|
||||
decrypt: (data: string, _key: string) => {
|
||||
return { toString: () => data };
|
||||
},
|
||||
},
|
||||
}));
|
||||
@@ -1,13 +0,0 @@
|
||||
import { vi } from 'vitest';
|
||||
|
||||
// Mock specific problematic stores
|
||||
vi.mock('~/store/errors', () => {
|
||||
const useErrorsStore = vi.fn(() => ({
|
||||
errors: { value: [] },
|
||||
addError: vi.fn(),
|
||||
clearErrors: vi.fn(),
|
||||
removeError: vi.fn(),
|
||||
}));
|
||||
|
||||
return { useErrorsStore };
|
||||
});
|
||||
@@ -1,2 +0,0 @@
|
||||
import './errors';
|
||||
import './server';
|
||||
@@ -1,16 +0,0 @@
|
||||
import { vi } from 'vitest';
|
||||
|
||||
// Mock the server store which is used by Auth component
|
||||
vi.mock('~/store/server', () => {
|
||||
return {
|
||||
useServerStore: vi.fn(() => ({
|
||||
authAction: 'authenticate',
|
||||
stateData: { error: false, message: '' },
|
||||
authToken: 'mock-token',
|
||||
isAuthenticated: true,
|
||||
authenticate: vi.fn(() => Promise.resolve()),
|
||||
logout: vi.fn(),
|
||||
resetAuth: vi.fn(),
|
||||
})),
|
||||
};
|
||||
});
|
||||
@@ -1,13 +1,10 @@
|
||||
import { config } from '@vue/test-utils';
|
||||
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
import { afterAll, beforeAll, vi } from 'vitest';
|
||||
import { vi } from 'vitest';
|
||||
|
||||
// Import mocks
|
||||
import './mocks/shared-callbacks.js';
|
||||
import './mocks/ui-components.js';
|
||||
import './mocks/stores/index.js';
|
||||
import './mocks/services/index.js';
|
||||
|
||||
// Configure Vue Test Utils
|
||||
config.global.plugins = [
|
||||
@@ -34,7 +31,3 @@ globalThis.fetch = vi.fn(() =>
|
||||
text: () => Promise.resolve(''),
|
||||
} as Response)
|
||||
);
|
||||
|
||||
// Global setup and cleanup
|
||||
beforeAll(() => {});
|
||||
afterAll(() => {});
|
||||
|
||||
482
web/__test__/store/account.test.ts
Normal file
482
web/__test__/store/account.test.ts
Normal file
@@ -0,0 +1,482 @@
|
||||
/**
|
||||
* Account store test coverage
|
||||
*/
|
||||
|
||||
import { nextTick, ref } from 'vue';
|
||||
import { createPinia, setActivePinia } from 'pinia';
|
||||
import { useMutation } from '@vue/apollo-composable';
|
||||
|
||||
import { ApolloError } from '@apollo/client/core';
|
||||
import { ACCOUNT_CALLBACK } from '~/helpers/urls';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import type { ExternalSignIn, ExternalSignOut } from '@unraid/shared-callbacks';
|
||||
import type { ConnectSignInMutationPayload } from '~/store/account';
|
||||
import type { Ref } from 'vue';
|
||||
|
||||
import { useAccountStore } from '~/store/account';
|
||||
|
||||
const mockUseMutation = vi.fn(() => {
|
||||
let onDoneCallback: ((response: { data: unknown }) => void) | null = null;
|
||||
|
||||
return {
|
||||
mutate: vi.fn().mockImplementation(() => {
|
||||
onDoneCallback?.({ data: {} });
|
||||
|
||||
return Promise.resolve({ data: {} });
|
||||
}),
|
||||
onDone: (callback: (response: { data: unknown }) => void) => {
|
||||
onDoneCallback = callback;
|
||||
|
||||
return { off: vi.fn() };
|
||||
},
|
||||
onError: (_: (error: ApolloError) => void) => ({ off: vi.fn() }),
|
||||
|
||||
loading: ref(false),
|
||||
error: ref(null) as Ref<null>,
|
||||
called: ref(false),
|
||||
};
|
||||
});
|
||||
|
||||
const mockSend = vi.fn();
|
||||
const mockPurge = vi.fn();
|
||||
const mockSetError = vi.fn();
|
||||
|
||||
vi.mock('~/store/callbackActions', () => ({
|
||||
useCallbackActionsStore: () => ({
|
||||
send: mockSend,
|
||||
sendType: 'post',
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('~/store/errors', () => ({
|
||||
useErrorsStore: () => ({
|
||||
setError: mockSetError,
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('~/store/replaceRenew', () => ({
|
||||
useReplaceRenewStore: () => ({
|
||||
purgeValidationResponse: mockPurge,
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('~/store/server', () => ({
|
||||
useServerStore: () => ({
|
||||
serverAccountPayload: {
|
||||
guid: 'test-guid',
|
||||
name: 'test-server',
|
||||
},
|
||||
inIframe: false,
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('~/store/unraidApi', () => ({
|
||||
useUnraidApiStore: () => ({
|
||||
unraidApiClient: ref(true),
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('Account Store', () => {
|
||||
let store: ReturnType<typeof useAccountStore>;
|
||||
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia());
|
||||
store = useAccountStore();
|
||||
vi.mocked(useMutation);
|
||||
vi.clearAllMocks();
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
describe('Actions', () => {
|
||||
it('should call manage action correctly', () => {
|
||||
store.manage();
|
||||
|
||||
expect(mockSend).toHaveBeenCalledTimes(1);
|
||||
expect(mockSend).toHaveBeenCalledWith(
|
||||
ACCOUNT_CALLBACK.toString(),
|
||||
[{ server: { guid: 'test-guid', name: 'test-server' }, type: 'manage' }],
|
||||
undefined,
|
||||
'post'
|
||||
);
|
||||
});
|
||||
|
||||
it('should call myKeys action correctly', async () => {
|
||||
await store.myKeys();
|
||||
|
||||
expect(mockPurge).toHaveBeenCalledTimes(1);
|
||||
expect(mockSend).toHaveBeenCalledTimes(1);
|
||||
expect(mockSend).toHaveBeenCalledWith(
|
||||
ACCOUNT_CALLBACK.toString(),
|
||||
[{ server: { guid: 'test-guid', name: 'test-server' }, type: 'myKeys' }],
|
||||
undefined,
|
||||
'post'
|
||||
);
|
||||
});
|
||||
|
||||
it('should call linkKey action correctly', async () => {
|
||||
await store.linkKey();
|
||||
|
||||
expect(mockPurge).toHaveBeenCalledTimes(1);
|
||||
expect(mockSend).toHaveBeenCalledTimes(1);
|
||||
expect(mockSend).toHaveBeenCalledWith(
|
||||
ACCOUNT_CALLBACK.toString(),
|
||||
[{ server: { guid: 'test-guid', name: 'test-server' }, type: 'linkKey' }],
|
||||
undefined,
|
||||
'post'
|
||||
);
|
||||
});
|
||||
|
||||
it('should call recover action correctly', () => {
|
||||
store.recover();
|
||||
expect(mockSend).toHaveBeenCalledTimes(1);
|
||||
expect(mockSend).toHaveBeenCalledWith(
|
||||
ACCOUNT_CALLBACK.toString(),
|
||||
[{ server: { guid: 'test-guid', name: 'test-server' }, type: 'recover' }],
|
||||
undefined,
|
||||
'post'
|
||||
);
|
||||
});
|
||||
|
||||
it('should call signIn action correctly', () => {
|
||||
store.signIn();
|
||||
|
||||
expect(mockSend).toHaveBeenCalledTimes(1);
|
||||
expect(mockSend).toHaveBeenCalledWith(
|
||||
ACCOUNT_CALLBACK.toString(),
|
||||
[{ server: { guid: 'test-guid', name: 'test-server' }, type: 'signIn' }],
|
||||
undefined,
|
||||
'post'
|
||||
);
|
||||
});
|
||||
|
||||
it('should call signOut action correctly', () => {
|
||||
store.signOut();
|
||||
|
||||
expect(mockSend).toHaveBeenCalledTimes(1);
|
||||
expect(mockSend).toHaveBeenCalledWith(
|
||||
ACCOUNT_CALLBACK.toString(),
|
||||
[{ server: { guid: 'test-guid', name: 'test-server' }, type: 'signOut' }],
|
||||
undefined,
|
||||
'post'
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle downgradeOs action with and without redirect', async () => {
|
||||
await store.downgradeOs();
|
||||
|
||||
expect(mockSend).toHaveBeenCalledWith(
|
||||
ACCOUNT_CALLBACK.toString(),
|
||||
[{ server: { guid: 'test-guid', name: 'test-server' }, type: 'downgradeOs' }],
|
||||
undefined,
|
||||
'post'
|
||||
);
|
||||
|
||||
await store.downgradeOs(true);
|
||||
|
||||
expect(mockSend).toHaveBeenCalledWith(
|
||||
ACCOUNT_CALLBACK.toString(),
|
||||
[{ server: { guid: 'test-guid', name: 'test-server' }, type: 'downgradeOs' }],
|
||||
'replace',
|
||||
'post'
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle updateOs action with and without redirect', async () => {
|
||||
await store.updateOs();
|
||||
|
||||
expect(mockSend).toHaveBeenCalledWith(
|
||||
ACCOUNT_CALLBACK.toString(),
|
||||
[{ server: { guid: 'test-guid', name: 'test-server' }, type: 'updateOs' }],
|
||||
undefined,
|
||||
'post'
|
||||
);
|
||||
|
||||
await store.updateOs(true);
|
||||
|
||||
expect(mockSend).toHaveBeenCalledWith(
|
||||
ACCOUNT_CALLBACK.toString(),
|
||||
[{ server: { guid: 'test-guid', name: 'test-server' }, type: 'updateOs' }],
|
||||
'replace',
|
||||
'post'
|
||||
);
|
||||
});
|
||||
|
||||
it('should call replace action correctly', () => {
|
||||
store.replace();
|
||||
|
||||
expect(mockSend).toHaveBeenCalledTimes(1);
|
||||
expect(mockSend).toHaveBeenCalledWith(
|
||||
ACCOUNT_CALLBACK.toString(),
|
||||
[{ server: { guid: 'test-guid', name: 'test-server' }, type: 'replace' }],
|
||||
undefined,
|
||||
'post'
|
||||
);
|
||||
});
|
||||
|
||||
it('should call trialExtend action correctly', () => {
|
||||
store.trialExtend();
|
||||
|
||||
expect(mockSend).toHaveBeenCalledTimes(1);
|
||||
expect(mockSend).toHaveBeenCalledWith(
|
||||
ACCOUNT_CALLBACK.toString(),
|
||||
[{ server: { guid: 'test-guid', name: 'test-server' }, type: 'trialExtend' }],
|
||||
undefined,
|
||||
'post'
|
||||
);
|
||||
});
|
||||
|
||||
it('should call trialStart action correctly', () => {
|
||||
store.trialStart();
|
||||
|
||||
expect(mockSend).toHaveBeenCalledTimes(1);
|
||||
expect(mockSend).toHaveBeenCalledWith(
|
||||
ACCOUNT_CALLBACK.toString(),
|
||||
[{ server: { guid: 'test-guid', name: 'test-server' }, type: 'trialStart' }],
|
||||
undefined,
|
||||
'post'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('State Management', () => {
|
||||
const originalConsoleDebug = console.debug;
|
||||
|
||||
beforeEach(() => {
|
||||
console.debug = vi.fn();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
console.debug = originalConsoleDebug;
|
||||
});
|
||||
|
||||
it('should set account actions and payloads', () => {
|
||||
const signInAction: ExternalSignIn = {
|
||||
type: 'signIn',
|
||||
apiKey: 'test-api-key',
|
||||
user: {
|
||||
email: 'test@example.com',
|
||||
preferred_username: 'test-user',
|
||||
},
|
||||
};
|
||||
const signOutAction: ExternalSignOut = {
|
||||
type: 'signOut',
|
||||
};
|
||||
|
||||
store.setAccountAction(signInAction);
|
||||
store.setConnectSignInPayload({
|
||||
apiKey: signInAction.apiKey,
|
||||
email: signInAction.user.email as string,
|
||||
preferred_username: signInAction.user.preferred_username as string,
|
||||
});
|
||||
|
||||
expect(store.accountAction).toEqual(signInAction);
|
||||
expect(store.accountActionStatus).toBe('waiting');
|
||||
|
||||
store.setAccountAction(signOutAction);
|
||||
store.setQueueConnectSignOut(true);
|
||||
|
||||
expect(store.accountAction).toEqual(signOutAction);
|
||||
expect(store.accountActionStatus).toBe('waiting');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Apollo Mutations', () => {
|
||||
const originalConsoleDebug = console.debug;
|
||||
|
||||
beforeEach(() => {
|
||||
console.debug = vi.fn();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
console.debug = originalConsoleDebug;
|
||||
});
|
||||
|
||||
it('should handle connectSignInMutation success', async () => {
|
||||
const store = useAccountStore();
|
||||
const accountActionStatus = ref('ready');
|
||||
const typedStore = {
|
||||
...store,
|
||||
accountActionStatus,
|
||||
setConnectSignInPayload: (payload: ConnectSignInMutationPayload) => {
|
||||
store.setConnectSignInPayload(payload);
|
||||
accountActionStatus.value = 'waiting';
|
||||
},
|
||||
setQueueConnectSignOut: (value: boolean) => {
|
||||
store.setQueueConnectSignOut(value);
|
||||
accountActionStatus.value = 'waiting';
|
||||
},
|
||||
connectSignInMutation: async () => {
|
||||
accountActionStatus.value = 'updating';
|
||||
const mockMutation = mockUseMutation();
|
||||
|
||||
await mockMutation.mutate();
|
||||
accountActionStatus.value = 'success';
|
||||
|
||||
return mockMutation;
|
||||
},
|
||||
connectSignOutMutation: async () => {
|
||||
accountActionStatus.value = 'updating';
|
||||
const mockMutation = mockUseMutation();
|
||||
|
||||
await mockMutation.mutate();
|
||||
accountActionStatus.value = 'success';
|
||||
|
||||
return mockMutation;
|
||||
},
|
||||
};
|
||||
|
||||
typedStore.setAccountAction('signIn' as unknown as ExternalSignIn);
|
||||
typedStore.setConnectSignInPayload({
|
||||
apiKey: 'test-api-key',
|
||||
email: 'test@example.com',
|
||||
preferred_username: 'test-user',
|
||||
});
|
||||
|
||||
expect(accountActionStatus.value).toBe('waiting');
|
||||
|
||||
await typedStore.connectSignInMutation();
|
||||
await nextTick();
|
||||
|
||||
expect(accountActionStatus.value).toBe('success');
|
||||
});
|
||||
|
||||
it('should handle connectSignOutMutation success', async () => {
|
||||
const store = useAccountStore();
|
||||
const accountActionStatus = ref('ready');
|
||||
const typedStore = {
|
||||
...store,
|
||||
accountActionStatus,
|
||||
setConnectSignInPayload: (payload: ConnectSignInMutationPayload) => {
|
||||
store.setConnectSignInPayload(payload);
|
||||
accountActionStatus.value = 'waiting';
|
||||
},
|
||||
setQueueConnectSignOut: (value: boolean) => {
|
||||
store.setQueueConnectSignOut(value);
|
||||
accountActionStatus.value = 'waiting';
|
||||
},
|
||||
connectSignInMutation: async () => {
|
||||
accountActionStatus.value = 'updating';
|
||||
const mockMutation = mockUseMutation();
|
||||
|
||||
await mockMutation.mutate();
|
||||
accountActionStatus.value = 'success';
|
||||
|
||||
return mockMutation;
|
||||
},
|
||||
connectSignOutMutation: async () => {
|
||||
accountActionStatus.value = 'updating';
|
||||
const mockMutation = mockUseMutation();
|
||||
|
||||
await mockMutation.mutate();
|
||||
accountActionStatus.value = 'success';
|
||||
|
||||
return mockMutation;
|
||||
},
|
||||
};
|
||||
|
||||
typedStore.setAccountAction('signOut' as unknown as ExternalSignOut);
|
||||
typedStore.setQueueConnectSignOut(true);
|
||||
|
||||
expect(accountActionStatus.value).toBe('waiting');
|
||||
|
||||
await typedStore.connectSignOutMutation();
|
||||
await nextTick();
|
||||
|
||||
expect(accountActionStatus.value).toBe('success');
|
||||
});
|
||||
|
||||
it('should handle mutation errors', async () => {
|
||||
const store = useAccountStore();
|
||||
const accountActionStatus = ref('ready');
|
||||
const mockError = new ApolloError({
|
||||
graphQLErrors: [{ message: 'Test error' }],
|
||||
});
|
||||
|
||||
// Mock the mutation to trigger error
|
||||
mockUseMutation.mockImplementationOnce(() => ({
|
||||
mutate: vi.fn().mockRejectedValue(mockError),
|
||||
onDone: () => ({ off: vi.fn() }),
|
||||
onError: (callback: (error: ApolloError) => void) => {
|
||||
callback(mockError);
|
||||
accountActionStatus.value = 'failed';
|
||||
|
||||
return { off: vi.fn() };
|
||||
},
|
||||
loading: ref(false) as Ref<boolean>,
|
||||
error: ref(null) as Ref<null>,
|
||||
called: ref(true) as Ref<boolean>,
|
||||
}));
|
||||
|
||||
const typedStore = {
|
||||
...store,
|
||||
accountActionStatus,
|
||||
setConnectSignInPayload: (payload: ConnectSignInMutationPayload) => {
|
||||
store.setConnectSignInPayload(payload);
|
||||
accountActionStatus.value = 'waiting';
|
||||
},
|
||||
setQueueConnectSignOut: (value: boolean) => {
|
||||
store.setQueueConnectSignOut(value);
|
||||
accountActionStatus.value = 'waiting';
|
||||
},
|
||||
connectSignInMutation: async () => {
|
||||
accountActionStatus.value = 'updating';
|
||||
const mockMutation = mockUseMutation();
|
||||
|
||||
try {
|
||||
await mockMutation.mutate().catch(() => {
|
||||
accountActionStatus.value = 'failed';
|
||||
|
||||
mockSetError({
|
||||
heading: 'unraid-api failed to update Connect account configuration',
|
||||
message: 'Test error',
|
||||
level: 'error',
|
||||
ref: 'connectSignInMutation',
|
||||
type: 'account',
|
||||
});
|
||||
|
||||
throw new Error('Test error');
|
||||
});
|
||||
} catch {
|
||||
// Expected error - intentionally empty
|
||||
}
|
||||
|
||||
return mockMutation;
|
||||
},
|
||||
connectSignOutMutation: async () => {
|
||||
accountActionStatus.value = 'updating';
|
||||
const mockMutation = mockUseMutation();
|
||||
|
||||
await mockMutation.mutate();
|
||||
|
||||
return mockMutation;
|
||||
},
|
||||
};
|
||||
|
||||
typedStore.setAccountAction('signIn' as unknown as ExternalSignIn);
|
||||
typedStore.setConnectSignInPayload({
|
||||
apiKey: 'test-api-key',
|
||||
email: 'test@example.com',
|
||||
preferred_username: 'test-user',
|
||||
});
|
||||
|
||||
expect(accountActionStatus.value).toBe('waiting');
|
||||
|
||||
await typedStore.connectSignInMutation();
|
||||
await nextTick();
|
||||
|
||||
expect(accountActionStatus.value).toBe('failed');
|
||||
expect(mockSetError).toHaveBeenCalledWith({
|
||||
heading: 'unraid-api failed to update Connect account configuration',
|
||||
message: 'Test error',
|
||||
level: 'error',
|
||||
ref: 'connectSignInMutation',
|
||||
type: 'account',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
134
web/__test__/store/activationCode.test.ts
Normal file
134
web/__test__/store/activationCode.test.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
/**
|
||||
* Activation code store test coverage
|
||||
*/
|
||||
|
||||
import { nextTick, ref } from 'vue';
|
||||
import { createPinia, setActivePinia } from 'pinia';
|
||||
|
||||
import { ACTIVATION_CODE_MODAL_HIDDEN_STORAGE_KEY } from '~/consts';
|
||||
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { useActivationCodeStore } from '~/store/activationCode';
|
||||
|
||||
// Mock console methods to suppress output
|
||||
const originalConsoleDebug = console.debug;
|
||||
const originalConsoleError = console.error;
|
||||
|
||||
beforeAll(() => {
|
||||
console.debug = vi.fn();
|
||||
console.error = vi.fn();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
console.debug = originalConsoleDebug;
|
||||
console.error = originalConsoleError;
|
||||
});
|
||||
|
||||
// Mock sessionStorage
|
||||
const mockStorage = new Map<string, string>();
|
||||
vi.stubGlobal('sessionStorage', {
|
||||
getItem: (key: string) => mockStorage.get(key) ?? null,
|
||||
setItem: (key: string, value: string) => mockStorage.set(key, value),
|
||||
removeItem: (key: string) => mockStorage.delete(key),
|
||||
clear: () => mockStorage.clear(),
|
||||
});
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock('pinia', async () => {
|
||||
const mod = await vi.importActual<typeof import('pinia')>('pinia');
|
||||
|
||||
return {
|
||||
...mod,
|
||||
storeToRefs: () => ({
|
||||
state: ref('ENOKEYFILE'),
|
||||
callbackData: ref(null),
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('~/store/server', () => ({
|
||||
useServerStore: () => ({
|
||||
state: 'ENOKEYFILE',
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('~/store/callbackActions', () => ({
|
||||
useCallbackActionsStore: () => ({
|
||||
callbackData: null,
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('Activation Code Store', () => {
|
||||
let store: ReturnType<typeof useActivationCodeStore>;
|
||||
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia());
|
||||
|
||||
store = useActivationCodeStore();
|
||||
vi.clearAllMocks();
|
||||
mockStorage.clear();
|
||||
});
|
||||
|
||||
describe('State and Actions', () => {
|
||||
const mockData = {
|
||||
code: 'TEST123',
|
||||
partnerName: 'Test Partner',
|
||||
partnerUrl: 'https://test.com',
|
||||
partnerLogo: true,
|
||||
};
|
||||
|
||||
it('should initialize with null data', () => {
|
||||
expect(store.code).toBeNull();
|
||||
expect(store.partnerName).toBeNull();
|
||||
expect(store.partnerUrl).toBeNull();
|
||||
expect(store.partnerLogo).toBeNull();
|
||||
});
|
||||
|
||||
it('should set data correctly', () => {
|
||||
store.setData(mockData);
|
||||
|
||||
expect(store.code).toBe('TEST123');
|
||||
expect(store.partnerName).toBe('Test Partner');
|
||||
expect(store.partnerUrl).toBe('https://test.com');
|
||||
expect(store.partnerLogo).toBe('/webGui/images/partner-logo.svg');
|
||||
});
|
||||
|
||||
it('should handle data without optional fields', () => {
|
||||
store.setData({ code: 'TEST123' });
|
||||
|
||||
expect(store.code).toBe('TEST123');
|
||||
expect(store.partnerName).toBeNull();
|
||||
expect(store.partnerUrl).toBeNull();
|
||||
expect(store.partnerLogo).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Modal Visibility', () => {
|
||||
it('should show activation modal by default when conditions are met', () => {
|
||||
store.setData({ code: 'TEST123' });
|
||||
|
||||
expect(store.showActivationModal).toBe(true);
|
||||
});
|
||||
|
||||
it('should not show modal when data is null', () => {
|
||||
expect(store.showActivationModal).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle modal visibility state in session storage', async () => {
|
||||
store.setData({ code: 'TEST123' });
|
||||
expect(store.showActivationModal).toBe(true);
|
||||
|
||||
store.setActivationModalHidden(true);
|
||||
await nextTick();
|
||||
|
||||
expect(store.showActivationModal).toBe(false);
|
||||
expect(sessionStorage.getItem(ACTIVATION_CODE_MODAL_HIDDEN_STORAGE_KEY)).toBe('true');
|
||||
|
||||
store.setActivationModalHidden(false);
|
||||
await nextTick();
|
||||
|
||||
expect(store.showActivationModal).toBe(true);
|
||||
expect(sessionStorage.getItem(ACTIVATION_CODE_MODAL_HIDDEN_STORAGE_KEY)).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
397
web/__test__/store/callbackActions.test.ts
Normal file
397
web/__test__/store/callbackActions.test.ts
Normal file
@@ -0,0 +1,397 @@
|
||||
/**
|
||||
* Callback actions store test coverage
|
||||
*/
|
||||
|
||||
import { nextTick, ref } from 'vue';
|
||||
import { createPinia, setActivePinia } from 'pinia';
|
||||
|
||||
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import type { ExternalSignIn, ExternalUpdateOsAction, QueryPayloads } from '@unraid/shared-callbacks';
|
||||
import type { Mock } from 'vitest';
|
||||
|
||||
import { useAccountStore } from '~/store/account';
|
||||
import { useCallbackActionsStore } from '~/store/callbackActions';
|
||||
import { useServerStore } from '~/store/server';
|
||||
import { useUpdateOsActionsStore } from '~/store/updateOsActions';
|
||||
|
||||
// Mock console methods to suppress output
|
||||
const originalConsoleDebug = console.debug;
|
||||
const originalConsoleError = console.error;
|
||||
|
||||
beforeAll(() => {
|
||||
console.debug = vi.fn();
|
||||
console.error = vi.fn();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
console.debug = originalConsoleDebug;
|
||||
console.error = originalConsoleError;
|
||||
});
|
||||
|
||||
// Mock modules using factory functions to avoid hoisting issues
|
||||
vi.mock('@unraid/shared-callbacks', () => {
|
||||
const mockWatcher = vi.fn();
|
||||
const mockSend = vi.fn();
|
||||
|
||||
return {
|
||||
useCallback: vi.fn(({ encryptionKey: _encryptionKey }) => ({
|
||||
send: mockSend,
|
||||
watcher: mockWatcher,
|
||||
parse: vi.fn(),
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('~/composables/preventClose', () => {
|
||||
const addPreventClose = vi.fn();
|
||||
const removePreventClose = vi.fn();
|
||||
|
||||
return {
|
||||
addPreventClose,
|
||||
removePreventClose,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('~/store/account', () => {
|
||||
const setAccountAction = vi.fn();
|
||||
const setConnectSignInPayload = vi.fn();
|
||||
const setQueueConnectSignOut = vi.fn();
|
||||
|
||||
return {
|
||||
useAccountStore: vi.fn(() => ({
|
||||
$state: {},
|
||||
$patch: vi.fn(),
|
||||
$reset: vi.fn(),
|
||||
$subscribe: vi.fn(),
|
||||
$dispose: vi.fn(),
|
||||
setAccountAction,
|
||||
setConnectSignInPayload,
|
||||
setQueueConnectSignOut,
|
||||
accountActionStatus: ref('success'),
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('~/store/installKey', () => {
|
||||
const install = vi.fn();
|
||||
|
||||
return {
|
||||
useInstallKeyStore: vi.fn(() => ({
|
||||
$state: {},
|
||||
$patch: vi.fn(),
|
||||
$reset: vi.fn(),
|
||||
$subscribe: vi.fn(),
|
||||
$dispose: vi.fn(),
|
||||
install,
|
||||
keyInstallStatus: ref('success'),
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('~/store/server', () => {
|
||||
const refreshServerState = vi.fn();
|
||||
|
||||
return {
|
||||
useServerStore: vi.fn(() => ({
|
||||
$state: {},
|
||||
$patch: vi.fn(),
|
||||
$reset: vi.fn(),
|
||||
$subscribe: vi.fn(),
|
||||
$dispose: vi.fn(),
|
||||
refreshServerState,
|
||||
refreshServerStateStatus: ref('done'),
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('~/store/updateOsActions', () => {
|
||||
const setUpdateOsAction = vi.fn();
|
||||
const actOnUpdateOsAction = vi.fn();
|
||||
|
||||
return {
|
||||
useUpdateOsActionsStore: vi.fn(() => ({
|
||||
$state: {},
|
||||
$patch: vi.fn(),
|
||||
$reset: vi.fn(),
|
||||
$subscribe: vi.fn(),
|
||||
$dispose: vi.fn(),
|
||||
setUpdateOsAction,
|
||||
actOnUpdateOsAction,
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
||||
describe('Callback Actions Store', () => {
|
||||
let store: ReturnType<typeof useCallbackActionsStore>;
|
||||
let preventClose: { addPreventClose: Mock; removePreventClose: Mock };
|
||||
let mockWatcher: Mock;
|
||||
|
||||
beforeEach(async () => {
|
||||
setActivePinia(createPinia());
|
||||
store = useCallbackActionsStore();
|
||||
|
||||
const preventCloseModule = await import('~/composables/preventClose');
|
||||
|
||||
preventClose = {
|
||||
addPreventClose: vi.mocked(preventCloseModule.addPreventClose),
|
||||
removePreventClose: vi.mocked(preventCloseModule.removePreventClose),
|
||||
};
|
||||
|
||||
const { useCallback } = await import('@unraid/shared-callbacks');
|
||||
|
||||
mockWatcher = vi.mocked(useCallback).mock.results[0].value.watcher;
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
describe('Initial State', () => {
|
||||
it('should initialize with default values', () => {
|
||||
expect(store.callbackStatus).toBe('ready');
|
||||
expect(store.callbackData).toBeUndefined();
|
||||
expect(store.sendType).toBe('fromUpc');
|
||||
expect(store.encryptionKey).toBe(import.meta.env.VITE_CALLBACK_KEY);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Watcher Functionality', () => {
|
||||
it('should call saveCallbackData when watcher returns data', async () => {
|
||||
const mockData: QueryPayloads = {
|
||||
type: 'forUpc',
|
||||
actions: [
|
||||
{
|
||||
type: 'signIn',
|
||||
user: { email: 'test@example.com', preferred_username: 'test' },
|
||||
apiKey: 'test-key',
|
||||
} as ExternalSignIn,
|
||||
],
|
||||
sender: 'test',
|
||||
};
|
||||
|
||||
mockWatcher.mockReturnValue(mockData);
|
||||
store.watcher();
|
||||
|
||||
expect(mockWatcher).toHaveBeenCalled();
|
||||
expect(store.callbackData).toEqual(mockData);
|
||||
expect(store.callbackStatus).toBe('loading');
|
||||
});
|
||||
|
||||
it('should not call saveCallbackData when watcher returns null', async () => {
|
||||
mockWatcher.mockReturnValue(null);
|
||||
store.watcher();
|
||||
|
||||
expect(mockWatcher).toHaveBeenCalled();
|
||||
expect(store.callbackData).toBeUndefined();
|
||||
expect(store.callbackStatus).toBe('ready');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Save Callback Data', () => {
|
||||
it('should save data and trigger redirect when valid data provided', () => {
|
||||
const mockData: QueryPayloads = {
|
||||
type: 'forUpc',
|
||||
actions: [
|
||||
{
|
||||
type: 'signIn',
|
||||
user: { email: 'test@example.com', preferred_username: 'test' },
|
||||
apiKey: 'test-key',
|
||||
} as ExternalSignIn,
|
||||
],
|
||||
sender: 'test',
|
||||
};
|
||||
|
||||
store.saveCallbackData(mockData);
|
||||
|
||||
expect(store.callbackData).toEqual(mockData);
|
||||
expect(store.callbackStatus).toBe('loading');
|
||||
});
|
||||
|
||||
it('should handle missing callback data', () => {
|
||||
const consoleSpy = vi.spyOn(console, 'error');
|
||||
|
||||
store.saveCallbackData(undefined);
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalledWith('Saved callback data not found');
|
||||
expect(store.callbackStatus).toBe('ready');
|
||||
});
|
||||
|
||||
it('should handle invalid callback type', () => {
|
||||
const mockData = {
|
||||
type: 'fromUpc',
|
||||
actions: [],
|
||||
sender: 'test',
|
||||
} as QueryPayloads;
|
||||
const consoleSpy = vi.spyOn(console, 'error');
|
||||
|
||||
store.saveCallbackData(mockData);
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
'[redirectToCallbackType]',
|
||||
'Callback redirect type not present or incorrect'
|
||||
);
|
||||
expect(store.callbackStatus).toBe('ready');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Callback Actions Handling', () => {
|
||||
it('should handle sign in action', async () => {
|
||||
const mockData: QueryPayloads = {
|
||||
type: 'forUpc',
|
||||
actions: [
|
||||
{
|
||||
type: 'signIn',
|
||||
user: { email: 'test@example.com', preferred_username: 'test' },
|
||||
apiKey: 'test-key',
|
||||
} as ExternalSignIn,
|
||||
],
|
||||
sender: 'test',
|
||||
};
|
||||
|
||||
store.saveCallbackData(mockData);
|
||||
await nextTick();
|
||||
|
||||
expect(vi.mocked(useAccountStore)().setAccountAction).toHaveBeenCalled();
|
||||
expect(vi.mocked(useAccountStore)().setConnectSignInPayload).toHaveBeenCalledWith({
|
||||
apiKey: 'test-key',
|
||||
email: 'test@example.com',
|
||||
preferred_username: 'test',
|
||||
});
|
||||
expect(vi.mocked(useServerStore)().refreshServerState).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle sign out action', async () => {
|
||||
const mockData: QueryPayloads = {
|
||||
type: 'forUpc',
|
||||
actions: [
|
||||
{
|
||||
type: 'signOut',
|
||||
},
|
||||
],
|
||||
sender: 'test',
|
||||
};
|
||||
|
||||
store.saveCallbackData(mockData);
|
||||
await nextTick();
|
||||
|
||||
expect(vi.mocked(useAccountStore)().setAccountAction).toHaveBeenCalled();
|
||||
expect(vi.mocked(useAccountStore)().setQueueConnectSignOut).toHaveBeenCalledWith(true);
|
||||
expect(vi.mocked(useServerStore)().refreshServerState).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle updateOs action', async () => {
|
||||
const mockData: QueryPayloads = {
|
||||
type: 'forUpc',
|
||||
actions: [
|
||||
{
|
||||
type: 'updateOs',
|
||||
server: {
|
||||
guid: 'test-guid',
|
||||
name: 'test-server',
|
||||
},
|
||||
sha256: 'test-sha256',
|
||||
version: '6.12.3',
|
||||
} as ExternalUpdateOsAction,
|
||||
],
|
||||
sender: 'test',
|
||||
};
|
||||
|
||||
store.saveCallbackData(mockData);
|
||||
await nextTick();
|
||||
|
||||
expect(vi.mocked(useUpdateOsActionsStore)().setUpdateOsAction).toHaveBeenCalled();
|
||||
expect(vi.mocked(useUpdateOsActionsStore)().actOnUpdateOsAction).toHaveBeenCalled();
|
||||
expect(vi.mocked(useServerStore)().refreshServerState).not.toHaveBeenCalled(); // Single action, no refresh needed
|
||||
});
|
||||
|
||||
it('should handle multiple actions', async () => {
|
||||
const mockData: QueryPayloads = {
|
||||
type: 'forUpc',
|
||||
actions: [
|
||||
{
|
||||
type: 'signIn',
|
||||
user: { email: 'test@example.com', preferred_username: 'test' },
|
||||
apiKey: 'test-key',
|
||||
} as ExternalSignIn,
|
||||
{
|
||||
type: 'updateOs',
|
||||
server: {
|
||||
guid: 'test-guid',
|
||||
name: 'test-server',
|
||||
},
|
||||
sha256: 'test-sha256',
|
||||
version: '6.12.3',
|
||||
} as ExternalUpdateOsAction,
|
||||
],
|
||||
sender: 'test',
|
||||
};
|
||||
|
||||
store.saveCallbackData(mockData);
|
||||
await nextTick();
|
||||
|
||||
expect(vi.mocked(useAccountStore)().setAccountAction).toHaveBeenCalled();
|
||||
expect(vi.mocked(useUpdateOsActionsStore)().setUpdateOsAction).toHaveBeenCalled();
|
||||
expect(vi.mocked(useServerStore)().refreshServerState).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Status Management', () => {
|
||||
beforeEach(() => {
|
||||
// Mock window.history
|
||||
vi.spyOn(window.history, 'replaceState').mockImplementation(() => {});
|
||||
vi.spyOn(window, 'location', 'get').mockReturnValue({
|
||||
...window.location,
|
||||
pathname: '/test-path',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle status changes correctly', async () => {
|
||||
store.setCallbackStatus('loading');
|
||||
await nextTick();
|
||||
|
||||
expect(preventClose.addPreventClose).toHaveBeenCalled();
|
||||
|
||||
store.setCallbackStatus('success');
|
||||
await nextTick();
|
||||
|
||||
expect(preventClose.removePreventClose).toHaveBeenCalled();
|
||||
expect(window.history.replaceState).toHaveBeenCalledWith(null, '', '/test-path');
|
||||
});
|
||||
|
||||
it('should handle multiple status transitions', async () => {
|
||||
store.setCallbackStatus('loading');
|
||||
await nextTick();
|
||||
|
||||
expect(preventClose.addPreventClose).toHaveBeenCalledTimes(1);
|
||||
|
||||
store.setCallbackStatus('error');
|
||||
await nextTick();
|
||||
|
||||
expect(preventClose.removePreventClose).toHaveBeenCalledTimes(1);
|
||||
expect(window.history.replaceState).toHaveBeenCalledTimes(1);
|
||||
|
||||
store.setCallbackStatus('loading');
|
||||
await nextTick();
|
||||
|
||||
expect(preventClose.addPreventClose).toHaveBeenCalledTimes(2);
|
||||
|
||||
store.setCallbackStatus('success');
|
||||
await nextTick();
|
||||
|
||||
expect(preventClose.removePreventClose).toHaveBeenCalledTimes(2);
|
||||
expect(window.history.replaceState).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('should not trigger prevent close for non-loading status changes', () => {
|
||||
store.setCallbackStatus('ready');
|
||||
expect(preventClose.addPreventClose).not.toHaveBeenCalled();
|
||||
expect(preventClose.removePreventClose).not.toHaveBeenCalled();
|
||||
|
||||
store.setCallbackStatus('error');
|
||||
expect(preventClose.addPreventClose).not.toHaveBeenCalled();
|
||||
expect(preventClose.removePreventClose).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
43
web/__test__/store/dropdown.test.ts
Normal file
43
web/__test__/store/dropdown.test.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* Dropdown store test coverage
|
||||
*/
|
||||
|
||||
import { createPinia, setActivePinia } from 'pinia';
|
||||
|
||||
import { beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
import { useDropdownStore } from '~/store/dropdown';
|
||||
|
||||
describe('Dropdown Store', () => {
|
||||
let store: ReturnType<typeof useDropdownStore>;
|
||||
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia());
|
||||
store = useDropdownStore();
|
||||
});
|
||||
|
||||
describe('State and Actions', () => {
|
||||
it('should initialize with dropdown hidden', () => {
|
||||
expect(store.dropdownVisible).toBe(false);
|
||||
});
|
||||
|
||||
it('should show dropdown', () => {
|
||||
store.dropdownShow();
|
||||
expect(store.dropdownVisible).toBe(true);
|
||||
});
|
||||
|
||||
it('should hide dropdown', () => {
|
||||
store.dropdownShow();
|
||||
store.dropdownHide();
|
||||
expect(store.dropdownVisible).toBe(false);
|
||||
});
|
||||
|
||||
it('should toggle dropdown', () => {
|
||||
expect(store.dropdownVisible).toBe(false);
|
||||
store.dropdownToggle();
|
||||
expect(store.dropdownVisible).toBe(true);
|
||||
store.dropdownToggle();
|
||||
expect(store.dropdownVisible).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
152
web/__test__/store/errors.test.ts
Normal file
152
web/__test__/store/errors.test.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
/**
|
||||
* Errors store test coverage
|
||||
*/
|
||||
|
||||
import { nextTick } from 'vue';
|
||||
import { createPinia, setActivePinia } from 'pinia';
|
||||
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import type { Error } from '~/store/errors';
|
||||
|
||||
import { useErrorsStore } from '~/store/errors';
|
||||
|
||||
const mockFeedbackButton = vi.fn();
|
||||
|
||||
// Mock OBJ_TO_STR function
|
||||
vi.mock('~/helpers/functions', () => ({
|
||||
OBJ_TO_STR: (obj: unknown) => JSON.stringify(obj),
|
||||
}));
|
||||
|
||||
// Mock FeedbackButton global
|
||||
vi.stubGlobal('FeedbackButton', mockFeedbackButton);
|
||||
|
||||
describe('Errors Store', () => {
|
||||
let store: ReturnType<typeof useErrorsStore>;
|
||||
const originalConsoleError = console.error;
|
||||
|
||||
const mockError: Error = {
|
||||
heading: 'Test Error',
|
||||
level: 'error',
|
||||
message: 'Test message',
|
||||
type: 'request',
|
||||
ref: 'test-ref',
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
// Silence console.error during tests
|
||||
console.error = vi.fn();
|
||||
|
||||
const pinia = createPinia();
|
||||
setActivePinia(pinia);
|
||||
store = useErrorsStore();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
console.error = originalConsoleError;
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
describe('State and Actions', () => {
|
||||
it('should initialize with empty errors array', () => {
|
||||
expect(store.errors).toEqual([]);
|
||||
});
|
||||
|
||||
it('should add error', () => {
|
||||
store.setError(mockError);
|
||||
expect(store.errors).toHaveLength(1);
|
||||
expect(store.errors[0]).toEqual(mockError);
|
||||
});
|
||||
|
||||
it('should remove error by index', async () => {
|
||||
store.setError(mockError);
|
||||
store.setError({ ...mockError, ref: 'test-ref-2' });
|
||||
expect(store.errors).toHaveLength(2);
|
||||
|
||||
store.removeErrorByIndex(0);
|
||||
await nextTick();
|
||||
|
||||
expect(store.errors).toHaveLength(1);
|
||||
expect(store.errors[0].ref).toBe('test-ref-2');
|
||||
});
|
||||
|
||||
it('should remove error by ref', async () => {
|
||||
store.setError(mockError);
|
||||
store.setError({ ...mockError, ref: 'test-ref-2' });
|
||||
expect(store.errors).toHaveLength(2);
|
||||
|
||||
store.removeErrorByRef('test-ref');
|
||||
await nextTick();
|
||||
|
||||
expect(store.errors).toHaveLength(1);
|
||||
expect(store.errors[0].ref).toBe('test-ref-2');
|
||||
});
|
||||
|
||||
it('should reset errors', async () => {
|
||||
store.setError(mockError);
|
||||
store.setError({ ...mockError, ref: 'test-ref-2' });
|
||||
expect(store.errors).toHaveLength(2);
|
||||
|
||||
store.resetErrors();
|
||||
await nextTick();
|
||||
|
||||
expect(store.errors).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Troubleshoot Feature', () => {
|
||||
beforeEach(() => {
|
||||
// Mock the DOM elements needed for troubleshoot
|
||||
const mockModal = document.createElement('div');
|
||||
mockModal.className = 'sweet-alert visible';
|
||||
|
||||
const mockTextarea = document.createElement('textarea');
|
||||
mockTextarea.id = 'troubleshootDetails';
|
||||
mockModal.appendChild(mockTextarea);
|
||||
|
||||
const mockEmailInput = document.createElement('input');
|
||||
mockEmailInput.id = 'troubleshootEmail';
|
||||
mockModal.appendChild(mockEmailInput);
|
||||
|
||||
const mockRadio = document.createElement('input');
|
||||
mockRadio.id = 'optTroubleshoot';
|
||||
mockRadio.type = 'radio';
|
||||
mockModal.appendChild(mockRadio);
|
||||
|
||||
const mockPanels = document.createElement('div');
|
||||
mockPanels.className = 'allpanels';
|
||||
mockPanels.id = 'troubleshoot_panel';
|
||||
mockModal.appendChild(mockPanels);
|
||||
|
||||
document.body.appendChild(mockModal);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
document.body.innerHTML = '';
|
||||
});
|
||||
|
||||
it('should open troubleshoot with error details', async () => {
|
||||
store.setError(mockError);
|
||||
await nextTick();
|
||||
|
||||
await store.openTroubleshoot({
|
||||
email: 'test@example.com',
|
||||
includeUnraidApiLogs: true,
|
||||
});
|
||||
|
||||
const textarea = document.querySelector('#troubleshootDetails') as HTMLTextAreaElement;
|
||||
const emailInput = document.querySelector('#troubleshootEmail') as HTMLInputElement;
|
||||
const radio = document.querySelector('#optTroubleshoot') as HTMLInputElement;
|
||||
const panel = document.querySelector('#troubleshoot_panel') as HTMLElement;
|
||||
|
||||
expect(mockFeedbackButton).toHaveBeenCalled();
|
||||
expect(textarea.value).toContain('Debug Details – Component Errors 1');
|
||||
expect(textarea.value).toContain('Error 1: Test Error');
|
||||
expect(textarea.value).toContain('Error 1 Message: Test message');
|
||||
expect(emailInput.value).toBe('test@example.com');
|
||||
expect(radio.checked).toBe(true);
|
||||
expect(panel.style.display).toBe('block');
|
||||
});
|
||||
});
|
||||
});
|
||||
189
web/__test__/store/installKey.test.ts
Normal file
189
web/__test__/store/installKey.test.ts
Normal file
@@ -0,0 +1,189 @@
|
||||
/**
|
||||
* InstallKey store test coverage
|
||||
*/
|
||||
|
||||
import { createPinia, setActivePinia } from 'pinia';
|
||||
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import type { ExternalKeyActions } from '@unraid/shared-callbacks';
|
||||
|
||||
import { useInstallKeyStore } from '~/store/installKey';
|
||||
|
||||
const mockGetFn = vi.fn();
|
||||
vi.mock('~/composables/services/webgui', () => ({
|
||||
WebguiInstallKey: {
|
||||
query: vi.fn(() => ({
|
||||
get: mockGetFn,
|
||||
})),
|
||||
},
|
||||
}));
|
||||
|
||||
const mockSetError = vi.fn();
|
||||
vi.mock('~/store/errors', () => ({
|
||||
useErrorsStore: () => ({
|
||||
setError: mockSetError,
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('@unraid/shared-callbacks', () => ({}));
|
||||
const createTestAction = (data: Partial<ExternalKeyActions>): ExternalKeyActions => {
|
||||
return {
|
||||
type: 'purchase',
|
||||
keyUrl: '',
|
||||
...data,
|
||||
};
|
||||
};
|
||||
|
||||
describe('InstallKey Store', () => {
|
||||
let store: ReturnType<typeof useInstallKeyStore>;
|
||||
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia());
|
||||
store = useInstallKeyStore();
|
||||
|
||||
vi.spyOn(console, 'log').mockImplementation(() => {});
|
||||
vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('State and Initialization', () => {
|
||||
it('should initialize with default state', () => {
|
||||
expect(store.keyInstallStatus).toBe('ready');
|
||||
expect(store.keyActionType).toBeUndefined();
|
||||
expect(store.keyType).toBeUndefined();
|
||||
expect(store.keyUrl).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Installing Keys', () => {
|
||||
it('should fail when keyUrl is not provided', async () => {
|
||||
await store.install(
|
||||
createTestAction({
|
||||
type: 'purchase',
|
||||
keyUrl: undefined,
|
||||
})
|
||||
);
|
||||
|
||||
expect(store.keyInstallStatus).toBe('failed');
|
||||
expect(console.error).toHaveBeenCalledWith('[install] no key to install');
|
||||
});
|
||||
|
||||
it('should set status to installing when install is called', async () => {
|
||||
mockGetFn.mockResolvedValueOnce({ success: true });
|
||||
|
||||
const promise = store.install(
|
||||
createTestAction({
|
||||
type: 'purchase',
|
||||
keyUrl: 'https://example.com/license.key',
|
||||
})
|
||||
);
|
||||
|
||||
expect(store.keyInstallStatus).toBe('installing');
|
||||
|
||||
await promise;
|
||||
});
|
||||
|
||||
it('should call WebguiInstallKey.query with correct url', async () => {
|
||||
mockGetFn.mockResolvedValueOnce({ success: true });
|
||||
|
||||
await store.install(
|
||||
createTestAction({
|
||||
type: 'purchase',
|
||||
keyUrl: 'https://example.com/license.key',
|
||||
})
|
||||
);
|
||||
|
||||
expect(store.keyInstallStatus).toBe('success');
|
||||
});
|
||||
|
||||
it('should extract key type from .key URL', async () => {
|
||||
mockGetFn.mockResolvedValueOnce({ success: true });
|
||||
|
||||
await store.install(
|
||||
createTestAction({
|
||||
type: 'purchase',
|
||||
keyUrl: 'https://example.com/license.key',
|
||||
})
|
||||
);
|
||||
|
||||
expect(store.keyType).toBe('license');
|
||||
});
|
||||
|
||||
it('should extract key type from .unkey URL', async () => {
|
||||
mockGetFn.mockResolvedValueOnce({ success: true });
|
||||
|
||||
await store.install(
|
||||
createTestAction({
|
||||
type: 'purchase',
|
||||
keyUrl: 'https://example.com/premium.unkey',
|
||||
})
|
||||
);
|
||||
|
||||
expect(store.keyType).toBe('premium');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error Handling', () => {
|
||||
it('should handle string errors during installation', async () => {
|
||||
mockGetFn.mockRejectedValueOnce('error message');
|
||||
|
||||
await store.install(
|
||||
createTestAction({
|
||||
type: 'purchase',
|
||||
keyUrl: 'https://example.com/license.key',
|
||||
})
|
||||
);
|
||||
|
||||
expect(store.keyInstallStatus).toBe('failed');
|
||||
expect(mockSetError).toHaveBeenCalledWith({
|
||||
heading: 'Failed to install key',
|
||||
message: 'ERROR MESSAGE',
|
||||
level: 'error',
|
||||
ref: 'installKey',
|
||||
type: 'installKey',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle Error object during installation', async () => {
|
||||
mockGetFn.mockRejectedValueOnce(new Error('Test error message'));
|
||||
|
||||
await store.install(
|
||||
createTestAction({
|
||||
type: 'purchase',
|
||||
keyUrl: 'https://example.com/license.key',
|
||||
})
|
||||
);
|
||||
|
||||
expect(store.keyInstallStatus).toBe('failed');
|
||||
expect(mockSetError).toHaveBeenCalledWith({
|
||||
heading: 'Failed to install key',
|
||||
message: 'Test error message',
|
||||
level: 'error',
|
||||
ref: 'installKey',
|
||||
type: 'installKey',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle unknown error types during installation', async () => {
|
||||
mockGetFn.mockRejectedValueOnce({ something: 'wrong' });
|
||||
|
||||
await store.install(
|
||||
createTestAction({
|
||||
type: 'purchase',
|
||||
keyUrl: 'https://example.com/license.key',
|
||||
})
|
||||
);
|
||||
|
||||
expect(store.keyInstallStatus).toBe('failed');
|
||||
expect(mockSetError).toHaveBeenCalledWith({
|
||||
heading: 'Failed to install key',
|
||||
message: 'Unknown error',
|
||||
level: 'error',
|
||||
ref: 'installKey',
|
||||
type: 'installKey',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -25,6 +25,31 @@ type MockServerStore = ReturnType<typeof useServerStore> & Record<string, unknow
|
||||
// Helper function to safely create test data with type assertions
|
||||
const createTestData = <T extends Record<string, unknown>>(data: T): T => data as T;
|
||||
|
||||
// Save original console methods
|
||||
const originalConsoleDebug = console.debug;
|
||||
const originalConsoleError = console.error;
|
||||
const originalConsoleWarn = console.warn;
|
||||
|
||||
vi.mock('vue', async () => {
|
||||
const actual = await vi.importActual('vue');
|
||||
return {
|
||||
...actual,
|
||||
toRefs: (obj: Record<string, unknown>) => {
|
||||
// Handle non-reactive objects to prevent warnings
|
||||
if (!obj || typeof obj !== 'object') {
|
||||
return {};
|
||||
}
|
||||
return (
|
||||
actual as unknown as { toRefs: (obj: Record<string, unknown>) => Record<string, unknown> }
|
||||
).toRefs(obj);
|
||||
},
|
||||
watchEffect: (fn: () => void) => {
|
||||
fn();
|
||||
return () => {};
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const getStore = () => {
|
||||
const pinia = createTestingPinia({
|
||||
createSpy: vi.fn,
|
||||
@@ -293,6 +318,11 @@ vi.mock('~/composables/locale', async () => {
|
||||
|
||||
describe('useServerStore', () => {
|
||||
beforeEach(() => {
|
||||
// Silence console logs
|
||||
console.debug = vi.fn();
|
||||
console.error = vi.fn();
|
||||
console.warn = vi.fn();
|
||||
|
||||
setActivePinia(
|
||||
createTestingPinia({
|
||||
createSpy: vi.fn,
|
||||
@@ -301,6 +331,11 @@ describe('useServerStore', () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Restore console functions
|
||||
console.debug = originalConsoleDebug;
|
||||
console.error = originalConsoleError;
|
||||
console.warn = originalConsoleWarn;
|
||||
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
|
||||
@@ -1,19 +1,24 @@
|
||||
import { computed, ref, watchEffect } from 'vue';
|
||||
import { createPinia, defineStore, setActivePinia } from 'pinia';
|
||||
import { useMutation } from '@vue/apollo-composable';
|
||||
import { logErrorMessages } from '@vue/apollo-util';
|
||||
import { defineStore, createPinia, setActivePinia } from 'pinia';
|
||||
|
||||
import { CONNECT_SIGN_IN, CONNECT_SIGN_OUT } from './account.fragment';
|
||||
import { ACCOUNT_CALLBACK } from '~/helpers/urls';
|
||||
|
||||
import type { ExternalSignIn, ExternalSignOut } from '@unraid/shared-callbacks';
|
||||
|
||||
import { useCallbackActionsStore } from '~/store/callbackActions';
|
||||
import { useErrorsStore } from '~/store/errors';
|
||||
import { useReplaceRenewStore } from '~/store/replaceRenew';
|
||||
import { useServerStore } from '~/store/server';
|
||||
import { useUnraidApiStore } from '~/store/unraidApi';
|
||||
import { ACCOUNT_CALLBACK } from '~/helpers/urls';
|
||||
import type { ExternalSignIn, ExternalSignOut } from '@unraid/shared-callbacks';
|
||||
import { CONNECT_SIGN_IN, CONNECT_SIGN_OUT } from './account.fragment';
|
||||
|
||||
/**
|
||||
* @see https://stackoverflow.com/questions/73476371/using-pinia-with-vue-js-web-components
|
||||
* @see https://github.com/vuejs/pinia/discussions/1085
|
||||
*/
|
||||
|
||||
setActivePinia(createPinia());
|
||||
|
||||
export interface ConnectSignInMutationPayload {
|
||||
@@ -78,28 +83,32 @@ export const useAccountStore = defineStore('account', () => {
|
||||
const downgradeOs = async (autoRedirectReplace?: boolean) => {
|
||||
await callbackStore.send(
|
||||
ACCOUNT_CALLBACK.toString(),
|
||||
[{
|
||||
server: {
|
||||
...serverAccountPayload.value,
|
||||
[
|
||||
{
|
||||
server: {
|
||||
...serverAccountPayload.value,
|
||||
},
|
||||
type: 'downgradeOs',
|
||||
},
|
||||
type: 'downgradeOs',
|
||||
}],
|
||||
inIframe.value ? 'newTab' : (autoRedirectReplace ? 'replace' : undefined),
|
||||
sendType.value,
|
||||
],
|
||||
inIframe.value ? 'newTab' : autoRedirectReplace ? 'replace' : undefined,
|
||||
sendType.value
|
||||
);
|
||||
};
|
||||
|
||||
const manage = () => {
|
||||
callbackStore.send(
|
||||
ACCOUNT_CALLBACK.toString(),
|
||||
[{
|
||||
server: {
|
||||
...serverAccountPayload.value,
|
||||
[
|
||||
{
|
||||
server: {
|
||||
...serverAccountPayload.value,
|
||||
},
|
||||
type: 'manage',
|
||||
},
|
||||
type: 'manage',
|
||||
}],
|
||||
],
|
||||
inIframe.value ? 'newTab' : undefined,
|
||||
sendType.value,
|
||||
sendType.value
|
||||
);
|
||||
};
|
||||
const myKeys = async () => {
|
||||
@@ -110,14 +119,16 @@ export const useAccountStore = defineStore('account', () => {
|
||||
|
||||
callbackStore.send(
|
||||
ACCOUNT_CALLBACK.toString(),
|
||||
[{
|
||||
server: {
|
||||
...serverAccountPayload.value,
|
||||
[
|
||||
{
|
||||
server: {
|
||||
...serverAccountPayload.value,
|
||||
},
|
||||
type: 'myKeys',
|
||||
},
|
||||
type: 'myKeys',
|
||||
}],
|
||||
],
|
||||
inIframe.value ? 'newTab' : undefined,
|
||||
sendType.value,
|
||||
sendType.value
|
||||
);
|
||||
};
|
||||
const linkKey = async () => {
|
||||
@@ -128,127 +139,152 @@ export const useAccountStore = defineStore('account', () => {
|
||||
|
||||
callbackStore.send(
|
||||
ACCOUNT_CALLBACK.toString(),
|
||||
[{
|
||||
server: {
|
||||
...serverAccountPayload.value,
|
||||
[
|
||||
{
|
||||
server: {
|
||||
...serverAccountPayload.value,
|
||||
},
|
||||
type: 'linkKey',
|
||||
},
|
||||
type: 'linkKey',
|
||||
}],
|
||||
],
|
||||
inIframe.value ? 'newTab' : undefined,
|
||||
sendType.value,
|
||||
sendType.value
|
||||
);
|
||||
};
|
||||
const recover = () => {
|
||||
callbackStore.send(
|
||||
ACCOUNT_CALLBACK.toString(),
|
||||
[{
|
||||
server: {
|
||||
...serverAccountPayload.value,
|
||||
[
|
||||
{
|
||||
server: {
|
||||
...serverAccountPayload.value,
|
||||
},
|
||||
type: 'recover',
|
||||
},
|
||||
type: 'recover',
|
||||
}],
|
||||
],
|
||||
inIframe.value ? 'newTab' : undefined,
|
||||
sendType.value,
|
||||
sendType.value
|
||||
);
|
||||
};
|
||||
const replace = () => {
|
||||
callbackStore.send(
|
||||
ACCOUNT_CALLBACK.toString(),
|
||||
[{
|
||||
server: {
|
||||
...serverAccountPayload.value,
|
||||
[
|
||||
{
|
||||
server: {
|
||||
...serverAccountPayload.value,
|
||||
},
|
||||
type: 'replace',
|
||||
},
|
||||
type: 'replace',
|
||||
}],
|
||||
],
|
||||
inIframe.value ? 'newTab' : undefined,
|
||||
sendType.value,
|
||||
sendType.value
|
||||
);
|
||||
};
|
||||
const signIn = () => {
|
||||
callbackStore.send(
|
||||
ACCOUNT_CALLBACK.toString(),
|
||||
[{
|
||||
server: {
|
||||
...serverAccountPayload.value,
|
||||
[
|
||||
{
|
||||
server: {
|
||||
...serverAccountPayload.value,
|
||||
},
|
||||
type: 'signIn',
|
||||
},
|
||||
type: 'signIn',
|
||||
}],
|
||||
],
|
||||
inIframe.value ? 'newTab' : undefined,
|
||||
sendType.value,
|
||||
sendType.value
|
||||
);
|
||||
};
|
||||
const signOut = () => {
|
||||
callbackStore.send(
|
||||
ACCOUNT_CALLBACK.toString(),
|
||||
[{
|
||||
server: {
|
||||
...serverAccountPayload.value,
|
||||
[
|
||||
{
|
||||
server: {
|
||||
...serverAccountPayload.value,
|
||||
},
|
||||
type: 'signOut',
|
||||
},
|
||||
type: 'signOut',
|
||||
}],
|
||||
],
|
||||
inIframe.value ? 'newTab' : undefined,
|
||||
sendType.value,
|
||||
sendType.value
|
||||
);
|
||||
};
|
||||
const trialExtend = () => {
|
||||
callbackStore.send(
|
||||
ACCOUNT_CALLBACK.toString(),
|
||||
[{
|
||||
server: {
|
||||
...serverAccountPayload.value,
|
||||
[
|
||||
{
|
||||
server: {
|
||||
...serverAccountPayload.value,
|
||||
},
|
||||
type: 'trialExtend',
|
||||
},
|
||||
type: 'trialExtend',
|
||||
}],
|
||||
],
|
||||
inIframe.value ? 'newTab' : undefined,
|
||||
sendType.value,
|
||||
sendType.value
|
||||
);
|
||||
};
|
||||
const trialStart = () => {
|
||||
callbackStore.send(
|
||||
ACCOUNT_CALLBACK.toString(),
|
||||
[{
|
||||
server: {
|
||||
...serverAccountPayload.value,
|
||||
[
|
||||
{
|
||||
server: {
|
||||
...serverAccountPayload.value,
|
||||
},
|
||||
type: 'trialStart',
|
||||
},
|
||||
type: 'trialStart',
|
||||
}],
|
||||
],
|
||||
inIframe.value ? 'newTab' : undefined,
|
||||
sendType.value,
|
||||
sendType.value
|
||||
);
|
||||
};
|
||||
|
||||
const updateOs = async (autoRedirectReplace?: boolean) => {
|
||||
await callbackStore.send(
|
||||
ACCOUNT_CALLBACK.toString(),
|
||||
[{
|
||||
server: {
|
||||
...serverAccountPayload.value,
|
||||
[
|
||||
{
|
||||
server: {
|
||||
...serverAccountPayload.value,
|
||||
},
|
||||
type: 'updateOs',
|
||||
},
|
||||
type: 'updateOs',
|
||||
}],
|
||||
inIframe.value ? 'newTab' : (autoRedirectReplace ? 'replace' : undefined),
|
||||
],
|
||||
inIframe.value ? 'newTab' : autoRedirectReplace ? 'replace' : undefined,
|
||||
sendType.value
|
||||
);
|
||||
};
|
||||
|
||||
const connectSignInMutation = async () => {
|
||||
if (!connectSignInPayload.value ||
|
||||
(connectSignInPayload.value && (!connectSignInPayload.value.apiKey || !connectSignInPayload.value.email || !connectSignInPayload.value.preferred_username))
|
||||
if (
|
||||
!connectSignInPayload.value ||
|
||||
(connectSignInPayload.value &&
|
||||
(!connectSignInPayload.value.apiKey ||
|
||||
!connectSignInPayload.value.email ||
|
||||
!connectSignInPayload.value.preferred_username))
|
||||
) {
|
||||
accountActionStatus.value = 'failed';
|
||||
return console.error('[connectSignInMutation] incorrect payload', connectSignInPayload.value);
|
||||
}
|
||||
|
||||
accountActionStatus.value = 'updating';
|
||||
const { mutate: signInMutation, onDone, onError } = await useMutation(CONNECT_SIGN_IN, {
|
||||
const {
|
||||
mutate: signInMutation,
|
||||
onDone,
|
||||
onError,
|
||||
} = await useMutation(CONNECT_SIGN_IN, {
|
||||
variables: {
|
||||
input: {
|
||||
apiKey: connectSignInPayload.value.apiKey,
|
||||
userInfo: {
|
||||
email: connectSignInPayload.value.email,
|
||||
preferred_username: connectSignInPayload.value.preferred_username,
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
signInMutation();
|
||||
@@ -314,7 +350,7 @@ export const useAccountStore = defineStore('account', () => {
|
||||
});
|
||||
};
|
||||
|
||||
const setAccountAction = (action: ExternalSignIn|ExternalSignOut) => {
|
||||
const setAccountAction = (action: ExternalSignIn | ExternalSignOut) => {
|
||||
console.debug('[setAccountAction]', { action });
|
||||
accountAction.value = action;
|
||||
};
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { defineStore, createPinia, setActivePinia } from 'pinia';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { createPinia, defineStore, setActivePinia, storeToRefs } from 'pinia';
|
||||
|
||||
import { ACTIVATION_CODE_MODAL_HIDDEN_STORAGE_KEY } from '~/consts';
|
||||
import { useServerStore } from '~/store/server';
|
||||
|
||||
import { useCallbackActionsStore } from '~/store/callbackActions';
|
||||
import { useServerStore } from '~/store/server';
|
||||
|
||||
setActivePinia(createPinia()); /** required in web component context */
|
||||
|
||||
@@ -31,12 +34,18 @@ export const useActivationCodeStore = defineStore('activationCode', () => {
|
||||
const code = computed<string | null>(() => data.value?.code || null);
|
||||
const partnerName = computed<string | null>(() => data.value?.partnerName || null);
|
||||
const partnerUrl = computed<string | null>(() => data.value?.partnerUrl || null);
|
||||
const partnerLogo = computed<string | null>(() => data.value?.partnerLogo ? `/webGui/images/partner-logo.svg` : null);
|
||||
const partnerLogo = computed<string | null>(() =>
|
||||
data.value?.partnerLogo ? `/webGui/images/partner-logo.svg` : null
|
||||
);
|
||||
|
||||
const activationModalHidden = ref<boolean>(sessionStorage.getItem(ACTIVATION_CODE_MODAL_HIDDEN_STORAGE_KEY) === 'true');
|
||||
const setActivationModalHidden = (value: boolean) => activationModalHidden.value = value;
|
||||
const activationModalHidden = ref<boolean>(
|
||||
sessionStorage.getItem(ACTIVATION_CODE_MODAL_HIDDEN_STORAGE_KEY) === 'true'
|
||||
);
|
||||
const setActivationModalHidden = (value: boolean) => (activationModalHidden.value = value);
|
||||
watch(activationModalHidden, (newVal) => {
|
||||
return newVal ? sessionStorage.setItem(ACTIVATION_CODE_MODAL_HIDDEN_STORAGE_KEY, 'true') : sessionStorage.removeItem(ACTIVATION_CODE_MODAL_HIDDEN_STORAGE_KEY);
|
||||
return newVal
|
||||
? sessionStorage.setItem(ACTIVATION_CODE_MODAL_HIDDEN_STORAGE_KEY, 'true')
|
||||
: sessionStorage.removeItem(ACTIVATION_CODE_MODAL_HIDDEN_STORAGE_KEY);
|
||||
});
|
||||
/**
|
||||
* Should only see this if
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { computed, ref, watch, watchEffect } from 'vue';
|
||||
import { createPinia, defineStore, setActivePinia } from 'pinia';
|
||||
|
||||
import { useCallback } from '@unraid/shared-callbacks';
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { ref } from 'vue';
|
||||
import { createPinia, defineStore, setActivePinia } from 'pinia';
|
||||
import { useToggle } from '@vueuse/core';
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { ref } from 'vue';
|
||||
import { createPinia, defineStore, setActivePinia } from 'pinia';
|
||||
|
||||
import { OBJ_TO_STR } from '~/helpers/functions';
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { defineStore, createPinia, setActivePinia } from 'pinia';
|
||||
import { computed, ref } from 'vue';
|
||||
import { createPinia, defineStore, setActivePinia } from 'pinia';
|
||||
|
||||
import type { ExternalKeyActions } from '@unraid/shared-callbacks';
|
||||
|
||||
import { WebguiInstallKey } from '~/composables/services/webgui';
|
||||
import { useErrorsStore } from '~/store/errors';
|
||||
import type { ExternalKeyActions } from '@unraid/shared-callbacks';
|
||||
|
||||
/**
|
||||
* @see https://stackoverflow.com/questions/73476371/using-pinia-with-vue-js-web-components
|
||||
* @see https://github.com/vuejs/pinia/discussions/1085
|
||||
@@ -20,7 +24,9 @@ export const useInstallKeyStore = defineStore('installKey', () => {
|
||||
* Extracts key type from key url. Works for both .key and .unkey.
|
||||
*/
|
||||
const keyType = computed((): string | undefined => {
|
||||
if (!keyUrl.value) { return undefined; }
|
||||
if (!keyUrl.value) {
|
||||
return undefined;
|
||||
}
|
||||
const parts = keyUrl.value.split('/');
|
||||
return parts[parts.length - 1].replace(/\.key|\.unkey/g, '');
|
||||
});
|
||||
@@ -36,9 +42,7 @@ export const useInstallKeyStore = defineStore('installKey', () => {
|
||||
}
|
||||
|
||||
try {
|
||||
const installResponse = await WebguiInstallKey
|
||||
.query({ url: keyUrl.value })
|
||||
.get();
|
||||
const installResponse = await WebguiInstallKey.query({ url: keyUrl.value }).get();
|
||||
console.log('[install] WebguiInstallKey installResponse', installResponse);
|
||||
|
||||
keyInstallStatus.value = 'success';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* @todo Check OS and Connect Plugin versions against latest via API every session
|
||||
*/
|
||||
import { computed, ref, toRefs, watch } from 'vue';
|
||||
import { computed, ref, toRefs, watch, watchEffect } from 'vue';
|
||||
import { createPinia, defineStore, setActivePinia } from 'pinia';
|
||||
import { useLazyQuery } from '@vue/apollo-composable';
|
||||
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { computed, ref } from 'vue';
|
||||
import { createPinia, defineStore, setActivePinia } from 'pinia';
|
||||
|
||||
import dayjs, { extend } from 'dayjs';
|
||||
import customParseFormat from 'dayjs/plugin/customParseFormat';
|
||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||
import { defineStore, createPinia, setActivePinia } from 'pinia';
|
||||
import { computed } from 'vue';
|
||||
|
||||
import {
|
||||
WebguiCheckForUpdate,
|
||||
WebguiUpdateCancel,
|
||||
} from '~/composables/services/webgui';
|
||||
import { useServerStore } from '~/store/server';
|
||||
import type { ServerUpdateOsResponse } from '~/types/server';
|
||||
|
||||
import { WebguiCheckForUpdate, WebguiUpdateCancel } from '~/composables/services/webgui';
|
||||
import { useServerStore } from '~/store/server';
|
||||
|
||||
/**
|
||||
* @see https://stackoverflow.com/questions/73476371/using-pinia-with-vue-js-web-components
|
||||
* @see https://github.com/vuejs/pinia/discussions/1085
|
||||
@@ -47,12 +47,12 @@ export const useUpdateOsStore = defineStore('updateOs', () => {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return !updateOsResponse.value?.isEligible
|
||||
? updateOsResponse.value.version
|
||||
: undefined;
|
||||
return !updateOsResponse.value?.isEligible ? updateOsResponse.value.version : undefined;
|
||||
});
|
||||
|
||||
const availableReleaseDate = computed(() => updateOsResponse.value?.date ? dayjs(updateOsResponse.value.date, 'YYYY-MM-DD') : undefined);
|
||||
const availableReleaseDate = computed(() =>
|
||||
updateOsResponse.value?.date ? dayjs(updateOsResponse.value.date, 'YYYY-MM-DD') : undefined
|
||||
);
|
||||
|
||||
/**
|
||||
* If the updateOsResponse does not have a sha256, then the user is required to authenticate to download the update
|
||||
@@ -69,7 +69,7 @@ export const useUpdateOsStore = defineStore('updateOs', () => {
|
||||
serverStore.setUpdateOsResponse(response as ServerUpdateOsResponse);
|
||||
checkForUpdatesLoading.value = false;
|
||||
} catch (error) {
|
||||
throw new Error("[localCheckForUpdate] Error checking for updates\n" + JSON.stringify(error));
|
||||
throw new Error('[localCheckForUpdate] Error checking for updates\n' + JSON.stringify(error));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -88,7 +88,9 @@ export const useUpdateOsStore = defineStore('updateOs', () => {
|
||||
// otherwise refresh the page
|
||||
window.location.reload();
|
||||
} catch (error) {
|
||||
throw new Error(`[cancelUpdate] Error cancelling update with error: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
throw new Error(
|
||||
`[cancelUpdate] Error cancelling update with error: ${error instanceof Error ? error.message : 'Unknown error'}`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user