diff options
Diffstat (limited to 'opendc-web/opendc-web-server/src/test/java/org/opendc/web/server')
11 files changed, 1966 insertions, 0 deletions
diff --git a/opendc-web/opendc-web-server/src/test/java/org/opendc/web/server/rest/SchedulerResourceTest.java b/opendc-web/opendc-web-server/src/test/java/org/opendc/web/server/rest/SchedulerResourceTest.java new file mode 100644 index 00000000..feeac4d3 --- /dev/null +++ b/opendc-web/opendc-web-server/src/test/java/org/opendc/web/server/rest/SchedulerResourceTest.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2023 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.web.server.rest; + +import static io.restassured.RestAssured.when; + +import io.quarkus.test.common.http.TestHTTPEndpoint; +import io.quarkus.test.junit.QuarkusTest; +import io.restassured.http.ContentType; +import org.junit.jupiter.api.Test; + +/** + * Test suite for {@link SchedulerResource}. + */ +@QuarkusTest +@TestHTTPEndpoint(SchedulerResource.class) +public final class SchedulerResourceTest { + /** + * Test to verify whether we can obtain all schedulers. + */ + @Test + public void testGetSchedulers() { + when().get().then().statusCode(200).contentType(ContentType.JSON); + } +} diff --git a/opendc-web/opendc-web-server/src/test/java/org/opendc/web/server/rest/TraceResourceTest.java b/opendc-web/opendc-web-server/src/test/java/org/opendc/web/server/rest/TraceResourceTest.java new file mode 100644 index 00000000..5c5976db --- /dev/null +++ b/opendc-web/opendc-web-server/src/test/java/org/opendc/web/server/rest/TraceResourceTest.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2023 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.web.server.rest; + +import static io.restassured.RestAssured.when; +import static org.hamcrest.Matchers.equalTo; + +import io.quarkus.test.common.http.TestHTTPEndpoint; +import io.quarkus.test.junit.QuarkusTest; +import io.restassured.http.ContentType; +import org.junit.jupiter.api.Test; + +/** + * Test suite for {@link TraceResource}. + */ +@QuarkusTest +@TestHTTPEndpoint(TraceResource.class) +public final class TraceResourceTest { + /** + * Test that tries to obtain all traces. + */ + @Test + public void testGetAllEmpty() { + when().get().then().statusCode(200).contentType(ContentType.JSON); + } + + /** + * Test that tries to obtain a non-existent trace. + */ + @Test + public void testGetNonExisting() { + when().get("/unknown").then().statusCode(404).contentType(ContentType.JSON); + } + + /** + * Test that tries to obtain an existing trace. + */ + @Test + public void testGetExisting() { + when().get("/bitbrains-small") + .then() + .statusCode(200) + .contentType(ContentType.JSON) + .body("name", equalTo("Bitbrains Small")); + } +} diff --git a/opendc-web/opendc-web-server/src/test/java/org/opendc/web/server/rest/runner/JobResourceTest.java b/opendc-web/opendc-web-server/src/test/java/org/opendc/web/server/rest/runner/JobResourceTest.java new file mode 100644 index 00000000..94b2cef0 --- /dev/null +++ b/opendc-web/opendc-web-server/src/test/java/org/opendc/web/server/rest/runner/JobResourceTest.java @@ -0,0 +1,145 @@ +/* + * Copyright (c) 2023 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.web.server.rest.runner; + +import static io.restassured.RestAssured.given; +import static io.restassured.RestAssured.when; +import static org.hamcrest.Matchers.equalTo; + +import io.quarkus.test.common.http.TestHTTPEndpoint; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.security.TestSecurity; +import io.restassured.http.ContentType; +import org.junit.jupiter.api.Test; +import org.opendc.web.proto.JobState; + +/** + * Test suite for {@link JobResource}. + */ +@QuarkusTest +@TestHTTPEndpoint(JobResource.class) +public final class JobResourceTest { + /** + * Test that tries to query the pending jobs without token. + */ + @Test + public void testQueryWithoutToken() { + when().get().then().statusCode(401); + } + + /** + * Test that tries to query the pending jobs for a user. + */ + @Test + @TestSecurity( + user = "test", + roles = {"openid"}) + public void testQueryInvalidScope() { + when().get().then().statusCode(403); + } + + /** + * Test that tries to query the pending jobs for a runner. + */ + @Test + @TestSecurity( + user = "test", + roles = {"runner"}) + public void testQuery() { + when().get().then().statusCode(200).contentType(ContentType.JSON).body("get(0).state", equalTo("PENDING")); + } + + /** + * Test that tries to obtain a non-existent job. + */ + @Test + @TestSecurity( + user = "test", + roles = {"runner"}) + public void testGetNonExisting() { + when().get("/0").then().statusCode(404).contentType(ContentType.JSON); + } + + /** + * Test that tries to obtain a job. + */ + @Test + @TestSecurity( + user = "test", + roles = {"runner"}) + public void testGetExisting() { + when().get("/1").then().statusCode(200).contentType(ContentType.JSON).body("id", equalTo(1)); + } + + /** + * Test that tries to update a non-existent job. + */ + @Test + @TestSecurity( + user = "test", + roles = {"runner"}) + public void testUpdateNonExistent() { + given().body(new org.opendc.web.proto.runner.Job.Update(JobState.PENDING, 0, null)) + .contentType(ContentType.JSON) + .when() + .post("/0") + .then() + .statusCode(404) + .contentType(ContentType.JSON); + } + + /** + * Test that tries to update a job. + */ + @Test + @TestSecurity( + user = "test", + roles = {"runner"}) + public void testUpdateState() { + given().body(new org.opendc.web.proto.runner.Job.Update(JobState.CLAIMED, 0, null)) + .contentType(ContentType.JSON) + .when() + .post("/2") + .then() + .statusCode(200) + .contentType(ContentType.JSON) + .body("state", equalTo(JobState.CLAIMED.toString())); + } + + /** + * Test that tries to update a job with invalid input. + */ + @Test + @TestSecurity( + user = "test", + roles = {"runner"}) + public void testUpdateInvalidInput() { + given().body("{ \"test\": \"test\" }") + .contentType(ContentType.JSON) + .when() + .post("/1") + .then() + .statusCode(400) + .contentType(ContentType.JSON); + } +} diff --git a/opendc-web/opendc-web-server/src/test/java/org/opendc/web/server/rest/user/PortfolioResourceTest.java b/opendc-web/opendc-web-server/src/test/java/org/opendc/web/server/rest/user/PortfolioResourceTest.java new file mode 100644 index 00000000..a952d83f --- /dev/null +++ b/opendc-web/opendc-web-server/src/test/java/org/opendc/web/server/rest/user/PortfolioResourceTest.java @@ -0,0 +1,287 @@ +/* + * Copyright (c) 2023 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.web.server.rest.user; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.equalTo; + +import io.quarkus.test.common.http.TestHTTPEndpoint; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.security.TestSecurity; +import io.restassured.http.ContentType; +import java.util.Set; +import org.junit.jupiter.api.Test; +import org.opendc.web.proto.Targets; + +/** + * Test suite for {@link PortfolioResource}. + */ +@QuarkusTest +@TestHTTPEndpoint(PortfolioResource.class) +public final class PortfolioResourceTest { + /** + * Test that tries to obtain the list of portfolios belonging to a project. + */ + @Test + @TestSecurity( + user = "owner", + roles = {"openid"}) + public void testGetForProject() { + given().pathParam("project", 1).when().get().then().statusCode(200).contentType(ContentType.JSON); + } + + /** + * Test that tries to obtain the list of portfolios belonging to a project without authorization. + */ + @Test + @TestSecurity( + user = "unknown", + roles = {"openid"}) + public void testGetForProjectNoAuthorization() { + given().pathParam("project", 1).when().get().then().statusCode(200).contentType(ContentType.JSON); + } + + /** + * Test that tries to create a topology for a project. + */ + @Test + @TestSecurity( + user = "owner", + roles = {"openid"}) + public void testCreateNonExistent() { + given().pathParam("project", "0") + .body(new org.opendc.web.proto.user.Portfolio.Create("test", new Targets(Set.of(), 1))) + .contentType(ContentType.JSON) + .when() + .post() + .then() + .statusCode(404) + .contentType(ContentType.JSON); + } + + /** + * Test that tries to create a topology for a project. + */ + @Test + @TestSecurity( + user = "viewer", + roles = {"openid"}) + public void testCreateNotPermitted() { + given().pathParam("project", "1") + .body(new org.opendc.web.proto.user.Portfolio.Create("test", new Targets(Set.of(), 1))) + .contentType(ContentType.JSON) + .when() + .post() + .then() + .statusCode(403) + .contentType(ContentType.JSON); + } + + /** + * Test that tries to create a portfolio for a project. + */ + @Test + @TestSecurity( + user = "editor", + roles = {"openid"}) + public void testCreate() { + given().pathParam("project", "1") + .body(new org.opendc.web.proto.user.Portfolio.Create("test", new Targets(Set.of(), 1))) + .contentType(ContentType.JSON) + .when() + .post() + .then() + .statusCode(200) + .contentType(ContentType.JSON) + .body("name", equalTo("test")); + } + + /** + * Test to create a portfolio with an empty body. + */ + @Test + @TestSecurity( + user = "editor", + roles = {"openid"}) + public void testCreateEmpty() { + given().pathParam("project", "1") + .body("{}") + .contentType(ContentType.JSON) + .when() + .post() + .then() + .statusCode(400) + .contentType(ContentType.JSON); + } + + /** + * Test to create a portfolio with a blank name. + */ + @Test + @TestSecurity( + user = "editor", + roles = {"openid"}) + public void testCreateBlankName() { + given().pathParam("project", "1") + .body(new org.opendc.web.proto.user.Portfolio.Create("", new Targets(Set.of(), 1))) + .contentType(ContentType.JSON) + .when() + .post() + .then() + .statusCode(400) + .contentType(ContentType.JSON); + } + + /** + * Test that tries to obtain a portfolio without token. + */ + @Test + public void testGetWithoutToken() { + given().pathParam("project", "1").when().get("/1").then().statusCode(401); + } + + /** + * Test that tries to obtain a portfolio with an invalid scope. + */ + @Test + @TestSecurity( + user = "owner", + roles = {"runner"}) + public void testGetInvalidToken() { + given().pathParam("project", "1").when().get("/1").then().statusCode(403); + } + + /** + * Test that tries to obtain a non-existent portfolio. + */ + @Test + @TestSecurity( + user = "owner", + roles = {"openid"}) + public void testGetNonExisting() { + given().pathParam("project", "1") + .when() + .get("/0") + .then() + .statusCode(404) + .contentType(ContentType.JSON); + } + + /** + * Test that tries to obtain a portfolio for a non-existent project. + */ + @Test + @TestSecurity( + user = "owner", + roles = {"openid"}) + public void testGetNonExistingProject() { + given().pathParam("project", "0") + .when() + .get("/1") + .then() + .statusCode(404) + .contentType(ContentType.JSON); + } + + /** + * Test that tries to obtain a portfolio. + */ + @Test + @TestSecurity( + user = "owner", + roles = {"openid"}) + public void testGetExisting() { + given().pathParam("project", "1") + .when() + .get("/1") + .then() + .statusCode(200) + .contentType(ContentType.JSON) + .body("id", equalTo(1)); + } + + /** + * Test to delete a non-existent portfolio. + */ + @Test + @TestSecurity( + user = "owner", + roles = {"openid"}) + public void testDeleteNonExistent() { + given().pathParam("project", "1").when().delete("/0").then().statusCode(404); + } + + /** + * Test to delete a portfolio on a non-existent project. + */ + @Test + @TestSecurity( + user = "owner", + roles = {"openid"}) + public void testDeleteNonExistentProject() { + given().pathParam("project", "0").when().delete("/1").then().statusCode(404); + } + + /** + * Test to delete a portfolio. + */ + @Test + @TestSecurity( + user = "owner", + roles = {"openid"}) + public void testDelete() { + int number = given().pathParam("project", "1") + .body(new org.opendc.web.proto.user.Portfolio.Create("Delete Portfolio", new Targets(Set.of(), 1))) + .contentType(ContentType.JSON) + .when() + .post() + .then() + .statusCode(200) + .contentType(ContentType.JSON) + .extract() + .path("number"); + + given().pathParam("project", "1") + .when() + .delete("/" + number) + .then() + .statusCode(200) + .contentType(ContentType.JSON); + } + + /** + * Test to delete a portfolio as a viewer. + */ + @Test + @TestSecurity( + user = "viewer", + roles = {"openid"}) + public void testDeleteAsViewer() { + given().pathParam("project", "1") + .when() + .delete("/1") + .then() + .statusCode(403) + .contentType(ContentType.JSON); + } +} diff --git a/opendc-web/opendc-web-server/src/test/java/org/opendc/web/server/rest/user/PortfolioScenarioResourceTest.java b/opendc-web/opendc-web-server/src/test/java/org/opendc/web/server/rest/user/PortfolioScenarioResourceTest.java new file mode 100644 index 00000000..4f8d412c --- /dev/null +++ b/opendc-web/opendc-web-server/src/test/java/org/opendc/web/server/rest/user/PortfolioScenarioResourceTest.java @@ -0,0 +1,273 @@ +/* + * Copyright (c) 2023 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.web.server.rest.user; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.equalTo; + +import io.quarkus.test.common.http.TestHTTPEndpoint; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.security.TestSecurity; +import io.restassured.http.ContentType; +import org.junit.jupiter.api.Test; +import org.opendc.web.proto.OperationalPhenomena; +import org.opendc.web.proto.Workload; +import org.opendc.web.proto.user.Scenario; + +/** + * Test suite for {@link PortfolioScenarioResource}. + */ +@QuarkusTest +@TestHTTPEndpoint(PortfolioScenarioResource.class) +public final class PortfolioScenarioResourceTest { + /** + * Test that tries to obtain a portfolio without token. + */ + @Test + public void testGetWithoutToken() { + given().pathParam("project", "1") + .pathParam("portfolio", "1") + .when() + .get() + .then() + .statusCode(401); + } + + /** + * Test that tries to obtain a portfolio with an invalid scope. + */ + @Test + @TestSecurity( + user = "owner", + roles = {"runner"}) + public void testGetInvalidToken() { + given().pathParam("project", "1") + .pathParam("portfolio", "1") + .when() + .get() + .then() + .statusCode(403); + } + + /** + * Test that tries to obtain a scenario without authorization. + */ + @Test + @TestSecurity( + user = "unknown", + roles = {"openid"}) + public void testGetUnauthorized() { + given().pathParam("project", "1") + .pathParam("portfolio", "1") + .when() + .get() + .then() + .statusCode(200) + .contentType(ContentType.JSON); + } + + /** + * Test that tries to obtain a scenario. + */ + @Test + @TestSecurity( + user = "owner", + roles = {"openid"}) + public void testGet() { + given().pathParam("project", "1") + .pathParam("portfolio", "1") + .when() + .get() + .then() + .statusCode(200) + .contentType(ContentType.JSON); + } + + /** + * Test that tries to create a scenario for a portfolio. + */ + @Test + @TestSecurity( + user = "owner", + roles = {"openid"}) + public void testCreateNonExistent() { + given().pathParam("project", "1") + .pathParam("portfolio", "0") + .body(new Scenario.Create( + "test", new Workload.Spec("test", 1.0), 1, new OperationalPhenomena(false, false), "test")) + .contentType(ContentType.JSON) + .when() + .post() + .then() + .statusCode(404) + .contentType(ContentType.JSON); + } + + /** + * Test that tries to create a scenario for a portfolio without authorization. + */ + @Test + @TestSecurity( + user = "unknown", + roles = {"openid"}) + public void testCreateUnauthorized() { + given().pathParam("project", "1") + .pathParam("portfolio", "0") + .body(new Scenario.Create( + "test", new Workload.Spec("test", 1.0), 1, new OperationalPhenomena(false, false), "test")) + .contentType(ContentType.JSON) + .when() + .post() + .then() + .statusCode(404) + .contentType(ContentType.JSON); + } + + /** + * Test that tries to create a scenario for a portfolio as a viewer. + */ + @Test + @TestSecurity( + user = "viewer", + roles = {"openid"}) + public void testCreateAsViewer() { + given().pathParam("project", "1") + .pathParam("portfolio", "0") + .body(new Scenario.Create( + "test", new Workload.Spec("test", 1.0), 1, new OperationalPhenomena(false, false), "test")) + .contentType(ContentType.JSON) + .when() + .post() + .then() + .statusCode(403) + .contentType(ContentType.JSON); + } + + /** + * Test that tries to create a scenario for a portfolio. + */ + @Test + @TestSecurity( + user = "owner", + roles = {"openid"}) + public void testCreate() { + given().pathParam("project", "1") + .pathParam("portfolio", "1") + .body(new Scenario.Create( + "test", + new Workload.Spec("bitbrains-small", 1.0), + 1, + new OperationalPhenomena(false, false), + "test")) + .contentType(ContentType.JSON) + .when() + .post() + .then() + .statusCode(200) + .contentType(ContentType.JSON) + .body("name", equalTo("test")); + } + + /** + * Test to create a project with an empty body. + */ + @Test + @TestSecurity( + user = "owner", + roles = {"openid"}) + public void testCreateEmpty() { + given().pathParam("project", "1") + .pathParam("portfolio", "1") + .body("{}") + .contentType(ContentType.JSON) + .when() + .post() + .then() + .statusCode(400) + .contentType(ContentType.JSON); + } + + /** + * Test to create a project with a blank name. + */ + @Test + @TestSecurity( + user = "owner", + roles = {"openid"}) + public void testCreateBlankName() { + given().pathParam("project", "1") + .pathParam("portfolio", "1") + .body(new Scenario.Create( + "", new Workload.Spec("test", 1.0), 1, new OperationalPhenomena(false, false), "test")) + .contentType(ContentType.JSON) + .when() + .post() + .then() + .statusCode(400) + .contentType(ContentType.JSON); + } + + /** + * Test that tries to create a scenario for a portfolio. + */ + @Test + @TestSecurity( + user = "owner", + roles = {"openid"}) + public void testCreateUnknownTopology() { + given().pathParam("project", "1") + .pathParam("portfolio", "1") + .body(new Scenario.Create( + "test", + new Workload.Spec("bitbrains-small", 1.0), + -1, + new OperationalPhenomena(false, false), + "test")) + .contentType(ContentType.JSON) + .when() + .post() + .then() + .statusCode(400) + .contentType(ContentType.JSON); + } + + /** + * Test that tries to create a scenario for a portfolio. + */ + @Test + @TestSecurity( + user = "owner", + roles = {"openid"}) + public void testCreateUnknownTrace() { + given().pathParam("project", "1") + .pathParam("portfolio", "1") + .body(new Scenario.Create( + "test", new Workload.Spec("unknown", 1.0), 1, new OperationalPhenomena(false, false), "test")) + .contentType(ContentType.JSON) + .when() + .post() + .then() + .statusCode(400) + .contentType(ContentType.JSON); + } +} diff --git a/opendc-web/opendc-web-server/src/test/java/org/opendc/web/server/rest/user/ProjectResourceTest.java b/opendc-web/opendc-web-server/src/test/java/org/opendc/web/server/rest/user/ProjectResourceTest.java new file mode 100644 index 00000000..8bd60808 --- /dev/null +++ b/opendc-web/opendc-web-server/src/test/java/org/opendc/web/server/rest/user/ProjectResourceTest.java @@ -0,0 +1,187 @@ +/* + * Copyright (c) 2023 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.web.server.rest.user; + +import static io.restassured.RestAssured.given; +import static io.restassured.RestAssured.when; +import static org.hamcrest.Matchers.equalTo; + +import io.quarkus.test.common.http.TestHTTPEndpoint; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.security.TestSecurity; +import io.restassured.http.ContentType; +import org.junit.jupiter.api.Test; + +/** + * Test suite for [ProjectResource]. + */ +@QuarkusTest +@TestHTTPEndpoint(ProjectResource.class) +public final class ProjectResourceTest { + /** + * Test that tries to obtain all projects without token. + */ + @Test + public void testGetAllWithoutToken() { + when().get().then().statusCode(401); + } + + /** + * Test that tries to obtain all projects with an invalid scope. + */ + @Test + @TestSecurity( + user = "owner", + roles = {"runner"}) + public void testGetAllWithInvalidScope() { + when().get().then().statusCode(403); + } + + /** + * Test that tries to obtain all project for a user. + */ + @Test + @TestSecurity( + user = "owner", + roles = {"openid"}) + public void testGetAll() { + when().get().then().statusCode(200).contentType(ContentType.JSON).body("get(0).name", equalTo("Test Project")); + } + + /** + * Test that tries to obtain a non-existent project. + */ + @Test + @TestSecurity( + user = "owner", + roles = {"openid"}) + public void testGetNonExisting() { + when().get("/0").then().statusCode(404).contentType(ContentType.JSON); + } + + /** + * Test that tries to obtain a project. + */ + @Test + @TestSecurity( + user = "owner", + roles = {"openid"}) + public void testGetExisting() { + when().get("/1").then().statusCode(200).contentType(ContentType.JSON).body("id", equalTo(1)); + } + + /** + * Test that tries to create a project. + */ + @Test + @TestSecurity( + user = "owner", + roles = {"openid"}) + public void testCreate() { + given().body(new org.opendc.web.proto.user.Project.Create("test")) + .contentType(ContentType.JSON) + .when() + .post() + .then() + .statusCode(200) + .contentType(ContentType.JSON) + .body("name", equalTo("test")); + } + + /** + * Test to create a project with an empty body. + */ + @Test + @TestSecurity( + user = "owner", + roles = {"openid"}) + public void testCreateEmpty() { + given().body("{}") + .contentType(ContentType.JSON) + .when() + .post() + .then() + .statusCode(400) + .contentType(ContentType.JSON); + } + + /** + * Test to create a project with a blank name. + */ + @Test + @TestSecurity( + user = "owner", + roles = {"openid"}) + public void testCreateBlankName() { + given().body(new org.opendc.web.proto.user.Project.Create("")) + .contentType(ContentType.JSON) + .when() + .post() + .then() + .statusCode(400) + .contentType(ContentType.JSON); + } + + /** + * Test to delete a non-existent project. + */ + @Test + @TestSecurity( + user = "owner", + roles = {"openid"}) + public void testDeleteNonExistent() { + when().delete("/0").then().statusCode(404).contentType(ContentType.JSON); + } + + /** + * Test to delete a project. + */ + @Test + @TestSecurity( + user = "owner", + roles = {"openid"}) + public void testDelete() { + int id = given().body(new org.opendc.web.proto.user.Project.Create("Delete Project")) + .contentType(ContentType.JSON) + .when() + .post() + .then() + .statusCode(200) + .contentType(ContentType.JSON) + .extract() + .path("id"); + + when().delete("/" + id).then().statusCode(200).contentType(ContentType.JSON); + } + + /** + * Test to delete a project which the user does not own. + */ + @Test + @TestSecurity( + user = "viewer", + roles = {"openid"}) + public void testDeleteNonOwner() { + when().delete("/1").then().statusCode(403).contentType(ContentType.JSON); + } +} diff --git a/opendc-web/opendc-web-server/src/test/java/org/opendc/web/server/rest/user/ScenarioResourceTest.java b/opendc-web/opendc-web-server/src/test/java/org/opendc/web/server/rest/user/ScenarioResourceTest.java new file mode 100644 index 00000000..a980e4e2 --- /dev/null +++ b/opendc-web/opendc-web-server/src/test/java/org/opendc/web/server/rest/user/ScenarioResourceTest.java @@ -0,0 +1,203 @@ +/* + * Copyright (c) 2023 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.web.server.rest.user; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.equalTo; + +import io.quarkus.test.common.http.TestHTTPEndpoint; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.security.TestSecurity; +import io.restassured.builder.RequestSpecBuilder; +import io.restassured.http.ContentType; +import io.restassured.specification.RequestSpecification; +import org.junit.jupiter.api.Test; +import org.opendc.web.proto.OperationalPhenomena; +import org.opendc.web.proto.Workload; +import org.opendc.web.proto.user.Scenario; + +/** + * Test suite for {@link ScenarioResource}. + */ +@QuarkusTest +@TestHTTPEndpoint(ScenarioResource.class) +public final class ScenarioResourceTest { + /** + * Test that tries to obtain all scenarios belonging to a project without authorization. + */ + @Test + @TestSecurity( + user = "unknown", + roles = {"openid"}) + public void testGetAllUnauthorized() { + given().pathParam("project", "1").when().get().then().statusCode(404).contentType(ContentType.JSON); + } + + /** + * Test that tries to obtain all scenarios belonging to a project. + */ + @Test + @TestSecurity( + user = "owner", + roles = {"openid"}) + public void testGetAll() { + given().pathParam("project", "1").when().get().then().statusCode(200).contentType(ContentType.JSON); + } + + /** + * Test that tries to obtain a scenario without token. + */ + @Test + public void testGetWithoutToken() { + given().pathParam("project", "1").when().get("/1").then().statusCode(401); + } + + /** + * Test that tries to obtain a scenario with an invalid scope. + */ + @Test + @TestSecurity( + user = "owner", + roles = {"runner"}) + public void testGetInvalidToken() { + given().pathParam("project", "1").when().get("/1").then().statusCode(403); + } + + /** + * Test that tries to obtain a non-existent scenario. + */ + @Test + @TestSecurity( + user = "owner", + roles = {"openid"}) + public void testGetNonExisting() { + given().pathParam("project", "1") + .when() + .get("/0") + .then() + .statusCode(404) + .contentType(ContentType.JSON); + } + + /** + * Test that tries to obtain a scenario. + */ + @Test + @TestSecurity( + user = "unknown", + roles = {"openid"}) + public void testGetExistingUnauthorized() { + given().pathParam("project", "1") + .when() + .get("/1") + .then() + .statusCode(404) + .contentType(ContentType.JSON); + } + + /** + * Test that tries to obtain a scenario. + */ + @Test + @TestSecurity( + user = "owner", + roles = {"openid"}) + public void testGetExisting() { + given().pathParam("project", "1") + .when() + .get("/1") + .then() + .statusCode(200) + .contentType(ContentType.JSON) + .body("id", equalTo(1)); + } + + /** + * Test to delete a non-existent scenario. + */ + @Test + @TestSecurity( + user = "owner", + roles = {"openid"}) + public void testDeleteNonExistent() { + given().pathParam("project", "1").when().delete("/0").then().statusCode(404); + } + + /** + * Test to delete a scenario without authorization. + */ + @Test + @TestSecurity( + user = "unknown", + roles = {"openid"}) + public void testDeleteUnauthorized() { + given().pathParam("project", "1").when().delete("/1").then().statusCode(404); + } + + /** + * Test to delete a scenario as a viewer. + */ + @Test + @TestSecurity( + user = "viewer", + roles = {"openid"}) + public void testDeleteAsViewer() { + given().pathParam("project", "1").when().delete("/1").then().statusCode(403); + } + + /** + * Test to delete a scenario. + */ + @Test + @TestSecurity( + user = "owner", + roles = {"openid"}) + public void testDelete() { + RequestSpecification spec = new RequestSpecBuilder() + .setBasePath("/projects/1/portfolios/1/scenarios") + .build(); + + int number = given(spec) + .body(new Scenario.Create( + "test", + new Workload.Spec("bitbrains-small", 1.0), + 1, + new OperationalPhenomena(false, false), + "test")) + .contentType(ContentType.JSON) + .when() + .post() + .then() + .statusCode(200) + .contentType(ContentType.JSON) + .extract() + .path("number"); + + given().pathParam("project", "1") + .when() + .delete("/" + number) + .then() + .statusCode(200) + .contentType(ContentType.JSON); + } +} diff --git a/opendc-web/opendc-web-server/src/test/java/org/opendc/web/server/rest/user/TopologyResourceTest.java b/opendc-web/opendc-web-server/src/test/java/org/opendc/web/server/rest/user/TopologyResourceTest.java new file mode 100644 index 00000000..21e35b09 --- /dev/null +++ b/opendc-web/opendc-web-server/src/test/java/org/opendc/web/server/rest/user/TopologyResourceTest.java @@ -0,0 +1,358 @@ +/* + * Copyright (c) 2023 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.web.server.rest.user; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.equalTo; + +import io.quarkus.test.common.http.TestHTTPEndpoint; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.security.TestSecurity; +import io.restassured.http.ContentType; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.opendc.web.proto.user.Topology; + +/** + * Test suite for {@link TopologyResource}. + */ +@QuarkusTest +@TestHTTPEndpoint(TopologyResource.class) +public final class TopologyResourceTest { + /** + * Test that tries to obtain the list of topologies of a project without proper authorization. + */ + @Test + @TestSecurity( + user = "unknown", + roles = {"openid"}) + public void testGetAllWithoutAuth() { + given().pathParam("project", "1") + .when() + .get() + .then() + .statusCode(200) + .contentType(ContentType.JSON) + .body(equalTo("[]")); + } + + /** + * Test that tries to obtain the list of topologies belonging to a project. + */ + @Test + @TestSecurity( + user = "owner", + roles = {"openid"}) + public void testGetAll() { + given().pathParam("project", "1").when().get().then().statusCode(200).contentType(ContentType.JSON); + } + + /** + * Test that tries to create a topology for a project. + */ + @Test + @TestSecurity( + user = "owner", + roles = {"openid"}) + public void testCreateNonExistent() { + given().pathParam("project", "0") + .body(new Topology.Create("test", List.of())) + .contentType(ContentType.JSON) + .when() + .post() + .then() + .statusCode(404) + .contentType(ContentType.JSON); + } + + /** + * Test that tries to create a topology for a project as viewer. + */ + @Test + @TestSecurity( + user = "viewer", + roles = {"openid"}) + public void testCreateUnauthorized() { + given().pathParam("project", "1") + .body(new Topology.Create("test", List.of())) + .contentType(ContentType.JSON) + .when() + .post() + .then() + .statusCode(403) + .contentType(ContentType.JSON); + } + + /** + * Test that tries to create a topology for a project. + */ + @Test + @TestSecurity( + user = "owner", + roles = {"openid"}) + public void testCreate() { + given().pathParam("project", "1") + .body(new Topology.Create("test", List.of())) + .contentType(ContentType.JSON) + .when() + .post() + .then() + .statusCode(200) + .contentType(ContentType.JSON) + .body("name", equalTo("test")); + } + + /** + * Test to create a topology with an empty body. + */ + @Test + @TestSecurity( + user = "owner", + roles = {"openid"}) + public void testCreateEmpty() { + given().pathParam("project", "1") + .body("{}") + .contentType(ContentType.JSON) + .when() + .post() + .then() + .statusCode(400) + .contentType(ContentType.JSON); + } + + /** + * Test to create a topology with a blank name. + */ + @Test + @TestSecurity( + user = "owner", + roles = {"openid"}) + public void testCreateBlankName() { + given().pathParam("project", "1") + .body(new Topology.Create("", List.of())) + .contentType(ContentType.JSON) + .when() + .post() + .then() + .statusCode(400) + .contentType(ContentType.JSON); + } + + /** + * Test that tries to obtain a topology without token. + */ + @Test + public void testGetWithoutToken() { + given().pathParam("project", "1").when().get("/1").then().statusCode(401); + } + + /** + * Test that tries to obtain a topology with an invalid scope. + */ + @Test + @TestSecurity( + user = "owner", + roles = {"runner"}) + public void testGetInvalidToken() { + given().pathParam("project", "1").when().get("/1").then().statusCode(403); + } + + /** + * Test that tries to obtain a non-existent topology. + */ + @Test + @TestSecurity( + user = "owner", + roles = {"openid"}) + public void testGetNonExisting() { + given().pathParam("project", "1") + .when() + .get("/0") + .then() + .statusCode(404) + .contentType(ContentType.JSON); + } + + /** + * Test that tries to obtain a topology without authorization. + */ + @Test + @TestSecurity( + user = "unknown", + roles = {"openid"}) + public void testGetUnauthorized() { + given().pathParam("project", "1") + .when() + .get("/1") + .then() + .statusCode(404) + .contentType(ContentType.JSON); + } + + /** + * Test that tries to obtain a topology. + */ + @Test + @TestSecurity( + user = "owner", + roles = {"openid"}) + public void testGetExisting() { + given().pathParam("project", "1") + .when() + .get("/1") + .then() + .statusCode(200) + .contentType(ContentType.JSON) + .body("id", equalTo(1)); + } + + /** + * Test to delete a non-existent topology. + */ + @Test + @TestSecurity( + user = "owner", + roles = {"openid"}) + public void testUpdateNonExistent() { + given().pathParam("project", "1") + .body(new Topology.Update(List.of())) + .contentType(ContentType.JSON) + .when() + .put("/0") + .then() + .statusCode(404); + } + + /** + * Test to delete a topology without authorization. + */ + @Test + @TestSecurity( + user = "unknown", + roles = {"openid"}) + public void testUpdateUnauthorized() { + given().pathParam("project", "1") + .body(new Topology.Update(List.of())) + .contentType(ContentType.JSON) + .when() + .put("/1") + .then() + .statusCode(404); + } + + /** + * Test to update a topology as a viewer. + */ + @Test + @TestSecurity( + user = "viewer", + roles = {"openid"}) + public void testUpdateAsViewer() { + given().pathParam("project", "1") + .body(new Topology.Update(List.of())) + .contentType(ContentType.JSON) + .when() + .put("/1") + .then() + .statusCode(403) + .contentType(ContentType.JSON); + } + + /** + * Test to update a topology. + */ + @Test + @TestSecurity( + user = "owner", + roles = {"openid"}) + public void testUpdate() { + given().pathParam("project", "1") + .body(new Topology.Update(List.of())) + .contentType(ContentType.JSON) + .when() + .put("/1") + .then() + .statusCode(200) + .contentType(ContentType.JSON); + } + + /** + * Test to delete a non-existent topology. + */ + @Test + @TestSecurity( + user = "owner", + roles = {"openid"}) + public void testDeleteNonExistent() { + given().pathParam("project", "1").when().delete("/0").then().statusCode(404); + } + + /** + * Test to delete a topology without authorization. + */ + @Test + @TestSecurity( + user = "unknown", + roles = {"openid"}) + public void testDeleteUnauthorized() { + given().pathParam("project", "1").when().delete("/1").then().statusCode(404); + } + + /** + * Test to delete a topology as a viewer. + */ + @Test + @TestSecurity( + user = "viewer", + roles = {"openid"}) + public void testDeleteAsViewer() { + given().pathParam("project", "1").when().delete("/1").then().statusCode(403); + } + + /** + * Test to delete a topology. + */ + @Test + @TestSecurity( + user = "owner", + roles = {"openid"}) + public void testDelete() { + int number = given().pathParam("project", "1") + .body(new Topology.Create("Delete Topology", List.of())) + .contentType(ContentType.JSON) + .when() + .post() + .then() + .statusCode(200) + .contentType(ContentType.JSON) + .extract() + .path("number"); + + given().pathParam("project", "1") + .when() + .delete("/" + number) + .then() + .statusCode(200) + .contentType(ContentType.JSON); + } +} diff --git a/opendc-web/opendc-web-server/src/test/java/org/opendc/web/server/rest/user/UserResourceTest.java b/opendc-web/opendc-web-server/src/test/java/org/opendc/web/server/rest/user/UserResourceTest.java new file mode 100644 index 00000000..6dcb3b4d --- /dev/null +++ b/opendc-web/opendc-web-server/src/test/java/org/opendc/web/server/rest/user/UserResourceTest.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2023 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.web.server.rest.user; + +import static io.restassured.RestAssured.when; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.greaterThan; + +import io.quarkus.test.common.http.TestHTTPEndpoint; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.security.TestSecurity; +import io.restassured.http.ContentType; +import org.junit.jupiter.api.Test; + +/** + * Test suite for [UserResource]. + */ +@QuarkusTest +@TestHTTPEndpoint(UserResource.class) +public final class UserResourceTest { + /** + * Test that tries to obtain the profile of the active user. + */ + @Test + @TestSecurity( + user = "testUser", + roles = {"openid"}) + public void testMe() { + when().get("me") + .then() + .statusCode(200) + .contentType(ContentType.JSON) + .body("userId", equalTo("testUser")) + .body("accounting.simulationTime", equalTo(0)) + .body("accounting.simulationTimeBudget", greaterThan(0)); + } + + /** + * Test that tries to obtain the profile of the active user without authorization. + */ + @Test + public void testMeUnauthorized() { + when().get("me").then().statusCode(401); + } +} diff --git a/opendc-web/opendc-web-server/src/test/java/org/opendc/web/server/service/JobServiceTest.java b/opendc-web/opendc-web-server/src/test/java/org/opendc/web/server/service/JobServiceTest.java new file mode 100644 index 00000000..f6d871c0 --- /dev/null +++ b/opendc-web/opendc-web-server/src/test/java/org/opendc/web/server/service/JobServiceTest.java @@ -0,0 +1,124 @@ +/* + * Copyright (c) 2023 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.web.server.service; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; + +import io.quarkus.test.junit.QuarkusTest; +import java.time.Instant; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.opendc.web.proto.JobState; +import org.opendc.web.server.model.Job; + +/** + * Test suite for the {@link JobService}. + */ +@QuarkusTest +public class JobServiceTest { + /** + * The {@link JobService} instance under test. + */ + private JobService service; + + /** + * The mock {@link UserAccountingService}. + */ + private UserAccountingService mockAccountingService; + + @BeforeEach + public void setUp() { + mockAccountingService = Mockito.mock(UserAccountingService.class); + service = new JobService(mockAccountingService); + } + + @Test + public void testUpdateInvalidTransition() { + Job job = new Job(null, "test", Instant.now(), 1); + job.state = JobState.RUNNING; + + assertThrows(IllegalArgumentException.class, () -> service.updateJob(job, JobState.CLAIMED, 0, null)); + + Mockito.verifyNoInteractions(mockAccountingService); + } + + @Test + public void testUpdateNoBudget() { + Job job = Mockito.spy(new Job(null, "test", Instant.now(), 1)); + job.state = JobState.RUNNING; + + Mockito.when(mockAccountingService.consumeSimulationBudget(any(), anyInt())) + .thenReturn(true); + Mockito.doReturn(true).when(job).updateAtomically(any(), any(), anyInt(), any()); + + service.updateJob(job, JobState.RUNNING, 0, null); + + Mockito.verify(job).updateAtomically(eq(JobState.FAILED), any(), anyInt(), any()); + } + + @Test + public void testUpdateNoBudgetWhenFinishing() { + Job job = Mockito.spy(new Job(null, "test", Instant.now(), 1)); + job.state = JobState.RUNNING; + + Mockito.when(mockAccountingService.consumeSimulationBudget(any(), anyInt())) + .thenReturn(true); + Mockito.doReturn(true).when(job).updateAtomically(any(), any(), anyInt(), any()); + + service.updateJob(job, JobState.FINISHED, 0, null); + + Mockito.verify(job).updateAtomically(eq(JobState.FINISHED), any(), anyInt(), any()); + } + + @Test + public void testUpdateSuccess() { + Job job = Mockito.spy(new Job(null, "test", Instant.now(), 1)); + job.state = JobState.RUNNING; + + Mockito.when(mockAccountingService.consumeSimulationBudget(any(), anyInt())) + .thenReturn(false); + Mockito.doReturn(true).when(job).updateAtomically(any(), any(), anyInt(), any()); + + service.updateJob(job, JobState.FINISHED, 0, null); + + Mockito.verify(job).updateAtomically(eq(JobState.FINISHED), any(), anyInt(), any()); + } + + @Test + public void testUpdateConflict() { + Job job = Mockito.spy(new Job(null, "test", Instant.now(), 1)); + job.state = JobState.RUNNING; + + Mockito.when(mockAccountingService.consumeSimulationBudget(any(), anyInt())) + .thenReturn(false); + Mockito.doReturn(false).when(job).updateAtomically(any(), any(), anyInt(), any()); + + assertThrows(IllegalStateException.class, () -> service.updateJob(job, JobState.FINISHED, 0, null)); + + Mockito.verify(job).updateAtomically(eq(JobState.FINISHED), any(), anyInt(), any()); + } +} diff --git a/opendc-web/opendc-web-server/src/test/java/org/opendc/web/server/service/UserAccountingServiceTest.java b/opendc-web/opendc-web-server/src/test/java/org/opendc/web/server/service/UserAccountingServiceTest.java new file mode 100644 index 00000000..d1d82097 --- /dev/null +++ b/opendc-web/opendc-web-server/src/test/java/org/opendc/web/server/service/UserAccountingServiceTest.java @@ -0,0 +1,213 @@ +/* + * Copyright (c) 2023 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.web.server.service; + +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +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 io.quarkus.panache.mock.PanacheMock; +import io.quarkus.test.junit.QuarkusTest; +import java.time.Duration; +import java.time.LocalDate; +import javax.persistence.EntityExistsException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.opendc.web.server.model.UserAccounting; + +/** + * Test suite for the {@link UserAccountingService}. + */ +@QuarkusTest +public class UserAccountingServiceTest { + /** + * The {@link UserAccountingService} instance under test. + */ + private UserAccountingService service; + + /** + * The user id to test with + */ + private final String userId = "test"; + + @BeforeEach + public void setUp() { + PanacheMock.mock(UserAccounting.class); + service = new UserAccountingService(Duration.ofHours(1)); + } + + @Test + public void testGetUserDoesNotExist() { + Mockito.when(UserAccounting.findByUser(userId)).thenReturn(null); + + var accounting = service.getAccounting(userId); + + assertTrue(accounting.getPeriodEnd().isAfter(LocalDate.now())); + assertEquals(0, accounting.getSimulationTime()); + } + + @Test + public void testGetUserDoesExist() { + var now = LocalDate.now(); + var periodEnd = now.plusMonths(1); + + var mockAccounting = new UserAccounting(userId, periodEnd, 3600); + mockAccounting.simulationTime = 32; + + Mockito.when(UserAccounting.findByUser(userId)).thenReturn(mockAccounting); + + var accounting = service.getAccounting(userId); + + assertAll( + () -> assertEquals(periodEnd, accounting.getPeriodEnd()), + () -> assertEquals(32, accounting.getSimulationTime()), + () -> assertEquals(3600, accounting.getSimulationTimeBudget())); + } + + @Test + public void testHasBudgetUserDoesNotExist() { + Mockito.when(UserAccounting.findByUser(userId)).thenReturn(null); + + assertTrue(service.hasSimulationBudget(userId)); + } + + @Test + public void testHasBudget() { + var periodEnd = LocalDate.now().plusMonths(2); + + var mockAccounting = new UserAccounting(userId, periodEnd, 3600); + Mockito.when(UserAccounting.findByUser(userId)).thenReturn(mockAccounting); + + assertTrue(service.hasSimulationBudget(userId)); + } + + @Test + public void testHasBudgetExceededButPeriodExpired() { + var periodEnd = LocalDate.now().minusMonths(2); + + var mockAccounting = new UserAccounting(userId, periodEnd, 3600); + mockAccounting.simulationTime = 3900; + Mockito.when(UserAccounting.findByUser(userId)).thenReturn(mockAccounting); + + assertTrue(service.hasSimulationBudget(userId)); + } + + @Test + public void testHasBudgetPeriodExpired() { + var periodEnd = LocalDate.now().minusMonths(2); + + var mockAccounting = new UserAccounting(userId, periodEnd, 3600); + Mockito.when(UserAccounting.findByUser(userId)).thenReturn(mockAccounting); + + assertTrue(service.hasSimulationBudget(userId)); + } + + @Test + public void testHasBudgetExceeded() { + var periodEnd = LocalDate.now().plusMonths(1); + + var mockAccounting = new UserAccounting(userId, periodEnd, 3600); + mockAccounting.simulationTime = 3900; + Mockito.when(UserAccounting.findByUser(userId)).thenReturn(mockAccounting); + + assertFalse(service.hasSimulationBudget(userId)); + } + + @Test + public void testConsumeBudgetNewUser() { + Mockito.when(UserAccounting.findByUser(userId)).thenReturn(null); + Mockito.when(UserAccounting.create(anyString(), any(), anyInt(), anyInt())) + .thenAnswer((i) -> { + var accounting = new UserAccounting(i.getArgument(0), i.getArgument(1), i.getArgument(2)); + accounting.simulationTime = i.getArgument(3); + return accounting; + }); + + assertFalse(service.consumeSimulationBudget(userId, 10)); + } + + @Test + public void testConsumeBudgetNewUserExceeded() { + Mockito.when(UserAccounting.findByUser(userId)).thenReturn(null); + Mockito.when(UserAccounting.create(anyString(), any(), anyInt(), anyInt())) + .thenAnswer((i) -> { + var accounting = new UserAccounting(i.getArgument(0), i.getArgument(1), i.getArgument(2)); + accounting.simulationTime = i.getArgument(3); + return accounting; + }); + + assertTrue(service.consumeSimulationBudget(userId, 4000)); + } + + @Test + public void testConsumeBudgetNewUserConflict() { + var periodEnd = LocalDate.now().plusMonths(1); + var accountingMock = Mockito.spy(new UserAccounting(userId, periodEnd, 3600)); + + Mockito.when(UserAccounting.findByUser(userId)).thenReturn(null).thenReturn(accountingMock); + Mockito.when(UserAccounting.create(anyString(), any(), anyInt(), anyInt())) + .thenThrow(new EntityExistsException()); + Mockito.when(accountingMock.consumeBudget(anyInt())).thenAnswer((i) -> { + accountingMock.simulationTime += i.<Integer>getArgument(0); + return true; + }); + + assertFalse(service.consumeSimulationBudget(userId, 10)); + } + + @Test + public void testConsumeBudgetResetSuccess() { + var periodEnd = LocalDate.now().minusMonths(2); + var accountingMock = Mockito.spy(new UserAccounting(userId, periodEnd, 3600)); + accountingMock.simulationTime = 3900; + + Mockito.when(UserAccounting.findByUser(userId)).thenReturn(accountingMock); + Mockito.when(accountingMock.resetBudget(any(), anyInt())).thenAnswer((i) -> { + accountingMock.periodEnd = i.getArgument(0); + accountingMock.simulationTime += i.<Integer>getArgument(1); + return true; + }); + + assertTrue(service.consumeSimulationBudget(userId, 4000)); + } + + @Test + public void testInfiniteConflict() { + var periodEnd = LocalDate.now().plusMonths(1); + var accountingMock = Mockito.spy(new UserAccounting(userId, periodEnd, 3600)); + + Mockito.when(UserAccounting.findByUser(userId)).thenReturn(accountingMock); + Mockito.when(accountingMock.consumeBudget(anyInt())).thenAnswer((i) -> { + accountingMock.simulationTime += i.<Integer>getArgument(0); + return false; + }); + + assertThrows(IllegalStateException.class, () -> service.consumeSimulationBudget(userId, 10)); + } +} |
