Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,42 @@ describe('generateSecretsSchemaFromSpec', () => {
const schema = generateSecretsSchemaFromSpec({ types: [] });
expect(z.toJSONSchema(schema)).toMatchSnapshot();
});

describe('runtime parse behavior', () => {
test('parses valid secrets for none auth type', () => {
const schema = generateSecretsSchemaFromSpec({ types: ['none'] });
expect(schema.parse({ authType: 'none' })).toEqual({ authType: 'none' });
});

test('parses valid secrets for basic auth type', () => {
const schema = generateSecretsSchemaFromSpec({ types: ['basic'] });
const secrets = { authType: 'basic', username: 'user', password: 'pass' };
expect(schema.parse(secrets)).toEqual(secrets);
});

test('parses valid secrets for bearer auth type', () => {
const schema = generateSecretsSchemaFromSpec({ types: ['bearer'] });
const secrets = { authType: 'bearer', token: 'my-token' };
expect(schema.parse(secrets)).toEqual(secrets);
});

test('throws for invalid authType', () => {
const schema = generateSecretsSchemaFromSpec({ types: ['basic'] });
expect(() =>
schema.parse({ authType: 'invalid_type', username: 'u', password: 'p' })
).toThrow();
});

test('throws for missing required fields in basic auth', () => {
const schema = generateSecretsSchemaFromSpec({ types: ['basic'] });
expect(() => schema.parse({ authType: 'basic', username: 'user' })).toThrow(
/password|Required/
);
});

test('parses empty object when no auth types', () => {
const schema = generateSecretsSchemaFromSpec({ types: [] });
expect(schema.parse({})).toEqual({});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -1538,6 +1538,46 @@ describe('Action Executor', () => {
}
});
}

describe('schema validation on execute', () => {
test('returns error result when params fail Zod validation', async () => {
encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce(
connectorSavedObject
);
connectorTypeRegistry.get.mockReturnValueOnce(connectorType);

const result = await actionExecutor.execute({
...executeParams,
params: { foo: 'not-a-boolean' },
});

expect(result.status).toBe('error');
expect(result.message).toMatch(/error validating action params/);
expect(result.errorSource).toBe(TaskErrorSource.USER);
expect(connectorType.executor).not.toHaveBeenCalled();
});

test('returns error result when config fails Zod validation against saved object', async () => {
encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce(
connectorSavedObject
);
const typeWithStringBar: jest.Mocked<ConnectorType> = {
...connectorType,
validate: {
...connectorType.validate,
config: { schema: z.object({ bar: z.string() }) },
},
};
connectorTypeRegistry.get.mockReturnValueOnce(typeWithStringBar);

const result = await actionExecutor.execute(executeParams);

expect(result.status).toBe('error');
expect(result.message).toMatch(/error validating connector type config/);
expect(result.errorSource).toBe(TaskErrorSource.FRAMEWORK);
expect(typeWithStringBar.executor).not.toHaveBeenCalled();
});
});
});

