diff --git a/lib/base/connection.js b/lib/base/connection.js index 70cec19c97..572e3c2225 100644 --- a/lib/base/connection.js +++ b/lib/base/connection.js @@ -37,6 +37,7 @@ class BaseConnection extends EventEmitter { constructor(opts) { super(); this.config = opts.config; + this.state = 'disconnected'; // TODO: fill defaults // if no params, connect to /var/lib/mysql/mysql.sock ( /tmp/mysql.sock on OSX ) // if host is given, connect to host:3306 @@ -134,10 +135,12 @@ class BaseConnection extends EventEmitter { } this._handshakePacket = handshakeCommand.handshake; this.threadId = handshakeCommand.handshake.connectionId; + this.state = "connected"; this.emit('connect', handshakeCommand.handshake); }); handshakeCommand.on('error', (err) => { this._closing = true; + this.state = "disconnected"; this._notifyError(err); }); this.addCommand(handshakeCommand); @@ -207,6 +210,7 @@ class BaseConnection extends EventEmitter { // notify all commands in the queue and bubble error as connection "error" // called on stream error or unexpected termination _notifyError(err) { + this.state = "protocol_error"; if (this.connectTimeout) { Timers.clearTimeout(this.connectTimeout); this.connectTimeout = null; @@ -358,8 +362,8 @@ class BaseConnection extends EventEmitter { checkServerIdentity: verifyIdentity ? Tls.checkServerIdentity : function () { - return undefined; - }, + return undefined; + }, secureContext, isServer: false, socket: this.stream, @@ -409,6 +413,7 @@ class BaseConnection extends EventEmitter { const err = new Error(message); err.fatal = true; err.code = code || 'PROTOCOL_ERROR'; + this.state = "protocol_error"; this.emit('error', err); } @@ -769,6 +774,7 @@ class BaseConnection extends EventEmitter { this.connectTimeout = null; } this._closing = true; + this.state = 'disconnected'; this.stream.end(); this.addCommand = this._addCommandClosedState; } @@ -893,6 +899,7 @@ class BaseConnection extends EventEmitter { // =============================================================== end(callback) { + this.state = 'disconnected'; if (this.config.isServer) { this._closing = true; const quitCmd = new EventEmitter(); diff --git a/test/unit/connection/test-connection-state.test.cjs b/test/unit/connection/test-connection-state.test.cjs new file mode 100644 index 0000000000..0533871667 --- /dev/null +++ b/test/unit/connection/test-connection-state.test.cjs @@ -0,0 +1,98 @@ +'use strict'; +const { assert, describe, it, beforeEach, afterEach } = require('poku'); +const common = require('../../common.test.cjs'); + +(async () => { + await describe('Connection state', async () => { + let connection; + + beforeEach(() => (connection = common.createConnection().promise())); + + afterEach(async () => { + if (!connection) return; + try { + await connection.end(); + } catch (e) { + // ignore teardown errors + } + }); + + await it('connects and sets state to connected', async () => { + await connection?.connect(); + assert.equal(connection.connection.state, 'connected'); + }); + + await it('connects and disconnect: sets state to connected and disconnected close() is called.', async () => { + await connection?.connect(); + assert.equal(connection.connection.state, 'connected'); + await connection.close(); + assert.equal(connection.connection.state, 'disconnected'); + }); + + await it('connects and disconnect: sets state to connected and disconnected when end() is called.', async () => { + await connection?.connect(); + assert.equal(connection.connection.state, 'connected'); + await connection.end(); + assert.equal(connection.connection.state, 'disconnected'); + }); + + await it('state is protocol_error if creds are wrong.', async () => { + const badConnection = common.createConnection({ password: "WR0NG", user: 'wrong' }).promise(); + try { + await badConnection.connect(); + assert.fail('expected connect() to throw for bad credentials'); + } catch (err) { + assert.equal(badConnection.connection.state, 'protocol_error'); + } finally { + try { + await bad.end(); + } catch (e) { } + } + }); + + await it('ping keeps connection operational', async () => { + await connection?.connect(); + await connection.ping(); + assert.equal(connection.connection.state, 'connected'); + }); + + await it('simple query keeps state operational and returns rows', async () => { + await connection?.connect(); + const [rows] = await connection.query('SELECT 1 AS x'); + assert.equal(rows[0].x, 1); + assert.equal(connection.connection.state, 'connected'); + }); + + await it('socket destroy produces disconnected', async () => { + await connection?.connect(); + const raw = connection.connection; + // attach error listener early to avoid uncaught exceptions + let sawError = null; + raw.once('error', (e) => { sawError = e; }); + // Force a network-style drop + raw.stream.destroy(new Error('simulate network drop')); + // wait a bit for event propagation + await new Promise((r) => setTimeout(r, 50)); + assert.ok(sawError instanceof Error, 'expected an error emitted'); + assert.equal(connection.connection.state, 'disconnected'); + }); + + await it('changeUser to invalid account yields disconnected', async () => { + await connection?.connect(); + const raw = connection.connection; + // use changeUser to induce auth error (assuming user 'nope' doesn't exist) + let gotErr = null; + assert.equal(raw.state, "connected"); + raw.on('error', (e) => { gotErr = e; }); + try { + await connection.changeUser({ user: 'nope', password: 'bad' }); + } catch (err) { + gotErr = err; + } + // allow propagation + await new Promise((r) => setTimeout(r, 20)); + assert.ok(gotErr instanceof Error, 'expected changeUser to emit error'); + assert.equal(raw.state, 'disconnected'); + }); + }); +})();