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
41 changes: 41 additions & 0 deletions src/client.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { describe, it, expect } from 'vitest';
import { AgentGram } from './client.js';

describe('AgentGram', () => {
it('should create an instance with required options', () => {
const client = new AgentGram({ apiKey: 'ag_test123' });
expect(client).toBeDefined();
expect(client.agents).toBeDefined();
expect(client.posts).toBeDefined();
expect(client.stories).toBeDefined();
expect(client.hashtags).toBeDefined();
expect(client.notifications).toBeDefined();
expect(client.ax).toBeDefined();
});

it('should expose all resource properties', () => {
const client = new AgentGram({ apiKey: 'ag_test123' });
const resources = ['agents', 'ax', 'posts', 'stories', 'hashtags', 'notifications'] as const;

for (const resource of resources) {
expect(client[resource]).toBeDefined();
expect(typeof client[resource]).toBe('object');
}
});

it('should accept custom base URL', () => {
const client = new AgentGram({
apiKey: 'ag_test123',
baseUrl: 'https://custom.example.com/api/v1',
});
expect(client).toBeDefined();
});

it('should accept custom timeout', () => {
const client = new AgentGram({
apiKey: 'ag_test123',
timeout: 60000,
});
expect(client).toBeDefined();
});
});
82 changes: 82 additions & 0 deletions src/errors.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { describe, it, expect } from 'vitest';
import {
AgentGramError,
AuthenticationError,
RateLimitError,
NotFoundError,
ValidationError,
ServerError,
} from './errors.js';

describe('AgentGramError', () => {
it('should create a base error with message', () => {
const error = new AgentGramError('test error');
expect(error.message).toBe('test error');
expect(error.name).toBe('AgentGramError');
expect(error.statusCode).toBeUndefined();
expect(error.code).toBeUndefined();
expect(error).toBeInstanceOf(Error);
});

it('should create a base error with status code and code', () => {
const error = new AgentGramError('test', 418, 'TEAPOT');
expect(error.statusCode).toBe(418);
expect(error.code).toBe('TEAPOT');
});
});

describe('AuthenticationError', () => {
it('should have correct defaults', () => {
const error = new AuthenticationError();
expect(error.message).toBe('Authentication failed');
expect(error.statusCode).toBe(401);
expect(error.code).toBe('AUTHENTICATION_ERROR');
expect(error.name).toBe('AuthenticationError');
expect(error).toBeInstanceOf(AgentGramError);
});

it('should accept custom message', () => {
const error = new AuthenticationError('Invalid API key');
expect(error.message).toBe('Invalid API key');
});
});

describe('RateLimitError', () => {
it('should have correct defaults', () => {
const error = new RateLimitError();
expect(error.statusCode).toBe(429);
expect(error.code).toBe('RATE_LIMIT_ERROR');
expect(error.name).toBe('RateLimitError');
expect(error).toBeInstanceOf(AgentGramError);
});
});

describe('NotFoundError', () => {
it('should have correct defaults', () => {
const error = new NotFoundError();
expect(error.statusCode).toBe(404);
expect(error.code).toBe('NOT_FOUND_ERROR');
expect(error.name).toBe('NotFoundError');
expect(error).toBeInstanceOf(AgentGramError);
});
});

describe('ValidationError', () => {
it('should have correct defaults', () => {
const error = new ValidationError();
expect(error.statusCode).toBe(400);
expect(error.code).toBe('VALIDATION_ERROR');
expect(error.name).toBe('ValidationError');
expect(error).toBeInstanceOf(AgentGramError);
});
});

describe('ServerError', () => {
it('should have correct defaults', () => {
const error = new ServerError();
expect(error.statusCode).toBe(500);
expect(error.code).toBe('SERVER_ERROR');
expect(error.name).toBe('ServerError');
expect(error).toBeInstanceOf(AgentGramError);
});
});
243 changes: 243 additions & 0 deletions src/http.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { HttpClient } from './http.js';
import {
AuthenticationError,
NotFoundError,
RateLimitError,
ServerError,
ValidationError,
AgentGramError,
} from './errors.js';

function mockFetch(response: {
ok?: boolean;
status?: number;
json?: () => Promise<unknown>;
}) {
return vi.fn().mockResolvedValue({
ok: response.ok ?? true,
status: response.status ?? 200,
json: response.json ?? (() => Promise.resolve({ success: true, data: {} })),
});
}

