Kick off (#1)

* Kick off
* Update LICENSE copyright
This commit is contained in:
2026-04-11 13:22:36 +02:00
committed by GitHub
parent 1f1e74c9f8
commit 3882a1941a
76 changed files with 17154 additions and 1 deletions

251
src/checks/dns.test.ts Normal file
View File

@@ -0,0 +1,251 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import type { DnsCheckRequest } from '../types.js';
import { executeDnsCheck } from './dns.js';
const createCheckRequest = (overrides: Partial<DnsCheckRequest> = {}): DnsCheckRequest => ({
name: 'test-dns',
type: 'dns',
target: 'example.com',
timeoutMs: 5000,
retries: 2,
retryDelayMs: 100,
...overrides,
});
describe('executeDnsCheck', () => {
beforeEach(() => {
vi.stubGlobal('fetch', vi.fn());
});
afterEach(() => {
vi.unstubAllGlobals();
});
it('returns up status on successful DNS resolution', async () => {
vi.mocked(fetch).mockResolvedValue({
ok: true,
json: async () => ({ Answer: [{ data: '93.184.216.34' }] }),
} as unknown as Response);
const check = createCheckRequest();
const result = await executeDnsCheck(check);
expect(result.status).toBe('up');
expect(result.name).toBe('test-dns');
expect(result.error).toBe('');
expect(result.attempts).toBe(1);
});
it('uses correct DoH URL with record type', async () => {
vi.mocked(fetch).mockResolvedValue({
ok: true,
json: async () => ({ Answer: [{ data: 'mx.example.com' }] }),
} as unknown as Response);
const check = createCheckRequest({ recordType: 'MX' });
await executeDnsCheck(check);
expect(fetch).toHaveBeenCalledWith(expect.stringContaining('type=MX'), expect.any(Object));
});
it('defaults to A record type', async () => {
vi.mocked(fetch).mockResolvedValue({
ok: true,
json: async () => ({ Answer: [{ data: '93.184.216.34' }] }),
} as unknown as Response);
const check = createCheckRequest();
await executeDnsCheck(check);
expect(fetch).toHaveBeenCalledWith(expect.stringContaining('type=A'), expect.any(Object));
});
it('returns down status when DNS query fails', async () => {
vi.mocked(fetch).mockResolvedValue({
ok: false,
status: 500,
} as unknown as Response);
const check = createCheckRequest({ retries: 0 });
const result = await executeDnsCheck(check);
expect(result.status).toBe('down');
expect(result.error).toContain('DNS query failed');
});
it('validates expected values', async () => {
vi.mocked(fetch).mockResolvedValue({
ok: true,
json: async () => ({ Answer: [{ data: '93.184.216.34' }] }),
} as unknown as Response);
const check = createCheckRequest({
expectedValues: ['93.184.216.34'],
});
const result = await executeDnsCheck(check);
expect(result.status).toBe('up');
});
it('returns down status when expected values do not match', async () => {
vi.mocked(fetch).mockResolvedValue({
ok: true,
json: async () => ({ Answer: [{ data: '1.2.3.4' }] }),
} as unknown as Response);
const check = createCheckRequest({
expectedValues: ['93.184.216.34'],
retries: 0,
});
const result = await executeDnsCheck(check);
expect(result.status).toBe('down');
expect(result.error).toContain('Expected 93.184.216.34');
});
it('validates multiple expected values', async () => {
vi.mocked(fetch).mockResolvedValue({
ok: true,
json: async () => ({
Answer: [{ data: '93.184.216.34' }, { data: '93.184.216.35' }],
}),
} as unknown as Response);
const check = createCheckRequest({
expectedValues: ['93.184.216.34', '93.184.216.35'],
});
const result = await executeDnsCheck(check);
expect(result.status).toBe('up');
});
it('fails when not all expected values are found', async () => {
vi.mocked(fetch).mockResolvedValue({
ok: true,
json: async () => ({ Answer: [{ data: '93.184.216.34' }] }),
} as unknown as Response);
const check = createCheckRequest({
expectedValues: ['93.184.216.34', '93.184.216.35'],
retries: 0,
});
const result = await executeDnsCheck(check);
expect(result.status).toBe('down');
});
it('retries on failure and eventually succeeds', async () => {
vi.mocked(fetch)
.mockRejectedValueOnce(new Error('Network error'))
.mockResolvedValueOnce({
ok: true,
json: async () => ({ Answer: [{ data: '93.184.216.34' }] }),
} as unknown as Response);
const check = createCheckRequest({ retries: 2, retryDelayMs: 10 });
const result = await executeDnsCheck(check);
expect(result.status).toBe('up');
expect(result.attempts).toBe(2);
});
it('retries on failure and eventually fails', async () => {
vi.mocked(fetch).mockRejectedValue(new Error('Network error'));
const check = createCheckRequest({ retries: 2, retryDelayMs: 10 });
const result = await executeDnsCheck(check);
expect(result.status).toBe('down');
expect(result.error).toBe('Network error');
expect(result.attempts).toBe(3);
});
it('handles empty Answer array', async () => {
vi.mocked(fetch).mockResolvedValue({
ok: true,
json: async () => ({ Answer: [] }),
} as unknown as Response);
const check = createCheckRequest({
expectedValues: ['93.184.216.34'],
retries: 0,
});
const result = await executeDnsCheck(check);
expect(result.status).toBe('down');
});
it('handles missing Answer field', async () => {
vi.mocked(fetch).mockResolvedValue({
ok: true,
json: async () => ({}),
} as unknown as Response);
const check = createCheckRequest({
expectedValues: ['93.184.216.34'],
retries: 0,
});
const result = await executeDnsCheck(check);
expect(result.status).toBe('down');
});
it('passes when no expected values specified', async () => {
vi.mocked(fetch).mockResolvedValue({
ok: true,
json: async () => ({}),
} as unknown as Response);
const check = createCheckRequest({ expectedValues: undefined });
const result = await executeDnsCheck(check);
expect(result.status).toBe('up');
});
it('handles unknown error types', async () => {
vi.mocked(fetch).mockRejectedValue('string error');
const check = createCheckRequest({ retries: 0 });
const result = await executeDnsCheck(check);
expect(result.status).toBe('down');
expect(result.error).toBe('Unknown error');
});
it('encodes target in URL', async () => {
vi.mocked(fetch).mockResolvedValue({
ok: true,
json: async () => ({ Answer: [{ data: '93.184.216.34' }] }),
} as unknown as Response);
const check = createCheckRequest({ target: 'sub.example.com' });
await executeDnsCheck(check);
expect(fetch).toHaveBeenCalledWith(
expect.stringContaining('name=sub.example.com'),
expect.any(Object)
);
});
it('retries on wrong expected values then succeeds', async () => {
vi.mocked(fetch)
.mockResolvedValueOnce({
ok: true,
json: async () => ({ Answer: [{ data: 'wrong' }] }),
} as unknown as Response)
.mockResolvedValueOnce({
ok: true,
json: async () => ({ Answer: [{ data: '93.184.216.34' }] }),
} as unknown as Response);
const check = createCheckRequest({
expectedValues: ['93.184.216.34'],
retries: 2,
retryDelayMs: 10,
});
const result = await executeDnsCheck(check);
expect(result.status).toBe('up');
expect(result.attempts).toBe(2);
});
});

114
src/checks/dns.ts Normal file
View File

@@ -0,0 +1,114 @@
import type { DnsCheckRequest, CheckResult } from '../types.js';
import { sleep } from './utils.js';
type DnsResponse = {
Answer?: Array<{ data: string }>;
};
export async function executeDnsCheck(check: DnsCheckRequest): Promise<CheckResult> {
const startTime = Date.now();
let attempts = 0;
let lastError = '';
const recordType = check.recordType ?? 'A';
const dohUrl = `https://cloudflare-dns.com/dns-query?name=${encodeURIComponent(check.target)}&type=${recordType}`;
for (let i = 0; i <= check.retries; i++) {
attempts++;
const controller = new AbortController();
let timeout: ReturnType<typeof setTimeout> | undefined;
try {
timeout = setTimeout(() => {
controller.abort();
}, check.timeoutMs);
const response = await fetch(dohUrl, {
headers: { Accept: 'application/dns-json' },
signal: controller.signal,
});
clearTimeout(timeout);
timeout = undefined;
if (!response.ok) {
lastError = `DNS query failed: ${response.status}`;
if (i < check.retries) {
await sleep(check.retryDelayMs);
continue;
}
return {
name: check.name,
status: 'down',
responseTimeMs: Date.now() - startTime,
error: lastError,
attempts,
};
}
const data: DnsResponse = await response.json();
const responseTime = Date.now() - startTime;
const { expectedValues } = check;
if (!expectedValues || expectedValues.length === 0) {
return {
name: check.name,
status: 'up',
responseTimeMs: responseTime,
error: '',
attempts,
};
}
const resolvedValues = data.Answer?.map(a => a.data) ?? [];
const allExpectedFound = expectedValues.every(expected => resolvedValues.includes(expected));
if (allExpectedFound) {
return {
name: check.name,
status: 'up',
responseTimeMs: responseTime,
error: '',
attempts,
};
}
lastError = `Expected ${expectedValues.join(', ')}, got ${resolvedValues.join(', ')}`;
if (i < check.retries) {
await sleep(check.retryDelayMs);
continue;
}
return {
name: check.name,
status: 'down',
responseTimeMs: responseTime,
error: lastError,
attempts,
};
} catch (error) {
if (timeout) {
clearTimeout(timeout);
timeout = undefined;
}
lastError = error instanceof Error ? error.message : 'Unknown error';
if (i < check.retries) {
await sleep(check.retryDelayMs);
}
} finally {
if (timeout) {
clearTimeout(timeout);
}
}
}
return {
name: check.name,
status: 'down',
responseTimeMs: Date.now() - startTime,
error: lastError,
attempts,
};
}

188
src/checks/http.test.ts Normal file
View File

@@ -0,0 +1,188 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import type { HttpCheckRequest } from '../types.js';
import { executeHttpCheck } from './http.js';
const createCheckRequest = (overrides: Partial<HttpCheckRequest> = {}): HttpCheckRequest => ({
name: 'test-http',
type: 'http',
target: 'https://example.com',
timeoutMs: 5000,
retries: 2,
retryDelayMs: 100,
...overrides,
});
describe('executeHttpCheck', () => {
beforeEach(() => {
vi.stubGlobal('fetch', vi.fn());
});
afterEach(() => {
vi.unstubAllGlobals();
});
it('returns up status on successful response', async () => {
vi.mocked(fetch).mockResolvedValue({
ok: true,
status: 200,
} as unknown as Response);
const check = createCheckRequest();
const result = await executeHttpCheck(check);
expect(result.status).toBe('up');
expect(result.name).toBe('test-http');
expect(result.error).toBe('');
expect(result.attempts).toBe(1);
expect(result.responseTimeMs).toBeGreaterThanOrEqual(0);
});
it('returns down status on wrong expected status code', async () => {
vi.mocked(fetch).mockResolvedValue({
ok: true,
status: 200,
} as unknown as Response);
const check = createCheckRequest({ expectedStatus: 201 });
const result = await executeHttpCheck(check);
expect(result.status).toBe('down');
expect(result.error).toContain('Expected status 201, got 200');
});
it('matches expected status code when correct', async () => {
vi.mocked(fetch).mockResolvedValue({
ok: true,
status: 201,
} as unknown as Response);
const check = createCheckRequest({ expectedStatus: 201 });
const result = await executeHttpCheck(check);
expect(result.status).toBe('up');
});
it('retries on failure and eventually fails', async () => {
vi.mocked(fetch).mockRejectedValue(new Error('Network error'));
const check = createCheckRequest({ retries: 2, retryDelayMs: 10 });
const result = await executeHttpCheck(check);
expect(result.status).toBe('down');
expect(result.error).toBe('Network error');
expect(result.attempts).toBe(3);
});
it('retries on failure and eventually succeeds', async () => {
vi.mocked(fetch)
.mockRejectedValueOnce(new Error('Network error'))
.mockResolvedValueOnce({ ok: true, status: 200 } as unknown as Response);
const check = createCheckRequest({ retries: 2, retryDelayMs: 10 });
const result = await executeHttpCheck(check);
expect(result.status).toBe('up');
expect(result.attempts).toBe(2);
});
it('uses correct HTTP method', async () => {
vi.mocked(fetch).mockResolvedValue({ ok: true, status: 200 } as unknown as Response);
const check = createCheckRequest({ method: 'POST' });
await executeHttpCheck(check);
expect(fetch).toHaveBeenCalledWith(
'https://example.com',
expect.objectContaining({ method: 'POST' })
);
});
it('defaults to GET method', async () => {
vi.mocked(fetch).mockResolvedValue({ ok: true, status: 200 } as unknown as Response);
const check = createCheckRequest();
await executeHttpCheck(check);
expect(fetch).toHaveBeenCalledWith(
'https://example.com',
expect.objectContaining({ method: 'GET' })
);
});
it('sends default User-Agent header', async () => {
vi.mocked(fetch).mockResolvedValue({ ok: true, status: 200 } as unknown as Response);
const check = createCheckRequest();
await executeHttpCheck(check);
expect(fetch).toHaveBeenCalledWith(
'https://example.com',
expect.objectContaining({
headers: expect.objectContaining({ 'User-Agent': 'atalaya-uptime' }),
})
);
});
it('merges custom headers with defaults', async () => {
vi.mocked(fetch).mockResolvedValue({ ok: true, status: 200 } as unknown as Response);
const check = createCheckRequest({
headers: { Authorization: 'Bearer token123' },
});
await executeHttpCheck(check);
const callHeaders = vi.mocked(fetch).mock.calls[0][1]?.headers as Record<string, string>;
expect(callHeaders['User-Agent']).toBe('atalaya-uptime');
expect(callHeaders['Authorization']).toBe('Bearer token123');
});
it('allows monitor headers to override defaults', async () => {
vi.mocked(fetch).mockResolvedValue({ ok: true, status: 200 } as unknown as Response);
const check = createCheckRequest({
headers: { 'User-Agent': 'custom-agent' },
});
await executeHttpCheck(check);
const callHeaders = vi.mocked(fetch).mock.calls[0][1]?.headers as Record<string, string>;
expect(callHeaders['User-Agent']).toBe('custom-agent');
});
it('handles abort signal timeout', async () => {
vi.mocked(fetch).mockImplementation(
async () =>
new Promise((_, reject) => {
setTimeout(() => {
reject(new Error('The operation was aborted'));
}, 100);
})
);
const check = createCheckRequest({ timeoutMs: 50, retries: 0 });
const result = await executeHttpCheck(check);
expect(result.status).toBe('down');
});
it('retries on wrong status code', async () => {
vi.mocked(fetch)
.mockResolvedValueOnce({ ok: false, status: 500 } as unknown as Response)
.mockResolvedValueOnce({ ok: true, status: 200 } as unknown as Response);
const check = createCheckRequest({ expectedStatus: 200, retries: 2, retryDelayMs: 10 });
const result = await executeHttpCheck(check);
expect(result.status).toBe('up');
expect(result.attempts).toBe(2);
});
it('handles unknown error types', async () => {
vi.mocked(fetch).mockRejectedValue('string error');
const check = createCheckRequest({ retries: 0 });
const result = await executeHttpCheck(check);
expect(result.status).toBe('down');
expect(result.error).toBe('Unknown error');
});
});

82
src/checks/http.ts Normal file
View File

@@ -0,0 +1,82 @@
import type { HttpCheckRequest, CheckResult } from '../types.js';
import { sleep } from './utils.js';
const DEFAULT_HEADERS: Record<string, string> = {
'User-Agent': 'atalaya-uptime',
};
export async function executeHttpCheck(check: HttpCheckRequest): Promise<CheckResult> {
const startTime = Date.now();
let attempts = 0;
let lastError = '';
const headers = { ...DEFAULT_HEADERS, ...check.headers };
for (let i = 0; i <= check.retries; i++) {
attempts++;
const controller = new AbortController();
let timeout: ReturnType<typeof setTimeout> | undefined;
try {
timeout = setTimeout(() => {
controller.abort();
}, check.timeoutMs);
const response = await fetch(check.target, {
method: check.method ?? 'GET',
headers,
signal: controller.signal,
});
clearTimeout(timeout);
timeout = undefined;
const responseTime = Date.now() - startTime;
if (check.expectedStatus && response.status !== check.expectedStatus) {
lastError = `Expected status ${check.expectedStatus}, got ${response.status}`;
if (i < check.retries) {
await sleep(check.retryDelayMs);
continue;
}
return {
name: check.name,
status: 'down',
responseTimeMs: responseTime,
error: lastError,
attempts,
};
}
return {
name: check.name,
status: 'up',
responseTimeMs: responseTime,
error: '',
attempts,
};
} catch (error) {
if (timeout) {
clearTimeout(timeout);
timeout = undefined;
}
lastError = error instanceof Error ? error.message : 'Unknown error';
if (i < check.retries) {
await sleep(check.retryDelayMs);
}
} finally {
if (timeout) {
clearTimeout(timeout);
}
}
}
return {
name: check.name,
status: 'down',
responseTimeMs: Date.now() - startTime,
error: lastError,
attempts,
};
}

197
src/checks/tcp.test.ts Normal file
View File

@@ -0,0 +1,197 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { connect } from 'cloudflare:sockets';
import type { TcpCheckRequest } from '../types.js';
import { executeTcpCheck } from './tcp.js';
type Socket = ReturnType<typeof connect>;
vi.mock('cloudflare:sockets', () => ({
connect: vi.fn(),
}));
const createCheckRequest = (overrides: Partial<TcpCheckRequest> = {}): TcpCheckRequest => ({
name: 'test-tcp',
type: 'tcp',
target: 'example.com:443',
timeoutMs: 5000,
retries: 2,
retryDelayMs: 100,
...overrides,
});
type MockSocket = {
opened: Promise<unknown>;
close: ReturnType<typeof vi.fn>;
};
function createMockSocket(options: { shouldOpen?: boolean; openDelay?: number } = {}): MockSocket {
const { shouldOpen = true, openDelay = 0 } = options;
const mockClose = vi.fn().mockResolvedValue(undefined);
let mockOpened: Promise<unknown>;
if (shouldOpen) {
mockOpened = new Promise(resolve => {
setTimeout(resolve, openDelay);
});
} else {
mockOpened = new Promise((_, reject) => {
setTimeout(() => {
reject(new Error('Connection refused'));
}, openDelay);
});
}
return {
opened: mockOpened,
close: mockClose,
};
}
describe('executeTcpCheck', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('returns down status for invalid target format', async () => {
const check = createCheckRequest({ target: 'invalid-target' });
const result = await executeTcpCheck(check);
expect(result.status).toBe('down');
expect(result.error).toContain('Invalid target format');
expect(result.attempts).toBe(1);
});
it('returns down status for invalid port number (NaN)', async () => {
const check = createCheckRequest({ target: 'example.com:abc' });
const result = await executeTcpCheck(check);
expect(result.status).toBe('down');
expect(result.error).toContain('Invalid port number');
});
it('returns down status for port out of range (0)', async () => {
const check = createCheckRequest({ target: 'example.com:0' });
const result = await executeTcpCheck(check);
expect(result.status).toBe('down');
expect(result.error).toContain('Invalid port number');
});
it('returns down status for port out of range (65536)', async () => {
const check = createCheckRequest({ target: 'example.com:65536' });
const result = await executeTcpCheck(check);
expect(result.status).toBe('down');
expect(result.error).toContain('Invalid port number');
});
it('returns up status on successful connection', async () => {
vi.mocked(connect).mockReturnValue(createMockSocket() as unknown as Socket);
const check = createCheckRequest();
const result = await executeTcpCheck(check);
expect(result.status).toBe('up');
expect(result.name).toBe('test-tcp');
expect(result.error).toBe('');
expect(result.attempts).toBe(1);
expect(connect).toHaveBeenCalledWith({ hostname: 'example.com', port: 443 });
});
it('returns down status on connection failure', async () => {
vi.mocked(connect).mockReturnValue(
createMockSocket({ shouldOpen: false }) as unknown as Socket
);
const check = createCheckRequest({ retries: 0 });
const result = await executeTcpCheck(check);
expect(result.status).toBe('down');
expect(result.error).toContain('Connection refused');
});
it('retries on failure and eventually succeeds', async () => {
vi.mocked(connect)
.mockReturnValueOnce(createMockSocket({ shouldOpen: false }) as unknown as Socket)
.mockReturnValueOnce(createMockSocket({ shouldOpen: true }) as unknown as Socket);
const check = createCheckRequest({ retries: 2, retryDelayMs: 10 });
const result = await executeTcpCheck(check);
expect(result.status).toBe('up');
expect(result.attempts).toBe(2);
});
it('retries on failure and eventually fails', async () => {
vi.mocked(connect).mockReturnValue(
createMockSocket({ shouldOpen: false }) as unknown as Socket
);
const check = createCheckRequest({ retries: 2, retryDelayMs: 10 });
const result = await executeTcpCheck(check);
expect(result.status).toBe('down');
expect(result.attempts).toBe(3);
});
it('handles connection timeout', async () => {
vi.mocked(connect).mockReturnValue({
opened: new Promise(() => {
// Never resolves
}),
close: vi.fn().mockResolvedValue(undefined),
} as unknown as Socket);
const check = createCheckRequest({ timeoutMs: 50, retries: 0 });
const result = await executeTcpCheck(check);
expect(result.status).toBe('down');
expect(result.error).toContain('timeout');
});
it('closes socket after successful connection', async () => {
const mockSocket = createMockSocket();
vi.mocked(connect).mockReturnValue(mockSocket as unknown as Socket);
const check = createCheckRequest();
await executeTcpCheck(check);
expect(mockSocket.close).toHaveBeenCalled();
});
it('handles socket close error gracefully in finally block', async () => {
const mockSocket = {
opened: Promise.reject(new Error('Connection failed')),
close: vi.fn().mockRejectedValue(new Error('Close error')),
};
vi.mocked(connect).mockReturnValue(mockSocket as unknown as Socket);
const check = createCheckRequest({ retries: 0 });
const result = await executeTcpCheck(check);
expect(result.status).toBe('down');
expect(mockSocket.close).toHaveBeenCalled();
});
it('handles unknown error types', async () => {
vi.mocked(connect).mockReturnValue({
opened: Promise.reject(new Error('string error')),
close: vi.fn().mockResolvedValue(undefined),
} as unknown as Socket);
const check = createCheckRequest({ retries: 0 });
const result = await executeTcpCheck(check);
expect(result.status).toBe('down');
expect(result.error).toBe('string error');
});
it('parses port correctly', async () => {
vi.mocked(connect).mockReturnValue(createMockSocket() as unknown as Socket);
const check = createCheckRequest({ target: 'db.example.com:5432' });
await executeTcpCheck(check);
expect(connect).toHaveBeenCalledWith({ hostname: 'db.example.com', port: 5432 });
});
});

