This is an automated email from the ASF dual-hosted git repository.
tbonelee pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/zeppelin.git
The following commit(s) were added to refs/heads/master by this push:
new a15b82c711 [ZEPPELIN-5994] Fix cron tests for ZeppelinRestApiTest
a15b82c711 is described below
commit a15b82c711e730e2fac08cb73bf775cc604a786d
Author: namuuCY <[email protected]>
AuthorDate: Thu Aug 6 11:07:33 2026 +0900
[ZEPPELIN-5994] Fix cron tests for ZeppelinRestApiTest
### What is this PR for?
This PR fixes and re-enables the cron REST API tests that were disabled in
`ZeppelinRestApiTest`.
Cron scheduling is disabled for security when Zeppelin runs in anonymous
mode, even if the cron configuration is enabled. This PR separates the cron
tests by authentication mode:
- Verify that cron creation and deletion are forbidden in anonymous mode.
- Add `AuthenticatedCronRestApiTest`, which starts Zeppelin with Shiro
authentication enabled.
- Verify the cron lifecycle for an authenticated administrator.
- Verify cron folder restrictions and invalid cron expression handling.
### What type of PR is it?
Bug Fix
### Todos
- [x] Verify that cron operations are rejected in anonymous mode.
- [x] Add authenticated cron REST API tests.
- [x] Test cron folder restrictions.
- [x] Test valid and invalid cron expressions.
### What is the Jira issue?
https://issues.apache.org/jira/browse/ZEPPELIN-5994
### How should this be tested?
Run the focused REST API tests:
```bash
./mvnw -pl zeppelin-server \
-Dtest=AuthenticatedCronRestApiTest \
test
./mvnw -pl zeppelin-server \
-Dtest='ZeppelinRestApiTest#testCronDisabledInAnonymousMode' \
test
```
### Questions:
- Does the license files need to update? No.
- Is there breaking changes for older versions? No.
- Does this needs documentation? No. This PR only updates test coverage.
Closes #5367 from namuuCY/master.
Signed-off-by: ChanHo Lee <[email protected]>
---
.../rest/AuthenticatedCronRestApiTest.java | 181 +++++++++++++++++++++
.../apache/zeppelin/rest/ZeppelinRestApiTest.java | 156 ++++--------------
2 files changed, 210 insertions(+), 127 deletions(-)
diff --git
a/zeppelin-server/src/test/java/org/apache/zeppelin/rest/AuthenticatedCronRestApiTest.java
b/zeppelin-server/src/test/java/org/apache/zeppelin/rest/AuthenticatedCronRestApiTest.java
new file mode 100644
index 0000000000..3aff15621f
--- /dev/null
+++
b/zeppelin-server/src/test/java/org/apache/zeppelin/rest/AuthenticatedCronRestApiTest.java
@@ -0,0 +1,181 @@
+/*
+ * 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.zeppelin.rest;
+
+import java.io.IOException;
+import java.util.Map;
+
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.zeppelin.MiniZeppelinServer;
+import org.apache.zeppelin.conf.ZeppelinConfiguration.ConfVars;
+import org.apache.zeppelin.notebook.Notebook;
+import org.apache.zeppelin.notebook.Paragraph;
+import org.apache.zeppelin.user.AuthenticationInfo;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class AuthenticatedCronRestApiTest extends AbstractTestRestApi {
+ private static final String ADMIN_USER = "admin";
+ private static final String ADMIN_PASSWORD = "password1";
+ private static final String VALID_CRON_REQUEST = "{\"cron\":\"0 0 0 1 1 ?
2099\"}";
+
+ private static MiniZeppelinServer zepServer;
+ private Notebook notebook;
+ private AuthenticationInfo admin;
+
+ @BeforeAll
+ static void init() throws Exception {
+ zepServer = new
MiniZeppelinServer(AuthenticatedCronRestApiTest.class.getSimpleName());
+ zepServer.addConfigFile("shiro.ini", ZEPPELIN_SHIRO);
+ zepServer.addInterpreter("md");
+ zepServer.getZeppelinConfiguration().setProperty(
+ ConfVars.ZEPPELIN_NOTEBOOK_CRON_ENABLE.getVarName(), "true");
+ zepServer.getZeppelinConfiguration().setProperty(
+ ConfVars.ZEPPELIN_NOTEBOOK_CRON_FOLDERS.getVarName(), "/System");
+ zepServer.start();
+ }
+
+ @AfterAll
+ static void destroy() throws Exception {
+ zepServer.destroy();
+ }
+
+ @BeforeEach
+ void setUp() {
+ zConf = zepServer.getZeppelinConfiguration();
+ notebook = zepServer.getService(Notebook.class);
+ admin = new AuthenticationInfo(ADMIN_USER);
+ }
+
+ @Test
+ void testCronForNonexistentNote() throws IOException {
+ try (
+ CloseableHttpResponse response =
+ httpPost(
+ "/notebook/cron/notexistnote",
+ VALID_CRON_REQUEST,
+ ADMIN_USER,
+ ADMIN_PASSWORD)) {
+ assertThat("", response, isNotFound());
+ }
+ }
+
+ @Test
+ void testCronLifecycleInConfiguredFolder() throws Exception {
+ String noteId = null;
+ try {
+ assertTrue(zConf.isAuthenticationEnabled());
+ assertTrue(zConf.isZeppelinNotebookCronEnable());
+ noteId =
notebook.createNote("/System/testCronLifecycleInConfiguredFolder", admin);
+ notebook.processNote(noteId,
+ note -> {
+ assertNotNull(note, "can't create new note");
+ note.setName("testCronLifecycleInConfiguredFolder");
+ Paragraph paragraph = note.addNewParagraph(admin);
+ Map<String, Object> config = paragraph.getConfig();
+ config.put("enabled", true);
+ paragraph.setConfig(config);
+ paragraph.setText("%md This is test paragraph.");
+ notebook.saveNote(note, admin);
+ return null;
+ });
+
+ try (
+ CloseableHttpResponse response =
+ httpPost(
+ "/notebook/cron/" + noteId,
+ VALID_CRON_REQUEST,
+ ADMIN_USER,
+ ADMIN_PASSWORD)) {
+ assertThat("", response, isAllowed());
+ }
+
+ try (
+ CloseableHttpResponse response =
+ httpGet(
+ "/notebook/cron/" + noteId,
+ ADMIN_USER,
+ ADMIN_PASSWORD)) {
+ assertThat("", response, isAllowed());
+ }
+
+ String invalidCronRequest = "{\"cron\":\"a * * * * ?\"}";
+ try (
+ CloseableHttpResponse response =
+ httpPost(
+ "/notebook/cron/" + noteId,
+ invalidCronRequest,
+ ADMIN_USER,
+ ADMIN_PASSWORD)) {
+ assertThat("", response, isBadRequest());
+ }
+
+ try (
+ CloseableHttpResponse response =
+ httpDelete(
+ "/notebook/cron/" + noteId,
+ ADMIN_USER,
+ ADMIN_PASSWORD)) {
+ assertThat("", response, isAllowed());
+ }
+ } finally {
+ if (noteId != null) {
+ notebook.removeNote(noteId, admin);
+ }
+ }
+ }
+
+ @Test
+ void testCronRejectedOutsideConfiguredFolder() throws Exception {
+ String noteId = null;
+ try {
+ noteId =
notebook.createNote("/Other/testCronRejectedOutsideConfiguredFolder", admin);
+ notebook.processNote(noteId,
+ note -> {
+ assertNotNull(note, "can't create new note");
+ note.setName("testCronRejectedOutsideConfiguredFolder");
+ Paragraph paragraph = note.addNewParagraph(admin);
+ Map<String, Object> config = paragraph.getConfig();
+ config.put("enabled", true);
+ paragraph.setConfig(config);
+ paragraph.setText("%md This is test paragraph.");
+ notebook.saveNote(note, admin);
+ return null;
+ });
+
+ try (
+ CloseableHttpResponse response =
+ httpPost(
+ "/notebook/cron/" + noteId,
+ VALID_CRON_REQUEST,
+ ADMIN_USER,
+ ADMIN_PASSWORD)) {
+ assertThat("", response, isForbidden());
+ }
+ } finally {
+ if (noteId != null) {
+ notebook.removeNote(noteId, admin);
+ }
+ }
+ }
+}
diff --git
a/zeppelin-server/src/test/java/org/apache/zeppelin/rest/ZeppelinRestApiTest.java
b/zeppelin-server/src/test/java/org/apache/zeppelin/rest/ZeppelinRestApiTest.java
index 5b3977c2ed..385050ba77 100644
---
a/zeppelin-server/src/test/java/org/apache/zeppelin/rest/ZeppelinRestApiTest.java
+++
b/zeppelin-server/src/test/java/org/apache/zeppelin/rest/ZeppelinRestApiTest.java
@@ -29,7 +29,6 @@ import org.apache.zeppelin.test.DownloadUtils;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
@@ -54,6 +53,7 @@ import org.apache.zeppelin.user.AuthenticationInfo;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -78,6 +78,8 @@ class ZeppelinRestApiTest extends AbstractTestRestApi {
zepServer.addInterpreter("sh");
zepServer.addInterpreter("spark");
zepServer.copyBinDir();
+ zepServer.getZeppelinConfiguration().setProperty(
+ ConfVars.ZEPPELIN_NOTEBOOK_CRON_ENABLE.getVarName(), "true");
zepServer.start();
TestHelper.configureSparkInterpreter(zepServer, sparkHome);
}
@@ -676,143 +678,43 @@ class ZeppelinRestApiTest extends AbstractTestRestApi {
}
}
- @Disabled // TODO(ZEPPELIN-5994): Fix and enable this test
@Test
- void testJobs() throws Exception {
- // create a note and a paragraph
+ void testCronDisabledInAnonymousMode() throws Exception {
String noteId = null;
try {
- System.setProperty(ConfVars.ZEPPELIN_NOTEBOOK_CRON_ENABLE.getVarName(),
"true");
- noteId = notebook.createNote("note1_testJobs", anonymous);
- // Use write lock, because name is overwritten
+ assertFalse(zConf.isAuthenticationEnabled());
+ assertFalse(zConf.isZeppelinNotebookCronEnable());
+ noteId = notebook.createNote("note1_testCronDisabledInAnonymousMode",
anonymous);
notebook.processNote(noteId,
- note -> {
- note.setName("note for run test");
- Paragraph paragraph =
note.addNewParagraph(AuthenticationInfo.ANONYMOUS);
- paragraph.setText("%md This is test paragraph.");
-
- Map<String, Object> config = paragraph.getConfig();
- config.put("enabled", true);
- paragraph.setConfig(config);
- return null;
- });
-
- notebook.processNote(noteId,
- note -> {
- try {
- note.runAll(AuthenticationInfo.ANONYMOUS, false, false, new
HashMap<>());
- } catch (Exception e) {
- fail();
- }
- return null;
- });
+ note -> {
+ assertNotNull(note, "can't create new note");
+ note.setName("note for anonymous cron test");
+ Paragraph paragraph =
note.addNewParagraph(AuthenticationInfo.ANONYMOUS);
+ Map<String, Object> config = paragraph.getConfig();
+ config.put("enabled", true);
+ paragraph.setConfig(config);
+ paragraph.setText("%md This is test paragraph.");
+ notebook.saveNote(note, anonymous);
+ return null;
+ });
String jsonRequest = "{\"cron\":\"* * * * * ?\" }";
- // right cron expression but not exist note.
- CloseableHttpResponse postCron = httpPost("/notebook/cron/notexistnote",
jsonRequest);
- assertThat("", postCron, isNotFound());
- postCron.close();
-
- // right cron expression.
- postCron = httpPost("/notebook/cron/" + noteId, jsonRequest);
- assertThat("", postCron, isAllowed());
- postCron.close();
- Thread.sleep(1000);
-
- // wrong cron expression.
- jsonRequest = "{\"cron\":\"a * * * * ?\" }";
- postCron = httpPost("/notebook/cron/" + noteId, jsonRequest);
- assertThat("", postCron, isBadRequest());
- postCron.close();
- Thread.sleep(1000);
-
- // remove cron job.
- CloseableHttpResponse deleteCron = httpDelete("/notebook/cron/" +
noteId);
- assertThat("", deleteCron, isAllowed());
- deleteCron.close();
- } finally {
- //cleanup
- if (null != noteId) {
- notebook.removeNote(noteId, anonymous);
+ try (
+ CloseableHttpResponse postCron =
+ httpPost(
+ "/notebook/cron/" + noteId,
+ jsonRequest)) {
+ assertThat("", postCron, isForbidden());
+ }
+ try (
+ CloseableHttpResponse deleteCron =
+ httpDelete("/notebook/cron/" + noteId)) {
+ assertThat("", deleteCron, isForbidden());
}
-
System.clearProperty(ConfVars.ZEPPELIN_NOTEBOOK_CRON_ENABLE.getVarName());
- }
- }
-
- @Disabled // TODO(ZEPPELIN-5994): Fix and enable this test
- @Test
- void testCronDisable() throws Exception {
- String noteId = null;
- try {
- // create a note and a paragraph
- System.setProperty(ConfVars.ZEPPELIN_NOTEBOOK_CRON_ENABLE.getVarName(),
"false");
- noteId = notebook.createNote("note1_testCronDisable", anonymous);
- // use write lock because Name is overwritten
- notebook.processNote(noteId,
- note -> {
- note.setName("note for run test");
- Paragraph paragraph =
note.addNewParagraph(AuthenticationInfo.ANONYMOUS);
- paragraph.setText("%md This is test paragraph.");
-
- Map<String, Object> config = paragraph.getConfig();
- config.put("enabled", true);
- paragraph.setConfig(config);
- return null;
- });
-
- notebook.processNote(noteId,
- note -> {
- try {
- note.runAll(AuthenticationInfo.ANONYMOUS, true, true, new
HashMap<>());
- } catch (Exception e) {
- fail();
- }
- return null;
- });
-
-
- String jsonRequest = "{\"cron\":\"* * * * * ?\" }";
- // right cron expression.
- CloseableHttpResponse postCron = httpPost("/notebook/cron/" + noteId,
jsonRequest);
- assertThat("", postCron, isForbidden());
- postCron.close();
-
- System.setProperty(ConfVars.ZEPPELIN_NOTEBOOK_CRON_ENABLE.getVarName(),
"true");
- System.setProperty(ConfVars.ZEPPELIN_NOTEBOOK_CRON_FOLDERS.getVarName(),
"/System");
-
- // use write lock, because Name is overwritten
- notebook.processNote(noteId,
- note -> {
- note.setName("System/test2");
- return null;
- });
- notebook.processNote(noteId,
- note -> {
- try {
- note.runAll(AuthenticationInfo.ANONYMOUS, true, true, new
HashMap<>());
- } catch (Exception e) {
- fail();
- }
- return null;
- });
- postCron = httpPost("/notebook/cron/" + noteId, jsonRequest);
- assertThat("", postCron, isAllowed());
- postCron.close();
- Thread.sleep(1000);
-
- // remove cron job.
- CloseableHttpResponse deleteCron = httpDelete("/notebook/cron/" +
noteId);
- assertThat("", deleteCron, isAllowed());
- deleteCron.close();
- Thread.sleep(1000);
-
-
System.clearProperty(ConfVars.ZEPPELIN_NOTEBOOK_CRON_FOLDERS.getVarName());
} finally {
- //cleanup
if (null != noteId) {
notebook.removeNote(noteId, anonymous);
}
-
System.clearProperty(ConfVars.ZEPPELIN_NOTEBOOK_CRON_ENABLE.getVarName());
}
}