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
3 changes: 3 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,9 @@ jobs:
cd ${{ github.workspace }}
pnpm install --frozen-lockfile --filter @unraid/ui

- name: Lint
run: pnpm run lint

- name: Build
run: pnpm run build:wc

Expand Down
4 changes: 2 additions & 2 deletions api/dev/Unraid.net/myservers.cfg
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[api]
version="4.1.3"
extraOrigins="https://google.com,https://test.com"
version="4.4.1"
extraOrigins="https://google.com, https://test.com"
[local]
sandbox="yes"
[remote]
Expand Down
6 changes: 3 additions & 3 deletions api/dev/states/myservers.cfg
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[api]
version="4.4.1"
extraOrigins="https://google.com,https://test.com"
extraOrigins="https://google.com, https://test.com"
[local]
sandbox="yes"
[remote]
Expand All @@ -18,7 +18,7 @@ idtoken=""
refreshtoken=""
dynamicRemoteAccessType="DISABLED"
ssoSubIds=""
allowedOrigins="/var/run/unraid-notifications.sock, /var/run/unraid-php.sock, /var/run/unraid-cli.sock, http://localhost:8080, https://localhost:4443, https://tower.local:4443, https://192.168.1.150:4443, https://tower:4443, https://192-168-1-150.thisisfourtyrandomcharacters012345678900.myunraid.net:4443, https://85-121-123-122.thisisfourtyrandomcharacters012345678900.myunraid.net:8443, https://10-252-0-1.hash.myunraid.net:4443, https://10-252-1-1.hash.myunraid.net:4443, https://10-253-3-1.hash.myunraid.net:4443, https://10-253-4-1.hash.myunraid.net:4443, https://10-253-5-1.hash.myunraid.net:4443, https://10-100-0-1.hash.myunraid.net:4443, https://10-100-0-2.hash.myunraid.net:4443, https://10-123-1-2.hash.myunraid.net:4443, https://221-123-121-112.hash.myunraid.net:4443, https://google.com, https://test.com, https://connect.myunraid.net, https://connect-staging.myunraid.net, https://dev-my.myunraid.net:4000, https://studio.apollographql.com"
allowedOrigins="/var/run/unraid-notifications.sock, /var/run/unraid-php.sock, /var/run/unraid-cli.sock, http://localhost:8080, https://localhost:4443, https://tower.local:4443, https://192.168.1.150:4443, https://tower:4443, https://192-168-1-150.thisisfourtyrandomcharacters012345678900.myunraid.net:4443, https://10-252-0-1.hash.myunraid.net:4443, https://10-252-1-1.hash.myunraid.net:4443, https://10-253-3-1.hash.myunraid.net:4443, https://10-253-4-1.hash.myunraid.net:4443, https://10-253-5-1.hash.myunraid.net:4443, https://10-100-0-1.hash.myunraid.net:4443, https://10-100-0-2.hash.myunraid.net:4443, https://10-123-1-2.hash.myunraid.net:4443, https://221-123-121-112.hash.myunraid.net:4443, https://google.com, https://test.com, https://connect.myunraid.net, https://connect-staging.myunraid.net, https://dev-my.myunraid.net:4000, https://studio.apollographql.com"
[connectionStatus]
minigraph="ERROR_RETRYING"
upnpStatus=""
upnpStatus="Success: UPNP Lease Renewed [4/2/2025 12:00:00 PM] Public Port [41820] Local Port [443]"
313 changes: 247 additions & 66 deletions api/src/__test__/store/modules/config.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,75 @@
import { expect, test } from 'vitest';
import { expect, test, vi } from 'vitest';

import { pubsub, PUBSUB_CHANNEL } from '@app/core/pubsub.js';
import { MinigraphStatus, WAN_ACCESS_TYPE, WAN_FORWARD_TYPE } from '@app/graphql/generated/api/types.js';
import { GraphQLClient } from '@app/mothership/graphql-client.js';
import { stopPingTimeoutJobs } from '@app/mothership/jobs/ping-timeout-jobs.js';
import { setGraphqlConnectionStatus } from '@app/store/actions/set-minigraph-status.js';
import { setupRemoteAccessThunk } from '@app/store/actions/setup-remote-access.js';
import { store } from '@app/store/index.js';
import { MyServersConfigMemory } from '@app/types/my-servers-config.js';

// Mock dependencies
vi.mock('@app/core/pubsub.js', () => ({
pubsub: {
publish: vi.fn(),
},
PUBSUB_CHANNEL: {
OWNER: 'OWNER',
SERVERS: 'SERVERS',
},
}));

vi.mock('@app/mothership/graphql-client.js', () => ({
GraphQLClient: {
clearInstance: vi.fn(),
},
}));

vi.mock('@app/mothership/jobs/ping-timeout-jobs.js', () => ({
stopPingTimeoutJobs: vi.fn(),
}));

