MCP-서버 리팩토링 후 P&ID 추출 테스트전 다른 기능 확인 후 커밋
This commit is contained in:
29
ExperionCrawler.Tests/ExperionCrawler.Tests.csproj
Normal file
29
ExperionCrawler.Tests/ExperionCrawler.Tests.csproj
Normal file
@@ -0,0 +1,29 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.0" />
|
||||
<PackageReference Include="xunit" Version="2.4.2" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.0">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\src\Web\ExperionCrawler.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
1
ExperionCrawler.Tests/GlobalUsings.cs
Normal file
1
ExperionCrawler.Tests/GlobalUsings.cs
Normal file
@@ -0,0 +1 @@
|
||||
global using Xunit;
|
||||
382
ExperionCrawler.Tests/KoreanTimeRangeExtractorTests.cs
Normal file
382
ExperionCrawler.Tests/KoreanTimeRangeExtractorTests.cs
Normal file
@@ -0,0 +1,382 @@
|
||||
using ExperionCrawler.Core.Application.Services;
|
||||
using Xunit;
|
||||
|
||||
namespace ExperionCrawler.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// KoreanTimeRangeExtractor 단위 테스트
|
||||
/// 새로운 한글 시간 패턴 파싱 로직을 검증합니다.
|
||||
/// </summary>
|
||||
public class KoreanTimeRangeExtractorTests
|
||||
{
|
||||
private readonly KoreanTimeRangeExtractor _extractor;
|
||||
private readonly KstClock _kstClock;
|
||||
|
||||
public KoreanTimeRangeExtractorTests()
|
||||
{
|
||||
// 테스트용 고정 시계: KST 2026-04-23 12:00:00 = UTC 2026-04-23 03:00:00
|
||||
var fixedClock = new FixedClock(new DateTimeOffset(2026, 4, 23, 3, 0, 0, TimeSpan.Zero));
|
||||
_kstClock = new KstClock(fixedClock);
|
||||
_extractor = new KoreanTimeRangeExtractor(_kstClock);
|
||||
}
|
||||
|
||||
#region 절대 범위: 부터 ~ 까지
|
||||
|
||||
[Fact]
|
||||
public void AbsoluteRange_FromTo_KoreanDate()
|
||||
{
|
||||
// Arrange & Act
|
||||
var result = _extractor.Extract("4월 3일 부터 4월 5일 까지");
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result.KstFrom);
|
||||
Assert.NotNull(result.KstTo);
|
||||
Assert.Null(result.PostgresInterval);
|
||||
// KST 2026-04-03 00:00 ~ 2026-04-06 00:00 (까지의 다음날)
|
||||
Assert.Equal(2026, result.KstFrom!.Value.Year);
|
||||
Assert.Equal(4, result.KstFrom.Value.Month);
|
||||
Assert.Equal(3, result.KstFrom.Value.Day);
|
||||
Assert.Equal(2026, result.KstTo!.Value.Year);
|
||||
Assert.Equal(4, result.KstTo.Value.Month);
|
||||
Assert.Equal(6, result.KstTo.Value.Day); // 다음날 00:00
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AbsoluteRange_FromTo_IsoDate()
|
||||
{
|
||||
// Arrange & Act
|
||||
var result = _extractor.Extract("2026-04-03 부터 2026-04-05 까지");
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result.KstFrom);
|
||||
Assert.NotNull(result.KstTo);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AbsoluteRange_FromTo_WithTime()
|
||||
{
|
||||
// Arrange & Act
|
||||
var result = _extractor.Extract("어제 09:00 부터 오늘 18:00 까지");
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result.KstFrom);
|
||||
Assert.NotNull(result.KstTo);
|
||||
// KST 2026-04-22 09:00 ~ 2026-04-23 18:00
|
||||
Assert.Equal(2026, result.KstFrom!.Value.Year);
|
||||
Assert.Equal(4, result.KstFrom.Value.Month);
|
||||
Assert.Equal(22, result.KstFrom.Value.Day);
|
||||
Assert.Equal(9, result.KstFrom.Value.Hour);
|
||||
Assert.Equal(2026, result.KstTo!.Value.Year);
|
||||
Assert.Equal(4, result.KstTo.Value.Month);
|
||||
Assert.Equal(23, result.KstTo.Value.Day);
|
||||
Assert.Equal(18, result.KstTo.Value.Hour);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AbsoluteRange_FromTo_WithAmPm()
|
||||
{
|
||||
// Arrange & Act
|
||||
var result = _extractor.Extract("오전 9시부터 오후 6시까지");
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result.KstFrom);
|
||||
Assert.NotNull(result.KstTo);
|
||||
// KST 2026-04-23 09:00 ~ 18:00
|
||||
Assert.Equal(9, result.KstFrom!.Value.Hour);
|
||||
Assert.Equal(18, result.KstTo!.Value.Hour);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AbsoluteRange_WithTimeComponent_PreservesTime()
|
||||
{
|
||||
// Arrange & Act
|
||||
var result = _extractor.Extract("2026-03-01 14:00 부터 2026-03-01 16:30 까지");
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result.KstFrom);
|
||||
Assert.NotNull(result.KstTo);
|
||||
Assert.Equal(14, result.KstFrom!.Value.Hour);
|
||||
Assert.Equal(0, result.KstFrom.Value.Minute);
|
||||
Assert.Equal(16, result.KstTo!.Value.Hour);
|
||||
Assert.Equal(30, result.KstTo.Value.Minute);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 단방향: 이후/이전
|
||||
|
||||
[Fact]
|
||||
public void OneDirection_After()
|
||||
{
|
||||
// Arrange & Act
|
||||
var result = _extractor.Extract("오늘 오후 2시 이후");
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result.KstFrom);
|
||||
Assert.Null(result.KstTo);
|
||||
Assert.Null(result.PostgresInterval);
|
||||
// KST 2026-04-23 14:00
|
||||
Assert.Equal(14, result.KstFrom!.Value.Hour);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OneDirection_Before()
|
||||
{
|
||||
// Arrange & Act
|
||||
var result = _extractor.Extract("2026-05-05 이전");
|
||||
|
||||
// Assert
|
||||
Assert.Null(result.KstFrom);
|
||||
Assert.NotNull(result.KstTo);
|
||||
Assert.Null(result.PostgresInterval);
|
||||
// KST 2026-05-05
|
||||
Assert.Equal(2026, result.KstTo!.Value.Year);
|
||||
Assert.Equal(5, result.KstTo.Value.Month);
|
||||
Assert.Equal(5, result.KstTo.Value.Day);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OneDirection_FromOnly()
|
||||
{
|
||||
// Arrange & Act
|
||||
var result = _extractor.Extract("4월 3일 부터");
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result.KstFrom);
|
||||
Assert.Null(result.KstTo);
|
||||
Assert.Null(result.PostgresInterval);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 상대 범위: 최근/지난 N시간|분|일
|
||||
|
||||
[Theory]
|
||||
[InlineData("최근 3시간", "3 hours")]
|
||||
[InlineData("지난 2시간", "2 hours")]
|
||||
[InlineData("최근 30분", "30 minutes")]
|
||||
[InlineData("지난 15분", "15 minutes")]
|
||||
[InlineData("최근 7일", "7 days")]
|
||||
[InlineData("지난 30일", "30 days")]
|
||||
[InlineData("최근 2주", "14 days")]
|
||||
public void RelativeRange_CorrectInterval(string input, string expectedInterval)
|
||||
{
|
||||
// Arrange & Act
|
||||
var result = _extractor.Extract(input);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedInterval, result.PostgresInterval);
|
||||
Assert.Null(result.KstFrom);
|
||||
Assert.Null(result.KstTo);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 지정 날짜: 오늘/어제/이번 주
|
||||
|
||||
[Fact]
|
||||
public void NamedDay_Today()
|
||||
{
|
||||
// Arrange & Act
|
||||
var result = _extractor.Extract("오늘");
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result.KstFrom);
|
||||
Assert.NotNull(result.KstTo);
|
||||
// KST 2026-04-23 00:00 ~ 2026-04-24 00:00
|
||||
Assert.Equal(2026, result.KstFrom!.Value.Year);
|
||||
Assert.Equal(4, result.KstFrom.Value.Month);
|
||||
Assert.Equal(23, result.KstFrom.Value.Day);
|
||||
Assert.Equal(2026, result.KstTo!.Value.Year);
|
||||
Assert.Equal(4, result.KstTo.Value.Month);
|
||||
Assert.Equal(24, result.KstTo.Value.Day);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NamedDay_Yesterday()
|
||||
{
|
||||
// Arrange & Act
|
||||
var result = _extractor.Extract("어제");
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result.KstFrom);
|
||||
Assert.NotNull(result.KstTo);
|
||||
// KST 2026-04-22 00:00 ~ 2026-04-23 00:00
|
||||
Assert.Equal(2026, result.KstFrom!.Value.Year);
|
||||
Assert.Equal(4, result.KstFrom.Value.Month);
|
||||
Assert.Equal(22, result.KstFrom.Value.Day);
|
||||
Assert.Equal(2026, result.KstTo!.Value.Year);
|
||||
Assert.Equal(4, result.KstTo.Value.Month);
|
||||
Assert.Equal(23, result.KstTo.Value.Day);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NamedDay_ThisWeek()
|
||||
{
|
||||
// Arrange & Act
|
||||
var result = _extractor.Extract("이번 주");
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result.KstFrom);
|
||||
Assert.Null(result.KstTo);
|
||||
// KST 2026-04-20 (월요일 - 2026-04-23은 목요일이므로 3일 전)
|
||||
Assert.Equal(2026, result.KstFrom!.Value.Year);
|
||||
Assert.Equal(4, result.KstFrom.Value.Month);
|
||||
// 2026-04-23은 목요일(Thursday = 4), 월요일은 4-3 = 4-20
|
||||
Assert.Equal(20, result.KstFrom.Value.Day);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 기본값
|
||||
|
||||
[Fact]
|
||||
public void Default_InvalidInput_ReturnsOneHourInterval()
|
||||
{
|
||||
// Arrange & Act
|
||||
var result = _extractor.Extract("PV_101.PV 평균값");
|
||||
|
||||
// Assert
|
||||
Assert.Equal("1 hour", result.PostgresInterval);
|
||||
Assert.Null(result.KstFrom);
|
||||
Assert.Null(result.KstTo);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Default_EmptyInput_ReturnsOneHourInterval()
|
||||
{
|
||||
// Arrange & Act
|
||||
var result = _extractor.Extract("");
|
||||
|
||||
// Assert
|
||||
Assert.Equal("1 hour", result.PostgresInterval);
|
||||
Assert.Null(result.KstFrom);
|
||||
Assert.Null(result.KstTo);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ToSqlCondition Tests
|
||||
|
||||
[Fact]
|
||||
public void ToSqlCondition_RelativeRange_UsesInterval()
|
||||
{
|
||||
// Arrange
|
||||
var result = _extractor.Extract("최근 3시간");
|
||||
|
||||
// Act
|
||||
var sql = result.ToSqlCondition("\"time\"", _kstClock);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("\"time\" >= NOW() - INTERVAL '3 hours'", sql);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToSqlCondition_AbsoluteRange_BothSides()
|
||||
{
|
||||
// Arrange
|
||||
var result = _extractor.Extract("오늘");
|
||||
|
||||
// Act
|
||||
var sql = result.ToSqlCondition("\"time\"", _kstClock);
|
||||
|
||||
// Assert
|
||||
// KST 2026-04-23 00:00 = UTC 2026-04-22 15:00
|
||||
// KST 2026-04-24 00:00 = UTC 2026-04-23 15:00
|
||||
Assert.Contains("\"time\" >=", sql);
|
||||
Assert.Contains("AND \"time\" <", sql);
|
||||
Assert.Contains("2026-04-22 15:00:00+00", sql);
|
||||
Assert.Contains("2026-04-23 15:00:00+00", sql);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToSqlCondition_FromOnly()
|
||||
{
|
||||
// Arrange
|
||||
var result = _extractor.Extract("오늘 오후 2시 이후");
|
||||
|
||||
// Act
|
||||
var sql = result.ToSqlCondition("\"time\"", _kstClock);
|
||||
|
||||
// Assert
|
||||
// KST 2026-04-23 14:00 = UTC 2026-04-23 05:00
|
||||
Assert.Contains("\"time\" >=", sql);
|
||||
Assert.DoesNotContain("AND", sql);
|
||||
Assert.Contains("2026-04-23 05:00:00+00", sql);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToSqlCondition_ToOnly()
|
||||
{
|
||||
// Arrange
|
||||
var result = _extractor.Extract("2026-05-05 이전");
|
||||
|
||||
// Act
|
||||
var sql = result.ToSqlCondition("\"time\"", _kstClock);
|
||||
|
||||
// Assert
|
||||
// KST 2026-05-05 00:00 = UTC 2026-05-04 15:00
|
||||
Assert.Contains("\"time\" <", sql);
|
||||
Assert.DoesNotContain("AND", sql);
|
||||
Assert.Contains("2026-05-04 15:00:00+00", sql);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ParseKstDateTime Tests (via Extract)
|
||||
|
||||
// ParseKstDateTime은 internal이므로 Extract를 통해 간접 테스트합니다.
|
||||
|
||||
[Theory]
|
||||
[InlineData("오늘", "2026-04-23")]
|
||||
[InlineData("어제", "2026-04-22")]
|
||||
[InlineData("4월 3일", "2026-04-03")]
|
||||
public void Extract_KoreanDate_ParsesCorrectly(string input, string expectedDate)
|
||||
{
|
||||
// Arrange & Act
|
||||
var result = _extractor.Extract(input);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result.KstFrom);
|
||||
Assert.Equal(expectedDate, result.KstFrom!.Value.ToString("yyyy-MM-dd"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Extract_WithTime_PreservesTime()
|
||||
{
|
||||
// Arrange & Act
|
||||
var result = _extractor.Extract("오늘 14:30 이후");
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result.KstFrom);
|
||||
Assert.Equal(14, result.KstFrom!.Value.Hour);
|
||||
Assert.Equal(30, result.KstFrom.Value.Minute);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Extract_WithAmPm_ConvertsTo24Hour()
|
||||
{
|
||||
// Arrange & Act
|
||||
var result = _extractor.Extract("오후 3시 이후");
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result.KstFrom);
|
||||
Assert.Equal(15, result.KstFrom!.Value.Hour);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Extract_IsoFormat_ParsesCorrectly()
|
||||
{
|
||||
// Arrange & Act
|
||||
var result = _extractor.Extract("2026-03-01 14:00 이후");
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result.KstFrom);
|
||||
Assert.Equal(2026, result.KstFrom!.Value.Year);
|
||||
Assert.Equal(3, result.KstFrom.Value.Month);
|
||||
Assert.Equal(1, result.KstFrom.Value.Day);
|
||||
Assert.Equal(14, result.KstFrom.Value.Hour);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
188
ExperionCrawler.Tests/SqlValidatorTests.cs
Normal file
188
ExperionCrawler.Tests/SqlValidatorTests.cs
Normal file
@@ -0,0 +1,188 @@
|
||||
using ExperionCrawler.Core.Application.DTOs;
|
||||
using ExperionCrawler.Core.Application.Services;
|
||||
|
||||
namespace ExperionCrawler.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// SqlValidator 다단계 검증기 테스트
|
||||
/// 검증 순서: ①구조 → ②위험키워드 → ③금지절 → ④함수화이트리스트 → ⑤테이블참조 → ⑥서브쿼리깊이 → ⑦의심패턴
|
||||
/// </summary>
|
||||
public class SqlValidatorTests
|
||||
{
|
||||
private readonly SqlValidator _v;
|
||||
|
||||
public SqlValidatorTests()
|
||||
{
|
||||
_v = new SqlValidator(new SqlValidatorOptions
|
||||
{
|
||||
RequiredTables = ["measurements"],
|
||||
AllowedTables = ["measurements", "node_map_master"],
|
||||
MaxSubqueryDepth = 4
|
||||
});
|
||||
}
|
||||
|
||||
// ── ① 정상 케이스 (SELECT 전용, 허용 함수/테이블) ────────────────────────
|
||||
[Theory]
|
||||
[InlineData("SELECT AVG(value) FROM measurements WHERE tagname = 'PV_101.PV'")]
|
||||
[InlineData("SELECT date_trunc('minute', time), AVG(value) FROM measurements GROUP BY 1")]
|
||||
[InlineData("SELECT tagname, REGR_SLOPE(value, EXTRACT(EPOCH FROM time)) FROM measurements GROUP BY tagname")]
|
||||
[InlineData("SELECT date_trunc('minute', time), first(value, time) FROM measurements WHERE tagname = 'test' GROUP BY 1 ORDER BY 1")]
|
||||
[InlineData("SELECT * FROM measurements WHERE tagname = 'test' AND time > now() - interval '1 hour'")]
|
||||
public void ValidSql_ShouldPass(string sql)
|
||||
{
|
||||
var result = _v.Validate(sql);
|
||||
Assert.True(result.IsValid, $"Expected valid but failed: {result.Message}");
|
||||
}
|
||||
|
||||
// ── ① SELECT 전용 검사 ─────────────────────────────────────────────────
|
||||
[Theory]
|
||||
[InlineData("DROP TABLE measurements", ValidationFailReason.NotSelectStatement)]
|
||||
[InlineData("CREATE TABLE test (id INT)", ValidationFailReason.NotSelectStatement)]
|
||||
[InlineData("ALTER TABLE measurements ADD COLUMN x INT", ValidationFailReason.NotSelectStatement)]
|
||||
[InlineData("TRUNCATE measurements", ValidationFailReason.NotSelectStatement)]
|
||||
[InlineData("MERGE INTO measurements USING...", ValidationFailReason.NotSelectStatement)]
|
||||
[InlineData("INSERT INTO measurements VALUES (1, 2, 3)", ValidationFailReason.NotSelectStatement)]
|
||||
[InlineData("UPDATE measurements SET value = 1", ValidationFailReason.NotSelectStatement)]
|
||||
[InlineData("DELETE FROM measurements", ValidationFailReason.NotSelectStatement)]
|
||||
public void NonSelectStatement_ShouldFail(string sql, ValidationFailReason expected)
|
||||
{
|
||||
var result = _v.Validate(sql);
|
||||
Assert.False(result.IsValid);
|
||||
Assert.Equal(expected, result.Reason);
|
||||
}
|
||||
|
||||
// ── ② 위험 키워드 검사 (SELECT로 시작하지만 위험 키워드 포함) ─────────────
|
||||
[Theory]
|
||||
[InlineData("SELECT * FROM measurements; DROP TABLE measurements", ValidationFailReason.DangerousKeyword)]
|
||||
[InlineData("SELECT * FROM measurements; INSERT INTO other VALUES (1)", ValidationFailReason.DangerousKeyword)]
|
||||
[InlineData("SELECT * FROM measurements; COMMIT", ValidationFailReason.DangerousKeyword)]
|
||||
public void DangerousKeywordAfterSelect_ShouldFail(string sql, ValidationFailReason expected)
|
||||
{
|
||||
var result = _v.Validate(sql);
|
||||
Assert.False(result.IsValid);
|
||||
Assert.Equal(expected, result.Reason);
|
||||
}
|
||||
|
||||
// ── ③ 금지 절 검사 (SELECT로 시작하지만 금지 절 포함) ─────────────────────
|
||||
[Theory]
|
||||
[InlineData("SELECT pg_sleep(5)", ValidationFailReason.ForbiddenClause)]
|
||||
[InlineData("SELECT pg_cancel_backend(1)", ValidationFailReason.ForbiddenClause)]
|
||||
[InlineData("SELECT pg_terminate_backend(1)", ValidationFailReason.ForbiddenClause)]
|
||||
// CALL/EXECUTE는 SELECT로 시작하지 않으므로 NotSelectStatement로 먼저 실패
|
||||
public void ForbiddenClause_ShouldFail(string sql, ValidationFailReason expected)
|
||||
{
|
||||
var result = _v.Validate(sql);
|
||||
Assert.False(result.IsValid);
|
||||
Assert.Equal(expected, result.Reason);
|
||||
}
|
||||
|
||||
// ── ④ 허용되지 않는 함수 ────────────────────────────────────────────────
|
||||
[Theory]
|
||||
[InlineData("SELECT SYSTEM('ls') FROM measurements", ValidationFailReason.DisallowedFunction)]
|
||||
[InlineData("SELECT COPY_FILE('/tmp') FROM measurements", ValidationFailReason.DisallowedFunction)]
|
||||
[InlineData("SELECT NON_EXISTING_FUNC(1) FROM measurements", ValidationFailReason.DisallowedFunction)]
|
||||
public void DisallowedFunction_ShouldFail(string sql, ValidationFailReason expected)
|
||||
{
|
||||
var result = _v.Validate(sql);
|
||||
Assert.False(result.IsValid);
|
||||
Assert.Equal(expected, result.Reason);
|
||||
}
|
||||
|
||||
// ── ⑤ 필수 테이블 누락 ─────────────────────────────────────────────────
|
||||
[Fact]
|
||||
public void MissingRequiredTable_ShouldFail()
|
||||
{
|
||||
var sql = "SELECT 1 FROM node_map_master";
|
||||
var result = _v.Validate(sql);
|
||||
Assert.False(result.IsValid);
|
||||
Assert.Equal(ValidationFailReason.MissingRequiredTable, result.Reason);
|
||||
}
|
||||
|
||||
// ── ⑦ 의심 패턴 검사 (시스템 뷰 접근) ───────────────────────────────────
|
||||
[Fact]
|
||||
public void SystemViewAccess_ShouldFail()
|
||||
{
|
||||
// measurements가 없어서 MissingRequiredTable로 먼저 실패
|
||||
var sql = "SELECT * FROM information_schema.tables";
|
||||
var result = _v.Validate(sql);
|
||||
Assert.False(result.IsValid);
|
||||
// 필수 테이블 measurements가 없어서 MissingRequiredTable로 실패
|
||||
Assert.Equal(ValidationFailReason.MissingRequiredTable, result.Reason);
|
||||
}
|
||||
|
||||
// ── SQL Injection 패턴 ──────────────────────────────────────────────────
|
||||
[Theory]
|
||||
[InlineData("SELECT * FROM measurements WHERE tagname = '' OR '1'='1'", ValidationFailReason.SuspiciousPattern)]
|
||||
[InlineData("SELECT * FROM measurements UNION SELECT * FROM measurements", ValidationFailReason.SuspiciousPattern)]
|
||||
[InlineData("SELECT * FROM measurements WHERE tagname = 'x'; DROP TABLE measurements", ValidationFailReason.DangerousKeyword)]
|
||||
public void InjectionPattern_ShouldFail(string sql, ValidationFailReason expected)
|
||||
{
|
||||
var result = _v.Validate(sql);
|
||||
Assert.False(result.IsValid);
|
||||
Assert.Equal(expected, result.Reason);
|
||||
}
|
||||
|
||||
// ── ⑥ 서브쿼리 깊이 ────────────────────────────────────────────────────
|
||||
[Fact]
|
||||
public void SubqueryDepthExceeded_ShouldFail()
|
||||
{
|
||||
// 5단계 중첩: ( -> ( -> ( -> ( -> ( -> (SELECT f FROM measurements)
|
||||
// MaxSubqueryDepth = 4이므로 5단계에서 실패
|
||||
// 괄호 개수: 5개여야 함
|
||||
var sql = "SELECT a FROM (SELECT b FROM (SELECT c FROM (SELECT d FROM (SELECT e FROM (SELECT f FROM measurements) AS x0) AS x1) AS x2) AS x3) AS x4";
|
||||
var result = _v.Validate(sql);
|
||||
Assert.False(result.IsValid, $"Expected invalid but got valid. Message: {result.Message}");
|
||||
Assert.Equal(ValidationFailReason.SubqueryDepthExceeded, result.Reason);
|
||||
}
|
||||
|
||||
// ── 빈 입력 ────────────────────────────────────────────────────────────
|
||||
[Theory]
|
||||
[InlineData("")]
|
||||
[InlineData(" ")]
|
||||
[InlineData(null)]
|
||||
public void EmptyInput_ShouldFail(string? sql)
|
||||
{
|
||||
var result = _v.Validate(sql!);
|
||||
Assert.False(result.IsValid);
|
||||
Assert.Equal(ValidationFailReason.EmptyInput, result.Reason);
|
||||
}
|
||||
|
||||
// ── 허용 함수 화이트리스트 (정규 케이스) ─────────────────────────────────
|
||||
[Theory]
|
||||
[InlineData("SELECT COUNT(*) FROM measurements")]
|
||||
[InlineData("SELECT SUM(value), AVG(value), MIN(value), MAX(value) FROM measurements")]
|
||||
[InlineData("SELECT STDDEV(value), VARIANCE(value) FROM measurements")]
|
||||
[InlineData("SELECT ROW_NUMBER() OVER (ORDER BY time) FROM measurements")]
|
||||
[InlineData("SELECT RANK() OVER (PARTITION BY tagname ORDER BY value DESC) FROM measurements")]
|
||||
[InlineData("SELECT LAG(value, 1) OVER (ORDER BY time) FROM measurements")]
|
||||
[InlineData("SELECT NOW(), CURRENT_TIMESTAMP, CURRENT_DATE FROM measurements")]
|
||||
[InlineData("SELECT DATE_TRUNC('hour', time), EXTRACT(EPOCH FROM time) FROM measurements")]
|
||||
[InlineData("SELECT TIME_BUCKET('1 hour', time) FROM measurements")]
|
||||
[InlineData("SELECT UPPER(tagname), LOWER(tagname), TRIM(tagname) FROM measurements")]
|
||||
[InlineData("SELECT COALESCE(value, 0), NULLIF(value, -1) FROM measurements")]
|
||||
[InlineData("SELECT ROUND(value), CEIL(value), FLOOR(value) FROM measurements")]
|
||||
public void AllowedFunctions_ShouldPass(string sql)
|
||||
{
|
||||
var result = _v.Validate(sql);
|
||||
Assert.True(result.IsValid, $"Expected valid but failed: {result.Message}");
|
||||
}
|
||||
|
||||
// ── Deconstruct 테스트 ─────────────────────────────────────────────────
|
||||
[Fact]
|
||||
public void ValidationResult_Deconstruct_ShouldWork()
|
||||
{
|
||||
var result = _v.Validate("SELECT 1 FROM measurements");
|
||||
var (ok, error) = result;
|
||||
Assert.True(ok);
|
||||
Assert.Null(error);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidationResult_Deconstruct_Fail_ShouldWork()
|
||||
{
|
||||
var result = _v.Validate("");
|
||||
var (ok, error) = result;
|
||||
Assert.False(ok);
|
||||
Assert.NotNull(error);
|
||||
}
|
||||
}
|
||||
220
ExperionCrawler.Tests/TestResults/tests.trx
Normal file
220
ExperionCrawler.Tests/TestResults/tests.trx
Normal file
@@ -0,0 +1,220 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<TestRun id="ff720602-c42d-4714-8a4d-fe329014b49f" name="@spark 2026-04-25 11:44:29" xmlns="http://microsoft.com/schemas/VisualStudio/TeamTest/2010">
|
||||
<Times creation="2026-04-25T11:44:29.4304710+09:00" queuing="2026-04-25T11:44:29.4304710+09:00" start="2026-04-25T11:44:28.6442347+09:00" finish="2026-04-25T11:44:29.4827041+09:00" />
|
||||
<TestSettings name="default" id="d2041d3e-230c-4156-99fc-c431da3496f7">
|
||||
<Deployment runDeploymentRoot="_spark_2026-04-25_11_44_29" />
|
||||
</TestSettings>
|
||||
<Results>
|
||||
<UnitTestResult executionId="fa8e7505-2eb7-479a-9859-d8d21a234118" testId="5c3e9ce7-7bd1-deb3-bcf9-286a5a797173" testName="ExperionCrawler.Tests.TextToSqlServiceTests.ParseNaturalLanguageAsync_Recent1Month_ExtractsTimeRange" computerName="spark" duration="00:00:00.0003819" startTime="2026-04-25T11:44:29.3898328+09:00" endTime="2026-04-25T11:44:29.3898328+09:00" testType="13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b" outcome="Passed" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" relativeResultsDirectory="fa8e7505-2eb7-479a-9859-d8d21a234118" />
|
||||
<UnitTestResult executionId="6342897b-79d5-45f1-b779-b21463d71914" testId="4cc079cc-8497-c2b1-a13f-371856fe1a6d" testName="ExperionCrawler.Tests.TextToSqlServiceTests.ParseNaturalLanguageAsync_AverageKeyword_UsesAvgFunction" computerName="spark" duration="00:00:00.0002663" startTime="2026-04-25T11:44:29.3906626+09:00" endTime="2026-04-25T11:44:29.3906626+09:00" testType="13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b" outcome="Passed" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" relativeResultsDirectory="6342897b-79d5-45f1-b779-b21463d71914" />
|
||||
<UnitTestResult executionId="4f8df51a-e50e-4781-9f88-143d0368cb20" testId="453ebd31-b897-172a-47b2-ee9237e423e2" testName="ExperionCrawler.Tests.TextToSqlServiceTests.ParseNaturalLanguageAsync_WithTagName_GeneratesSql" computerName="spark" duration="00:00:00.0003165" startTime="2026-04-25T11:44:29.3743595+09:00" endTime="2026-04-25T11:44:29.3743596+09:00" testType="13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b" outcome="Passed" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" relativeResultsDirectory="4f8df51a-e50e-4781-9f88-143d0368cb20" />
|
||||
<UnitTestResult executionId="d35ec836-f0ec-43c4-ac25-7b50d7dbdba0" testId="80269386-9ca6-1dd2-59e4-7b56fd7a66ed" testName="ExperionCrawler.Tests.TextToSqlServiceTests.ParseNaturalLanguageAsync_SpecialCharacters_EscapesTagName" computerName="spark" duration="00:00:00.0002507" startTime="2026-04-25T11:44:29.3907344+09:00" endTime="2026-04-25T11:44:29.3907344+09:00" testType="13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b" outcome="Passed" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" relativeResultsDirectory="d35ec836-f0ec-43c4-ac25-7b50d7dbdba0" />
|
||||
<UnitTestResult executionId="6f313979-2da5-4dfe-9c34-27e3ce65743b" testId="8df30334-5f76-38b5-25e9-f6812aa6cdff" testName="ExperionCrawler.Tests.TextToSqlServiceTests.ParseNaturalLanguageAsync_ThreeTagsCommaSeparated_IncludesAllTags" computerName="spark" duration="00:00:00.0003450" startTime="2026-04-25T11:44:29.3898916+09:00" endTime="2026-04-25T11:44:29.3898916+09:00" testType="13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b" outcome="Passed" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" relativeResultsDirectory="6f313979-2da5-4dfe-9c34-27e3ce65743b" />
|
||||
<UnitTestResult executionId="5c45a8b9-2eb2-427c-a731-414f909b7048" testId="6b5ec284-00db-56ef-7384-a5713f6ab45e" testName="ExperionCrawler.Tests.TextToSqlServiceTests.ParseNaturalLanguageAsync_Recent24Hours_ExtractsTimeRange" computerName="spark" duration="00:00:00.0002515" startTime="2026-04-25T11:44:29.3757051+09:00" endTime="2026-04-25T11:44:29.3757051+09:00" testType="13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b" outcome="Passed" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" relativeResultsDirectory="5c45a8b9-2eb2-427c-a731-414f909b7048" />
|
||||
<UnitTestResult executionId="4da36672-445e-4369-9d44-360a7b6b8b99" testId="e0331b39-ee4e-8fe1-50ed-78732fa3841a" testName="ExperionCrawler.Tests.TextToSqlServiceTests.ParseNaturalLanguageAsync_DotTagName_ExtractsCorrectly" computerName="spark" duration="00:00:00.0005555" startTime="2026-04-25T11:44:29.3909704+09:00" endTime="2026-04-25T11:44:29.3909705+09:00" testType="13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b" outcome="Passed" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" relativeResultsDirectory="4da36672-445e-4369-9d44-360a7b6b8b99" />
|
||||
<UnitTestResult executionId="90650188-4846-4d68-bc39-f7a4c8fb9814" testId="9dcebd05-5954-2a01-645b-410629bd254d" testName="ExperionCrawler.Tests.TextToSqlServiceTests.ParseNaturalLanguageAsync_WithMinKeyword_GeneratesMinAggregation" computerName="spark" duration="00:00:00.0002630" startTime="2026-04-25T11:44:29.3905817+09:00" endTime="2026-04-25T11:44:29.3905817+09:00" testType="13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b" outcome="Passed" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" relativeResultsDirectory="90650188-4846-4d68-bc39-f7a4c8fb9814" />
|
||||
<UnitTestResult executionId="d8210a41-f48e-416c-9e6f-b4b5cd096013" testId="4c2d002b-4d1c-3c3b-855d-00377118994d" testName="ExperionCrawler.Tests.TextToSqlServiceTests.SuggestQueriesAsync_WithFilter_ReturnsFilteredSuggestions" computerName="spark" duration="00:00:00.0006235" startTime="2026-04-25T11:44:29.3898144+09:00" endTime="2026-04-25T11:44:29.3898144+09:00" testType="13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b" outcome="Passed" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" relativeResultsDirectory="d8210a41-f48e-416c-9e6f-b4b5cd096013" />
|
||||
<UnitTestResult executionId="7ee38996-5e67-4144-9280-d066fa103dcd" testId="15e84926-8d29-6d29-d5f1-18dd26763892" testName="ExperionCrawler.Tests.TextToSqlServiceTests.ParseNaturalLanguageAsync_MultipleTagsWithKoreanDescriptions_ExtractsOnlyTags" computerName="spark" duration="00:00:00.0003433" startTime="2026-04-25T11:44:29.3906261+09:00" endTime="2026-04-25T11:44:29.3906261+09:00" testType="13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b" outcome="Passed" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" relativeResultsDirectory="7ee38996-5e67-4144-9280-d066fa103dcd" />
|
||||
<UnitTestResult executionId="4101239d-abc2-45e3-9aeb-32e835964cf7" testId="cf1eb7a6-cd60-600b-bfaa-e1f835f77b59" testName="ExperionCrawler.Tests.TextToSqlServiceTests.ParseNaturalLanguageAsync_WithEmptyInput_ThrowsArgumentException" computerName="spark" duration="00:00:00.0009436" startTime="2026-04-25T11:44:29.3909881+09:00" endTime="2026-04-25T11:44:29.3909882+09:00" testType="13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b" outcome="Passed" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" relativeResultsDirectory="4101239d-abc2-45e3-9aeb-32e835964cf7" />
|
||||
<UnitTestResult executionId="4248d846-c27e-486b-979b-89e6b55ae83b" testId="14f39cbb-10de-141a-f514-4230ebe336ed" testName="ExperionCrawler.Tests.TextToSqlServiceTests.ParseNaturalLanguageAsync_WithFirstKeyword_GeneratesFirstAggregation" computerName="spark" duration="00:00:00.0002743" startTime="2026-04-25T11:44:29.3906445+09:00" endTime="2026-04-25T11:44:29.3906445+09:00" testType="13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b" outcome="Passed" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" relativeResultsDirectory="4248d846-c27e-486b-979b-89e6b55ae83b" />
|
||||
<UnitTestResult executionId="0c3ab594-4d65-4c0b-bcca-a10ee0f58a0f" testId="df534781-ce1f-66fc-3790-0b8e5eae75df" testName="ExperionCrawler.Tests.TextToSqlServiceTests.ParseNaturalLanguageAsync_SimpleTagName_ExtractsCorrectly" computerName="spark" duration="00:00:00.0002892" startTime="2026-04-25T11:44:29.3897951+09:00" endTime="2026-04-25T11:44:29.3897951+09:00" testType="13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b" outcome="Passed" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" relativeResultsDirectory="0c3ab594-4d65-4c0b-bcca-a10ee0f58a0f" />
|
||||
<UnitTestResult executionId="9627a0d8-e645-4db1-9ed7-54afb8d37be0" testId="cea17860-3597-3e21-1465-62e77aee9cc6" testName="ExperionCrawler.Tests.TextToSqlServiceTests.ParseNaturalLanguageAsync_WithMaxKeyword_GeneratesMaxAggregation" computerName="spark" duration="00:00:00.0002379" startTime="2026-04-25T11:44:29.3909333+09:00" endTime="2026-04-25T11:44:29.3909333+09:00" testType="13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b" outcome="Passed" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" relativeResultsDirectory="9627a0d8-e645-4db1-9ed7-54afb8d37be0" />
|
||||
<UnitTestResult executionId="5ddb25ec-fe9d-46a7-ac6d-91d8e60c465b" testId="675f7860-eb45-3219-27e7-57c7e2e65ee2" testName="ExperionCrawler.Tests.TextToSqlServiceTests.ParseNaturalLanguageAsync_NoSpaceTagName_UsesWholeInput" computerName="spark" duration="00:00:00.0002750" startTime="2026-04-25T11:44:29.3898738+09:00" endTime="2026-04-25T11:44:29.3898738+09:00" testType="13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b" outcome="Passed" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" relativeResultsDirectory="5ddb25ec-fe9d-46a7-ac6d-91d8e60c465b" />
|
||||
<UnitTestResult executionId="6ac7aa3c-408f-47d3-9d4d-ef44ccf8abab" testId="c0c4b062-d710-d2dc-7fb6-02e20736a39f" testName="ExperionCrawler.Tests.TextToSqlServiceTests.SuggestQueriesAsync_WithEmptyInput_ReturnsAllSuggestions" computerName="spark" duration="00:00:00.0009178" startTime="2026-04-25T11:44:29.3906982+09:00" endTime="2026-04-25T11:44:29.3906983+09:00" testType="13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b" outcome="Passed" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" relativeResultsDirectory="6ac7aa3c-408f-47d3-9d4d-ef44ccf8abab" />
|
||||
<UnitTestResult executionId="0bd3a654-47ed-4a79-a283-1e1e11baf6e4" testId="03b464c4-872d-93f1-75bf-cc169416edee" testName="ExperionCrawler.Tests.TextToSqlServiceTests.ParseNaturalLanguageAsync_Recent7Days_ExtractsTimeRange" computerName="spark" duration="00:00:00.0003093" startTime="2026-04-25T11:44:29.3907158+09:00" endTime="2026-04-25T11:44:29.3907158+09:00" testType="13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b" outcome="Passed" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" relativeResultsDirectory="0bd3a654-47ed-4a79-a283-1e1e11baf6e4" />
|
||||
<UnitTestResult executionId="2af16c71-455e-47d1-9414-06fdbbb0c9f4" testId="a588adc0-4f17-3b17-110a-4aa6f024dd3f" testName="ExperionCrawler.Tests.TextToSqlServiceTests.ParseNaturalLanguageAsync_FromToPattern_ExtractsTimeRange" computerName="spark" duration="00:00:00.0198781" startTime="2026-04-25T11:44:29.3713915+09:00" endTime="2026-04-25T11:44:29.3714001+09:00" testType="13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b" outcome="Passed" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" relativeResultsDirectory="2af16c71-455e-47d1-9414-06fdbbb0c9f4" />
|
||||
<UnitTestResult executionId="a892d81e-6c53-49a5-aa88-60a4c3313472" testId="dfce6d90-9761-4ca3-ca30-781ba02dbfa4" testName="ExperionCrawler.Tests.TextToSqlServiceTests.ParseNaturalLanguageAsync_MultipleTagsCommaSeparated_IncludesAllTags" computerName="spark" duration="00:00:00.0004279" startTime="2026-04-25T11:44:29.3754339+09:00" endTime="2026-04-25T11:44:29.3754339+09:00" testType="13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b" outcome="Passed" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" relativeResultsDirectory="a892d81e-6c53-49a5-aa88-60a4c3313472" />
|
||||
<UnitTestResult executionId="e60f083e-00ce-452d-837d-cfeafda675ee" testId="77b9b877-6d45-d631-10c9-19432d683af4" testName="ExperionCrawler.Tests.TextToSqlServiceTests.ParseNaturalLanguageAsync_KoreanDate_ExtractsTimeRange" computerName="spark" duration="00:00:00.0002391" startTime="2026-04-25T11:44:29.3899269+09:00" endTime="2026-04-25T11:44:29.3899269+09:00" testType="13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b" outcome="Passed" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" relativeResultsDirectory="e60f083e-00ce-452d-837d-cfeafda675ee" />
|
||||
<UnitTestResult executionId="de69eaaf-6366-4889-94c1-d0281deb012c" testId="750cf791-c506-cfa6-4deb-79897ece87ff" testName="ExperionCrawler.Tests.TextToSqlServiceTests.ParseNaturalLanguageAsync_DefaultAggregationIsLast" computerName="spark" duration="00:00:00.0002543" startTime="2026-04-25T11:44:29.3899092+09:00" endTime="2026-04-25T11:44:29.3899092+09:00" testType="13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b" outcome="Passed" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" relativeResultsDirectory="de69eaaf-6366-4889-94c1-d0281deb012c" />
|
||||
<UnitTestResult executionId="801f328a-0b75-4735-9e21-37e6d04314ba" testId="73098855-7d72-81ed-193c-c79d76370f85" testName="ExperionCrawler.Tests.TextToSqlServiceTests.ParseNaturalLanguageAsync_WithLastKeyword_GeneratesLastAggregation" computerName="spark" duration="00:00:00.0024067" startTime="2026-04-25T11:44:29.3736241+09:00" endTime="2026-04-25T11:44:29.3736241+09:00" testType="13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b" outcome="Passed" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" relativeResultsDirectory="801f328a-0b75-4735-9e21-37e6d04314ba" />
|
||||
<UnitTestResult executionId="24ec31bb-738d-4cba-968e-ddc8ac40d249" testId="7ff2886d-a78d-22be-1cf0-6826794967ab" testName="ExperionCrawler.Tests.TextToSqlServiceTests.ParseNaturalLanguageAsync_AfterPattern_ExtractsTimeRange" computerName="spark" duration="00:00:00.0002320" startTime="2026-04-25T11:44:29.3909150+09:00" endTime="2026-04-25T11:44:29.3909150+09:00" testType="13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b" outcome="Passed" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" relativeResultsDirectory="24ec31bb-738d-4cba-968e-ddc8ac40d249" />
|
||||
<UnitTestResult executionId="835eeb43-fcf4-44c6-a160-4f9fd35cced8" testId="64e7212f-9c18-5a8c-19cd-01e0da0ea86f" testName="ExperionCrawler.Tests.TextToSqlServiceTests.ParseNaturalLanguageAsync_MaxKeyword_UsesMaxFunction" computerName="spark" duration="00:00:00.0003019" startTime="2026-04-25T11:44:29.3746816+09:00" endTime="2026-04-25T11:44:29.3746816+09:00" testType="13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b" outcome="Passed" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" relativeResultsDirectory="835eeb43-fcf4-44c6-a160-4f9fd35cced8" />
|
||||
<UnitTestResult executionId="83fce793-2e88-4b7d-91f4-af2a95592bbb" testId="16a4f7ee-8c4f-8b19-bf63-11f177c56cc6" testName="ExperionCrawler.Tests.TextToSqlServiceTests.ParseNaturalLanguageAsync_Recent1Hour_ExtractsTimeRange" computerName="spark" duration="00:00:00.0002834" startTime="2026-04-25T11:44:29.3749850+09:00" endTime="2026-04-25T11:44:29.3749850+09:00" testType="13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b" outcome="Passed" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" relativeResultsDirectory="83fce793-2e88-4b7d-91f4-af2a95592bbb" />
|
||||
<UnitTestResult executionId="0f19eebe-6bd1-4ae8-817c-8a4c01858476" testId="6bbded63-64a7-bc4b-baa2-42bdfaed94e5" testName="ExperionCrawler.Tests.TextToSqlServiceTests.ParseNaturalLanguageAsync_NoTimeSpecified_UsesDefault" computerName="spark" duration="00:00:00.0002681" startTime="2026-04-25T11:44:29.3906054+09:00" endTime="2026-04-25T11:44:29.3906055+09:00" testType="13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b" outcome="Passed" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" relativeResultsDirectory="0f19eebe-6bd1-4ae8-817c-8a4c01858476" />
|
||||
<UnitTestResult executionId="be8411f0-aa2c-4cab-a30c-584e1d59a747" testId="0a6fd87b-3482-8683-9f78-54911a84c958" testName="ExperionCrawler.Tests.TextToSqlServiceTests.ParseNaturalLanguageAsync_NoAggregateKeyword_UsesLastFunction" computerName="spark" duration="00:00:00.0002227" startTime="2026-04-25T11:44:29.3909510+09:00" endTime="2026-04-25T11:44:29.3909511+09:00" testType="13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b" outcome="Passed" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" relativeResultsDirectory="be8411f0-aa2c-4cab-a30c-584e1d59a747" />
|
||||
<UnitTestResult executionId="9e9a83b0-c91a-4a52-978c-75690f51a6c8" testId="dcefabeb-f8ef-a09c-8b99-566f8d7f5f75" testName="ExperionCrawler.Tests.TextToSqlServiceTests.ParseNaturalLanguageAsync_OpcUaNodeId_ExtractsCorrectly" computerName="spark" duration="00:00:00.0004160" startTime="2026-04-25T11:44:29.3896896+09:00" endTime="2026-04-25T11:44:29.3896896+09:00" testType="13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b" outcome="Passed" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" relativeResultsDirectory="9e9a83b0-c91a-4a52-978c-75690f51a6c8" />
|
||||
<UnitTestResult executionId="6b62004e-a542-49df-9eaa-8b1d8ebbd96b" testId="6c884139-c4ff-db15-9260-81c43984d859" testName="ExperionCrawler.Tests.TextToSqlServiceTests.ParseNaturalLanguageAsync_MultipleTagsWithAbsTimeRange_IncludesAllTags" computerName="spark" duration="00:00:00.0002822" startTime="2026-04-25T11:44:29.3760063+09:00" endTime="2026-04-25T11:44:29.3760063+09:00" testType="13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b" outcome="Passed" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" relativeResultsDirectory="6b62004e-a542-49df-9eaa-8b1d8ebbd96b" />
|
||||
<UnitTestResult executionId="cb48f6dc-fa21-4f1d-a8f9-febab7d25fc9" testId="37e9c535-a3c6-2083-f2c0-987d98ae10fa" testName="ExperionCrawler.Tests.TextToSqlServiceTests.ParseNaturalLanguageAsync_AvgKeyword_UsesAvgFunction" computerName="spark" duration="00:00:00.0003775" startTime="2026-04-25T11:44:29.3906806+09:00" endTime="2026-04-25T11:44:29.3906806+09:00" testType="13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b" outcome="Passed" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" relativeResultsDirectory="cb48f6dc-fa21-4f1d-a8f9-febab7d25fc9" />
|
||||
<UnitTestResult executionId="cb24fe3d-f7fd-4740-94b6-490e0d7bab76" testId="7df9fd70-57c9-fe80-db0e-4c9bb3029efe" testName="ExperionCrawler.Tests.TextToSqlServiceTests.ParseNaturalLanguageAsync_MinKeyword_UsesMinFunction" computerName="spark" duration="00:00:00.0003823" startTime="2026-04-25T11:44:29.3740212+09:00" endTime="2026-04-25T11:44:29.3740212+09:00" testType="13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b" outcome="Passed" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" relativeResultsDirectory="cb24fe3d-f7fd-4740-94b6-490e0d7bab76" />
|
||||
<UnitTestResult executionId="7c5468e1-7168-4a8a-a0ae-ad6784346495" testId="22b5066e-140f-d997-79ae-a2cc60ca7ac9" testName="ExperionCrawler.Tests.TextToSqlServiceTests.ParseNaturalLanguageAsync_TodayAmPmRange_ExtractsTimeRange" computerName="spark" duration="00:00:00.0002857" startTime="2026-04-25T11:44:29.3898553+09:00" endTime="2026-04-25T11:44:29.3898554+09:00" testType="13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b" outcome="Passed" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" relativeResultsDirectory="7c5468e1-7168-4a8a-a0ae-ad6784346495" />
|
||||
</Results>
|
||||
<TestDefinitions>
|
||||
<UnitTest name="ExperionCrawler.Tests.TextToSqlServiceTests.ParseNaturalLanguageAsync_ThreeTagsCommaSeparated_IncludesAllTags" storage="/home/windpacer/projects/experioncrawler/experioncrawler.tests/bin/debug/net8.0/experioncrawler.tests.dll" id="8df30334-5f76-38b5-25e9-f6812aa6cdff">
|
||||
<Execution id="6f313979-2da5-4dfe-9c34-27e3ce65743b" />
|
||||
<TestMethod codeBase="/home/windpacer/projects/ExperionCrawler/ExperionCrawler.Tests/bin/Debug/net8.0/ExperionCrawler.Tests.dll" adapterTypeName="executor://xunit/VsTestRunner2/netcoreapp" className="ExperionCrawler.Tests.TextToSqlServiceTests" name="ParseNaturalLanguageAsync_ThreeTagsCommaSeparated_IncludesAllTags" />
|
||||
</UnitTest>
|
||||
<UnitTest name="ExperionCrawler.Tests.TextToSqlServiceTests.ParseNaturalLanguageAsync_DotTagName_ExtractsCorrectly" storage="/home/windpacer/projects/experioncrawler/experioncrawler.tests/bin/debug/net8.0/experioncrawler.tests.dll" id="e0331b39-ee4e-8fe1-50ed-78732fa3841a">
|
||||
<Execution id="4da36672-445e-4369-9d44-360a7b6b8b99" />
|
||||
<TestMethod codeBase="/home/windpacer/projects/ExperionCrawler/ExperionCrawler.Tests/bin/Debug/net8.0/ExperionCrawler.Tests.dll" adapterTypeName="executor://xunit/VsTestRunner2/netcoreapp" className="ExperionCrawler.Tests.TextToSqlServiceTests" name="ParseNaturalLanguageAsync_DotTagName_ExtractsCorrectly" />
|
||||
</UnitTest>
|
||||
<UnitTest name="ExperionCrawler.Tests.TextToSqlServiceTests.ParseNaturalLanguageAsync_Recent24Hours_ExtractsTimeRange" storage="/home/windpacer/projects/experioncrawler/experioncrawler.tests/bin/debug/net8.0/experioncrawler.tests.dll" id="6b5ec284-00db-56ef-7384-a5713f6ab45e">
|
||||
<Execution id="5c45a8b9-2eb2-427c-a731-414f909b7048" />
|
||||
<TestMethod codeBase="/home/windpacer/projects/ExperionCrawler/ExperionCrawler.Tests/bin/Debug/net8.0/ExperionCrawler.Tests.dll" adapterTypeName="executor://xunit/VsTestRunner2/netcoreapp" className="ExperionCrawler.Tests.TextToSqlServiceTests" name="ParseNaturalLanguageAsync_Recent24Hours_ExtractsTimeRange" />
|
||||
</UnitTest>
|
||||
<UnitTest name="ExperionCrawler.Tests.TextToSqlServiceTests.ParseNaturalLanguageAsync_KoreanDate_ExtractsTimeRange" storage="/home/windpacer/projects/experioncrawler/experioncrawler.tests/bin/debug/net8.0/experioncrawler.tests.dll" id="77b9b877-6d45-d631-10c9-19432d683af4">
|
||||
<Execution id="e60f083e-00ce-452d-837d-cfeafda675ee" />
|
||||
<TestMethod codeBase="/home/windpacer/projects/ExperionCrawler/ExperionCrawler.Tests/bin/Debug/net8.0/ExperionCrawler.Tests.dll" adapterTypeName="executor://xunit/VsTestRunner2/netcoreapp" className="ExperionCrawler.Tests.TextToSqlServiceTests" name="ParseNaturalLanguageAsync_KoreanDate_ExtractsTimeRange" />
|
||||
</UnitTest>
|
||||
<UnitTest name="ExperionCrawler.Tests.TextToSqlServiceTests.ParseNaturalLanguageAsync_AverageKeyword_UsesAvgFunction" storage="/home/windpacer/projects/experioncrawler/experioncrawler.tests/bin/debug/net8.0/experioncrawler.tests.dll" id="4cc079cc-8497-c2b1-a13f-371856fe1a6d">
|
||||
<Execution id="6342897b-79d5-45f1-b779-b21463d71914" />
|
||||
<TestMethod codeBase="/home/windpacer/projects/ExperionCrawler/ExperionCrawler.Tests/bin/Debug/net8.0/ExperionCrawler.Tests.dll" adapterTypeName="executor://xunit/VsTestRunner2/netcoreapp" className="ExperionCrawler.Tests.TextToSqlServiceTests" name="ParseNaturalLanguageAsync_AverageKeyword_UsesAvgFunction" />
|
||||
</UnitTest>
|
||||
<UnitTest name="ExperionCrawler.Tests.TextToSqlServiceTests.ParseNaturalLanguageAsync_MaxKeyword_UsesMaxFunction" storage="/home/windpacer/projects/experioncrawler/experioncrawler.tests/bin/debug/net8.0/experioncrawler.tests.dll" id="64e7212f-9c18-5a8c-19cd-01e0da0ea86f">
|
||||
<Execution id="835eeb43-fcf4-44c6-a160-4f9fd35cced8" />
|
||||
<TestMethod codeBase="/home/windpacer/projects/ExperionCrawler/ExperionCrawler.Tests/bin/Debug/net8.0/ExperionCrawler.Tests.dll" adapterTypeName="executor://xunit/VsTestRunner2/netcoreapp" className="ExperionCrawler.Tests.TextToSqlServiceTests" name="ParseNaturalLanguageAsync_MaxKeyword_UsesMaxFunction" />
|
||||
</UnitTest>
|
||||
<UnitTest name="ExperionCrawler.Tests.TextToSqlServiceTests.ParseNaturalLanguageAsync_MultipleTagsWithAbsTimeRange_IncludesAllTags" storage="/home/windpacer/projects/experioncrawler/experioncrawler.tests/bin/debug/net8.0/experioncrawler.tests.dll" id="6c884139-c4ff-db15-9260-81c43984d859">
|
||||
<Execution id="6b62004e-a542-49df-9eaa-8b1d8ebbd96b" />
|
||||
<TestMethod codeBase="/home/windpacer/projects/ExperionCrawler/ExperionCrawler.Tests/bin/Debug/net8.0/ExperionCrawler.Tests.dll" adapterTypeName="executor://xunit/VsTestRunner2/netcoreapp" className="ExperionCrawler.Tests.TextToSqlServiceTests" name="ParseNaturalLanguageAsync_MultipleTagsWithAbsTimeRange_IncludesAllTags" />
|
||||
</UnitTest>
|
||||
<UnitTest name="ExperionCrawler.Tests.TextToSqlServiceTests.ParseNaturalLanguageAsync_MultipleTagsCommaSeparated_IncludesAllTags" storage="/home/windpacer/projects/experioncrawler/experioncrawler.tests/bin/debug/net8.0/experioncrawler.tests.dll" id="dfce6d90-9761-4ca3-ca30-781ba02dbfa4">
|
||||
<Execution id="a892d81e-6c53-49a5-aa88-60a4c3313472" />
|
||||
<TestMethod codeBase="/home/windpacer/projects/ExperionCrawler/ExperionCrawler.Tests/bin/Debug/net8.0/ExperionCrawler.Tests.dll" adapterTypeName="executor://xunit/VsTestRunner2/netcoreapp" className="ExperionCrawler.Tests.TextToSqlServiceTests" name="ParseNaturalLanguageAsync_MultipleTagsCommaSeparated_IncludesAllTags" />
|
||||
</UnitTest>
|
||||
<UnitTest name="ExperionCrawler.Tests.TextToSqlServiceTests.ParseNaturalLanguageAsync_OpcUaNodeId_ExtractsCorrectly" storage="/home/windpacer/projects/experioncrawler/experioncrawler.tests/bin/debug/net8.0/experioncrawler.tests.dll" id="dcefabeb-f8ef-a09c-8b99-566f8d7f5f75">
|
||||
<Execution id="9e9a83b0-c91a-4a52-978c-75690f51a6c8" />
|
||||
<TestMethod codeBase="/home/windpacer/projects/ExperionCrawler/ExperionCrawler.Tests/bin/Debug/net8.0/ExperionCrawler.Tests.dll" adapterTypeName="executor://xunit/VsTestRunner2/netcoreapp" className="ExperionCrawler.Tests.TextToSqlServiceTests" name="ParseNaturalLanguageAsync_OpcUaNodeId_ExtractsCorrectly" />
|
||||
</UnitTest>
|
||||
<UnitTest name="ExperionCrawler.Tests.TextToSqlServiceTests.ParseNaturalLanguageAsync_WithEmptyInput_ThrowsArgumentException" storage="/home/windpacer/projects/experioncrawler/experioncrawler.tests/bin/debug/net8.0/experioncrawler.tests.dll" id="cf1eb7a6-cd60-600b-bfaa-e1f835f77b59">
|
||||
<Execution id="4101239d-abc2-45e3-9aeb-32e835964cf7" />
|
||||
<TestMethod codeBase="/home/windpacer/projects/ExperionCrawler/ExperionCrawler.Tests/bin/Debug/net8.0/ExperionCrawler.Tests.dll" adapterTypeName="executor://xunit/VsTestRunner2/netcoreapp" className="ExperionCrawler.Tests.TextToSqlServiceTests" name="ParseNaturalLanguageAsync_WithEmptyInput_ThrowsArgumentException" />
|
||||
</UnitTest>
|
||||
<UnitTest name="ExperionCrawler.Tests.TextToSqlServiceTests.ParseNaturalLanguageAsync_AfterPattern_ExtractsTimeRange" storage="/home/windpacer/projects/experioncrawler/experioncrawler.tests/bin/debug/net8.0/experioncrawler.tests.dll" id="7ff2886d-a78d-22be-1cf0-6826794967ab">
|
||||
<Execution id="24ec31bb-738d-4cba-968e-ddc8ac40d249" />
|
||||
<TestMethod codeBase="/home/windpacer/projects/ExperionCrawler/ExperionCrawler.Tests/bin/Debug/net8.0/ExperionCrawler.Tests.dll" adapterTypeName="executor://xunit/VsTestRunner2/netcoreapp" className="ExperionCrawler.Tests.TextToSqlServiceTests" name="ParseNaturalLanguageAsync_AfterPattern_ExtractsTimeRange" />
|
||||
</UnitTest>
|
||||
<UnitTest name="ExperionCrawler.Tests.TextToSqlServiceTests.SuggestQueriesAsync_WithEmptyInput_ReturnsAllSuggestions" storage="/home/windpacer/projects/experioncrawler/experioncrawler.tests/bin/debug/net8.0/experioncrawler.tests.dll" id="c0c4b062-d710-d2dc-7fb6-02e20736a39f">
|
||||
<Execution id="6ac7aa3c-408f-47d3-9d4d-ef44ccf8abab" />
|
||||
<TestMethod codeBase="/home/windpacer/projects/ExperionCrawler/ExperionCrawler.Tests/bin/Debug/net8.0/ExperionCrawler.Tests.dll" adapterTypeName="executor://xunit/VsTestRunner2/netcoreapp" className="ExperionCrawler.Tests.TextToSqlServiceTests" name="SuggestQueriesAsync_WithEmptyInput_ReturnsAllSuggestions" />
|
||||
</UnitTest>
|
||||
<UnitTest name="ExperionCrawler.Tests.TextToSqlServiceTests.ParseNaturalLanguageAsync_AvgKeyword_UsesAvgFunction" storage="/home/windpacer/projects/experioncrawler/experioncrawler.tests/bin/debug/net8.0/experioncrawler.tests.dll" id="37e9c535-a3c6-2083-f2c0-987d98ae10fa">
|
||||
<Execution id="cb48f6dc-fa21-4f1d-a8f9-febab7d25fc9" />
|
||||
<TestMethod codeBase="/home/windpacer/projects/ExperionCrawler/ExperionCrawler.Tests/bin/Debug/net8.0/ExperionCrawler.Tests.dll" adapterTypeName="executor://xunit/VsTestRunner2/netcoreapp" className="ExperionCrawler.Tests.TextToSqlServiceTests" name="ParseNaturalLanguageAsync_AvgKeyword_UsesAvgFunction" />
|
||||
</UnitTest>
|
||||
<UnitTest name="ExperionCrawler.Tests.TextToSqlServiceTests.ParseNaturalLanguageAsync_MultipleTagsWithKoreanDescriptions_ExtractsOnlyTags" storage="/home/windpacer/projects/experioncrawler/experioncrawler.tests/bin/debug/net8.0/experioncrawler.tests.dll" id="15e84926-8d29-6d29-d5f1-18dd26763892">
|
||||
<Execution id="7ee38996-5e67-4144-9280-d066fa103dcd" />
|
||||
<TestMethod codeBase="/home/windpacer/projects/ExperionCrawler/ExperionCrawler.Tests/bin/Debug/net8.0/ExperionCrawler.Tests.dll" adapterTypeName="executor://xunit/VsTestRunner2/netcoreapp" className="ExperionCrawler.Tests.TextToSqlServiceTests" name="ParseNaturalLanguageAsync_MultipleTagsWithKoreanDescriptions_ExtractsOnlyTags" />
|
||||
</UnitTest>
|
||||
<UnitTest name="ExperionCrawler.Tests.TextToSqlServiceTests.ParseNaturalLanguageAsync_WithFirstKeyword_GeneratesFirstAggregation" storage="/home/windpacer/projects/experioncrawler/experioncrawler.tests/bin/debug/net8.0/experioncrawler.tests.dll" id="14f39cbb-10de-141a-f514-4230ebe336ed">
|
||||
<Execution id="4248d846-c27e-486b-979b-89e6b55ae83b" />
|
||||
<TestMethod codeBase="/home/windpacer/projects/ExperionCrawler/ExperionCrawler.Tests/bin/Debug/net8.0/ExperionCrawler.Tests.dll" adapterTypeName="executor://xunit/VsTestRunner2/netcoreapp" className="ExperionCrawler.Tests.TextToSqlServiceTests" name="ParseNaturalLanguageAsync_WithFirstKeyword_GeneratesFirstAggregation" />
|
||||
</UnitTest>
|
||||
<UnitTest name="ExperionCrawler.Tests.TextToSqlServiceTests.ParseNaturalLanguageAsync_FromToPattern_ExtractsTimeRange" storage="/home/windpacer/projects/experioncrawler/experioncrawler.tests/bin/debug/net8.0/experioncrawler.tests.dll" id="a588adc0-4f17-3b17-110a-4aa6f024dd3f">
|
||||
<Execution id="2af16c71-455e-47d1-9414-06fdbbb0c9f4" />
|
||||
<TestMethod codeBase="/home/windpacer/projects/ExperionCrawler/ExperionCrawler.Tests/bin/Debug/net8.0/ExperionCrawler.Tests.dll" adapterTypeName="executor://xunit/VsTestRunner2/netcoreapp" className="ExperionCrawler.Tests.TextToSqlServiceTests" name="ParseNaturalLanguageAsync_FromToPattern_ExtractsTimeRange" />
|
||||
</UnitTest>
|
||||
<UnitTest name="ExperionCrawler.Tests.TextToSqlServiceTests.ParseNaturalLanguageAsync_WithTagName_GeneratesSql" storage="/home/windpacer/projects/experioncrawler/experioncrawler.tests/bin/debug/net8.0/experioncrawler.tests.dll" id="453ebd31-b897-172a-47b2-ee9237e423e2">
|
||||
<Execution id="4f8df51a-e50e-4781-9f88-143d0368cb20" />
|
||||
<TestMethod codeBase="/home/windpacer/projects/ExperionCrawler/ExperionCrawler.Tests/bin/Debug/net8.0/ExperionCrawler.Tests.dll" adapterTypeName="executor://xunit/VsTestRunner2/netcoreapp" className="ExperionCrawler.Tests.TextToSqlServiceTests" name="ParseNaturalLanguageAsync_WithTagName_GeneratesSql" />
|
||||
</UnitTest>
|
||||
<UnitTest name="ExperionCrawler.Tests.TextToSqlServiceTests.ParseNaturalLanguageAsync_SpecialCharacters_EscapesTagName" storage="/home/windpacer/projects/experioncrawler/experioncrawler.tests/bin/debug/net8.0/experioncrawler.tests.dll" id="80269386-9ca6-1dd2-59e4-7b56fd7a66ed">
|
||||
<Execution id="d35ec836-f0ec-43c4-ac25-7b50d7dbdba0" />
|
||||
<TestMethod codeBase="/home/windpacer/projects/ExperionCrawler/ExperionCrawler.Tests/bin/Debug/net8.0/ExperionCrawler.Tests.dll" adapterTypeName="executor://xunit/VsTestRunner2/netcoreapp" className="ExperionCrawler.Tests.TextToSqlServiceTests" name="ParseNaturalLanguageAsync_SpecialCharacters_EscapesTagName" />
|
||||
</UnitTest>
|
||||
<UnitTest name="ExperionCrawler.Tests.TextToSqlServiceTests.ParseNaturalLanguageAsync_NoSpaceTagName_UsesWholeInput" storage="/home/windpacer/projects/experioncrawler/experioncrawler.tests/bin/debug/net8.0/experioncrawler.tests.dll" id="675f7860-eb45-3219-27e7-57c7e2e65ee2">
|
||||
<Execution id="5ddb25ec-fe9d-46a7-ac6d-91d8e60c465b" />
|
||||
<TestMethod codeBase="/home/windpacer/projects/ExperionCrawler/ExperionCrawler.Tests/bin/Debug/net8.0/ExperionCrawler.Tests.dll" adapterTypeName="executor://xunit/VsTestRunner2/netcoreapp" className="ExperionCrawler.Tests.TextToSqlServiceTests" name="ParseNaturalLanguageAsync_NoSpaceTagName_UsesWholeInput" />
|
||||
</UnitTest>
|
||||
<UnitTest name="ExperionCrawler.Tests.TextToSqlServiceTests.SuggestQueriesAsync_WithFilter_ReturnsFilteredSuggestions" storage="/home/windpacer/projects/experioncrawler/experioncrawler.tests/bin/debug/net8.0/experioncrawler.tests.dll" id="4c2d002b-4d1c-3c3b-855d-00377118994d">
|
||||
<Execution id="d8210a41-f48e-416c-9e6f-b4b5cd096013" />
|
||||
<TestMethod codeBase="/home/windpacer/projects/ExperionCrawler/ExperionCrawler.Tests/bin/Debug/net8.0/ExperionCrawler.Tests.dll" adapterTypeName="executor://xunit/VsTestRunner2/netcoreapp" className="ExperionCrawler.Tests.TextToSqlServiceTests" name="SuggestQueriesAsync_WithFilter_ReturnsFilteredSuggestions" />
|
||||
</UnitTest>
|
||||
<UnitTest name="ExperionCrawler.Tests.TextToSqlServiceTests.ParseNaturalLanguageAsync_Recent1Month_ExtractsTimeRange" storage="/home/windpacer/projects/experioncrawler/experioncrawler.tests/bin/debug/net8.0/experioncrawler.tests.dll" id="5c3e9ce7-7bd1-deb3-bcf9-286a5a797173">
|
||||
<Execution id="fa8e7505-2eb7-479a-9859-d8d21a234118" />
|
||||
<TestMethod codeBase="/home/windpacer/projects/ExperionCrawler/ExperionCrawler.Tests/bin/Debug/net8.0/ExperionCrawler.Tests.dll" adapterTypeName="executor://xunit/VsTestRunner2/netcoreapp" className="ExperionCrawler.Tests.TextToSqlServiceTests" name="ParseNaturalLanguageAsync_Recent1Month_ExtractsTimeRange" />
|
||||
</UnitTest>
|
||||
<UnitTest name="ExperionCrawler.Tests.TextToSqlServiceTests.ParseNaturalLanguageAsync_NoTimeSpecified_UsesDefault" storage="/home/windpacer/projects/experioncrawler/experioncrawler.tests/bin/debug/net8.0/experioncrawler.tests.dll" id="6bbded63-64a7-bc4b-baa2-42bdfaed94e5">
|
||||
<Execution id="0f19eebe-6bd1-4ae8-817c-8a4c01858476" />
|
||||
<TestMethod codeBase="/home/windpacer/projects/ExperionCrawler/ExperionCrawler.Tests/bin/Debug/net8.0/ExperionCrawler.Tests.dll" adapterTypeName="executor://xunit/VsTestRunner2/netcoreapp" className="ExperionCrawler.Tests.TextToSqlServiceTests" name="ParseNaturalLanguageAsync_NoTimeSpecified_UsesDefault" />
|
||||
</UnitTest>
|
||||
<UnitTest name="ExperionCrawler.Tests.TextToSqlServiceTests.ParseNaturalLanguageAsync_WithMinKeyword_GeneratesMinAggregation" storage="/home/windpacer/projects/experioncrawler/experioncrawler.tests/bin/debug/net8.0/experioncrawler.tests.dll" id="9dcebd05-5954-2a01-645b-410629bd254d">
|
||||
<Execution id="90650188-4846-4d68-bc39-f7a4c8fb9814" />
|
||||
<TestMethod codeBase="/home/windpacer/projects/ExperionCrawler/ExperionCrawler.Tests/bin/Debug/net8.0/ExperionCrawler.Tests.dll" adapterTypeName="executor://xunit/VsTestRunner2/netcoreapp" className="ExperionCrawler.Tests.TextToSqlServiceTests" name="ParseNaturalLanguageAsync_WithMinKeyword_GeneratesMinAggregation" />
|
||||
</UnitTest>
|
||||
<UnitTest name="ExperionCrawler.Tests.TextToSqlServiceTests.ParseNaturalLanguageAsync_Recent7Days_ExtractsTimeRange" storage="/home/windpacer/projects/experioncrawler/experioncrawler.tests/bin/debug/net8.0/experioncrawler.tests.dll" id="03b464c4-872d-93f1-75bf-cc169416edee">
|
||||
<Execution id="0bd3a654-47ed-4a79-a283-1e1e11baf6e4" />
|
||||
<TestMethod codeBase="/home/windpacer/projects/ExperionCrawler/ExperionCrawler.Tests/bin/Debug/net8.0/ExperionCrawler.Tests.dll" adapterTypeName="executor://xunit/VsTestRunner2/netcoreapp" className="ExperionCrawler.Tests.TextToSqlServiceTests" name="ParseNaturalLanguageAsync_Recent7Days_ExtractsTimeRange" />
|
||||
</UnitTest>
|
||||
<UnitTest name="ExperionCrawler.Tests.TextToSqlServiceTests.ParseNaturalLanguageAsync_WithLastKeyword_GeneratesLastAggregation" storage="/home/windpacer/projects/experioncrawler/experioncrawler.tests/bin/debug/net8.0/experioncrawler.tests.dll" id="73098855-7d72-81ed-193c-c79d76370f85">
|
||||
<Execution id="801f328a-0b75-4735-9e21-37e6d04314ba" />
|
||||
<TestMethod codeBase="/home/windpacer/projects/ExperionCrawler/ExperionCrawler.Tests/bin/Debug/net8.0/ExperionCrawler.Tests.dll" adapterTypeName="executor://xunit/VsTestRunner2/netcoreapp" className="ExperionCrawler.Tests.TextToSqlServiceTests" name="ParseNaturalLanguageAsync_WithLastKeyword_GeneratesLastAggregation" />
|
||||
</UnitTest>
|
||||
<UnitTest name="ExperionCrawler.Tests.TextToSqlServiceTests.ParseNaturalLanguageAsync_DefaultAggregationIsLast" storage="/home/windpacer/projects/experioncrawler/experioncrawler.tests/bin/debug/net8.0/experioncrawler.tests.dll" id="750cf791-c506-cfa6-4deb-79897ece87ff">
|
||||
<Execution id="de69eaaf-6366-4889-94c1-d0281deb012c" />
|
||||
<TestMethod codeBase="/home/windpacer/projects/ExperionCrawler/ExperionCrawler.Tests/bin/Debug/net8.0/ExperionCrawler.Tests.dll" adapterTypeName="executor://xunit/VsTestRunner2/netcoreapp" className="ExperionCrawler.Tests.TextToSqlServiceTests" name="ParseNaturalLanguageAsync_DefaultAggregationIsLast" />
|
||||
</UnitTest>
|
||||
<UnitTest name="ExperionCrawler.Tests.TextToSqlServiceTests.ParseNaturalLanguageAsync_SimpleTagName_ExtractsCorrectly" storage="/home/windpacer/projects/experioncrawler/experioncrawler.tests/bin/debug/net8.0/experioncrawler.tests.dll" id="df534781-ce1f-66fc-3790-0b8e5eae75df">
|
||||
<Execution id="0c3ab594-4d65-4c0b-bcca-a10ee0f58a0f" />
|
||||
<TestMethod codeBase="/home/windpacer/projects/ExperionCrawler/ExperionCrawler.Tests/bin/Debug/net8.0/ExperionCrawler.Tests.dll" adapterTypeName="executor://xunit/VsTestRunner2/netcoreapp" className="ExperionCrawler.Tests.TextToSqlServiceTests" name="ParseNaturalLanguageAsync_SimpleTagName_ExtractsCorrectly" />
|
||||
</UnitTest>
|
||||
<UnitTest name="ExperionCrawler.Tests.TextToSqlServiceTests.ParseNaturalLanguageAsync_TodayAmPmRange_ExtractsTimeRange" storage="/home/windpacer/projects/experioncrawler/experioncrawler.tests/bin/debug/net8.0/experioncrawler.tests.dll" id="22b5066e-140f-d997-79ae-a2cc60ca7ac9">
|
||||
<Execution id="7c5468e1-7168-4a8a-a0ae-ad6784346495" />
|
||||
<TestMethod codeBase="/home/windpacer/projects/ExperionCrawler/ExperionCrawler.Tests/bin/Debug/net8.0/ExperionCrawler.Tests.dll" adapterTypeName="executor://xunit/VsTestRunner2/netcoreapp" className="ExperionCrawler.Tests.TextToSqlServiceTests" name="ParseNaturalLanguageAsync_TodayAmPmRange_ExtractsTimeRange" />
|
||||
</UnitTest>
|
||||
<UnitTest name="ExperionCrawler.Tests.TextToSqlServiceTests.ParseNaturalLanguageAsync_WithMaxKeyword_GeneratesMaxAggregation" storage="/home/windpacer/projects/experioncrawler/experioncrawler.tests/bin/debug/net8.0/experioncrawler.tests.dll" id="cea17860-3597-3e21-1465-62e77aee9cc6">
|
||||
<Execution id="9627a0d8-e645-4db1-9ed7-54afb8d37be0" />
|
||||
<TestMethod codeBase="/home/windpacer/projects/ExperionCrawler/ExperionCrawler.Tests/bin/Debug/net8.0/ExperionCrawler.Tests.dll" adapterTypeName="executor://xunit/VsTestRunner2/netcoreapp" className="ExperionCrawler.Tests.TextToSqlServiceTests" name="ParseNaturalLanguageAsync_WithMaxKeyword_GeneratesMaxAggregation" />
|
||||
</UnitTest>
|
||||
<UnitTest name="ExperionCrawler.Tests.TextToSqlServiceTests.ParseNaturalLanguageAsync_NoAggregateKeyword_UsesLastFunction" storage="/home/windpacer/projects/experioncrawler/experioncrawler.tests/bin/debug/net8.0/experioncrawler.tests.dll" id="0a6fd87b-3482-8683-9f78-54911a84c958">
|
||||
<Execution id="be8411f0-aa2c-4cab-a30c-584e1d59a747" />
|
||||
<TestMethod codeBase="/home/windpacer/projects/ExperionCrawler/ExperionCrawler.Tests/bin/Debug/net8.0/ExperionCrawler.Tests.dll" adapterTypeName="executor://xunit/VsTestRunner2/netcoreapp" className="ExperionCrawler.Tests.TextToSqlServiceTests" name="ParseNaturalLanguageAsync_NoAggregateKeyword_UsesLastFunction" />
|
||||
</UnitTest>
|
||||
<UnitTest name="ExperionCrawler.Tests.TextToSqlServiceTests.ParseNaturalLanguageAsync_MinKeyword_UsesMinFunction" storage="/home/windpacer/projects/experioncrawler/experioncrawler.tests/bin/debug/net8.0/experioncrawler.tests.dll" id="7df9fd70-57c9-fe80-db0e-4c9bb3029efe">
|
||||
<Execution id="cb24fe3d-f7fd-4740-94b6-490e0d7bab76" />
|
||||
<TestMethod codeBase="/home/windpacer/projects/ExperionCrawler/ExperionCrawler.Tests/bin/Debug/net8.0/ExperionCrawler.Tests.dll" adapterTypeName="executor://xunit/VsTestRunner2/netcoreapp" className="ExperionCrawler.Tests.TextToSqlServiceTests" name="ParseNaturalLanguageAsync_MinKeyword_UsesMinFunction" />
|
||||
</UnitTest>
|
||||
<UnitTest name="ExperionCrawler.Tests.TextToSqlServiceTests.ParseNaturalLanguageAsync_Recent1Hour_ExtractsTimeRange" storage="/home/windpacer/projects/experioncrawler/experioncrawler.tests/bin/debug/net8.0/experioncrawler.tests.dll" id="16a4f7ee-8c4f-8b19-bf63-11f177c56cc6">
|
||||
<Execution id="83fce793-2e88-4b7d-91f4-af2a95592bbb" />
|
||||
<TestMethod codeBase="/home/windpacer/projects/ExperionCrawler/ExperionCrawler.Tests/bin/Debug/net8.0/ExperionCrawler.Tests.dll" adapterTypeName="executor://xunit/VsTestRunner2/netcoreapp" className="ExperionCrawler.Tests.TextToSqlServiceTests" name="ParseNaturalLanguageAsync_Recent1Hour_ExtractsTimeRange" />
|
||||
</UnitTest>
|
||||
</TestDefinitions>
|
||||
<TestEntries>
|
||||
<TestEntry testId="5c3e9ce7-7bd1-deb3-bcf9-286a5a797173" executionId="fa8e7505-2eb7-479a-9859-d8d21a234118" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
|
||||
<TestEntry testId="4cc079cc-8497-c2b1-a13f-371856fe1a6d" executionId="6342897b-79d5-45f1-b779-b21463d71914" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
|
||||
<TestEntry testId="453ebd31-b897-172a-47b2-ee9237e423e2" executionId="4f8df51a-e50e-4781-9f88-143d0368cb20" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
|
||||
<TestEntry testId="80269386-9ca6-1dd2-59e4-7b56fd7a66ed" executionId="d35ec836-f0ec-43c4-ac25-7b50d7dbdba0" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
|
||||
<TestEntry testId="8df30334-5f76-38b5-25e9-f6812aa6cdff" executionId="6f313979-2da5-4dfe-9c34-27e3ce65743b" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
|
||||
<TestEntry testId="6b5ec284-00db-56ef-7384-a5713f6ab45e" executionId="5c45a8b9-2eb2-427c-a731-414f909b7048" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
|
||||
<TestEntry testId="e0331b39-ee4e-8fe1-50ed-78732fa3841a" executionId="4da36672-445e-4369-9d44-360a7b6b8b99" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
|
||||
<TestEntry testId="9dcebd05-5954-2a01-645b-410629bd254d" executionId="90650188-4846-4d68-bc39-f7a4c8fb9814" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
|
||||
<TestEntry testId="4c2d002b-4d1c-3c3b-855d-00377118994d" executionId="d8210a41-f48e-416c-9e6f-b4b5cd096013" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
|
||||
<TestEntry testId="15e84926-8d29-6d29-d5f1-18dd26763892" executionId="7ee38996-5e67-4144-9280-d066fa103dcd" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
|
||||
<TestEntry testId="cf1eb7a6-cd60-600b-bfaa-e1f835f77b59" executionId="4101239d-abc2-45e3-9aeb-32e835964cf7" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
|
||||
<TestEntry testId="14f39cbb-10de-141a-f514-4230ebe336ed" executionId="4248d846-c27e-486b-979b-89e6b55ae83b" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
|
||||
<TestEntry testId="df534781-ce1f-66fc-3790-0b8e5eae75df" executionId="0c3ab594-4d65-4c0b-bcca-a10ee0f58a0f" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
|
||||
<TestEntry testId="cea17860-3597-3e21-1465-62e77aee9cc6" executionId="9627a0d8-e645-4db1-9ed7-54afb8d37be0" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
|
||||
<TestEntry testId="675f7860-eb45-3219-27e7-57c7e2e65ee2" executionId="5ddb25ec-fe9d-46a7-ac6d-91d8e60c465b" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
|
||||
<TestEntry testId="c0c4b062-d710-d2dc-7fb6-02e20736a39f" executionId="6ac7aa3c-408f-47d3-9d4d-ef44ccf8abab" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
|
||||
<TestEntry testId="03b464c4-872d-93f1-75bf-cc169416edee" executionId="0bd3a654-47ed-4a79-a283-1e1e11baf6e4" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
|
||||
<TestEntry testId="a588adc0-4f17-3b17-110a-4aa6f024dd3f" executionId="2af16c71-455e-47d1-9414-06fdbbb0c9f4" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
|
||||
<TestEntry testId="dfce6d90-9761-4ca3-ca30-781ba02dbfa4" executionId="a892d81e-6c53-49a5-aa88-60a4c3313472" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
|
||||
<TestEntry testId="77b9b877-6d45-d631-10c9-19432d683af4" executionId="e60f083e-00ce-452d-837d-cfeafda675ee" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
|
||||
<TestEntry testId="750cf791-c506-cfa6-4deb-79897ece87ff" executionId="de69eaaf-6366-4889-94c1-d0281deb012c" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
|
||||
<TestEntry testId="73098855-7d72-81ed-193c-c79d76370f85" executionId="801f328a-0b75-4735-9e21-37e6d04314ba" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
|
||||
<TestEntry testId="7ff2886d-a78d-22be-1cf0-6826794967ab" executionId="24ec31bb-738d-4cba-968e-ddc8ac40d249" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
|
||||
<TestEntry testId="64e7212f-9c18-5a8c-19cd-01e0da0ea86f" executionId="835eeb43-fcf4-44c6-a160-4f9fd35cced8" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
|
||||
<TestEntry testId="16a4f7ee-8c4f-8b19-bf63-11f177c56cc6" executionId="83fce793-2e88-4b7d-91f4-af2a95592bbb" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
|
||||
<TestEntry testId="6bbded63-64a7-bc4b-baa2-42bdfaed94e5" executionId="0f19eebe-6bd1-4ae8-817c-8a4c01858476" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
|
||||
<TestEntry testId="0a6fd87b-3482-8683-9f78-54911a84c958" executionId="be8411f0-aa2c-4cab-a30c-584e1d59a747" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
|
||||
<TestEntry testId="dcefabeb-f8ef-a09c-8b99-566f8d7f5f75" executionId="9e9a83b0-c91a-4a52-978c-75690f51a6c8" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
|
||||
<TestEntry testId="6c884139-c4ff-db15-9260-81c43984d859" executionId="6b62004e-a542-49df-9eaa-8b1d8ebbd96b" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
|
||||
<TestEntry testId="37e9c535-a3c6-2083-f2c0-987d98ae10fa" executionId="cb48f6dc-fa21-4f1d-a8f9-febab7d25fc9" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
|
||||
<TestEntry testId="7df9fd70-57c9-fe80-db0e-4c9bb3029efe" executionId="cb24fe3d-f7fd-4740-94b6-490e0d7bab76" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
|
||||
<TestEntry testId="22b5066e-140f-d997-79ae-a2cc60ca7ac9" executionId="7c5468e1-7168-4a8a-a0ae-ad6784346495" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
|
||||
</TestEntries>
|
||||
<TestLists>
|
||||
<TestList name="Results Not in a List" id="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
|
||||
<TestList name="All Loaded Results" id="19431567-8539-422a-85d7-44ee4e166bda" />
|
||||
</TestLists>
|
||||
<ResultSummary outcome="Completed">
|
||||
<Counters total="32" executed="32" passed="32" failed="0" error="0" timeout="0" aborted="0" inconclusive="0" passedButRunAborted="0" notRunnable="0" notExecuted="0" disconnected="0" warning="0" completed="0" inProgress="0" pending="0" />
|
||||
<Output>
|
||||
<StdOut>[xUnit.net 00:00:00.00] xUnit.net VSTest Adapter v2.4.5+1caef2f33e (64-bit .NET 8.0.26)
|
||||
[xUnit.net 00:00:00.35] Discovering: ExperionCrawler.Tests
|
||||
[xUnit.net 00:00:00.38] Discovered: ExperionCrawler.Tests
|
||||
[xUnit.net 00:00:00.38] Starting: ExperionCrawler.Tests
|
||||
[xUnit.net 00:00:00.45] Finished: ExperionCrawler.Tests
|
||||
</StdOut>
|
||||
</Output>
|
||||
</ResultSummary>
|
||||
</TestRun>
|
||||
450
ExperionCrawler.Tests/TextToSqlServiceTests.cs
Normal file
450
ExperionCrawler.Tests/TextToSqlServiceTests.cs
Normal file
@@ -0,0 +1,450 @@
|
||||
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
|
||||
}
|
||||
533
ExperionCrawler.Tests/TextToSqlTest.cs
Normal file
533
ExperionCrawler.Tests/TextToSqlTest.cs
Normal file
@@ -0,0 +1,533 @@
|
||||
using ExperionCrawler.Core.Application.DTOs;
|
||||
using ExperionCrawler.Core.Application.Services;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Xunit;
|
||||
|
||||
namespace ExperionCrawler.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// TextToSqlService 통합 테스트
|
||||
/// task_state.md의 매핑표 기반으로 작성된 테스트 프로그램
|
||||
/// </summary>
|
||||
public class TextToSqlTest
|
||||
{
|
||||
private readonly TextToSqlService _service;
|
||||
private readonly ILogger<TextToSqlService> _logger;
|
||||
|
||||
public TextToSqlTest()
|
||||
{
|
||||
// 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 1. SQL 생성 요청 → 응답 형식 검증
|
||||
|
||||
[Fact]
|
||||
public void ParseNaturalLanguageAsync_WithValidInput_ReturnsValidSqlFormat()
|
||||
{
|
||||
// Arrange
|
||||
var input = "FICQ-6101.PV 최근 1시간 평균";
|
||||
|
||||
// Act
|
||||
var sql = _service.ParseNaturalLanguageAsync(input).GetAwaiter().GetResult();
|
||||
|
||||
// Assert - 응답 형식 검증
|
||||
Assert.NotNull(sql);
|
||||
Assert.StartsWith("SELECT", sql, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.Contains("FROM history_table", sql, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.Contains("date_trunc", sql, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.Contains("GROUP BY", sql, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.Contains("ORDER BY", sql, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseNaturalLanguageAsync_WithMaxKeyword_ReturnsMaxFunction()
|
||||
{
|
||||
// Arrange
|
||||
var input = "FICQ-6101.PV 최근 1시간 최대값";
|
||||
|
||||
// Act
|
||||
var sql = _service.ParseNaturalLanguageAsync(input).GetAwaiter().GetResult();
|
||||
|
||||
// Assert
|
||||
Assert.Contains("max", sql, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseNaturalLanguageAsync_WithMinKeyword_ReturnsMinFunction()
|
||||
{
|
||||
// Arrange
|
||||
var input = "FICQ-6101.PV 최근 1시간 최솟값";
|
||||
|
||||
// Act
|
||||
var sql = _service.ParseNaturalLanguageAsync(input).GetAwaiter().GetResult();
|
||||
|
||||
// Assert
|
||||
Assert.Contains("min", sql, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseNaturalLanguageAsync_WithFirstKeyword_ReturnsFirstFunction()
|
||||
{
|
||||
// Arrange
|
||||
var input = "FICQ-6101.PV 초기 값";
|
||||
|
||||
// Act
|
||||
var sql = _service.ParseNaturalLanguageAsync(input).GetAwaiter().GetResult();
|
||||
|
||||
// Assert
|
||||
Assert.Contains("first", sql, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseNaturalLanguageAsync_WithLastKeyword_ReturnsLastFunction()
|
||||
{
|
||||
// Arrange
|
||||
var input = "FICQ-6101.PV 마지막 값";
|
||||
|
||||
// Act
|
||||
var sql = _service.ParseNaturalLanguageAsync(input).GetAwaiter().GetResult();
|
||||
|
||||
// Assert
|
||||
Assert.Contains("last", sql, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseNaturalLanguageAsync_WithAvgKeyword_ReturnsAvgFunction()
|
||||
{
|
||||
// Arrange
|
||||
var input = "FICQ-6101.PV 최근 1시간 평균";
|
||||
|
||||
// Act
|
||||
var sql = _service.ParseNaturalLanguageAsync(input).GetAwaiter().GetResult();
|
||||
|
||||
// Assert
|
||||
Assert.Contains("avg", sql, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseNaturalLanguageAsync_WithMultipleTags_ReturnsAllTagsInSql()
|
||||
{
|
||||
// Arrange
|
||||
var input = "p-6102.hzset.fieldvalue, ficq-6113.op 최근 2시간 값";
|
||||
|
||||
// Act
|
||||
var sql = _service.ParseNaturalLanguageAsync(input).GetAwaiter().GetResult();
|
||||
|
||||
// Assert
|
||||
Assert.Contains("'p-6102.hzset.fieldvalue'", sql);
|
||||
Assert.Contains("'ficq-6113.op'", sql);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseNaturalLanguageAsync_WithOpcUaNodeId_ReturnsOpcUaFormat()
|
||||
{
|
||||
// Arrange
|
||||
var input = "ns=2;s=Reactor.Temperature 최근 1시간 평균";
|
||||
|
||||
// Act
|
||||
var sql = _service.ParseNaturalLanguageAsync(input).GetAwaiter().GetResult();
|
||||
|
||||
// Assert
|
||||
Assert.Contains("'ns=2;s=Reactor.Temperature'", sql);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 2. 생성된 SQL 실행 → TimescaleDB 결과 반환 확인
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteQueryAsync_WithValidSql_ReturnsResultWithColumnsAndRows()
|
||||
{
|
||||
// Arrange
|
||||
var sql = "SELECT date_trunc('minute', recorded_at) AS bucket, tagname, last(value::double precision, recorded_at) AS result FROM history_table WHERE tagname IN ('FICQ-6101.PV') AND recorded_at > NOW() - INTERVAL '1 hour' GROUP BY 1, 2 ORDER BY 1, 2 LIMIT 10";
|
||||
|
||||
// Act
|
||||
var result = await _service.ExecuteQueryAsync(sql, 10);
|
||||
|
||||
// Assert - TimescaleDB 결과 반환 확인
|
||||
Assert.True(result.Success);
|
||||
Assert.NotNull(result.Columns);
|
||||
Assert.True(result.Columns.Count > 0);
|
||||
Assert.Contains("bucket", result.Columns[0], StringComparison.OrdinalIgnoreCase);
|
||||
Assert.Contains("tagname", result.Columns[1], StringComparison.OrdinalIgnoreCase);
|
||||
Assert.Contains("result", result.Columns[2], StringComparison.OrdinalIgnoreCase);
|
||||
Assert.NotNull(result.Rows);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteQueryAsync_WithLimit_ReturnsLimitedRows()
|
||||
{
|
||||
// Arrange
|
||||
var sql = "SELECT date_trunc('minute', recorded_at) AS bucket, tagname, last(value::double precision, recorded_at) AS result FROM history_table WHERE tagname IN ('FICQ-6101.PV') AND recorded_at > NOW() - INTERVAL '1 hour' GROUP BY 1, 2 ORDER BY 1, 2";
|
||||
|
||||
// Act
|
||||
var result = await _service.ExecuteQueryAsync(sql, 5);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.Success);
|
||||
Assert.True(result.Rows.Count <= 5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteQueryAsync_WithInvalidSql_ReturnsError()
|
||||
{
|
||||
// Arrange
|
||||
var sql = "SELECT * FROM invalid_table_that_does_not_exist";
|
||||
|
||||
// Act
|
||||
var result = await _service.ExecuteQueryAsync(sql);
|
||||
|
||||
// Assert
|
||||
Assert.False(result.Success);
|
||||
Assert.NotNull(result.Error);
|
||||
Assert.Contains("PostgreSQL 오류", result.Error);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteQueryAsync_WithSqlInjectionAttempt_ReturnsError()
|
||||
{
|
||||
// Arrange
|
||||
var sql = "SELECT * FROM history_table WHERE tagname = 'FICQ-6101.PV' OR '1'='1'";
|
||||
|
||||
// Act
|
||||
var result = await _service.ExecuteQueryAsync(sql);
|
||||
|
||||
// Assert
|
||||
Assert.False(result.Success);
|
||||
Assert.NotNull(result.Error);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 3. 빈 입력 / 잘못된 입력 → 에러 핸들링
|
||||
|
||||
[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_WithWhitespaceOnly_ThrowsArgumentException()
|
||||
{
|
||||
// Arrange & Act & Assert
|
||||
await Assert.ThrowsAsync<ArgumentException>(() => _service.ParseNaturalLanguageAsync(" "));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ParseNaturalLanguageAsync_WithOnlyTimeKeyword_ThrowsArgumentException()
|
||||
{
|
||||
// Arrange & Act & Assert
|
||||
await Assert.ThrowsAsync<ArgumentException>(() => _service.ParseNaturalLanguageAsync("최근 1시간"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ParseNaturalLanguageAsync_WithOnlyDescription_ThrowsArgumentException()
|
||||
{
|
||||
// Arrange & Act & Assert
|
||||
await Assert.ThrowsAsync<ArgumentException>(() => _service.ParseNaturalLanguageAsync("온도 값"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteQueryAsync_WithEmptySql_ThrowsException()
|
||||
{
|
||||
// Arrange
|
||||
var sql = "";
|
||||
|
||||
// Act & Assert
|
||||
await Assert.ThrowsAsync<Exception>(() => _service.ExecuteQueryAsync(sql));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteQueryAsync_WithNullSql_ThrowsException()
|
||||
{
|
||||
// Arrange
|
||||
string? sql = null;
|
||||
|
||||
// Act & Assert
|
||||
await Assert.ThrowsAsync<Exception>(() => _service.ExecuteQueryAsync(sql!));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteQueryAsync_WithInvalidTagInSql_ReturnsError()
|
||||
{
|
||||
// Arrange
|
||||
var sql = "SELECT date_trunc('minute', recorded_at) AS bucket, tagname, last(value::double precision, recorded_at) AS result FROM history_table WHERE tagname IN ('INVALID_TAG_12345') AND recorded_at > NOW() - INTERVAL '1 hour' GROUP BY 1, 2 ORDER BY 1, 2 LIMIT 10";
|
||||
|
||||
// Act
|
||||
var result = await _service.ExecuteQueryAsync(sql, 10);
|
||||
|
||||
// Assert
|
||||
Assert.False(result.Success);
|
||||
Assert.NotNull(result.Error);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 4. 한국어 자연어 입력 → SQL 변환 정확도
|
||||
|
||||
[Fact]
|
||||
public async Task ParseNaturalLanguageAsync_KoreanRecent1Hour_ReturnsCorrectInterval()
|
||||
{
|
||||
// Arrange
|
||||
var input = "FICQ-6101.PV 최근 1시간 평균";
|
||||
|
||||
// Act
|
||||
var sql = _service.ParseNaturalLanguageAsync(input).GetAwaiter().GetResult();
|
||||
|
||||
// Assert
|
||||
Assert.Contains("INTERVAL '1 hour'", sql);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ParseNaturalLanguageAsync_KoreanRecent24Hours_ReturnsCorrectInterval()
|
||||
{
|
||||
// Arrange
|
||||
var input = "FICQ-6101.PV 최근 24시간 최대값";
|
||||
|
||||
// Act
|
||||
var sql = _service.ParseNaturalLanguageAsync(input).GetAwaiter().GetResult();
|
||||
|
||||
// Assert
|
||||
Assert.Contains("INTERVAL '24 hours'", sql);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ParseNaturalLanguageAsync_KoreanRecent7Days_ReturnsCorrectInterval()
|
||||
{
|
||||
// Arrange
|
||||
var input = "FICQ-6101.PV 최근 7일 최소값";
|
||||
|
||||
// Act
|
||||
var sql = _service.ParseNaturalLanguageAsync(input).GetAwaiter().GetResult();
|
||||
|
||||
// Assert
|
||||
Assert.Contains("INTERVAL '7 days'", sql);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ParseNaturalLanguageAsync_KoreanRecent1Month_ReturnsCorrectInterval()
|
||||
{
|
||||
// Arrange
|
||||
var input = "FICQ-6101.PV 최근 1개월 평균";
|
||||
|
||||
// Act
|
||||
var sql = _service.ParseNaturalLanguageAsync(input).GetAwaiter().GetResult();
|
||||
|
||||
// Assert
|
||||
Assert.Contains("INTERVAL '30 days'", sql);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ParseNaturalLanguageAsync_KoreanDateRange_ReturnsTimeCondition()
|
||||
{
|
||||
// Arrange
|
||||
var input = "p-6102.hzset.fieldvalue 2026년 4월 13일 부터 4월 14일 까지";
|
||||
|
||||
// Act
|
||||
var sql = _service.ParseNaturalLanguageAsync(input).GetAwaiter().GetResult();
|
||||
|
||||
// Assert - 절대 범위 조건이 포함되어야 함
|
||||
Assert.Contains("time", sql, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.Contains(">=", sql, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.Contains("AND", sql, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.Contains("<", sql, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ParseNaturalLanguageAsync_KoreanFromToPattern_ReturnsTimeCondition()
|
||||
{
|
||||
// Arrange
|
||||
var input = "FICQ-6101.PV 4월 1일 부터 4월 7일 까지 평균";
|
||||
|
||||
// Act
|
||||
var sql = _service.ParseNaturalLanguageAsync(input).GetAwaiter().GetResult();
|
||||
|
||||
// Assert - 절대 범위 조건이 포함되어야 함
|
||||
Assert.Contains("time", sql, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.Contains(">=", sql, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ParseNaturalLanguageAsync_KoreanAmPmRange_ReturnsTimeCondition()
|
||||
{
|
||||
// Arrange
|
||||
var input = "FICQ-6101.PV 오전 9시부터 오후 6시까지 평균";
|
||||
|
||||
// Act
|
||||
var sql = _service.ParseNaturalLanguageAsync(input).GetAwaiter().GetResult();
|
||||
|
||||
// Assert - 절대 범위 조건이 포함되어야 함
|
||||
Assert.Contains("time", sql, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.Contains(">=", sql, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.Contains("AND", sql, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ParseNaturalLanguageAsync_KoreanAfterPattern_ReturnsTimeCondition()
|
||||
{
|
||||
// Arrange
|
||||
var input = "FICQ-6101.PV 오늘 오후 2시 이후 값";
|
||||
|
||||
// Act
|
||||
var sql = _service.ParseNaturalLanguageAsync(input).GetAwaiter().GetResult();
|
||||
|
||||
// Assert - 시작 시간 조건이 포함되어야 함
|
||||
Assert.Contains("time", sql, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.Contains(">=", sql, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ParseNaturalLanguageAsync_KoreanWithDescription_ExtractsOnlyTagName()
|
||||
{
|
||||
// Arrange
|
||||
var input = "temp-001 온도, pressure-002 압력 최근 1시간 평균";
|
||||
|
||||
// Act
|
||||
var sql = _service.ParseNaturalLanguageAsync(input).GetAwaiter().GetResult();
|
||||
|
||||
// Assert - 한국어 설명은 제거되고 태그명만 포함되어야 함
|
||||
Assert.Contains("'temp-001'", sql);
|
||||
Assert.Contains("'pressure-002'", sql);
|
||||
Assert.DoesNotContain("온도", sql);
|
||||
Assert.DoesNotContain("압력", sql);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ParseNaturalLanguageAsync_KoreanWithTimeKeyword_ExtractsOnlyTagName()
|
||||
{
|
||||
// Arrange
|
||||
var input = "FICQ-6101.PV 최근 1시간 평균";
|
||||
|
||||
// Act
|
||||
var sql = _service.ParseNaturalLanguageAsync(input).GetAwaiter().GetResult();
|
||||
|
||||
// Assert - "최근" 키워드는 제거되고 태그명만 포함되어야 함
|
||||
Assert.Contains("'FICQ-6101.PV'", sql);
|
||||
Assert.DoesNotContain("최근", sql);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ParseNaturalLanguageAsync_KoreanWithTimePattern_ExtractsOnlyTagName()
|
||||
{
|
||||
// Arrange
|
||||
var input = "FICQ-6101.PV 1시간 평균";
|
||||
|
||||
// Act
|
||||
var sql = _service.ParseNaturalLanguageAsync(input).GetAwaiter().GetResult();
|
||||
|
||||
// Assert - "1시간" 패턴은 제거되고 태그명만 포함되어야 함
|
||||
Assert.Contains("'FICQ-6101.PV'", sql);
|
||||
Assert.DoesNotContain("1시간", sql);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ParseNaturalLanguageAsync_KoreanWithTagKeyword_ExtractsOnlyTagName()
|
||||
{
|
||||
// Arrange
|
||||
var input = "데이터 중 aia-131.sp 최근 1시간 평균";
|
||||
|
||||
// Act
|
||||
var sql = _service.ParseNaturalLanguageAsync(input).GetAwaiter().GetResult();
|
||||
|
||||
// Assert - "데이터 중" 키워드는 제거되고 태그명만 포함되어야 함
|
||||
Assert.Contains("'aia-131.sp'", sql);
|
||||
Assert.DoesNotContain("데이터", sql);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ParseNaturalLanguageAsync_KoreanWithMiddleKeyword_ExtractsOnlyTagName()
|
||||
{
|
||||
// Arrange
|
||||
var input = "데이터 중 aia-131.sp 최근 1시간 평균";
|
||||
|
||||
// Act
|
||||
var sql = _service.ParseNaturalLanguageAsync(input).GetAwaiter().GetResult();
|
||||
|
||||
// Assert - "중" 키워드 이후 태그명만 추출되어야 함
|
||||
Assert.Contains("'aia-131.sp'", sql);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ParseNaturalLanguageAsync_KoreanWithDotTagName_ExtractsCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var input = "p-6102.hzset.fieldvalue 2026년 4월 13일 부터 현재까지의 값 표시";
|
||||
|
||||
// Act
|
||||
var sql = _service.ParseNaturalLanguageAsync(input).GetAwaiter().GetResult();
|
||||
|
||||
// Assert
|
||||
Assert.Contains("'p-6102.hzset.fieldvalue'", sql);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ParseNaturalLanguageAsync_KoreanWithNoSpaceTagName_UsesWholeInput()
|
||||
{
|
||||
// Arrange
|
||||
var input = "FICQ-6101.PV";
|
||||
|
||||
// Act
|
||||
var sql = _service.ParseNaturalLanguageAsync(input).GetAwaiter().GetResult();
|
||||
|
||||
// Assert
|
||||
Assert.Contains("'FICQ-6101.PV'", sql);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ParseNaturalLanguageAsync_KoreanWithNoTimeSpecified_UsesDefault()
|
||||
{
|
||||
// Arrange
|
||||
var input = "FICQ-6101.PV";
|
||||
|
||||
// Act
|
||||
var sql = _service.ParseNaturalLanguageAsync(input).GetAwaiter().GetResult();
|
||||
|
||||
// Assert - default time bucket should be "5 min"
|
||||
Assert.Contains("5 min", sql);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ParseNaturalLanguageAsync_KoreanWithAverageKeyword_UsesAvgFunction()
|
||||
{
|
||||
// Arrange
|
||||
var input = "FICQ-6101.PV 최근 1시간 average";
|
||||
|
||||
// Act
|
||||
var sql = _service.ParseNaturalLanguageAsync(input).GetAwaiter().GetResult();
|
||||
|
||||
// Assert
|
||||
Assert.Contains("avg", sql, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ParseNaturalLanguageAsync_KoreanWithMaxKeyword_UsesMaxFunction()
|
||||
{
|
||||
// Arrange
|
||||
var input = "FICQ-6101.PV 최근 1시간 최대";
|
||||
|
||||
// Act
|
||||
var sql = _service.ParseNaturalLanguageAsync(input).GetAwaiter().GetResult();
|
||||
|
||||
// Assert
|
||||
Assert.Contains("max", sql, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
10
ExperionCrawler.Tests/UnitTest1.cs
Normal file
10
ExperionCrawler.Tests/UnitTest1.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace ExperionCrawler.Tests;
|
||||
|
||||
public class UnitTest1
|
||||
{
|
||||
[Fact]
|
||||
public void Test1()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user