blob: 95caa7232d61a335f3ba7fe85281705a3a524dc5 [file]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
// TLS integration tests for the Node.js SDK.
//
// These tests require a TLS-enabled Iggy server and are skipped by default.
// To run them locally:
//
// 1. Start the server with TLS:
// IGGY_ROOT_USERNAME=iggy IGGY_ROOT_PASSWORD=iggy \
// IGGY_TCP_TLS_ENABLED=true \
// IGGY_TCP_TLS_CERT_FILE=core/certs/iggy_cert.pem \
// IGGY_TCP_TLS_KEY_FILE=core/certs/iggy_key.pem \
// cargo r --bin iggy-server
//
// 2. Run the tests:
// cd foreign/node
// IGGY_TCP_TLS_ENABLED=true IGGY_TCP_ADDRESS=127.0.0.1:8090 \
// node --import @swc-node/register/esm-register --test src/e2e/tls.system.e2e.ts
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { after, describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { Client } from '../client/client.js';
import type { TlsOption } from '../client/client.type.js';
import { Partitioning, Consumer, PollingStrategy } from '../wire/index.js';
import { getIggyAddress } from '../tcp.sm.utils.js';
const tlsEnabled = process.env.IGGY_TCP_TLS_ENABLED === 'true';
// Path to the CA certificate. Override with E2E_ROOT_CA_CERT env var,
// otherwise fall back to the default relative path from the repo root.
const caCertPath = process.env.E2E_ROOT_CA_CERT
|| resolve(process.cwd(), '../../core/certs/iggy_ca_cert.pem');
const getTlsClient = () => {
const [, port] = getIggyAddress();
const caCert = readFileSync(caCertPath);
// The server certificate SAN is DNS:localhost, so we connect via 'localhost'
// for proper hostname verification (consistent with Python and C# TLS tests).
return new Client({
transport: 'TLS',
options: {
port,
host: 'localhost',
ca: caCert,
},
credentials: { username: 'iggy', password: 'iggy' },
});
};
describe('e2e -> tls', { skip: !tlsEnabled && 'IGGY_TCP_TLS_ENABLED is not set' }, async () => {
const c = getTlsClient();
const credentials = { username: 'iggy', password: 'iggy' };
it('e2e -> tls::ping', async () => {
assert.ok(await c.system.ping());
});
it('e2e -> tls::login', async () => {
assert.deepEqual(
await c.session.login(credentials),
{ userId: 0 }
);
});
it('e2e -> tls::getStats', async () => {
const stats = await c.system.getStats();
assert.ok(stats);
assert.ok('processId' in stats);
assert.ok('hostname' in stats);
});
it('e2e -> tls::send and poll messages', async () => {
const streamName = 'tls-e2e-stream';
const topicName = 'tls-e2e-topic';
await c.stream.create({ name: streamName });
await c.topic.create({
streamId: streamName,
name: topicName,
partitionCount: 1,
compressionAlgorithm: 1,
});
const messages = [
{ id: 1, headers: [], payload: 'tls-message-1' },
{ id: 2, headers: [], payload: 'tls-message-2' },
{ id: 3, headers: [], payload: 'tls-message-3' },
];
await c.message.send({
streamId: streamName,
topicId: topicName,
messages,
partition: Partitioning.PartitionId(0),
});
const polled = await c.message.poll({
streamId: streamName,
topicId: topicName,
consumer: Consumer.Single,
partitionId: 0,
pollingStrategy: PollingStrategy.First,
count: 10,
autocommit: false,
});
assert.equal(polled.messages.length, 3);
await c.stream.delete({ streamId: streamName });
});
it('e2e -> tls::connect via connection string', async () => {
const [, port] = getIggyAddress();
// The tls_ca_file path is resolved by the connection string parser;
// the certificate itself is read when the TLS socket connects.
const csClient = new Client(
'iggy://iggy:iggy@localhost:' +
`${port}?tls=true&tls_ca_file=${caCertPath}`
);
try {
// tls=true alone: servername defaults to the DNS host.
assert.equal(csClient._config.transport, 'TLS');
const options = csClient._config.options as TlsOption;
assert.equal(options.servername, undefined);
assert.ok(await csClient.system.ping());
assert.deepEqual(
await csClient.session.login(credentials),
{ userId: 0 }
);
} finally {
await csClient.destroy();
}
});
it('e2e -> tls::connect via connection string with explicit tls_domain',
async () => {
const [, port] = getIggyAddress();
const csClient = new Client(
'iggy://iggy:iggy@localhost:' +
`${port}?tls=true&tls_domain=localhost&tls_ca_file=${caCertPath}` +
'&heartbeat_interval=15s'
);
try {
assert.equal(csClient._config.transport, 'TLS');
assert.equal(
(csClient._config.options as TlsOption).servername,
'localhost'
);
assert.equal(csClient._config.heartbeatInterval, 15000);
assert.ok(await csClient.system.ping());
} finally {
await csClient.destroy();
}
}
);
it('e2e -> tls::rejects a wrong ca path with a TypeError', () => {
// The client is constructed eagerly, so an unreadable CA file surfaces
// as a TypeError from the constructor instead of a raw ENOENT Error
// leaking from the socket layer.
assert.throws(
() =>
new Client(
'iggy://iggy:iggy@localhost:8090' +
'?tls=true&tls_ca_file=/does/not/exist.pem'
),
(error: unknown) =>
error instanceof TypeError &&
/cannot read tls_ca_file/.test(error.message)
);
});
it('e2e -> tls::logout', async () => {
assert.ok(await c.session.logout());
});
after(() => {
c.destroy();
});
});