describe('System actions', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,64 @@ it('handles invalid objects', () => {
ensureInvalid(loggerMock, [{ id: 'some-id', type: 'some-type', x: 42 }]);
});

describe('empty string validation', () => {
beforeEach(() => {
jest.resetAllMocks();
});

it('rejects empty string for required id field', () => {
const result = validatedRelatedSavedObjects(loggerMock, [{ id: '', type: 'some-type' }]);
expect(result).toEqual([]);
expect(loggerMock.warn).toHaveBeenCalledWith(
expect.stringContaining('ignoring invalid related saved objects')
);
});

it('rejects empty string for required type field', () => {
const result = validatedRelatedSavedObjects(loggerMock, [{ id: 'some-id', type: '' }]);
expect(result).toEqual([]);
expect(loggerMock.warn).toHaveBeenCalledWith(
expect.stringContaining('ignoring invalid related saved objects')
);
});

it('rejects empty string for optional namespace field when provided', () => {
const result = validatedRelatedSavedObjects(loggerMock, [
{ id: 'some-id', type: 'some-type', namespace: '' },
]);
expect(result).toEqual([]);
expect(loggerMock.warn).toHaveBeenCalledWith(
expect.stringContaining('ignoring invalid related saved objects')
);
});

it('rejects empty string for optional typeId field when provided', () => {
const result = validatedRelatedSavedObjects(loggerMock, [
{ id: 'some-id', type: 'some-type', typeId: '' },
]);
expect(result).toEqual([]);
expect(loggerMock.warn).toHaveBeenCalledWith(
expect.stringContaining('ignoring invalid related saved objects')
);
});

it('accepts omitted optional namespace (undefined is allowed)', () => {
const result = validatedRelatedSavedObjects(loggerMock, [
{ id: 'some-id', type: 'some-type', namespace: undefined },
]);
expect(result).toEqual([{ id: 'some-id', type: 'some-type' }]);
expect(loggerMock.warn).not.toHaveBeenCalled();
});

it('accepts omitted optional typeId (undefined is allowed)', () => {
const result = validatedRelatedSavedObjects(loggerMock, [
{ id: 'some-id', type: 'some-type', typeId: undefined },
]);
expect(result).toEqual([{ id: 'some-id', type: 'some-type' }]);
expect(loggerMock.warn).not.toHaveBeenCalled();
});
});

function ensureValid(logger: Logger, savedObjects: unknown) {
const result = validatedRelatedSavedObjects(logger, savedObjects);
expect(result).toEqual(savedObjects === undefined ? [] : savedObjects);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/*
* 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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { z } from '@kbn/zod/v4';
import { generateConfigSchema } from './generate_config_schema';

describe('generateConfigSchema', () => {
it('returns schema with authType when base schema is provided', () => {
const baseSchema = z.object({ apiUrl: z.string() });
const result = generateConfigSchema(baseSchema);

expect(result.schema).toBeDefined();
const parsed = result.schema.parse({ apiUrl: 'https://example.com' });
expect(parsed).toEqual({ apiUrl: 'https://example.com' });
});

it('allows optional authType in config', () => {
const baseSchema = z.object({ apiUrl: z.string() });
const result = generateConfigSchema(baseSchema);

const withAuthType = result.schema.parse({
apiUrl: 'https://example.com',
authType: 'basic',
});
expect(withAuthType).toEqual({ apiUrl: 'https://example.com', authType: 'basic' });

const withoutAuthType = result.schema.parse({ apiUrl: 'https://example.com' });
expect(withoutAuthType).toEqual({ apiUrl: 'https://example.com' });
});

it('returns z.object with authType when schema is undefined', () => {
const result = generateConfigSchema(undefined);

const withAuthType = result.schema.parse({ authType: 'basic' });
expect(withAuthType).toEqual({ authType: 'basic' });

const withoutAuthType = result.schema.parse({});
expect(withoutAuthType).toEqual({});
});

it('parses valid config with multiple fields', () => {
const baseSchema = z.object({
apiUrl: z.string(),
timeout: z.number().optional(),
});
const result = generateConfigSchema(baseSchema);

const parsed = result.schema.parse({
apiUrl: 'https://api.example.com',
timeout: 5000,
authType: 'bearer',
});
expect(parsed).toEqual({
apiUrl: 'https://api.example.com',
timeout: 5000,
authType: 'bearer',
});
});

it('rejects invalid config with wrong types', () => {
const baseSchema = z.object({ apiUrl: z.string() }).strict();
const result = generateConfigSchema(baseSchema);

expect(() => result.schema.parse({ apiUrl: 123 })).toThrow(/apiUrl|string|number/);
});

it('rejects config with missing required fields', () => {
const baseSchema = z.object({ apiUrl: z.string() });
const result = generateConfigSchema(baseSchema);

expect(() => result.schema.parse({})).toThrow(/apiUrl|Required|undefined/);
});

it('rejects extra keys when base schema is strict', () => {
const baseSchema = z.object({ apiUrl: z.string() }).strict();
const result = generateConfigSchema(baseSchema);

expect(() =>
result.schema.parse({ apiUrl: 'https://example.com', unknownKey: 'value' })
).toThrow(/unknownKey|Unrecognized/);
});
});
Loading
Loading