initial commit
This commit is contained in:
106 files changed
+24584
No files matched your search
@@ -0,0 +1,137 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { MockGameServerProvider } from '../src/modules/pterodactyl/mock-provider.js';
|
||||
import { PterodactylProvider } from '../src/modules/pterodactyl/pterodactyl-provider.js';
|
||||
import { ApiError } from '../src/lib/errors.js';
|
||||
|
||||
const API_KEY = 'ptlc_super_secret_key_123';
|
||||
|
||||
function jsonResponse(body: unknown, status = 200): Response {
|
||||
return new Response(JSON.stringify(body), {
|
||||
status,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
|
||||
describe('MockGameServerProvider', () => {
|
||||
it('reports online with plausible resources by default', async () => {
|
||||
const provider = new MockGameServerProvider();
|
||||
expect(await provider.getServerStatus()).toBe('online');
|
||||
const resources = await provider.getServerResources();
|
||||
expect(resources.status).toBe('online');
|
||||
expect(resources.memoryLimitBytes).toBeGreaterThan(0);
|
||||
expect(resources.uptimeMs).toBeGreaterThan(0);
|
||||
provider.dispose();
|
||||
});
|
||||
|
||||
it('transitions through stopping on stop', async () => {
|
||||
const provider = new MockGameServerProvider();
|
||||
await provider.stopServer();
|
||||
expect(await provider.getServerStatus()).toBe('stopping');
|
||||
provider.dispose();
|
||||
});
|
||||
|
||||
it('serves a parseable console.log fixture', async () => {
|
||||
const provider = new MockGameServerProvider();
|
||||
const file = await provider.downloadTextFile('any', '/profile/logs/console.log');
|
||||
expect(file.content).toContain('connected');
|
||||
expect(file.content).toContain('Log started');
|
||||
expect(file.contentStartOffset).toBe(0);
|
||||
provider.dispose();
|
||||
});
|
||||
|
||||
it('rejects unknown paths instead of exposing a file system', async () => {
|
||||
const provider = new MockGameServerProvider();
|
||||
await expect(provider.downloadTextFile('any', '/etc/passwd')).rejects.toThrow(ApiError);
|
||||
provider.dispose();
|
||||
});
|
||||
});
|
||||
|
||||
describe('PterodactylProvider', () => {
|
||||
it('maps resource responses from the Client API', async () => {
|
||||
const fetchImpl = vi.fn(async (url: string | URL, _init?: RequestInit) => {
|
||||
const path = String(url);
|
||||
if (path.endsWith('/resources')) {
|
||||
return jsonResponse({
|
||||
object: 'stats',
|
||||
attributes: {
|
||||
current_state: 'running',
|
||||
resources: {
|
||||
memory_bytes: 1024,
|
||||
cpu_absolute: 51.5,
|
||||
disk_bytes: 2048,
|
||||
network_rx_bytes: 10,
|
||||
network_tx_bytes: 20,
|
||||
uptime: 5000,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
return jsonResponse({ attributes: { limits: { cpu: 400, memory: 8192, disk: 40960 } } });
|
||||
});
|
||||
const provider = new PterodactylProvider({
|
||||
baseUrl: 'https://panel.example.com',
|
||||
apiKey: API_KEY,
|
||||
fetchImpl: fetchImpl as unknown as typeof fetch,
|
||||
});
|
||||
|
||||
const resources = await provider.getServerResources('abc123');
|
||||
expect(resources.status).toBe('online');
|
||||
expect(resources.cpuPercent).toBe(51.5);
|
||||
expect(resources.cpuLimitPercent).toBe(400);
|
||||
expect(resources.memoryLimitBytes).toBe(8192 * 1024 * 1024);
|
||||
|
||||
const [calledUrl, calledInit] = fetchImpl.mock.calls[0]!;
|
||||
expect(String(calledUrl)).toBe('https://panel.example.com/api/client/servers/abc123/resources');
|
||||
const headers = calledInit?.headers as Record<string, string>;
|
||||
expect(headers.Authorization).toBe(`Bearer ${API_KEY}`);
|
||||
});
|
||||
|
||||
it('sends power signals with the expected body', async () => {
|
||||
const fetchImpl = vi.fn(async (_url: string | URL, _init?: RequestInit) => {
|
||||
return new Response(null, { status: 204 });
|
||||
});
|
||||
const provider = new PterodactylProvider({
|
||||
baseUrl: 'https://panel.example.com',
|
||||
apiKey: API_KEY,
|
||||
fetchImpl: fetchImpl as unknown as typeof fetch,
|
||||
});
|
||||
await provider.restartServer('abc123');
|
||||
const [url, init] = fetchImpl.mock.calls[0]!;
|
||||
expect(String(url)).toContain('/power');
|
||||
expect(init?.method).toBe('POST');
|
||||
expect(JSON.parse(String(init?.body))).toEqual({ signal: 'restart' });
|
||||
});
|
||||
|
||||
it('maps HTTP errors without leaking the API key or full URL', async () => {
|
||||
const fetchImpl = vi.fn(async () => new Response('nope', { status: 500 }));
|
||||
const provider = new PterodactylProvider({
|
||||
baseUrl: 'https://panel.example.com',
|
||||
apiKey: API_KEY,
|
||||
fetchImpl: fetchImpl as unknown as typeof fetch,
|
||||
});
|
||||
const error = await provider.getServerResources('abc123').catch((e: unknown) => e as ApiError);
|
||||
expect(error).toBeInstanceOf(ApiError);
|
||||
expect((error as ApiError).code).toBe('UPSTREAM_UNAVAILABLE');
|
||||
expect((error as ApiError).message).not.toContain(API_KEY);
|
||||
expect((error as ApiError).message).not.toContain('panel.example.com');
|
||||
expect((error as ApiError).message).toContain('500');
|
||||
});
|
||||
|
||||
it('maps timeouts to a sanitized upstream error', async () => {
|
||||
const timeoutError = new Error('The operation was aborted due to timeout');
|
||||
timeoutError.name = 'TimeoutError';
|
||||
const fetchImpl = vi.fn(async () => {
|
||||
throw timeoutError;
|
||||
});
|
||||
const provider = new PterodactylProvider({
|
||||
baseUrl: 'https://panel.example.com',
|
||||
apiKey: API_KEY,
|
||||
fetchImpl: fetchImpl as unknown as typeof fetch,
|
||||
timeoutMs: 50,
|
||||
});
|
||||
const error = await provider.getServerStatus('abc123').catch((e: unknown) => e as ApiError);
|
||||
expect(error).toBeInstanceOf(ApiError);
|
||||
expect((error as ApiError).message).toContain('timed out');
|
||||
expect((error as ApiError).message).not.toContain(API_KEY);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user