This is an automated email from the ASF dual-hosted git repository.
j1wonpark pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/amoro.git
The following commit(s) were added to refs/heads/master by this push:
new eeaf68154 [AMORO-4401][AMS]Validate optimizer list pagination
parameters (#4404)
eeaf68154 is described below
commit eeaf68154125f4619aee417e72a7914ba5573a6e
Author: hutiefang76 <[email protected]>
AuthorDate: Sat Oct 3 06:35:07 2026 +0800
[AMORO-4401][AMS]Validate optimizer list pagination parameters (#4404)
* [AMS] Validate optimizer list pagination parameters
Generated-by: DeepSeek v4 Flash via Reasonix 1.39.4
* [AMS] Cover optimizer pagination boundaries
Generated-by: DeepSeek v4 Flash via Reasonix 1.39.4
---
.../controller/OptimizerGroupController.java | 26 ++-
.../controller/TestOptimizerGroupPagination.java | 224 +++++++++++++++++++++
2 files changed, 246 insertions(+), 4 deletions(-)
diff --git
a/amoro-ams/src/main/java/org/apache/amoro/server/dashboard/controller/OptimizerGroupController.java
b/amoro-ams/src/main/java/org/apache/amoro/server/dashboard/controller/OptimizerGroupController.java
index 2362018b6..586891d27 100644
---
a/amoro-ams/src/main/java/org/apache/amoro/server/dashboard/controller/OptimizerGroupController.java
+++
b/amoro-ams/src/main/java/org/apache/amoro/server/dashboard/controller/OptimizerGroupController.java
@@ -83,8 +83,17 @@ public class OptimizerGroupController {
String optimizerGroup = ctx.pathParam("optimizerGroup");
String dbFilterStr = ctx.queryParam("dbSearchInput");
String tableFilterStr = ctx.queryParam("tableSearchInput");
- Integer page = ctx.queryParamAsClass("page",
Integer.class).getOrDefault(1);
- Integer pageSize = ctx.queryParamAsClass("pageSize",
Integer.class).getOrDefault(20);
+ Integer pageSize =
+ ctx.queryParamAsClass("pageSize", Integer.class)
+ .check(size -> size >= 1, "pageSize must be greater than 0")
+ .getOrDefault(20);
+ Integer page =
+ ctx.queryParamAsClass("page", Integer.class)
+ .check(value -> value >= 1, "page must be greater than 0")
+ .check(
+ value -> ((long) value - 1) * pageSize <= Integer.MAX_VALUE,
+ "page offset exceeds int range")
+ .getOrDefault(1);
Set<String> actionFilter = new HashSet<>(ctx.queryParams("actions[]"));
int offset = (page - 1) * pageSize;
@@ -120,8 +129,17 @@ public class OptimizerGroupController {
/** get optimizers. */
public void getOptimizers(Context ctx) {
String optimizerGroup = ctx.pathParam("optimizerGroup");
- Integer page = ctx.queryParamAsClass("page",
Integer.class).getOrDefault(1);
- Integer pageSize = ctx.queryParamAsClass("pageSize",
Integer.class).getOrDefault(20);
+ Integer pageSize =
+ ctx.queryParamAsClass("pageSize", Integer.class)
+ .check(size -> size >= 1, "pageSize must be greater than 0")
+ .getOrDefault(20);
+ Integer page =
+ ctx.queryParamAsClass("page", Integer.class)
+ .check(value -> value >= 1, "page must be greater than 0")
+ .check(
+ value -> ((long) value - 1) * pageSize <= Integer.MAX_VALUE,
+ "page offset exceeds int range")
+ .getOrDefault(1);
int offset = (page - 1) * pageSize;
List<OptimizerInstance> optimizers;
diff --git
a/amoro-ams/src/test/java/org/apache/amoro/server/dashboard/controller/TestOptimizerGroupPagination.java
b/amoro-ams/src/test/java/org/apache/amoro/server/dashboard/controller/TestOptimizerGroupPagination.java
new file mode 100644
index 000000000..78003053e
--- /dev/null
+++
b/amoro-ams/src/test/java/org/apache/amoro/server/dashboard/controller/TestOptimizerGroupPagination.java
@@ -0,0 +1,224 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.amoro.server.dashboard.controller;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.isNull;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.when;
+
+import io.javalin.core.validation.ValidationException;
+import io.javalin.core.validation.Validator;
+import io.javalin.http.Context;
+import org.apache.amoro.server.dashboard.model.OptimizerInstanceInfo;
+import org.apache.amoro.server.dashboard.model.TableOptimizingInfo;
+import org.apache.amoro.server.dashboard.response.OkResponse;
+import org.apache.amoro.server.dashboard.response.PageResult;
+import org.apache.amoro.server.resource.OptimizerInstance;
+import org.apache.amoro.server.resource.OptimizerManager;
+import org.apache.amoro.server.table.TableManager;
+import org.apache.commons.lang3.tuple.Pair;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+import org.junit.jupiter.params.provider.ValueSource;
+import org.mockito.ArgumentCaptor;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class TestOptimizerGroupPagination {
+
+ private TableManager tableManager;
+ private OptimizerManager optimizerManager;
+ private OptimizerGroupController controller;
+ private Context ctx;
+ private final Map<String, String> queryValues = new HashMap<>();
+
+ @BeforeEach
+ void setUp() {
+ tableManager = mock(TableManager.class);
+ optimizerManager = mock(OptimizerManager.class);
+ controller = new OptimizerGroupController(tableManager, optimizerManager);
+ ctx = mock(Context.class);
+ queryValues.clear();
+ when(ctx.pathParam("optimizerGroup")).thenReturn("all");
+ when(ctx.queryParams("actions[]")).thenReturn(Collections.emptyList());
+ when(ctx.queryParamAsClass(anyString(), eq(Integer.class)))
+ .thenAnswer(
+ invocation ->
+ Validator.create(
+ Integer.class,
+ queryValues.get(invocation.getArgument(0, String.class)),
+ invocation.getArgument(0, String.class)));
+ }
+
+ private void usePagination(String page, String pageSize) {
+ queryValues.put("page", page);
+ queryValues.put("pageSize", pageSize);
+ }
+
+ private void stubEmptyTableQuery() {
+ when(tableManager.queryTableOptimizingInfo(any(), any(), any(), any(),
anyInt(), anyInt()))
+ .thenReturn(Pair.of(Collections.<TableOptimizingInfo>emptyList(), 0));
+ }
+
+ @ParameterizedTest
+ @CsvSource({"0,20", "-1,20", "1,0", "1,-1", "2147483647,20"})
+ void getOptimizersRejectsInvalidPagination(String page, String pageSize) {
+ usePagination(page, pageSize);
+
+ assertThrows(ValidationException.class, () ->
controller.getOptimizers(ctx));
+ verifyNoInteractions(optimizerManager, tableManager);
+ }
+
+ @ParameterizedTest
+ @CsvSource({"0,20", "-1,20", "1,0", "1,-1", "2147483647,20"})
+ void getOptimizerTablesRejectsInvalidPagination(String page, String
pageSize) {
+ usePagination(page, pageSize);
+
+ assertThrows(ValidationException.class, () ->
controller.getOptimizerTables(ctx));
+ verifyNoInteractions(optimizerManager, tableManager);
+ }
+
+ @Test
+ void getOptimizerTablesUsesDefaultPaginationAndFilters() {
+ stubEmptyTableQuery();
+
+ controller.getOptimizerTables(ctx);
+
+ verify(tableManager)
+ .queryTableOptimizingInfo(isNull(), isNull(), isNull(), isNull(),
eq(20), eq(0));
+ PageResult<?> pageResult = responsePageResult();
+ assertEquals(0, pageResult.getTotal());
+ assertTrue(pageResult.getList().isEmpty());
+ }
+
+ @Test
+ void getOptimizerTablesPassesPageAndSizeAsLimitAndOffset() {
+ stubEmptyTableQuery();
+ usePagination("3", "2");
+
+ controller.getOptimizerTables(ctx);
+
+ verify(tableManager)
+ .queryTableOptimizingInfo(isNull(), isNull(), isNull(), isNull(),
eq(2), eq(4));
+ }
+
+ @ParameterizedTest
+ @CsvSource({"2,2147483647,2147483647,2147483647",
"2147483647,1,1,2147483646"})
+ void getOptimizerTablesAcceptsBoundaryPagination(
+ String page, String pageSize, int expectedLimit, int expectedOffset) {
+ stubEmptyTableQuery();
+ usePagination(page, pageSize);
+
+ controller.getOptimizerTables(ctx);
+
+ verify(tableManager)
+ .queryTableOptimizingInfo(
+ isNull(), isNull(), isNull(), isNull(), eq(expectedLimit),
eq(expectedOffset));
+ PageResult<?> pageResult = responsePageResult();
+ assertEquals(0, pageResult.getTotal());
+ assertTrue(pageResult.getList().isEmpty());
+ }
+
+ @ParameterizedTest
+ @CsvSource({"2,2147483647", "2147483647,1"})
+ void getOptimizersAcceptsBoundaryPagination(String page, String pageSize) {
+
when(optimizerManager.listOptimizers()).thenReturn(Collections.emptyList());
+ usePagination(page, pageSize);
+
+ controller.getOptimizers(ctx);
+
+ PageResult<?> pageResult = responsePageResult();
+ assertEquals(0, pageResult.getTotal());
+ assertTrue(pageResult.getList().isEmpty());
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"all", "group1"})
+ void getOptimizersSortsAndPaginates(String optimizerGroup) {
+ when(ctx.pathParam("optimizerGroup")).thenReturn(optimizerGroup);
+ List<OptimizerInstance> unsorted =
+ Arrays.asList(optimizer("three", 3L), optimizer("one", 1L),
optimizer("two", 2L));
+ if ("all".equals(optimizerGroup)) {
+ when(optimizerManager.listOptimizers()).thenReturn(unsorted);
+ } else {
+
when(optimizerManager.listOptimizers(optimizerGroup)).thenReturn(unsorted);
+ }
+ usePagination("2", "1");
+
+ controller.getOptimizers(ctx);
+
+ if ("all".equals(optimizerGroup)) {
+ verify(optimizerManager).listOptimizers();
+ } else {
+ verify(optimizerManager).listOptimizers(optimizerGroup);
+ }
+ PageResult<?> pageResult = responsePageResult();
+ assertEquals(3, pageResult.getTotal());
+ assertEquals(1, pageResult.getList().size());
+ assertEquals("two", optimizerToken(pageResult.getList().get(0)));
+ }
+
+ @Test
+ void getOptimizersReturnsFirstPageByDefault() {
+ List<OptimizerInstance> optimizers =
Collections.singletonList(optimizer("one", 1L));
+ when(optimizerManager.listOptimizers()).thenReturn(optimizers);
+
+ controller.getOptimizers(ctx);
+
+ PageResult<?> pageResult = responsePageResult();
+ assertEquals(1, pageResult.getTotal());
+ assertEquals(1, pageResult.getList().size());
+ assertEquals("one", optimizerToken(pageResult.getList().get(0)));
+ }
+
+ private static OptimizerInstance optimizer(String token, long startTime) {
+ OptimizerInstance instance = mock(OptimizerInstance.class);
+ when(instance.getToken()).thenReturn(token);
+ when(instance.getStartTime()).thenReturn(startTime);
+ return instance;
+ }
+
+ private static String optimizerToken(Object info) {
+ return ((OptimizerInstanceInfo) info).getToken();
+ }
+
+ private PageResult<?> responsePageResult() {
+ ArgumentCaptor<Object> jsonCaptor = ArgumentCaptor.forClass(Object.class);
+ verify(ctx).json(jsonCaptor.capture());
+ Object payload = jsonCaptor.getValue();
+ assertNotNull(payload);
+ return (PageResult<?>) ((OkResponse<?>) payload).getResult();
+ }
+}