9b395f5cc3
E2E Headed Chrome / e2e-headed (macos-15) (push) Has been cancelled
E2E Headed Chrome / e2e-headed (ubuntu-latest) (push) Has been cancelled
E2E Headed Chrome / e2e-headed (windows-latest) (push) Has been cancelled
CI / build (macos-latest) (push) Has been cancelled
CI / build (ubuntu-latest) (push) Has been cancelled
CI / build (windows-latest) (push) Has been cancelled
CI / unit-test (push) Has been cancelled
CI / bun-test (push) Has been cancelled
CI / adapter-test (push) Has been cancelled
CI / smoke-test (macos-latest) (push) Has been cancelled
CI / smoke-test (ubuntu-latest) (push) Has been cancelled
Security Audit / audit (push) Has been cancelled
Build Chrome Extension / build (push) Has been cancelled
Trigger Website Rebuild (Docs Updated) / dispatch (push) Has been cancelled
110 lines
5.0 KiB
JavaScript
110 lines
5.0 KiB
JavaScript
import { describe, expect, it, vi } from 'vitest';
|
|
import { getRegistry } from '@jackwener/opencli/registry';
|
|
import { AuthRequiredError, CliError, CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors';
|
|
import './connections.js';
|
|
|
|
const { mapConnection } = await import('./connections.js').then((m) => m.__test__);
|
|
|
|
function makePage({ evaluateResults = [false], cookies = [{ name: 'JSESSIONID', value: '"ajax:12345"' }] } = {}) {
|
|
const evaluate = vi.fn();
|
|
for (const result of evaluateResults) evaluate.mockResolvedValueOnce(result);
|
|
return {
|
|
goto: vi.fn().mockResolvedValue(undefined),
|
|
wait: vi.fn().mockResolvedValue(undefined),
|
|
getCookies: vi.fn().mockResolvedValue(cookies),
|
|
evaluate,
|
|
};
|
|
}
|
|
|
|
const conn = (id) => ({
|
|
createdAt: 1700000000000 + id,
|
|
miniProfile: { firstName: 'First' + id, lastName: 'Last' + id, occupation: 'Job ' + id, publicIdentifier: 'user' + id },
|
|
});
|
|
|
|
describe('linkedin connections', () => {
|
|
it('maps a connection element to a row', () => {
|
|
expect(mapConnection(conn(1), 0)).toEqual({
|
|
rank: 1,
|
|
name: 'First1 Last1',
|
|
occupation: 'Job 1',
|
|
public_id: 'user1',
|
|
connected_at: 1700000000001,
|
|
url: 'https://www.linkedin.com/in/user1',
|
|
});
|
|
});
|
|
|
|
it('throws when an element has no miniProfile', () => {
|
|
expect(() => mapConnection({ createdAt: 1 }, 0)).toThrow(CommandExecutionError);
|
|
});
|
|
|
|
it('requires a stable public profile identity for every row', () => {
|
|
expect(() => mapConnection({
|
|
createdAt: 1,
|
|
miniProfile: { firstName: 'Only', lastName: 'Name', occupation: 'No id' },
|
|
}, 0)).toThrow(CommandExecutionError);
|
|
expect(() => mapConnection({
|
|
createdAt: 1,
|
|
miniProfile: { firstName: 'Bad', lastName: 'Id', publicIdentifier: 'bad/id' },
|
|
}, 0)).toThrow(CommandExecutionError);
|
|
});
|
|
|
|
it('fails closed for malformed miniProfile scalar fields', () => {
|
|
expect(() => mapConnection({
|
|
createdAt: 1,
|
|
miniProfile: { firstName: { text: 'Alice' }, lastName: 'Example', publicIdentifier: 'alice' },
|
|
}, 0)).toThrow(CommandExecutionError);
|
|
});
|
|
|
|
it('returns rows from the voyager connections API', async () => {
|
|
const cmd = getRegistry().get('linkedin/connections');
|
|
expect(cmd?.func).toBeTypeOf('function');
|
|
const page = makePage({ evaluateResults: [false, { json: { elements: [conn(1), conn(2)] } }] });
|
|
const rows = await cmd.func(page, { limit: 2 });
|
|
expect(rows.map((r) => r.public_id)).toEqual(['user1', 'user2']);
|
|
expect(page.evaluate.mock.calls[1][0]).toContain('/voyager/api/relationships/connections?start=0&count=2');
|
|
expect(page.evaluate.mock.calls[1][0]).toContain('ajax:12345');
|
|
});
|
|
|
|
it('throws AuthRequiredError when the connections page is an auth wall', async () => {
|
|
const cmd = getRegistry().get('linkedin/connections');
|
|
const page = makePage({ evaluateResults: [true] });
|
|
await expect(cmd.func(page, { limit: 2 })).rejects.toBeInstanceOf(AuthRequiredError);
|
|
});
|
|
|
|
it('throws AuthRequiredError when JSESSIONID is missing', async () => {
|
|
const cmd = getRegistry().get('linkedin/connections');
|
|
const page = makePage({ evaluateResults: [false], cookies: [] });
|
|
await expect(cmd.func(page, { limit: 2 })).rejects.toBeInstanceOf(AuthRequiredError);
|
|
});
|
|
|
|
it('maps an authRequired fetch result to AuthRequiredError', async () => {
|
|
const cmd = getRegistry().get('linkedin/connections');
|
|
const page = makePage({ evaluateResults: [false, { authRequired: true, error: 'HTTP 403' }] });
|
|
await expect(cmd.func(page, { limit: 2 })).rejects.toBeInstanceOf(AuthRequiredError);
|
|
});
|
|
|
|
it('maps HTML checkpoint responses to AuthRequiredError', async () => {
|
|
const cmd = getRegistry().get('linkedin/connections');
|
|
const page = makePage({ evaluateResults: [false, { authRequired: true, error: 'HTML auth/checkpoint response' }] });
|
|
await expect(cmd.func(page, { limit: 2 })).rejects.toBeInstanceOf(AuthRequiredError);
|
|
});
|
|
|
|
it('fails closed for non-JSON API drift', async () => {
|
|
const cmd = getRegistry().get('linkedin/connections');
|
|
const page = makePage({ evaluateResults: [false, { error: 'response was not valid JSON' }] });
|
|
await expect(cmd.func(page, { limit: 2 })).rejects.toBeInstanceOf(CommandExecutionError);
|
|
});
|
|
|
|
it('throws EmptyResultError when there are no connections', async () => {
|
|
const cmd = getRegistry().get('linkedin/connections');
|
|
const page = makePage({ evaluateResults: [false, { json: { elements: [] } }] });
|
|
await expect(cmd.func(page, { limit: 2 })).rejects.toBeInstanceOf(EmptyResultError);
|
|
});
|
|
|
|
it('rejects invalid limits', async () => {
|
|
const cmd = getRegistry().get('linkedin/connections');
|
|
const page = makePage();
|
|
await expect(cmd.func(page, { limit: 0 })).rejects.toBeInstanceOf(CliError);
|
|
});
|
|
});
|