Skip to content

Commit

Permalink
feat(mysql): Add MYSQL connection test
Browse files Browse the repository at this point in the history
  • Loading branch information
nagyszabi authored and burgerni10 committed Jun 21, 2023
1 parent a5afba5 commit 44ee46a
Show file tree
Hide file tree
Showing 2 changed files with 261 additions and 10 deletions.
194 changes: 188 additions & 6 deletions backend/src/south/south-mysql/south-mysql.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,12 +179,6 @@ describe('SouthMySQL with authentication', () => {
expect(utils.createFolder).toHaveBeenCalledWith(path.resolve('baseFolder', 'tmp'));
});

it('should test connection with oracle', async () => {
// TODO
await expect(SouthMySQL.testConnection({}, logger, encryptionService)).rejects.toThrow('TODO: method needs to be implemented');
expect(logger.trace).toHaveBeenCalledWith(`Testing connection`);
});

it('should properly run historyQuery', async () => {
const startTime = '2020-01-01T00:00:00.000Z';
south.queryData = jest
Expand Down Expand Up @@ -354,3 +348,191 @@ describe('SouthMySQL without authentication', () => {
expect(error).toEqual(new Error('connection error'));
});
});

describe('SouthMySQL test connection', () => {
const configuration: SouthConnectorDTO = {
id: 'southId',
name: 'south',
type: 'mysql',
description: 'my test connector',
enabled: true,
history: {
maxInstantPerItem: true,
maxReadInterval: 3600,
readDelay: 0
},
settings: {
host: 'localhost',
port: 3306,
database: 'db',
username: 'username',
password: 'password',
connectionTimeout: 1000,
requestTimeout: 1000
}
};
const settings = { ...configuration.settings };

// Error codes handled by the test function
// With the expected error messages to throw
const ERROR_CODES = {
ETIMEDOUT: 'Please check host and port',
ECONNREFUSED: 'Please check host and port',
ER_ACCESS_DENIED_ERROR: 'Please check username and password',
ER_DBACCESS_DENIED_ERROR: `User '${settings.username}' does not have access to database '${settings.database}'`,
ER_BAD_DB_ERROR: `Database '${settings.database}' does not exist`,
DEFAULT: 'Please check logs' // For exceptions that we aren't explicitly specifying
} as const;

type ErrorCodes = keyof typeof ERROR_CODES;

class MYSQL2Error extends Error {
private code: ErrorCodes;
constructor(message: string, code: ErrorCodes) {
super();
this.name = 'MYSQL2Error';
this.message = message;
this.code = code;
}
}

beforeEach(async () => {
jest.clearAllMocks();
jest.useFakeTimers().setSystemTime(new Date(nowDateString));
});

it('Database is reachable and has tables', async () => {
const result = [{ table_name: 'logs', columns: 'data(INTEGER), timestamp(datetime)' }];
const mysqlConnection = {
execute: jest.fn().mockReturnValueOnce([result]),
ping: jest.fn(),
end: jest.fn()
};
(mysql.createConnection as jest.Mock).mockReturnValue(mysqlConnection);

const test = SouthMySQL.testConnection(settings, logger, encryptionService);
await expect(test).resolves.not.toThrow();

expect(mysqlConnection.end).toBeCalled();
expect((logger.trace as jest.Mock).mock.calls).toEqual([
[`Testing if MYSQL connection settings are correct`],
[`Pinging the database`],
[`Testing system table query`]
]);

const tables = result.map((row: any) => `${row.table_name}: [${row.columns}]`).join(',\n');
expect(logger.info).toHaveBeenCalledWith('Database is live with tables (table:[columns]):\n%s', tables);
});

it('Unable to create connection', async () => {
let code: ErrorCodes;
const errorMessage = 'Error creating connection';

for (code in ERROR_CODES) {
(logger.error as jest.Mock).mockClear();
(logger.trace as jest.Mock).mockClear();
(mysql.createConnection as jest.Mock).mockImplementationOnce(() => {
throw new MYSQL2Error(errorMessage, code);
});

const test = SouthMySQL.testConnection(settings, logger, encryptionService);
await expect(test).rejects.toThrow(new Error(ERROR_CODES[code]));

expect((logger.error as jest.Mock).mock.calls).toEqual([[`Unable to connect to database: ${errorMessage}`]]);
expect((logger.trace as jest.Mock).mock.calls).toEqual([[`Testing if MYSQL connection settings are correct`]]);
}
});

it('Unable to ping database', async () => {
let code: ErrorCodes;
const errorMessage = 'Error pinging database';

for (code in ERROR_CODES) {
(logger.error as jest.Mock).mockClear();
(logger.trace as jest.Mock).mockClear();
const mysqlConnection = {
ping: () => {
throw new MYSQL2Error(errorMessage, code);
},
end: jest.fn()
};
(mysql.createConnection as jest.Mock).mockReturnValue(mysqlConnection);

const test = SouthMySQL.testConnection(settings, logger, encryptionService);
await expect(test).rejects.toThrow(new Error(ERROR_CODES[code]));

expect(mysqlConnection.end).toBeCalled();
expect((logger.error as jest.Mock).mock.calls).toEqual([[`Unable to connect to database: ${errorMessage}`]]);
expect((logger.trace as jest.Mock).mock.calls).toEqual([
[`Testing if MYSQL connection settings are correct`],
[`Pinging the database`]
]);
}
});

it('System table unreachable', async () => {
const errorMessage = 'information_schema.TABLES does not exist';
const mysqlConnection = {
execute: jest.fn().mockImplementationOnce(() => {
throw new Error(errorMessage);
}),
ping: jest.fn(),
end: jest.fn()
};
(mysql.createConnection as jest.Mock).mockReturnValue(mysqlConnection);

const test = SouthMySQL.testConnection(settings, logger, encryptionService);

await expect(test).rejects.toThrow(new Error(`Unable to read tables in database '${settings.database}', check logs`));

expect(mysqlConnection.end).toBeCalled();
expect((logger.error as jest.Mock).mock.calls).toEqual([[`Unable to read tables in database '${settings.database}': ${errorMessage}`]]);
expect((logger.trace as jest.Mock).mock.calls).toEqual([
[`Testing if MYSQL connection settings are correct`],
[`Pinging the database`],
[`Testing system table query`]
]);
});

it('Database has no tables', async () => {
const mysqlConnection = {
execute: jest.fn().mockReturnValueOnce([[]]),
ping: jest.fn(),
end: jest.fn()
};
(mysql.createConnection as jest.Mock).mockReturnValue(mysqlConnection);

const test = SouthMySQL.testConnection(settings, logger, encryptionService);

await expect(test).rejects.toThrow(new Error('Database has no tables'));

expect(mysqlConnection.end).toBeCalled();
expect((logger.warn as jest.Mock).mock.calls).toEqual([[`Database '${settings.database}' has no tables`]]);
expect((logger.trace as jest.Mock).mock.calls).toEqual([
[`Testing if MYSQL connection settings are correct`],
[`Pinging the database`],
[`Testing system table query`]
]);
});

it('Unable to ping database without password', async () => {
configuration.settings.password = '';
let code: ErrorCodes;
const errorMessage = 'Error pinging database';

for (code in ERROR_CODES) {
(logger.error as jest.Mock).mockClear();
(logger.trace as jest.Mock).mockClear();
const mysqlConnection = {
ping: () => {
throw new MYSQL2Error(errorMessage, code);
},
end: jest.fn()
};
(mysql.createConnection as jest.Mock).mockReturnValue(mysqlConnection);

const test = SouthMySQL.testConnection(configuration.settings, logger, encryptionService);
await expect(test).rejects.toThrow(new Error(ERROR_CODES[code]));
}
});
});
77 changes: 73 additions & 4 deletions backend/src/south/south-mysql/south-mysql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,14 +62,83 @@ export default class SouthMySQL extends SouthConnector implements QueriesHistory
await super.start();
}