94
src/checks/tcp.ts Normal file
View File

@@ -0,0 +1,94 @@
import { connect } from 'cloudflare:sockets';
import type { TcpCheckRequest, CheckResult } from '../types.js';
import { sleep } from './utils.js';
export async function executeTcpCheck(check: TcpCheckRequest): Promise<CheckResult> {
const startTime = Date.now();
let attempts = 0;
let lastError = '';
const parts = check.target.split(':');
if (parts.length !== 2) {
return {
name: check.name,
status: 'down',
responseTimeMs: 0,
error: 'Invalid target format (expected host:port)',
attempts: 1,
};
}
const [hostname, portString] = parts;
const port = Number.parseInt(portString, 10);
if (Number.isNaN(port) || port <= 0 || port > 65_535) {
return {
name: check.name,
status: 'down',
responseTimeMs: 0,
error: 'Invalid port number',
attempts: 1,
};
}
for (let i = 0; i <= check.retries; i++) {
attempts++;
let socket: ReturnType<typeof connect> | undefined;
let timeout: ReturnType<typeof setTimeout> | undefined;
try {
socket = connect({ hostname, port });
const timeoutPromise = new Promise<never>((_, reject) => {
timeout = setTimeout(() => {
reject(new Error('Connection timeout'));
}, check.timeoutMs);
});
await Promise.race([socket.opened, timeoutPromise]);
if (timeout) {
clearTimeout(timeout);
timeout = undefined;
}
await socket.close();
return {
name: check.name,
status: 'up',
responseTimeMs: Date.now() - startTime,
error: '',
attempts,
};
} catch (error) {
if (timeout) {
clearTimeout(timeout);
timeout = undefined;
}
lastError = error instanceof Error ? error.message : 'Unknown error';
if (i < check.retries) {
await sleep(check.retryDelayMs);
}
} finally {
if (timeout) {
clearTimeout(timeout);
}
if (socket) {
try {
await socket.close();
} catch {
/* ignore */
}
}
}
}
return {
name: check.name,
status: 'down',
responseTimeMs: Date.now() - startTime,
error: lastError,
attempts,
};
}

27
src/checks/utils.test.ts Normal file
View File

@@ -0,0 +1,27 @@
import { describe, it, expect, vi } from 'vitest';
import { sleep } from './utils.js';
describe('sleep', () => {
it('resolves after specified time', async () => {
vi.useFakeTimers();
const promise = sleep(1000);
vi.advanceTimersByTime(1000);
await expect(promise).resolves.toBeUndefined();
vi.useRealTimers();
});
it('waits approximately the specified time', async () => {
const start = Date.now();
await sleep(50);
const elapsed = Date.now() - start;
expect(elapsed).toBeGreaterThanOrEqual(40);
expect(elapsed).toBeLessThan(200);
});
it('handles zero delay', async () => {
const start = Date.now();
await sleep(0);
const elapsed = Date.now() - start;
expect(elapsed).toBeLessThan(50);
});
});

5
src/checks/utils.ts Normal file
View File

@@ -0,0 +1,5 @@
export async function sleep(ms: number): Promise<void> {
return new Promise(resolve => {
setTimeout(resolve, ms);
});
}