Fix build and code structure improvements. New but essential UI functionality. CI improvements. Documentation improvements. AI module improvements.
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
using StellaOps.Scanner.Sources.Contracts;
|
||||
using StellaOps.Scanner.Sources.Domain;
|
||||
|
||||
namespace StellaOps.Scanner.Sources.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Service for managing SBOM sources.
|
||||
/// </summary>
|
||||
public interface ISbomSourceService
|
||||
{
|
||||
/// <summary>
|
||||
/// Get a source by ID.
|
||||
/// </summary>
|
||||
Task<SourceResponse?> GetAsync(string tenantId, Guid sourceId, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Get a source by name.
|
||||
/// </summary>
|
||||
Task<SourceResponse?> GetByNameAsync(string tenantId, string name, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// List sources with optional filters.
|
||||
/// </summary>
|
||||
Task<PagedResponse<SourceResponse>> ListAsync(
|
||||
string tenantId,
|
||||
ListSourcesRequest request,
|
||||
CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Create a new source.
|
||||
/// </summary>
|
||||
Task<SourceResponse> CreateAsync(
|
||||
string tenantId,
|
||||
CreateSourceRequest request,
|
||||
string createdBy,
|
||||
CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Update an existing source.
|
||||
/// </summary>
|
||||
Task<SourceResponse> UpdateAsync(
|
||||
string tenantId,
|
||||
Guid sourceId,
|
||||
UpdateSourceRequest request,
|
||||
string updatedBy,
|
||||
CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Delete a source.
|
||||
/// </summary>
|
||||
Task DeleteAsync(string tenantId, Guid sourceId, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Test source connection.
|
||||
/// </summary>
|
||||
Task<ConnectionTestResult> TestConnectionAsync(
|
||||
string tenantId,
|
||||
Guid sourceId,
|
||||
CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Test connection for a new source (before creation).
|
||||
/// </summary>
|
||||
Task<ConnectionTestResult> TestNewConnectionAsync(
|
||||
string tenantId,
|
||||
TestConnectionRequest request,
|
||||
CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Pause a source.
|
||||
/// </summary>
|
||||
Task<SourceResponse> PauseAsync(
|
||||
string tenantId,
|
||||
Guid sourceId,
|
||||
PauseSourceRequest request,
|
||||
string pausedBy,
|
||||
CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Resume a paused source.
|
||||
/// </summary>
|
||||
Task<SourceResponse> ResumeAsync(
|
||||
string tenantId,
|
||||
Guid sourceId,
|
||||
string resumedBy,
|
||||
CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Trigger a manual scan for a source.
|
||||
/// </summary>
|
||||
Task<TriggerScanResult> TriggerScanAsync(
|
||||
string tenantId,
|
||||
Guid sourceId,
|
||||
TriggerScanRequest? request,
|
||||
string triggeredBy,
|
||||
CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Get run history for a source.
|
||||
/// </summary>
|
||||
Task<PagedResponse<SourceRunResponse>> GetRunsAsync(
|
||||
string tenantId,
|
||||
Guid sourceId,
|
||||
ListSourceRunsRequest request,
|
||||
CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Get run details.
|
||||
/// </summary>
|
||||
Task<SourceRunResponse?> GetRunAsync(
|
||||
string tenantId,
|
||||
Guid sourceId,
|
||||
Guid runId,
|
||||
CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Activate a source (after validation).
|
||||
/// </summary>
|
||||
Task<SourceResponse> ActivateAsync(
|
||||
string tenantId,
|
||||
Guid sourceId,
|
||||
string activatedBy,
|
||||
CancellationToken ct = default);
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
using System.Text.Json;
|
||||
using StellaOps.Scanner.Sources.Contracts;
|
||||
using StellaOps.Scanner.Sources.Domain;
|
||||
|
||||
namespace StellaOps.Scanner.Sources.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Interface for testing source connections.
|
||||
/// </summary>
|
||||
public interface ISourceConnectionTester
|
||||
{
|
||||
/// <summary>
|
||||
/// Tests connection to the source using stored credentials.
|
||||
/// </summary>
|
||||
Task<ConnectionTestResult> TestAsync(SbomSource source, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Tests connection using provided test credentials (for setup validation).
|
||||
/// </summary>
|
||||
Task<ConnectionTestResult> TestAsync(
|
||||
SbomSource source,
|
||||
JsonDocument? testCredentials,
|
||||
CancellationToken ct = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Interface for type-specific connection testing.
|
||||
/// </summary>
|
||||
public interface ISourceTypeConnectionTester
|
||||
{
|
||||
/// <summary>
|
||||
/// The source type this tester handles.
|
||||
/// </summary>
|
||||
SbomSourceType SourceType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Tests connection to the source.
|
||||
/// </summary>
|
||||
Task<ConnectionTestResult> TestAsync(
|
||||
SbomSource source,
|
||||
JsonDocument? overrideCredentials,
|
||||
CancellationToken ct = default);
|
||||
}
|
||||
@@ -0,0 +1,422 @@
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Scanner.Sources.Configuration;
|
||||
using StellaOps.Scanner.Sources.Contracts;
|
||||
using StellaOps.Scanner.Sources.Domain;
|
||||
using StellaOps.Scanner.Sources.Persistence;
|
||||
|
||||
namespace StellaOps.Scanner.Sources.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Service for managing SBOM sources.
|
||||
/// </summary>
|
||||
public sealed class SbomSourceService : ISbomSourceService
|
||||
{
|
||||
private readonly ISbomSourceRepository _sourceRepository;
|
||||
private readonly ISbomSourceRunRepository _runRepository;
|
||||
private readonly ISourceConfigValidator _configValidator;
|
||||
private readonly ISourceConnectionTester _connectionTester;
|
||||
private readonly ILogger<SbomSourceService> _logger;
|
||||
|
||||
public SbomSourceService(
|
||||
ISbomSourceRepository sourceRepository,
|
||||
ISbomSourceRunRepository runRepository,
|
||||
ISourceConfigValidator configValidator,
|
||||
ISourceConnectionTester connectionTester,
|
||||
ILogger<SbomSourceService> logger)
|
||||
{
|
||||
_sourceRepository = sourceRepository;
|
||||
_runRepository = runRepository;
|
||||
_configValidator = configValidator;
|
||||
_connectionTester = connectionTester;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<SourceResponse?> GetAsync(string tenantId, Guid sourceId, CancellationToken ct = default)
|
||||
{
|
||||
var source = await _sourceRepository.GetByIdAsync(tenantId, sourceId, ct);
|
||||
return source == null ? null : SourceResponse.FromDomain(source);
|
||||
}
|
||||
|
||||
public async Task<SourceResponse?> GetByNameAsync(string tenantId, string name, CancellationToken ct = default)
|
||||
{
|
||||
var source = await _sourceRepository.GetByNameAsync(tenantId, name, ct);
|
||||
return source == null ? null : SourceResponse.FromDomain(source);
|
||||
}
|
||||
|
||||
public async Task<PagedResponse<SourceResponse>> ListAsync(
|
||||
string tenantId,
|
||||
ListSourcesRequest request,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var result = await _sourceRepository.ListAsync(tenantId, request, ct);
|
||||
|
||||
return new PagedResponse<SourceResponse>
|
||||
{
|
||||
Items = result.Items.Select(SourceResponse.FromDomain).ToList(),
|
||||
TotalCount = result.TotalCount,
|
||||
NextCursor = result.NextCursor
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<SourceResponse> CreateAsync(
|
||||
string tenantId,
|
||||
CreateSourceRequest request,
|
||||
string createdBy,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
// Validate name uniqueness
|
||||
if (await _sourceRepository.NameExistsAsync(tenantId, request.Name, ct: ct))
|
||||
{
|
||||
throw new InvalidOperationException($"Source with name '{request.Name}' already exists");
|
||||
}
|
||||
|
||||
// Validate configuration
|
||||
var validationResult = _configValidator.Validate(request.SourceType, request.Configuration);
|
||||
if (!validationResult.IsValid)
|
||||
{
|
||||
throw new ArgumentException($"Invalid configuration: {string.Join(", ", validationResult.Errors)}");
|
||||
}
|
||||
|
||||
// Validate cron schedule if provided
|
||||
if (!string.IsNullOrEmpty(request.CronSchedule))
|
||||
{
|
||||
try
|
||||
{
|
||||
Cronos.CronExpression.Parse(request.CronSchedule);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new ArgumentException($"Invalid cron schedule: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
// Create domain entity
|
||||
var source = SbomSource.Create(
|
||||
tenantId,
|
||||
request.Name,
|
||||
request.SourceType,
|
||||
request.Configuration,
|
||||
createdBy,
|
||||
request.Description,
|
||||
request.AuthRef,
|
||||
request.CronSchedule,
|
||||
request.CronTimezone);
|
||||
|
||||
if (request.MaxScansPerHour.HasValue)
|
||||
{
|
||||
source.MaxScansPerHour = request.MaxScansPerHour;
|
||||
}
|
||||
|
||||
if (request.Tags != null)
|
||||
{
|
||||
source.Tags = request.Tags;
|
||||
}
|
||||
|
||||
if (request.Metadata != null)
|
||||
{
|
||||
source.Metadata = request.Metadata;
|
||||
}
|
||||
|
||||
await _sourceRepository.CreateAsync(source, ct);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Created source {SourceId} ({Name}) of type {SourceType} for tenant {TenantId}",
|
||||
source.SourceId, source.Name, source.SourceType, tenantId);
|
||||
|
||||
return SourceResponse.FromDomain(source);
|
||||
}
|
||||
|
||||
public async Task<SourceResponse> UpdateAsync(
|
||||
string tenantId,
|
||||
Guid sourceId,
|
||||
UpdateSourceRequest request,
|
||||
string updatedBy,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var source = await _sourceRepository.GetByIdAsync(tenantId, sourceId, ct)
|
||||
?? throw new KeyNotFoundException($"Source {sourceId} not found");
|
||||
|
||||
// Validate name uniqueness if changed
|
||||
if (request.Name != null && request.Name != source.Name)
|
||||
{
|
||||
if (await _sourceRepository.NameExistsAsync(tenantId, request.Name, sourceId, ct))
|
||||
{
|
||||
throw new InvalidOperationException($"Source with name '{request.Name}' already exists");
|
||||
}
|
||||
}
|
||||
|
||||
// Validate configuration if provided
|
||||
if (request.Configuration != null)
|
||||
{
|
||||
var validationResult = _configValidator.Validate(source.SourceType, request.Configuration);
|
||||
if (!validationResult.IsValid)
|
||||
{
|
||||
throw new ArgumentException($"Invalid configuration: {string.Join(", ", validationResult.Errors)}");
|
||||
}
|
||||
|
||||
source.UpdateConfiguration(request.Configuration, updatedBy);
|
||||
}
|
||||
|
||||
// Validate cron schedule if provided
|
||||
if (request.CronSchedule != null)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(request.CronSchedule))
|
||||
{
|
||||
try
|
||||
{
|
||||
Cronos.CronExpression.Parse(request.CronSchedule);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new ArgumentException($"Invalid cron schedule: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
source.CronSchedule = request.CronSchedule;
|
||||
source.CalculateNextScheduledRun();
|
||||
}
|
||||
|
||||
// Update simple fields via reflection (maintaining encapsulation)
|
||||
if (request.Name != null)
|
||||
{
|
||||
SetProperty(source, "Name", request.Name);
|
||||
}
|
||||
|
||||
if (request.Description != null)
|
||||
{
|
||||
source.Description = request.Description;
|
||||
}
|
||||
|
||||
if (request.AuthRef != null)
|
||||
{
|
||||
source.AuthRef = request.AuthRef;
|
||||
}
|
||||
|
||||
if (request.CronTimezone != null)
|
||||
{
|
||||
source.CronTimezone = request.CronTimezone;
|
||||
source.CalculateNextScheduledRun();
|
||||
}
|
||||
|
||||
if (request.MaxScansPerHour.HasValue)
|
||||
{
|
||||
source.MaxScansPerHour = request.MaxScansPerHour;
|
||||
}
|
||||
|
||||
if (request.Tags != null)
|
||||
{
|
||||
source.Tags = request.Tags;
|
||||
}
|
||||
|
||||
if (request.Metadata != null)
|
||||
{
|
||||
source.Metadata = request.Metadata;
|
||||
}
|
||||
|
||||
// Touch updated fields
|
||||
SetProperty(source, "UpdatedAt", DateTimeOffset.UtcNow);
|
||||
SetProperty(source, "UpdatedBy", updatedBy);
|
||||
|
||||
await _sourceRepository.UpdateAsync(source, ct);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Updated source {SourceId} ({Name}) for tenant {TenantId}",
|
||||
source.SourceId, source.Name, tenantId);
|
||||
|
||||
return SourceResponse.FromDomain(source);
|
||||
}
|
||||
|
||||
public async Task DeleteAsync(string tenantId, Guid sourceId, CancellationToken ct = default)
|
||||
{
|
||||
var source = await _sourceRepository.GetByIdAsync(tenantId, sourceId, ct)
|
||||
?? throw new KeyNotFoundException($"Source {sourceId} not found");
|
||||
|
||||
await _sourceRepository.DeleteAsync(tenantId, sourceId, ct);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Deleted source {SourceId} ({Name}) for tenant {TenantId}",
|
||||
sourceId, source.Name, tenantId);
|
||||
}
|
||||
|
||||
public async Task<ConnectionTestResult> TestConnectionAsync(
|
||||
string tenantId,
|
||||
Guid sourceId,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var source = await _sourceRepository.GetByIdAsync(tenantId, sourceId, ct)
|
||||
?? throw new KeyNotFoundException($"Source {sourceId} not found");
|
||||
|
||||
return await _connectionTester.TestAsync(source, ct);
|
||||
}
|
||||
|
||||
public async Task<ConnectionTestResult> TestNewConnectionAsync(
|
||||
string tenantId,
|
||||
TestConnectionRequest request,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
// Create a temporary source for testing
|
||||
var tempSource = SbomSource.Create(
|
||||
tenantId,
|
||||
"__test__",
|
||||
request.SourceType,
|
||||
request.Configuration,
|
||||
"__test__",
|
||||
authRef: request.AuthRef);
|
||||
|
||||
return await _connectionTester.TestAsync(tempSource, request.TestCredentials, ct);
|
||||
}
|
||||
|
||||
public async Task<SourceResponse> PauseAsync(
|
||||
string tenantId,
|
||||
Guid sourceId,
|
||||
PauseSourceRequest request,
|
||||
string pausedBy,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var source = await _sourceRepository.GetByIdAsync(tenantId, sourceId, ct)
|
||||
?? throw new KeyNotFoundException($"Source {sourceId} not found");
|
||||
|
||||
source.Pause(request.Reason, request.Ticket, pausedBy);
|
||||
await _sourceRepository.UpdateAsync(source, ct);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Paused source {SourceId} ({Name}) by {PausedBy}: {Reason}",
|
||||
sourceId, source.Name, pausedBy, request.Reason);
|
||||
|
||||
return SourceResponse.FromDomain(source);
|
||||
}
|
||||
|
||||
public async Task<SourceResponse> ResumeAsync(
|
||||
string tenantId,
|
||||
Guid sourceId,
|
||||
string resumedBy,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var source = await _sourceRepository.GetByIdAsync(tenantId, sourceId, ct)
|
||||
?? throw new KeyNotFoundException($"Source {sourceId} not found");
|
||||
|
||||
source.Resume(resumedBy);
|
||||
await _sourceRepository.UpdateAsync(source, ct);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Resumed source {SourceId} ({Name}) by {ResumedBy}",
|
||||
sourceId, source.Name, resumedBy);
|
||||
|
||||
return SourceResponse.FromDomain(source);
|
||||
}
|
||||
|
||||
public async Task<TriggerScanResult> TriggerScanAsync(
|
||||
string tenantId,
|
||||
Guid sourceId,
|
||||
TriggerScanRequest? request,
|
||||
string triggeredBy,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var source = await _sourceRepository.GetByIdAsync(tenantId, sourceId, ct)
|
||||
?? throw new KeyNotFoundException($"Source {sourceId} not found");
|
||||
|
||||
// Check if source can be triggered
|
||||
if (source.Status == SbomSourceStatus.Disabled)
|
||||
{
|
||||
throw new InvalidOperationException("Cannot trigger a disabled source");
|
||||
}
|
||||
|
||||
if (source.Paused && request?.Force != true)
|
||||
{
|
||||
throw new InvalidOperationException($"Source is paused: {source.PauseReason}");
|
||||
}
|
||||
|
||||
if (source.IsRateLimited() && request?.Force != true)
|
||||
{
|
||||
throw new InvalidOperationException("Source is rate limited. Use force=true to override.");
|
||||
}
|
||||
|
||||
// Create a run record
|
||||
var run = SbomSourceRun.Create(
|
||||
sourceId,
|
||||
tenantId,
|
||||
SbomSourceRunTrigger.Manual,
|
||||
Guid.NewGuid().ToString("N"),
|
||||
$"Triggered by {triggeredBy}");
|
||||
|
||||
await _runRepository.CreateAsync(run, ct);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Triggered manual scan for source {SourceId} ({Name}), run {RunId}",
|
||||
sourceId, source.Name, run.RunId);
|
||||
|
||||
// TODO: Actually dispatch the scan to the trigger service
|
||||
// For now, just return the run info
|
||||
return new TriggerScanResult
|
||||
{
|
||||
RunId = run.RunId,
|
||||
Status = run.Status,
|
||||
Message = "Scan triggered successfully"
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<PagedResponse<SourceRunResponse>> GetRunsAsync(
|
||||
string tenantId,
|
||||
Guid sourceId,
|
||||
ListSourceRunsRequest request,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
// Verify source exists and belongs to tenant
|
||||
var source = await _sourceRepository.GetByIdAsync(tenantId, sourceId, ct)
|
||||
?? throw new KeyNotFoundException($"Source {sourceId} not found");
|
||||
|
||||
var result = await _runRepository.ListForSourceAsync(sourceId, request, ct);
|
||||
|
||||
return new PagedResponse<SourceRunResponse>
|
||||
{
|
||||
Items = result.Items.Select(SourceRunResponse.FromDomain).ToList(),
|
||||
TotalCount = result.TotalCount,
|
||||
NextCursor = result.NextCursor
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<SourceRunResponse?> GetRunAsync(
|
||||
string tenantId,
|
||||
Guid sourceId,
|
||||
Guid runId,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
// Verify source exists and belongs to tenant
|
||||
_ = await _sourceRepository.GetByIdAsync(tenantId, sourceId, ct)
|
||||
?? throw new KeyNotFoundException($"Source {sourceId} not found");
|
||||
|
||||
var run = await _runRepository.GetByIdAsync(runId, ct);
|
||||
if (run == null || run.SourceId != sourceId)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return SourceRunResponse.FromDomain(run);
|
||||
}
|
||||
|
||||
public async Task<SourceResponse> ActivateAsync(
|
||||
string tenantId,
|
||||
Guid sourceId,
|
||||
string activatedBy,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var source = await _sourceRepository.GetByIdAsync(tenantId, sourceId, ct)
|
||||
?? throw new KeyNotFoundException($"Source {sourceId} not found");
|
||||
|
||||
source.Activate(activatedBy);
|
||||
await _sourceRepository.UpdateAsync(source, ct);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Activated source {SourceId} ({Name}) by {ActivatedBy}",
|
||||
sourceId, source.Name, activatedBy);
|
||||
|
||||
return SourceResponse.FromDomain(source);
|
||||
}
|
||||
|
||||
private static void SetProperty(object obj, string propertyName, object value)
|
||||
{
|
||||
var property = obj.GetType().GetProperty(propertyName);
|
||||
property?.SetValue(obj, value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Scanner.Sources.Contracts;
|
||||
using StellaOps.Scanner.Sources.Domain;
|
||||
|
||||
namespace StellaOps.Scanner.Sources.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Dispatches connection tests to type-specific testers.
|
||||
/// </summary>
|
||||
public sealed class SourceConnectionTester : ISourceConnectionTester
|
||||
{
|
||||
private readonly IEnumerable<ISourceTypeConnectionTester> _testers;
|
||||
private readonly ILogger<SourceConnectionTester> _logger;
|
||||
|
||||
public SourceConnectionTester(
|
||||
IEnumerable<ISourceTypeConnectionTester> testers,
|
||||
ILogger<SourceConnectionTester> logger)
|
||||
{
|
||||
_testers = testers;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public Task<ConnectionTestResult> TestAsync(SbomSource source, CancellationToken ct = default)
|
||||
{
|
||||
return TestAsync(source, null, ct);
|
||||
}
|
||||
|
||||
public async Task<ConnectionTestResult> TestAsync(
|
||||
SbomSource source,
|
||||
JsonDocument? testCredentials,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var tester = _testers.FirstOrDefault(t => t.SourceType == source.SourceType);
|
||||
if (tester == null)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"No connection tester registered for source type {SourceType}",
|
||||
source.SourceType);
|
||||
|
||||
return new ConnectionTestResult
|
||||
{
|
||||
Success = false,
|
||||
Message = $"No connection tester available for source type {source.SourceType}",
|
||||
TestedAt = DateTimeOffset.UtcNow
|
||||
};
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_logger.LogDebug(
|
||||
"Testing connection for source {SourceId} ({SourceType})",
|
||||
source.SourceId, source.SourceType);
|
||||
|
||||
var result = await tester.TestAsync(source, testCredentials, ct);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Connection test for source {SourceId} {Result}: {Message}",
|
||||
source.SourceId,
|
||||
result.Success ? "succeeded" : "failed",
|
||||
result.Message);
|
||||
|
||||
return result;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Connection test failed for source {SourceId}", source.SourceId);
|
||||
|
||||
return new ConnectionTestResult
|
||||
{
|
||||
Success = false,
|
||||
Message = $"Connection test error: {ex.Message}",
|
||||
TestedAt = DateTimeOffset.UtcNow,
|
||||
Details = new Dictionary<string, object>
|
||||
{
|
||||
["exceptionType"] = ex.GetType().Name
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user