const createConfigMatcher = (specificValues: Partial<MyServersConfigMemory> = {}) => {
const defaultMatcher = {
api: expect.objectContaining({
extraOrigins: expect.any(String),
version: expect.any(String),
}),
connectionStatus: expect.objectContaining({
minigraph: expect.any(String),
upnpStatus: expect.any(String),
}),
local: expect.objectContaining({
sandbox: expect.any(String),
}),
nodeEnv: expect.any(String),
remote: expect.objectContaining({
accesstoken: expect.any(String),
allowedOrigins: expect.any(String),
apikey: expect.any(String),
avatar: expect.any(String),
dynamicRemoteAccessType: expect.any(String),
email: expect.any(String),
idtoken: expect.any(String),
localApiKey: expect.any(String),
refreshtoken: expect.any(String),
regWizTime: expect.any(String),
ssoSubIds: expect.any(String),
upnpEnabled: expect.any(String),
username: expect.any(String),
wanaccess: expect.any(String),
wanport: expect.any(String),
}),
status: expect.any(String),
};

return expect.objectContaining({
...defaultMatcher,
...specificValues,
});
};

test('Before init returns default values for all fields', async () => {
const state = store.getState().config;
expect(state).toMatchSnapshot();
Expand All @@ -16,40 +83,7 @@ test('After init returns values from cfg file for all fields', async () => {

// Check if store has cfg contents loaded
const state = store.getState().config;
expect(state).toMatchObject(
expect.objectContaining({
api: {
extraOrigins: expect.stringMatching('https://google.com,https://test.com'),
version: expect.any(String),
},
connectionStatus: {
minigraph: 'PRE_INIT',
upnpStatus: '',
},
local: {
sandbox: expect.any(String),
},
nodeEnv: 'test',
remote: {
accesstoken: '',
allowedOrigins: '',
apikey: '_______________________BIG_API_KEY_HERE_________________________',
avatar: 'https://via.placeholder.com/200',
dynamicRemoteAccessType: 'DISABLED',
email: 'test@example.com',
idtoken: '',
localApiKey: '_______________________LOCAL_API_KEY_HERE_________________________',
refreshtoken: '',
regWizTime: '1611175408732_0951-1653-3509-FBA155FA23C0',
ssoSubIds: '',
upnpEnabled: 'no',
username: 'zspearmint',
wanaccess: 'yes',
wanport: '8443',
},
status: 'LOADED',
})
);
expect(state).toMatchObject(createConfigMatcher());
});

test('updateUserConfig merges in changes to current state', async () => {
Expand All @@ -67,37 +101,184 @@ test('updateUserConfig merges in changes to current state', async () => {

const state = store.getState().config;
expect(state).toMatchObject(
expect.objectContaining({
api: {
extraOrigins: expect.stringMatching('https://google.com,https://test.com'),
version: expect.any(String),
},
connectionStatus: {
minigraph: 'PRE_INIT',
upnpStatus: '',
},
local: {
sandbox: expect.any(String),
},
nodeEnv: 'test',
remote: {
accesstoken: '',
allowedOrigins: '',
apikey: '_______________________BIG_API_KEY_HERE_________________________',
createConfigMatcher({
remote: expect.objectContaining({
avatar: 'https://via.placeholder.com/200',
dynamicRemoteAccessType: 'DISABLED',
email: 'test@example.com',
idtoken: '',
localApiKey: '_______________________LOCAL_API_KEY_HERE_________________________',
refreshtoken: '',
regWizTime: '1611175408732_0951-1653-3509-FBA155FA23C0',
ssoSubIds: '',
upnpEnabled: 'no',
username: 'zspearmint',
wanaccess: 'yes',
wanport: '8443',
},
status: 'LOADED',
} as MyServersConfigMemory)
}),
})
);
});

test('loginUser updates state and publishes to pubsub', async () => {
const { loginUser } = await import('@app/store/modules/config.js');
const userInfo = {
email: 'test@example.com',
avatar: 'https://via.placeholder.com/200',
username: 'testuser',
apikey: 'test-api-key',
localApiKey: 'test-local-api-key',
};

await store.dispatch(loginUser(userInfo));

expect(pubsub.publish).toHaveBeenCalledWith(PUBSUB_CHANNEL.OWNER, {
owner: {
username: userInfo.username,
avatar: userInfo.avatar,
},
});

const state = store.getState().config;
expect(state).toMatchObject(
createConfigMatcher({
remote: expect.objectContaining(userInfo),
})
);
});

test('logoutUser clears state and publishes to pubsub', async () => {
const { logoutUser } = await import('@app/store/modules/config.js');

await store.dispatch(logoutUser({ reason: 'test logout' }));

expect(pubsub.publish).toHaveBeenCalledWith(PUBSUB_CHANNEL.SERVERS, { servers: [] });
expect(pubsub.publish).toHaveBeenCalledWith(PUBSUB_CHANNEL.OWNER, {
owner: {
username: 'root',
url: '',
avatar: '',
},
});
expect(stopPingTimeoutJobs).toHaveBeenCalled();
expect(GraphQLClient.clearInstance).toHaveBeenCalled();
});

test('updateAccessTokens updates token fields', async () => {
const { updateAccessTokens } = await import('@app/store/modules/config.js');
const tokens = {
accesstoken: 'new-access-token',
refreshtoken: 'new-refresh-token',
idtoken: 'new-id-token',
};

store.dispatch(updateAccessTokens(tokens));

const state = store.getState().config;
expect(state).toMatchObject(
createConfigMatcher({
remote: expect.objectContaining(tokens),
})
);
});

test('updateAllowedOrigins updates extraOrigins', async () => {
const { updateAllowedOrigins } = await import('@app/store/modules/config.js');
const origins = ['https://test1.com', 'https://test2.com'];

store.dispatch(updateAllowedOrigins(origins));

const state = store.getState().config;
expect(state.api.extraOrigins).toBe(origins.join(', '));
});

test('setUpnpState updates upnp settings', async () => {
const { setUpnpState } = await import('@app/store/modules/config.js');

store.dispatch(setUpnpState({ enabled: 'yes', status: 'active' }));

const state = store.getState().config;
expect(state.remote.upnpEnabled).toBe('yes');
expect(state.connectionStatus.upnpStatus).toBe('active');
});

test('setWanPortToValue updates wanport', async () => {
const { setWanPortToValue } = await import('@app/store/modules/config.js');

store.dispatch(setWanPortToValue(8443));

const state = store.getState().config;
expect(state.remote.wanport).toBe('8443');
});

test('setWanAccess updates wanaccess', async () => {
const { setWanAccess } = await import('@app/store/modules/config.js');

store.dispatch(setWanAccess('yes'));

const state = store.getState().config;
expect(state.remote.wanaccess).toBe('yes');
});

test('addSsoUser adds user to ssoSubIds', async () => {
const { addSsoUser } = await import('@app/store/modules/config.js');

store.dispatch(addSsoUser('user1'));
store.dispatch(addSsoUser('user2'));

const state = store.getState().config;
expect(state.remote.ssoSubIds).toBe('user1,user2');
});

test('removeSsoUser removes user from ssoSubIds', async () => {
const { addSsoUser, removeSsoUser } = await import('@app/store/modules/config.js');

store.dispatch(addSsoUser('user1'));
store.dispatch(addSsoUser('user2'));
store.dispatch(removeSsoUser('user1'));

const state = store.getState().config;
expect(state.remote.ssoSubIds).toBe('user2');
});

test('removeSsoUser with null clears all ssoSubIds', async () => {
const { addSsoUser, removeSsoUser } = await import('@app/store/modules/config.js');

store.dispatch(addSsoUser('user1'));
store.dispatch(addSsoUser('user2'));
store.dispatch(removeSsoUser(null));

const state = store.getState().config;
expect(state.remote.ssoSubIds).toBe('');
});

test('setLocalApiKey updates localApiKey', async () => {
const { setLocalApiKey } = await import('@app/store/modules/config.js');

store.dispatch(setLocalApiKey('new-local-api-key'));

const state = store.getState().config;
expect(state.remote.localApiKey).toBe('new-local-api-key');
});

test('setLocalApiKey with null clears localApiKey', async () => {
const { setLocalApiKey } = await import('@app/store/modules/config.js');

store.dispatch(setLocalApiKey(null));

const state = store.getState().config;
expect(state.remote.localApiKey).toBe('');
});

test('setGraphqlConnectionStatus updates minigraph status', async () => {
store.dispatch(setGraphqlConnectionStatus({ status: MinigraphStatus.CONNECTED, error: null }));

const state = store.getState().config;
expect(state.connectionStatus.minigraph).toBe(MinigraphStatus.CONNECTED);
});

test('setupRemoteAccessThunk.fulfilled updates remote access settings', async () => {
const remoteAccessSettings = {
accessType: WAN_ACCESS_TYPE.DYNAMIC,
forwardType: WAN_FORWARD_TYPE.UPNP,
};

await store.dispatch(setupRemoteAccessThunk(remoteAccessSettings));

const state = store.getState().config;
expect(state.remote).toMatchObject({
wanaccess: 'no',
dynamicRemoteAccessType: 'UPNP',
wanport: '',
upnpEnabled: 'yes',
});
});
4 changes: 1 addition & 3 deletions api/src/store/modules/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,9 +234,7 @@ export const config = createSlice({
state.remote.ssoSubIds = stateAsArray.join(',');
},
setLocalApiKey(state, action: PayloadAction<string | null>) {
if (action.payload) {
state.remote.localApiKey = action.payload;
}
state.remote.localApiKey = action.payload ?? '';
},
},
extraReducers(builder) {
Expand Down
Loading
Loading