mirror of
https://github.com/dcarrillo/atalaya.git
synced 2026-04-18 02:24:05 +00:00
feat: add expectedBodyContains for HTTP checks (#6)
This commit is contained in:
@@ -46,7 +46,7 @@ __ ,--~' ~~----____
|
|||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- HTTP, TCP, and DNS monitoring.
|
- HTTP/HTTPs, TCP, and DNS monitoring.
|
||||||
- Regional monitoring from specific Cloudflare locations.
|
- Regional monitoring from specific Cloudflare locations.
|
||||||
- Configurable retries with immediate retry on failure.
|
- Configurable retries with immediate retry on failure.
|
||||||
- Configurable failure thresholds before alerting.
|
- Configurable failure thresholds before alerting.
|
||||||
@@ -159,6 +159,7 @@ Each monitor can override the global default\_\* settings:
|
|||||||
target: 'https://api.example.com/health'
|
target: 'https://api.example.com/health'
|
||||||
method: GET
|
method: GET
|
||||||
expected_status: 200
|
expected_status: 200
|
||||||
|
expected_body_contains: 'OK' # Optional: check if response body contains this text
|
||||||
headers: # optional, merged with default User-Agent: atalaya-uptime
|
headers: # optional, merged with default User-Agent: atalaya-uptime
|
||||||
Authorization: 'Bearer ${API_TOKEN}'
|
Authorization: 'Bearer ${API_TOKEN}'
|
||||||
Accept: 'application/json'
|
Accept: 'application/json'
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ describe('formatWebhookPayload', () => {
|
|||||||
method: 'GET',
|
method: 'GET',
|
||||||
expectedStatus: 200,
|
expectedStatus: 200,
|
||||||
headers: {},
|
headers: {},
|
||||||
|
expectedBodyContains: '',
|
||||||
timeoutMs: 5000,
|
timeoutMs: 5000,
|
||||||
retries: 3,
|
retries: 3,
|
||||||
retryDelayMs: 1000,
|
retryDelayMs: 1000,
|
||||||
@@ -105,6 +106,7 @@ describe('formatWebhookPayload', () => {
|
|||||||
method: 'GET',
|
method: 'GET',
|
||||||
expectedStatus: 200,
|
expectedStatus: 200,
|
||||||
headers: {},
|
headers: {},
|
||||||
|
expectedBodyContains: '',
|
||||||
timeoutMs: 5000,
|
timeoutMs: 5000,
|
||||||
retries: 3,
|
retries: 3,
|
||||||
retryDelayMs: 1000,
|
retryDelayMs: 1000,
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ describe('prepareChecks', () => {
|
|||||||
target: 'https://example.com',
|
target: 'https://example.com',
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
expectedStatus: 200,
|
expectedStatus: 200,
|
||||||
|
expectedBodyContains: '',
|
||||||
headers: {},
|
headers: {},
|
||||||
timeoutMs: 5000,
|
timeoutMs: 5000,
|
||||||
retries: 3,
|
retries: 3,
|
||||||
@@ -103,6 +104,7 @@ describe('prepareChecks', () => {
|
|||||||
target: 'https://example.com',
|
target: 'https://example.com',
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
expectedStatus: 200,
|
expectedStatus: 200,
|
||||||
|
expectedBodyContains: '',
|
||||||
headers: { Authorization: 'Bearer token' },
|
headers: { Authorization: 'Bearer token' },
|
||||||
timeoutMs: 5000,
|
timeoutMs: 5000,
|
||||||
retries: 3,
|
retries: 3,
|
||||||
@@ -135,6 +137,7 @@ describe('prepareChecks', () => {
|
|||||||
target: 'https://example.com',
|
target: 'https://example.com',
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
expectedStatus: 200,
|
expectedStatus: 200,
|
||||||
|
expectedBodyContains: '',
|
||||||
headers: {},
|
headers: {},
|
||||||
timeoutMs: 5000,
|
timeoutMs: 5000,
|
||||||
retries: 3,
|
retries: 3,
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ export function prepareChecks(config: Config): CheckRequest[] {
|
|||||||
type: m.type,
|
type: m.type,
|
||||||
method: m.method || undefined,
|
method: m.method || undefined,
|
||||||
expectedStatus: m.expectedStatus || undefined,
|
expectedStatus: m.expectedStatus || undefined,
|
||||||
|
expectedBodyContains: m.expectedBodyContains || undefined,
|
||||||
headers: Object.keys(m.headers).length > 0 ? m.headers : undefined,
|
headers: Object.keys(m.headers).length > 0 ? m.headers : undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ describe('executeHttpCheck', () => {
|
|||||||
vi.mocked(fetch).mockResolvedValue({
|
vi.mocked(fetch).mockResolvedValue({
|
||||||
ok: true,
|
ok: true,
|
||||||
status: 200,
|
status: 200,
|
||||||
|
text: vi.fn().mockResolvedValue(''),
|
||||||
} as unknown as Response);
|
} as unknown as Response);
|
||||||
|
|
||||||
const check = createCheckRequest();
|
const check = createCheckRequest();
|
||||||
@@ -41,6 +42,7 @@ describe('executeHttpCheck', () => {
|
|||||||
vi.mocked(fetch).mockResolvedValue({
|
vi.mocked(fetch).mockResolvedValue({
|
||||||
ok: true,
|
ok: true,
|
||||||
status: 200,
|
status: 200,
|
||||||
|
text: vi.fn().mockResolvedValue(''),
|
||||||
} as unknown as Response);
|
} as unknown as Response);
|
||||||
|
|
||||||
const check = createCheckRequest({ expectedStatus: 201 });
|
const check = createCheckRequest({ expectedStatus: 201 });
|
||||||
@@ -54,6 +56,7 @@ describe('executeHttpCheck', () => {
|
|||||||
vi.mocked(fetch).mockResolvedValue({
|
vi.mocked(fetch).mockResolvedValue({
|
||||||
ok: true,
|
ok: true,
|
||||||
status: 201,
|
status: 201,
|
||||||
|
text: vi.fn().mockResolvedValue(''),
|
||||||
} as unknown as Response);
|
} as unknown as Response);
|
||||||
|
|
||||||
const check = createCheckRequest({ expectedStatus: 201 });
|
const check = createCheckRequest({ expectedStatus: 201 });
|
||||||
@@ -76,7 +79,11 @@ describe('executeHttpCheck', () => {
|
|||||||
it('retries on failure and eventually succeeds', async () => {
|
it('retries on failure and eventually succeeds', async () => {
|
||||||
vi.mocked(fetch)
|
vi.mocked(fetch)
|
||||||
.mockRejectedValueOnce(new Error('Network error'))
|
.mockRejectedValueOnce(new Error('Network error'))
|
||||||
.mockResolvedValueOnce({ ok: true, status: 200 } as unknown as Response);
|
.mockResolvedValueOnce({
|
||||||
|
ok: true,
|
||||||
|
status: 200,
|
||||||
|
text: vi.fn().mockResolvedValue(''),
|
||||||
|
} as unknown as Response);
|
||||||
|
|
||||||
const check = createCheckRequest({ retries: 2, retryDelayMs: 10 });
|
const check = createCheckRequest({ retries: 2, retryDelayMs: 10 });
|
||||||
const result = await executeHttpCheck(check);
|
const result = await executeHttpCheck(check);
|
||||||
@@ -86,7 +93,11 @@ describe('executeHttpCheck', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('uses correct HTTP method', async () => {
|
it('uses correct HTTP method', async () => {
|
||||||
vi.mocked(fetch).mockResolvedValue({ ok: true, status: 200 } as unknown as Response);
|
vi.mocked(fetch).mockResolvedValue({
|
||||||
|
ok: true,
|
||||||
|
status: 200,
|
||||||
|
text: vi.fn().mockResolvedValue(''),
|
||||||
|
} as unknown as Response);
|
||||||
|
|
||||||
const check = createCheckRequest({ method: 'POST' });
|
const check = createCheckRequest({ method: 'POST' });
|
||||||
await executeHttpCheck(check);
|
await executeHttpCheck(check);
|
||||||
@@ -98,7 +109,11 @@ describe('executeHttpCheck', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('defaults to GET method', async () => {
|
it('defaults to GET method', async () => {
|
||||||
vi.mocked(fetch).mockResolvedValue({ ok: true, status: 200 } as unknown as Response);
|
vi.mocked(fetch).mockResolvedValue({
|
||||||
|
ok: true,
|
||||||
|
status: 200,
|
||||||
|
text: vi.fn().mockResolvedValue(''),
|
||||||
|
} as unknown as Response);
|
||||||
|
|
||||||
const check = createCheckRequest();
|
const check = createCheckRequest();
|
||||||
await executeHttpCheck(check);
|
await executeHttpCheck(check);
|
||||||
@@ -110,7 +125,11 @@ describe('executeHttpCheck', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('sends default User-Agent header', async () => {
|
it('sends default User-Agent header', async () => {
|
||||||
vi.mocked(fetch).mockResolvedValue({ ok: true, status: 200 } as unknown as Response);
|
vi.mocked(fetch).mockResolvedValue({
|
||||||
|
ok: true,
|
||||||
|
status: 200,
|
||||||
|
text: vi.fn().mockResolvedValue(''),
|
||||||
|
} as unknown as Response);
|
||||||
|
|
||||||
const check = createCheckRequest();
|
const check = createCheckRequest();
|
||||||
await executeHttpCheck(check);
|
await executeHttpCheck(check);
|
||||||
@@ -124,7 +143,11 @@ describe('executeHttpCheck', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('merges custom headers with defaults', async () => {
|
it('merges custom headers with defaults', async () => {
|
||||||
vi.mocked(fetch).mockResolvedValue({ ok: true, status: 200 } as unknown as Response);
|
vi.mocked(fetch).mockResolvedValue({
|
||||||
|
ok: true,
|
||||||
|
status: 200,
|
||||||
|
text: vi.fn().mockResolvedValue(''),
|
||||||
|
} as unknown as Response);
|
||||||
|
|
||||||
const check = createCheckRequest({
|
const check = createCheckRequest({
|
||||||
headers: { Authorization: 'Bearer token123' },
|
headers: { Authorization: 'Bearer token123' },
|
||||||
@@ -137,7 +160,11 @@ describe('executeHttpCheck', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('allows monitor headers to override defaults', async () => {
|
it('allows monitor headers to override defaults', async () => {
|
||||||
vi.mocked(fetch).mockResolvedValue({ ok: true, status: 200 } as unknown as Response);
|
vi.mocked(fetch).mockResolvedValue({
|
||||||
|
ok: true,
|
||||||
|
status: 200,
|
||||||
|
text: vi.fn().mockResolvedValue(''),
|
||||||
|
} as unknown as Response);
|
||||||
|
|
||||||
const check = createCheckRequest({
|
const check = createCheckRequest({
|
||||||
headers: { 'User-Agent': 'custom-agent' },
|
headers: { 'User-Agent': 'custom-agent' },
|
||||||
@@ -166,8 +193,16 @@ describe('executeHttpCheck', () => {
|
|||||||
|
|
||||||
it('retries on wrong status code', async () => {
|
it('retries on wrong status code', async () => {
|
||||||
vi.mocked(fetch)
|
vi.mocked(fetch)
|
||||||
.mockResolvedValueOnce({ ok: false, status: 500 } as unknown as Response)
|
.mockResolvedValueOnce({
|
||||||
.mockResolvedValueOnce({ ok: true, status: 200 } as unknown as Response);
|
ok: false,
|
||||||
|
status: 500,
|
||||||
|
text: vi.fn().mockResolvedValue(''),
|
||||||
|
} as unknown as Response)
|
||||||
|
.mockResolvedValueOnce({
|
||||||
|
ok: true,
|
||||||
|
status: 200,
|
||||||
|
text: vi.fn().mockResolvedValue(''),
|
||||||
|
} as unknown as Response);
|
||||||
|
|
||||||
const check = createCheckRequest({ expectedStatus: 200, retries: 2, retryDelayMs: 10 });
|
const check = createCheckRequest({ expectedStatus: 200, retries: 2, retryDelayMs: 10 });
|
||||||
const result = await executeHttpCheck(check);
|
const result = await executeHttpCheck(check);
|
||||||
@@ -185,4 +220,159 @@ describe('executeHttpCheck', () => {
|
|||||||
expect(result.status).toBe('down');
|
expect(result.status).toBe('down');
|
||||||
expect(result.error).toBe('Unknown error');
|
expect(result.error).toBe('Unknown error');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('returns up when body contains expected content', async () => {
|
||||||
|
vi.mocked(fetch).mockResolvedValue({
|
||||||
|
ok: true,
|
||||||
|
status: 200,
|
||||||
|
text: vi.fn().mockResolvedValue('Welcome to our site'),
|
||||||
|
} as unknown as Response);
|
||||||
|
|
||||||
|
const check = createCheckRequest({ expectedBodyContains: 'Welcome' });
|
||||||
|
const result = await executeHttpCheck(check);
|
||||||
|
|
||||||
|
expect(result.status).toBe('up');
|
||||||
|
expect(result.error).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns down when body does not contain expected content', async () => {
|
||||||
|
vi.mocked(fetch).mockResolvedValue({
|
||||||
|
ok: true,
|
||||||
|
status: 200,
|
||||||
|
text: vi.fn().mockResolvedValue('Error: Not found'),
|
||||||
|
} as unknown as Response);
|
||||||
|
|
||||||
|
const check = createCheckRequest({ expectedBodyContains: 'Welcome' });
|
||||||
|
const result = await executeHttpCheck(check);
|
||||||
|
|
||||||
|
expect(result.status).toBe('down');
|
||||||
|
expect(result.error).toContain("Expected body to contain 'Welcome'");
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns up when both status and body match', async () => {
|
||||||
|
vi.mocked(fetch).mockResolvedValue({
|
||||||
|
ok: true,
|
||||||
|
status: 201,
|
||||||
|
text: vi.fn().mockResolvedValue('User created successfully'),
|
||||||
|
} as unknown as Response);
|
||||||
|
|
||||||
|
const check = createCheckRequest({ expectedStatus: 201, expectedBodyContains: 'created' });
|
||||||
|
const result = await executeHttpCheck(check);
|
||||||
|
|
||||||
|
expect(result.status).toBe('up');
|
||||||
|
expect(result.error).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns down when status matches but body does not', async () => {
|
||||||
|
vi.mocked(fetch).mockResolvedValue({
|
||||||
|
ok: true,
|
||||||
|
status: 201,
|
||||||
|
text: vi.fn().mockResolvedValue('Error: Invalid input'),
|
||||||
|
} as unknown as Response);
|
||||||
|
|
||||||
|
const check = createCheckRequest({ expectedStatus: 201, expectedBodyContains: 'created' });
|
||||||
|
const result = await executeHttpCheck(check);
|
||||||
|
|
||||||
|
expect(result.status).toBe('down');
|
||||||
|
expect(result.error).toContain("Expected body to contain 'created'");
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns down when body matches but status does not', async () => {
|
||||||
|
vi.mocked(fetch).mockResolvedValue({
|
||||||
|
ok: true,
|
||||||
|
status: 200,
|
||||||
|
text: vi.fn().mockResolvedValue('User created successfully'),
|
||||||
|
} as unknown as Response);
|
||||||
|
|
||||||
|
const check = createCheckRequest({ expectedStatus: 201, expectedBodyContains: 'created' });
|
||||||
|
const result = await executeHttpCheck(check);
|
||||||
|
|
||||||
|
expect(result.status).toBe('down');
|
||||||
|
expect(result.error).toContain('Expected status 201, got 200');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns up when expected body is empty string (no body check)', async () => {
|
||||||
|
vi.mocked(fetch).mockResolvedValue({
|
||||||
|
ok: true,
|
||||||
|
status: 200,
|
||||||
|
text: vi.fn().mockResolvedValue(''),
|
||||||
|
} as unknown as Response);
|
||||||
|
|
||||||
|
const check = createCheckRequest({ expectedBodyContains: '' });
|
||||||
|
const result = await executeHttpCheck(check);
|
||||||
|
|
||||||
|
expect(result.status).toBe('up');
|
||||||
|
expect(result.error).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns up when expected body is whitespace only (no body check)', async () => {
|
||||||
|
vi.mocked(fetch).mockResolvedValue({
|
||||||
|
ok: true,
|
||||||
|
status: 200,
|
||||||
|
text: vi.fn().mockResolvedValue('Some content'),
|
||||||
|
} as unknown as Response);
|
||||||
|
|
||||||
|
const check = createCheckRequest({ expectedBodyContains: ' ' });
|
||||||
|
const result = await executeHttpCheck(check);
|
||||||
|
|
||||||
|
expect(result.status).toBe('up');
|
||||||
|
expect(result.error).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('performs case-sensitive body matching', async () => {
|
||||||
|
vi.mocked(fetch).mockResolvedValue({
|
||||||
|
ok: true,
|
||||||
|
status: 200,
|
||||||
|
text: vi.fn().mockResolvedValue('HELLO WORLD'),
|
||||||
|
} as unknown as Response);
|
||||||
|
|
||||||
|
const check = createCheckRequest({ expectedBodyContains: 'hello' });
|
||||||
|
const result = await executeHttpCheck(check);
|
||||||
|
|
||||||
|
expect(result.status).toBe('down');
|
||||||
|
expect(result.error).toContain("Expected body to contain 'hello'");
|
||||||
|
});
|
||||||
|
|
||||||
|
it('retries on body mismatch and eventually succeeds', async () => {
|
||||||
|
vi.mocked(fetch)
|
||||||
|
.mockResolvedValueOnce({
|
||||||
|
ok: true,
|
||||||
|
status: 200,
|
||||||
|
text: vi.fn().mockResolvedValue('Loading...'),
|
||||||
|
} as unknown as Response)
|
||||||
|
.mockResolvedValueOnce({
|
||||||
|
ok: true,
|
||||||
|
status: 200,
|
||||||
|
text: vi.fn().mockResolvedValue('Ready'),
|
||||||
|
} as unknown as Response);
|
||||||
|
|
||||||
|
const check = createCheckRequest({
|
||||||
|
expectedBodyContains: 'Ready',
|
||||||
|
retries: 2,
|
||||||
|
retryDelayMs: 10,
|
||||||
|
});
|
||||||
|
const result = await executeHttpCheck(check);
|
||||||
|
|
||||||
|
expect(result.status).toBe('up');
|
||||||
|
expect(result.attempts).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('retries on body mismatch and eventually fails', async () => {
|
||||||
|
vi.mocked(fetch).mockResolvedValue({
|
||||||
|
ok: true,
|
||||||
|
status: 200,
|
||||||
|
text: vi.fn().mockResolvedValue('Error'),
|
||||||
|
} as unknown as Response);
|
||||||
|
|
||||||
|
const check = createCheckRequest({
|
||||||
|
expectedBodyContains: 'Ready',
|
||||||
|
retries: 2,
|
||||||
|
retryDelayMs: 10,
|
||||||
|
});
|
||||||
|
const result = await executeHttpCheck(check);
|
||||||
|
|
||||||
|
expect(result.status).toBe('down');
|
||||||
|
expect(result.error).toContain("Expected body to contain 'Ready'");
|
||||||
|
expect(result.attempts).toBe(3);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -31,9 +31,42 @@ export async function executeHttpCheck(check: HttpCheckRequest): Promise<CheckRe
|
|||||||
timeout = undefined;
|
timeout = undefined;
|
||||||
|
|
||||||
const responseTime = Date.now() - startTime;
|
const responseTime = Date.now() - startTime;
|
||||||
|
const bodyText = await response.text();
|
||||||
|
|
||||||
if (check.expectedStatus && response.status !== check.expectedStatus) {
|
let statusOk = true;
|
||||||
lastError = `Expected status ${check.expectedStatus}, got ${response.status}`;
|
let bodyOk = true;
|
||||||
|
let errorMessages: string[] = [];
|
||||||
|
|
||||||
|
if (check.expectedStatus && check.expectedStatus > 0) {
|
||||||
|
if (response.status !== check.expectedStatus) {
|
||||||
|
statusOk = false;
|
||||||
|
errorMessages.push(`Expected status ${check.expectedStatus}, got ${response.status}`);
|
||||||
|
}
|
||||||
|
} else if (response.status < 200 || response.status >= 400) {
|
||||||
|
statusOk = false;
|
||||||
|
errorMessages.push(`Expected 2xx/3xx status, got ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
check.expectedBodyContains &&
|
||||||
|
check.expectedBodyContains.trim() &&
|
||||||
|
!bodyText.includes(check.expectedBodyContains)
|
||||||
|
) {
|
||||||
|
bodyOk = false;
|
||||||
|
errorMessages.push(`Expected body to contain '${check.expectedBodyContains}', not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (statusOk && bodyOk) {
|
||||||
|
return {
|
||||||
|
name: check.name,
|
||||||
|
status: 'up',
|
||||||
|
responseTimeMs: responseTime,
|
||||||
|
error: '',
|
||||||
|
attempts,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
lastError = errorMessages.join('; ');
|
||||||
if (i < check.retries) {
|
if (i < check.retries) {
|
||||||
await sleep(check.retryDelayMs);
|
await sleep(check.retryDelayMs);
|
||||||
continue;
|
continue;
|
||||||
@@ -46,15 +79,6 @@ export async function executeHttpCheck(check: HttpCheckRequest): Promise<CheckRe
|
|||||||
error: lastError,
|
error: lastError,
|
||||||
attempts,
|
attempts,
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
name: check.name,
|
|
||||||
status: 'up',
|
|
||||||
responseTimeMs: responseTime,
|
|
||||||
error: '',
|
|
||||||
attempts,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (timeout) {
|
if (timeout) {
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
|
|||||||
@@ -226,4 +226,35 @@ monitors:
|
|||||||
const config = parseConfig(yaml);
|
const config = parseConfig(yaml);
|
||||||
expect(config.settings.title).toBe('Atalaya Uptime Monitor');
|
expect(config.settings.title).toBe('Atalaya Uptime Monitor');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('parses expected_body_contains field', () => {
|
||||||
|
const yaml = `
|
||||||
|
monitors:
|
||||||
|
- name: "api-health"
|
||||||
|
type: http
|
||||||
|
target: "https://api.example.com/health"
|
||||||
|
expected_body_contains: "healthy"
|
||||||
|
`;
|
||||||
|
const config = parseConfig(yaml);
|
||||||
|
|
||||||
|
expect(config.monitors).toHaveLength(1);
|
||||||
|
if (config.monitors[0].type === 'http') {
|
||||||
|
expect(config.monitors[0].expectedBodyContains).toBe('healthy');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('defaults expected_body_contains to empty string', () => {
|
||||||
|
const yaml = `
|
||||||
|
monitors:
|
||||||
|
- name: "no-body-check"
|
||||||
|
type: http
|
||||||
|
target: "https://example.com"
|
||||||
|
`;
|
||||||
|
const config = parseConfig(yaml);
|
||||||
|
|
||||||
|
expect(config.monitors).toHaveLength(1);
|
||||||
|
if (config.monitors[0].type === 'http') {
|
||||||
|
expect(config.monitors[0].expectedBodyContains).toBe('');
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ function applyDefaults(raw: RawYamlConfig): Config {
|
|||||||
type,
|
type,
|
||||||
method: m.method ?? '',
|
method: m.method ?? '',
|
||||||
expectedStatus: m.expected_status ?? 0,
|
expectedStatus: m.expected_status ?? 0,
|
||||||
|
expectedBodyContains: m.expected_body_contains ?? '',
|
||||||
headers: m.headers ?? {},
|
headers: m.headers ?? {},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ export interface HttpMonitor extends MonitorBase {
|
|||||||
type: 'http';
|
type: 'http';
|
||||||
method: string;
|
method: string;
|
||||||
expectedStatus: number;
|
expectedStatus: number;
|
||||||
|
expectedBodyContains: string;
|
||||||
headers: Record<string, string>;
|
headers: Record<string, string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,6 +77,7 @@ export type RawYamlConfig = {
|
|||||||
target?: string;
|
target?: string;
|
||||||
method?: string;
|
method?: string;
|
||||||
expected_status?: number;
|
expected_status?: number;
|
||||||
|
expected_body_contains?: string;
|
||||||
headers?: Record<string, string>;
|
headers?: Record<string, string>;
|
||||||
record_type?: string;
|
record_type?: string;
|
||||||
expected_values?: string[];
|
expected_values?: string[];
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ describe('processResults', () => {
|
|||||||
method: 'GET',
|
method: 'GET',
|
||||||
expectedStatus: 200,
|
expectedStatus: 200,
|
||||||
headers: {},
|
headers: {},
|
||||||
|
expectedBodyContains: '',
|
||||||
timeoutMs: 5000,
|
timeoutMs: 5000,
|
||||||
retries: 3,
|
retries: 3,
|
||||||
retryDelayMs: 1000,
|
retryDelayMs: 1000,
|
||||||
@@ -102,6 +103,7 @@ describe('processResults', () => {
|
|||||||
method: 'GET',
|
method: 'GET',
|
||||||
expectedStatus: 200,
|
expectedStatus: 200,
|
||||||
headers: {},
|
headers: {},
|
||||||
|
expectedBodyContains: '',
|
||||||
timeoutMs: 5000,
|
timeoutMs: 5000,
|
||||||
retries: 3,
|
retries: 3,
|
||||||
retryDelayMs: 1000,
|
retryDelayMs: 1000,
|
||||||
@@ -125,7 +127,7 @@ describe('processResults', () => {
|
|||||||
{
|
{
|
||||||
monitor_name: 'test',
|
monitor_name: 'test',
|
||||||
current_status: 'down',
|
current_status: 'down',
|
||||||
consecutive_failures: 3,
|
consecutive_failures: 0,
|
||||||
last_status_change: 0,
|
last_status_change: 0,
|
||||||
last_checked: 0,
|
last_checked: 0,
|
||||||
},
|
},
|
||||||
@@ -145,7 +147,16 @@ describe('processResults', () => {
|
|||||||
defaultTimeoutMs: 5000,
|
defaultTimeoutMs: 5000,
|
||||||
defaultFailureThreshold: 3,
|
defaultFailureThreshold: 3,
|
||||||
},
|
},
|
||||||
alerts: [],
|
alerts: [
|
||||||
|
{
|
||||||
|
type: 'webhook' as const,
|
||||||
|
name: 'alert',
|
||||||
|
url: 'https://example.com',
|
||||||
|
method: 'POST',
|
||||||
|
headers: {},
|
||||||
|
bodyTemplate: '',
|
||||||
|
},
|
||||||
|
],
|
||||||
monitors: [
|
monitors: [
|
||||||
{
|
{
|
||||||
name: 'test',
|
name: 'test',
|
||||||
@@ -154,10 +165,11 @@ describe('processResults', () => {
|
|||||||
method: 'GET',
|
method: 'GET',
|
||||||
expectedStatus: 200,
|
expectedStatus: 200,
|
||||||
headers: {},
|
headers: {},
|
||||||
|
expectedBodyContains: '',
|
||||||
timeoutMs: 5000,
|
timeoutMs: 5000,
|
||||||
retries: 3,
|
retries: 3,
|
||||||
retryDelayMs: 1000,
|
retryDelayMs: 1000,
|
||||||
failureThreshold: 3,
|
failureThreshold: 2,
|
||||||
alerts: ['alert'],
|
alerts: ['alert'],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -177,7 +189,7 @@ describe('processResults', () => {
|
|||||||
{
|
{
|
||||||
monitor_name: 'test',
|
monitor_name: 'test',
|
||||||
current_status: 'up',
|
current_status: 'up',
|
||||||
consecutive_failures: 1,
|
consecutive_failures: 0,
|
||||||
last_status_change: 0,
|
last_status_change: 0,
|
||||||
last_checked: 0,
|
last_checked: 0,
|
||||||
},
|
},
|
||||||
@@ -199,17 +211,18 @@ describe('processResults', () => {
|
|||||||
alerts: [],
|
alerts: [],
|
||||||
monitors: [
|
monitors: [
|
||||||
{
|
{
|
||||||
name: 'known',
|
name: 'test',
|
||||||
type: 'http',
|
type: 'http',
|
||||||
target: 'https://example.com',
|
target: 'https://example.com',
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
expectedStatus: 200,
|
expectedStatus: 200,
|
||||||
headers: {},
|
headers: {},
|
||||||
|
expectedBodyContains: '',
|
||||||
timeoutMs: 5000,
|
timeoutMs: 5000,
|
||||||
retries: 3,
|
retries: 3,
|
||||||
retryDelayMs: 1000,
|
retryDelayMs: 1000,
|
||||||
failureThreshold: 2,
|
failureThreshold: 2,
|
||||||
alerts: [],
|
alerts: ['alert'],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
@@ -260,24 +273,25 @@ describe('processResults', () => {
|
|||||||
alerts: [],
|
alerts: [],
|
||||||
monitors: [
|
monitors: [
|
||||||
{
|
{
|
||||||
name: 'new-monitor',
|
name: 'test',
|
||||||
type: 'http',
|
type: 'http',
|
||||||
target: 'https://example.com',
|
target: 'https://example.com',
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
expectedStatus: 200,
|
expectedStatus: 200,
|
||||||
headers: {},
|
headers: {},
|
||||||
|
expectedBodyContains: '',
|
||||||
timeoutMs: 5000,
|
timeoutMs: 5000,
|
||||||
retries: 3,
|
retries: 3,
|
||||||
retryDelayMs: 1000,
|
retryDelayMs: 1000,
|
||||||
failureThreshold: 2,
|
failureThreshold: 3,
|
||||||
alerts: [],
|
alerts: ['alert'],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
const results: CheckResult[] = [
|
const results: CheckResult[] = [
|
||||||
{
|
{
|
||||||
name: 'new-monitor',
|
name: 'test',
|
||||||
status: 'up',
|
status: 'up',
|
||||||
responseTimeMs: 100,
|
responseTimeMs: 100,
|
||||||
error: '',
|
error: '',
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export interface HttpCheckRequest extends CheckRequestBase {
|
|||||||
method?: string;
|
method?: string;
|
||||||
expectedStatus?: number;
|
expectedStatus?: number;
|
||||||
headers?: Record<string, string>;
|
headers?: Record<string, string>;
|
||||||
|
expectedBodyContains?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TcpCheckRequest extends CheckRequestBase {
|
export interface TcpCheckRequest extends CheckRequestBase {
|
||||||
|
|||||||
@@ -111,6 +111,17 @@ monitors:
|
|||||||
expected_status: 200
|
expected_status: 200
|
||||||
timeout_ms: 10000
|
timeout_ms: 10000
|
||||||
alerts: ["default"]
|
alerts: ["default"]
|
||||||
|
|
||||||
|
# Example with expected_body_contains
|
||||||
|
- name: "json-api-health"
|
||||||
|
type: http
|
||||||
|
target: "https://api.example.com/health"
|
||||||
|
method: GET
|
||||||
|
expected_status: 200
|
||||||
|
expected_body_contains: '"status":"healthy"' # Check JSON response contains healthy status
|
||||||
|
headers:
|
||||||
|
Content-Type: "application/json"
|
||||||
|
alerts: ["default"]
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# optional
|
# optional
|
||||||
|
|||||||
Reference in New Issue
Block a user