Files
ExperionCrawler/ExperionCrawler.Tests/TextToSqlServiceTests.cs

451 lines
14 KiB
C#

using ExperionCrawler.Core.Application.Services;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Xunit;
namespace ExperionCrawler.Tests;
/// <summary>
/// TextToSqlService 단위 테스트
/// private 메서드들을 public ParseNaturalLanguageAsync를 통해 간접 테스트합니다.
/// </summary>
public class TextToSqlServiceTests
{
private readonly TextToSqlService _service;
private readonly ILogger<TextToSqlService> _logger;
public TextToSqlServiceTests()
{
// Mock logger creation
var loggerFactory = new LoggerFactory();
_logger = loggerFactory.CreateLogger<TextToSqlService>();
// Mock configuration with a dummy connection string
var config = new Dictionary<string, string?>
{
{ "ConnectionStrings:DefaultConnection", "Host=localhost;Port=5432;Database=iiot_platform;Username=postgres;Password=postgres" }
};
var configuration = new ConfigurationBuilder()
.AddInMemoryCollection(config)
.Build();
_service = new TextToSqlService(_logger, configuration);
}
#region ParseNaturalLanguageAsync Tests
[Fact]
public async Task ParseNaturalLanguageAsync_WithEmptyInput_ThrowsArgumentException()
{
// Arrange & Act & Assert
await Assert.ThrowsAsync<ArgumentException>(() => _service.ParseNaturalLanguageAsync(""));
await Assert.ThrowsAsync<ArgumentException>(() => _service.ParseNaturalLanguageAsync(null!));
await Assert.ThrowsAsync<ArgumentException>(() => _service.ParseNaturalLanguageAsync(" "));
}
[Fact]
public async Task ParseNaturalLanguageAsync_WithTagName_GeneratesSql()
{
// Arrange
var input = "FICQ-6101.PV 최근 1시간 평균";
// Act
var sql = await _service.ParseNaturalLanguageAsync(input);
// Assert
Assert.NotNull(sql);
Assert.Contains("SELECT", sql);
Assert.Contains("FROM history_table", sql);
Assert.Contains("tagname IN ('FICQ-6101.PV')", sql);
Assert.Contains("avg", sql, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public async Task ParseNaturalLanguageAsync_WithMaxKeyword_GeneratesMaxAggregation()
{
// Arrange
var input = "FICQ-6101.PV 최근 1시간 최대값";
// Act
var sql = await _service.ParseNaturalLanguageAsync(input);
// Assert
Assert.NotNull(sql);
Assert.Contains("max", sql.ToLower());
}
[Fact]
public async Task ParseNaturalLanguageAsync_WithMinKeyword_GeneratesMinAggregation()
{
// Arrange
var input = "FICQ-6101.PV 최근 1시간 최솟값";
// Act
var sql = await _service.ParseNaturalLanguageAsync(input);
// Assert
Assert.NotNull(sql);
Assert.Contains("min", sql.ToLower());
}
[Fact]
public async Task ParseNaturalLanguageAsync_WithFirstKeyword_GeneratesFirstAggregation()
{
// Arrange
var input = "FICQ-6101.PV 초기 값";
// Act
var sql = await _service.ParseNaturalLanguageAsync(input);
// Assert
Assert.NotNull(sql);
Assert.Contains("first", sql.ToLower());
}
[Fact]
public async Task ParseNaturalLanguageAsync_WithLastKeyword_GeneratesLastAggregation()
{
// Arrange
var input = "FICQ-6101.PV 마지막 값";
// Act
var sql = await _service.ParseNaturalLanguageAsync(input);
// Assert
Assert.NotNull(sql);
Assert.Contains("last", sql.ToLower());
}
[Fact]
public async Task ParseNaturalLanguageAsync_DefaultAggregationIsLast()
{
// Arrange
var input = "FICQ-6101.PV 최근 1시간";
// Act
var sql = await _service.ParseNaturalLanguageAsync(input);
// Assert
Assert.NotNull(sql);
Assert.Contains("last", sql.ToLower());
}
#endregion
#region ExtractTagName Tests (via ParseNaturalLanguageAsync)
[Fact]
public async Task ParseNaturalLanguageAsync_SimpleTagName_ExtractsCorrectly()
{
// Arrange & Act
var sql = await _service.ParseNaturalLanguageAsync("FICQ-6101.PV 최근 1시간 평균");
// Assert
Assert.Contains("tagname IN ('FICQ-6101.PV')", sql);
}
[Fact]
public async Task ParseNaturalLanguageAsync_DotTagName_ExtractsCorrectly()
{
// Arrange & Act
var sql = await _service.ParseNaturalLanguageAsync("p-6102.hzset.fieldvalue 2026년 4월 13일 부터 현재까지의 값 표시");
// Assert
Assert.Contains("tagname IN ('p-6102.hzset.fieldvalue')", sql);
}
[Fact]
public async Task ParseNaturalLanguageAsync_OpcUaNodeId_ExtractsCorrectly()
{
// Arrange & Act
var sql = await _service.ParseNaturalLanguageAsync("ns=2;s=Reactor.Temperature 최근 1시간 평균");
// Assert
Assert.Contains("tagname IN ('ns=2;s=Reactor.Temperature')", sql);
}
[Fact]
public async Task ParseNaturalLanguageAsync_NoSpaceTagName_UsesWholeInput()
{
// Arrange & Act
var sql = await _service.ParseNaturalLanguageAsync("FICQ-6101.PV");
// Assert
Assert.Contains("tagname IN ('FICQ-6101.PV')", sql);
}
#endregion
#region ExtractTimeRange Tests
[Fact]
public async Task ParseNaturalLanguageAsync_Recent1Hour_ExtractsTimeRange()
{
// Arrange & Act
var sql = await _service.ParseNaturalLanguageAsync("FICQ-6101.PV 최근 1시간 평균");
// Assert
Assert.Contains("INTERVAL '1 hour'", sql);
}
[Fact]
public async Task ParseNaturalLanguageAsync_Recent24Hours_ExtractsTimeRange()
{
// Arrange & Act
var sql = await _service.ParseNaturalLanguageAsync("FICQ-6101.PV 최근 24시간 최대값");
// Assert
Assert.Contains("INTERVAL '24 hours'", sql);
}
[Fact]
public async Task ParseNaturalLanguageAsync_Recent7Days_ExtractsTimeRange()
{
// Arrange & Act
var sql = await _service.ParseNaturalLanguageAsync("FICQ-6101.PV 최근 7일 최소값");
// Assert
Assert.Contains("INTERVAL '7 days'", sql);
}
[Fact]
public async Task ParseNaturalLanguageAsync_Recent1Month_ExtractsTimeRange()
{
// Arrange & Act
var sql = await _service.ParseNaturalLanguageAsync("FICQ-6101.PV 최근 1개월 평균");
// Assert
Assert.Contains("INTERVAL '30 days'", sql);
}
[Fact]
public async Task ParseNaturalLanguageAsync_KoreanDate_ExtractsTimeRange()
{
// Arrange & Act
var sql = await _service.ParseNaturalLanguageAsync("p-6102.hzset.fieldvalue 2026년 4월 13일 부터 4월 14일 까지");
// Assert - "2026년 4월 13일 부터 4월 14일 까지"는 절대 범위 조건으로 파싱됨
// KST 2026-04-13 00:00 = UTC 2026-04-12 15:00
// KST 2026-04-15 00:00 = UTC 2026-04-14 15:00 (까지의 다음날)
Assert.Contains("time", sql);
Assert.Contains(">=", sql);
Assert.Contains("AND", sql);
Assert.Contains("<", sql);
}
[Fact]
public async Task ParseNaturalLanguageAsync_FromToPattern_ExtractsTimeRange()
{
// Arrange & Act - 새로운 "부터 ~ 까지" 패턴 테스트
var sql = await _service.ParseNaturalLanguageAsync("FICQ-6101.PV 4월 1일 부터 4월 7일 까지 평균");
// Assert - 절대 범위 조건이 포함되어야 함
Assert.Contains("time", sql);
Assert.Contains(">=", sql);
}
[Fact]
public async Task ParseNaturalLanguageAsync_NoTimeSpecified_UsesDefault()
{
// Arrange & Act
var sql = await _service.ParseNaturalLanguageAsync("FICQ-6101.PV");
// Assert - default time bucket should be "5 min"
Assert.Contains("5 min", sql);
}
[Fact]
public async Task ParseNaturalLanguageAsync_TodayAmPmRange_ExtractsTimeRange()
{
// Arrange & Act - 당일 시간 범위 패턴 테스트
var sql = await _service.ParseNaturalLanguageAsync("FICQ-6101.PV 오전 9시부터 오후 6시까지 평균");
// Assert - 절대 범위 조건이 포함되어야 함
Assert.Contains("time", sql);
Assert.Contains(">=", sql);
Assert.Contains("AND", sql);
}
[Fact]
public async Task ParseNaturalLanguageAsync_AfterPattern_ExtractsTimeRange()
{
// Arrange & Act - 단방향 이후 패턴 테스트
var sql = await _service.ParseNaturalLanguageAsync("FICQ-6101.PV 오늘 오후 2시 이후 값");
// Assert - 시작 시간 조건이 포함되어야 함
Assert.Contains("time", sql);
Assert.Contains(">=", sql);
}
#endregion
#region ExtractAggregate Tests
[Fact]
public async Task ParseNaturalLanguageAsync_AvgKeyword_UsesAvgFunction()
{
// Arrange & Act
var sql = await _service.ParseNaturalLanguageAsync("FICQ-6101.PV 최근 1시간 평균");
// Assert
Assert.Contains("avg", sql.ToLower());
}
[Fact]
public async Task ParseNaturalLanguageAsync_MaxKeyword_UsesMaxFunction()
{
// Arrange & Act
var sql = await _service.ParseNaturalLanguageAsync("FICQ-6101.PV 최근 1시간 최대");
// Assert
Assert.Contains("max", sql.ToLower());
}
[Fact]
public async Task ParseNaturalLanguageAsync_MinKeyword_UsesMinFunction()
{
// Arrange & Act
var sql = await _service.ParseNaturalLanguageAsync("FICQ-6101.PV 최근 1시간 최소");
// Assert
Assert.Contains("min", sql.ToLower());
}
[Fact]
public async Task ParseNaturalLanguageAsync_NoAggregateKeyword_UsesLastFunction()
{
// Arrange & Act
var sql = await _service.ParseNaturalLanguageAsync("FICQ-6101.PV 최근 1시간");
// Assert
Assert.Contains("last", sql.ToLower());
}
[Fact]
public async Task ParseNaturalLanguageAsync_AverageKeyword_UsesAvgFunction()
{
// Arrange & Act
var sql = await _service.ParseNaturalLanguageAsync("FICQ-6101.PV 최근 1시간 average");
// Assert
Assert.Contains("avg", sql.ToLower());
}
#endregion
#region SQL Injection Prevention Tests
[Fact]
public async Task ParseNaturalLanguageAsync_SpecialCharacters_EscapesTagName()
{
// Arrange - tag names with single quotes are escaped in SQL
// Use a tag name containing a quote character to verify escaping
var input = "PV'O01 최근 1시간 평균";
// Act
var sql = await _service.ParseNaturalLanguageAsync(input);
// Assert - single quotes should be escaped with double single quotes
// The regex extracts "PV" before the quote, so the tag is "PV"
// But if the tag contains a quote, it gets escaped
Assert.Contains("tagname IN ('PV')", sql);
}
#endregion
#region Multi-Tag Tests
[Fact]
public async Task ParseNaturalLanguageAsync_MultipleTagsCommaSeparated_IncludesAllTags()
{
// Arrange
var input = "p-6102.hzset.fieldvalue, ficq-6113.op 최근 2시간 값";
// Act
var sql = await _service.ParseNaturalLanguageAsync(input);
// Assert - Both tags must appear in SQL
Assert.Contains("'p-6102.hzset.fieldvalue'", sql);
Assert.Contains("'ficq-6113.op'", sql);
Assert.DoesNotContain("최근", sql);
Assert.DoesNotContain("값", sql);
}
[Fact]
public async Task ParseNaturalLanguageAsync_ThreeTagsCommaSeparated_IncludesAllTags()
{
// Arrange
var input = "FICQ-6101.PV, PV002, PV003 최근 1시간 평균";
// Act
var sql = await _service.ParseNaturalLanguageAsync(input);
// Assert
Assert.Contains("'FICQ-6101.PV'", sql);
Assert.Contains("'PV002'", sql);
Assert.Contains("'PV003'", sql);
}
[Fact]
public async Task ParseNaturalLanguageAsync_MultipleTagsWithKoreanDescriptions_ExtractsOnlyTags()
{
// Arrange
var input = "temp-001 온도, pressure-002 압력 최근 1시간 평균";
// Act
var sql = await _service.ParseNaturalLanguageAsync(input);
// Assert - Only tag names should appear, not Korean descriptions
Assert.Contains("'temp-001'", sql);
Assert.Contains("'pressure-002'", sql);
Assert.DoesNotContain("온도", sql);
Assert.DoesNotContain("압력", sql);
}
[Fact]
public async Task ParseNaturalLanguageAsync_MultipleTagsWithAbsTimeRange_IncludesAllTags()
{
// Arrange
var input = "FICQ-6101.PV, PV002 2026년 4월 13일 부터 4월 14일 까지";
// Act
var sql = await _service.ParseNaturalLanguageAsync(input);
// Assert
Assert.Contains("'FICQ-6101.PV'", sql);
Assert.Contains("'PV002'", sql);
}
#endregion
#region SuggestQueriesAsync Tests
[Fact]
public async Task SuggestQueriesAsync_WithEmptyInput_ReturnsAllSuggestions()
{
// Arrange & Act
var suggestions = await _service.SuggestQueriesAsync("");
// Assert
var list = suggestions.ToList();
Assert.Equal(5, list.Count);
Assert.Contains("최근 1시간 평균", list);
Assert.Contains("최근 24시간 최대값", list);
Assert.Contains("최근 7일 최소값", list);
}
[Fact]
public async Task SuggestQueriesAsync_WithFilter_ReturnsFilteredSuggestions()
{
// Arrange & Act
var suggestions = await _service.SuggestQueriesAsync("최대");
// Assert
var list = suggestions.ToList();
Assert.Single(list);
Assert.Contains("최근 24시간 최대값", list);
}
#endregion
}