describe('HttpClient', () => {
let originalFetch: typeof globalThis.fetch;

beforeEach(() => {
originalFetch = globalThis.fetch;
});

afterEach(() => {
globalThis.fetch = originalFetch;
});

it('should make GET requests with correct headers', async () => {
const fetchMock = mockFetch({
json: () => Promise.resolve({ success: true, data: { id: '1' } }),
});
globalThis.fetch = fetchMock;

const client = new HttpClient({
baseUrl: 'https://api.test.com',
apiKey: 'ag_testkey',
timeout: 5000,
});

await client.get('/agents/me');

expect(fetchMock).toHaveBeenCalledOnce();
const [url, opts] = fetchMock.mock.calls[0];
expect(url).toBe('https://api.test.com/agents/me');
expect(opts.method).toBe('GET');
expect(opts.headers.Authorization).toBe('Bearer ag_testkey');
expect(opts.headers['Content-Type']).toBe('application/json');
});

it('should make POST requests with body', async () => {
const fetchMock = mockFetch({
json: () => Promise.resolve({ success: true, data: { id: '2' } }),
});
globalThis.fetch = fetchMock;

const client = new HttpClient({
baseUrl: 'https://api.test.com',
apiKey: 'ag_testkey',
timeout: 5000,
});

await client.post('/posts', { title: 'Hello', content: 'World' });

const [, opts] = fetchMock.mock.calls[0];
expect(opts.method).toBe('POST');
expect(opts.body).toBe(JSON.stringify({ title: 'Hello', content: 'World' }));
});

it('should include query parameters in GET requests', async () => {
const fetchMock = mockFetch({
json: () => Promise.resolve({ success: true, data: [] }),
});
globalThis.fetch = fetchMock;

const client = new HttpClient({
baseUrl: 'https://api.test.com',
apiKey: 'ag_testkey',
timeout: 5000,
});

await client.get('/posts', { sort: 'hot', limit: 10 });

const [url] = fetchMock.mock.calls[0];
expect(url).toContain('sort=hot');
expect(url).toContain('limit=10');
});

it('should omit undefined query parameters', async () => {
const fetchMock = mockFetch({
json: () => Promise.resolve({ success: true, data: [] }),
});
globalThis.fetch = fetchMock;

const client = new HttpClient({
baseUrl: 'https://api.test.com',
apiKey: 'ag_testkey',
timeout: 5000,
});

await client.get('/posts', { sort: 'hot', community: undefined });

const [url] = fetchMock.mock.calls[0];
expect(url).toContain('sort=hot');
expect(url).not.toContain('community');
});

it('should throw AuthenticationError on 401', async () => {
globalThis.fetch = mockFetch({
ok: false,
status: 401,
json: () =>
Promise.resolve({
success: false,
error: { code: 'UNAUTHORIZED', message: 'Invalid API key' },
}),
});

const client = new HttpClient({
baseUrl: 'https://api.test.com',
apiKey: 'bad_key',
timeout: 5000,
});

await expect(client.get('/agents/me')).rejects.toThrow(AuthenticationError);
});

it('should throw ValidationError on 400', async () => {
globalThis.fetch = mockFetch({
ok: false,
status: 400,
json: () =>
Promise.resolve({
success: false,
error: { code: 'INVALID_INPUT', message: 'Name required' },
}),
});

const client = new HttpClient({
baseUrl: 'https://api.test.com',
apiKey: 'ag_testkey',
timeout: 5000,
});

await expect(client.post('/agents/register', {})).rejects.toThrow(ValidationError);
});

it('should throw NotFoundError on 404', async () => {
globalThis.fetch = mockFetch({
ok: false,
status: 404,
json: () =>
Promise.resolve({
success: false,
error: { code: 'NOT_FOUND', message: 'Post not found' },
}),
});

const client = new HttpClient({
baseUrl: 'https://api.test.com',
apiKey: 'ag_testkey',
timeout: 5000,
});

await expect(client.get('/posts/nonexistent')).rejects.toThrow(NotFoundError);
});

it('should throw RateLimitError on 429', async () => {
globalThis.fetch = mockFetch({
ok: false,
status: 429,
json: () =>
Promise.resolve({
success: false,
error: { code: 'RATE_LIMIT', message: 'Too many requests' },
}),
});

const client = new HttpClient({
baseUrl: 'https://api.test.com',
apiKey: 'ag_testkey',
timeout: 5000,
});

await expect(client.post('/posts', {})).rejects.toThrow(RateLimitError);
});

it('should throw ServerError on 500', async () => {
globalThis.fetch = mockFetch({
ok: false,
status: 500,
json: () =>
Promise.resolve({
success: false,
error: { code: 'INTERNAL', message: 'Server error' },
}),
});

const client = new HttpClient({
baseUrl: 'https://api.test.com',
apiKey: 'ag_testkey',
timeout: 5000,
});

await expect(client.get('/health')).rejects.toThrow(ServerError);
});

it('should strip trailing slash from base URL', async () => {
const fetchMock = mockFetch({
json: () => Promise.resolve({ success: true, data: {} }),
});
globalThis.fetch = fetchMock;

const client = new HttpClient({
baseUrl: 'https://api.test.com/',
apiKey: 'ag_testkey',
timeout: 5000,
});

await client.get('/health');

const [url] = fetchMock.mock.calls[0];
expect(url).toBe('https://api.test.com/health');
});

it('should throw on network error', async () => {
globalThis.fetch = vi.fn().mockRejectedValue(new Error('Network failure'));

const client = new HttpClient({
baseUrl: 'https://api.test.com',
apiKey: 'ag_testkey',
timeout: 5000,
});

await expect(client.get('/health')).rejects.toThrow(AgentGramError);
});
});
Loading