test: build comprehensive test suite reaching 80%+ coverage threshold
Add 22 test files (~700 tests) covering security-critical modules, core infrastructure, API routes, and error handling. Final coverage: 86.73% statements / 80.57% branches / 85.57% functions / 87.42% lines, all above the 80% threshold enforced by jest.config.js. Highlights: - Unit tests for crypto-utils, credential-manager, auth-manager, csrf, input-validator, state-manager, health-checker, backup-manager, update-manager, resource-monitor, app-templates, platform-paths, port-lock-manager, errors, error-handler, pagination, url-resolver - Route tests for health, services, and containers (supertest + mocked deps) - Shared test-utils helper for mock factories and Express app builder - npm scripts for CI: test:ci, test:unit, test:routes, test:security, test:changed, test:debug - jest.config.js: expand coverage targets, add 80% threshold gate - routes/services.js: import ValidationError and NotFoundError from errors - .gitignore: exclude coverage/, *.bak, *.log Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
537
dashcaddy-api/__tests__/routes/containers.routes.test.js
Normal file
537
dashcaddy-api/__tests__/routes/containers.routes.test.js
Normal file
@@ -0,0 +1,537 @@
|
||||
// Container Routes Tests
|
||||
// Validates container lifecycle operations (start/stop/restart/update/delete/discover)
|
||||
|
||||
const express = require('express');
|
||||
const request = require('supertest');
|
||||
|
||||
// Build a test app with the containers route
|
||||
function buildApp(mockDeps) {
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
|
||||
const { errorMiddleware } = require('../../error-handler');
|
||||
const containersRouteFactory = require('../../routes/containers');
|
||||
app.use('/api/containers', containersRouteFactory(mockDeps));
|
||||
app.use(errorMiddleware);
|
||||
return app;
|
||||
}
|
||||
|
||||
// Mock container factory
|
||||
function mockContainer(overrides = {}) {
|
||||
return {
|
||||
inspect: jest.fn().mockResolvedValue({
|
||||
Id: 'abc123def456',
|
||||
Name: '/plex',
|
||||
Config: {
|
||||
Image: 'lscr.io/linuxserver/plex:latest',
|
||||
Env: ['TZ=America/New_York', 'PLEX_CLAIM='],
|
||||
ExposedPorts: { '32400/tcp': {} },
|
||||
Labels: { 'sami.managed': 'true', 'sami.app': 'plex', 'sami.subdomain': 'plex' }
|
||||
},
|
||||
Image: 'sha256:abc123',
|
||||
HostConfig: {
|
||||
Binds: ['E:/dockerdata/plex:/config'],
|
||||
PortBindings: { '32400/tcp': [{ HostPort: '32400' }] },
|
||||
RestartPolicy: { Name: 'unless-stopped' },
|
||||
NetworkMode: 'bridge',
|
||||
ExtraHosts: [],
|
||||
Privileged: false,
|
||||
CapAdd: null,
|
||||
CapDrop: null,
|
||||
Devices: [],
|
||||
LogConfig: { Type: 'json-file', Config: { 'max-size': '10m', 'max-file': '3' } },
|
||||
Memory: 2147483648, // 2GB
|
||||
MemoryReservation: 1073741824, // 1GB
|
||||
NanoCpus: 2000000000, // 2 cores
|
||||
},
|
||||
NetworkSettings: { Networks: { bridge: {} } }
|
||||
}),
|
||||
start: jest.fn().mockResolvedValue(),
|
||||
stop: jest.fn().mockResolvedValue(),
|
||||
restart: jest.fn().mockResolvedValue(),
|
||||
remove: jest.fn().mockResolvedValue(),
|
||||
update: jest.fn().mockResolvedValue(),
|
||||
logs: jest.fn().mockResolvedValue(Buffer.from('2026-04-05T10:00:00Z Plex server started')),
|
||||
...overrides
|
||||
};
|
||||
}
|
||||
|
||||
function createMockDeps(containerInstance) {
|
||||
const container = containerInstance || mockContainer();
|
||||
|
||||
return {
|
||||
docker: {
|
||||
client: {
|
||||
getContainer: jest.fn().mockReturnValue(container),
|
||||
createContainer: jest.fn().mockResolvedValue({
|
||||
start: jest.fn().mockResolvedValue(),
|
||||
inspect: jest.fn().mockResolvedValue({ Id: 'new123' }),
|
||||
remove: jest.fn().mockResolvedValue(),
|
||||
}),
|
||||
getImage: jest.fn().mockReturnValue({
|
||||
inspect: jest.fn().mockResolvedValue({ RepoDigests: ['sha256:olddigest'] })
|
||||
}),
|
||||
listContainers: jest.fn().mockResolvedValue([]),
|
||||
pruneImages: jest.fn().mockResolvedValue({ SpaceReclaimed: 0 }),
|
||||
},
|
||||
pull: jest.fn().mockResolvedValue([]),
|
||||
},
|
||||
log: {
|
||||
info: jest.fn(),
|
||||
error: jest.fn(),
|
||||
debug: jest.fn(),
|
||||
},
|
||||
asyncHandler: (fn, name) => async (req, res, next) => {
|
||||
try { await fn(req, res, next); } catch (err) { next(err); }
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe('Container Routes — DashCaddy container lifecycle', () => {
|
||||
|
||||
describe('POST /:id/start', () => {
|
||||
it('starts a stopped container', async () => {
|
||||
const deps = createMockDeps();
|
||||
const app = buildApp(deps);
|
||||
const res = await request(app).post('/api/containers/abc123/start');
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body.success).toBe(true);
|
||||
expect(res.body.message).toContain('started');
|
||||
});
|
||||
|
||||
it('returns 404 for missing container', async () => {
|
||||
const container = mockContainer();
|
||||
const notFound = new Error('no such container');
|
||||
notFound.statusCode = 404;
|
||||
container.inspect.mockRejectedValue(notFound);
|
||||
const deps = createMockDeps(container);
|
||||
const app = buildApp(deps);
|
||||
|
||||
const res = await request(app).post('/api/containers/missing123/start');
|
||||
expect(res.status).toBe(404);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /:id/stop', () => {
|
||||
it('stops a running container', async () => {
|
||||
const deps = createMockDeps();
|
||||
const app = buildApp(deps);
|
||||
const res = await request(app).post('/api/containers/abc123/stop');
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body.message).toContain('stopped');
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /:id/restart', () => {
|
||||
it('restarts a container', async () => {
|
||||
const deps = createMockDeps();
|
||||
const app = buildApp(deps);
|
||||
const res = await request(app).post('/api/containers/abc123/restart');
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body.message).toContain('restarted');
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /:id/logs', () => {
|
||||
it('returns last 100 log lines', async () => {
|
||||
const deps = createMockDeps();
|
||||
const app = buildApp(deps);
|
||||
const res = await request(app).get('/api/containers/abc123/logs');
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body.logs).toContain('Plex server started');
|
||||
});
|
||||
});
|
||||
|
||||
describe('PUT /:id/resources', () => {
|
||||
it('updates memory and CPU limits', async () => {
|
||||
const container = mockContainer();
|
||||
const deps = createMockDeps(container);
|
||||
const app = buildApp(deps);
|
||||
|
||||
const res = await request(app)
|
||||
.put('/api/containers/abc123/resources')
|
||||
.send({ memory: 4096, cpus: 4 });
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(container.update).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
Memory: 4096 * 1024 * 1024,
|
||||
NanoCpus: 4 * 1e9,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('sets 0 for unlimited', async () => {
|
||||
const container = mockContainer();
|
||||
const deps = createMockDeps(container);
|
||||
const app = buildApp(deps);
|
||||
|
||||
const res = await request(app)
|
||||
.put('/api/containers/abc123/resources')
|
||||
.send({ memory: 0, cpus: 0 });
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(container.update).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
Memory: 0,
|
||||
NanoCpus: 0,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /:id/resources', () => {
|
||||
it('returns current resource limits in human units', async () => {
|
||||
const deps = createMockDeps();
|
||||
const app = buildApp(deps);
|
||||
|
||||
const res = await request(app).get('/api/containers/abc123/resources');
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body.memory).toBe(2048); // 2GB in MB
|
||||
expect(res.body.memoryReservation).toBe(1024); // 1GB in MB
|
||||
expect(res.body.cpus).toBe(2); // 2 cores
|
||||
});
|
||||
});
|
||||
|
||||
describe('DELETE /:id', () => {
|
||||
it('force-removes a container', async () => {
|
||||
const container = mockContainer();
|
||||
const deps = createMockDeps(container);
|
||||
const app = buildApp(deps);
|
||||
|
||||
const res = await request(app).delete('/api/containers/abc123');
|
||||
expect(res.status).toBe(200);
|
||||
expect(container.remove).toHaveBeenCalledWith({ force: true });
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /discover', () => {
|
||||
it('returns only sami.managed containers', async () => {
|
||||
const deps = createMockDeps();
|
||||
deps.docker.client.listContainers.mockResolvedValue([
|
||||
{
|
||||
Id: 'abc123', Names: ['/plex'], Image: 'linuxserver/plex',
|
||||
State: 'running', Status: 'Up 3 days',
|
||||
Labels: { 'sami.managed': 'true', 'sami.app': 'plex', 'sami.subdomain': 'plex' },
|
||||
Ports: [{ PrivatePort: 32400, PublicPort: 32400 }]
|
||||
},
|
||||
{
|
||||
Id: 'xyz789', Names: ['/random-container'], Image: 'nginx',
|
||||
State: 'running', Status: 'Up 1 hour',
|
||||
Labels: {},
|
||||
Ports: [{ PrivatePort: 80, PublicPort: 80 }]
|
||||
}
|
||||
]);
|
||||
const app = buildApp(deps);
|
||||
|
||||
const res = await request(app).get('/api/containers/discover');
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body.containers).toHaveLength(1);
|
||||
expect(res.body.containers[0].appTemplate).toBe('plex');
|
||||
});
|
||||
|
||||
it('returns empty array when no managed containers', async () => {
|
||||
const deps = createMockDeps();
|
||||
const app = buildApp(deps);
|
||||
const res = await request(app).get('/api/containers/discover');
|
||||
expect(res.body.containers).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /:id/update — error and edge cases', () => {
|
||||
it('preserves custom network mode (non-bridge/host/none)', async () => {
|
||||
const container = mockContainer();
|
||||
container.inspect.mockResolvedValue({
|
||||
Id: 'abc123', Name: '/plex',
|
||||
Config: { Image: 'plex:latest', Env: [], ExposedPorts: {}, Labels: {} },
|
||||
Image: 'sha256:abc',
|
||||
HostConfig: {
|
||||
Binds: [], PortBindings: {}, RestartPolicy: { Name: 'unless-stopped' },
|
||||
NetworkMode: 'my-custom-network',
|
||||
ExtraHosts: [], Privileged: false, CapAdd: null, CapDrop: null, Devices: []
|
||||
},
|
||||
NetworkSettings: { Networks: { 'my-custom-network': { IPAddress: '172.20.0.5' } } }
|
||||
});
|
||||
const newContainer = {
|
||||
start: jest.fn().mockResolvedValue(),
|
||||
inspect: jest.fn().mockResolvedValue({ Id: 'new123' })
|
||||
};
|
||||
const deps = createMockDeps(container);
|
||||
deps.docker.client.createContainer.mockResolvedValue(newContainer);
|
||||
|
||||
const app = buildApp(deps);
|
||||
const res = await request(app).post('/api/containers/abc123/update');
|
||||
expect(res.status).toBe(200);
|
||||
|
||||
const createCall = deps.docker.client.createContainer.mock.calls[0][0];
|
||||
expect(createCall.NetworkingConfig.EndpointsConfig['my-custom-network'])
|
||||
.toEqual({ IPAddress: '172.20.0.5' });
|
||||
});
|
||||
|
||||
it('cleans up failed new container when start fails', async () => {
|
||||
const container = mockContainer();
|
||||
const newContainer = {
|
||||
start: jest.fn().mockRejectedValue(new Error('port already allocated')),
|
||||
remove: jest.fn().mockResolvedValue()
|
||||
};
|
||||
const deps = createMockDeps(container);
|
||||
deps.docker.client.createContainer.mockResolvedValue(newContainer);
|
||||
|
||||
const app = buildApp(deps);
|
||||
const res = await request(app).post('/api/containers/abc123/update');
|
||||
|
||||
expect(res.status).toBeGreaterThanOrEqual(500);
|
||||
expect(newContainer.remove).toHaveBeenCalledWith({ force: true });
|
||||
});
|
||||
|
||||
it('handles new container remove cleanup failure gracefully', async () => {
|
||||
const container = mockContainer();
|
||||
const newContainer = {
|
||||
start: jest.fn().mockRejectedValue(new Error('start failed')),
|
||||
remove: jest.fn().mockRejectedValue(new Error('already gone'))
|
||||
};
|
||||
const deps = createMockDeps(container);
|
||||
deps.docker.client.createContainer.mockResolvedValue(newContainer);
|
||||
|
||||
const app = buildApp(deps);
|
||||
const res = await request(app).post('/api/containers/abc123/update');
|
||||
expect(res.status).toBeGreaterThanOrEqual(500);
|
||||
});
|
||||
|
||||
it('logs space reclaimed when image prune frees disk', async () => {
|
||||
const container = mockContainer();
|
||||
const deps = createMockDeps(container);
|
||||
deps.docker.client.pruneImages.mockResolvedValue({ SpaceReclaimed: 50 * 1024 * 1024 }); // 50MB
|
||||
|
||||
const app = buildApp(deps);
|
||||
const res = await request(app).post('/api/containers/abc123/update');
|
||||
expect(res.status).toBe(200);
|
||||
expect(deps.log.info).toHaveBeenCalledWith(
|
||||
'docker',
|
||||
'Pruned dangling images after update',
|
||||
expect.objectContaining({ spaceReclaimed: '50MB' })
|
||||
);
|
||||
});
|
||||
|
||||
it('continues if image prune fails', async () => {
|
||||
const container = mockContainer();
|
||||
const deps = createMockDeps(container);
|
||||
deps.docker.client.pruneImages.mockRejectedValue(new Error('prune failed'));
|
||||
|
||||
const app = buildApp(deps);
|
||||
const res = await request(app).post('/api/containers/abc123/update');
|
||||
expect(res.status).toBe(200);
|
||||
expect(deps.log.debug).toHaveBeenCalledWith(
|
||||
'docker',
|
||||
'Image prune after update failed',
|
||||
expect.any(Object)
|
||||
);
|
||||
});
|
||||
|
||||
it('ignores already-stopped error when stopping container', async () => {
|
||||
const container = mockContainer();
|
||||
container.stop.mockRejectedValue(new Error('container already stopped'));
|
||||
const deps = createMockDeps(container);
|
||||
|
||||
const app = buildApp(deps);
|
||||
const res = await request(app).post('/api/containers/abc123/update');
|
||||
expect(res.status).toBe(200);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /:id/check-update', () => {
|
||||
it('reports no updates when local and new digests match', async () => {
|
||||
const container = mockContainer();
|
||||
const deps = createMockDeps(container);
|
||||
deps.docker.client.getImage.mockReturnValue({
|
||||
inspect: jest.fn().mockResolvedValue({ RepoDigests: ['sha256:samedigest'] })
|
||||
});
|
||||
deps.docker.pull.mockResolvedValue([]);
|
||||
|
||||
const app = buildApp(deps);
|
||||
const res = await request(app).get('/api/containers/abc123/check-update');
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body.updateAvailable).toBe(false);
|
||||
});
|
||||
|
||||
it('reports update available when downloads occur', async () => {
|
||||
const container = mockContainer();
|
||||
const deps = createMockDeps(container);
|
||||
deps.docker.pull.mockResolvedValue([
|
||||
{ status: 'Downloading', id: 'layer1' },
|
||||
{ status: 'Download complete', id: 'layer2' }
|
||||
]);
|
||||
|
||||
const app = buildApp(deps);
|
||||
const res = await request(app).get('/api/containers/abc123/check-update');
|
||||
expect(res.body.updateAvailable).toBe(true);
|
||||
});
|
||||
|
||||
it('reports update available when digests differ', async () => {
|
||||
const container = mockContainer();
|
||||
const deps = createMockDeps(container);
|
||||
let callCount = 0;
|
||||
deps.docker.client.getImage.mockImplementation(() => {
|
||||
callCount++;
|
||||
return {
|
||||
inspect: jest.fn().mockResolvedValue({
|
||||
RepoDigests: callCount === 1
|
||||
? ['sha256:olddigest']
|
||||
: ['sha256:newdigest']
|
||||
})
|
||||
};
|
||||
});
|
||||
deps.docker.pull.mockResolvedValue([]);
|
||||
|
||||
const app = buildApp(deps);
|
||||
const res = await request(app).get('/api/containers/abc123/check-update');
|
||||
expect(res.body.updateAvailable).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false when pull throws (registry unreachable)', async () => {
|
||||
const container = mockContainer();
|
||||
const deps = createMockDeps(container);
|
||||
deps.docker.pull.mockRejectedValue(new Error('registry timeout'));
|
||||
|
||||
const app = buildApp(deps);
|
||||
const res = await request(app).get('/api/containers/abc123/check-update');
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body.updateAvailable).toBe(false);
|
||||
});
|
||||
|
||||
it('handles missing local repo digests gracefully', async () => {
|
||||
const container = mockContainer();
|
||||
const deps = createMockDeps(container);
|
||||
deps.docker.client.getImage.mockReturnValue({
|
||||
inspect: jest.fn().mockResolvedValue({ RepoDigests: null })
|
||||
});
|
||||
|
||||
const app = buildApp(deps);
|
||||
const res = await request(app).get('/api/containers/abc123/check-update');
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body.currentDigest).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getVerifiedContainer error paths', () => {
|
||||
it('returns 404 when error message includes "no such container"', async () => {
|
||||
const container = mockContainer();
|
||||
container.inspect.mockRejectedValue(new Error('Error: no such container: missing'));
|
||||
const deps = createMockDeps(container);
|
||||
const app = buildApp(deps);
|
||||
|
||||
const res = await request(app).post('/api/containers/missing/start');
|
||||
expect(res.status).toBe(404);
|
||||
});
|
||||
|
||||
it('rethrows non-404 errors from inspect', async () => {
|
||||
const container = mockContainer();
|
||||
container.inspect.mockRejectedValue(new Error('docker daemon not running'));
|
||||
const deps = createMockDeps(container);
|
||||
const app = buildApp(deps);
|
||||
|
||||
const res = await request(app).post('/api/containers/abc123/start');
|
||||
expect(res.status).toBeGreaterThanOrEqual(500);
|
||||
});
|
||||
});
|
||||
|
||||
describe('PUT /:id/resources — partial updates', () => {
|
||||
it('updates only memory when cpus omitted', async () => {
|
||||
const container = mockContainer();
|
||||
const deps = createMockDeps(container);
|
||||
const app = buildApp(deps);
|
||||
|
||||
const res = await request(app)
|
||||
.put('/api/containers/abc123/resources')
|
||||
.send({ memory: 2048 });
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
const call = container.update.mock.calls[0][0];
|
||||
expect(call.Memory).toBe(2048 * 1024 * 1024);
|
||||
expect(call.NanoCpus).toBeUndefined();
|
||||
});
|
||||
|
||||
it('updates only cpus when memory omitted', async () => {
|
||||
const container = mockContainer();
|
||||
const deps = createMockDeps(container);
|
||||
const app = buildApp(deps);
|
||||
|
||||
const res = await request(app)
|
||||
.put('/api/containers/abc123/resources')
|
||||
.send({ cpus: 1.5 });
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
const call = container.update.mock.calls[0][0];
|
||||
expect(call.NanoCpus).toBe(1.5 * 1e9);
|
||||
expect(call.Memory).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /:id/resources — zero values', () => {
|
||||
it('returns 0 when no limits set', async () => {
|
||||
const container = mockContainer();
|
||||
container.inspect.mockResolvedValue({
|
||||
Id: 'abc', Name: '/test', Config: { Image: 'test:latest' },
|
||||
HostConfig: { Memory: 0, MemoryReservation: 0, NanoCpus: 0 }
|
||||
});
|
||||
const deps = createMockDeps(container);
|
||||
const app = buildApp(deps);
|
||||
|
||||
const res = await request(app).get('/api/containers/abc123/resources');
|
||||
expect(res.body.memory).toBe(0);
|
||||
expect(res.body.memoryReservation).toBe(0);
|
||||
expect(res.body.cpus).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /discover — pagination', () => {
|
||||
it('paginates results when paginate query params provided', async () => {
|
||||
const containers = Array.from({ length: 25 }, (_, i) => ({
|
||||
Id: `id${i}`,
|
||||
Names: [`/svc${i}`],
|
||||
Image: 'test:latest',
|
||||
State: 'running',
|
||||
Status: 'Up',
|
||||
Labels: { 'sami.managed': 'true', 'sami.app': 'test', 'sami.subdomain': `svc${i}` },
|
||||
Ports: []
|
||||
}));
|
||||
const deps = createMockDeps();
|
||||
deps.docker.client.listContainers.mockResolvedValue(containers);
|
||||
const app = buildApp(deps);
|
||||
|
||||
const res = await request(app).get('/api/containers/discover?page=1&limit=10');
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body.containers.length).toBeLessThanOrEqual(10);
|
||||
});
|
||||
});
|
||||
|
||||
describe('DashCaddy-specific scenarios', () => {
|
||||
it('Plex container: verifies correct resource read (2GB, 2 cores)', async () => {
|
||||
const deps = createMockDeps();
|
||||
const app = buildApp(deps);
|
||||
const res = await request(app).get('/api/containers/abc123/resources');
|
||||
expect(res.body.memory).toBe(2048);
|
||||
expect(res.body.cpus).toBe(2);
|
||||
});
|
||||
|
||||
it('container update: preserves Env, PortBindings, RestartPolicy', async () => {
|
||||
const container = mockContainer();
|
||||
const newContainer = {
|
||||
start: jest.fn().mockResolvedValue(),
|
||||
inspect: jest.fn().mockResolvedValue({ Id: 'new456' }),
|
||||
remove: jest.fn().mockResolvedValue(),
|
||||
};
|
||||
const deps = createMockDeps(container);
|
||||
deps.docker.client.createContainer.mockResolvedValue(newContainer);
|
||||
const app = buildApp(deps);
|
||||
|
||||
const res = await request(app).post('/api/containers/abc123/update');
|
||||
expect(res.status).toBe(200);
|
||||
|
||||
const createCall = deps.docker.client.createContainer.mock.calls[0][0];
|
||||
expect(createCall.Env).toContain('TZ=America/New_York');
|
||||
expect(createCall.HostConfig.PortBindings['32400/tcp']).toEqual([{ HostPort: '32400' }]);
|
||||
expect(createCall.HostConfig.RestartPolicy).toEqual({ Name: 'unless-stopped' });
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user