diff --git a/src/main/java/org/codelibs/fess/api/chat/ChatApiManager.java b/src/main/java/org/codelibs/fess/api/chat/ChatApiManager.java index eb7b39f00..a5b4b1761 100644 --- a/src/main/java/org/codelibs/fess/api/chat/ChatApiManager.java +++ b/src/main/java/org/codelibs/fess/api/chat/ChatApiManager.java @@ -27,6 +27,7 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.codelibs.core.lang.StringUtil; +import org.codelibs.fess.Constants; import org.codelibs.fess.api.BaseApiManager; import org.codelibs.fess.chat.ChatClient.ChatResult; import org.codelibs.fess.chat.ChatPhaseCallback; @@ -183,6 +184,11 @@ protected void processChatRequest(final HttpServletRequest request, final HttpSe } final String userId = getUserId(request); + + // Set LLM type name as Access Type for search log + request.setAttribute(Constants.SEARCH_LOG_ACCESS_TYPE, + ComponentUtil.getFessConfig().getSystemProperty("rag.llm.name", "ollama")); + final Map fields = parseFieldFilters(request); final String[] extraQueries = parseExtraQueries(request); final ChatResult result; @@ -257,6 +263,10 @@ protected void processStreamRequest(final HttpServletRequest request, final Http try (final PrintWriter writer = response.getWriter()) { final String userId = getUserId(request); + // Set LLM type name as Access Type for search log + request.setAttribute(Constants.SEARCH_LOG_ACCESS_TYPE, + ComponentUtil.getFessConfig().getSystemProperty("rag.llm.name", "ollama")); + // Create phase callback for SSE events final ChatPhaseCallback phaseCallback = new ChatPhaseCallback() { @Override diff --git a/src/main/java/org/codelibs/fess/helper/SearchLogHelper.java b/src/main/java/org/codelibs/fess/helper/SearchLogHelper.java index f7b359e8d..ed51710c0 100644 --- a/src/main/java/org/codelibs/fess/helper/SearchLogHelper.java +++ b/src/main/java/org/codelibs/fess/helper/SearchLogHelper.java @@ -56,6 +56,8 @@ import org.dbflute.optional.OptionalThing; import org.lastaflute.web.util.LaRequestUtil; import org.opensearch.action.update.UpdateRequest; + +import jakarta.servlet.http.HttpServletRequest; import org.opensearch.script.Script; import com.fasterxml.jackson.core.JsonProcessingException; @@ -124,6 +126,28 @@ public UserInfo load(final String key) throws Exception { searchLogLogger = LogManager.getLogger(loggerName); } + /** Holds resolved dependencies for search log creation, decoupled from ComponentUtil. */ + protected static class SearchLogContext { + final FessConfig fessConfig; + final String[] roles; + final String userCode; + final String userId; + final HttpServletRequest request; + final String clientIp; + final String virtualHostKey; + + SearchLogContext(final FessConfig fessConfig, final String[] roles, final String userCode, final String userId, + final HttpServletRequest request, final String clientIp, final String virtualHostKey) { + this.fessConfig = fessConfig; + this.roles = roles; + this.userCode = userCode; + this.userId = userId; + this.request = request; + this.clientIp = clientIp; + this.virtualHostKey = virtualHostKey; + } + } + /** * Adds a search log to the queue. * @@ -143,19 +167,51 @@ public void addSearchLog(final SearchRequestParams params, final LocalDateTime r return; } - final RoleQueryHelper roleQueryHelper = ComponentUtil.getRoleQueryHelper(); - final UserInfoHelper userInfoHelper = ComponentUtil.getUserInfoHelper(); + final SearchLogContext context = createSearchLogContext(params, fessConfig); + createSearchLog(params, requestedTime, queryId, query, pageStart, pageSize, queryResponseList, context); + } + + /** + * Resolves the runtime dependencies needed to build a SearchLog. + * + * @param params The search request parameters. + * @param fessConfig The Fess configuration. + * @return The resolved search log context. + */ + protected SearchLogContext createSearchLogContext(final SearchRequestParams params, final FessConfig fessConfig) { + final String[] roles = ComponentUtil.getRoleQueryHelper().build(params.getType()).stream().toArray(n -> new String[n]); + final String userCode = fessConfig.isUserInfo() ? ComponentUtil.getUserInfoHelper().getUserCode() : null; + final String userId = ComponentUtil.getRequestManager().findUserBean(FessUserBean.class).map(FessUserBean::getUserId).orElse(null); + final HttpServletRequest request = LaRequestUtil.getOptionalRequest().orElse(null); + final String clientIp = request != null ? ComponentUtil.getViewHelper().getClientIp(request) : null; + final String virtualHostKey = ComponentUtil.getVirtualHostHelper().getVirtualHostKey(); + + return new SearchLogContext(fessConfig, roles, userCode, userId, request, clientIp, virtualHostKey); + } + + /** + * Builds a SearchLog from the given parameters and context, then adds it to the queue. + * + * @param params The search request parameters. + * @param requestedTime The time the search was requested. + * @param queryId The ID of the search query. + * @param query The search query string. + * @param pageStart The start position of the page. + * @param pageSize The size of the page. + * @param queryResponseList The list of query responses. + * @param context The search log context holding resolved dependencies. + */ + protected void createSearchLog(final SearchRequestParams params, final LocalDateTime requestedTime, final String queryId, + final String query, final int pageStart, final int pageSize, final QueryResponseList queryResponseList, + final SearchLogContext context) { final SearchLog searchLog = new SearchLog(); - if (fessConfig.isUserInfo()) { - final String userCode = userInfoHelper.getUserCode(); - if (userCode != null) { - searchLog.setUserSessionId(userCode); - searchLog.setUserInfo(getUserInfo(userCode)); - } + if (context.userCode != null) { + searchLog.setUserSessionId(context.userCode); + searchLog.setUserInfo(getUserInfo(context.userCode)); } - searchLog.setRoles(roleQueryHelper.build(params.getType()).stream().toArray(n -> new String[n])); + searchLog.setRoles(context.roles); searchLog.setQueryId(queryId); searchLog.setHitCount(queryResponseList.getAllRecordCount()); searchLog.setHitCountRelation(queryResponseList.getAllRecordCountRelation()); @@ -166,27 +222,19 @@ public void addSearchLog(final SearchRequestParams params, final LocalDateTime r searchLog.setSearchQuery(StringUtils.abbreviate(queryResponseList.getSearchQuery(), 1000)); searchLog.setQueryOffset(pageStart); searchLog.setQueryPageSize(pageSize); - ComponentUtil.getRequestManager().findUserBean(FessUserBean.class).ifPresent(user -> { - searchLog.setUser(user.getUserId()); - }); - LaRequestUtil.getOptionalRequest().ifPresent(req -> { - searchLog.setClientIp(StringUtils.abbreviate(ComponentUtil.getViewHelper().getClientIp(req), 100)); - searchLog.setReferer(StringUtils.abbreviate(req.getHeader("referer"), 1000)); - searchLog.setUserAgent(StringUtils.abbreviate(req.getHeader("user-agent"), 255)); - final Object accessType = req.getAttribute(Constants.SEARCH_LOG_ACCESS_TYPE); - if (Constants.SEARCH_LOG_ACCESS_TYPE_JSON.equals(accessType)) { - searchLog.setAccessType(Constants.SEARCH_LOG_ACCESS_TYPE_JSON); - } else if (Constants.SEARCH_LOG_ACCESS_TYPE_GSA.equals(accessType)) { - searchLog.setAccessType(Constants.SEARCH_LOG_ACCESS_TYPE_GSA); - } else if (Constants.SEARCH_LOG_ACCESS_TYPE_OTHER.equals(accessType)) { - searchLog.setAccessType(Constants.SEARCH_LOG_ACCESS_TYPE_OTHER); - } else if (Constants.SEARCH_LOG_ACCESS_TYPE_ADMIN.equals(accessType)) { - searchLog.setAccessType(Constants.SEARCH_LOG_ACCESS_TYPE_ADMIN); - } else { - searchLog.setAccessType(Constants.SEARCH_LOG_ACCESS_TYPE_WEB); - } - final Object languages = req.getAttribute(Constants.REQUEST_LANGUAGES); + if (context.userId != null) { + searchLog.setUser(context.userId); + } + + if (context.request != null) { + searchLog.setClientIp(StringUtils.abbreviate(context.clientIp, 100)); + searchLog.setReferer(StringUtils.abbreviate(context.request.getHeader("referer"), 1000)); + searchLog.setUserAgent(StringUtils.abbreviate(context.request.getHeader("user-agent"), 255)); + + searchLog.setAccessType(determineAccessType(context.request.getAttribute(Constants.SEARCH_LOG_ACCESS_TYPE))); + + final Object languages = context.request.getAttribute(Constants.REQUEST_LANGUAGES); if (languages != null) { searchLog.setLanguages(StringUtils.join((String[]) languages, ",")); } else { @@ -194,9 +242,9 @@ public void addSearchLog(final SearchRequestParams params, final LocalDateTime r } @SuppressWarnings("unchecked") - final Map> fieldLogMap = (Map>) req.getAttribute(Constants.FIELD_LOGS); + final Map> fieldLogMap = (Map>) context.request.getAttribute(Constants.FIELD_LOGS); if (fieldLogMap != null) { - final int queryMaxLength = fessConfig.getQueryMaxLengthAsInteger(); + final int queryMaxLength = context.fessConfig.getQueryMaxLengthAsInteger(); for (final Map.Entry> logEntry : fieldLogMap.entrySet()) { for (final String value : logEntry.getValue()) { searchLog.addSearchFieldLogValue(logEntry.getKey(), StringUtils.abbreviate(value, queryMaxLength)); @@ -204,26 +252,45 @@ public void addSearchLog(final SearchRequestParams params, final LocalDateTime r } } - for (final String s : fessConfig.getSearchlogRequestHeadersAsArray()) { + for (final String s : context.fessConfig.getSearchlogRequestHeadersAsArray()) { final String key = s.replace('-', '_').toLowerCase(Locale.ENGLISH); - Collections.list(req.getHeaders(s)).stream().forEach(v -> { + Collections.list(context.request.getHeaders(s)).stream().forEach(v -> { searchLog.addRequestHeaderValue(key, v); }); } - }); + } - final String virtualHostKey = ComponentUtil.getVirtualHostHelper().getVirtualHostKey(); - if (StringUtil.isNotBlank(virtualHostKey)) { - searchLog.setVirtualHost(virtualHostKey); + if (StringUtil.isNotBlank(context.virtualHostKey)) { + searchLog.setVirtualHost(context.virtualHostKey); } else { searchLog.setVirtualHost(StringUtil.EMPTY); } addDocumentsInResponse(queryResponseList, searchLog); - searchLogQueue.add(searchLog); } + /** + * Returns the access type string from the given request attribute value, defaulting to web. + * + * @param accessType The access type attribute value from the request. + * @return The access type string. + */ + protected String determineAccessType(final Object accessType) { + if (Constants.SEARCH_LOG_ACCESS_TYPE_JSON.equals(accessType)) { + return Constants.SEARCH_LOG_ACCESS_TYPE_JSON; + } else if (Constants.SEARCH_LOG_ACCESS_TYPE_GSA.equals(accessType)) { + return Constants.SEARCH_LOG_ACCESS_TYPE_GSA; + } else if (Constants.SEARCH_LOG_ACCESS_TYPE_OTHER.equals(accessType)) { + return Constants.SEARCH_LOG_ACCESS_TYPE_OTHER; + } else if (Constants.SEARCH_LOG_ACCESS_TYPE_ADMIN.equals(accessType)) { + return Constants.SEARCH_LOG_ACCESS_TYPE_ADMIN; + } else if (accessType instanceof String && StringUtil.isNotBlank((String) accessType)) { + return (String) accessType; + } + return Constants.SEARCH_LOG_ACCESS_TYPE_WEB; + } + /** * Adds documents in the response to the search log. * diff --git a/src/test/java/org/codelibs/fess/api/chat/ChatApiManagerTest.java b/src/test/java/org/codelibs/fess/api/chat/ChatApiManagerTest.java index 4d2c13514..1d1e37811 100644 --- a/src/test/java/org/codelibs/fess/api/chat/ChatApiManagerTest.java +++ b/src/test/java/org/codelibs/fess/api/chat/ChatApiManagerTest.java @@ -22,6 +22,7 @@ import java.util.List; import java.util.Map; +import org.codelibs.fess.Constants; import org.codelibs.fess.entity.ChatMessage.ChatSource; import org.codelibs.fess.entity.FacetQueryView; import org.codelibs.fess.helper.ViewHelper; @@ -371,6 +372,105 @@ public void test_createSuccessResponse_emptySources() { assertEquals(sources, response.get("sources")); } + // ===== getMaxMessageLength tests ===== + + @Test + public void test_getMaxMessageLength_default() { + ComponentUtil.setFessConfig(new FessConfig.SimpleImpl() { + private static final long serialVersionUID = 1L; + + @Override + public String getOrDefault(final String key, final String defaultValue) { + if ("rag.chat.message.max.length".equals(key)) { + return defaultValue; + } + return defaultValue; + } + }); + + final int result = chatApiManager.getMaxMessageLength(ComponentUtil.getFessConfig()); + assertEquals(4000, result); + } + + @Test + public void test_getMaxMessageLength_customValue() { + ComponentUtil.setFessConfig(new FessConfig.SimpleImpl() { + private static final long serialVersionUID = 1L; + + @Override + public String getOrDefault(final String key, final String defaultValue) { + if ("rag.chat.message.max.length".equals(key)) { + return "8000"; + } + return defaultValue; + } + }); + + final int result = chatApiManager.getMaxMessageLength(ComponentUtil.getFessConfig()); + assertEquals(8000, result); + } + + @Test + public void test_getMaxMessageLength_invalidValue() { + ComponentUtil.setFessConfig(new FessConfig.SimpleImpl() { + private static final long serialVersionUID = 1L; + + @Override + public String getOrDefault(final String key, final String defaultValue) { + if ("rag.chat.message.max.length".equals(key)) { + return "not-a-number"; + } + return defaultValue; + } + }); + + final int result = chatApiManager.getMaxMessageLength(ComponentUtil.getFessConfig()); + assertEquals(4000, result); + } + + // ===== Access Type attribute tests ===== + + @Test + public void test_mockRequest_attributeStorage() { + final MockHttpServletRequest request = new MockHttpServletRequest(); + assertNull(request.getAttribute(Constants.SEARCH_LOG_ACCESS_TYPE)); + + request.setAttribute(Constants.SEARCH_LOG_ACCESS_TYPE, "ollama"); + assertEquals("ollama", request.getAttribute(Constants.SEARCH_LOG_ACCESS_TYPE)); + } + + @Test + public void test_mockRequest_attributeOverwrite() { + final MockHttpServletRequest request = new MockHttpServletRequest(); + + request.setAttribute(Constants.SEARCH_LOG_ACCESS_TYPE, "ollama"); + assertEquals("ollama", request.getAttribute(Constants.SEARCH_LOG_ACCESS_TYPE)); + + request.setAttribute(Constants.SEARCH_LOG_ACCESS_TYPE, "openai"); + assertEquals("openai", request.getAttribute(Constants.SEARCH_LOG_ACCESS_TYPE)); + } + + @Test + public void test_accessType_constantValues() { + assertEquals("json", Constants.SEARCH_LOG_ACCESS_TYPE_JSON); + assertEquals("gsa", Constants.SEARCH_LOG_ACCESS_TYPE_GSA); + assertEquals("web", Constants.SEARCH_LOG_ACCESS_TYPE_WEB); + assertEquals("admin", Constants.SEARCH_LOG_ACCESS_TYPE_ADMIN); + assertEquals("other", Constants.SEARCH_LOG_ACCESS_TYPE_OTHER); + } + + @Test + public void test_accessType_llmNamesAreDifferentFromBuiltinTypes() { + final String[] llmNames = { "ollama", "openai", "gemini" }; + for (final String llmName : llmNames) { + assertFalse(Constants.SEARCH_LOG_ACCESS_TYPE_JSON.equals(llmName)); + assertFalse(Constants.SEARCH_LOG_ACCESS_TYPE_GSA.equals(llmName)); + assertFalse(Constants.SEARCH_LOG_ACCESS_TYPE_WEB.equals(llmName)); + assertFalse(Constants.SEARCH_LOG_ACCESS_TYPE_ADMIN.equals(llmName)); + assertFalse(Constants.SEARCH_LOG_ACCESS_TYPE_OTHER.equals(llmName)); + } + } + // ===== parseExtraQueries tests ===== private void setupViewHelperWithFacetGroups(FacetQueryView... views) { @@ -549,6 +649,7 @@ private static class MockHttpServletRequest extends jakarta.servlet.http.HttpSer private String servletPath; private String method = "POST"; private final Map parameterValuesMap = new HashMap<>(); + private final Map attributeMap = new HashMap<>(); public MockHttpServletRequest() { super(new MockServletRequest()); @@ -580,6 +681,16 @@ public String[] getParameterValues(String name) { public void setParameterValues(String name, String[] values) { parameterValuesMap.put(name, values); } + + @Override + public Object getAttribute(String name) { + return attributeMap.get(name); + } + + @Override + public void setAttribute(String name, Object o) { + attributeMap.put(name, o); + } } /** diff --git a/src/test/java/org/codelibs/fess/helper/SearchLogHelperTest.java b/src/test/java/org/codelibs/fess/helper/SearchLogHelperTest.java index d9e998a3d..6dd9e92ee 100644 --- a/src/test/java/org/codelibs/fess/helper/SearchLogHelperTest.java +++ b/src/test/java/org/codelibs/fess/helper/SearchLogHelperTest.java @@ -15,23 +15,34 @@ */ package org.codelibs.fess.helper; +import java.time.LocalDateTime; +import java.util.Collections; import java.util.HashMap; import java.util.Map; - +import java.util.Set; + +import org.codelibs.fess.Constants; +import org.codelibs.fess.entity.FacetInfo; +import org.codelibs.fess.entity.GeoInfo; +import org.codelibs.fess.entity.HighlightInfo; +import org.codelibs.fess.entity.SearchRequestParams; +import org.codelibs.fess.entity.SearchRequestParams.SearchRequestType; import org.codelibs.fess.mylasta.direction.FessConfig; +import org.codelibs.fess.opensearch.log.exentity.SearchLog; import org.codelibs.fess.unit.UnitFessTestCase; import org.codelibs.fess.util.ComponentUtil; +import org.codelibs.fess.util.QueryResponseList; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInfo; public class SearchLogHelperTest extends UnitFessTestCase { - private SearchLogHelper searchLogHelper; + private TestableSearchLogHelper searchLogHelper; @Override protected void setUp(TestInfo testInfo) throws Exception { super.setUp(testInfo); - searchLogHelper = new SearchLogHelper(); + searchLogHelper = new TestableSearchLogHelper(); setupMockComponents(); searchLogHelper.init(); } @@ -146,6 +157,278 @@ public void test_clickLogQueue_initialization() { assertTrue(searchLogHelper.clickLogQueue.isEmpty()); } + // ===== addSearchLog Access Type tests ===== + + private SearchLogHelper.SearchLogContext createTestContext(final jakarta.servlet.http.HttpServletRequest request) { + return new SearchLogHelper.SearchLogContext((FessConfig) ComponentUtil.getFessConfig(), new String[0], // roles + null, // userCode + null, // userId + request, // request + "127.0.0.1", // clientIp + "" // virtualHostKey + ); + } + + private SearchLog callCreateSearchLogAndGetResult(final String accessTypeAttribute) { + if (accessTypeAttribute != null) { + setMockRequestAttribute(Constants.SEARCH_LOG_ACCESS_TYPE, accessTypeAttribute); + } + + final jakarta.servlet.http.HttpServletRequest request = org.lastaflute.web.util.LaRequestUtil.getOptionalRequest().orElse(null); + final SearchLogHelper.SearchLogContext context = createTestContext(request); + + final MockSearchRequestParams params = new MockSearchRequestParams(); + final LocalDateTime now = LocalDateTime.now(); + final QueryResponseList queryResponseList = new QueryResponseList(Collections.emptyList(), 0L, "eq", 0L, false, null, 0, 10, 0); + + searchLogHelper.createSearchLog(params, now, "test-query-id", "test query", 0, 10, queryResponseList, context); + + assertFalse(searchLogHelper.searchLogQueue.isEmpty()); + return searchLogHelper.searchLogQueue.poll(); + } + + @Test + public void test_addSearchLog_accessType_json() { + final SearchLog searchLog = callCreateSearchLogAndGetResult(Constants.SEARCH_LOG_ACCESS_TYPE_JSON); + assertEquals(Constants.SEARCH_LOG_ACCESS_TYPE_JSON, searchLog.getAccessType()); + } + + @Test + public void test_addSearchLog_accessType_gsa() { + final SearchLog searchLog = callCreateSearchLogAndGetResult(Constants.SEARCH_LOG_ACCESS_TYPE_GSA); + assertEquals(Constants.SEARCH_LOG_ACCESS_TYPE_GSA, searchLog.getAccessType()); + } + + @Test + public void test_addSearchLog_accessType_other() { + final SearchLog searchLog = callCreateSearchLogAndGetResult(Constants.SEARCH_LOG_ACCESS_TYPE_OTHER); + assertEquals(Constants.SEARCH_LOG_ACCESS_TYPE_OTHER, searchLog.getAccessType()); + } + + @Test + public void test_addSearchLog_accessType_admin() { + final SearchLog searchLog = callCreateSearchLogAndGetResult(Constants.SEARCH_LOG_ACCESS_TYPE_ADMIN); + assertEquals(Constants.SEARCH_LOG_ACCESS_TYPE_ADMIN, searchLog.getAccessType()); + } + + @Test + public void test_addSearchLog_accessType_defaultWeb() { + final SearchLog searchLog = callCreateSearchLogAndGetResult(null); + assertEquals(Constants.SEARCH_LOG_ACCESS_TYPE_WEB, searchLog.getAccessType()); + } + + @Test + public void test_addSearchLog_accessType_ollama() { + final SearchLog searchLog = callCreateSearchLogAndGetResult("ollama"); + assertEquals("ollama", searchLog.getAccessType()); + } + + @Test + public void test_addSearchLog_accessType_openai() { + final SearchLog searchLog = callCreateSearchLogAndGetResult("openai"); + assertEquals("openai", searchLog.getAccessType()); + } + + @Test + public void test_addSearchLog_accessType_gemini() { + final SearchLog searchLog = callCreateSearchLogAndGetResult("gemini"); + assertEquals("gemini", searchLog.getAccessType()); + } + + @Test + public void test_addSearchLog_accessType_customLlmName() { + final SearchLog searchLog = callCreateSearchLogAndGetResult("my-custom-llm"); + assertEquals("my-custom-llm", searchLog.getAccessType()); + } + + @Test + public void test_addSearchLog_accessType_blankStringDefaultsToWeb() { + final SearchLog searchLog = callCreateSearchLogAndGetResult(""); + assertEquals(Constants.SEARCH_LOG_ACCESS_TYPE_WEB, searchLog.getAccessType()); + } + + @Test + public void test_addSearchLog_accessType_whitespaceOnlyDefaultsToWeb() { + final SearchLog searchLog = callCreateSearchLogAndGetResult(" "); + assertEquals(Constants.SEARCH_LOG_ACCESS_TYPE_WEB, searchLog.getAccessType()); + } + + @Test + public void test_addSearchLog_accessType_nonStringObjectDefaultsToWeb() { + setMockRequestAttribute(Constants.SEARCH_LOG_ACCESS_TYPE, Integer.valueOf(123)); + + final jakarta.servlet.http.HttpServletRequest request = org.lastaflute.web.util.LaRequestUtil.getOptionalRequest().orElse(null); + final SearchLogHelper.SearchLogContext context = createTestContext(request); + + final MockSearchRequestParams params = new MockSearchRequestParams(); + final LocalDateTime now = LocalDateTime.now(); + final QueryResponseList queryResponseList = new QueryResponseList(Collections.emptyList(), 0L, "eq", 0L, false, null, 0, 10, 0); + + searchLogHelper.createSearchLog(params, now, "test-query-id", "test query", 0, 10, queryResponseList, context); + + assertFalse(searchLogHelper.searchLogQueue.isEmpty()); + final SearchLog searchLog = searchLogHelper.searchLogQueue.poll(); + assertEquals(Constants.SEARCH_LOG_ACCESS_TYPE_WEB, searchLog.getAccessType()); + } + + // ===== determineAccessType direct tests ===== + + @Test + public void test_determineAccessType_json() { + assertEquals(Constants.SEARCH_LOG_ACCESS_TYPE_JSON, searchLogHelper.determineAccessType(Constants.SEARCH_LOG_ACCESS_TYPE_JSON)); + } + + @Test + public void test_determineAccessType_gsa() { + assertEquals(Constants.SEARCH_LOG_ACCESS_TYPE_GSA, searchLogHelper.determineAccessType(Constants.SEARCH_LOG_ACCESS_TYPE_GSA)); + } + + @Test + public void test_determineAccessType_other() { + assertEquals(Constants.SEARCH_LOG_ACCESS_TYPE_OTHER, searchLogHelper.determineAccessType(Constants.SEARCH_LOG_ACCESS_TYPE_OTHER)); + } + + @Test + public void test_determineAccessType_admin() { + assertEquals(Constants.SEARCH_LOG_ACCESS_TYPE_ADMIN, searchLogHelper.determineAccessType(Constants.SEARCH_LOG_ACCESS_TYPE_ADMIN)); + } + + @Test + public void test_determineAccessType_customString() { + assertEquals("my-custom-llm", searchLogHelper.determineAccessType("my-custom-llm")); + } + + @Test + public void test_determineAccessType_blankDefaultsToWeb() { + assertEquals(Constants.SEARCH_LOG_ACCESS_TYPE_WEB, searchLogHelper.determineAccessType("")); + } + + @Test + public void test_determineAccessType_nullDefaultsToWeb() { + assertEquals(Constants.SEARCH_LOG_ACCESS_TYPE_WEB, searchLogHelper.determineAccessType(null)); + } + + @Test + public void test_determineAccessType_nonStringDefaultsToWeb() { + assertEquals(Constants.SEARCH_LOG_ACCESS_TYPE_WEB, searchLogHelper.determineAccessType(Integer.valueOf(123))); + } + + // ===== addSearchLog integration test (exercises wrapper wiring) ===== + + @Test + public void test_addSearchLog_wiring() { + setMockRequestAttribute(Constants.SEARCH_LOG_ACCESS_TYPE, Constants.SEARCH_LOG_ACCESS_TYPE_JSON); + final jakarta.servlet.http.HttpServletRequest request = org.lastaflute.web.util.LaRequestUtil.getOptionalRequest().orElse(null); + searchLogHelper.testContext = createTestContext(request); + + final MockSearchRequestParams params = new MockSearchRequestParams(); + final LocalDateTime now = LocalDateTime.now(); + final QueryResponseList queryResponseList = new QueryResponseList(Collections.emptyList(), 0L, "eq", 0L, false, null, 0, 10, 0); + + searchLogHelper.addSearchLog(params, now, "test-query-id", "test query", 0, 10, queryResponseList); + + assertFalse(searchLogHelper.searchLogQueue.isEmpty()); + final SearchLog searchLog = searchLogHelper.searchLogQueue.poll(); + assertEquals(Constants.SEARCH_LOG_ACCESS_TYPE_JSON, searchLog.getAccessType()); + assertEquals("test query", searchLog.getSearchWord()); + assertEquals("test-query-id", searchLog.getQueryId()); + assertEquals("127.0.0.1", searchLog.getClientIp()); + assertNull(searchLog.getVirtualHost()); + } + + private static class TestableSearchLogHelper extends SearchLogHelper { + private SearchLogContext testContext; + + @Override + protected SearchLogContext createSearchLogContext(final SearchRequestParams params, final FessConfig fessConfig) { + return testContext != null ? testContext : super.createSearchLogContext(params, fessConfig); + } + } + + // Mock classes for addSearchLog tests + + private static class MockSearchRequestParams extends SearchRequestParams { + @Override + public String getQuery() { + return "test query"; + } + + @Override + public Map getFields() { + return Collections.emptyMap(); + } + + @Override + public Map getConditions() { + return Collections.emptyMap(); + } + + @Override + public String[] getLanguages() { + return new String[0]; + } + + @Override + public GeoInfo getGeoInfo() { + return null; + } + + @Override + public FacetInfo getFacetInfo() { + return null; + } + + @Override + public HighlightInfo getHighlightInfo() { + return null; + } + + @Override + public String getSort() { + return null; + } + + @Override + public int getStartPosition() { + return 0; + } + + @Override + public int getPageSize() { + return 10; + } + + @Override + public int getOffset() { + return 0; + } + + @Override + public String[] getExtraQueries() { + return new String[0]; + } + + @Override + public Object getAttribute(final String name) { + return null; + } + + @Override + public SearchRequestType getType() { + return SearchRequestType.SEARCH; + } + + @Override + public String getSimilarDocHash() { + return null; + } + + @Override + public java.util.Locale getLocale() { + return java.util.Locale.ROOT; + } + } + // Mock classes private static class MockFessConfig extends FessConfig.SimpleImpl { private static final long serialVersionUID = 1L; @@ -212,4 +495,4 @@ public java.time.LocalDateTime getCurrentTimeAsLocalDateTime() { return java.time.LocalDateTime.now(); } } -} \ No newline at end of file +}