All files / languages/sparql/services sparql-endpoint-tester.ts

100% Statements 29/29
86.95% Branches 20/23
100% Functions 2/2
100% Lines 29/29

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111                            10x         10x   10x           10x   10x                   10x 2x     8x   8x 8x               8x   8x 3x     8x   8x 1x     8x           6x 3x 3x     3x             10x 1x         3x         3x   3x   2x         2x   2x        
import * as vscode from 'vscode';
import { ICredentialStorageService } from '@src/services/core';
import { AuthCredential } from '@src/services/core/credential';
import { SparqlConnection } from './sparql-connection';
import { WORKSPACE_CONNECTION } from './workspace-store';
import { getAuthorizationHeader } from './sparql-auth';
import { ISparqlEndpointTester } from './sparql-endpoint-tester.interface';
 
/**
 * Tests the reachability of SPARQL endpoint connections by sending an ASK query
 * via HTTP POST. Test progress is surfaced through events; results are returned
 * to the caller — surfacing errors to the user is the caller's concern.
 */
export class SparqlEndpointTester implements ISparqlEndpointTester {
	private _onDidConnectionTestStart = new vscode.EventEmitter<SparqlConnection>();
 
	/**
	 * Fired immediately before a connection test begins.
	 */
	public readonly onDidConnectionTestStart = this._onDidConnectionTestStart.event;
 
	private _onDidConnectionTestEnd = new vscode.EventEmitter<{ connection: SparqlConnection; error: { code: number; message: string } | null }>();
 
	/**
	 * Fired when a connection test completes. `error` is `null` on success, or
	 * contains the error code and message on failure.
	 */
	public readonly onDidConnectionTestEnd = this._onDidConnectionTestEnd.event;
 
	constructor(private readonly _credentialStorage: ICredentialStorageService) { }
 
	/**
	 * Tests whether a SPARQL endpoint can be reached by sending an ASK query via HTTP POST.
	 * The workspace store is always considered reachable without testing.
	 * @param connection The SPARQL endpoint connection to test.
	 * @param credential If provided, uses these credentials instead of loading stored ones.
	 * @returns `null` on success, or an error object `{ code, message }` on failure.
	 */
	async testConnection(connection: SparqlConnection, credential?: AuthCredential | null): Promise<null | { code: number; message: string }> {
		if (connection.id === WORKSPACE_CONNECTION.id) {
			return null;
		}
 
		this._onDidConnectionTestStart.fire(connection);
 
		try {
			const headers: Record<string, string> = {
				'Content-Type': 'application/sparql-query',
				'Accept': 'application/sparql-results+json,application/json'
			};
 
			// An explicit credential (or explicit null) comes from the connection
			// editor, where the user is entering credentials right now; only a
			// stored-credential lookup can come back empty unexpectedly.
			const usingStoredCredential = credential === undefined;
 
			if (credential === undefined) {
				credential = await this._credentialStorage.getCredential(connection.id);
			}
 
			const authHeader = await getAuthorizationHeader(credential ?? undefined);
 
			if (authHeader) {
				headers.Authorization = authHeader;
			}
 
			const response = await fetch(connection.endpointUrl, {
				method: 'POST',
				headers,
				body: 'ASK WHERE { ?s ?p ?o }'
			});
 
			if (response.ok) {
				this._onDidConnectionTestEnd.fire({ connection, error: null });
				return null;
			}
 
			let message = await response.text() || response.statusText;
 
			// Credentials live in the machine-local secret storage and are not
			// synced with the settings a connection is stored in. A connection
			// arriving from another machine (or another user, via version control)
			// therefore fails authentication with no stored credential — explain
			// that instead of leaving a bare HTTP error.
			if ((response.status === 401 || response.status === 407) && usingStoredCredential && !credential) {
				message = 'The endpoint requires authentication, but no credentials are stored for this connection on this machine. '
					+ 'Credentials are kept in the local secret storage and do not sync with settings — '
					+ `open the connection editor to enter them. (${message})`;
			}
 
			const error = {
				code: response.status,
				message
			};
 
			this._onDidConnectionTestEnd.fire({ connection, error });
 
			return error;
		} catch (e: any) {
			const error = {
				code: e.status || e.code || 0,
				message: e.message || String(e)
			};
 
			this._onDidConnectionTestEnd.fire({ connection, error });
 
			return error;
		}
	}
}