Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 101 additions & 0 deletions src/__tests__/cloud-connect-auth.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { afterEach, test, vi } from 'vitest';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { connectCommand } from '../cli/commands/connection.ts';
import { resolveCloudAccessForConnect } from '../cli/auth-session.ts';
import { readActiveConnectionState } from '../remote-connection-state.ts';
import type { AgentDeviceClient } from '../client.ts';

afterEach(() => {
vi.unstubAllEnvs();
vi.unstubAllGlobals();
vi.restoreAllMocks();
});

test('cloud connect reuses explicit env auth when login is disabled', async () => {
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-device-connect-cloud-auth-'));
const stateDir = path.join(tempRoot, '.state');
const fetchMock = mockConnectionProfileFetch();
vi.stubEnv('AGENT_DEVICE_DAEMON_AUTH_TOKEN', 'adc_live_service');
vi.stubEnv('AGENT_DEVICE_CLOUD_BASE_URL', 'https://cloud.example');

try {
await connectWithNoLogin(stateDir);

const state = readActiveConnectionState({ stateDir });
assert.equal(state?.tenant, 'acme');
assert.equal(fetchMock.mock.calls.length, 1);
assert.equal(
fetchMock.mock.calls[0]?.[0]?.toString(),
'https://cloud.example/api/control-plane/connection-profile',
);
const request = fetchMock.mock.calls[0]?.[1] as RequestInit | undefined;
assert.ok(request);
const headers = request.headers as Record<string, string>;
assert.equal(headers.authorization, 'Bearer adc_live_service');
} finally {
fs.rmSync(tempRoot, { recursive: true, force: true });
}
});

test('cloud access with no-login reports auth requirement after reuse options are exhausted', async () => {
await assert.rejects(
resolveCloudAccessForConnect({
stateDir: fs.mkdtempSync(path.join(os.tmpdir(), 'agent-device-connect-cloud-auth-miss-')),
flags: {
json: false,
help: false,
version: false,
noLogin: true,
},
env: {},
io: {
env: {},
fetch: vi.fn(),
},
}),
/Cloud connection profile authentication is required/,
);
});

function mockConnectionProfileFetch(): ReturnType<typeof vi.fn> {
const fetchMock = vi.fn(
async () =>
new Response(
JSON.stringify({
connection: {
remoteConfigProfile: {
daemonBaseUrl: 'https://bridge.example.com/agent-device',
daemonTransport: 'http',
tenant: 'acme',
runId: 'demo-run-001',
},
},
}),
{ status: 200, headers: { 'content-type': 'application/json' } },
),
);
vi.stubGlobal('fetch', fetchMock);
return fetchMock;
}

async function connectWithNoLogin(stateDir: string): Promise<void> {
const stdoutWrite = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
try {
await connectCommand({
positionals: [],
flags: {
json: true,
help: false,
version: false,
noLogin: true,
stateDir,
},
client: {} as AgentDeviceClient,
});
} finally {
stdoutWrite.mockRestore();
}
}
205 changes: 205 additions & 0 deletions src/__tests__/cloud-connect-profile.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
import { afterEach, test, vi } from 'vitest';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { connectCommand } from '../cli/commands/connection.ts';
import { resolveCloudAccessForConnect } from '../cli/auth-session.ts';
import {
hashRemoteConfigFile,
readActiveConnectionState,
type RemoteConnectionState,
} from '../remote-connection-state.ts';
import type { AgentDeviceClient } from '../client.ts';

vi.mock('../cli/auth-session.ts', () => ({
resolveCloudAccessForConnect: vi.fn(),
}));

afterEach(() => {
vi.clearAllMocks();
vi.restoreAllMocks();
vi.unstubAllGlobals();
});

const mockedResolveCloudAccessForConnect = vi.mocked(resolveCloudAccessForConnect);

test('connect without remote config generates one from cloud connection profile', async () => {
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-device-connect-cloud-'));
const stateDir = path.join(tempRoot, '.state');
const fetchMock = mockCloudConnectionProfile({
remoteConfigProfile: {
daemonBaseUrl: 'https://bridge.example.com/agent-device',
daemonTransport: 'http',
tenant: 'acme',
runId: 'demo-run-001',
sessionIsolation: 'tenant',
metroKind: 'auto',
metroPublicBaseUrl: 'http://127.0.0.1:8081',
metroProxyBaseUrl: 'https://bridge.example.com',
},
});

try {
await connectWithGeneratedCloudProfile(stateDir);
await connectWithGeneratedCloudProfile(stateDir);

assertGeneratedProfileState(readRequiredActiveState(stateDir));
assert.equal(
fetchProfileUrl(fetchMock),
'https://cloud.example/api/control-plane/connection-profile',
);
assert.equal(fetchMock.mock.calls.length, 2);
} finally {
fs.rmSync(tempRoot, { recursive: true, force: true });
}
});

