mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-21 10:43:36 +00:00
f53550fc6c
- Remove confusing "preview mode" - channels switch and save immediately - Eliminate complex localStorage channel persistence logic - Reduce update reload time from 10+ seconds to 3 seconds - Remove redundant "Switch to X & Update" buttons - Make channel switching instant with immediate feedback Users can now simply change the dropdown to switch channels instantly, then click Apply Update for a fast 3-second reload experience. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1169 lines
48 KiB
JavaScript
1169 lines
48 KiB
JavaScript
const { fetchDiscoveryData, fetchMetricsData, fetchPbsData, clearCaches } = require('../dataFetcher');
|
|
// Don't require the real apiClients, we will mock it
|
|
// const { initializeApiClients } = require('../apiClients');
|
|
|
|
// Mock the modules used by dataFetcher
|
|
jest.mock('axios'); // Keep this in case axios is used directly anywhere unexpected
|
|
jest.mock('../pbsUtils', () => ({
|
|
// Ensure processPbsTasks returns the expected structure
|
|
processPbsTasks: jest.fn().mockReturnValue({ backupTasks: [], verifyTasks: [], gcTasks: [] }),
|
|
}));
|
|
jest.mock('../apiClients'); // <-- MOCK apiClients module
|
|
|
|
// --- REMOVE Mock for fetchPbsData within dataFetcher ---
|
|
// jest.mock('../dataFetcher', ...);
|
|
// --- END REMOVE ---
|
|
|
|
// Import the mocked version AFTER mocking it
|
|
const { initializeApiClients } = require('../apiClients');
|
|
|
|
process.env.NODE_ENV = 'test';
|
|
|
|
|
|
describe('Data Fetcher', () => {
|
|
// --- Declare variables used across tests/hooks ---
|
|
let originalEnv; // <--- Declare here
|
|
let mockPveClientInstance;
|
|
let mockPveApiClient;
|
|
let mockPbsClientInstance;
|
|
let mockPbsApiClient;
|
|
// --- End declare vars ---
|
|
|
|
// Helper to set up a basic PBS client mock (MOVED TO OUTER SCOPE)
|
|
const setupMockPbsClient = (id, configOverrides = {}, clientMocks = {}) => {
|
|
mockPbsClientInstance = {
|
|
get: jest.fn(),
|
|
...clientMocks // Allow overriding .get or adding other methods
|
|
};
|
|
// Use the mockPbsApiClient defined in the outer scope
|
|
mockPbsApiClient[id] = {
|
|
client: mockPbsClientInstance,
|
|
config: {
|
|
id: `${id}_config_id`,
|
|
name: `PBS Instance ${id}`,
|
|
host: `${id}.pbs.example.com`,
|
|
// Add other default config properties as needed
|
|
...configOverrides
|
|
}
|
|
};
|
|
return mockPbsClientInstance; // Return the mock instance for further configuration
|
|
};
|
|
|
|
beforeEach(() => {
|
|
// Store environment (assign to variable declared above)
|
|
originalEnv = { ...process.env };
|
|
// Reset the mocked initializeApiClients function and other mocks
|
|
jest.clearAllMocks();
|
|
|
|
// Define the *default* return value for the mocked initializer
|
|
// Tests can override this if needed
|
|
mockPveClientInstance = { get: jest.fn() };
|
|
mockPveApiClient = {
|
|
primary: { client: mockPveClientInstance, config: { /* ... */ } }
|
|
};
|
|
mockPbsClientInstance = { get: jest.fn() };
|
|
mockPbsApiClient = {};
|
|
initializeApiClients.mockResolvedValue({
|
|
apiClients: mockPveApiClient,
|
|
pbsApiClients: mockPbsApiClient
|
|
});
|
|
|
|
// --- Remove console mocks ---
|
|
// jest.spyOn(console, 'warn').mockImplementation(() => {});
|
|
// jest.spyOn(console, 'log').mockImplementation(() => {});
|
|
// jest.spyOn(console, 'error').mockImplementation(() => {});
|
|
});
|
|
|
|
afterEach(() => {
|
|
// Clear caches to prevent test pollution
|
|
clearCaches();
|
|
|
|
// Restore environment (can now access originalEnv)
|
|
const currentEnvKeys = Object.keys(process.env);
|
|
currentEnvKeys.forEach(key => delete process.env[key]);
|
|
Object.keys(originalEnv).forEach(key => { process.env[key] = originalEnv[key]; });
|
|
// --- Remove console restore ---
|
|
// jest.restoreAllMocks();
|
|
});
|
|
|
|
describe('fetchDiscoveryData', () => {
|
|
test('should return empty structure when no PVE clients configured', async () => {
|
|
const mockPbsFunction = jest.fn().mockResolvedValue([]);
|
|
|
|
const result = await fetchDiscoveryData({}, mockPbsApiClient, mockPbsFunction);
|
|
|
|
expect(result.nodes).toEqual([]);
|
|
expect(result.vms).toEqual([]);
|
|
expect(result.containers).toEqual([]);
|
|
expect(result.pbs).toEqual([]);
|
|
expect(mockPbsFunction).toHaveBeenCalled();
|
|
});
|
|
|
|
test('should fetch basic PVE cluster data successfully', async () => {
|
|
const mockClient = {
|
|
primary: {
|
|
client: {
|
|
get: jest.fn()
|
|
.mockResolvedValueOnce({ data: { data: [{ type: 'cluster', nodes: 1 }] } })
|
|
.mockResolvedValue({ data: { data: [] } })
|
|
},
|
|
config: { id: 'primary', name: 'Primary PVE' }
|
|
}
|
|
};
|
|
|
|
const result = await fetchDiscoveryData(mockClient, {});
|
|
|
|
expect(result).toHaveProperty('nodes');
|
|
expect(result).toHaveProperty('vms');
|
|
expect(result).toHaveProperty('containers');
|
|
expect(result).toHaveProperty('pbs');
|
|
expect(result).toHaveProperty('pveBackups');
|
|
expect(Array.isArray(result.nodes)).toBe(true);
|
|
expect(Array.isArray(result.vms)).toBe(true);
|
|
expect(Array.isArray(result.containers)).toBe(true);
|
|
});
|
|
|
|
test('should handle bad node storage data gracefully', async () => {
|
|
const mockClient = {
|
|
primary: {
|
|
client: {
|
|
get: jest.fn().mockResolvedValue({ data: { data: [] } })
|
|
},
|
|
config: { id: 'primary', name: 'Primary PVE' }
|
|
}
|
|
};
|
|
|
|
const result = await fetchDiscoveryData(mockClient, {});
|
|
|
|
expect(result).toHaveProperty('nodes');
|
|
expect(Array.isArray(result.nodes)).toBe(true);
|
|
});
|
|
|
|
test('should handle missing node data gracefully', async () => {
|
|
const mockClient = {
|
|
primary: {
|
|
client: {
|
|
get: jest.fn().mockResolvedValue({ data: { data: null } })
|
|
},
|
|
config: { id: 'primary', name: 'Primary PVE' }
|
|
}
|
|
};
|
|
|
|
const result = await fetchDiscoveryData(mockClient, {});
|
|
|
|
expect(result).toEqual({
|
|
nodes: [],
|
|
vms: [],
|
|
containers: [],
|
|
pbs: [],
|
|
pveBackups: expect.any(Object)
|
|
});
|
|
});
|
|
|
|
test('should work with multiple PVE endpoints', async () => {
|
|
const mockClients = {
|
|
pve1: {
|
|
client: {
|
|
get: jest.fn().mockResolvedValue({ data: { data: [] } })
|
|
},
|
|
config: { id: 'pve1', name: 'PVE1' }
|
|
},
|
|
pve2: {
|
|
client: {
|
|
get: jest.fn().mockRejectedValue(new Error('Network error'))
|
|
},
|
|
config: { id: 'pve2', name: 'PVE2' }
|
|
}
|
|
};
|
|
|
|
const result = await fetchDiscoveryData(mockClients, {});
|
|
|
|
expect(result).toEqual({
|
|
nodes: [],
|
|
vms: [],
|
|
containers: [],
|
|
pbs: [],
|
|
pveBackups: expect.any(Object)
|
|
});
|
|
});
|
|
|
|
test('should handle API error when fetching guests for a specific node', async () => {
|
|
// Arrange: Uses the default mock clients from beforeEach
|
|
const nodeNameGood = 'node-good';
|
|
const nodeNameBad = 'node-bad-guests';
|
|
|
|
// Use mockImplementation on the default mockPveClientInstance
|
|
mockPveClientInstance.get.mockImplementation(async (url) => {
|
|
console.log(`Mock API call: ${url}`); // Added for debugging
|
|
if (url === '/nodes') {
|
|
return { data: { data: [
|
|
{ node: nodeNameGood, status: 'online', id: `node/${nodeNameGood}` },
|
|
{ node: nodeNameBad, status: 'online', id: `node/${nodeNameBad}` }
|
|
]}};
|
|
}
|
|
if (url === `/nodes/${nodeNameGood}/status`) {
|
|
return { data: { data: { cpu: 0.1, uptime: 10 } } };
|
|
}
|
|
if (url === `/nodes/${nodeNameGood}/storage`) {
|
|
return { data: { data: [] } };
|
|
}
|
|
if (url === `/nodes/${nodeNameGood}/qemu`) {
|
|
return { data: { data: [ { vmid: 100, name: 'vm-good', status: 'running' } ] } };
|
|
}
|
|
if (url === `/nodes/${nodeNameGood}/lxc`) {
|
|
return { data: { data: [] } };
|
|
}
|
|
if (url === `/nodes/${nodeNameBad}/status`) {
|
|
return { data: { data: { cpu: 0.2, uptime: 20 } } };
|
|
}
|
|
if (url === `/nodes/${nodeNameBad}/storage`) {
|
|
return { data: { data: [] } };
|
|
}
|
|
if (url === `/nodes/${nodeNameBad}/qemu`) {
|
|
// Simulate API error for this specific call
|
|
throw new Error('Simulated API Error Fetching Guests');
|
|
}
|
|
if (url === `/nodes/${nodeNameBad}/lxc`) {
|
|
return { data: { data: [] } }; // Successful but empty
|
|
}
|
|
// Default fallback for unexpected calls
|
|
throw new Error(`Unexpected API call in mock: ${url}`);
|
|
});
|
|
|
|
// Act: Uses the default mock clients from beforeEach
|
|
const result = await fetchDiscoveryData(mockPveApiClient, mockPbsApiClient);
|
|
|
|
// Assert
|
|
// Check that the correct number of nodes is returned
|
|
expect(result.nodes).toHaveLength(2);
|
|
|
|
// Find the nodes in the result
|
|
const goodNodeResult = result.nodes.find(n => n.node === nodeNameGood);
|
|
const badNodeResult = result.nodes.find(n => n.node === nodeNameBad);
|
|
|
|
expect(goodNodeResult).toBeDefined();
|
|
expect(badNodeResult).toBeDefined();
|
|
|
|
// Assertions for the node where all calls succeeded (nodeNameGood)
|
|
expect(goodNodeResult.cpu).toBe(0.1); // Should have CPU data from successful /status call
|
|
expect(goodNodeResult.status).toBe('online'); // Status updated by uptime > 0
|
|
expect(goodNodeResult.vms).toBeUndefined(); // VMs/CTs are in the top-level result.vms/result.containers
|
|
|
|
// Assertions for the node where /qemu failed (nodeNameBad)
|
|
// It should still have basic info from /nodes and status info from its successful /status call
|
|
expect(badNodeResult.cpu).toBe(0.2); // CPU data from its OWN successful /status call
|
|
expect(badNodeResult.status).toBe('online'); // Status updated by uptime > 0
|
|
// It should not have contributed VMs/CTs because fetchDataForNode rejected
|
|
|
|
// Assert overall VMs/Containers (only from the successful node)
|
|
expect(result.vms).toHaveLength(1);
|
|
expect(result.vms[0].vmid).toBe(100); // VM from nodeNameGood
|
|
expect(result.containers).toHaveLength(0);
|
|
|
|
// Assert PBS is empty
|
|
expect(result.pbs).toEqual([]);
|
|
});
|
|
|
|
test('should integrate PVE and PBS data successfully', async () => {
|
|
const mockPveClient = {
|
|
primary: {
|
|
client: {
|
|
get: jest.fn().mockResolvedValue({ data: { data: [] } })
|
|
},
|
|
config: { id: 'primary', name: 'Primary PVE' }
|
|
}
|
|
};
|
|
const mockPbsClient = {
|
|
'pbs-1': {
|
|
client: {
|
|
get: jest.fn().mockResolvedValue({ data: { data: [] } })
|
|
},
|
|
config: { name: 'PBS Instance' }
|
|
}
|
|
};
|
|
|
|
const result = await fetchDiscoveryData(mockPveClient, mockPbsClient);
|
|
|
|
expect(result).toHaveProperty('nodes');
|
|
expect(result).toHaveProperty('vms');
|
|
expect(result).toHaveProperty('containers');
|
|
expect(result).toHaveProperty('pbs');
|
|
expect(result).toHaveProperty('pveBackups');
|
|
});
|
|
|
|
test('should handle errors from fetchPbsData gracefully', async () => {
|
|
// Arrange PVE (same simple mock as above)
|
|
const nodeName = 'pve-node';
|
|
const vmId = 200;
|
|
mockPveClientInstance.get
|
|
.mockResolvedValueOnce({ data: { data: [{ node: nodeName, status: 'online' }] } }) // /nodes
|
|
.mockResolvedValueOnce({ data: { data: { uptime: 1 } } }) // status
|
|
.mockResolvedValueOnce({ data: { data: [] } }) // storage
|
|
.mockResolvedValueOnce({ data: { data: [{ vmid: vmId, name: 'pve-vm' }] } }) // qemu
|
|
.mockResolvedValueOnce({ data: { data: [] } }); // lxc
|
|
|
|
// Arrange PBS (Mock the function to be injected)
|
|
const mockPbsFunction = jest.fn();
|
|
const pbsError = new Error('PBS Connection Failed');
|
|
// Revert to mockRejectedValue
|
|
mockPbsFunction.mockRejectedValue(pbsError);
|
|
const result = await fetchDiscoveryData(mockPveApiClient, mockPbsApiClient, mockPbsFunction);
|
|
|
|
expect(result.nodes).toHaveLength(0);
|
|
expect(result.vms).toHaveLength(0);
|
|
expect(result.containers).toHaveLength(0);
|
|
expect(mockPbsFunction).toHaveBeenCalledWith(mockPbsApiClient);
|
|
expect(result.pbs).toEqual([]);
|
|
});
|
|
|
|
test('should work without PBS clients configured', async () => {
|
|
const mockPveClient = {
|
|
primary: {
|
|
client: {
|
|
get: jest.fn().mockResolvedValue({ data: { data: [] } })
|
|
},
|
|
config: { id: 'primary', name: 'Primary PVE' }
|
|
}
|
|
};
|
|
|
|
const result = await fetchDiscoveryData(mockPveClient, {});
|
|
|
|
expect(result).toHaveProperty('pbs');
|
|
});
|
|
|
|
test('should handle error fetching Containers (lxc)', async () => {
|
|
// Arrange: Uses the default mock clients from beforeEach
|
|
const nodeNameGood = 'node-good';
|
|
const nodeNameBad = 'node-bad-guests'; // This node will have the LXC fetch error
|
|
const endpointId = 'primary'; // Default endpointId from mockPveApiClient setup
|
|
mockPveClientInstance.get.mockImplementation(async (url) => {
|
|
if (url === '/cluster/status') {
|
|
return { data: { data: [{ type: 'cluster', nodes: 2, name: 'test-cluster' }] } };
|
|
}
|
|
if (url === '/nodes') {
|
|
return { data: { data: [
|
|
{ node: nodeNameGood, status: 'online', id: `node/${nodeNameGood}` },
|
|
{ node: nodeNameBad, status: 'online', id: `node/${nodeNameBad}` }
|
|
]}};
|
|
}
|
|
if (url === `/nodes/${nodeNameGood}/status`) return { data: { data: { cpu: 0.1, uptime: 10 } } };
|
|
if (url === `/nodes/${nodeNameGood}/storage`) return { data: { data: [] } };
|
|
if (url === `/nodes/${nodeNameGood}/qemu`) return { data: { data: [ { vmid: 100, name: 'vm-good', status: 'running' } ] } };
|
|
if (url === `/nodes/${nodeNameGood}/lxc`) return { data: { data: [] } };
|
|
if (url === `/nodes/${nodeNameBad}/status`) return { data: { data: { cpu: 0.2, uptime: 20 } } };
|
|
if (url === `/nodes/${nodeNameBad}/storage`) return { data: { data: [] } };
|
|
if (url === `/nodes/${nodeNameBad}/qemu`) return { data: { data: [] } };
|
|
if (url === `/nodes/${nodeNameBad}/lxc`) {
|
|
throw new Error('Simulated LXC Fetch Error');
|
|
}
|
|
throw new Error(`Unexpected API call in mock: ${url}`);
|
|
});
|
|
|
|
const result = await fetchDiscoveryData(mockPveApiClient, mockPbsApiClient);
|
|
|
|
expect(result.nodes).toHaveLength(2);
|
|
const goodNodeResult = result.nodes.find(n => n.node === nodeNameGood);
|
|
const badNodeResult = result.nodes.find(n => n.node === nodeNameBad);
|
|
expect(goodNodeResult).toBeDefined();
|
|
expect(badNodeResult).toBeDefined();
|
|
expect(result.vms).toHaveLength(1);
|
|
expect(result.vms[0].vmid).toBe(100);
|
|
expect(result.containers).toHaveLength(0);
|
|
});
|
|
|
|
test('should handle API failures gracefully', async () => {
|
|
// Test the behavior: when APIs fail, return empty results instead of crashing
|
|
const failingApiClient = {
|
|
primary: {
|
|
client: {
|
|
get: jest.fn().mockRejectedValue(new Error('API unavailable'))
|
|
},
|
|
config: { id: 'primary', name: 'Primary PVE' }
|
|
}
|
|
};
|
|
|
|
const result = await fetchDiscoveryData(failingApiClient, {});
|
|
|
|
// Verify behavior: should return empty structure, not crash
|
|
expect(result).toEqual({
|
|
nodes: [],
|
|
vms: [],
|
|
containers: [],
|
|
pbs: [],
|
|
pveBackups: expect.any(Object)
|
|
});
|
|
});
|
|
|
|
test('should handle invalid node status data gracefully', async () => {
|
|
const mockClient = {
|
|
primary: {
|
|
client: {
|
|
get: jest.fn().mockResolvedValue({ data: { data: null } })
|
|
},
|
|
config: { id: 'primary', name: 'Primary PVE' }
|
|
}
|
|
};
|
|
|
|
const result = await fetchDiscoveryData(mockClient, {});
|
|
|
|
expect(result).toEqual({
|
|
nodes: [],
|
|
vms: [],
|
|
containers: [],
|
|
pbs: [],
|
|
pveBackups: expect.any(Object)
|
|
});
|
|
});
|
|
|
|
|
|
test('should handle malformed API responses gracefully', async () => {
|
|
const invalidApiClient = {
|
|
primary: {
|
|
client: {
|
|
get: jest.fn().mockResolvedValue({ data: { data: 'invalid-format' } })
|
|
},
|
|
config: { id: 'primary', name: 'Primary PVE' }
|
|
}
|
|
};
|
|
|
|
const result = await fetchDiscoveryData(invalidApiClient, {});
|
|
|
|
expect(result).toEqual({
|
|
nodes: [],
|
|
vms: [],
|
|
containers: [],
|
|
pbs: [],
|
|
pveBackups: expect.any(Object)
|
|
});
|
|
});
|
|
|
|
test('should continue working when some endpoints fail', async () => {
|
|
const mixedClients = {
|
|
primary: {
|
|
client: {
|
|
get: jest.fn().mockResolvedValue({ data: { data: [] } })
|
|
},
|
|
config: { id: 'primary', name: 'Working PVE' }
|
|
},
|
|
broken: null
|
|
};
|
|
|
|
const result = await fetchDiscoveryData(mixedClients, {});
|
|
|
|
expect(result).toEqual({
|
|
nodes: [],
|
|
vms: [],
|
|
containers: [],
|
|
pbs: [],
|
|
pveBackups: expect.any(Object)
|
|
});
|
|
});
|
|
|
|
test('should work without PBS clients', async () => {
|
|
const pveOnlyClient = {
|
|
primary: {
|
|
client: {
|
|
get: jest.fn().mockResolvedValue({ data: { data: [] } })
|
|
},
|
|
config: { id: 'primary', name: 'Primary PVE' }
|
|
}
|
|
};
|
|
|
|
const result = await fetchDiscoveryData(pveOnlyClient, null);
|
|
|
|
expect(result.pbs).toEqual([]);
|
|
expect(result).toHaveProperty('nodes');
|
|
expect(result).toHaveProperty('vms');
|
|
expect(result).toHaveProperty('containers');
|
|
});
|
|
|
|
});
|
|
|
|
// --- NEW: describe block for fetchMetricsData ---
|
|
describe('fetchMetricsData', () => {
|
|
// Note: This block now relies on mockPveApiClient and mockPveClientInstance
|
|
// set up in the main beforeEach of the outer describe block.
|
|
let mockCurrentApiClients; // Keep this structure locally if tests modify it
|
|
|
|
beforeEach(() => {
|
|
// Reset only the client's get method, as the client itself is setup outside
|
|
mockPveClientInstance.get.mockClear();
|
|
|
|
// Use the mock PVE client setup in the outer scope.
|
|
// Tests within this block might add more clients (e.g., pve2) to this object.
|
|
mockCurrentApiClients = { ...mockPveApiClient };
|
|
});
|
|
|
|
test('should return empty array when no running guests are provided', async () => {
|
|
const result = await fetchMetricsData([], [], mockCurrentApiClients);
|
|
expect(result).toEqual([]);
|
|
expect(mockPveClientInstance.get).not.toHaveBeenCalled(); // Use outer mock instance
|
|
});
|
|
|
|
test('should fetch VM metrics successfully', async () => {
|
|
const mockApiClients = {
|
|
'primary': {
|
|
client: {
|
|
get: jest.fn().mockResolvedValue({ data: { data: [{ cpu: 0.5 }] } })
|
|
},
|
|
config: { name: 'Primary PVE' }
|
|
}
|
|
};
|
|
|
|
const runningVms = [
|
|
{ endpointId: 'primary', node: 'node1', vmid: 100, type: 'qemu', name: 'vm-test' }
|
|
];
|
|
|
|
const result = await fetchMetricsData(runningVms, [], mockApiClients);
|
|
|
|
expect(Array.isArray(result)).toBe(true);
|
|
expect(result.length).toBeGreaterThanOrEqual(0);
|
|
});
|
|
|
|
test('should fetch container metrics successfully', async () => {
|
|
const mockApiClients = {
|
|
'primary': {
|
|
client: {
|
|
get: jest.fn().mockResolvedValue({ data: { data: [{ cpu: 0.2 }] } })
|
|
},
|
|
config: { name: 'Primary PVE' }
|
|
}
|
|
};
|
|
|
|
const runningContainers = [
|
|
{ endpointId: 'primary', node: 'node2', vmid: 101, type: 'lxc', name: 'ct-test' }
|
|
];
|
|
|
|
const result = await fetchMetricsData([], runningContainers, mockApiClients);
|
|
|
|
expect(Array.isArray(result)).toBe(true);
|
|
expect(result.length).toBeGreaterThanOrEqual(0);
|
|
});
|
|
|
|
test('should fetch metrics for multiple guests successfully', async () => {
|
|
const mockApiClients = {
|
|
'primary': {
|
|
client: {
|
|
get: jest.fn().mockResolvedValue({ data: { data: [{ cpu: 0.1 }] } })
|
|
},
|
|
config: { name: 'Primary PVE' }
|
|
}
|
|
};
|
|
|
|
const runningVms = [
|
|
{ endpointId: 'primary', node: 'node1', vmid: 100, type: 'qemu', name: 'vm1' }
|
|
];
|
|
const runningContainers = [
|
|
{ endpointId: 'primary', node: 'node1', vmid: 101, type: 'lxc', name: 'ct1' }
|
|
];
|
|
|
|
const result = await fetchMetricsData(runningVms, runningContainers, mockApiClients);
|
|
|
|
expect(Array.isArray(result)).toBe(true);
|
|
expect(result.length).toBeGreaterThanOrEqual(0);
|
|
});
|
|
|
|
test('should handle missing API client gracefully', async () => {
|
|
const mockApiClients = {
|
|
'primary': {
|
|
client: {
|
|
get: jest.fn().mockResolvedValue({ data: { data: [{ cpu: 0.5 }] } })
|
|
},
|
|
config: { name: 'Primary PVE' }
|
|
}
|
|
};
|
|
|
|
const runningVms = [
|
|
{ endpointId: 'primary', node: 'node1', vmid: 100, type: 'qemu', name: 'vm-good' },
|
|
{ endpointId: 'missing', node: 'nodeX', vmid: 999, type: 'qemu', name: 'vm-bad' }
|
|
];
|
|
|
|
const result = await fetchMetricsData(runningVms, [], mockApiClients);
|
|
|
|
expect(Array.isArray(result)).toBe(true);
|
|
expect(result.length).toBeGreaterThanOrEqual(0);
|
|
});
|
|
|
|
|
|
test('should handle API error when fetching RRD data for one guest', async () => {
|
|
const runningVms = [
|
|
{ endpointId: 'primary', node: 'node1', vmid: 100, type: 'qemu', name: 'vm-ok' },
|
|
{ endpointId: 'primary', node: 'node1', vmid: 101, type: 'qemu', name: 'vm-fail-rrd' }
|
|
];
|
|
|
|
const error = new Error('RRD Fetch Failed');
|
|
const endpointName = mockCurrentApiClients.primary.config.name || 'primary';
|
|
|
|
// Mock success for vm-ok
|
|
mockPveClientInstance.get.mockResolvedValueOnce({ data: { data: [{ cpu: 0.1 }] } }); // rrd ok
|
|
mockPveClientInstance.get.mockResolvedValueOnce({ data: { data: { cpu: 0.11 } } }); // current ok
|
|
|
|
// Mock failure for vm-fail-rrd (RRD call fails, current call succeeds)
|
|
const result = await fetchMetricsData(runningVms, [], mockCurrentApiClients);
|
|
|
|
expect(Array.isArray(result)).toBe(true);
|
|
});
|
|
|
|
test('should handle API error when fetching current status for one guest', async () => {
|
|
const runningVms = [
|
|
{ endpointId: 'primary', node: 'node1', vmid: 100, type: 'qemu', name: 'vm-ok' },
|
|
{ endpointId: 'primary', node: 'node1', vmid: 101, type: 'qemu', name: 'vm-fail-current' }
|
|
];
|
|
|
|
const error = new Error('Current Status Fetch Failed');
|
|
mockPveClientInstance.get.mockResolvedValueOnce({ data: { data: [{ cpu: 0.1 }] } });
|
|
mockPveClientInstance.get.mockResolvedValueOnce({ data: { data: { cpu: 0.11 } } });
|
|
mockPveClientInstance.get.mockResolvedValueOnce({ data: { data: [{ cpu: 0.2 }] } });
|
|
mockPveClientInstance.get.mockRejectedValueOnce(error);
|
|
|
|
const result = await fetchMetricsData(runningVms, [], mockCurrentApiClients);
|
|
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0].id).toBe(100);
|
|
});
|
|
|
|
test('should handle API 400 error gracefully (guest likely stopped)', async () => {
|
|
const runningVms = [
|
|
{ endpointId: 'primary', node: 'node1', vmid: 100, type: 'qemu', name: 'vm-ok' },
|
|
{ endpointId: 'primary', node: 'node1', vmid: 101, type: 'qemu', name: 'vm-stopped' }
|
|
];
|
|
const endpointName = mockCurrentApiClients.primary.config.name || 'primary';
|
|
|
|
// Simulate a 400 error response
|
|
const error400 = new Error('Bad Request');
|
|
error400.response = { status: 400 };
|
|
|
|
// Mock success for vm-ok
|
|
mockPveClientInstance.get.mockResolvedValueOnce({ data: { data: [{ cpu: 0.1 }] } }); // rrd ok
|
|
mockPveClientInstance.get.mockResolvedValueOnce({ data: { data: { cpu: 0.11 } } }); // current ok
|
|
|
|
// Mock 400 failure for vm-stopped (assume RRD call fails first)
|
|
mockPveClientInstance.get.mockRejectedValueOnce(error400); // rrd fails with 400
|
|
// The current status call for the failing guest might not even happen if RRD fails hard,
|
|
// but mock it just in case the error handling changes. Let's assume it would succeed if called.
|
|
mockPveClientInstance.get.mockResolvedValueOnce({ data: { data: { cpu: 0.22 } } });
|
|
|
|
const result = await fetchMetricsData(runningVms, [], mockCurrentApiClients);
|
|
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0].id).toBe(100);
|
|
});
|
|
|
|
test('should handle empty RRD data array gracefully', async () => {
|
|
const runningVms = [
|
|
{ endpointId: 'primary', node: 'node1', vmid: 100, type: 'qemu', name: 'vm-no-rrd-data' }
|
|
];
|
|
|
|
// Mock RRD data response with empty data array
|
|
mockPveClientInstance.get.mockResolvedValueOnce({ data: { data: [] } });
|
|
// Mock current status response
|
|
mockPveClientInstance.get.mockResolvedValueOnce({ data: { data: { cpu: 0.5, mem: 1024 } } });
|
|
|
|
const result = await fetchMetricsData(runningVms, [], mockCurrentApiClients);
|
|
const endpointName = mockCurrentApiClients.primary.config.name || 'primary';
|
|
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0]).toMatchObject({
|
|
id: 100,
|
|
endpointName: endpointName,
|
|
data: [], // RRD data should be an empty array
|
|
current: { cpu: 0.5, mem: 1024 }
|
|
});
|
|
});
|
|
|
|
test('should handle null current status data gracefully', async () => {
|
|
const runningVms = [
|
|
{ endpointId: 'primary', node: 'node1', vmid: 100, type: 'qemu', name: 'vm-no-current-data' }
|
|
];
|
|
|
|
// Mock RRD data response
|
|
mockPveClientInstance.get.mockResolvedValueOnce({ data: { data: [{ time: 1, cpu: 0.5 }] } });
|
|
// Mock current status response with null data
|
|
mockPveClientInstance.get.mockResolvedValueOnce({ data: { data: null } });
|
|
|
|
const result = await fetchMetricsData(runningVms, [], mockCurrentApiClients);
|
|
const endpointName = mockCurrentApiClients.primary.config.name || 'primary';
|
|
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0]).toMatchObject({
|
|
id: 100,
|
|
endpointName: endpointName,
|
|
data: [{ time: 1, cpu: 0.5 }],
|
|
current: null // Current data should be null
|
|
});
|
|
});
|
|
|
|
// --- Tests for QEMU Guest Agent Memory Fetching ---
|
|
test('should fetch QEMU guest agent memory info when agent is enabled and responsive', async () => {
|
|
const runningVms = [
|
|
{ endpointId: 'primary', node: 'node1', vmid: 100, type: 'qemu', name: 'vm-agent-ok', agent: '1' }
|
|
];
|
|
mockPveClientInstance.get
|
|
.mockResolvedValueOnce({ data: { data: [{ time: 1, cpu: 0.5 }] } }) // RRD
|
|
.mockResolvedValueOnce({ data: { data: { cpu: 0.5, mem: 2048*1024*1024, disk: 2048, agent: 1 } } }); // Current status (agent enabled)
|
|
|
|
// Mock the POST call for guest agent
|
|
mockPveClientInstance.post = jest.fn().mockResolvedValueOnce({
|
|
data: {
|
|
data: {
|
|
result: { total: 2048*1024*1024, free: 1024*1024*1024, available: 1536*1024*1024 }
|
|
}
|
|
}
|
|
});
|
|
|
|
const result = await fetchMetricsData(runningVms, [], mockCurrentApiClients);
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0].current).toBeDefined();
|
|
expect(result[0].current.guest_mem_total_bytes).toBe(2048*1024*1024);
|
|
expect(result[0].current.guest_mem_free_bytes).toBe(1024*1024*1024);
|
|
expect(result[0].current.guest_mem_available_bytes).toBe(1536*1024*1024);
|
|
expect(result[0].current.guest_mem_actual_used_bytes).toBe((2048-1536)*1024*1024);
|
|
expect(mockPveClientInstance.post).toHaveBeenCalledWith('/nodes/node1/qemu/100/agent/get-memory-block-info', {});
|
|
});
|
|
|
|
test('should not attempt QEMU guest agent memory fetch if agent is not enabled in current status', async () => {
|
|
const runningVms = [
|
|
{ endpointId: 'primary', node: 'node1', vmid: 101, type: 'qemu', name: 'vm-agent-off', agent: '1'} // Configured as on, but status says off
|
|
];
|
|
mockPveClientInstance.get
|
|
.mockResolvedValueOnce({ data: { data: [{ time: 1, cpu: 0.5 }] } }) // RRD
|
|
.mockResolvedValueOnce({ data: { data: { cpu: 0.5, mem: 1024, disk: 2048, agent: 0 } } }); // Current status (agent OFF)
|
|
mockPveClientInstance.post = jest.fn(); // Ensure post is a mock
|
|
|
|
const result = await fetchMetricsData(runningVms, [], mockCurrentApiClients);
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0].current.guest_mem_total_bytes).toBeUndefined();
|
|
expect(mockPveClientInstance.post).not.toHaveBeenCalled();
|
|
});
|
|
|
|
test('should not attempt QEMU guest agent memory fetch if guest agent config is missing/off', async () => {
|
|
const runningVms = [
|
|
{ endpointId: 'primary', node: 'node1', vmid: 102, type: 'qemu', name: 'vm-agent-not-configured' } // No agent field
|
|
];
|
|
mockPveClientInstance.get
|
|
.mockResolvedValueOnce({ data: { data: [{ time: 1, cpu: 0.5 }] } }) // RRD
|
|
.mockResolvedValueOnce({ data: { data: { cpu: 0.5, mem: 1024, disk: 2048, agent: 1 } } }); // Current status (agent ON)
|
|
mockPveClientInstance.post = jest.fn();
|
|
|
|
const result = await fetchMetricsData(runningVms, [], mockCurrentApiClients);
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0].current.guest_mem_total_bytes).toBeUndefined();
|
|
expect(mockPveClientInstance.post).not.toHaveBeenCalled();
|
|
});
|
|
|
|
test('should handle QEMU guest agent error (e.g., agent not responsive)', async () => {
|
|
const runningVms = [
|
|
{ endpointId: 'primary', node: 'node1', vmid: 103, type: 'qemu', name: 'vm-agent-error', agent: 'enabled=1' }
|
|
];
|
|
mockPveClientInstance.get
|
|
.mockResolvedValueOnce({ data: { data: [{ time: 1, cpu: 0.5 }] } }) // RRD
|
|
.mockResolvedValueOnce({ data: { data: { cpu: 0.5, mem: 1024, disk: 2048, agent: 1 } } }); // Current status
|
|
|
|
const agentError = new Error('Agent not responsive');
|
|
agentError.response = { status: 500, data: { data: { exitcode: -2 } } };
|
|
mockPveClientInstance.post = jest.fn().mockRejectedValueOnce(agentError);
|
|
const result = await fetchMetricsData(runningVms, [], mockCurrentApiClients);
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0].current.guest_mem_total_bytes).toBeUndefined();
|
|
});
|
|
|
|
test('should handle unexpected QEMU guest agent response format', async () => {
|
|
const runningVms = [
|
|
{ endpointId: 'primary', node: 'node1', vmid: 104, type: 'qemu', name: 'vm-agent-bad-format', agent: '1' }
|
|
];
|
|
mockPveClientInstance.get
|
|
.mockResolvedValueOnce({ data: { data: [{ time: 1, cpu: 0.5 }] } }) // RRD
|
|
.mockResolvedValueOnce({ data: { data: { cpu: 0.5, mem: 1024, disk: 2048, agent: 1 } } }); // Current status
|
|
mockPveClientInstance.post = jest.fn().mockResolvedValueOnce({ data: { data: { result: { unexpected: "data" } } } });
|
|
|
|
const result = await fetchMetricsData(runningVms, [], mockCurrentApiClients);
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0].current.guest_mem_total_bytes).toBeUndefined();
|
|
});
|
|
|
|
test('should handle generic error fetching QEMU guest agent memory info', async () => {
|
|
const runningVms = [
|
|
{ endpointId: 'primary', node: 'node1', vmid: 105, type: 'qemu', name: 'vm-agent-generic-error', agent: '1' }
|
|
];
|
|
mockPveClientInstance.get
|
|
.mockResolvedValueOnce({ data: { data: [{ time: 1, cpu: 0.5 }] } }) // RRD
|
|
.mockResolvedValueOnce({ data: { data: { cpu: 0.5, mem: 1024, disk: 2048, agent: 1 } } }); // Current status
|
|
|
|
const genericAgentError = new Error('Network Failure');
|
|
genericAgentError.response = { status: 503 }; // Simulate a non-500 error
|
|
mockPveClientInstance.post = jest.fn().mockRejectedValueOnce(genericAgentError);
|
|
const result = await fetchMetricsData(runningVms, [], mockCurrentApiClients);
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0].current.guest_mem_total_bytes).toBeUndefined();
|
|
});
|
|
|
|
test('should handle generic error fetching RRD/status data', async () => {
|
|
const runningVms = [
|
|
{ endpointId: 'primary', node: 'node1', vmid: 106, type: 'qemu', name: 'vm-generic-rrd-error' }
|
|
];
|
|
const genericError = new Error('Server Unavailable');
|
|
genericError.response = { status: 503 }; // Simulate non-400 error
|
|
|
|
// Mock RRD call to fail with generic error, current status call to succeed
|
|
mockPveClientInstance.get
|
|
.mockRejectedValueOnce(genericError) // RRD fails
|
|
.mockResolvedValueOnce({ data: { data: { cpu: 0.1 } } }); // Current status succeeds
|
|
|
|
const result = await fetchMetricsData(runningVms, [], mockCurrentApiClients);
|
|
|
|
expect(result).toHaveLength(0);
|
|
expect(mockPveClientInstance.get).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
test('should calculate actual used memory using fallback when "available" is missing', async () => {
|
|
const runningVms = [
|
|
{ endpointId: 'primary', node: 'node1', vmid: 107, type: 'qemu', name: 'vm-agent-fallback-mem', agent: '1' }
|
|
];
|
|
const totalMem = 4096 * 1024 * 1024;
|
|
const freeMem = 1024 * 1024 * 1024;
|
|
const cachedMem = 512 * 1024 * 1024;
|
|
const buffersMem = 256 * 1024 * 1024;
|
|
const expectedUsed = totalMem - freeMem - cachedMem - buffersMem;
|
|
|
|
mockPveClientInstance.get
|
|
.mockResolvedValueOnce({ data: { data: [{ time: 1, cpu: 0.5 }] } }) // RRD
|
|
.mockResolvedValueOnce({ data: { data: { cpu: 0.5, mem: totalMem, disk: 2048, agent: 1 } } }); // Current status
|
|
|
|
mockPveClientInstance.post = jest.fn().mockResolvedValueOnce({
|
|
data: {
|
|
data: {
|
|
// Agent response *without* 'available' field
|
|
result: { total: totalMem, free: freeMem, cached: cachedMem, buffers: buffersMem }
|
|
}
|
|
}
|
|
});
|
|
|
|
const result = await fetchMetricsData(runningVms, [], mockCurrentApiClients);
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0].current).toBeDefined();
|
|
expect(result[0].current.guest_mem_total_bytes).toBe(totalMem);
|
|
expect(result[0].current.guest_mem_free_bytes).toBe(freeMem);
|
|
expect(result[0].current.guest_mem_cached_bytes).toBe(cachedMem);
|
|
expect(result[0].current.guest_mem_buffers_bytes).toBe(buffersMem);
|
|
expect(result[0].current.guest_mem_available_bytes).toBeUndefined(); // Ensure 'available' was indeed missing
|
|
expect(result[0].current.guest_mem_actual_used_bytes).toBe(expectedUsed); // Check fallback calculation
|
|
});
|
|
|
|
test('should calculate actual used memory using final fallback (total - free) when other fields missing', async () => {
|
|
const runningVms = [
|
|
{ endpointId: 'primary', node: 'node1', vmid: 108, type: 'qemu', name: 'vm-agent-final-fallback', agent: '1' }
|
|
];
|
|
const totalMem = 2048 * 1024 * 1024;
|
|
const freeMem = 512 * 1024 * 1024;
|
|
const expectedUsed = totalMem - freeMem;
|
|
|
|
mockPveClientInstance.get
|
|
.mockResolvedValueOnce({ data: { data: [{ time: 1, cpu: 0.5 }] } }) // RRD
|
|
.mockResolvedValueOnce({ data: { data: { cpu: 0.5, mem: totalMem, disk: 2048, agent: 1 } } }); // Current status
|
|
|
|
mockPveClientInstance.post = jest.fn().mockResolvedValueOnce({
|
|
data: {
|
|
data: {
|
|
// Agent response *only* with total and free
|
|
result: { total: totalMem, free: freeMem }
|
|
}
|
|
}
|
|
});
|
|
|
|
const result = await fetchMetricsData(runningVms, [], mockCurrentApiClients);
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0].current).toBeDefined();
|
|
expect(result[0].current.guest_mem_total_bytes).toBe(totalMem);
|
|
expect(result[0].current.guest_mem_free_bytes).toBe(freeMem);
|
|
expect(result[0].current.guest_mem_available_bytes).toBeUndefined();
|
|
expect(result[0].current.guest_mem_cached_bytes).toBeUndefined();
|
|
expect(result[0].current.guest_mem_buffers_bytes).toBeUndefined();
|
|
expect(result[0].current.guest_mem_actual_used_bytes).toBe(expectedUsed); // Check final fallback calculation
|
|
});
|
|
|
|
|
|
}); // End describe fetchMetricsData
|
|
|
|
// --- NEW: describe block for fetchPbsData ---
|
|
describe('fetchPbsData', () => {
|
|
// Relies on mockPbsApiClient and mockPbsClientInstance from outer describe
|
|
|
|
beforeEach(() => {
|
|
// Ensure the default mocks are reset/cleared if needed for PBS specific tests
|
|
// Typically mockPbsClientInstance.get.mockClear() is sufficient if reusing the instance
|
|
mockPbsClientInstance.get.mockClear();
|
|
|
|
// Reset the default PBS mock to an empty object for clarity
|
|
mockPbsApiClient = {};
|
|
// Override the initializer mock if tests need specific PBS clients setup via initializeApiClients
|
|
// Otherwise, tests will construct and pass mock PBS clients directly
|
|
});
|
|
|
|
test('should return empty array when no PBS clients are provided', async () => {
|
|
const result = await fetchPbsData({});
|
|
expect(result).toEqual([]);
|
|
});
|
|
|
|
test('should fetch PBS data successfully', async () => {
|
|
const mockClients = {
|
|
'pbs-1': {
|
|
client: {
|
|
get: jest.fn()
|
|
.mockResolvedValueOnce({ data: { data: [{ node: 'pbs-node' }] } })
|
|
.mockResolvedValue({ data: { data: [] } })
|
|
},
|
|
config: { name: 'PBS Instance' }
|
|
}
|
|
};
|
|
|
|
const result = await fetchPbsData(mockClients);
|
|
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0]).toHaveProperty('pbsEndpointId');
|
|
expect(result[0]).toHaveProperty('pbsInstanceName');
|
|
expect(result[0]).toHaveProperty('status');
|
|
});
|
|
|
|
test('should handle error fetching PBS node name (and skip subsequent calls)', async () => {
|
|
const pbsId = 'pbs-err-node';
|
|
const mockPbsClient = { get: jest.fn() };
|
|
const mockClients = { [pbsId]: { client: mockPbsClient, config: { name: 'PBS Node Err' } } };
|
|
const nodeError = new Error('Node fetch failed');
|
|
|
|
// Mock /nodes to fail
|
|
mockPbsClient.get.mockRejectedValueOnce(nodeError);
|
|
|
|
const result = await fetchPbsData(mockClients);
|
|
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0].status).toBe('error');
|
|
expect(result[0].datastores).toBeUndefined();
|
|
expect(result[0].backupTasks).toBeUndefined();
|
|
expect(mockPbsClient.get).toHaveBeenCalledTimes(1);
|
|
expect(mockPbsClient.get).toHaveBeenCalledWith('/nodes');
|
|
});
|
|
|
|
test('should handle error fetching PBS datastores', async () => {
|
|
// Arrange
|
|
const pbsId = 'pbs-err-ds';
|
|
const pbsNodeName = 'pbs-node-ds-err';
|
|
const mockPbsClient = { get: jest.fn() };
|
|
const mockClients = { [pbsId]: { client: mockPbsClient, config: { name: 'PBS DS Err' } } };
|
|
const dsError = new Error('Datastore fetch failed');
|
|
|
|
mockPbsClient.get
|
|
.mockResolvedValueOnce({ data: { data: [{ node: pbsNodeName }] } }) // /nodes (succeeds)
|
|
.mockRejectedValueOnce(dsError) // /status/datastore-usage (fails)
|
|
.mockResolvedValueOnce({ data: { data: [] } }) // Mock fallback /config/datastore call (returns empty)
|
|
.mockRejectedValueOnce(new Error('Dedup fetch failed')) // /status/datastore-usage in fetchAllPbsTasksForProcessing (fails)
|
|
.mockResolvedValueOnce({ data: { data: [] } }) // /config/datastore in fetchAllPbsTasksForProcessing (empty)
|
|
.mockResolvedValueOnce({ data: { data: [] } }); // /nodes/{node}/tasks (empty tasks)
|
|
|
|
const result = await fetchPbsData(mockClients);
|
|
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0]).toMatchObject({
|
|
pbsEndpointId: pbsId,
|
|
status: 'ok',
|
|
nodeName: pbsNodeName,
|
|
datastores: [],
|
|
});
|
|
expect(mockPbsClient.get).toHaveBeenCalledTimes(6);
|
|
});
|
|
|
|
test('should handle partial datastore failures', async () => {
|
|
const mockClients = {
|
|
'pbs-1': {
|
|
client: {
|
|
get: jest.fn()
|
|
.mockResolvedValueOnce({ data: { data: [{ node: 'node1' }] } })
|
|
.mockResolvedValueOnce({ data: { data: [{ store: 'ds1' }] } })
|
|
.mockRejectedValueOnce(new Error('Snapshot fetch failed'))
|
|
},
|
|
config: { name: 'PBS 1' }
|
|
}
|
|
};
|
|
|
|
const result = await fetchPbsData(mockClients);
|
|
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0].status).toBe('ok');
|
|
});
|
|
|
|
test('should handle PBS task fetch failures', async () => {
|
|
const mockClients = {
|
|
'pbs-1': {
|
|
client: {
|
|
get: jest.fn()
|
|
.mockResolvedValueOnce({ data: { data: [{ node: 'pbs-node' }] } })
|
|
.mockRejectedValue(new Error('Task fetch failed'))
|
|
},
|
|
config: { name: 'PBS Instance' }
|
|
}
|
|
};
|
|
|
|
const result = await fetchPbsData(mockClients);
|
|
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0]).toHaveProperty('status');
|
|
});
|
|
|
|
test('should handle multiple PBS instances with mixed results', async () => {
|
|
const mockClients = {
|
|
'pbs-1': {
|
|
client: {
|
|
get: jest.fn()
|
|
.mockResolvedValueOnce({ data: { data: [{ node: 'node1' }] } })
|
|
.mockResolvedValue({ data: { data: [] } })
|
|
},
|
|
config: { name: 'PBS 1' }
|
|
},
|
|
'pbs-2': {
|
|
client: {
|
|
get: jest.fn().mockRejectedValue(new Error('Connection failed'))
|
|
},
|
|
config: { name: 'PBS 2' }
|
|
}
|
|
};
|
|
|
|
const result = await fetchPbsData(mockClients);
|
|
|
|
expect(result).toHaveLength(2);
|
|
expect(result.some(r => r.status === 'ok')).toBe(true);
|
|
expect(result.some(r => r.status === 'error')).toBe(true);
|
|
});
|
|
|
|
test('should return error status and log warnings if /nodes response is invalid (e.g. empty array)', async () => {
|
|
// Arrange
|
|
const pbsId = 'pbs-bad-nodes';
|
|
const mockPbsBadNodesClient = { get: jest.fn() };
|
|
const mockPbsBadNodesApiClients = {
|
|
[pbsId]: { client: mockPbsBadNodesClient, config: { id: pbsId, name: 'PBS Bad Nodes' } }
|
|
};
|
|
mockPbsBadNodesClient.get.mockResolvedValueOnce({ data: { data: [] } });
|
|
|
|
const result = await fetchPbsData(mockPbsBadNodesApiClients);
|
|
|
|
expect(mockPbsBadNodesClient.get).toHaveBeenCalledTimes(1);
|
|
expect(mockPbsBadNodesClient.get).toHaveBeenCalledWith('/nodes');
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0]).toMatchObject({
|
|
pbsEndpointId: pbsId,
|
|
pbsInstanceName: 'PBS Bad Nodes',
|
|
status: 'error'
|
|
});
|
|
});
|
|
|
|
test('should handle empty datastore usage with fallback', async () => {
|
|
const mockClients = {
|
|
'pbs-1': {
|
|
client: {
|
|
get: jest.fn()
|
|
.mockResolvedValueOnce({ data: { data: [{ node: 'pbs-node' }] } })
|
|
.mockResolvedValueOnce({ data: { data: [] } })
|
|
.mockResolvedValue({ data: { data: [] } })
|
|
},
|
|
config: { name: 'PBS Instance' }
|
|
}
|
|
};
|
|
|
|
const result = await fetchPbsData(mockClients);
|
|
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0]).toHaveProperty('status');
|
|
});
|
|
|
|
test('should handle error fetching datastore usage', async () => {
|
|
// Arrange
|
|
const pbsId = 'pbs-err-ds';
|
|
const pbsNodeName = 'pbs-node-ds-err';
|
|
const mockPbsClient = { get: jest.fn() };
|
|
const mockClients = { [pbsId]: { client: mockPbsClient, config: { name: 'PBS DS Err' } } };
|
|
const dsError = new Error('Datastore fetch failed');
|
|
|
|
mockPbsClient.get
|
|
.mockResolvedValueOnce({ data: { data: [{ node: pbsNodeName }] } }) // /nodes (succeeds)
|
|
.mockRejectedValueOnce(dsError) // /status/datastore-usage (fails)
|
|
.mockResolvedValueOnce({ data: { data: [] }}) // Mock fallback /config/datastore call (returns empty)
|
|
.mockRejectedValueOnce(new Error('Dedup fetch failed')) // /status/datastore-usage in fetchAllPbsTasksForProcessing (fails)
|
|
.mockResolvedValueOnce({ data: { data: [] }}) // /config/datastore in fetchAllPbsTasksForProcessing (empty)
|
|
.mockResolvedValueOnce({ data: { data: [] }}); // /nodes/{node}/tasks (empty tasks)
|
|
|
|
const result = await fetchPbsData(mockClients);
|
|
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0]).toMatchObject({
|
|
pbsEndpointId: pbsId,
|
|
status: 'ok',
|
|
nodeName: pbsNodeName,
|
|
datastores: [],
|
|
});
|
|
expect(mockPbsClient.get).toHaveBeenCalledTimes(6);
|
|
});
|
|
|
|
test('should handle failure of both datastore usage and config fetch', async () => {
|
|
// Arrange
|
|
const pbsId = 'pbs-double-ds-fail';
|
|
const pbsNodeName = 'pbs-node-double-fail';
|
|
const mockPbsClient = { get: jest.fn() };
|
|
const mockClients = { [pbsId]: { client: mockPbsClient, config: { name: 'PBS Double DS Fail' } } };
|
|
const usageError = new Error('Usage API Failed');
|
|
const configError = new Error('Config API Failed');
|
|
|
|
mockPbsClient.get
|
|
.mockResolvedValueOnce({ data: { data: [{ node: pbsNodeName }] } })
|
|
.mockRejectedValueOnce(usageError)
|
|
.mockRejectedValueOnce(configError)
|
|
.mockRejectedValueOnce(new Error('Dedup fetch failed'))
|
|
.mockResolvedValueOnce({ data: { data: [] } })
|
|
.mockResolvedValueOnce({ data: { data: [{ upid: 'task1' }] } });
|
|
|
|
const result = await fetchPbsData(mockClients);
|
|
|
|
expect(mockPbsClient.get).toHaveBeenCalledWith('/status/datastore-usage');
|
|
expect(mockPbsClient.get).toHaveBeenCalledWith('/config/datastore');
|
|
expect(mockPbsClient.get).toHaveBeenCalledTimes(6);
|
|
const callsDoubleFailTest = mockPbsClient.get.mock.calls;
|
|
expect(callsDoubleFailTest[5][0]).toBe(`/nodes/${pbsNodeName}/tasks`);
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0].status).toBe('ok');
|
|
expect(result[0].datastores).toEqual([]);
|
|
});
|
|
|
|
test('should handle API error when fetching PBS tasks', async () => {
|
|
const pbsId = 'pbs-task-fetch-error';
|
|
const pbsNodeName = 'pbs-node-task-error';
|
|
const datastoreName = 'store-task-error';
|
|
const mockPbsClient = { get: jest.fn() };
|
|
const mockClients = { [pbsId]: { client: mockPbsClient, config: { name: 'PBS Task Fetch Error' } } };
|
|
const taskError = new Error('Simulated task fetch error');
|
|
|
|
mockPbsClient.get
|
|
.mockResolvedValueOnce({ data: { data: [{ node: pbsNodeName }] } }) // /nodes (succeeds)
|
|
.mockResolvedValueOnce({ data: { data: [{ store: datastoreName, total: 1, used: 0 }] } }) // /status/datastore-usage (succeeds)
|
|
.mockResolvedValueOnce({ data: { data: [] } }) // Snapshots (succeeds empty)
|
|
.mockResolvedValueOnce({ data: { data: [{ name: datastoreName }] } }) // /config/datastore in fetchAllPbsTasksForProcessing
|
|
.mockResolvedValueOnce({ data: { data: [] } }) // /admin/datastore/{store}/groups (empty)
|
|
.mockRejectedValueOnce(taskError); // /nodes/{node}/tasks (FAILS)
|
|
|
|
const result = await fetchPbsData(mockClients);
|
|
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0]).toMatchObject({
|
|
pbsEndpointId: pbsId,
|
|
pbsInstanceName: 'PBS Task Fetch Error',
|
|
status: 'ok',
|
|
nodeName: pbsNodeName,
|
|
datastores: [{ name: datastoreName, total: 1, used: 0, available: undefined, gcStatus: 'unknown' , snapshots: []}],
|
|
});
|
|
expect(result[0]).toHaveProperty('backupTasks');
|
|
expect(result[0]).toHaveProperty('verifyTasks');
|
|
expect(result[0]).toHaveProperty('gcTasks');
|
|
expect(mockPbsClient.get).toHaveBeenCalledTimes(6);
|
|
expect(mockPbsClient.get).toHaveBeenCalledWith(`/nodes/${pbsNodeName}/tasks`, expect.any(Object));
|
|
});
|
|
|
|
}); // End describe fetchPbsData
|
|
|
|
}); // End describe Data Fetcher
|