// TODO: method needs to be implemented
static async testConnection(
settings: SouthConnectorDTO['settings'],
logger: pino.Logger,
_encryptionService: EncryptionService
encryptionService: EncryptionService
): Promise<void> {
logger.trace(`Testing connection`);
throw new Error('TODO: method needs to be implemented');
const config: mysql.ConnectionOptions = {
host: settings.host,
port: settings.port,
user: settings.username,
password: settings.password ? await encryptionService.decryptText(settings.password) : '',
database: settings.database,
connectTimeout: settings.connectionTimeout,
timezone: 'Z'
};
let connection;
logger.trace(`Testing if MYSQL connection settings are correct`);
try {
connection = await mysql.createConnection(config);
logger.trace(`Pinging the database`);
await connection.ping();
} catch (error: any) {
logger.error(`Unable to connect to database: ${error.message}`);
if (connection) {
await connection.end();
}

switch (error.code) {
case 'ETIMEDOUT':
case 'ECONNREFUSED':
throw new Error('Please check host and port');

case 'ER_ACCESS_DENIED_ERROR':
throw new Error('Please check username and password');

case 'ER_DBACCESS_DENIED_ERROR':
throw new Error(`User '${settings.username}' does not have access to database '${settings.database}'`);

case 'ER_BAD_DB_ERROR':
throw new Error(`Database '${settings.database}' does not exist`);

default:
throw new Error('Please check logs');
}
}

logger.trace(`Testing system table query`);

let tables;
try {
[tables] = await connection.execute<mysql.RowDataPacket[]>(`
SELECT TABLES.TABLE_NAME AS table_name,
(SELECT GROUP_CONCAT(CONCAT(COLUMN_NAME, '(', DATA_TYPE, ')') SEPARATOR ', ')
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = TABLES.TABLE_NAME
GROUP BY TABLE_SCHEMA) AS 'columns'
FROM information_schema.TABLES AS TABLES
WHERE table_schema = DATABASE()
AND table_type = 'BASE TABLE'
`);
} catch (error: any) {
await connection.end();

logger.error(`Unable to read tables in database '${settings.database}': ${error.message}`);
throw new Error(`Unable to read tables in database '${settings.database}', check logs`);
}

await connection.end();

if (tables.length === 0) {
logger.warn(`Database '${settings.database}' has no tables`);
throw new Error('Database has no tables');
}

const tablesString = tables.map((row: any) => `${row.table_name}: [${row.columns}]`).join(',\n');

logger.info('Database is live with tables (table:[columns]):\n%s', tablesString);
}

/**
Expand Down

0 comments on commit 44ee46a

Please sign in to comment.