← back to Designer Wallcoverings

DW-Agents/__tests__/health/health-check.test.ts

183 lines

/**
 * Health Check Tests
 * Tests for /api/health endpoint and system health monitoring
 */

import axios from 'axios';

const BASE_URL = process.env.BASE_URL || 'http://localhost';
const MASTER_HUB_PORT = process.env.MASTER_HUB_PORT || '9800';
const HEALTH_ENDPOINT = `${BASE_URL}:${MASTER_HUB_PORT}/api/health`;

describe('Health Check Endpoint', () => {

  describe('GET /api/health', () => {
    it('should return 200 status code', async () => {
      const response = await axios.get(HEALTH_ENDPOINT, {
        validateStatus: () => true // Don't throw on non-2xx
      });

      expect(response.status).toBe(200);
    });

    it('should return valid JSON response', async () => {
      const response = await axios.get(HEALTH_ENDPOINT);

      expect(response.headers['content-type']).toMatch(/application\/json/);
      expect(response.data).toBeDefined();
    });

    it('should include status field', async () => {
      const response = await axios.get(HEALTH_ENDPOINT);

      expect(response.data).toHaveProperty('status');
      expect(['healthy', 'degraded', 'unhealthy']).toContain(response.data.status);
    });

    it('should include timestamp', async () => {
      const response = await axios.get(HEALTH_ENDPOINT);

      expect(response.data).toHaveProperty('timestamp');

      const timestamp = new Date(response.data.timestamp);
      expect(timestamp.getTime()).not.toBeNaN();

      // Timestamp should be recent (within last 5 seconds)
      const now = new Date();
      const diff = now.getTime() - timestamp.getTime();
      expect(diff).toBeLessThan(5000);
    });

    it('should respond within 1 second', async () => {
      const startTime = Date.now();
      await axios.get(HEALTH_ENDPOINT);
      const duration = Date.now() - startTime;

      expect(duration).toBeLessThan(1000);
    });

    it('should handle multiple concurrent requests', async () => {
      const requests = Array(10).fill(null).map(() =>
        axios.get(HEALTH_ENDPOINT)
      );

      const responses = await Promise.all(requests);

      responses.forEach(response => {
        expect(response.status).toBe(200);
        expect(response.data).toHaveProperty('status');
      });
    });

    it('should include uptime information if available', async () => {
      const response = await axios.get(HEALTH_ENDPOINT);

      // Uptime is optional but if present should be valid
      if (response.data.uptime) {
        expect(typeof response.data.uptime).toBe('number');
        expect(response.data.uptime).toBeGreaterThan(0);
      }
    });

    it('should include service information', async () => {
      const response = await axios.get(HEALTH_ENDPOINT);

      // Service info is optional but recommended
      if (response.data.service) {
        expect(response.data.service).toHaveProperty('name');
        expect(response.data.service).toHaveProperty('version');
      }
    });
  });

  describe('Health Check Reliability', () => {
    it('should be idempotent - multiple calls return consistent results', async () => {
      const response1 = await axios.get(HEALTH_ENDPOINT);
      await new Promise(resolve => setTimeout(resolve, 100));
      const response2 = await axios.get(HEALTH_ENDPOINT);

      expect(response1.data.status).toBe(response2.data.status);
    });

    it('should handle invalid query parameters gracefully', async () => {
      const response = await axios.get(`${HEALTH_ENDPOINT}?invalid=param`, {
        validateStatus: () => true
      });

      // Should still return 200 and ignore invalid params
      expect(response.status).toBe(200);
      expect(response.data).toHaveProperty('status');
    });

    it('should not expose sensitive information', async () => {
      const response = await axios.get(HEALTH_ENDPOINT);
      const dataStr = JSON.stringify(response.data).toLowerCase();

      // Should not contain common sensitive fields
      expect(dataStr).not.toMatch(/password/);
      expect(dataStr).not.toMatch(/secret/);
      expect(dataStr).not.toMatch(/token/);
      expect(dataStr).not.toMatch(/api[_-]?key/);
    });
  });

  describe('Health Check Performance', () => {
    it('should maintain performance under load', async () => {
      const iterations = 50;
      const durations: number[] = [];

      for (let i = 0; i < iterations; i++) {
        const start = Date.now();
        await axios.get(HEALTH_ENDPOINT);
        durations.push(Date.now() - start);
      }

      const avgDuration = durations.reduce((a, b) => a + b, 0) / durations.length;
      const maxDuration = Math.max(...durations);

      expect(avgDuration).toBeLessThan(500); // Average under 500ms
      expect(maxDuration).toBeLessThan(2000); // Max under 2s
    });

    it('should handle rapid successive calls', async () => {
      const promises = [];

      // Fire 20 requests as fast as possible
      for (let i = 0; i < 20; i++) {
        promises.push(axios.get(HEALTH_ENDPOINT));
      }

      const responses = await Promise.all(promises);

      // All should succeed
      expect(responses.length).toBe(20);
      responses.forEach(response => {
        expect(response.status).toBe(200);
      });
    });
  });
});

describe('System Health Monitoring', () => {

  it('should detect when service becomes unhealthy', async () => {
    // This is a placeholder for more sophisticated health detection
    // In a real scenario, we'd test actual failure conditions

    const response = await axios.get(HEALTH_ENDPOINT);

    // For now, just verify the health check is working
    expect(['healthy', 'degraded', 'unhealthy']).toContain(response.data.status);
  });

  it('should provide health check with appropriate cache headers', async () => {
    const response = await axios.get(HEALTH_ENDPOINT);

    // Health checks should not be cached
    const cacheControl = response.headers['cache-control'];
    if (cacheControl) {
      expect(cacheControl).toMatch(/no-cache|no-store|max-age=0/);
    }
  });
});