[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
```
This commit is contained in:
Dzmitry Lemechko 2024-12-19 17:36:07 +01:00 committed by GitHub
parent 87d15ba2e1
commit 2ba3247526
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
43 changed files with 1234 additions and 227 deletions

View file

@ -193,6 +193,12 @@ npx playwright test --config <plugin-path>/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.

View file

@ -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,

View file

@ -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'));

View file

@ -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'));

View file

@ -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);

View file

@ -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);
});
});

View file

@ -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<string, any>;
private [$values]: ScoutServerConfig;
constructor(data: Record<string, any>) {
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({

View file

@ -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';

View file

@ -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';

View file

@ -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);
});
});
});

View file

@ -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<Config> => {
export const readConfigFile = async (configPath: string): Promise<Config> => {
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}`);
}

View file

@ -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(),

View file

@ -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,

View file

@ -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,

View file

@ -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,

View file

@ -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({

View file

@ -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: {

View file

@ -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;

View file

@ -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<Config> {
// 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;
};

View file

@ -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);
});
});

View file

@ -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=<type>".`
`Invalid config format: "${config}". Expected "stateful" or "serverless=<type>".`
);
}
return path.join(__dirname, 'serverless', `${type}.serverless.config.ts`);
return path.join(baseDir, 'serverless', `${type}.serverless.config.ts`);
};

View file

@ -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';

View file

@ -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();
});
});

View file

@ -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<Config> {
// 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;
}

View file

@ -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}`
);
});
});

View file

@ -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}`);
}
};

View file

@ -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;
};

View file

@ -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);
});
});

View file

@ -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<ScoutTestOptions>({
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,
// },
});
}

View file

@ -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<ScoutTestOptions>({
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';

View file

@ -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;

View file

@ -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);
}

View file

@ -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 <relative path to .ts file>`
);
});
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 <relative path to .ts file>`
);
});
});

View file

@ -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 <relative path to .ts file>`
);
}
// 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<ScoutTestOptions>;
// 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',
});`
);
}

View file

@ -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 <file path>'
);
});
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,
});
});
});

View file

@ -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 <file path>`);
}
await validatePlaywrightConfig(configPath);
const configFullPath = path.resolve(REPO_ROOT, configPath);
await validatePlaywrightConfig(configFullPath);
return {
...options,

View file

@ -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';

View file

@ -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'.`
);
});
});

View file

@ -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;
};

View file

@ -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,
});
});
});

View file

@ -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';

View file

@ -9,7 +9,7 @@
import { UrlParts } from '@kbn/test';
export interface ScoutLoaderConfig {
export interface ScoutServerConfig {
serverless?: boolean;
servers: {
kibana: UrlParts;

View file

@ -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;