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
4 changes: 2 additions & 2 deletions api/dev/states/myservers.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -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://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://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"
[connectionStatus]
minigraph="ERROR_RETRYING"
upnpStatus="Success: UPNP Lease Renewed [4/2/2025 12:00:00 PM] Public Port [41820] Local Port [443]"
upnpStatus=""
49 changes: 36 additions & 13 deletions api/generated-schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,8 @@ type ArrayCapacity {
}

type ArrayDisk {
color: ArrayDiskFsColor

""" User comment on disk """
comment: String

Expand Down Expand Up @@ -187,14 +189,35 @@ type ArrayDisk {
}

enum ArrayDiskFsColor {
"""Disk is OK and not running"""
green_off
"""New device, in standby mode (spun-down)"""
blue_blink

"""New device"""
blue_on

"""Disk is OK and running"""
"""Device is in standby mode (spun-down)"""
green_blink

"""Normal operation, device is active"""
green_on

"""Device not present"""
grey_off

"""
Device is missing (disabled) or contents emulated / Parity device is missing
"""
red_off

"""Device is disabled or contents emulated / Parity device is disabled"""
red_on
yellow_off

"""
Device contents invalid or emulated / Parity is invalid, in standby mode (spun-down)
"""
yellow_blink

"""Device contents invalid or emulated / Parity is invalid"""
yellow_on
}

Expand Down Expand Up @@ -507,7 +530,7 @@ type Disk {
serialNum: String!
size: Long!
smartStatus: DiskSmartStatus!
temperature: Long!
temperature: Long
totalCylinders: Long!
totalHeads: Long!
totalSectors: Long!
Expand All @@ -519,6 +542,8 @@ type Disk {

enum DiskFsType {
btrfs
ext4
ntfs
vfat
xfs
zfs
Expand Down Expand Up @@ -570,7 +595,6 @@ type Display {
type Docker implements Node {
containers: [DockerContainer!]
id: ID!
mutations: DockerMutations!
networks: [DockerNetwork!]
}

Expand All @@ -595,8 +619,11 @@ type DockerContainer {
}

type DockerMutations {
startContainer(id: ID!): DockerContainer!
stopContainer(id: ID!): DockerContainer!
""" Start a container """
start(id: ID!): DockerContainer!

""" Stop a container """
stop(id: ID!): DockerContainer!
}

type DockerNetwork {
Expand Down Expand Up @@ -836,6 +863,7 @@ type Mutation {

"""Delete a user"""
deleteUser(input: deleteUserInput!): User
docker: DockerMutations
enableDynamicRemoteAccess(input: EnableDynamicRemoteAccessInput!): Boolean!
login(password: String!, username: String!): String

Expand Down Expand Up @@ -1084,9 +1112,6 @@ type Query {
display: Display
docker: Docker!

"""All Docker containers"""
dockerContainers(all: Boolean): [DockerContainer!]!

"""Docker network"""
dockerNetwork(id: ID!): DockerNetwork!

Expand Down Expand Up @@ -1322,8 +1347,6 @@ type Subscription {
array: Array!
config: Config!
display: Display
dockerContainer(id: ID!): DockerContainer!
dockerContainers: [DockerContainer]
dockerNetwork(id: ID!): DockerNetwork!
dockerNetworks: [DockerNetwork]!
flash: Flash!
Expand Down
64 changes: 4 additions & 60 deletions api/src/__test__/common/allowed-origins.test.ts
Original file line number Diff line number Diff line change
@@ -1,72 +1,16 @@
import { getAllowedOrigins, getExtraOrigins } from '@app/common/allowed-origins.js';
import { getServerIps } from '@app/graphql/resolvers/subscription/network.js';
import { getAllowedOrigins } from '@app/common/allowed-origins.js';
import { store } from '@app/store/index.js';
import { loadConfigFile } from '@app/store/modules/config.js';
import { loadStateFiles } from '@app/store/modules/emhttp.js';

import 'reflect-metadata';

import { beforeEach, expect, test, vi } from 'vitest';

// Mock the dependencies that provide dynamic values
vi.mock('@app/graphql/resolvers/subscription/network.js', () => ({
getServerIps: vi.fn(),
getUrlForField: vi.fn(({ url, port, portSsl }) => {
if (port) return `http://${url}:${port}`;
if (portSsl) return `https://${url}:${portSsl}`;
return `https://${url}`;
}),
}));

vi.mock('@app/store/index.js', () => ({
store: {
getState: vi.fn(() => ({
emhttp: {
status: 'LOADED',
nginx: {
httpPort: 8080,
httpsPort: 4443,
},
},
})),
dispatch: vi.fn(),
},
getters: {
config: vi.fn(() => ({
api: {
extraOrigins: 'https://google.com,https://test.com',
},
})),
},
}));

beforeEach(() => {
vi.clearAllMocks();

// Mock getServerIps to return a consistent set of URLs
(getServerIps as any).mockReturnValue({
urls: [
{ ipv4: 'https://tower.local:4443' },
{ ipv4: 'https://192.168.1.150:4443' },
{ ipv4: 'https://tower:4443' },
{ ipv4: 'https://192-168-1-150.thisisfourtyrandomcharacters012345678900.myunraid.net:4443' },
{ ipv4: 'https://10-252-0-1.hash.myunraid.net:4443' },
{ ipv4: 'https://10-252-1-1.hash.myunraid.net:4443' },
{ ipv4: 'https://10-253-3-1.hash.myunraid.net:4443' },
{ ipv4: 'https://10-253-4-1.hash.myunraid.net:4443' },
{ ipv4: 'https://10-253-5-1.hash.myunraid.net:4443' },
{ ipv4: 'https://10-100-0-1.hash.myunraid.net:4443' },
{ ipv4: 'https://10-100-0-2.hash.myunraid.net:4443' },
{ ipv4: 'https://10-123-1-2.hash.myunraid.net:4443' },
{ ipv4: 'https://221-123-121-112.hash.myunraid.net:4443' },
],
});
});
import { expect, test } from 'vitest';

test('Returns allowed origins', async () => {
// Load state files into store
await store.dispatch(loadStateFiles());
await store.dispatch(loadConfigFile());
await store.dispatch(loadStateFiles()).unwrap();
await store.dispatch(loadConfigFile()).unwrap();

// Get allowed origins
const allowedOrigins = getAllowedOrigins();
Expand Down
6 changes: 3 additions & 3 deletions api/src/__test__/core/utils/misc/get-key-file.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ test('Returns empty key if key location is empty', async () => {

// Check if store has state files loaded
const { status } = store.getState().registration;
expect(status).toBe(FileLoadStatus.LOADED);
expect(status).toBe(FileLoadStatus.UNLOADED);
await expect(getKeyFile()).resolves.toBe('');
});

Expand All @@ -53,10 +53,10 @@ test(
async () => {
const { getKeyFile } = await import('@app/core/utils/misc/get-key-file.js');
const { loadStateFiles } = await import('@app/store/modules/emhttp.js');

const { loadRegistrationKey } = await import('@app/store/modules/registration.js');
// Load state files into store
await store.dispatch(loadStateFiles());

await store.dispatch(loadRegistrationKey());
// Check if store has state files loaded
const { status } = store.getState().registration;
expect(status).toBe(FileLoadStatus.LOADED);
Expand Down
1 change: 1 addition & 0 deletions api/src/__test__/core/utils/shares/get-shares.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@ test('Returns shares by type', async () => {
});

test('Returns shares by name', async () => {
await store.dispatch(loadStateFiles());
expect(getShares('user', { name: 'domains' })).toMatchInlineSnapshot(`
{
"allocator": "highwater",
Expand Down
8 changes: 8 additions & 0 deletions api/src/__test__/setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import '@app/__test__/setup/env-setup.js';
// import './setup/mock-fs-setup';
import '@app/__test__/setup/keyserver-mock.js';
import '@app/__test__/setup/config-setup.js';
import '@app/__test__/setup/store-reset.js';

// This file is automatically loaded by Vitest before running tests
// It imports all the setup files that need to be run before tests
45 changes: 45 additions & 0 deletions api/src/__test__/setup/mock-fs-setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { beforeEach, vi } from 'vitest';

// Create a global mock file system that can be used across all tests
export const mockFileSystem = new Map<string, string>();

// Mock fs/promises
vi.mock('node:fs/promises', () => ({
writeFile: vi.fn().mockImplementation((path, content) => {
mockFileSystem.set(path.toString(), content.toString());
return Promise.resolve();
}),
readFile: vi.fn().mockImplementation((path) => {
const content = mockFileSystem.get(path.toString());
if (content === undefined) {
return Promise.reject(new Error(`File not found: ${path}`));
}
return Promise.resolve(content);
}),
access: vi.fn().mockImplementation((path) => {
if (mockFileSystem.has(path.toString())) {
return Promise.resolve();
}
return Promise.reject(new Error(`File not found: ${path}`));
}),
}));

// Mock fs-extra
vi.mock('fs-extra', () => ({
emptyDir: vi.fn().mockImplementation(() => {
mockFileSystem.clear();
return Promise.resolve();
}),
}));

// Mock file-exists utility
vi.mock('@app/core/utils/files/file-exists.js', () => ({
fileExists: vi.fn().mockImplementation((path) => {
return Promise.resolve(mockFileSystem.has(path.toString()));
}),
}));

// Clear the mock file system before each test
beforeEach(() => {
mockFileSystem.clear();
});
8 changes: 8 additions & 0 deletions api/src/__test__/setup/store-reset.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { beforeEach } from 'vitest';

import { resetStore } from '@app/store/actions/reset-store.js';
import { store } from '@app/store/index.js';

beforeEach(() => {
store.dispatch(resetStore());
});
41 changes: 31 additions & 10 deletions api/src/__test__/store/modules/config.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { expect, test, vi } from 'vitest';
import { beforeEach, 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';
Expand All @@ -10,15 +10,36 @@ 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/core/pubsub.js', () => {
const mockPublish = vi.fn();
return {
pubsub: {
publish: mockPublish,
},
PUBSUB_CHANNEL: {
OWNER: 'OWNER',
SERVERS: 'SERVERS',
},
__esModule: true,
default: {
pubsub: {
publish: mockPublish,
},
PUBSUB_CHANNEL: {
OWNER: 'OWNER',
SERVERS: 'SERVERS',
},
},
};
});

// Get the mock function for pubsub.publish
const mockPublish = vi.mocked(pubsub.publish);

// Clear mock before each test
beforeEach(() => {
mockPublish.mockClear();
});

vi.mock('@app/mothership/graphql-client.js', () => ({
GraphQLClient: {
Expand Down
8 changes: 4 additions & 4 deletions api/src/__test__/store/modules/registration.test.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { expect, test } from 'vitest';

import { store } from '@app/store/index.js';
import { getters, store } from '@app/store/index.js';
import { loadRegistrationKey } from '@app/store/modules/registration.js';
import { FileLoadStatus, StateFileKey } from '@app/store/types.js';

// Preloading imports for faster tests

test('Before loading key returns null', async () => {
const { status, keyFile } = store.getState().registration;
const { status, keyFile } = getters.registration();
expect(status).toBe(FileLoadStatus.UNLOADED);
expect(keyFile).toBe(null);
});
Expand All @@ -17,7 +17,7 @@ test('Requires emhttp to be loaded to find key file', async () => {
await store.dispatch(loadRegistrationKey());

// Check if store has state files loaded
const { status, keyFile } = store.getState().registration;
const { status, keyFile } = getters.registration();

expect(status).toBe(FileLoadStatus.LOADED);
expect(keyFile).toBe(null);
Expand All @@ -42,7 +42,7 @@ test('Returns empty key if key location is empty', async () => {
await store.dispatch(loadRegistrationKey());

// Check if store has state files loaded
const { status, keyFile } = store.getState().registration;
const { status, keyFile } = getters.registration();
expect(status).toBe(FileLoadStatus.LOADED);
expect(keyFile).toBe('');
});
3 changes: 2 additions & 1 deletion api/src/common/allowed-origins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const getAllowedSocks = (): string[] => [

const getLocalAccessUrlsForServer = (state: RootState = store.getState()): string[] => {
const { emhttp } = state;

if (emhttp.status !== FileLoadStatus.LOADED) {
return [];
}
Expand Down Expand Up @@ -90,7 +91,7 @@ const getApolloSandbox = (): string[] => {
export const getAllowedOrigins = (state: RootState = store.getState()): string[] =>
uniq([
...getAllowedSocks(),
...getLocalAccessUrlsForServer(),
...getLocalAccessUrlsForServer(state),
...getRemoteAccessUrlsForAllowedOrigins(state),
...getExtraOrigins(),
...getConnectOrigins(),
Expand Down
Loading
Loading