test('connect without remote config rejects legacy remoteConfig string profile response', async () => {
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-device-connect-cloud-legacy-'));
const stateDir = path.join(tempRoot, '.state');
mockCloudConnectionProfile({
remoteConfig: JSON.stringify({
daemonBaseUrl: 'https://bridge.example.com/agent-device',
daemonTransport: 'http',
tenant: 'acme',
runId: 'demo-run-001',
}),
});

try {
await assert.rejects(connectWithGeneratedCloudProfile(stateDir), (error: unknown) => {
assert.equal((error as { code?: string }).code, 'COMMAND_FAILED');
assert.match((error as Error).message, /did not include remoteConfigProfile/);
return true;
});
} finally {
fs.rmSync(tempRoot, { recursive: true, force: true });
}
});

test('connect without remote config reports cloud profile authorization failures', async () => {
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-device-connect-cloud-denied-'));
const stateDir = path.join(tempRoot, '.state');
mockedResolveCloudAccessForConnect.mockResolvedValue({
accessToken: 'adc_agent_cloud',
cloudBaseUrl: 'https://cloud.example',
});
vi.stubGlobal(
'fetch',
vi.fn(
async () =>
new Response(JSON.stringify({ error: 'forbidden' }), {
status: 403,
headers: { 'content-type': 'application/json' },
}),
),
);

try {
await assert.rejects(connectWithGeneratedCloudProfile(stateDir), (error: unknown) => {
assert.equal((error as { code?: string }).code, 'UNAUTHORIZED');
assert.match(
(error as Error).message,
/Cloud connection profile endpoint rejected the request/,
);
return true;
});
} finally {
fs.rmSync(tempRoot, { recursive: true, force: true });
}
});

test('connect without remote config reports unsupported cloud profile keys', async () => {
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-device-connect-cloud-invalid-'));
const stateDir = path.join(tempRoot, '.state');
mockCloudConnectionProfile({
remoteConfigProfile: {
daemonBaseUrl: 'https://bridge.example.com/agent-device',
tenant: 'acme',
runId: 'demo-run-001',
typoTenant: 'wrong',
},
});

try {
await assert.rejects(connectWithGeneratedCloudProfile(stateDir), (error: unknown) => {
assert.equal((error as { code?: string }).code, 'COMMAND_FAILED');
assert.match((error as Error).message, /invalid remote config/);
return true;
});
} finally {
fs.rmSync(tempRoot, { recursive: true, force: true });
}
});

function mockCloudConnectionProfile(connection: Record<string, unknown>): ReturnType<typeof vi.fn> {
mockedResolveCloudAccessForConnect.mockResolvedValue({
accessToken: 'adc_agent_cloud',
cloudBaseUrl: 'https://cloud.example',
});
const fetchMock = vi.fn(
async () =>
new Response(JSON.stringify({ ok: true, connection }), {
status: 200,
headers: { 'content-type': 'application/json' },
}),
);
vi.stubGlobal('fetch', fetchMock);
return fetchMock;
}

function assertGeneratedProfileState(state: RemoteConnectionState): void {
assert.equal(state.tenant, 'acme');
assert.equal(state.runId, 'demo-run-001');
assert.equal(state.daemon?.baseUrl, 'https://bridge.example.com/agent-device');
assert.match(state.remoteConfigPath, /remote-connections\/generated\/cloud-[a-f0-9]{16}\.json$/);
assert.equal(state.remoteConfigHash, hashRemoteConfigFile(state.remoteConfigPath));
assert.deepEqual(readGeneratedConfigKeys(state.remoteConfigPath), [
'daemonBaseUrl',
'daemonTransport',
'metroKind',
'metroProxyBaseUrl',
'metroPublicBaseUrl',
'runId',
'sessionIsolation',
'tenant',
]);
assert.equal(readGeneratedConfig(state.remoteConfigPath).tenant, 'acme');
}

function fetchProfileUrl(fetchMock: ReturnType<typeof vi.fn>): string | undefined {
return fetchMock.mock.calls[0]?.[0]?.toString();
}

async function connectWithGeneratedCloudProfile(stateDir: string): Promise<void> {
const stdoutWrite = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
try {
await connectCommand({
positionals: [],
flags: {
json: true,
help: false,
version: false,
stateDir,
},
client: {} as AgentDeviceClient,
});
} finally {
stdoutWrite.mockRestore();
}
}

function readGeneratedConfig(configPath: string): { tenant?: string } {
return JSON.parse(fs.readFileSync(configPath, 'utf8')) as { tenant?: string };
}

function readGeneratedConfigKeys(configPath: string): string[] {
return Object.keys(readGeneratedConfig(configPath));
}

function readRequiredActiveState(stateDir: string): RemoteConnectionState {
const state = readActiveConnectionState({ stateDir });
assert.ok(state);
return state;
}
Loading
Loading