glam/frontend/tests/unit/sparql-client.test.ts
kempersc 2761857b0d Add scripts for converting OWL/Turtle ontology to Mermaid and PlantUML diagrams
- Implemented `owl_to_mermaid.py` to convert OWL/Turtle files into Mermaid class diagrams.
- Implemented `owl_to_plantuml.py` to convert OWL/Turtle files into PlantUML class diagrams.
- Added two new PlantUML files for custodian multi-aspect diagrams.
2025-11-22 23:01:13 +01:00

283 lines
8.1 KiB
TypeScript

/**
* Tests for SPARQL HTTP Client
*/
import { describe, it, expect, beforeEach, vi } from 'vitest';
import {
SparqlClient,
SparqlError,
createSparqlClient,
getDefaultClient,
type SelectResults,
} from '../../src/lib/sparql/client';
describe('SparqlClient', () => {
let client: SparqlClient;
beforeEach(() => {
client = new SparqlClient({ endpoint: 'http://localhost:7878/query' });
// Clear fetch mocks
vi.restoreAllMocks();
});
describe('constructor', () => {
it('should create client with endpoint', () => {
expect(client).toBeInstanceOf(SparqlClient);
});
it('should use default timeout if not specified', () => {
const client = new SparqlClient({ endpoint: 'http://test' });
expect(client).toBeDefined();
});
it('should accept custom timeout', () => {
const client = new SparqlClient({
endpoint: 'http://test',
timeout: 60000,
});
expect(client).toBeDefined();
});
it('should accept custom headers', () => {
const client = new SparqlClient({
endpoint: 'http://test',
headers: { 'X-Custom': 'value' },
});
expect(client).toBeDefined();
});
});
describe('executeSelect', () => {
it('should execute SELECT query successfully', async () => {
const mockResponse: SelectResults = {
head: { vars: ['s', 'p', 'o'] },
results: {
bindings: [
{
s: { type: 'uri', value: 'http://example.org/subject' },
p: { type: 'uri', value: 'http://example.org/predicate' },
o: { type: 'literal', value: 'object' },
},
],
},
};
global.fetch = vi.fn().mockResolvedValue({
ok: true,
json: async () => mockResponse,
});
const result = await client.executeSelect('SELECT * WHERE { ?s ?p ?o }');
expect(result).toEqual(mockResponse);
expect(global.fetch).toHaveBeenCalledWith(
'http://localhost:7878/query',
expect.objectContaining({
method: 'POST',
headers: expect.objectContaining({
'Content-Type': 'application/sparql-query',
Accept: 'application/sparql-results+json',
}),
body: 'SELECT * WHERE { ?s ?p ?o }',
})
);
});
it('should handle empty results', async () => {
global.fetch = vi.fn().mockResolvedValue({
ok: true,
json: async () => ({
head: { vars: [] },
results: { bindings: [] },
}),
});
const result = await client.executeSelect('SELECT * WHERE { ?s ?p ?o }');
expect(result.results.bindings).toHaveLength(0);
});
it('should throw SparqlError on HTTP error', async () => {
global.fetch = vi.fn().mockResolvedValue({
ok: false,
status: 400,
statusText: 'Bad Request',
text: async () => 'Parse error at line 1',
});
await expect(
client.executeSelect('INVALID QUERY')
).rejects.toThrow(SparqlError);
});
it('should throw SparqlError on network error', async () => {
global.fetch = vi.fn().mockRejectedValue(new Error('Network failure'));
await expect(
client.executeSelect('SELECT * WHERE { ?s ?p ?o }')
).rejects.toThrow(SparqlError);
});
it.skip('should timeout long queries', async () => {
// Note: Testing timeout with mocked fetch is complex due to timing issues
// This test would work in integration tests with real HTTP calls
const slowClient = new SparqlClient({
endpoint: 'http://localhost:7878/query',
timeout: 100, // 100ms timeout
});
global.fetch = vi.fn().mockImplementation(
() =>
new Promise((resolve) => {
setTimeout(() => resolve({
ok: true,
json: async () => ({ head: { vars: [] }, results: { bindings: [] } })
}), 200); // 200ms delay
})
);
await expect(
slowClient.executeSelect('SELECT * WHERE { ?s ?p ?o }')
).rejects.toThrow(/timeout/i);
});
});
describe('executeAsk', () => {
it('should execute ASK query and return true', async () => {
global.fetch = vi.fn().mockResolvedValue({
ok: true,
json: async () => ({ head: {}, boolean: true }),
});
const result = await client.executeAsk('ASK { ?s ?p ?o }');
expect(result).toBe(true);
});
it('should execute ASK query and return false', async () => {
global.fetch = vi.fn().mockResolvedValue({
ok: true,
json: async () => ({ head: {}, boolean: false }),
});
const result = await client.executeAsk('ASK { <http://no> <http://match> <http://here> }');
expect(result).toBe(false);
});
});
describe('executeConstruct', () => {
it('should execute CONSTRUCT query and return N-Triples', async () => {
const ntriples = '<http://s> <http://p> <http://o> .';
global.fetch = vi.fn().mockResolvedValue({
ok: true,
text: async () => ntriples,
});
const result = await client.executeConstruct(
'CONSTRUCT { ?s ?p ?o } WHERE { ?s ?p ?o }'
);
expect(result).toBe(ntriples);
expect(global.fetch).toHaveBeenCalledWith(
'http://localhost:7878/query',
expect.objectContaining({
headers: expect.objectContaining({
Accept: 'application/n-triples',
}),
})
);
});
});
describe('testConnection', () => {
it('should return true for successful connection', async () => {
global.fetch = vi.fn().mockResolvedValue({
ok: true,
json: async () => ({ head: {}, boolean: true }),
});
const result = await client.testConnection();
expect(result).toBe(true);
});
it('should return false for failed connection', async () => {
global.fetch = vi.fn().mockRejectedValue(new Error('Connection refused'));
const result = await client.testConnection();
expect(result).toBe(false);
});
});
describe('getTripleCount', () => {
it('should return count of triples', async () => {
global.fetch = vi.fn().mockResolvedValue({
ok: true,
json: async () => ({
head: { vars: ['count'] },
results: {
bindings: [
{
count: {
type: 'literal',
value: '12345',
datatype: 'http://www.w3.org/2001/XMLSchema#integer',
},
},
],
},
}),
});
const count = await client.getTripleCount();
expect(count).toBe(12345);
});
it('should return 0 for empty database', async () => {
global.fetch = vi.fn().mockResolvedValue({
ok: true,
json: async () => ({
head: { vars: ['count'] },
results: { bindings: [] },
}),
});
const count = await client.getTripleCount();
expect(count).toBe(0);
});
});
});
describe('SparqlError', () => {
it('should create error with message', () => {
const error = new SparqlError('Test error');
expect(error.message).toBe('Test error');
expect(error.name).toBe('SparqlError');
});
it('should include status code and response', () => {
const error = new SparqlError('Bad request', 400, 'Parse error');
expect(error.statusCode).toBe(400);
expect(error.response).toBe('Parse error');
});
});
describe('createSparqlClient', () => {
it('should create client from environment', () => {
import.meta.env.VITE_SPARQL_ENDPOINT = 'http://localhost:7878/query';
const client = createSparqlClient();
expect(client).toBeInstanceOf(SparqlClient);
});
it('should throw error if endpoint not configured', () => {
delete import.meta.env.VITE_SPARQL_ENDPOINT;
expect(() => createSparqlClient()).toThrow(/VITE_SPARQL_ENDPOINT/);
});
});
describe('getDefaultClient', () => {
it('should return singleton client', () => {
import.meta.env.VITE_SPARQL_ENDPOINT = 'http://localhost:7878/query';
const client1 = getDefaultClient();
const client2 = getDefaultClient();
expect(client1).toBe(client2); // Same instance
});
});