fix(resources): harden Resource Explorer with auth, validation, design, and UX fixes (#527)

- Sanitize error messages in all delete/prune/create/inspect endpoints
  to prevent Docker internals from leaking to the frontend
- Add CIDR, IPv4, and Docker resource ID input validation
- Add requirePaid gate to network topology endpoint
- Add invalidateNodeCaches after image/volume/network mutations
- Fix design system violations: card borders, destructive button variant,
  visible DialogDescription, overflow-auto replaced with ScrollArea,
  hardcoded Tailwind colors replaced with tokens
- Gate purge button behind isAdmin to prevent silent 403s
- Fix shared inspect loading state to be per-network-row
- Parse error response bodies for meaningful toast messages
- Add clipboard API fallback for non-HTTPS contexts
- Render Options section in network inspect sheet
- Add operational and diagnostic logging for resource operations
- Extend validation and DockerController test suites
- Update docs with Options field in network inspect
This commit is contained in:
Anso
2026-04-12 15:31:35 -04:00
committed by GitHub
parent a5cb316ca9
commit 4909c35e50
10 changed files with 303 additions and 52 deletions
@@ -555,3 +555,52 @@ describe('DockerController - inspectNetwork edge cases', () => {
await expect(dc.inspectNetwork('any-id')).rejects.toThrow('Cannot connect to Docker daemon');
});
});
// --- createNetwork validation --------------------------------------------------
describe('createNetwork', () => {
it('rejects empty network name', async () => {
const dc = DockerController.getInstance(1);
await expect(dc.createNetwork({ Name: '' })).rejects.toThrow('Invalid network name');
});
it('rejects names with invalid characters', async () => {
const dc = DockerController.getInstance(1);
await expect(dc.createNetwork({ Name: 'net work' })).rejects.toThrow('Invalid network name');
await expect(dc.createNetwork({ Name: '../escape' })).rejects.toThrow('Invalid network name');
await expect(dc.createNetwork({ Name: 'net;rm' })).rejects.toThrow('Invalid network name');
});
it('accepts valid network names and passes through to Docker', async () => {
mockDocker.createNetwork.mockResolvedValue({ id: 'new-net-id' });
const dc = DockerController.getInstance(1);
const result = await dc.createNetwork({ Name: 'my-network_v2' });
expect(result.id).toBe('new-net-id');
expect(mockDocker.createNetwork).toHaveBeenCalledWith({ Name: 'my-network_v2' });
});
});
// --- removeContainers mixed results -------------------------------------------
describe('removeContainers', () => {
it('returns mixed results when some removals fail', async () => {
mockDocker.getContainer.mockImplementation((id: string) => {
if (id === 'fail-id') {
return { remove: vi.fn().mockRejectedValue(new Error('no such container')) };
}
return { remove: vi.fn().mockResolvedValue(undefined) };
});
const dc = DockerController.getInstance(1);
const results = await dc.removeContainers(['ok-id-000000', 'fail-id']);
expect(results).toHaveLength(2);
expect(results[0]).toEqual({ id: 'ok-id-000000', success: true });
expect(results[1]).toMatchObject({ id: 'fail-id', success: false });
});
it('returns empty array for empty input', async () => {
const dc = DockerController.getInstance(1);
const results = await dc.removeContainers([]);
expect(results).toEqual([]);
});
});