[Developer] #567 show recent SQL cursor candidates
This commit is contained in:
@@ -152,4 +152,6 @@ Oracle은 VPD의 내부 rewrite 결과를 별도 최종 SQL 문자열로 `V$SQL`
|
|||||||
|
|
||||||
최근 SQL 매칭은 최신 10건과 보호 객체명으로 고르므로, 같은 객체를 동시에 호출하는 운영 환경에서는 다른 요청 cursor가 선택될 가능성이 있다. 현재 UI는 이를 “최근 실행 증적”으로 명시한다. 요청별 완전 상관이 필요해지면 ORDS Handler에 검증 요청 ID를 주입해 고유 SQL comment/module-action으로 cursor를 추적하는 후속 작업으로 확장한다.
|
최근 SQL 매칭은 최신 10건과 보호 객체명으로 고르므로, 같은 객체를 동시에 호출하는 운영 환경에서는 다른 요청 cursor가 선택될 가능성이 있다. 현재 UI는 이를 “최근 실행 증적”으로 명시한다. 요청별 완전 상관이 필요해지면 ORDS Handler에 검증 요청 ID를 주입해 고유 SQL comment/module-action으로 cursor를 추적하는 후속 작업으로 확장한다.
|
||||||
|
|
||||||
|
매칭에 실패해도 후보를 버리지 않는다. 화면은 최신 10건의 `SQL_ID`, child cursor, parsing schema, 마지막 실행 시각, 원문 SQL을 그대로 보이며, Java가 보호 객체명과 일치시킨 후보에는 별도 배지를 표시한다. 운영자는 이 목록에서 실제 Handler SQL의 형태를 직접 확인할 수 있다.
|
||||||
|
|
||||||
실행 증적 연결에는 최소한 `V$SQL`과 `DBMS_XPLAN.DISPLAY_CURSOR`를 조회할 수 있는 catalog 권한이 필요하다. 증적은 ORDS parsing schema가 아니라 화면을 표시하는 `BACKOFFICE_DB_USERNAME` 연결에서 조회한다. 실제 SQL 실행 계정과 진단 조회 계정은 달라도 된다. Autonomous의 일반 `ADMIN` 계정은 `SYS.V_$SQL` 권한을 다른 계정에 위임하지 못할 수 있으므로, 이 경우에는 DBA가 [34_agent_ords_execution_evidence_grant.sql](../../sql/adb/34_agent_ords_execution_evidence_grant.sql)을 실행해야 한다. cursor 미발견과 catalog 권한 미보유는 UI에서 서로 다른 안내로 표시하며, 어느 경우도 권한 검증 결과를 실패시키지 않는다.
|
실행 증적 연결에는 최소한 `V$SQL`과 `DBMS_XPLAN.DISPLAY_CURSOR`를 조회할 수 있는 catalog 권한이 필요하다. 증적은 ORDS parsing schema가 아니라 화면을 표시하는 `BACKOFFICE_DB_USERNAME` 연결에서 조회한다. 실제 SQL 실행 계정과 진단 조회 계정은 달라도 된다. Autonomous의 일반 `ADMIN` 계정은 `SYS.V_$SQL` 권한을 다른 계정에 위임하지 못할 수 있으므로, 이 경우에는 DBA가 [34_agent_ords_execution_evidence_grant.sql](../../sql/adb/34_agent_ords_execution_evidence_grant.sql)을 실행해야 한다. cursor 미발견과 catalog 권한 미보유는 UI에서 서로 다른 안내로 표시하며, 어느 경우도 권한 검증 결과를 실패시키지 않는다.
|
||||||
|
|||||||
@@ -18,7 +18,8 @@ public record ProbeResult(
|
|||||||
String vpdPredicate,
|
String vpdPredicate,
|
||||||
String effectiveSql,
|
String effectiveSql,
|
||||||
SqlExecutionEvidence executionEvidence,
|
SqlExecutionEvidence executionEvidence,
|
||||||
String executionEvidenceMessage
|
String executionEvidenceMessage,
|
||||||
|
List<SqlExecutionCandidate> executionCandidates
|
||||||
) {
|
) {
|
||||||
|
|
||||||
public ProbeResult(
|
public ProbeResult(
|
||||||
@@ -49,7 +50,8 @@ public record ProbeResult(
|
|||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
null
|
null,
|
||||||
|
List.of()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,7 +85,8 @@ public record ProbeResult(
|
|||||||
vpdPredicate,
|
vpdPredicate,
|
||||||
effectiveSql,
|
effectiveSql,
|
||||||
null,
|
null,
|
||||||
null
|
null,
|
||||||
|
List.of()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,7 +142,8 @@ public record ProbeResult(
|
|||||||
predicate,
|
predicate,
|
||||||
sql,
|
sql,
|
||||||
executionEvidence,
|
executionEvidence,
|
||||||
executionEvidenceMessage
|
executionEvidenceMessage,
|
||||||
|
executionCandidates
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,6 +155,14 @@ public record ProbeResult(
|
|||||||
public ProbeResult withExecutionEvidence(
|
public ProbeResult withExecutionEvidence(
|
||||||
SqlExecutionEvidence evidence,
|
SqlExecutionEvidence evidence,
|
||||||
String unavailableMessage
|
String unavailableMessage
|
||||||
|
) {
|
||||||
|
return withExecutionEvidence(evidence, unavailableMessage, List.of());
|
||||||
|
}
|
||||||
|
|
||||||
|
public ProbeResult withExecutionEvidence(
|
||||||
|
SqlExecutionEvidence evidence,
|
||||||
|
String unavailableMessage,
|
||||||
|
List<SqlExecutionCandidate> candidates
|
||||||
) {
|
) {
|
||||||
return new ProbeResult(
|
return new ProbeResult(
|
||||||
status,
|
status,
|
||||||
@@ -167,10 +179,15 @@ public record ProbeResult(
|
|||||||
vpdPredicate,
|
vpdPredicate,
|
||||||
effectiveSql,
|
effectiveSql,
|
||||||
evidence,
|
evidence,
|
||||||
unavailableMessage
|
unavailableMessage,
|
||||||
|
candidates == null ? List.of() : List.copyOf(candidates)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean hasExecutionCandidates() {
|
||||||
|
return executionCandidates != null && !executionCandidates.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
public String title() {
|
public String title() {
|
||||||
return switch (status) {
|
return switch (status) {
|
||||||
case SUCCESS -> "권한에 따라 데이터를 볼 수 있습니다.";
|
case SUCCESS -> "권한에 따라 데이터를 볼 수 있습니다.";
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package com.cloudhandson.vpdbackoffice.domain.probe;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One of the most recently active database cursors shown verbatim during a
|
||||||
|
* probe. The match flag is calculated in Java, not by SQL text filtering in
|
||||||
|
* the database.
|
||||||
|
*/
|
||||||
|
public record SqlExecutionCandidate(
|
||||||
|
String sqlId,
|
||||||
|
int childNumber,
|
||||||
|
String parsingSchema,
|
||||||
|
String originalSql,
|
||||||
|
String lastActiveAt,
|
||||||
|
boolean matchesProtectedObject
|
||||||
|
) {
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import com.cloudhandson.vpdbackoffice.domain.audit.AuditEvent;
|
|||||||
import com.cloudhandson.vpdbackoffice.domain.probe.ProbeCommand;
|
import com.cloudhandson.vpdbackoffice.domain.probe.ProbeCommand;
|
||||||
import com.cloudhandson.vpdbackoffice.domain.probe.ProbeResult;
|
import com.cloudhandson.vpdbackoffice.domain.probe.ProbeResult;
|
||||||
import com.cloudhandson.vpdbackoffice.domain.probe.ProbeStatus;
|
import com.cloudhandson.vpdbackoffice.domain.probe.ProbeStatus;
|
||||||
|
import com.cloudhandson.vpdbackoffice.domain.probe.SqlExecutionCandidate;
|
||||||
import com.cloudhandson.vpdbackoffice.domain.probe.SqlExecutionEvidence;
|
import com.cloudhandson.vpdbackoffice.domain.probe.SqlExecutionEvidence;
|
||||||
import com.cloudhandson.vpdbackoffice.domain.protectedobject.ProtectedColumn;
|
import com.cloudhandson.vpdbackoffice.domain.protectedobject.ProtectedColumn;
|
||||||
import com.cloudhandson.vpdbackoffice.domain.protectedobject.ProtectedObject;
|
import com.cloudhandson.vpdbackoffice.domain.protectedobject.ProtectedObject;
|
||||||
@@ -49,6 +50,7 @@ public class OrdsProbeService {
|
|||||||
|
|
||||||
private record ExecutionEvidenceLookup(
|
private record ExecutionEvidenceLookup(
|
||||||
SqlExecutionEvidence evidence,
|
SqlExecutionEvidence evidence,
|
||||||
|
List<SqlExecutionCandidate> candidates,
|
||||||
boolean sqlCatalogAccessUnavailable
|
boolean sqlCatalogAccessUnavailable
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
@@ -56,6 +58,7 @@ public class OrdsProbeService {
|
|||||||
private record RecentSqlCursor(
|
private record RecentSqlCursor(
|
||||||
String sqlId,
|
String sqlId,
|
||||||
int childNumber,
|
int childNumber,
|
||||||
|
String parsingSchema,
|
||||||
String sqlFulltext,
|
String sqlFulltext,
|
||||||
String lastActiveAt,
|
String lastActiveAt,
|
||||||
long executions,
|
long executions,
|
||||||
@@ -270,30 +273,33 @@ public class OrdsProbeService {
|
|||||||
if (lookup.sqlCatalogAccessUnavailable()) {
|
if (lookup.sqlCatalogAccessUnavailable()) {
|
||||||
return result.withExecutionEvidence(null,
|
return result.withExecutionEvidence(null,
|
||||||
"백오피스 DB 연결이 실제 DB cursor SQL을 읽지 못했습니다. 현재 권한 검증 결과는 정상입니다. "
|
"백오피스 DB 연결이 실제 DB cursor SQL을 읽지 못했습니다. 현재 권한 검증 결과는 정상입니다. "
|
||||||
+ "V$SQL과 DBMS_XPLAN은 일반 데이터 접근과 별도인 SYS 진단 권한입니다.");
|
+ "V$SQL과 DBMS_XPLAN은 일반 데이터 접근과 별도인 SYS 진단 권한입니다.",
|
||||||
|
lookup.candidates());
|
||||||
}
|
}
|
||||||
return result.withExecutionEvidence(null,
|
return result.withExecutionEvidence(null,
|
||||||
"최근 15분의 최신 실행 cursor 10건에서 이 보호 대상을 찾지 못했습니다. "
|
"최근 15분의 최신 실행 cursor 10건에서 이 보호 대상을 찾지 못했습니다. "
|
||||||
+ "ORDS 응답 지연 또는 shared pool 교체로 증적이 남지 않았을 수 있습니다.");
|
+ "아래 원문 SQL을 직접 확인하세요.",
|
||||||
|
lookup.candidates());
|
||||||
}
|
}
|
||||||
String message = evidence.hasPredicatePlan() ? null
|
String message = evidence.hasPredicatePlan() ? null
|
||||||
: "SQL_ID는 찾았지만 DBMS_XPLAN Predicate Information을 읽지 못했습니다. "
|
: "SQL_ID는 찾았지만 DBMS_XPLAN Predicate Information을 읽지 못했습니다. "
|
||||||
+ "백오피스 DB 계정에 V$SQL/DBMS_XPLAN 조회 권한이 필요합니다.";
|
+ "백오피스 DB 계정에 V$SQL/DBMS_XPLAN 조회 권한이 필요합니다.";
|
||||||
return result.withExecutionEvidence(evidence, message);
|
return result.withExecutionEvidence(evidence, message, lookup.candidates());
|
||||||
} catch (RuntimeException exception) {
|
} catch (RuntimeException exception) {
|
||||||
log.debug("Recent SQL execution evidence unavailable for {}.{}: {}",
|
log.debug("Recent SQL execution evidence unavailable for {}.{}: {}",
|
||||||
object.owner(), object.objectName(), exception.getMessage());
|
object.owner(), object.objectName(), exception.getMessage());
|
||||||
return result.withExecutionEvidence(null,
|
return result.withExecutionEvidence(null,
|
||||||
"DB 실행 증적을 읽지 못했습니다. 백오피스 DB 계정에 V$SQL과 DBMS_XPLAN 조회 권한이 필요합니다.");
|
"DB 실행 증적을 읽지 못했습니다. 백오피스 DB 계정에 V$SQL과 DBMS_XPLAN 조회 권한이 필요합니다.",
|
||||||
|
List.of());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private ExecutionEvidenceLookup findRecentExecutionEvidence(ProtectedObject object) {
|
private ExecutionEvidenceLookup findRecentExecutionEvidence(ProtectedObject object) {
|
||||||
try {
|
try {
|
||||||
return new ExecutionEvidenceLookup(findRecentExecutionEvidence(jdbcTemplate, object), false);
|
return findRecentExecutionEvidence(jdbcTemplate, object);
|
||||||
} catch (RuntimeException exception) {
|
} catch (RuntimeException exception) {
|
||||||
log.debug("Backoffice cursor evidence is unavailable: {}", exception.getMessage());
|
log.debug("Backoffice cursor evidence is unavailable: {}", exception.getMessage());
|
||||||
return new ExecutionEvidenceLookup(null, isSqlCatalogAccessUnavailable(exception));
|
return new ExecutionEvidenceLookup(null, List.of(), isSqlCatalogAccessUnavailable(exception));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -308,13 +314,14 @@ public class OrdsProbeService {
|
|||||||
|| normalized.contains("V$SQL");
|
|| normalized.contains("V$SQL");
|
||||||
}
|
}
|
||||||
|
|
||||||
private SqlExecutionEvidence findRecentExecutionEvidence(
|
private ExecutionEvidenceLookup findRecentExecutionEvidence(
|
||||||
JdbcTemplate evidenceJdbcTemplate,
|
JdbcTemplate evidenceJdbcTemplate,
|
||||||
ProtectedObject object
|
ProtectedObject object
|
||||||
) {
|
) {
|
||||||
List<RecentSqlCursor> candidates = evidenceJdbcTemplate.query("""
|
List<RecentSqlCursor> candidates = evidenceJdbcTemplate.query("""
|
||||||
SELECT sql_id,
|
SELECT sql_id,
|
||||||
child_number,
|
child_number,
|
||||||
|
parsing_schema_name,
|
||||||
sql_fulltext,
|
sql_fulltext,
|
||||||
last_active_time,
|
last_active_time,
|
||||||
executions,
|
executions,
|
||||||
@@ -324,6 +331,7 @@ public class OrdsProbeService {
|
|||||||
FROM (
|
FROM (
|
||||||
SELECT sql_id,
|
SELECT sql_id,
|
||||||
child_number,
|
child_number,
|
||||||
|
parsing_schema_name,
|
||||||
sql_fulltext,
|
sql_fulltext,
|
||||||
last_active_time,
|
last_active_time,
|
||||||
executions,
|
executions,
|
||||||
@@ -338,6 +346,7 @@ public class OrdsProbeService {
|
|||||||
""", (resultSet, rowNum) -> new RecentSqlCursor(
|
""", (resultSet, rowNum) -> new RecentSqlCursor(
|
||||||
resultSet.getString("sql_id"),
|
resultSet.getString("sql_id"),
|
||||||
resultSet.getInt("child_number"),
|
resultSet.getInt("child_number"),
|
||||||
|
resultSet.getString("parsing_schema_name"),
|
||||||
resultSet.getString("sql_fulltext"),
|
resultSet.getString("sql_fulltext"),
|
||||||
resultSet.getTimestamp("last_active_time") == null
|
resultSet.getTimestamp("last_active_time") == null
|
||||||
? null : resultSet.getTimestamp("last_active_time").toLocalDateTime().toString(),
|
? null : resultSet.getTimestamp("last_active_time").toLocalDateTime().toString(),
|
||||||
@@ -347,7 +356,18 @@ public class OrdsProbeService {
|
|||||||
resultSet.getLong("buffer_gets")
|
resultSet.getLong("buffer_gets")
|
||||||
), EXECUTION_EVIDENCE_LOOKBACK_MINUTES);
|
), EXECUTION_EVIDENCE_LOOKBACK_MINUTES);
|
||||||
|
|
||||||
return candidates.stream()
|
List<SqlExecutionCandidate> candidateViews = candidates.stream()
|
||||||
|
.map(candidate -> new SqlExecutionCandidate(
|
||||||
|
candidate.sqlId(),
|
||||||
|
candidate.childNumber(),
|
||||||
|
candidate.parsingSchema(),
|
||||||
|
candidate.sqlFulltext(),
|
||||||
|
candidate.lastActiveAt(),
|
||||||
|
referencesProtectedObject(candidate.sqlFulltext(), object)
|
||||||
|
))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
SqlExecutionEvidence evidence = candidates.stream()
|
||||||
.filter(candidate -> referencesProtectedObject(candidate.sqlFulltext(), object))
|
.filter(candidate -> referencesProtectedObject(candidate.sqlFulltext(), object))
|
||||||
.findFirst()
|
.findFirst()
|
||||||
.map(candidate -> new SqlExecutionEvidence(
|
.map(candidate -> new SqlExecutionEvidence(
|
||||||
@@ -362,6 +382,7 @@ public class OrdsProbeService {
|
|||||||
findPredicatePlan(evidenceJdbcTemplate, candidate.sqlId(), candidate.childNumber())
|
findPredicatePlan(evidenceJdbcTemplate, candidate.sqlId(), candidate.childNumber())
|
||||||
))
|
))
|
||||||
.orElse(null);
|
.orElse(null);
|
||||||
|
return new ExecutionEvidenceLookup(evidence, candidateViews, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
static boolean referencesProtectedObject(String sqlFulltext, ProtectedObject object) {
|
static boolean referencesProtectedObject(String sqlFulltext, ProtectedObject object) {
|
||||||
|
|||||||
@@ -142,6 +142,29 @@
|
|||||||
th:text="${result.executionEvidenceMessage()}"></div>
|
th:text="${result.executionEvidenceMessage()}"></div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section class="result-section" th:if="${result.hasExecutionCandidates()}">
|
||||||
|
<div class="section-heading compact-heading">
|
||||||
|
<div>
|
||||||
|
<h3>최근 DB cursor 10건</h3>
|
||||||
|
<p class="section-subtitle">백오피스 DB 연결이 읽은 최신 cursor입니다. 보호 대상과 일치한 SQL은 초록 배지로 표시합니다.</p>
|
||||||
|
</div>
|
||||||
|
<span class="badge text-bg-light" th:text="${result.executionCandidates().size() + '건'}">10건</span>
|
||||||
|
</div>
|
||||||
|
<article class="probe-exchange mt-3" th:each="candidate : ${result.executionCandidates()}">
|
||||||
|
<div class="section-heading compact-heading">
|
||||||
|
<div>
|
||||||
|
<h3 th:text="${'SQL_ID ' + candidate.sqlId()}">SQL_ID</h3>
|
||||||
|
<p class="form-hint mb-0"
|
||||||
|
th:text="${'최근 실행: ' + (candidate.lastActiveAt() ?: '-') + ' · Parsing schema: ' + (candidate.parsingSchema() ?: '-') + ' · Child cursor: ' + candidate.childNumber()}">최근 실행</p>
|
||||||
|
</div>
|
||||||
|
<span class="badge"
|
||||||
|
th:classappend="${candidate.matchesProtectedObject()} ? ' text-bg-success' : ' text-bg-light'"
|
||||||
|
th:text="${candidate.matchesProtectedObject()} ? '보호 대상 일치' : '다른 SQL'">다른 SQL</span>
|
||||||
|
</div>
|
||||||
|
<pre class="mt-2 mb-0" th:text="${candidate.originalSql()} ?: '-'">SELECT ...</pre>
|
||||||
|
</article>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section class="result-section sql-trace-section" th:if="${result.hasSqlTrace()}">
|
<section class="result-section sql-trace-section" th:if="${result.hasSqlTrace()}">
|
||||||
<div class="section-heading compact-heading">
|
<div class="section-heading compact-heading">
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -156,4 +156,28 @@ class ProbeResultTest {
|
|||||||
assertThat(result.executionEvidence().hasPredicatePlan()).isTrue();
|
assertThat(result.executionEvidence().hasPredicatePlan()).isTrue();
|
||||||
assertThat(result.effectiveSql()).contains("WHERE");
|
assertThat(result.effectiveSql()).contains("WHERE");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void keepsRecentCursorCandidatesWhenNoObjectMatchWasFound() {
|
||||||
|
ProbeResult result = new ProbeResult(
|
||||||
|
ProbeStatus.SUCCESS,
|
||||||
|
List.of(),
|
||||||
|
List.of(),
|
||||||
|
0,
|
||||||
|
List.of(),
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
).withExecutionEvidence(null, "최근 cursor를 찾지 못했습니다.", List.of(
|
||||||
|
new SqlExecutionCandidate(
|
||||||
|
"candidate1", 0, "CB_ORDS", "SELECT 1 FROM dual", "2026-07-01T15:00", false)
|
||||||
|
));
|
||||||
|
|
||||||
|
assertThat(result.hasExecutionEvidence()).isFalse();
|
||||||
|
assertThat(result.hasExecutionCandidates()).isTrue();
|
||||||
|
assertThat(result.executionCandidates()).hasSize(1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -110,6 +110,7 @@ class GuidedFlowTemplateTest {
|
|||||||
assertThat(result)
|
assertThat(result)
|
||||||
.contains("적용된 사용자와 권한")
|
.contains("적용된 사용자와 권한")
|
||||||
.contains("DB 실행 증적")
|
.contains("DB 실행 증적")
|
||||||
|
.contains("최근 DB cursor 10건")
|
||||||
.contains("실행 요청 SQL")
|
.contains("실행 요청 SQL")
|
||||||
.contains("실제 DB cursor SQL")
|
.contains("실제 DB cursor SQL")
|
||||||
.contains("set_vpd_context 사용자 컨텍스트")
|
.contains("set_vpd_context 사용자 컨텍스트")
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
|||||||
|
|
||||||
import com.cloudhandson.vpdbackoffice.domain.probe.ProbeResult;
|
import com.cloudhandson.vpdbackoffice.domain.probe.ProbeResult;
|
||||||
import com.cloudhandson.vpdbackoffice.domain.probe.ProbeStatus;
|
import com.cloudhandson.vpdbackoffice.domain.probe.ProbeStatus;
|
||||||
|
import com.cloudhandson.vpdbackoffice.domain.probe.SqlExecutionCandidate;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
@@ -38,7 +39,10 @@ class ProbeResultTemplateRenderTest {
|
|||||||
"{}",
|
"{}",
|
||||||
"{}"
|
"{}"
|
||||||
).withExecutionEvidence(null,
|
).withExecutionEvidence(null,
|
||||||
"백오피스 DB 연결이 실제 DB cursor SQL을 읽지 못했습니다. 현재 권한 검증 결과는 정상입니다.");
|
"최근 cursor를 찾지 못했습니다.",
|
||||||
|
List.of(new SqlExecutionCandidate(
|
||||||
|
"4f2jz7n2k0s3p", 0, "CB_ORDS",
|
||||||
|
"SELECT * FROM ADMIN.CB_V_SEARCH_DOCUMENTS", "2026-07-01T15:00", true)));
|
||||||
var context = new Context(Locale.KOREAN);
|
var context = new Context(Locale.KOREAN);
|
||||||
context.setVariable("result", result);
|
context.setVariable("result", result);
|
||||||
context.setVariable("vectorSearch", false);
|
context.setVariable("vectorSearch", false);
|
||||||
@@ -47,6 +51,10 @@ class ProbeResultTemplateRenderTest {
|
|||||||
|
|
||||||
String rendered = engine.process("fragments/probe-result", context);
|
String rendered = engine.process("fragments/probe-result", context);
|
||||||
|
|
||||||
assertThat(rendered).contains("백오피스 DB 연결이 실제 DB cursor SQL을 읽지 못했습니다.");
|
assertThat(rendered)
|
||||||
|
.contains("최근 cursor를 찾지 못했습니다.")
|
||||||
|
.contains("최근 DB cursor 10건")
|
||||||
|
.contains("4f2jz7n2k0s3p")
|
||||||
|
.contains("보호 대상 일치");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user