diff --git a/tests/CustomPortPersistence.test.ts b/tests/CustomPortPersistence.test.ts new file mode 100644 index 00000000..d0c72467 --- /dev/null +++ b/tests/CustomPortPersistence.test.ts @@ -0,0 +1,285 @@ +import AppsDataStore from '../src/datastore/AppsDataStore' +import DockerApi from '../src/docker/DockerApi' + +describe('custom port persistence', () => { + test('preserves protocol and publishMode when updating an app definition', async () => { + const appsDataStore = new AppsDataStore({} as any, 'captain') + const existingApp = { + description: '', + deployedVersion: 0, + notExposeAsWebApp: false, + hasPersistentData: false, + hasDefaultSubDomainSsl: false, + captainDefinitionRelativeFilePath: './captain-definition', + forceSsl: false, + websocketSupport: false, + instanceCount: 1, + networks: ['captain-overlay-network'], + customDomain: [], + ports: [], + volumes: [], + envVars: [], + versions: [], + } as any + + jest.spyOn(appsDataStore, 'getAppDefinition').mockResolvedValue( + existingApp + ) + const saveApp = jest + .fn() + .mockResolvedValue(undefined) + ;(appsDataStore as any).saveApp = saveApp + + const ports = [ + { + containerPort: 3000, + hostPort: 8000, + protocol: 'tcp' as const, + publishMode: 'host' as const, + }, + { + containerPort: 3001, + hostPort: 8001, + protocol: 'udp' as const, + publishMode: 'ingress' as const, + }, + { + containerPort: 3002, + hostPort: 8002, + }, + ] + + await appsDataStore.updateAppDefinitionInDb( + 'test-app', + undefined, + '', + 1, + './captain-definition', + [], + [], + [], + '', + false, + 80, + undefined, + false, + ports, + undefined as any, + {} as any, + '', + '', + '', + '', + false, + { enabled: false } + ) + + expect(saveApp).toHaveBeenCalledWith( + 'test-app', + expect.objectContaining({ + ports: [ + { + containerPort: 3000, + hostPort: 8000, + protocol: 'tcp', + publishMode: 'host', + }, + { + containerPort: 3001, + hostPort: 8001, + protocol: 'udp', + publishMode: 'ingress', + }, + { + containerPort: 3002, + hostPort: 8002, + }, + ], + }) + ) + }) + + test('creates Docker port mappings with protocol and publish mode intact', async () => { + const dockerApi = new DockerApi({} as any) + const createService = jest.fn().mockResolvedValue({}) + ;(dockerApi as any).dockerode = { createService } + + await dockerApi.createServiceOnNodeId( + 'nginx:latest', + 'test-service', + [ + { + containerPort: 3000, + hostPort: 8000, + protocol: 'tcp', + publishMode: 'host', + }, + { + containerPort: 3001, + hostPort: 8001, + protocol: 'udp', + publishMode: 'ingress', + }, + { + containerPort: 3002, + hostPort: 8002, + publishMode: 'host', + }, + { + containerPort: 3003, + hostPort: 8003, + protocol: 'tcp', + }, + ], + undefined, + undefined, + undefined + ) + + expect(createService).toHaveBeenCalledWith( + expect.objectContaining({ + EndpointSpec: { + Ports: [ + { + Protocol: 'tcp', + TargetPort: 3000, + PublishedPort: 8000, + PublishMode: 'host', + }, + { + Protocol: 'udp', + TargetPort: 3001, + PublishedPort: 8001, + PublishMode: 'ingress', + }, + { + Protocol: 'tcp', + TargetPort: 3002, + PublishedPort: 8002, + PublishMode: 'host', + }, + { + Protocol: 'udp', + TargetPort: 3002, + PublishedPort: 8002, + PublishMode: 'host', + }, + { + Protocol: 'tcp', + TargetPort: 3003, + PublishedPort: 8003, + }, + ], + }, + }) + ) + }) + + test('updates Docker port mappings with protocol and publish mode intact', async () => { + jest.useFakeTimers() + + const dockerApi = new DockerApi({} as any) + const update = jest.fn().mockResolvedValue({}) + const service = { + inspect: jest.fn().mockResolvedValue({ + Version: { Index: 1 }, + Spec: { + TaskTemplate: { + ContainerSpec: {}, + Placement: { Constraints: [] }, + }, + EndpointSpec: { Ports: [] }, + Mode: { Replicated: { Replicas: 1 } }, + Labels: {}, + }, + }), + update, + } + ;(dockerApi as any).dockerode = { + getService: jest.fn().mockReturnValue(service), + } + jest.spyOn(dockerApi, 'pruneContainers').mockResolvedValue(undefined) + + await dockerApi.updateService( + 'test-service', + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + [ + { + containerPort: 3000, + hostPort: 8000, + protocol: 'tcp', + publishMode: 'host', + }, + { + containerPort: 3001, + hostPort: 8001, + protocol: 'udp', + publishMode: 'ingress', + }, + { + containerPort: 3002, + hostPort: 8002, + publishMode: 'host', + }, + { + containerPort: 3003, + hostPort: 8003, + protocol: 'tcp', + }, + ], + undefined, + undefined, + undefined, + undefined + ) + + expect(update).toHaveBeenCalledWith( + expect.objectContaining({ + EndpointSpec: { + Ports: [ + { + Protocol: 'tcp', + TargetPort: 3000, + PublishedPort: 8000, + PublishMode: 'host', + }, + { + Protocol: 'udp', + TargetPort: 3001, + PublishedPort: 8001, + PublishMode: 'ingress', + }, + { + Protocol: 'tcp', + TargetPort: 3002, + PublishedPort: 8002, + PublishMode: 'host', + }, + { + Protocol: 'udp', + TargetPort: 3002, + PublishedPort: 8002, + PublishMode: 'host', + }, + { + Protocol: 'tcp', + TargetPort: 3003, + PublishedPort: 8003, + }, + ], + }, + }) + ) + + jest.runOnlyPendingTimers() + jest.useRealTimers() + }) +})