import { afterEach, beforeEach, describe, expect, it } from 'bun:test'; import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { closeDb } from '../../db/client'; import { runMigrations } from '../../db/migrate'; import { type ProxmoxCredentials, addContainerService, getContainerService, getServiceCredentials, updateVerificationStatus, } from '../../services/container-service'; import { handleServiceSetCredentials } from './service-set-credentials'; describe('service set-credentials', () => { let testDir: string; beforeEach(async () => { testDir = mkdtempSync(join(tmpdir(), 'celilo-service-credentials-test-')); process.env.CELILO_DB_PATH = join(testDir, 'test.db'); process.env.CELILO_MASTER_KEY_PATH = join(testDir, 'master.key'); writeFileSync(process.env.CELILO_MASTER_KEY_PATH, 'a'.repeat(64), 'utf8'); await runMigrations(process.env.CELILO_DB_PATH); }); afterEach(() => { closeDb(); delete process.env.CELILO_DB_PATH; delete process.env.CELILO_MASTER_KEY_PATH; delete process.env.PROXMOX_API_URL; delete process.env.PROXMOX_API_TOKEN_ID; delete process.env.PROXMOX_API_TOKEN_SECRET; delete process.env.DIGITALOCEAN_API_TOKEN; rmSync(testDir, { recursive: true, force: true }); }); it('updates only the Proxmox endpoint and retains the token', async () => { const service = await addContainerService({ name: 'Chubs', providerName: 'proxmox', zones: ['internal'], providerConfig: {}, apiCredentials: { api_url: 'https://192.168.0.50:8006', api_token_id: 'root@pam!celilo', api_token_secret: 'existing-secret', }, }); await updateVerificationStatus(service.id, { success: true, message: 'Connected' }); const result = await handleServiceSetCredentials(['chubs'], { 'api-url': 'https://10.77.20.50:8006', }); expect(result.success).toBe(true); expect(await getServiceCredentials(service.id)).toEqual({ api_url: 'https://10.77.20.50:8006', api_token_id: 'root@pam!celilo', api_token_secret: 'existing-secret', }); expect((await getContainerService(service.id))?.verified).toBe(false); if (!result.success) throw new Error(result.error); expect(result.message).not.toContain('existing-secret'); }); it('accepts the endpoint through the documented environment variable', async () => { const service = await addContainerService({ name: 'Nubs', providerName: 'proxmox', zones: ['internal'], providerConfig: {}, apiCredentials: { api_url: 'https://192.168.0.51:8006', api_token_id: 'root@pam!celilo', api_token_secret: 'existing-secret', }, }); process.env.PROXMOX_API_URL = 'https://10.77.20.51:8006'; const result = await handleServiceSetCredentials(['nubs']); expect(result.success).toBe(true); expect(((await getServiceCredentials(service.id)) as ProxmoxCredentials).api_url).toBe( 'https://10.77.20.51:8006', ); }); it('refuses a no-op that would only churn encrypted state', async () => { await addContainerService({ name: 'Chubs', providerName: 'proxmox', zones: ['internal'], providerConfig: {}, apiCredentials: { api_url: 'https://192.168.0.50:8006', api_token_id: 'root@pam!celilo', api_token_secret: 'existing-secret', }, }); const result = await handleServiceSetCredentials(['chubs']); expect(result.success).toBe(false); if (result.success) throw new Error('Expected set-credentials to reject a no-op'); expect(result.error).toContain('No credential changes supplied'); }); });