From 92419b19c544514d0f5c6db7eaa2f93ba72857da Mon Sep 17 00:00:00 2001 From: Dzmitry Lemechko Date: Thu, 19 Dec 2024 17:36:07 +0100 Subject: [PATCH] [scout] adding unit tests (#204567) ## Summary Adding tests and making adjustments/fixes based on the findings. Note: no integration tests were added to verify servers start as it is mostly equal to `@kbn-test` functionality that has jest integration tests. We can add it later, when Scout has specific logic. How to run: `node scripts/jest --config packages/kbn-scout/jest.config.js` Scope: ``` PASS packages/kbn-scout/src/config/config.test.ts PASS packages/kbn-scout/src/config/loader/read_config_file.test.ts PASS packages/kbn-scout/src/config/utils/get_config_file.test.ts PASS packages/kbn-scout/src/config/utils/load_servers_config.test.ts PASS packages/kbn-scout/src/config/utils/save_scout_test_config.test.ts PASS packages/kbn-scout/src/playwright/config/create_config.test.ts PASS packages/kbn-scout/src/playwright/runner/config_validator.test.ts PASS packages/kbn-scout/src/playwright/runner/flags.test.ts PASS packages/kbn-scout/src/playwright/utils/runner_utils.test.ts PASS packages/kbn-scout/src/servers/flags.test.ts ``` (cherry picked from commit 2ba3247526331ec152f2d1e236c40635e86b3ce3) --- packages/kbn-scout/README.md | 6 + .../kbn-scout/src/common/services/clients.ts | 6 +- .../kbn-scout/src/common/services/config.ts | 4 +- .../src/common/services/kibana_url.ts | 4 +- .../src/common/services/saml_auth.ts | 8 +- packages/kbn-scout/src/config/config.test.ts | 128 +++++++++++++++++ packages/kbn-scout/src/config/config.ts | 11 +- packages/kbn-scout/src/config/index.ts | 5 +- packages/kbn-scout/src/config/loader/index.ts | 10 ++ .../config/loader/read_config_file.test.ts | 83 +++++++++++ .../{config_load.ts => read_config_file.ts} | 5 +- .../kbn-scout/src/config/schema/schema.ts | 2 +- .../config/serverless/es.serverless.config.ts | 4 +- .../serverless/oblt.serverless.config.ts | 4 +- .../serverless/security.serverless.config.ts | 4 +- .../serverless/serverless.base.config.ts | 4 +- .../src/config/stateful/base.config.ts | 4 +- .../src/config/stateful/stateful.config.ts | 4 +- packages/kbn-scout/src/config/utils.ts | 83 ----------- .../src/config/utils/get_config_file.test.ts | 35 +++++ .../src/config/{ => utils}/get_config_file.ts | 11 +- packages/kbn-scout/src/config/utils/index.ts | 12 ++ .../config/utils/load_servers_config.test.ts | 91 ++++++++++++ .../src/config/utils/load_servers_config.ts | 38 +++++ .../utils/save_scout_test_config.test.ts | 130 ++++++++++++++++++ .../config/utils/save_scout_test_config.ts | 38 +++++ packages/kbn-scout/src/config/utils/utils.ts | 28 ++++ .../playwright/config/create_config.test.ts | 48 +++++++ .../src/playwright/config/create_config.ts | 76 ++++++++++ .../kbn-scout/src/playwright/config/index.ts | 68 +-------- .../playwright/fixtures/types/worker_scope.ts | 4 +- .../src/playwright/runner/config_loader.ts | 17 +++ .../runner/config_validator.test.ts | 99 +++++++++++++ .../src/playwright/runner/config_validator.ts | 25 ++-- .../src/playwright/runner/flags.test.ts | 102 ++++++++++++++ .../kbn-scout/src/playwright/runner/flags.ts | 5 +- .../kbn-scout/src/playwright/utils/index.ts | 21 +-- .../src/playwright/utils/runner_utils.test.ts | 108 +++++++++++++++ .../src/playwright/utils/runner_utils.ts | 43 ++++++ packages/kbn-scout/src/servers/flags.test.ts | 74 ++++++++++ packages/kbn-scout/src/types/index.ts | 4 +- .../types/{config.d.ts => server_config.d.ts} | 2 +- .../types/{servers.d.ts => test_config.d.ts} | 3 +- 43 files changed, 1234 insertions(+), 227 deletions(-) create mode 100644 packages/kbn-scout/src/config/config.test.ts create mode 100644 packages/kbn-scout/src/config/loader/index.ts create mode 100644 packages/kbn-scout/src/config/loader/read_config_file.test.ts rename packages/kbn-scout/src/config/loader/{config_load.ts => read_config_file.ts} (86%) delete mode 100644 packages/kbn-scout/src/config/utils.ts create mode 100644 packages/kbn-scout/src/config/utils/get_config_file.test.ts rename packages/kbn-scout/src/config/{ => utils}/get_config_file.ts (67%) create mode 100644 packages/kbn-scout/src/config/utils/index.ts create mode 100644 packages/kbn-scout/src/config/utils/load_servers_config.test.ts create mode 100644 packages/kbn-scout/src/config/utils/load_servers_config.ts create mode 100644 packages/kbn-scout/src/config/utils/save_scout_test_config.test.ts create mode 100644 packages/kbn-scout/src/config/utils/save_scout_test_config.ts create mode 100644 packages/kbn-scout/src/config/utils/utils.ts create mode 100644 packages/kbn-scout/src/playwright/config/create_config.test.ts create mode 100644 packages/kbn-scout/src/playwright/config/create_config.ts create mode 100644 packages/kbn-scout/src/playwright/runner/config_loader.ts create mode 100644 packages/kbn-scout/src/playwright/runner/config_validator.test.ts create mode 100644 packages/kbn-scout/src/playwright/runner/flags.test.ts create mode 100644 packages/kbn-scout/src/playwright/utils/runner_utils.test.ts create mode 100644 packages/kbn-scout/src/playwright/utils/runner_utils.ts create mode 100644 packages/kbn-scout/src/servers/flags.test.ts rename packages/kbn-scout/src/types/{config.d.ts => server_config.d.ts} (96%) rename packages/kbn-scout/src/types/{servers.d.ts => test_config.d.ts} (93%) diff --git a/packages/kbn-scout/README.md b/packages/kbn-scout/README.md index b5e64416d4ed2..7368b4d1bb48e 100644 --- a/packages/kbn-scout/README.md +++ b/packages/kbn-scout/README.md @@ -193,6 +193,12 @@ npx playwright test --config /ui_tests/playwright.config.ts We welcome contributions to improve and extend `kbn-scout`. This guide will help you get started, add new features, and align with existing project standards. +Make sure to run unit tests before opening the PR: + +```bash +node scripts/jest --config packages/kbn-scout/jest.config.js +``` + #### Setting Up the Environment Ensure you have the latest local copy of the Kibana repository. diff --git a/packages/kbn-scout/src/common/services/clients.ts b/packages/kbn-scout/src/common/services/clients.ts index 3a0dcf8bfe320..58a5d222e18e5 100644 --- a/packages/kbn-scout/src/common/services/clients.ts +++ b/packages/kbn-scout/src/common/services/clients.ts @@ -9,7 +9,7 @@ import { KbnClient, createEsClientForTesting } from '@kbn/test'; import type { ToolingLog } from '@kbn/tooling-log'; -import { ScoutServerConfig } from '../../types'; +import { ScoutTestConfig } from '../../types'; import { serviceLoadedMsg } from '../../playwright/utils'; interface ClientOptions { @@ -29,7 +29,7 @@ function createClientUrlWithAuth({ serviceName, url, username, password, log }: return clientUrl.toString(); } -export function createEsClient(config: ScoutServerConfig, log: ToolingLog) { +export function createEsClient(config: ScoutTestConfig, log: ToolingLog) { const { username, password } = config.auth; const elasticsearchUrl = createClientUrlWithAuth({ serviceName: 'Es', @@ -45,7 +45,7 @@ export function createEsClient(config: ScoutServerConfig, log: ToolingLog) { }); } -export function createKbnClient(config: ScoutServerConfig, log: ToolingLog) { +export function createKbnClient(config: ScoutTestConfig, log: ToolingLog) { const kibanaUrl = createClientUrlWithAuth({ serviceName: 'Kbn', url: config.hosts.kibana, diff --git a/packages/kbn-scout/src/common/services/config.ts b/packages/kbn-scout/src/common/services/config.ts index fe8e932194d91..dcbcdb2a17ab9 100644 --- a/packages/kbn-scout/src/common/services/config.ts +++ b/packages/kbn-scout/src/common/services/config.ts @@ -10,7 +10,7 @@ import path from 'path'; import fs from 'fs'; import { ToolingLog } from '@kbn/tooling-log'; -import { ScoutServerConfig } from '../../types'; +import { ScoutTestConfig } from '../../types'; import { serviceLoadedMsg } from '../../playwright/utils'; export function createScoutConfig(configDir: string, configName: string, log: ToolingLog) { @@ -21,7 +21,7 @@ export function createScoutConfig(configDir: string, configName: string, log: To const configPath = path.join(configDir, `${configName}.json`); log.info(`Reading test servers confiuration from file: ${configPath}`); - const config = JSON.parse(fs.readFileSync(configPath, 'utf-8')) as ScoutServerConfig; + const config = JSON.parse(fs.readFileSync(configPath, 'utf-8')) as ScoutTestConfig; log.debug(serviceLoadedMsg('config')); diff --git a/packages/kbn-scout/src/common/services/kibana_url.ts b/packages/kbn-scout/src/common/services/kibana_url.ts index cbfab5dc90796..65a21c80592e8 100644 --- a/packages/kbn-scout/src/common/services/kibana_url.ts +++ b/packages/kbn-scout/src/common/services/kibana_url.ts @@ -8,7 +8,7 @@ */ import type { ToolingLog } from '@kbn/tooling-log'; -import { ScoutServerConfig } from '../../types'; +import { ScoutTestConfig } from '../../types'; import { serviceLoadedMsg } from '../../playwright/utils'; export interface PathOptions { @@ -64,7 +64,7 @@ export class KibanaUrl { } } -export function createKbnUrl(scoutConfig: ScoutServerConfig, log: ToolingLog) { +export function createKbnUrl(scoutConfig: ScoutTestConfig, log: ToolingLog) { const kbnUrl = new KibanaUrl(new URL(scoutConfig.hosts.kibana)); log.debug(serviceLoadedMsg('kbnUrl')); diff --git a/packages/kbn-scout/src/common/services/saml_auth.ts b/packages/kbn-scout/src/common/services/saml_auth.ts index e3dbd47fc8c90..8d3daf8e3ccd6 100644 --- a/packages/kbn-scout/src/common/services/saml_auth.ts +++ b/packages/kbn-scout/src/common/services/saml_auth.ts @@ -17,17 +17,17 @@ import { import { REPO_ROOT } from '@kbn/repo-info'; import { HostOptions, SamlSessionManager } from '@kbn/test'; import { ToolingLog } from '@kbn/tooling-log'; -import { ScoutServerConfig } from '../../types'; +import { ScoutTestConfig } from '../../types'; import { Protocol } from '../../playwright/types'; import { serviceLoadedMsg } from '../../playwright/utils'; -const getResourceDirPath = (config: ScoutServerConfig) => { +const getResourceDirPath = (config: ScoutTestConfig) => { return config.serverless ? path.resolve(SERVERLESS_ROLES_ROOT_PATH, config.projectType!) : path.resolve(REPO_ROOT, STATEFUL_ROLES_ROOT_PATH); }; -const createKibanaHostOptions = (config: ScoutServerConfig): HostOptions => { +const createKibanaHostOptions = (config: ScoutTestConfig): HostOptions => { const kibanaUrl = new URL(config.hosts.kibana); kibanaUrl.username = config.auth.username; kibanaUrl.password = config.auth.password; @@ -42,7 +42,7 @@ const createKibanaHostOptions = (config: ScoutServerConfig): HostOptions => { }; export const createSamlSessionManager = ( - config: ScoutServerConfig, + config: ScoutTestConfig, log: ToolingLog ): SamlSessionManager => { const resourceDirPath = getResourceDirPath(config); diff --git a/packages/kbn-scout/src/config/config.test.ts b/packages/kbn-scout/src/config/config.test.ts new file mode 100644 index 0000000000000..f4401e8af2da2 --- /dev/null +++ b/packages/kbn-scout/src/config/config.test.ts @@ -0,0 +1,128 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { Config } from './config'; + +describe('Config.getScoutTestConfig', () => { + it(`should return a properly structured 'ScoutTestConfig' object for 'stateful'`, async () => { + const config = new Config({ + servers: { + elasticsearch: { + protocol: 'http', + hostname: 'localhost', + port: 9220, + username: 'kibana_system', + password: 'changeme', + }, + kibana: { + protocol: 'http', + hostname: 'localhost', + port: 5620, + username: 'elastic', + password: 'changeme', + }, + }, + dockerServers: {}, + esTestCluster: { + from: 'snapshot', + files: [], + serverArgs: [], + ssl: false, + }, + kbnTestServer: { + buildArgs: [], + env: {}, + sourceArgs: [], + serverArgs: [], + }, + }); + + const scoutConfig = config.getScoutTestConfig(); + + const expectedConfig = { + serverless: false, + projectType: undefined, + isCloud: false, + license: 'trial', + cloudUsersFilePath: expect.stringContaining('.ftr/role_users.json'), + hosts: { + kibana: 'http://localhost:5620', + elasticsearch: 'http://localhost:9220', + }, + auth: { + username: 'elastic', + password: 'changeme', + }, + metadata: { + generatedOn: expect.any(String), + config: expect.any(Object), + }, + }; + + expect(scoutConfig).toEqual(expectedConfig); + }); + + it(`should return a properly structured 'ScoutTestConfig' object for 'serverless=es'`, async () => { + const config = new Config({ + serverless: true, + servers: { + elasticsearch: { + protocol: 'https', + hostname: 'localhost', + port: 9220, + username: 'elastic_serverless', + password: 'changeme', + }, + kibana: { + protocol: 'http', + hostname: 'localhost', + port: 5620, + username: 'elastic_serverless', + password: 'changeme', + }, + }, + dockerServers: {}, + esTestCluster: { + from: 'serverless', + files: [], + serverArgs: [], + ssl: true, + }, + kbnTestServer: { + buildArgs: [], + env: {}, + sourceArgs: [], + serverArgs: ['--serverless=es'], + }, + }); + + const scoutConfig = config.getScoutTestConfig(); + const expectedConfig = { + serverless: true, + projectType: 'es', + isCloud: false, + license: 'trial', + cloudUsersFilePath: expect.stringContaining('.ftr/role_users.json'), + hosts: { + kibana: 'http://localhost:5620', + elasticsearch: 'https://localhost:9220', + }, + auth: { + username: 'elastic_serverless', + password: 'changeme', + }, + metadata: { + generatedOn: expect.any(String), + config: expect.any(Object), + }, + }; + + expect(scoutConfig).toEqual(expectedConfig); + }); +}); diff --git a/packages/kbn-scout/src/config/config.ts b/packages/kbn-scout/src/config/config.ts index a316aac61d69e..d790545d258e2 100644 --- a/packages/kbn-scout/src/config/config.ts +++ b/packages/kbn-scout/src/config/config.ts @@ -13,15 +13,15 @@ import Path from 'path'; import { cloneDeepWith, get, has, toPath } from 'lodash'; import { REPO_ROOT } from '@kbn/repo-info'; import { schema } from './schema'; -import { ScoutServerConfig } from '../types'; -import { formatCurrentDate, getProjectType } from './utils'; +import { ScoutServerConfig, ScoutTestConfig } from '../types'; +import { formatCurrentDate, getProjectType } from './utils/utils'; const $values = Symbol('values'); export class Config { - private [$values]: Record; + private [$values]: ScoutServerConfig; - constructor(data: Record) { + constructor(data: ScoutServerConfig) { const { error, value } = schema.validate(data, { abortEarly: false, }); @@ -104,13 +104,14 @@ export class Config { }); } - public getTestServersConfig(): ScoutServerConfig { + public getScoutTestConfig(): ScoutTestConfig { return { serverless: this.get('serverless'), projectType: this.get('serverless') ? getProjectType(this.get('kbnTestServer.serverArgs')) : undefined, isCloud: false, + license: this.get('esTestCluster.license'), cloudUsersFilePath: Path.resolve(REPO_ROOT, '.ftr', 'role_users.json'), hosts: { kibana: Url.format({ diff --git a/packages/kbn-scout/src/config/index.ts b/packages/kbn-scout/src/config/index.ts index 969edbe8e4483..fc9e334ad6596 100644 --- a/packages/kbn-scout/src/config/index.ts +++ b/packages/kbn-scout/src/config/index.ts @@ -7,7 +7,6 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -export { loadConfig } from './loader/config_load'; -export { getConfigFilePath } from './get_config_file'; -export { loadServersConfig } from './utils'; +export { readConfigFile } from './loader'; +export { getConfigFilePath, loadServersConfig } from './utils'; export type { Config } from './config'; diff --git a/packages/kbn-scout/src/config/loader/index.ts b/packages/kbn-scout/src/config/loader/index.ts new file mode 100644 index 0000000000000..f84f5e89a4022 --- /dev/null +++ b/packages/kbn-scout/src/config/loader/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +export { readConfigFile } from './read_config_file'; diff --git a/packages/kbn-scout/src/config/loader/read_config_file.test.ts b/packages/kbn-scout/src/config/loader/read_config_file.test.ts new file mode 100644 index 0000000000000..03f0009b3b395 --- /dev/null +++ b/packages/kbn-scout/src/config/loader/read_config_file.test.ts @@ -0,0 +1,83 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import path from 'path'; +import { Config } from '../config'; +import { readConfigFile } from './read_config_file'; + +jest.mock('path', () => ({ + resolve: jest.fn(), +})); + +jest.mock('../config', () => ({ + Config: jest.fn(), +})); + +describe('readConfigFile', () => { + const configPath = '/mock/config/path'; + const resolvedPath = '/resolved/config/path'; + const mockPathResolve = path.resolve as jest.Mock; + const mockConfigConstructor = Config as jest.Mock; + + beforeEach(() => { + jest.clearAllMocks(); + jest.resetModules(); + }); + + it(`should load and return a valid 'Config' instance when the config file exports 'servers'`, async () => { + const mockConfigModule = { servers: { host: 'localhost', port: 5601 } }; + + mockPathResolve.mockReturnValueOnce(resolvedPath); + + jest.isolateModules(async () => { + jest.mock(resolvedPath, () => mockConfigModule, { virtual: true }); + mockConfigConstructor.mockImplementation((servers) => ({ servers })); + + const result = await readConfigFile(configPath); + + expect(path.resolve).toHaveBeenCalledWith(configPath); + expect(result).toEqual({ servers: mockConfigModule.servers }); + }); + }); + + it(`should throw an error if the config file does not export 'servers'`, async () => { + const mockConfigModule = { otherProperty: 'value' }; + + mockPathResolve.mockReturnValueOnce(resolvedPath); + + jest.isolateModules(async () => { + jest.mock(resolvedPath, () => mockConfigModule, { virtual: true }); + + await expect(readConfigFile(configPath)).rejects.toThrow( + `No 'servers' found in the config file at path: ${resolvedPath}` + ); + expect(path.resolve).toHaveBeenCalledWith(configPath); + }); + }); + + it('should throw an error if the config file cannot be loaded', async () => { + mockPathResolve.mockReturnValueOnce(resolvedPath); + + jest.isolateModules(async () => { + const message = 'Module not found'; + jest.mock( + resolvedPath, + () => { + throw new Error(message); + }, + { virtual: true } + ); + + await expect(readConfigFile(configPath)).rejects.toThrow( + `Failed to load config from ${configPath}: ${message}` + ); + expect(path.resolve).toHaveBeenCalledWith(configPath); + }); + }); +}); diff --git a/packages/kbn-scout/src/config/loader/config_load.ts b/packages/kbn-scout/src/config/loader/read_config_file.ts similarity index 86% rename from packages/kbn-scout/src/config/loader/config_load.ts rename to packages/kbn-scout/src/config/loader/read_config_file.ts index c7e6b197d6a28..a4f153ff392ac 100644 --- a/packages/kbn-scout/src/config/loader/config_load.ts +++ b/packages/kbn-scout/src/config/loader/read_config_file.ts @@ -9,6 +9,7 @@ import path from 'path'; import { Config } from '../config'; +import { ScoutServerConfig } from '../../types'; /** * Dynamically loads server configuration file in the "kbn-scout" framework. It reads @@ -17,13 +18,13 @@ import { Config } from '../config'; * @param configPath Path to the configuration file to be loaded. * @returns Config instance that is used to start local servers */ -export const loadConfig = async (configPath: string): Promise => { +export const readConfigFile = async (configPath: string): Promise => { try { const absolutePath = path.resolve(configPath); const configModule = await import(absolutePath); if (configModule.servers) { - return new Config(configModule.servers); + return new Config(configModule.servers as ScoutServerConfig); } else { throw new Error(`No 'servers' found in the config file at path: ${absolutePath}`); } diff --git a/packages/kbn-scout/src/config/schema/schema.ts b/packages/kbn-scout/src/config/schema/schema.ts index 86add154cc661..77f3c352e5589 100644 --- a/packages/kbn-scout/src/config/schema/schema.ts +++ b/packages/kbn-scout/src/config/schema/schema.ts @@ -75,7 +75,7 @@ export const schema = Joi.object() esTestCluster: Joi.object() .keys({ - license: Joi.valid('basic', 'trial', 'gold').default('basic'), + license: Joi.valid('basic', 'trial', 'gold').default('trial'), from: Joi.string().default('snapshot'), serverArgs: Joi.array().items(Joi.string()).default([]), esJavaOpts: Joi.string(), diff --git a/packages/kbn-scout/src/config/serverless/es.serverless.config.ts b/packages/kbn-scout/src/config/serverless/es.serverless.config.ts index 0ae2c7e6f0b3f..6ad77bff4606f 100644 --- a/packages/kbn-scout/src/config/serverless/es.serverless.config.ts +++ b/packages/kbn-scout/src/config/serverless/es.serverless.config.ts @@ -7,10 +7,10 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { ScoutLoaderConfig } from '../../types'; +import { ScoutServerConfig } from '../../types'; import { defaultConfig } from './serverless.base.config'; -export const servers: ScoutLoaderConfig = { +export const servers: ScoutServerConfig = { ...defaultConfig, esTestCluster: { ...defaultConfig.esTestCluster, diff --git a/packages/kbn-scout/src/config/serverless/oblt.serverless.config.ts b/packages/kbn-scout/src/config/serverless/oblt.serverless.config.ts index 08eb4d9d7cf55..f0739af12d9a4 100644 --- a/packages/kbn-scout/src/config/serverless/oblt.serverless.config.ts +++ b/packages/kbn-scout/src/config/serverless/oblt.serverless.config.ts @@ -8,9 +8,9 @@ */ import { defaultConfig } from './serverless.base.config'; -import { ScoutLoaderConfig } from '../../types'; +import { ScoutServerConfig } from '../../types'; -export const servers: ScoutLoaderConfig = { +export const servers: ScoutServerConfig = { ...defaultConfig, esTestCluster: { ...defaultConfig.esTestCluster, diff --git a/packages/kbn-scout/src/config/serverless/security.serverless.config.ts b/packages/kbn-scout/src/config/serverless/security.serverless.config.ts index 289790a9ffeb1..e702ae960dca5 100644 --- a/packages/kbn-scout/src/config/serverless/security.serverless.config.ts +++ b/packages/kbn-scout/src/config/serverless/security.serverless.config.ts @@ -7,10 +7,10 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { ScoutLoaderConfig } from '../../types'; +import { ScoutServerConfig } from '../../types'; import { defaultConfig } from './serverless.base.config'; -export const servers: ScoutLoaderConfig = { +export const servers: ScoutServerConfig = { ...defaultConfig, esTestCluster: { ...defaultConfig.esTestCluster, diff --git a/packages/kbn-scout/src/config/serverless/serverless.base.config.ts b/packages/kbn-scout/src/config/serverless/serverless.base.config.ts index a20a0c3bbe7a7..0df42d354a1e1 100644 --- a/packages/kbn-scout/src/config/serverless/serverless.base.config.ts +++ b/packages/kbn-scout/src/config/serverless/serverless.base.config.ts @@ -17,7 +17,7 @@ import { MOCK_IDP_REALM_NAME } from '@kbn/mock-idp-utils'; import { dockerImage } from '@kbn/test-suites-xpack/fleet_api_integration/config.base'; import { REPO_ROOT } from '@kbn/repo-info'; -import { ScoutLoaderConfig } from '../../types'; +import { ScoutServerConfig } from '../../types'; import { SAML_IDP_PLUGIN_PATH, SERVERLESS_IDP_METADATA_PATH, JWKS_PATH } from '../constants'; const packageRegistryConfig = join(__dirname, './package_registry_config.yml'); @@ -49,7 +49,7 @@ const servers = { }, }; -export const defaultConfig: ScoutLoaderConfig = { +export const defaultConfig: ScoutServerConfig = { serverless: true, servers, dockerServers: defineDockerServersConfig({ diff --git a/packages/kbn-scout/src/config/stateful/base.config.ts b/packages/kbn-scout/src/config/stateful/base.config.ts index a2d6f1e0fa6eb..2bac0024ad4e3 100644 --- a/packages/kbn-scout/src/config/stateful/base.config.ts +++ b/packages/kbn-scout/src/config/stateful/base.config.ts @@ -25,7 +25,7 @@ import { MOCK_IDP_REALM_NAME } from '@kbn/mock-idp-utils'; import { dockerImage } from '@kbn/test-suites-xpack/fleet_api_integration/config.base'; import { REPO_ROOT } from '@kbn/repo-info'; import { STATEFUL_ROLES_ROOT_PATH } from '@kbn/es'; -import type { ScoutLoaderConfig } from '../../types'; +import type { ScoutServerConfig } from '../../types'; import { SAML_IDP_PLUGIN_PATH, STATEFUL_IDP_METADATA_PATH } from '../constants'; const packageRegistryConfig = join(__dirname, './package_registry_config.yml'); @@ -61,7 +61,7 @@ const servers = { const kbnUrl = `${servers.kibana.protocol}://${servers.kibana.hostname}:${servers.kibana.port}`; -export const defaultConfig: ScoutLoaderConfig = { +export const defaultConfig: ScoutServerConfig = { servers, dockerServers: defineDockerServersConfig({ registry: { diff --git a/packages/kbn-scout/src/config/stateful/stateful.config.ts b/packages/kbn-scout/src/config/stateful/stateful.config.ts index e67419c21fb37..2825f6e9f86df 100644 --- a/packages/kbn-scout/src/config/stateful/stateful.config.ts +++ b/packages/kbn-scout/src/config/stateful/stateful.config.ts @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { ScoutLoaderConfig } from '../../types'; +import { ScoutServerConfig } from '../../types'; import { defaultConfig } from './base.config'; -export const servers: ScoutLoaderConfig = defaultConfig; +export const servers: ScoutServerConfig = defaultConfig; diff --git a/packages/kbn-scout/src/config/utils.ts b/packages/kbn-scout/src/config/utils.ts deleted file mode 100644 index 38c65f1573b04..0000000000000 --- a/packages/kbn-scout/src/config/utils.ts +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import * as Fs from 'fs'; -import getopts from 'getopts'; -import path from 'path'; -import { ToolingLog } from '@kbn/tooling-log'; -import { ServerlessProjectType } from '@kbn/es'; -import { SCOUT_SERVERS_ROOT } from '@kbn/scout-info'; -import { CliSupportedServerModes, ScoutServerConfig } from '../types'; -import { getConfigFilePath } from './get_config_file'; -import { loadConfig } from './loader/config_load'; -import type { Config } from './config'; - -export const formatCurrentDate = () => { - const now = new Date(); - - const format = (num: number, length: number) => String(num).padStart(length, '0'); - - return ( - `${format(now.getDate(), 2)}/${format(now.getMonth() + 1, 2)}/${now.getFullYear()} ` + - `${format(now.getHours(), 2)}:${format(now.getMinutes(), 2)}:${format(now.getSeconds(), 2)}.` + - `${format(now.getMilliseconds(), 3)}` - ); -}; - -/** - * Saves Scout server configuration to the disk. - * @param testServersConfig configuration to be saved - * @param log Logger instance to report errors or debug information. - */ -const saveTestServersConfigOnDisk = (testServersConfig: ScoutServerConfig, log: ToolingLog) => { - const configFilePath = path.join(SCOUT_SERVERS_ROOT, `local.json`); - - try { - const jsonData = JSON.stringify(testServersConfig, null, 2); - - if (!Fs.existsSync(SCOUT_SERVERS_ROOT)) { - log.debug(`scout: creating configuration directory: ${SCOUT_SERVERS_ROOT}`); - Fs.mkdirSync(SCOUT_SERVERS_ROOT, { recursive: true }); - } - - Fs.writeFileSync(configFilePath, jsonData, 'utf-8'); - log.info(`scout: Test server configuration saved at ${configFilePath}`); - } catch (error) { - log.error(`scout: Failed to save test server configuration - ${error.message}`); - throw new Error(`Failed to save test server configuration at ${configFilePath}`); - } -}; - -/** - * Loads server configuration based on the mode, creates "kbn-test" compatible Config - * instance, that can be used to start local servers and saves its "Scout"-format copy - * to the disk. - * @param mode server local run mode - * @param log Logger instance to report errors or debug information. - * @returns "kbn-test" compatible Config instance - */ -export async function loadServersConfig( - mode: CliSupportedServerModes, - log: ToolingLog -): Promise { - // get path to one of the predefined config files - const configPath = getConfigFilePath(mode); - // load config that is compatible with kbn-test input format - const config = await loadConfig(configPath); - // construct config for Playwright Test - const scoutServerConfig = config.getTestServersConfig(); - // save test config to the file - saveTestServersConfigOnDisk(scoutServerConfig, log); - return config; -} - -export const getProjectType = (kbnServerArgs: string[]) => { - const options = getopts(kbnServerArgs); - return options.serverless as ServerlessProjectType; -}; diff --git a/packages/kbn-scout/src/config/utils/get_config_file.test.ts b/packages/kbn-scout/src/config/utils/get_config_file.test.ts new file mode 100644 index 0000000000000..09b300e9ae404 --- /dev/null +++ b/packages/kbn-scout/src/config/utils/get_config_file.test.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import path from 'path'; +import { getConfigFilePath } from './get_config_file'; +import { REPO_ROOT } from '@kbn/repo-info'; + +// Not mocking to validate the actual path to the config file +const CONFIG_ROOT = path.join(REPO_ROOT, 'packages', 'kbn-scout', 'src', 'config'); + +describe('getConfigFilePath', () => { + it('should return the correct path for stateful config', () => { + const config = 'stateful'; + const expectedPath = path.join(CONFIG_ROOT, 'stateful', 'stateful.config.ts'); + + const result = getConfigFilePath(config); + + expect(result).toBe(expectedPath); + }); + + it('should return the correct path for serverless config with a valid type', () => { + const config = 'serverless=oblt'; + const expectedPath = path.join(CONFIG_ROOT, 'serverless', 'oblt.serverless.config.ts'); + + const result = getConfigFilePath(config); + + expect(result).toBe(expectedPath); + }); +}); diff --git a/packages/kbn-scout/src/config/get_config_file.ts b/packages/kbn-scout/src/config/utils/get_config_file.ts similarity index 67% rename from packages/kbn-scout/src/config/get_config_file.ts rename to packages/kbn-scout/src/config/utils/get_config_file.ts index 5976db1265797..95fa49af0d669 100644 --- a/packages/kbn-scout/src/config/get_config_file.ts +++ b/packages/kbn-scout/src/config/utils/get_config_file.ts @@ -8,19 +8,22 @@ */ import path from 'path'; -import { CliSupportedServerModes } from '../types'; +import { CliSupportedServerModes } from '../../types'; export const getConfigFilePath = (config: CliSupportedServerModes): string => { + const baseDir = path.join(__dirname, '..'); // config base directory + if (config === 'stateful') { - return path.join(__dirname, 'stateful', 'stateful.config.ts'); + return path.join(baseDir, 'stateful', 'stateful.config.ts'); } const [mode, type] = config.split('='); + if (mode !== 'serverless' || !type) { throw new Error( - `Invalid config format: ${config}. Expected "stateful" or "serverless=".` + `Invalid config format: "${config}". Expected "stateful" or "serverless=".` ); } - return path.join(__dirname, 'serverless', `${type}.serverless.config.ts`); + return path.join(baseDir, 'serverless', `${type}.serverless.config.ts`); }; diff --git a/packages/kbn-scout/src/config/utils/index.ts b/packages/kbn-scout/src/config/utils/index.ts new file mode 100644 index 0000000000000..ebe3b0bc27c44 --- /dev/null +++ b/packages/kbn-scout/src/config/utils/index.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +export { getConfigFilePath } from './get_config_file'; +export { loadServersConfig } from './load_servers_config'; +export { formatCurrentDate, getProjectType } from './utils'; diff --git a/packages/kbn-scout/src/config/utils/load_servers_config.test.ts b/packages/kbn-scout/src/config/utils/load_servers_config.test.ts new file mode 100644 index 0000000000000..8a33663cb4989 --- /dev/null +++ b/packages/kbn-scout/src/config/utils/load_servers_config.test.ts @@ -0,0 +1,91 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { ToolingLog } from '@kbn/tooling-log'; +import { getConfigFilePath } from './get_config_file'; +import { readConfigFile } from '../loader'; +import { loadServersConfig } from '..'; +import { saveScoutTestConfigOnDisk } from './save_scout_test_config'; +import { CliSupportedServerModes, ScoutTestConfig } from '../../types'; + +jest.mock('./get_config_file', () => ({ + getConfigFilePath: jest.fn(), +})); + +jest.mock('../loader', () => ({ + readConfigFile: jest.fn(), +})); + +jest.mock('./save_scout_test_config', () => ({ + saveScoutTestConfigOnDisk: jest.fn(), +})); + +const mockScoutTestConfig: ScoutTestConfig = { + hosts: { + kibana: 'http://localhost:5601', + elasticsearch: 'http://localhost:9220', + }, + auth: { + username: 'elastic', + password: 'changeme', + }, + serverless: true, + projectType: 'oblt', + isCloud: true, + license: 'trial', + cloudUsersFilePath: '/path/to/users', +}; + +describe('loadServersConfig', () => { + let mockLog: ToolingLog; + + const mockMode = `serverless=${mockScoutTestConfig.projectType}` as CliSupportedServerModes; + const mockConfigPath = '/mock/config/path.ts'; + + const mockClusterConfig = { + getScoutTestConfig: jest.fn().mockReturnValue(mockScoutTestConfig), + }; + + beforeEach(() => { + jest.clearAllMocks(); + mockLog = { + debug: jest.fn(), + info: jest.fn(), + error: jest.fn(), + } as unknown as ToolingLog; + }); + + it('should load, save, and return cluster configuration', async () => { + (getConfigFilePath as jest.Mock).mockReturnValue(mockConfigPath); + (readConfigFile as jest.Mock).mockResolvedValue(mockClusterConfig); + + const result = await loadServersConfig(mockMode, mockLog); + + expect(getConfigFilePath).toHaveBeenCalledWith(mockMode); + expect(readConfigFile).toHaveBeenCalledWith(mockConfigPath); + expect(mockClusterConfig.getScoutTestConfig).toHaveBeenCalled(); + expect(saveScoutTestConfigOnDisk).toHaveBeenCalledWith(mockScoutTestConfig, mockLog); + expect(result).toBe(mockClusterConfig); + + // no errors should be logged + expect(mockLog.info).not.toHaveBeenCalledWith(expect.stringContaining('error')); + }); + + it('should throw an error if readConfigFile fails', async () => { + const errorMessage = 'Failed to read config file'; + (getConfigFilePath as jest.Mock).mockReturnValue(mockConfigPath); + (readConfigFile as jest.Mock).mockRejectedValue(new Error(errorMessage)); + + await expect(loadServersConfig(mockMode, mockLog)).rejects.toThrow(errorMessage); + + expect(getConfigFilePath).toHaveBeenCalledWith(mockMode); + expect(readConfigFile).toHaveBeenCalledWith(mockConfigPath); + expect(saveScoutTestConfigOnDisk).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/kbn-scout/src/config/utils/load_servers_config.ts b/packages/kbn-scout/src/config/utils/load_servers_config.ts new file mode 100644 index 0000000000000..007b2fd32a460 --- /dev/null +++ b/packages/kbn-scout/src/config/utils/load_servers_config.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { ToolingLog } from '@kbn/tooling-log'; +import { CliSupportedServerModes } from '../../types'; +import { getConfigFilePath } from './get_config_file'; +import { readConfigFile } from '../loader'; +import type { Config } from '../config'; +import { saveScoutTestConfigOnDisk } from './save_scout_test_config'; + +/** + * Loads server configuration based on the mode, creates "kbn-test" compatible Config + * instance, that can be used to start local servers and saves its "Scout"-format copy + * to the disk. + * @param mode server local run mode + * @param log Logger instance to report errors or debug information. + * @returns "kbn-test" compatible Config instance + */ +export async function loadServersConfig( + mode: CliSupportedServerModes, + log: ToolingLog +): Promise { + // get path to one of the predefined config files + const configPath = getConfigFilePath(mode); + // load config that is compatible with kbn-test input format + const clusterConfig = await readConfigFile(configPath); + // construct config for Playwright Test + const scoutServerConfig = clusterConfig.getScoutTestConfig(); + // save test config to the file + saveScoutTestConfigOnDisk(scoutServerConfig, log); + return clusterConfig; +} diff --git a/packages/kbn-scout/src/config/utils/save_scout_test_config.test.ts b/packages/kbn-scout/src/config/utils/save_scout_test_config.test.ts new file mode 100644 index 0000000000000..770145c8fdf35 --- /dev/null +++ b/packages/kbn-scout/src/config/utils/save_scout_test_config.test.ts @@ -0,0 +1,130 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import path from 'path'; +import Fs from 'fs'; +import { ToolingLog } from '@kbn/tooling-log'; +import { saveScoutTestConfigOnDisk } from './save_scout_test_config'; + +const MOCKED_SCOUT_SERVERS_ROOT = '/mock/repo/root/scout/servers'; + +jest.mock('fs'); + +jest.mock('@kbn/repo-info', () => ({ + REPO_ROOT: '/mock/repo/root', +})); + +jest.mock('@kbn/scout-info', () => ({ + SCOUT_SERVERS_ROOT: '/mock/repo/root/scout/servers', +})); + +const testServersConfig = { + hosts: { + kibana: 'http://localhost:5601', + elasticsearch: 'http://localhost:9220', + }, + auth: { + username: 'elastic', + password: 'changeme', + }, + serverless: true, + isCloud: true, + license: 'trial', + cloudUsersFilePath: '/path/to/users', +}; + +jest.mock('path', () => ({ + ...jest.requireActual('path'), + join: jest.fn((...args) => args.join('/')), +})); + +describe('saveScoutTestConfigOnDisk', () => { + let mockLog: ToolingLog; + + beforeEach(() => { + jest.clearAllMocks(); + mockLog = { + debug: jest.fn(), + info: jest.fn(), + error: jest.fn(), + } as unknown as ToolingLog; + }); + + it('should save configuration to disk successfully', () => { + const mockConfigFilePath = `${MOCKED_SCOUT_SERVERS_ROOT}/local.json`; + + // Mock path.join to return a fixed file path + (path.join as jest.Mock).mockReturnValueOnce(mockConfigFilePath); + + // Mock Fs.existsSync to return true + (Fs.existsSync as jest.Mock).mockReturnValueOnce(true); + + // Mock Fs.writeFileSync to do nothing + const writeFileSyncMock = jest.spyOn(Fs, 'writeFileSync'); + + saveScoutTestConfigOnDisk(testServersConfig, mockLog); + + expect(Fs.existsSync).toHaveBeenCalledWith(MOCKED_SCOUT_SERVERS_ROOT); + expect(writeFileSyncMock).toHaveBeenCalledWith( + mockConfigFilePath, + JSON.stringify(testServersConfig, null, 2), + 'utf-8' + ); + expect(mockLog.info).toHaveBeenCalledWith( + `scout: Test server configuration saved at ${mockConfigFilePath}` + ); + }); + + it('should throw an error if writing to file fails', () => { + const mockConfigFilePath = `${MOCKED_SCOUT_SERVERS_ROOT}/local.json`; + + (path.join as jest.Mock).mockReturnValueOnce(mockConfigFilePath); + (Fs.existsSync as jest.Mock).mockReturnValueOnce(true); + + // Mock writeFileSync to throw an error + (Fs.writeFileSync as jest.Mock).mockImplementationOnce(() => { + throw new Error('Disk is full'); + }); + + expect(() => saveScoutTestConfigOnDisk(testServersConfig, mockLog)).toThrow( + `Failed to save test server configuration at ${mockConfigFilePath}` + ); + expect(mockLog.error).toHaveBeenCalledWith( + `scout: Failed to save test server configuration - Disk is full` + ); + }); + + it('should create configuration directory if it does not exist', () => { + const mockConfigFilePath = `${MOCKED_SCOUT_SERVERS_ROOT}/local.json`; + + (path.join as jest.Mock).mockReturnValueOnce(mockConfigFilePath); + + // Mock existsSync to simulate non-existent directory + (Fs.existsSync as jest.Mock).mockReturnValueOnce(false); + + const mkdirSyncMock = jest.spyOn(Fs, 'mkdirSync'); + const writeFileSyncMock = jest.spyOn(Fs, 'writeFileSync'); + + saveScoutTestConfigOnDisk(testServersConfig, mockLog); + + expect(Fs.existsSync).toHaveBeenCalledWith(MOCKED_SCOUT_SERVERS_ROOT); + expect(mkdirSyncMock).toHaveBeenCalledWith(MOCKED_SCOUT_SERVERS_ROOT, { recursive: true }); + expect(writeFileSyncMock).toHaveBeenCalledWith( + mockConfigFilePath, + JSON.stringify(testServersConfig, null, 2), + 'utf-8' + ); + expect(mockLog.debug).toHaveBeenCalledWith( + `scout: creating configuration directory: ${MOCKED_SCOUT_SERVERS_ROOT}` + ); + expect(mockLog.info).toHaveBeenCalledWith( + `scout: Test server configuration saved at ${mockConfigFilePath}` + ); + }); +}); diff --git a/packages/kbn-scout/src/config/utils/save_scout_test_config.ts b/packages/kbn-scout/src/config/utils/save_scout_test_config.ts new file mode 100644 index 0000000000000..12d78d804bf9f --- /dev/null +++ b/packages/kbn-scout/src/config/utils/save_scout_test_config.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import * as Fs from 'fs'; +import path from 'path'; +import { ToolingLog } from '@kbn/tooling-log'; +import { SCOUT_SERVERS_ROOT } from '@kbn/scout-info'; +import { ScoutTestConfig } from '../../types'; + +/** + * Saves Scout server configuration to the disk. + * @param testServersConfig configuration to be saved + * @param log Logger instance to report errors or debug information. + */ +export const saveScoutTestConfigOnDisk = (testServersConfig: ScoutTestConfig, log: ToolingLog) => { + const configFilePath = path.join(SCOUT_SERVERS_ROOT, `local.json`); + + try { + const jsonData = JSON.stringify(testServersConfig, null, 2); + + if (!Fs.existsSync(SCOUT_SERVERS_ROOT)) { + log.debug(`scout: creating configuration directory: ${SCOUT_SERVERS_ROOT}`); + Fs.mkdirSync(SCOUT_SERVERS_ROOT, { recursive: true }); + } + + Fs.writeFileSync(configFilePath, jsonData, 'utf-8'); + log.info(`scout: Test server configuration saved at ${configFilePath}`); + } catch (error) { + log.error(`scout: Failed to save test server configuration - ${error.message}`); + throw new Error(`Failed to save test server configuration at ${configFilePath}`); + } +}; diff --git a/packages/kbn-scout/src/config/utils/utils.ts b/packages/kbn-scout/src/config/utils/utils.ts new file mode 100644 index 0000000000000..4fab350e8b4b1 --- /dev/null +++ b/packages/kbn-scout/src/config/utils/utils.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import getopts from 'getopts'; +import { ServerlessProjectType } from '@kbn/es'; + +export const formatCurrentDate = () => { + const now = new Date(); + + const format = (num: number, length: number) => String(num).padStart(length, '0'); + + return ( + `${format(now.getDate(), 2)}/${format(now.getMonth() + 1, 2)}/${now.getFullYear()} ` + + `${format(now.getHours(), 2)}:${format(now.getMinutes(), 2)}:${format(now.getSeconds(), 2)}.` + + `${format(now.getMilliseconds(), 3)}` + ); +}; + +export const getProjectType = (kbnServerArgs: string[]) => { + const options = getopts(kbnServerArgs); + return options.serverless as ServerlessProjectType; +}; diff --git a/packages/kbn-scout/src/playwright/config/create_config.test.ts b/packages/kbn-scout/src/playwright/config/create_config.test.ts new file mode 100644 index 0000000000000..730bdd5ef55e4 --- /dev/null +++ b/packages/kbn-scout/src/playwright/config/create_config.test.ts @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { SCOUT_SERVERS_ROOT } from '@kbn/scout-info'; +import { createPlaywrightConfig } from './create_config'; +import { VALID_CONFIG_MARKER } from '../types'; + +describe('createPlaywrightConfig', () => { + it('should return a valid default Playwright configuration', () => { + const testDir = './my_tests'; + const config = createPlaywrightConfig({ testDir }); + + expect(config.testDir).toBe(testDir); + expect(config.workers).toBe(1); + expect(config.fullyParallel).toBe(false); + expect(config.use).toEqual({ + serversConfigDir: SCOUT_SERVERS_ROOT, + [VALID_CONFIG_MARKER]: true, + screenshot: 'only-on-failure', + trace: 'on-first-retry', + }); + expect(config.globalSetup).toBeUndefined(); + expect(config.globalTeardown).toBeUndefined(); + expect(config.reporter).toEqual([ + ['html', { open: 'never', outputFolder: './output/reports' }], + ['json', { outputFile: './output/reports/test-results.json' }], + ['@kbn/scout-reporting/src/reporting/playwright.ts', { name: 'scout-playwright' }], + ]); + expect(config.timeout).toBe(60000); + expect(config.expect?.timeout).toBe(10000); + expect(config.outputDir).toBe('./output/test-artifacts'); + expect(config.projects![0].name).toEqual('chromium'); + }); + + it(`should override 'workers' count in Playwright configuration`, () => { + const testDir = './my_tests'; + const workers = 2; + + const config = createPlaywrightConfig({ testDir, workers }); + expect(config.workers).toBe(workers); + }); +}); diff --git a/packages/kbn-scout/src/playwright/config/create_config.ts b/packages/kbn-scout/src/playwright/config/create_config.ts new file mode 100644 index 0000000000000..cb1e371cb43e7 --- /dev/null +++ b/packages/kbn-scout/src/playwright/config/create_config.ts @@ -0,0 +1,76 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { defineConfig, PlaywrightTestConfig, devices } from '@playwright/test'; +import { scoutPlaywrightReporter } from '@kbn/scout-reporting'; +import { SCOUT_SERVERS_ROOT } from '@kbn/scout-info'; +import { ScoutPlaywrightOptions, ScoutTestOptions, VALID_CONFIG_MARKER } from '../types'; + +export function createPlaywrightConfig(options: ScoutPlaywrightOptions): PlaywrightTestConfig { + return defineConfig({ + testDir: options.testDir, + /* Run tests in files in parallel */ + fullyParallel: false, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: options.workers ?? 1, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: [ + ['html', { outputFolder: './output/reports', open: 'never' }], // HTML report configuration + ['json', { outputFile: './output/reports/test-results.json' }], // JSON report + scoutPlaywrightReporter({ name: 'scout-playwright' }), // Scout report + ], + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + serversConfigDir: SCOUT_SERVERS_ROOT, + [VALID_CONFIG_MARKER]: true, + /* Base URL to use in actions like `await page.goto('/')`. */ + // baseURL: 'http://127.0.0.1:3000', + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + screenshot: 'only-on-failure', + // video: 'retain-on-failure', + // storageState: './output/reports/state.json', // Store session state (like cookies) + }, + + // Timeout for each test, includes test, hooks and fixtures + timeout: 60000, + + // Timeout for each assertion + expect: { + timeout: 10000, + }, + + outputDir: './output/test-artifacts', // For other test artifacts (screenshots, videos, traces) + + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + + // { + // name: 'firefox', + // use: { ...devices['Desktop Firefox'] }, + // }, + ], + + /* Run your local dev server before starting the tests */ + // webServer: { + // command: 'npm run start', + // url: 'http://127.0.0.1:3000', + // reuseExistingServer: !process.env.CI, + // }, + }); +} diff --git a/packages/kbn-scout/src/playwright/config/index.ts b/packages/kbn-scout/src/playwright/config/index.ts index cb1e371cb43e7..c3949cca0af7a 100644 --- a/packages/kbn-scout/src/playwright/config/index.ts +++ b/packages/kbn-scout/src/playwright/config/index.ts @@ -7,70 +7,4 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { defineConfig, PlaywrightTestConfig, devices } from '@playwright/test'; -import { scoutPlaywrightReporter } from '@kbn/scout-reporting'; -import { SCOUT_SERVERS_ROOT } from '@kbn/scout-info'; -import { ScoutPlaywrightOptions, ScoutTestOptions, VALID_CONFIG_MARKER } from '../types'; - -export function createPlaywrightConfig(options: ScoutPlaywrightOptions): PlaywrightTestConfig { - return defineConfig({ - testDir: options.testDir, - /* Run tests in files in parallel */ - fullyParallel: false, - /* Fail the build on CI if you accidentally left test.only in the source code. */ - forbidOnly: !!process.env.CI, - /* Retry on CI only */ - retries: process.env.CI ? 2 : 0, - /* Opt out of parallel tests on CI. */ - workers: options.workers ?? 1, - /* Reporter to use. See https://playwright.dev/docs/test-reporters */ - reporter: [ - ['html', { outputFolder: './output/reports', open: 'never' }], // HTML report configuration - ['json', { outputFile: './output/reports/test-results.json' }], // JSON report - scoutPlaywrightReporter({ name: 'scout-playwright' }), // Scout report - ], - /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ - use: { - serversConfigDir: SCOUT_SERVERS_ROOT, - [VALID_CONFIG_MARKER]: true, - /* Base URL to use in actions like `await page.goto('/')`. */ - // baseURL: 'http://127.0.0.1:3000', - - /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ - trace: 'on-first-retry', - screenshot: 'only-on-failure', - // video: 'retain-on-failure', - // storageState: './output/reports/state.json', // Store session state (like cookies) - }, - - // Timeout for each test, includes test, hooks and fixtures - timeout: 60000, - - // Timeout for each assertion - expect: { - timeout: 10000, - }, - - outputDir: './output/test-artifacts', // For other test artifacts (screenshots, videos, traces) - - /* Configure projects for major browsers */ - projects: [ - { - name: 'chromium', - use: { ...devices['Desktop Chrome'] }, - }, - - // { - // name: 'firefox', - // use: { ...devices['Desktop Firefox'] }, - // }, - ], - - /* Run your local dev server before starting the tests */ - // webServer: { - // command: 'npm run start', - // url: 'http://127.0.0.1:3000', - // reuseExistingServer: !process.env.CI, - // }, - }); -} +export { createPlaywrightConfig } from './create_config'; diff --git a/packages/kbn-scout/src/playwright/fixtures/types/worker_scope.ts b/packages/kbn-scout/src/playwright/fixtures/types/worker_scope.ts index c42f7143d3191..29283cbea4517 100644 --- a/packages/kbn-scout/src/playwright/fixtures/types/worker_scope.ts +++ b/packages/kbn-scout/src/playwright/fixtures/types/worker_scope.ts @@ -14,7 +14,7 @@ import { LoadActionPerfOptions } from '@kbn/es-archiver'; import { IndexStats } from '@kbn/es-archiver/src/lib/stats'; import type { UiSettingValues } from '@kbn/test/src/kbn_client/kbn_client_ui_settings'; -import { ScoutServerConfig } from '../../../types'; +import { ScoutTestConfig } from '../../../types'; import { KibanaUrl } from '../../../common/services/kibana_url'; export interface EsArchiverFixture { @@ -58,7 +58,7 @@ export interface UiSettingsFixture { */ export interface ScoutWorkerFixtures { log: ToolingLog; - config: ScoutServerConfig; + config: ScoutTestConfig; kbnUrl: KibanaUrl; esClient: Client; kbnClient: KbnClient; diff --git a/packages/kbn-scout/src/playwright/runner/config_loader.ts b/packages/kbn-scout/src/playwright/runner/config_loader.ts new file mode 100644 index 0000000000000..c7758f1415634 --- /dev/null +++ b/packages/kbn-scout/src/playwright/runner/config_loader.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +/** + * Loads the config module dynamically + * @param configPath config absolute path + * @returns + */ +export async function loadConfigModule(configPath: string) { + return import(configPath); +} diff --git a/packages/kbn-scout/src/playwright/runner/config_validator.test.ts b/packages/kbn-scout/src/playwright/runner/config_validator.test.ts new file mode 100644 index 0000000000000..eb6d97ee8e576 --- /dev/null +++ b/packages/kbn-scout/src/playwright/runner/config_validator.test.ts @@ -0,0 +1,99 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { validatePlaywrightConfig } from './config_validator'; +import * as configLoader from './config_loader'; +import Fs from 'fs'; +import { VALID_CONFIG_MARKER } from '../types'; + +jest.mock('fs'); + +const existsSyncMock = jest.spyOn(Fs, 'existsSync'); +const loadConfigModuleMock = jest.spyOn(configLoader, 'loadConfigModule'); + +describe('validatePlaywrightConfig', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should pass validation for a valid config file', async () => { + const configPath = 'valid/path/config.ts'; + existsSyncMock.mockReturnValue(true); + loadConfigModuleMock.mockResolvedValue({ + default: { + use: { [VALID_CONFIG_MARKER]: true }, + testDir: './tests', + }, + }); + + await expect(validatePlaywrightConfig(configPath)).resolves.not.toThrow(); + }); + + it('should throw an error if the config file does not have the valid marker', async () => { + const configPath = 'valid/path/config.ts'; + existsSyncMock.mockReturnValue(true); + loadConfigModuleMock.mockResolvedValue({ + default: { + use: {}, + testDir: './tests', + }, + }); + + await expect(validatePlaywrightConfig(configPath)).rejects.toThrow( + `The config file at "${configPath}" must be created with "createPlaywrightConfig" from '@kbn/scout' package:` + ); + }); + + it(`should throw an error if the config file does not have a 'testDir'`, async () => { + const configPath = 'valid/path/config.ts'; + existsSyncMock.mockReturnValue(true); + loadConfigModuleMock.mockResolvedValue({ + default: { + use: { [VALID_CONFIG_MARKER]: true }, + }, + }); + + await expect(validatePlaywrightConfig(configPath)).rejects.toThrow( + `The config file at "${configPath}" must export a valid Playwright configuration with "testDir" property.` + ); + }); + + it('should throw an error if the config file does not have a default export', async () => { + const configPath = 'valid/path/config.ts'; + existsSyncMock.mockReturnValue(true); + loadConfigModuleMock.mockResolvedValue({ + test: { + use: {}, + testDir: './tests', + }, + }); + + await expect(validatePlaywrightConfig(configPath)).rejects.toThrow( + `The config file at "${configPath}" must export default function` + ); + }); + + it('should throw an error if the path does not exist', async () => { + const configPath = 'invalid/path/to/config.ts'; + existsSyncMock.mockReturnValue(false); + + await expect(validatePlaywrightConfig(configPath)).rejects.toThrow( + `Path to a valid TypeScript config file is required: --config ` + ); + }); + + it('should throw an error if the file does not have a .ts extension', async () => { + const configPath = 'config.js'; + existsSyncMock.mockReturnValue(true); + + await expect(validatePlaywrightConfig(configPath)).rejects.toThrow( + `Path to a valid TypeScript config file is required: --config ` + ); + }); +}); diff --git a/packages/kbn-scout/src/playwright/runner/config_validator.ts b/packages/kbn-scout/src/playwright/runner/config_validator.ts index a066a6dfba30c..41c44cfc6eabd 100644 --- a/packages/kbn-scout/src/playwright/runner/config_validator.ts +++ b/packages/kbn-scout/src/playwright/runner/config_validator.ts @@ -7,34 +7,35 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import * as Fs from 'fs'; -import { REPO_ROOT } from '@kbn/repo-info'; +import Fs from 'fs'; import { PlaywrightTestConfig } from 'playwright/test'; -import path from 'path'; import { createFlagError } from '@kbn/dev-cli-errors'; import { ScoutTestOptions, VALID_CONFIG_MARKER } from '../types'; +import { loadConfigModule } from './config_loader'; export async function validatePlaywrightConfig(configPath: string) { - const fullPath = path.resolve(REPO_ROOT, configPath); - // Check if the path exists and has a .ts extension - if (!configPath || !Fs.existsSync(fullPath) || !configPath.endsWith('.ts')) { + if (!configPath || !Fs.existsSync(configPath) || !configPath.endsWith('.ts')) { throw createFlagError( `Path to a valid TypeScript config file is required: --config ` ); } - // Dynamically import the file to check for a default export - const configModule = await import(fullPath); + const configModule = await loadConfigModule(configPath); + // Check for a default export const config = configModule.default as PlaywrightTestConfig; - // Check if the config's 'use' property has the valid marker + if (config === undefined) { + throw createFlagError(`The config file at "${configPath}" must export default function`); + } + if (!config?.use?.[VALID_CONFIG_MARKER]) { + // Check if the config's 'use' property has the valid marker throw createFlagError( `The config file at "${configPath}" must be created with "createPlaywrightConfig" from '@kbn/scout' package:\n -export default createPlaywrightConfig({ - testDir: './tests', -});` + export default createPlaywrightConfig({ + testDir: './tests', + });` ); } diff --git a/packages/kbn-scout/src/playwright/runner/flags.test.ts b/packages/kbn-scout/src/playwright/runner/flags.test.ts new file mode 100644 index 0000000000000..94683146e5278 --- /dev/null +++ b/packages/kbn-scout/src/playwright/runner/flags.test.ts @@ -0,0 +1,102 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { parseTestFlags } from './flags'; +import { FlagsReader } from '@kbn/dev-cli-runner'; +import * as configValidator from './config_validator'; + +const validatePlaywrightConfigMock = jest.spyOn(configValidator, 'validatePlaywrightConfig'); + +describe('parseTestFlags', () => { + it(`should throw an error without 'config' flag`, async () => { + const flags = new FlagsReader({ + stateful: true, + logToFile: false, + headed: false, + }); + + await expect(parseTestFlags(flags)).rejects.toThrow( + 'Path to playwright config is required: --config ' + ); + }); + + it(`should throw an error with '--stateful' flag as string value`, async () => { + const flags = new FlagsReader({ + stateful: 'true', + logToFile: false, + headed: false, + }); + + await expect(parseTestFlags(flags)).rejects.toThrow('expected --stateful to be a boolean'); + }); + + it(`should throw an error with '--serverless' flag as boolean`, async () => { + const flags = new FlagsReader({ + serverless: true, + logToFile: false, + headed: false, + }); + + await expect(parseTestFlags(flags)).rejects.toThrow('expected --serverless to be a string'); + }); + + it(`should throw an error with incorrect '--serverless' flag`, async () => { + const flags = new FlagsReader({ + serverless: 'a', + logToFile: false, + headed: false, + }); + + await expect(parseTestFlags(flags)).rejects.toThrow( + 'invalid --serverless, expected one of "es", "oblt", "security"' + ); + }); + + it(`should parse with correct config and serverless flags`, async () => { + const flags = new FlagsReader({ + config: '/path/to/config', + stateful: false, + serverless: 'oblt', + logToFile: false, + headed: false, + }); + validatePlaywrightConfigMock.mockResolvedValueOnce(); + const result = await parseTestFlags(flags); + + expect(result).toEqual({ + mode: 'serverless=oblt', + configPath: '/path/to/config', + headed: false, + esFrom: undefined, + installDir: undefined, + logsDir: undefined, + }); + }); + + it(`should parse with correct config and stateful flags`, async () => { + const flags = new FlagsReader({ + config: '/path/to/config', + stateful: true, + logToFile: false, + headed: true, + esFrom: 'snapshot', + }); + validatePlaywrightConfigMock.mockResolvedValueOnce(); + const result = await parseTestFlags(flags); + + expect(result).toEqual({ + mode: 'stateful', + configPath: '/path/to/config', + headed: true, + esFrom: 'snapshot', + installDir: undefined, + logsDir: undefined, + }); + }); +}); diff --git a/packages/kbn-scout/src/playwright/runner/flags.ts b/packages/kbn-scout/src/playwright/runner/flags.ts index 7d39d821705c1..e5bded7d6f1c2 100644 --- a/packages/kbn-scout/src/playwright/runner/flags.ts +++ b/packages/kbn-scout/src/playwright/runner/flags.ts @@ -7,6 +7,8 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ +import { REPO_ROOT } from '@kbn/repo-info'; +import path from 'path'; import { FlagOptions, FlagsReader } from '@kbn/dev-cli-runner'; import { createFlagError } from '@kbn/dev-cli-errors'; import { SERVER_FLAG_OPTIONS, parseServerFlags } from '../../servers'; @@ -42,7 +44,8 @@ export async function parseTestFlags(flags: FlagsReader) { throw createFlagError(`Path to playwright config is required: --config `); } - await validatePlaywrightConfig(configPath); + const configFullPath = path.resolve(REPO_ROOT, configPath); + await validatePlaywrightConfig(configFullPath); return { ...options, diff --git a/packages/kbn-scout/src/playwright/utils/index.ts b/packages/kbn-scout/src/playwright/utils/index.ts index 8956c6d7cc18f..347fe7f22d05b 100644 --- a/packages/kbn-scout/src/playwright/utils/index.ts +++ b/packages/kbn-scout/src/playwright/utils/index.ts @@ -7,23 +7,4 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import moment from 'moment'; -import { Config } from '../../config'; -import { tagsByMode } from '../tags'; - -export const serviceLoadedMsg = (name: string) => `scout service loaded: ${name}`; - -export const isValidUTCDate = (date: string): boolean => { - return !isNaN(Date.parse(date)) && new Date(date).toISOString() === date; -}; - -export function formatTime(date: string, fmt: string = 'MMM D, YYYY @ HH:mm:ss.SSS') { - return moment.utc(date, fmt).format(); -} - -export const getPlaywrightGrepTag = (config: Config): string => { - const serversConfig = config.getTestServersConfig(); - return serversConfig.serverless - ? tagsByMode.serverless[serversConfig.projectType!] - : tagsByMode.stateful; -}; +export { serviceLoadedMsg, isValidUTCDate, formatTime, getPlaywrightGrepTag } from './runner_utils'; diff --git a/packages/kbn-scout/src/playwright/utils/runner_utils.test.ts b/packages/kbn-scout/src/playwright/utils/runner_utils.test.ts new file mode 100644 index 0000000000000..007555ea4e7ad --- /dev/null +++ b/packages/kbn-scout/src/playwright/utils/runner_utils.test.ts @@ -0,0 +1,108 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { isValidUTCDate, formatTime, getPlaywrightGrepTag } from './runner_utils'; +import moment from 'moment'; +jest.mock('moment', () => { + const actualMoment = jest.requireActual('moment'); + return { + ...actualMoment, + utc: jest.fn((date, fmt) => actualMoment(date, fmt)), + }; +}); + +describe('isValidUTCDate', () => { + it('should return true for valid UTC date strings', () => { + expect(isValidUTCDate('2019-04-27T23:56:51.374Z')).toBe(true); + }); + + it('should return false for invalid date strings', () => { + expect(isValidUTCDate('invalid-date')).toBe(false); + }); + + it('should return false for valid non-UTC date strings', () => { + expect(isValidUTCDate('2015-09-19T06:31:44')).toBe(false); + expect(isValidUTCDate('Sep 19, 2015 @ 06:31:44.000')).toBe(false); + }); +}); + +describe('formatTime', () => { + it('should format the time using the default format', () => { + const mockDate = '2024-12-16T12:00:00.000Z'; + const mockFormat = 'MMM D, YYYY @ HH:mm:ss.SSS'; + (moment.utc as jest.Mock).mockReturnValue({ format: () => 'Dec 16, 2024 @ 12:00:00.000' }); + + const result = formatTime(mockDate); + + expect(moment.utc).toHaveBeenCalledWith(mockDate, mockFormat); + expect(result).toBe('Dec 16, 2024 @ 12:00:00.000'); + }); + + it('should format the time using a custom format', () => { + const mockDate = '2024-12-16T12:00:00.000Z'; + const customFormat = 'YYYY-MM-DD'; + (moment.utc as jest.Mock).mockReturnValue({ format: () => '2024-12-16' }); + + const result = formatTime(mockDate, customFormat); + + expect(moment.utc).toHaveBeenCalledWith(mockDate, customFormat); + expect(result).toBe('2024-12-16'); + }); +}); + +describe('getPlaywrightGrepTag', () => { + const mockConfig = { + getScoutTestConfig: jest.fn(), + }; + + it('should return the correct tag for serverless mode', () => { + mockConfig.getScoutTestConfig.mockReturnValue({ + serverless: true, + projectType: 'oblt', + }); + + const result = getPlaywrightGrepTag(mockConfig as any); + + expect(mockConfig.getScoutTestConfig).toHaveBeenCalled(); + expect(result).toBe('@svlOblt'); + }); + + it('should return the correct tag for stateful mode', () => { + mockConfig.getScoutTestConfig.mockReturnValue({ + serverless: false, + }); + + const result = getPlaywrightGrepTag(mockConfig as any); + + expect(mockConfig.getScoutTestConfig).toHaveBeenCalled(); + expect(result).toBe('@ess'); + }); + + it('should throw an error if projectType is missing in serverless mode', () => { + mockConfig.getScoutTestConfig.mockReturnValue({ + serverless: true, + projectType: undefined, + }); + + expect(() => getPlaywrightGrepTag(mockConfig as any)).toThrow( + `'projectType' is required to determine tags for 'serverless' mode.` + ); + }); + + it('should throw an error if unknown projectType is set in serverless mode', () => { + mockConfig.getScoutTestConfig.mockReturnValue({ + serverless: true, + projectType: 'a', + }); + + expect(() => getPlaywrightGrepTag(mockConfig as any)).toThrow( + `No tags found for projectType: 'a'.` + ); + }); +}); diff --git a/packages/kbn-scout/src/playwright/utils/runner_utils.ts b/packages/kbn-scout/src/playwright/utils/runner_utils.ts new file mode 100644 index 0000000000000..fda016f9389c8 --- /dev/null +++ b/packages/kbn-scout/src/playwright/utils/runner_utils.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import moment from 'moment'; +import { Config } from '../../config'; +import { tagsByMode } from '../tags'; + +export const serviceLoadedMsg = (name: string) => `scout service loaded: ${name}`; + +export const isValidUTCDate = (date: string): boolean => { + return !isNaN(Date.parse(date)) && new Date(date).toISOString() === date; +}; + +export function formatTime(date: string, fmt: string = 'MMM D, YYYY @ HH:mm:ss.SSS') { + return moment.utc(date, fmt).format(); +} + +export const getPlaywrightGrepTag = (config: Config): string => { + const serversConfig = config.getScoutTestConfig(); + + if (serversConfig.serverless) { + const { projectType } = serversConfig; + + if (!projectType) { + throw new Error(`'projectType' is required to determine tags for 'serverless' mode.`); + } + + const tag = tagsByMode.serverless[projectType]; + if (!tag) { + throw new Error(`No tags found for projectType: '${projectType}'.`); + } + + return tag; + } + + return tagsByMode.stateful; +}; diff --git a/packages/kbn-scout/src/servers/flags.test.ts b/packages/kbn-scout/src/servers/flags.test.ts new file mode 100644 index 0000000000000..1bf6f18c0236b --- /dev/null +++ b/packages/kbn-scout/src/servers/flags.test.ts @@ -0,0 +1,74 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { parseServerFlags } from './flags'; +import { FlagsReader } from '@kbn/dev-cli-runner'; + +describe('parseServerFlags', () => { + it(`should throw an error with '--stateful' flag as string value`, () => { + const flags = new FlagsReader({ + stateful: 'true', + logToFile: false, + }); + + expect(() => parseServerFlags(flags)).toThrow('expected --stateful to be a boolean'); + }); + + it(`should throw an error with '--serverless' flag as boolean`, () => { + const flags = new FlagsReader({ + serverless: true, + logToFile: false, + }); + + expect(() => parseServerFlags(flags)).toThrow('expected --serverless to be a string'); + }); + + it(`should throw an error with incorrect '--serverless' flag`, () => { + const flags = new FlagsReader({ + serverless: 'a', + logToFile: false, + }); + + expect(() => parseServerFlags(flags)).toThrow( + 'invalid --serverless, expected one of "es", "oblt", "security"' + ); + }); + + it(`should parse with correct config and serverless flags`, () => { + const flags = new FlagsReader({ + stateful: false, + serverless: 'oblt', + logToFile: false, + }); + const result = parseServerFlags(flags); + + expect(result).toEqual({ + mode: 'serverless=oblt', + esFrom: undefined, + installDir: undefined, + logsDir: undefined, + }); + }); + + it(`should parse with correct config and stateful flags`, () => { + const flags = new FlagsReader({ + stateful: true, + logToFile: false, + esFrom: 'snapshot', + }); + const result = parseServerFlags(flags); + + expect(result).toEqual({ + mode: 'stateful', + esFrom: 'snapshot', + installDir: undefined, + logsDir: undefined, + }); + }); +}); diff --git a/packages/kbn-scout/src/types/index.ts b/packages/kbn-scout/src/types/index.ts index 024a00ec29e45..5f79c9ba17b88 100644 --- a/packages/kbn-scout/src/types/index.ts +++ b/packages/kbn-scout/src/types/index.ts @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -export * from './config'; +export * from './server_config'; export * from './cli'; -export * from './servers'; +export * from './test_config'; export * from './services'; diff --git a/packages/kbn-scout/src/types/config.d.ts b/packages/kbn-scout/src/types/server_config.d.ts similarity index 96% rename from packages/kbn-scout/src/types/config.d.ts rename to packages/kbn-scout/src/types/server_config.d.ts index 08c4bc5f3f9b0..ea35377d88ca4 100644 --- a/packages/kbn-scout/src/types/config.d.ts +++ b/packages/kbn-scout/src/types/server_config.d.ts @@ -9,7 +9,7 @@ import { UrlParts } from '@kbn/test'; -export interface ScoutLoaderConfig { +export interface ScoutServerConfig { serverless?: boolean; servers: { kibana: UrlParts; diff --git a/packages/kbn-scout/src/types/servers.d.ts b/packages/kbn-scout/src/types/test_config.d.ts similarity index 93% rename from packages/kbn-scout/src/types/servers.d.ts rename to packages/kbn-scout/src/types/test_config.d.ts index 587e1d213b9ba..ffe7d56f83ed5 100644 --- a/packages/kbn-scout/src/types/servers.d.ts +++ b/packages/kbn-scout/src/types/test_config.d.ts @@ -9,10 +9,11 @@ import { ServerlessProjectType } from '@kbn/es'; -export interface ScoutServerConfig { +export interface ScoutTestConfig { serverless: boolean; projectType?: ServerlessProjectType; isCloud: boolean; + license: string; cloudUsersFilePath: string; hosts: { kibana: string;