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
27 changes: 27 additions & 0 deletions packages/provider-webdriver/src/runtime-session.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,33 @@ test('a create-timeout surfaces provider evidence for the maybe-leaked session',
}
});

test('cloud artifact lookup does not accept a provider session id without its lease', async () => {
let listCalls = 0;
const runtime = makeRuntime({
listArtifacts: async () => {
listCalls += 1;
return {
provider: 'webdriver-test',
status: 'ready',
cloudArtifacts: [],
};
},
});

try {
const cloudArtifacts = runtime.cloudArtifacts;
assert.ok(cloudArtifacts);
const result = await cloudArtifacts.listCloudArtifacts?.({
provider: 'webdriver-test',
providerSessionId: 'never-authorized',
});
assert.equal(result, undefined);
assert.equal(listCalls, 0);
} finally {
await runtime.shutdown();
}
});

function makeRuntime(overrides: Partial<CloudWebDriverRuntimeOptions> = {}) {
return createCloudWebDriverRuntime({
clientVersion: 'test',
Expand Down
23 changes: 18 additions & 5 deletions packages/provider-webdriver/src/runtime-session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,12 +146,25 @@ export class WebDriverSessionManager {

async listCloudArtifacts(query: CloudArtifactsQuery): Promise<CloudArtifactsResult | undefined> {
if (query.provider !== this.options.provider) return undefined;
const session = query.leaseId ? this.sessionsByLeaseId.get(query.leaseId) : undefined;
if (session) return await this.safeListArtifacts(session);
const providerSessionId =
query.providerSessionId ??
(query.leaseId ? this.releasedProviderSessionIdsByLeaseId.get(query.leaseId) : undefined);
const session = this.sessionsByLeaseId.get(query.leaseId ?? '');
if (session) return await this.listActiveCloudArtifacts(session, query.providerSessionId);
return await this.listReleasedCloudArtifacts(query);
}

private async listActiveCloudArtifacts(
session: WebDriverProviderSession,
providerSessionId: string | undefined,
): Promise<CloudArtifactsResult | undefined> {
if (providerSessionId && providerSessionId !== session.providerSessionId) return undefined;
return await this.safeListArtifacts(session);
}

private async listReleasedCloudArtifacts(
query: CloudArtifactsQuery,
): Promise<CloudArtifactsResult | undefined> {
const providerSessionId = this.releasedProviderSessionIdsByLeaseId.get(query.leaseId ?? '');
if (!providerSessionId || !this.options.listArtifacts) return undefined;
if (query.providerSessionId && query.providerSessionId !== providerSessionId) return undefined;
return await this.options.listArtifacts({
provider: this.options.provider,
providerSessionId,
Expand Down
30 changes: 8 additions & 22 deletions src/__tests__/daemon-entrypoint.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,11 +171,10 @@ test('daemon runtime publishes dual transport metadata', async () => {
}
});

test('daemon default provider composition serves cloud artifacts over RPC', async () => {
test('daemon rejects unowned cloud artifacts over RPC', async () => {
const stateDir = mkdtempForTestSync('agent-device-daemon-provider-');
const providerRequests: string[] = [];
const providerServer = http.createServer((req, res) => {
providerRequests.push(req.url ?? '');
const providerServer = http.createServer((_req, res) => {
res.setHeader('content-type', 'application/json');
res.end(
JSON.stringify({
Expand Down Expand Up @@ -220,27 +219,14 @@ test('daemon default provider composition serves cloud artifacts over RPC', asyn
}),
});
const body = (await response.json()) as {
result?: { ok?: boolean; data?: Record<string, unknown> };
error?: { code?: number; message?: string; data?: { code?: string; details?: unknown } };
};

assert.equal(response.status, 200);
assert.equal(body.result?.ok, true);
assert.deepEqual(body.result?.data, {
provider: 'browserstack',
providerSessionId: 'wd-1',
status: 'ready',
cloudArtifacts: [
{
provider: 'browserstack',
providerSessionId: 'wd-1',
kind: 'video',
name: 'Session video',
url: 'https://browserstack.example/video.mp4',
availability: 'ready',
},
],
});
assert.deepEqual(providerRequests, ['/sessions/wd-1.json']);
assert.equal(response.status, 401);
assert.equal(body.error?.code, -32000);
assert.equal(body.error?.data?.code, 'UNAUTHORIZED');
assert.deepEqual(body.error?.data?.details, { reason: 'PROVIDER_SESSION_NOT_OWNED' });
assert.deepEqual(providerRequests, []);
} finally {
await runtime?.shutdown();
await closeLoopbackServer(providerServer);
Expand Down
35 changes: 35 additions & 0 deletions src/daemon/__tests__/lease-lifecycle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,41 @@ test('releaseSessionLease releases with the stored session owner scope', async (
expect(provider).toEqual({ provider: 'proxy' });
});

test('releaseSessionLease retains provider session ownership for artifact lookup', async () => {
const leaseRegistry = new LeaseRegistry();
const lease = leaseRegistry.allocateLease({
tenantId: 'tenant-a',
runId: 'run-1',
leaseBackend: 'android-instance',
leaseProvider: 'browserstack',
});
const session = makeIosSession('default', {
lease: {
leaseId: lease.leaseId,
tenantId: lease.tenantId,
runId: lease.runId,
leaseBackend: lease.backend,
leaseProvider: lease.leaseProvider,
},
});

await releaseSessionLease({
session,
leaseRegistry,
leaseLifecycleProvider: {
release: async () => ({ providerSessionId: 'bs-session-1' }),
},
});

expect(
leaseRegistry.resolveProviderSession({
provider: 'browserstack',
providerSessionId: 'bs-session-1',
tenantId: 'tenant-a',
}),
).toMatchObject({ leaseId: lease.leaseId, tenantId: 'tenant-a' });
});

test('releaseExpiredProviderLease releases a provider-owned lease without a session', async () => {
const lease = new LeaseRegistry().allocateLease({
tenantId: 'tenant-a',
Expand Down
Loading
Loading