/** * Tailscale Route Tests * * Tests Tailscale status, configuration, and connection-checking endpoints. * The Tailscale routes are mounted at /api/tailscale/ on the API router: * - GET /api/tailscale/status — Tailscale status * - POST /api/tailscale/config — Update Tailscale configuration * - GET /api/tailscale/check-connection — Check if request comes from Tailscale IP * - GET /api/tailscale/devices — List Tailscale devices * - POST /api/tailscale/protect-service — Toggle Tailscale-only for a service * - POST /api/tailscale/oauth-config — OAuth credential setup (requires live API) * - GET /api/tailscale/api-devices — Enriched device list from API * - POST /api/tailscale/sync — Trigger API sync * - GET /api/tailscale/acl — Fetch ACL policy */ const request = require('supertest'); const fs = require('fs'); const path = require('path'); const os = require('os'); const testServicesFile = path.join(os.tmpdir(), `tailscale-services-${Date.now()}.json`); const testConfigFile = path.join(os.tmpdir(), `tailscale-config-${Date.now()}.json`); process.env.SERVICES_FILE = testServicesFile; process.env.CONFIG_FILE = testConfigFile; process.env.ENABLE_HEALTH_CHECKER = 'false'; process.env.NODE_ENV = 'test'; fs.writeFileSync(testServicesFile, '[]', 'utf8'); fs.writeFileSync(testConfigFile, '{}', 'utf8'); const app = require('../server'); describe('Tailscale Routes', () => { afterAll(() => { try { fs.unlinkSync(testServicesFile); } catch (e) { /* ignore */ } try { fs.unlinkSync(testConfigFile); } catch (e) { /* ignore */ } }); describe('GET /api/tailscale/status', () => { test('should return 200 with status data', async () => { const res = await request(app).get('/api/tailscale/status'); expect(res.statusCode).toBe(200); expect(res.body.success).toBe(true); // If Tailscale is not installed in test env, expect installed: false if (!res.body.installed) { expect(res.body.installed).toBe(false); expect(res.body.connected).toBe(false); expect(res.body.message).toBeDefined(); } else { // If installed, expect richer data expect(res.body).toHaveProperty('connected'); expect(res.body).toHaveProperty('self'); expect(res.body).toHaveProperty('config'); expect(res.body).toHaveProperty('devices'); expect(res.body).toHaveProperty('deviceCount'); } }); }); describe('GET /api/tailscale/check-connection', () => { test('should return 200 with connection info', async () => { const res = await request(app).get('/api/tailscale/check-connection'); expect(res.statusCode).toBe(200); expect(res.body.success).toBe(true); expect(res.body).toHaveProperty('isTailscale'); expect(typeof res.body.isTailscale).toBe('boolean'); expect(res.body).toHaveProperty('clientIP'); }); test('should detect non-Tailscale IP for localhost requests', async () => { const res = await request(app).get('/api/tailscale/check-connection'); expect(res.statusCode).toBe(200); // Supertest connects via loopback, not a 100.x.x.x address expect(res.body.isTailscale).toBe(false); }); }); describe('GET /api/tailscale/devices', () => { test('should return 200 with devices array', async () => { const res = await request(app).get('/api/tailscale/devices'); expect(res.statusCode).toBe(200); expect(res.body.success).toBe(true); expect(res.body).toHaveProperty('devices'); expect(Array.isArray(res.body.devices)).toBe(true); }); }); describe('POST /api/tailscale/oauth-config', () => { test('should reject missing required fields', async () => { const res = await request(app) .post('/api/tailscale/oauth-config') .send({}); expect(res.statusCode).toBe(400); }); test('should reject partial credentials', async () => { const res = await request(app) .post('/api/tailscale/oauth-config') .send({ clientId: 'test-id' }); expect(res.statusCode).toBe(400); }); }); describe('GET /api/tailscale/api-devices', () => { test('should return 400 when OAuth is not configured', async () => { const res = await request(app).get('/api/tailscale/api-devices'); expect(res.statusCode).toBe(400); }); }); describe('POST /api/tailscale/sync', () => { test('should return 400 when OAuth is not configured', async () => { const res = await request(app).post('/api/tailscale/sync'); expect(res.statusCode).toBe(400); }); }); describe('POST /api/tailscale/protect-service', () => { test('should reject missing subdomain', async () => { const res = await request(app) .post('/api/tailscale/protect-service') .send({}); expect(res.statusCode).toBe(400); }); }); });