From 49b3015a16287bb4486aa64c5c26f05f7c22089c Mon Sep 17 00:00:00 2001 From: Fabian Mastenbroek Date: Mon, 30 Jan 2023 22:22:59 +0000 Subject: refactor(web/server): Remove unnecessary service indirections This change removes the unnecessary service classes where they are only used to forward data from the resource to the entities. Furthermore, DTOs are now moved from the service layer to the resources. --- .../kotlin/org/opendc/web/proto/user/Scenario.kt | 4 +- .../main/java/org/opendc/web/server/model/Job.java | 43 +++- .../org/opendc/web/server/model/Portfolio.java | 17 +- .../web/server/model/ProjectAuthorization.java | 27 ++- .../java/org/opendc/web/server/model/Scenario.java | 42 ++-- .../java/org/opendc/web/server/model/Topology.java | 16 +- .../java/org/opendc/web/server/model/Trace.java | 2 + .../org/opendc/web/server/rest/BaseProtocol.java | 50 +++++ .../org/opendc/web/server/rest/TraceResource.java | 6 +- .../opendc/web/server/rest/runner/JobResource.java | 29 ++- .../web/server/rest/runner/RunnerProtocol.java | 78 +++++++ .../web/server/rest/user/PortfolioResource.java | 77 +++++-- .../rest/user/PortfolioScenarioResource.java | 94 +++++++-- .../web/server/rest/user/ProjectResource.java | 57 +++-- .../web/server/rest/user/ScenarioResource.java | 61 ++++-- .../web/server/rest/user/TopologyResource.java | 94 +++++++-- .../opendc/web/server/rest/user/UserProtocol.java | 132 ++++++++++++ .../org/opendc/web/server/service/JobService.java | 97 ++------- .../web/server/service/PortfolioService.java | 148 ------------- .../opendc/web/server/service/ProjectService.java | 106 ---------- .../opendc/web/server/service/ScenarioService.java | 231 --------------------- .../opendc/web/server/service/TopologyService.java | 178 ---------------- .../web/server/util/runner/QuarkusJobManager.java | 63 ++++-- .../src/main/resources/application-test.properties | 1 + .../src/main/resources/application.properties | 1 + .../main/resources/db/migration/V1.0.0__core.sql | 156 -------------- .../src/main/resources/db/migration/V3.0__core.sql | 160 ++++++++++++++ .../main/resources/db/testing/V3.0.1__entities.sql | 24 +++ .../opendc/web/server/rest/TraceResourceTest.java | 30 +-- .../web/server/rest/runner/JobResourceTest.java | 75 ++----- .../server/rest/user/PortfolioResourceTest.java | 145 ++++++++----- .../rest/user/PortfolioScenarioResourceTest.java | 171 +++++++++------ .../web/server/rest/user/ProjectResourceTest.java | 75 +++---- .../web/server/rest/user/ScenarioResourceTest.java | 127 +++++++---- .../web/server/rest/user/TopologyResourceTest.java | 175 +++++++++++----- .../opendc/web/server/service/JobServiceTest.java | 124 +++++++++++ .../src/components/portfolios/PortfolioResults.js | 19 +- .../src/components/portfolios/ScenarioTable.js | 4 +- opendc-web/opendc-web-ui/src/shapes.js | 2 +- 39 files changed, 1530 insertions(+), 1411 deletions(-) create mode 100644 opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/BaseProtocol.java create mode 100644 opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/runner/RunnerProtocol.java create mode 100644 opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/UserProtocol.java delete mode 100644 opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/PortfolioService.java delete mode 100644 opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/ProjectService.java delete mode 100644 opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/ScenarioService.java delete mode 100644 opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/TopologyService.java delete mode 100644 opendc-web/opendc-web-server/src/main/resources/db/migration/V1.0.0__core.sql create mode 100644 opendc-web/opendc-web-server/src/main/resources/db/migration/V3.0__core.sql create mode 100644 opendc-web/opendc-web-server/src/main/resources/db/testing/V3.0.1__entities.sql create mode 100644 opendc-web/opendc-web-server/src/test/java/org/opendc/web/server/service/JobServiceTest.java (limited to 'opendc-web') diff --git a/opendc-web/opendc-web-proto/src/main/kotlin/org/opendc/web/proto/user/Scenario.kt b/opendc-web/opendc-web-proto/src/main/kotlin/org/opendc/web/proto/user/Scenario.kt index ed77ef08..b9c7a4cf 100644 --- a/opendc-web/opendc-web-proto/src/main/kotlin/org/opendc/web/proto/user/Scenario.kt +++ b/opendc-web/opendc-web-proto/src/main/kotlin/org/opendc/web/proto/user/Scenario.kt @@ -40,7 +40,7 @@ public data class Scenario( val topology: Topology.Summary, val phenomena: OperationalPhenomena, val schedulerName: String, - val job: Job + val jobs: List ) { /** * Create a new scenario. @@ -81,6 +81,6 @@ public data class Scenario( val topology: Topology.Summary, val phenomena: OperationalPhenomena, val schedulerName: String, - val job: Job + val jobs: List ) } diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Job.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Job.java index 14fd3e2a..c5fb208e 100644 --- a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Job.java +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Job.java @@ -22,18 +22,22 @@ package org.opendc.web.server.model; +import io.quarkus.hibernate.orm.panache.Panache; import io.quarkus.hibernate.orm.panache.PanacheEntity; +import io.quarkus.hibernate.orm.panache.PanacheQuery; import io.quarkus.panache.common.Parameters; import java.time.Instant; -import java.util.List; import java.util.Map; import javax.persistence.Column; import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; import javax.persistence.FetchType; +import javax.persistence.ForeignKey; import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; import javax.persistence.NamedQueries; import javax.persistence.NamedQuery; -import javax.persistence.OneToOne; import javax.persistence.Table; import org.hibernate.annotations.Type; import org.opendc.web.proto.JobState; @@ -54,8 +58,8 @@ import org.opendc.web.proto.JobState; """) }) public class Job extends PanacheEntity { - @OneToOne(optional = false, mappedBy = "job", fetch = FetchType.EAGER) - @JoinColumn(name = "scenario_id", nullable = false) + @ManyToOne(optional = false, fetch = FetchType.EAGER) + @JoinColumn(name = "scenario_id", foreignKey = @ForeignKey(name = "fk_jobs_scenario"), nullable = false) public Scenario scenario; @Column(name = "created_by", nullable = false, updatable = false) @@ -74,12 +78,14 @@ public class Job extends PanacheEntity { * The instant at which the job was updated. */ @Column(name = "updated_at", nullable = false) - public Instant updatedAt = createdAt; + public Instant updatedAt; /** * The state of the job. */ - @Column(nullable = false) + @Type(type = "io.hypersistence.utils.hibernate.type.basic.PostgreSQLEnumType") + @Column(nullable = false, columnDefinition = "enum") + @Enumerated(EnumType.STRING) public JobState state = JobState.PENDING; /** @@ -102,6 +108,7 @@ public class Job extends PanacheEntity { this.createdBy = createdBy; this.scenario = scenario; this.createdAt = createdAt; + this.updatedAt = createdAt; this.repeats = repeats; } @@ -114,10 +121,10 @@ public class Job extends PanacheEntity { * Find {@link Job}s in the specified {@link JobState}. * * @param state The state of the jobs to find. - * @return The list of jobs that are in the specified state. + * @return A query for jobs that are in the specified state. */ - public static List findByState(JobState state) { - return find("state", state).list(); + public static PanacheQuery findByState(JobState state) { + return find("state", state); } /** @@ -137,6 +144,24 @@ public class Job extends PanacheEntity { .and("updatedAt", time) .and("runtime", runtime) .and("results", results)); + Panache.getEntityManager().refresh(this); return count > 0; } + + /** + * Determine whether the job is allowed to transition to newState. + * + * @param newState The new state to transition to. + * @return true if the transition to the new state is legal, false otherwise. + */ + public boolean canTransitionTo(JobState newState) { + // Note that we always allow transitions from the state + return newState == this.state + || switch (this.state) { + case PENDING -> newState == JobState.CLAIMED; + case CLAIMED -> newState == JobState.RUNNING || newState == JobState.FAILED; + case RUNNING -> newState == JobState.FINISHED || newState == JobState.FAILED; + case FINISHED, FAILED -> false; + }; + } } diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Portfolio.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Portfolio.java index 4c3af570..3a406683 100644 --- a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Portfolio.java +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Portfolio.java @@ -23,9 +23,9 @@ package org.opendc.web.server.model; import io.quarkus.hibernate.orm.panache.PanacheEntity; +import io.quarkus.hibernate.orm.panache.PanacheQuery; import io.quarkus.panache.common.Parameters; import java.util.HashSet; -import java.util.List; import java.util.Set; import javax.persistence.CascadeType; import javax.persistence.Column; @@ -48,8 +48,12 @@ import org.opendc.web.proto.Targets; @Entity @Table( name = "portfolios", - uniqueConstraints = {@UniqueConstraint(columnNames = {"project_id", "number"})}, - indexes = {@Index(name = "fn_portfolios_number", columnList = "project_id, number")}) + uniqueConstraints = { + @UniqueConstraint( + name = "uk_portfolios_number", + columnNames = {"project_id", "number"}) + }, + indexes = {@Index(name = "ux_portfolios_number", columnList = "project_id, number")}) @NamedQueries({ @NamedQuery(name = "Portfolio.findByProject", query = "SELECT p FROM Portfolio p WHERE p.project.id = :projectId"), @NamedQuery( @@ -112,11 +116,10 @@ public class Portfolio extends PanacheEntity { * Find all {@link Portfolio}s that belong to the specified project * * @param projectId The unique identifier of the project. - * @return The list of portfolios that belong to the specified project. + * @return The query of portfolios that belong to the specified project. */ - public static List findByProject(long projectId) { - return find("#Portfolio.findByProject", Parameters.with("projectId", projectId)) - .list(); + public static PanacheQuery findByProject(long projectId) { + return find("#Portfolio.findByProject", Parameters.with("projectId", projectId)); } /** diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/ProjectAuthorization.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/ProjectAuthorization.java index c10fcc64..1238f58d 100644 --- a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/ProjectAuthorization.java +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/ProjectAuthorization.java @@ -23,20 +23,25 @@ package org.opendc.web.server.model; import io.quarkus.hibernate.orm.panache.PanacheEntityBase; +import io.quarkus.hibernate.orm.panache.PanacheQuery; import io.quarkus.panache.common.Parameters; import java.io.Serializable; -import java.util.List; import java.util.Objects; import javax.persistence.Column; import javax.persistence.Embeddable; import javax.persistence.EmbeddedId; import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.FetchType; +import javax.persistence.ForeignKey; import javax.persistence.JoinColumn; import javax.persistence.ManyToOne; import javax.persistence.MapsId; import javax.persistence.NamedQueries; import javax.persistence.NamedQuery; import javax.persistence.Table; +import org.hibernate.annotations.Type; import org.opendc.web.proto.user.ProjectRole; /** @@ -64,15 +69,22 @@ public class ProjectAuthorization extends PanacheEntityBase { /** * The project that the user is authorized to participate in. */ - @ManyToOne(optional = false) + @ManyToOne(optional = false, fetch = FetchType.LAZY) @MapsId("projectId") - @JoinColumn(name = "project_id", updatable = false, insertable = false, nullable = false) + @JoinColumn( + name = "project_id", + updatable = false, + insertable = false, + nullable = false, + foreignKey = @ForeignKey(name = "fk_project_authorizations")) public Project project; /** * The role of the user in the project. */ - @Column(nullable = false) + @Type(type = "io.hypersistence.utils.hibernate.type.basic.PostgreSQLEnumType") + @Column(nullable = false, columnDefinition = "enum") + @Enumerated(EnumType.STRING) public ProjectRole role; /** @@ -93,11 +105,10 @@ public class ProjectAuthorization extends PanacheEntityBase { * List all projects for the user with the specified userId. * * @param userId The identifier of the user that is requesting the list of projects. - * @return A list of projects that the user has received authorization for. + * @return A query returning projects that the user has received authorization for. */ - public static List findByUser(String userId) { - return find("#ProjectAuthorization.findByUser", Parameters.with("userId", userId)) - .list(); + public static PanacheQuery findByUser(String userId) { + return find("#ProjectAuthorization.findByUser", Parameters.with("userId", userId)); } /** diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Scenario.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Scenario.java index 9381f9be..016e931b 100644 --- a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Scenario.java +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Scenario.java @@ -23,18 +23,22 @@ package org.opendc.web.server.model; import io.quarkus.hibernate.orm.panache.PanacheEntity; +import io.quarkus.hibernate.orm.panache.PanacheQuery; import io.quarkus.panache.common.Parameters; +import java.util.ArrayList; import java.util.List; import javax.persistence.CascadeType; import javax.persistence.Column; import javax.persistence.Embedded; import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.ForeignKey; import javax.persistence.Index; import javax.persistence.JoinColumn; import javax.persistence.ManyToOne; import javax.persistence.NamedQueries; import javax.persistence.NamedQuery; -import javax.persistence.OneToOne; +import javax.persistence.OneToMany; import javax.persistence.Table; import javax.persistence.UniqueConstraint; import org.hibernate.annotations.Type; @@ -46,8 +50,12 @@ import org.opendc.web.proto.OperationalPhenomena; @Entity @Table( name = "scenarios", - uniqueConstraints = {@UniqueConstraint(columnNames = {"project_id", "number"})}, - indexes = {@Index(name = "fn_scenarios_number", columnList = "project_id, number")}) + uniqueConstraints = { + @UniqueConstraint( + name = "uk_scenarios_number", + columnNames = {"project_id", "number"}) + }, + indexes = {@Index(name = "ux_scenarios_number", columnList = "project_id, number")}) @NamedQueries({ @NamedQuery(name = "Scenario.findByProject", query = "SELECT s FROM Scenario s WHERE s.project.id = :projectId"), @NamedQuery( @@ -68,14 +76,14 @@ public class Scenario extends PanacheEntity { * The {@link Project} to which this scenario belongs. */ @ManyToOne(optional = false) - @JoinColumn(name = "project_id", nullable = false) + @JoinColumn(name = "project_id", nullable = false, foreignKey = @ForeignKey(name = "fk_scenarios_project")) public Project project; /** * The {@link Portfolio} to which this scenario belongs. */ @ManyToOne(optional = false) - @JoinColumn(name = "portfolio_id", nullable = false) + @JoinColumn(name = "portfolio_id", nullable = false, foreignKey = @ForeignKey(name = "fk_scenarios_portfolio")) public Portfolio portfolio; /** @@ -100,6 +108,7 @@ public class Scenario extends PanacheEntity { * Topology details of the scenario. */ @ManyToOne(optional = false) + @JoinColumn(name = "topology_id", nullable = false, foreignKey = @ForeignKey(name = "fk_scenarios_topology")) public Topology topology; /** @@ -118,8 +127,11 @@ public class Scenario extends PanacheEntity { /** * The {@link Job} associated with the scenario. */ - @OneToOne(cascade = {CascadeType.ALL}) - public Job job; + @OneToMany( + cascade = {CascadeType.ALL}, + mappedBy = "scenario", + fetch = FetchType.LAZY) + public List jobs = new ArrayList<>(); /** * Construct a {@link Scenario} object. @@ -152,11 +164,10 @@ public class Scenario extends PanacheEntity { * Find all {@link Scenario}s that belong to the specified project * * @param projectId The unique identifier of the project. - * @return The list of scenarios that belong to the specified project. + * @return The query of scenarios that belong to the specified project. */ - public static List findByProject(long projectId) { - return find("#Scenario.findByProject", Parameters.with("projectId", projectId)) - .list(); + public static PanacheQuery findByProject(long projectId) { + return find("#Scenario.findByProject", Parameters.with("projectId", projectId)); } /** @@ -164,13 +175,12 @@ public class Scenario extends PanacheEntity { * * @param projectId The unique identifier of the project. * @param number The number of the portfolio. - * @return The list of scenarios that belong to the specified project and portfolio.. + * @return The query of scenarios that belong to the specified project and portfolio.. */ - public static List findByPortfolio(long projectId, int number) { + public static PanacheQuery findByPortfolio(long projectId, int number) { return find( - "#Scenario.findByPortfolio", - Parameters.with("projectId", projectId).and("number", number)) - .list(); + "#Scenario.findByPortfolio", + Parameters.with("projectId", projectId).and("number", number)); } /** diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Topology.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Topology.java index 6ec83f78..05a1ac12 100644 --- a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Topology.java +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Topology.java @@ -23,6 +23,7 @@ package org.opendc.web.server.model; import io.quarkus.hibernate.orm.panache.PanacheEntity; +import io.quarkus.hibernate.orm.panache.PanacheQuery; import io.quarkus.panache.common.Parameters; import java.time.Instant; import java.util.List; @@ -44,8 +45,12 @@ import org.opendc.web.proto.Room; @Entity @Table( name = "topologies", - uniqueConstraints = {@UniqueConstraint(columnNames = {"project_id", "number"})}, - indexes = {@Index(name = "fn_topologies_number", columnList = "project_id, number")}) + uniqueConstraints = { + @UniqueConstraint( + name = "uk_topologies_number", + columnNames = {"project_id", "number"}) + }, + indexes = {@Index(name = "ux_topologies_number", columnList = "project_id, number")}) @NamedQueries({ @NamedQuery(name = "Topology.findByProject", query = "SELECT t FROM Topology t WHERE t.project.id = :projectId"), @NamedQuery( @@ -112,11 +117,10 @@ public class Topology extends PanacheEntity { * Find all [Topology]s that belong to [project][projectId]. * * @param projectId The unique identifier of the project. - * @return The list of topologies that belong to the specified project. + * @return The query of topologies that belong to the specified project. */ - public static List findByProject(long projectId) { - return find("#Topology.findByProject", Parameters.with("projectId", projectId)) - .list(); + public static PanacheQuery findByProject(long projectId) { + return find("#Topology.findByProject", Parameters.with("projectId", projectId)); } /** diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Trace.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Trace.java index f73c8494..36d27abc 100644 --- a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Trace.java +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Trace.java @@ -26,11 +26,13 @@ import io.quarkus.hibernate.orm.panache.PanacheEntityBase; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.Id; +import javax.persistence.Table; /** * A workload trace available for simulation. */ @Entity +@Table(name = "traces") public class Trace extends PanacheEntityBase { /** * The unique identifier of the trace. diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/BaseProtocol.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/BaseProtocol.java new file mode 100644 index 00000000..44d2d569 --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/BaseProtocol.java @@ -0,0 +1,50 @@ +/* + * 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 org.opendc.web.server.model.Trace; +import org.opendc.web.server.model.Workload; + +/** + * DTO-conversions for the base protocol. + */ +public final class BaseProtocol { + /** + * Private constructor to prevent instantiation of class. + */ + private BaseProtocol() {} + + /** + * Convert a {@link Workload} entity into a DTO. + */ + public static org.opendc.web.proto.Workload toDto(Workload workload) { + return new org.opendc.web.proto.Workload(toDto(workload.trace), workload.samplingFraction); + } + + /** + * Convert a {@link Trace] entity into a {@link org.opendc.web.proto.Trace} DTO. + */ + public static org.opendc.web.proto.Trace toDto(Trace trace) { + return new org.opendc.web.proto.Trace(trace.id, trace.name, trace.type); + } +} diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/TraceResource.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/TraceResource.java index 2b1efb02..7316c93f 100644 --- a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/TraceResource.java +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/TraceResource.java @@ -43,7 +43,7 @@ public final class TraceResource { @GET public List getAll() { Stream entities = Trace.streamAll(); - return entities.map(TraceResource::toUserDto).toList(); + return entities.map(TraceResource::toDto).toList(); } /** @@ -58,13 +58,13 @@ public final class TraceResource { throw new WebApplicationException("Trace not found", 404); } - return toUserDto(trace); + return toDto(trace); } /** * Convert a {@link Trace] entity into a {@link org.opendc.web.proto.Trace} DTO. */ - public static org.opendc.web.proto.Trace toUserDto(Trace trace) { + public static org.opendc.web.proto.Trace toDto(Trace trace) { return new org.opendc.web.proto.Trace(trace.id, trace.name, trace.type); } } diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/runner/JobResource.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/runner/JobResource.java index 134c6814..dff52526 100644 --- a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/runner/JobResource.java +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/runner/JobResource.java @@ -33,6 +33,8 @@ import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.WebApplicationException; +import org.opendc.web.proto.JobState; +import org.opendc.web.server.model.Job; import org.opendc.web.server.service.JobService; /** @@ -43,14 +45,14 @@ import org.opendc.web.server.service.JobService; @RolesAllowed("runner") public final class JobResource { /** - * The {@link JobService} responsible for managing the jobs. + * The {@link JobService} for helping manage the job lifecycle. */ private final JobService jobService; /** * Construct a {@link JobResource} instance. * - * @param jobService The {@link JobService} responsible for managing the jobs. + * @param jobService The {@link JobService} for managing the job lifecycle. */ public JobResource(JobService jobService) { this.jobService = jobService; @@ -61,7 +63,9 @@ public final class JobResource { */ @GET public List queryPending() { - return jobService.listPending(); + return Job.findByState(JobState.PENDING).list().stream() + .map(RunnerProtocol::toDto) + .toList(); } /** @@ -70,12 +74,13 @@ public final class JobResource { @GET @Path("{job}") public org.opendc.web.proto.runner.Job get(@PathParam("job") long id) { - org.opendc.web.proto.runner.Job job = jobService.findById(id); + Job job = Job.findById(id); + if (job == null) { throw new WebApplicationException("Job not found", 404); } - return job; + return RunnerProtocol.toDto(job); } /** @@ -87,17 +92,19 @@ public final class JobResource { @Transactional public org.opendc.web.proto.runner.Job update( @PathParam("job") long id, @Valid org.opendc.web.proto.runner.Job.Update update) { - try { - var job = jobService.updateState(id, update.getState(), update.getRuntime(), update.getResults()); - if (job == null) { - throw new WebApplicationException("Job not found", 404); - } + Job job = Job.findById(id); + if (job == null) { + throw new WebApplicationException("Job not found", 404); + } - return job; + try { + jobService.updateJob(job, update.getState(), update.getRuntime(), update.getResults()); } catch (IllegalArgumentException e) { throw new WebApplicationException(e, 400); } catch (IllegalStateException e) { throw new WebApplicationException(e, 409); } + + return RunnerProtocol.toDto(job); } } diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/runner/RunnerProtocol.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/runner/RunnerProtocol.java new file mode 100644 index 00000000..6bf65d97 --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/runner/RunnerProtocol.java @@ -0,0 +1,78 @@ +/* + * 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 org.opendc.web.server.model.Job; +import org.opendc.web.server.model.Portfolio; +import org.opendc.web.server.model.Scenario; +import org.opendc.web.server.model.Topology; +import org.opendc.web.server.rest.BaseProtocol; + +/** + * DTO-conversions for the runner protocol. + */ +public final class RunnerProtocol { + /** + * Private constructor to prevent instantiation of class. + */ + private RunnerProtocol() {} + + /** + * Convert a {@link Job} into a runner-facing DTO. + */ + public static org.opendc.web.proto.runner.Job toDto(Job job) { + return new org.opendc.web.proto.runner.Job( + job.id, toDto(job.scenario), job.state, job.createdAt, job.updatedAt, job.runtime, job.results); + } + + /** + * Convert a {@link Scenario} into a runner-facing DTO. + */ + public static org.opendc.web.proto.runner.Scenario toDto(Scenario scenario) { + return new org.opendc.web.proto.runner.Scenario( + scenario.id, + scenario.number, + toDto(scenario.portfolio), + scenario.name, + BaseProtocol.toDto(scenario.workload), + toDto(scenario.topology), + scenario.phenomena, + scenario.schedulerName); + } + + /** + * Convert a {@link Portfolio} into a runner-facing DTO. + */ + public static org.opendc.web.proto.runner.Portfolio toDto(Portfolio portfolio) { + return new org.opendc.web.proto.runner.Portfolio( + portfolio.id, portfolio.number, portfolio.name, portfolio.targets); + } + + /** + * Convert a {@link Topology} into a runner-facing DTO. + */ + public static org.opendc.web.proto.runner.Topology toDto(Topology topology) { + return new org.opendc.web.proto.runner.Topology( + topology.id, topology.number, topology.name, topology.rooms, topology.createdAt, topology.updatedAt); + } +} diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/PortfolioResource.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/PortfolioResource.java index e8e05f97..d1fc980d 100644 --- a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/PortfolioResource.java +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/PortfolioResource.java @@ -23,10 +23,12 @@ package org.opendc.web.server.rest.user; import io.quarkus.security.identity.SecurityIdentity; +import java.time.Instant; import java.util.List; import javax.annotation.security.RolesAllowed; import javax.transaction.Transactional; import javax.validation.Valid; +import javax.ws.rs.Consumes; import javax.ws.rs.DELETE; import javax.ws.rs.GET; import javax.ws.rs.POST; @@ -34,7 +36,8 @@ import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.WebApplicationException; -import org.opendc.web.server.service.PortfolioService; +import org.opendc.web.server.model.Portfolio; +import org.opendc.web.server.model.ProjectAuthorization; /** * A resource representing the portfolios of a project. @@ -43,11 +46,6 @@ import org.opendc.web.server.service.PortfolioService; @Path("/projects/{project}/portfolios") @RolesAllowed("openid") public final class PortfolioResource { - /** - * The service for managing the user portfolios. - */ - private final PortfolioService portfolioService; - /** * The identity of the current user. */ @@ -56,11 +54,9 @@ public final class PortfolioResource { /** * Construct a {@link PortfolioResource}. * - * @param portfolioService The {@link PortfolioService} instance to use. * @param identity The {@link SecurityIdentity} of the current user. */ - public PortfolioResource(PortfolioService portfolioService, SecurityIdentity identity) { - this.portfolioService = portfolioService; + public PortfolioResource(SecurityIdentity identity) { this.identity = identity; } @@ -69,7 +65,17 @@ public final class PortfolioResource { */ @GET public List getAll(@PathParam("project") long projectId) { - return portfolioService.findByUser(identity.getPrincipal().getName(), projectId); + // User must have access to project + ProjectAuthorization auth = + ProjectAuthorization.findByUser(identity.getPrincipal().getName(), projectId); + + if (auth == null) { + return List.of(); + } + + return Portfolio.findByProject(projectId).list().stream() + .map((p) -> UserProtocol.toDto(p, auth)) + .toList(); } /** @@ -77,14 +83,29 @@ public final class PortfolioResource { */ @POST @Transactional + @Consumes("application/json") public org.opendc.web.proto.user.Portfolio create( @PathParam("project") long projectId, @Valid org.opendc.web.proto.user.Portfolio.Create request) { - var portfolio = portfolioService.create(identity.getPrincipal().getName(), projectId, request); - if (portfolio == null) { + // User must have access to project + ProjectAuthorization auth = + ProjectAuthorization.findByUser(identity.getPrincipal().getName(), projectId); + + if (auth == null) { throw new WebApplicationException("Project not found", 404); + } else if (!auth.canEdit()) { + throw new WebApplicationException("Not permitted to edit project", 403); } - return portfolio; + var now = Instant.now(); + var project = auth.project; + int number = project.allocatePortfolio(now); + + Portfolio portfolio = new Portfolio(project, number, request.getName(), request.getTargets()); + + project.portfolios.add(portfolio); + portfolio.persist(); + + return UserProtocol.toDto(portfolio, auth); } /** @@ -94,12 +115,21 @@ public final class PortfolioResource { @Path("{portfolio}") public org.opendc.web.proto.user.Portfolio get( @PathParam("project") long projectId, @PathParam("portfolio") int number) { - var portfolio = portfolioService.findByUser(identity.getPrincipal().getName(), projectId, number); + // User must have access to project + ProjectAuthorization auth = + ProjectAuthorization.findByUser(identity.getPrincipal().getName(), projectId); + + if (auth == null) { + throw new WebApplicationException("Portfolio not found", 404); + } + + Portfolio portfolio = Portfolio.findByProject(projectId, number); + if (portfolio == null) { throw new WebApplicationException("Portfolio not found", 404); } - return portfolio; + return UserProtocol.toDto(portfolio, auth); } /** @@ -110,11 +140,22 @@ public final class PortfolioResource { @Transactional public org.opendc.web.proto.user.Portfolio delete( @PathParam("project") long projectId, @PathParam("portfolio") int number) { - var portfolio = portfolioService.delete(identity.getPrincipal().getName(), projectId, number); - if (portfolio == null) { + // User must have access to project + ProjectAuthorization auth = + ProjectAuthorization.findByUser(identity.getPrincipal().getName(), projectId); + + if (auth == null) { + throw new WebApplicationException("Portfolio not found", 404); + } else if (!auth.canEdit()) { + throw new WebApplicationException("Not permitted to edit project", 403); + } + + Portfolio entity = Portfolio.findByProject(projectId, number); + if (entity == null) { throw new WebApplicationException("Portfolio not found", 404); } - return portfolio; + entity.delete(); + return UserProtocol.toDto(entity, auth); } } diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/PortfolioScenarioResource.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/PortfolioScenarioResource.java index a6db7c54..a058cd31 100644 --- a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/PortfolioScenarioResource.java +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/PortfolioScenarioResource.java @@ -23,28 +23,39 @@ package org.opendc.web.server.rest.user; import io.quarkus.security.identity.SecurityIdentity; +import java.time.Instant; import java.util.List; import javax.annotation.security.RolesAllowed; import javax.transaction.Transactional; import javax.validation.Valid; +import javax.ws.rs.Consumes; import javax.ws.rs.GET; import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; import javax.ws.rs.WebApplicationException; -import org.opendc.web.proto.user.Scenario; -import org.opendc.web.server.service.ScenarioService; +import org.opendc.web.proto.JobState; +import org.opendc.web.server.model.Job; +import org.opendc.web.server.model.Portfolio; +import org.opendc.web.server.model.ProjectAuthorization; +import org.opendc.web.server.model.Scenario; +import org.opendc.web.server.model.Topology; +import org.opendc.web.server.model.Trace; +import org.opendc.web.server.model.Workload; +import org.opendc.web.server.service.UserAccountingService; /** * A resource representing the scenarios of a portfolio. */ @Path("/projects/{project}/portfolios/{portfolio}/scenarios") @RolesAllowed("openid") +@Produces("application/json") public final class PortfolioScenarioResource { /** - * The service for managing the user scenarios. + * The service for managing the user accounting. */ - private final ScenarioService scenarioService; + private final UserAccountingService accountingService; /** * The identity of the current user. @@ -54,11 +65,11 @@ public final class PortfolioScenarioResource { /** * Construct a {@link PortfolioScenarioResource}. * - * @param scenarioService The {@link ScenarioService} instance to use. + * @param accountingService The {@link UserAccountingService} instance to use. * @param identity The {@link SecurityIdentity} of the current user. */ - public PortfolioScenarioResource(ScenarioService scenarioService, SecurityIdentity identity) { - this.scenarioService = scenarioService; + public PortfolioScenarioResource(UserAccountingService accountingService, SecurityIdentity identity) { + this.accountingService = accountingService; this.identity = identity; } @@ -66,8 +77,19 @@ public final class PortfolioScenarioResource { * Get all scenarios that belong to the specified portfolio. */ @GET - public List get(@PathParam("project") long projectId, @PathParam("portfolio") int portfolioNumber) { - return scenarioService.findAll(identity.getPrincipal().getName(), projectId, portfolioNumber); + public List get( + @PathParam("project") long projectId, @PathParam("portfolio") int portfolioNumber) { + // User must have access to project + ProjectAuthorization auth = + ProjectAuthorization.findByUser(identity.getPrincipal().getName(), projectId); + + if (auth == null) { + return List.of(); + } + + return org.opendc.web.server.model.Scenario.findByPortfolio(projectId, portfolioNumber).list().stream() + .map((s) -> UserProtocol.toDto(s, auth)) + .toList(); } /** @@ -75,15 +97,63 @@ public final class PortfolioScenarioResource { */ @POST @Transactional + @Consumes("application/json") public org.opendc.web.proto.user.Scenario create( @PathParam("project") long projectId, @PathParam("portfolio") int portfolioNumber, @Valid org.opendc.web.proto.user.Scenario.Create request) { - var scenario = scenarioService.create(identity.getPrincipal().getName(), projectId, portfolioNumber, request); - if (scenario == null) { + // User must have access to project + String userId = identity.getPrincipal().getName(); + ProjectAuthorization auth = ProjectAuthorization.findByUser(userId, projectId); + + if (auth == null) { throw new WebApplicationException("Portfolio not found", 404); + } else if (!auth.canEdit()) { + throw new WebApplicationException("Not permitted to edit project", 403); + } + + Portfolio portfolio = Portfolio.findByProject(projectId, portfolioNumber); + + if (portfolio == null) { + throw new WebApplicationException("Portfolio not found", 404); + } + + Topology topology = Topology.findByProject(projectId, (int) request.getTopology()); + if (topology == null) { + throw new WebApplicationException("Referred topology does not exist", 400); + } + + Trace trace = Trace.findById(request.getWorkload().getTrace()); + if (trace == null) { + throw new WebApplicationException("Referred trace does not exist", 400); } - return scenario; + var now = Instant.now(); + var project = auth.project; + int number = project.allocateScenario(now); + + Scenario scenario = new Scenario( + project, + portfolio, + number, + request.getName(), + new Workload(trace, request.getWorkload().getSamplingFraction()), + topology, + request.getPhenomena(), + request.getSchedulerName()); + scenario.persist(); + + Job job = new Job(scenario, userId, now, portfolio.targets.getRepeats()); + job.persist(); + + // Fail the job if there is not enough budget for the simulation + if (!accountingService.hasSimulationBudget(userId)) { + job.state = JobState.FAILED; + } + + scenario.jobs.add(job); + portfolio.scenarios.add(scenario); + + return UserProtocol.toDto(scenario, auth); } } diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/ProjectResource.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/ProjectResource.java index b0b8eb4e..da47c3ff 100644 --- a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/ProjectResource.java +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/ProjectResource.java @@ -23,6 +23,7 @@ package org.opendc.web.server.rest.user; import io.quarkus.security.identity.SecurityIdentity; +import java.time.Instant; import java.util.List; import javax.annotation.security.RolesAllowed; import javax.transaction.Transactional; @@ -35,7 +36,9 @@ import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.WebApplicationException; -import org.opendc.web.server.service.ProjectService; +import org.opendc.web.proto.user.ProjectRole; +import org.opendc.web.server.model.Project; +import org.opendc.web.server.model.ProjectAuthorization; /** * A resource representing the created projects. @@ -44,11 +47,6 @@ import org.opendc.web.server.service.ProjectService; @Path("/projects") @RolesAllowed("openid") public final class ProjectResource { - /** - * The service for managing the user projects. - */ - private final ProjectService projectService; - /** * The identity of the current user. */ @@ -57,11 +55,9 @@ public final class ProjectResource { /** * Construct a {@link ProjectResource}. * - * @param projectService The {@link ProjectService} instance to use. * @param identity The {@link SecurityIdentity} of the current user. */ - public ProjectResource(ProjectService projectService, SecurityIdentity identity) { - this.projectService = projectService; + public ProjectResource(SecurityIdentity identity) { this.identity = identity; } @@ -70,7 +66,9 @@ public final class ProjectResource { */ @GET public List getAll() { - return projectService.findByUser(identity.getPrincipal().getName()); + return ProjectAuthorization.findByUser(identity.getPrincipal().getName()).list().stream() + .map(UserProtocol::toDto) + .toList(); } /** @@ -80,7 +78,17 @@ public final class ProjectResource { @Transactional @Consumes("application/json") public org.opendc.web.proto.user.Project create(@Valid org.opendc.web.proto.user.Project.Create request) { - return projectService.create(identity.getPrincipal().getName(), request.getName()); + Instant now = Instant.now(); + Project entity = new Project(request.getName(), now); + entity.persist(); + + ProjectAuthorization authorization = + new ProjectAuthorization(entity, identity.getPrincipal().getName(), ProjectRole.OWNER); + + entity.authorizations.add(authorization); + authorization.persist(); + + return UserProtocol.toDto(authorization); } /** @@ -89,12 +97,14 @@ public final class ProjectResource { @GET @Path("{project}") public org.opendc.web.proto.user.Project get(@PathParam("project") long id) { - var project = projectService.findByUser(identity.getPrincipal().getName(), id); - if (project == null) { + ProjectAuthorization auth = + ProjectAuthorization.findByUser(identity.getPrincipal().getName(), id); + + if (auth == null) { throw new WebApplicationException("Project not found", 404); } - return project; + return UserProtocol.toDto(auth); } /** @@ -104,15 +114,18 @@ public final class ProjectResource { @Path("{project}") @Transactional public org.opendc.web.proto.user.Project delete(@PathParam("project") long id) { - try { - var project = projectService.delete(identity.getPrincipal().getName(), id); - if (project == null) { - throw new WebApplicationException("Project not found", 404); - } + ProjectAuthorization auth = + ProjectAuthorization.findByUser(identity.getPrincipal().getName(), id); - return project; - } catch (IllegalArgumentException e) { - throw new WebApplicationException(e.getMessage(), 403); + if (auth == null) { + throw new WebApplicationException("Project not found", 404); + } else if (!auth.canDelete()) { + throw new WebApplicationException("Not allowed to delete project", 403); } + + auth.project.updatedAt = Instant.now(); + org.opendc.web.proto.user.Project project = UserProtocol.toDto(auth); + auth.project.delete(); + return project; } } diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/ScenarioResource.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/ScenarioResource.java index a6838148..cf933c32 100644 --- a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/ScenarioResource.java +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/ScenarioResource.java @@ -23,6 +23,7 @@ package org.opendc.web.server.rest.user; import io.quarkus.security.identity.SecurityIdentity; +import java.util.List; import javax.annotation.security.RolesAllowed; import javax.transaction.Transactional; import javax.ws.rs.DELETE; @@ -31,7 +32,8 @@ import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.WebApplicationException; -import org.opendc.web.server.service.ScenarioService; +import org.opendc.web.server.model.ProjectAuthorization; +import org.opendc.web.server.model.Scenario; /** * A resource representing the scenarios of a portfolio. @@ -40,11 +42,6 @@ import org.opendc.web.server.service.ScenarioService; @Path("/projects/{project}/scenarios") @RolesAllowed("openid") public final class ScenarioResource { - /** - * The service for managing the user scenarios. - */ - private final ScenarioService scenarioService; - /** * The identity of the current user. */ @@ -53,14 +50,30 @@ public final class ScenarioResource { /** * Construct a {@link ScenarioResource}. * - * @param scenarioService The {@link ScenarioService} instance to use. * @param identity The {@link SecurityIdentity} of the current user. */ - public ScenarioResource(ScenarioService scenarioService, SecurityIdentity identity) { - this.scenarioService = scenarioService; + public ScenarioResource(SecurityIdentity identity) { this.identity = identity; } + /** + * Obtain the scenarios belonging to a project. + */ + @GET + public List getAll(@PathParam("project") long projectId) { + // User must have access to project + ProjectAuthorization auth = + ProjectAuthorization.findByUser(identity.getPrincipal().getName(), projectId); + + if (auth == null) { + throw new WebApplicationException("Project not found", 404); + } + + return Scenario.findByProject(projectId).list().stream() + .map((s) -> UserProtocol.toDto(s, auth)) + .toList(); + } + /** * Obtain a scenario by its identifier. */ @@ -68,12 +81,21 @@ public final class ScenarioResource { @Path("{scenario}") public org.opendc.web.proto.user.Scenario get( @PathParam("project") long projectId, @PathParam("scenario") int number) { - var scenario = scenarioService.findOne(identity.getPrincipal().getName(), projectId, number); + // User must have access to project + ProjectAuthorization auth = + ProjectAuthorization.findByUser(identity.getPrincipal().getName(), projectId); + + if (auth == null) { + throw new WebApplicationException("Project not found", 404); + } + + Scenario scenario = Scenario.findByProject(projectId, number); + if (scenario == null) { throw new WebApplicationException("Scenario not found", 404); } - return scenario; + return UserProtocol.toDto(scenario, auth); } /** @@ -84,11 +106,22 @@ public final class ScenarioResource { @Transactional public org.opendc.web.proto.user.Scenario delete( @PathParam("project") long projectId, @PathParam("scenario") int number) { - var scenario = scenarioService.delete(identity.getPrincipal().getName(), projectId, number); - if (scenario == null) { + // User must have access to project + ProjectAuthorization auth = + ProjectAuthorization.findByUser(identity.getPrincipal().getName(), projectId); + + if (auth == null) { + throw new WebApplicationException("Project not found", 404); + } else if (!auth.canEdit()) { + throw new WebApplicationException("Not permitted to edit project", 403); + } + + Scenario entity = Scenario.findByProject(projectId, number); + if (entity == null) { throw new WebApplicationException("Scenario not found", 404); } - return scenario; + entity.delete(); + return UserProtocol.toDto(entity, auth); } } diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/TopologyResource.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/TopologyResource.java index 54afc1ce..2b66b64b 100644 --- a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/TopologyResource.java +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/TopologyResource.java @@ -23,6 +23,7 @@ package org.opendc.web.server.rest.user; import io.quarkus.security.identity.SecurityIdentity; +import java.time.Instant; import java.util.List; import javax.annotation.security.RolesAllowed; import javax.transaction.Transactional; @@ -36,7 +37,9 @@ import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.WebApplicationException; -import org.opendc.web.server.service.TopologyService; +import org.opendc.web.server.model.Project; +import org.opendc.web.server.model.ProjectAuthorization; +import org.opendc.web.server.model.Topology; /** * A resource representing the constructed datacenter topologies. @@ -45,11 +48,6 @@ import org.opendc.web.server.service.TopologyService; @Path("/projects/{project}/topologies") @RolesAllowed("openid") public final class TopologyResource { - /** - * The service for managing the user topologies. - */ - private final TopologyService topologyService; - /** * The identity of the current user. */ @@ -58,11 +56,9 @@ public final class TopologyResource { /** * Construct a {@link TopologyResource}. * - * @param topologyService The {@link TopologyService} instance to use. * @param identity The {@link SecurityIdentity} of the current user. */ - public TopologyResource(TopologyService topologyService, SecurityIdentity identity) { - this.topologyService = topologyService; + public TopologyResource(SecurityIdentity identity) { this.identity = identity; } @@ -71,7 +67,17 @@ public final class TopologyResource { */ @GET public List getAll(@PathParam("project") long projectId) { - return topologyService.findAll(identity.getPrincipal().getName(), projectId); + // User must have access to project + ProjectAuthorization auth = + ProjectAuthorization.findByUser(identity.getPrincipal().getName(), projectId); + + if (auth == null) { + return List.of(); + } + + return Topology.findByProject(projectId).list().stream() + .map((t) -> UserProtocol.toDto(t, auth)) + .toList(); } /** @@ -82,13 +88,26 @@ public final class TopologyResource { @Transactional public org.opendc.web.proto.user.Topology create( @PathParam("project") long projectId, @Valid org.opendc.web.proto.user.Topology.Create request) { - var topology = topologyService.create(identity.getPrincipal().getName(), projectId, request); + // User must have access to project + ProjectAuthorization auth = + ProjectAuthorization.findByUser(identity.getPrincipal().getName(), projectId); - if (topology == null) { + if (auth == null) { throw new WebApplicationException("Topology not found", 404); + } else if (!auth.canEdit()) { + throw new WebApplicationException("Not permitted to edit project", 403); } - return topology; + Instant now = Instant.now(); + Project project = auth.project; + int number = project.allocateTopology(now); + + Topology topology = new Topology(project, number, request.getName(), now, request.getRooms()); + + project.topologies.add(topology); + topology.persist(); + + return UserProtocol.toDto(topology, auth); } /** @@ -98,13 +117,21 @@ public final class TopologyResource { @Path("{topology}") public org.opendc.web.proto.user.Topology get( @PathParam("project") long projectId, @PathParam("topology") int number) { - var topology = topologyService.findOne(identity.getPrincipal().getName(), projectId, number); + // User must have access to project + ProjectAuthorization auth = + ProjectAuthorization.findByUser(identity.getPrincipal().getName(), projectId); + + if (auth == null) { + throw new WebApplicationException("Topology not found", 404); + } + + Topology topology = Topology.findByProject(projectId, number); if (topology == null) { throw new WebApplicationException("Topology not found", 404); } - return topology; + return UserProtocol.toDto(topology, auth); } /** @@ -118,13 +145,26 @@ public final class TopologyResource { @PathParam("project") long projectId, @PathParam("topology") int number, @Valid org.opendc.web.proto.user.Topology.Update request) { - var topology = topologyService.update(identity.getPrincipal().getName(), projectId, number, request); + // User must have access to project + ProjectAuthorization auth = + ProjectAuthorization.findByUser(identity.getPrincipal().getName(), projectId); - if (topology == null) { + if (auth == null) { + throw new WebApplicationException("Topology not found", 404); + } else if (!auth.canEdit()) { + throw new WebApplicationException("Not permitted to edit project", 403); + } + + Topology entity = Topology.findByProject(projectId, number); + + if (entity == null) { throw new WebApplicationException("Topology not found", 404); } - return topology; + entity.updatedAt = Instant.now(); + entity.rooms = request.getRooms(); + + return UserProtocol.toDto(entity, auth); } /** @@ -135,12 +175,24 @@ public final class TopologyResource { @Transactional public org.opendc.web.proto.user.Topology delete( @PathParam("project") long projectId, @PathParam("topology") int number) { - var topology = topologyService.delete(identity.getPrincipal().getName(), projectId, number); + // User must have access to project + ProjectAuthorization auth = + ProjectAuthorization.findByUser(identity.getPrincipal().getName(), projectId); - if (topology == null) { + if (auth == null) { + throw new WebApplicationException("Topology not found", 404); + } else if (!auth.canEdit()) { + throw new WebApplicationException("Not permitted to edit project", 403); + } + + Topology entity = Topology.findByProject(projectId, number); + + if (entity == null) { throw new WebApplicationException("Topology not found", 404); } - return topology; + entity.updatedAt = Instant.now(); + entity.delete(); + return UserProtocol.toDto(entity, auth); } } diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/UserProtocol.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/UserProtocol.java new file mode 100644 index 00000000..8196a9d6 --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/UserProtocol.java @@ -0,0 +1,132 @@ +/* + * 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 org.opendc.web.server.model.Job; +import org.opendc.web.server.model.Portfolio; +import org.opendc.web.server.model.Project; +import org.opendc.web.server.model.ProjectAuthorization; +import org.opendc.web.server.model.Scenario; +import org.opendc.web.server.model.Topology; +import org.opendc.web.server.rest.BaseProtocol; + +/** + * DTO-conversions for the user protocol. + */ +public final class UserProtocol { + /** + * Private constructor to prevent instantiation of class. + */ + private UserProtocol() {} + + /** + * Convert a {@link ProjectAuthorization} entity into a {@link Project} DTO. + */ + public static org.opendc.web.proto.user.Project toDto(ProjectAuthorization auth) { + Project project = auth.project; + return new org.opendc.web.proto.user.Project( + project.id, project.name, project.createdAt, project.updatedAt, auth.role); + } + + /** + * Convert a {@link Portfolio} entity into a {@link org.opendc.web.proto.user.Portfolio} DTO. + */ + public static org.opendc.web.proto.user.Portfolio toDto(Portfolio portfolio, ProjectAuthorization auth) { + return new org.opendc.web.proto.user.Portfolio( + portfolio.id, + portfolio.number, + toDto(auth), + portfolio.name, + portfolio.targets, + portfolio.scenarios.stream().map(UserProtocol::toSummaryDto).toList()); + } + + /** + * Convert a {@link Portfolio} entity into a {@link org.opendc.web.proto.user.Portfolio.Summary} DTO. + */ + public static org.opendc.web.proto.user.Portfolio.Summary toSummaryDto(Portfolio portfolio) { + return new org.opendc.web.proto.user.Portfolio.Summary( + portfolio.id, portfolio.number, portfolio.name, portfolio.targets); + } + + /** + * Convert a {@link Topology} entity into a {@link org.opendc.web.proto.user.Topology} DTO. + */ + public static org.opendc.web.proto.user.Topology toDto(Topology topology, ProjectAuthorization auth) { + return new org.opendc.web.proto.user.Topology( + topology.id, + topology.number, + toDto(auth), + topology.name, + topology.rooms, + topology.createdAt, + topology.updatedAt); + } + + /** + * Convert a {@link Topology} entity into a {@link org.opendc.web.proto.user.Topology.Summary} DTO. + */ + public static org.opendc.web.proto.user.Topology.Summary toSummaryDto(Topology topology) { + return new org.opendc.web.proto.user.Topology.Summary( + topology.id, topology.number, topology.name, topology.createdAt, topology.updatedAt); + } + + /** + * Convert a {@link Scenario} entity into a {@link org.opendc.web.proto.user.Scenario} DTO. + */ + public static org.opendc.web.proto.user.Scenario toDto(Scenario scenario, ProjectAuthorization auth) { + return new org.opendc.web.proto.user.Scenario( + scenario.id, + scenario.number, + toDto(auth), + toSummaryDto(scenario.portfolio), + scenario.name, + BaseProtocol.toDto(scenario.workload), + toSummaryDto(scenario.topology), + scenario.phenomena, + scenario.schedulerName, + scenario.jobs.stream().map(UserProtocol::toDto).toList()); + } + + /** + * Convert a {@link Scenario} entity into a {@link org.opendc.web.proto.user.Scenario.Summary} DTO. + */ + public static org.opendc.web.proto.user.Scenario.Summary toSummaryDto(Scenario scenario) { + return new org.opendc.web.proto.user.Scenario.Summary( + scenario.id, + scenario.number, + scenario.name, + BaseProtocol.toDto(scenario.workload), + toSummaryDto(scenario.topology), + scenario.phenomena, + scenario.schedulerName, + scenario.jobs.stream().map(UserProtocol::toDto).toList()); + } + + /** + * Convert a {@link Job} entity into a {@link org.opendc.web.proto.user.Job} DTO. + */ + public static org.opendc.web.proto.user.Job toDto(Job job) { + return new org.opendc.web.proto.user.Job(job.id, job.state, job.createdAt, job.updatedAt, job.results); + } +} diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/JobService.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/JobService.java index 47f44d27..ed0eaf9c 100644 --- a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/JobService.java +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/JobService.java @@ -23,122 +23,59 @@ package org.opendc.web.server.service; import java.time.Instant; -import java.util.List; import java.util.Map; import javax.enterprise.context.ApplicationScoped; import org.opendc.web.proto.JobState; import org.opendc.web.server.model.Job; /** - * Service for managing {@link Job}s. + * A service for managing the lifecycle of a job and ensuring that the user does not consume + * too much simulation resources. */ @ApplicationScoped public final class JobService { /** - * The service for managing the user accounting. + * The {@link UserAccountingService} responsible for accounting the simulation time of users. */ private final UserAccountingService accountingService; /** * Construct a {@link JobService} instance. * - * @param accountingService The {@link UserAccountingService} instance to use. + * @param accountingService The {@link UserAccountingService} for accounting the simulation time of users. */ public JobService(UserAccountingService accountingService) { this.accountingService = accountingService; } /** - * Query the pending simulation jobs. - */ - public List listPending() { - return Job.findByState(JobState.PENDING).stream() - .map(JobService::toRunnerDto) - .toList(); - } - - /** - * Find a job by its identifier. - */ - public org.opendc.web.proto.runner.Job findById(long id) { - Job job = Job.findById(id); - - if (job == null) { - return null; - } - - return toRunnerDto(job); - } - - /** - * Atomically update the state of a {@link Job}. + * Update the job state. * - * @param id The identifier of the job. - * @param newState The next state for the job. - * @param runtime The runtime of the job (in seconds). - * @param results The potential results of the job. + * @param job The {@link Job} to update. + * @param newState The new state to transition the job to. + * @param runtime The runtime (in seconds) consumed by the simulation jbo so far. + * @param results The results to attach to the job. + * @throws IllegalArgumentException if the state transition is invalid. + * @throws IllegalStateException if someone tries to update the job concurrently. */ - public org.opendc.web.proto.runner.Job updateState( - long id, JobState newState, int runtime, Map results) { - Job entity = Job.findById(id); - if (entity == null) { - return null; - } + public void updateJob(Job job, JobState newState, int runtime, Map results) { + JobState state = job.state; - JobState state = entity.state; - if (!isTransitionLegal(state, newState)) { + if (!job.canTransitionTo(newState)) { throw new IllegalArgumentException("Invalid transition from %s to %s".formatted(state, newState)); } Instant now = Instant.now(); JobState nextState = newState; - int consumedBudget = Math.min(1, runtime - entity.runtime); + int consumedBudget = Math.min(1, runtime - job.runtime); // Check whether the user still has any simulation budget left - if (accountingService.consumeSimulationBudget(entity.createdBy, consumedBudget) - && nextState == JobState.RUNNING) { + if (accountingService.consumeSimulationBudget(job.createdBy, consumedBudget) && nextState == JobState.RUNNING) { nextState = JobState.FAILED; // User has consumed all their budget; cancel the job } - if (!entity.updateAtomically(nextState, now, runtime, results)) { + if (!job.updateAtomically(nextState, now, runtime, results)) { throw new IllegalStateException("Conflicting update"); } - - return toRunnerDto(entity); - } - - /** - * Determine whether the transition from [this] to [newState] is legal. - */ - public static boolean isTransitionLegal(JobState currentState, JobState newState) { - // Note that we always allow transitions from the state - return newState == currentState - || switch (currentState) { - case PENDING -> newState == JobState.CLAIMED; - case CLAIMED -> newState == JobState.RUNNING || newState == JobState.FAILED; - case RUNNING -> newState == JobState.FINISHED || newState == JobState.FAILED; - case FINISHED, FAILED -> false; - }; - } - - /** - * Convert a {@link Job} entity into a {@link org.opendc.web.proto.user.Job} DTO. - */ - public static org.opendc.web.proto.user.Job toUserDto(Job job) { - return new org.opendc.web.proto.user.Job(job.id, job.state, job.createdAt, job.updatedAt, job.results); - } - - /** - * Convert a {@link Job} into a runner-facing DTO. - */ - public static org.opendc.web.proto.runner.Job toRunnerDto(Job job) { - return new org.opendc.web.proto.runner.Job( - job.id, - ScenarioService.toRunnerDto(job.scenario), - job.state, - job.createdAt, - job.updatedAt, - job.runtime, - job.results); } } diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/PortfolioService.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/PortfolioService.java deleted file mode 100644 index 94da5195..00000000 --- a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/PortfolioService.java +++ /dev/null @@ -1,148 +0,0 @@ -/* - * 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 java.time.Instant; -import java.util.List; -import javax.enterprise.context.ApplicationScoped; -import org.opendc.web.server.model.Portfolio; -import org.opendc.web.server.model.ProjectAuthorization; - -/** - * Service for managing {@link Portfolio}s. - */ -@ApplicationScoped -public final class PortfolioService { - /** - * List all {@link Portfolio}s that belong a certain project. - */ - public List findByUser(String userId, long projectId) { - // User must have access to project - ProjectAuthorization auth = ProjectAuthorization.findByUser(userId, projectId); - - if (auth == null) { - return List.of(); - } - - return Portfolio.findByProject(projectId).stream() - .map((p) -> toUserDto(p, auth)) - .toList(); - } - - /** - * Find a {@link Portfolio} with the specified number belonging to projectId. - */ - public org.opendc.web.proto.user.Portfolio findByUser(String userId, long projectId, int number) { - // User must have access to project - ProjectAuthorization auth = ProjectAuthorization.findByUser(userId, projectId); - - if (auth == null) { - return null; - } - - Portfolio portfolio = Portfolio.findByProject(projectId, number); - - if (portfolio == null) { - return null; - } - - return toUserDto(portfolio, auth); - } - - /** - * Delete the portfolio with the specified number belonging to projectId. - */ - public org.opendc.web.proto.user.Portfolio delete(String userId, long projectId, int number) { - // User must have access to project - ProjectAuthorization auth = ProjectAuthorization.findByUser(userId, projectId); - - if (auth == null) { - return null; - } else if (!auth.canEdit()) { - throw new IllegalStateException("Not permitted to edit project"); - } - - Portfolio entity = Portfolio.findByProject(projectId, number); - if (entity == null) { - return null; - } - - entity.delete(); - return toUserDto(entity, auth); - } - - /** - * Construct a new {@link Portfolio} with the specified name. - */ - public org.opendc.web.proto.user.Portfolio create( - String userId, long projectId, org.opendc.web.proto.user.Portfolio.Create request) { - // User must have access to project - ProjectAuthorization auth = ProjectAuthorization.findByUser(userId, projectId); - - if (auth == null) { - return null; - } else if (!auth.canEdit()) { - throw new IllegalStateException("Not permitted to edit project"); - } - - var now = Instant.now(); - var project = auth.project; - int number = project.allocatePortfolio(now); - - Portfolio portfolio = new Portfolio(project, number, request.getName(), request.getTargets()); - - project.portfolios.add(portfolio); - portfolio.persist(); - - return toUserDto(portfolio, auth); - } - - /** - * Convert a {@link Portfolio} entity into a {@link org.opendc.web.proto.user.Portfolio} DTO. - */ - public static org.opendc.web.proto.user.Portfolio toUserDto(Portfolio portfolio, ProjectAuthorization auth) { - return new org.opendc.web.proto.user.Portfolio( - portfolio.id, - portfolio.number, - ProjectService.toUserDto(auth), - portfolio.name, - portfolio.targets, - portfolio.scenarios.stream().map(ScenarioService::toSummaryDto).toList()); - } - - /** - * Convert a {@link Portfolio} entity into a {@link org.opendc.web.proto.user.Portfolio.Summary} DTO. - */ - public static org.opendc.web.proto.user.Portfolio.Summary toSummaryDto(Portfolio portfolio) { - return new org.opendc.web.proto.user.Portfolio.Summary( - portfolio.id, portfolio.number, portfolio.name, portfolio.targets); - } - - /** - * Convert a {@link Portfolio} into a runner-facing DTO. - */ - public static org.opendc.web.proto.runner.Portfolio toRunnerDto(Portfolio portfolio) { - return new org.opendc.web.proto.runner.Portfolio( - portfolio.id, portfolio.number, portfolio.name, portfolio.targets); - } -} diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/ProjectService.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/ProjectService.java deleted file mode 100644 index aeef664e..00000000 --- a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/ProjectService.java +++ /dev/null @@ -1,106 +0,0 @@ -/* - * 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 java.time.Instant; -import java.util.List; -import javax.enterprise.context.ApplicationScoped; -import org.opendc.web.proto.user.ProjectRole; -import org.opendc.web.server.model.Project; -import org.opendc.web.server.model.ProjectAuthorization; - -/** - * Service for managing {@link Project}s. - */ -@ApplicationScoped -public final class ProjectService { - /** - * List all projects for the user with the specified userId. - */ - public List findByUser(String userId) { - return ProjectAuthorization.findByUser(userId).stream() - .map(ProjectService::toUserDto) - .toList(); - } - - /** - * Obtain the project with the specified id for the user with the specified userId. - */ - public org.opendc.web.proto.user.Project findByUser(String userId, long id) { - ProjectAuthorization auth = ProjectAuthorization.findByUser(userId, id); - - if (auth == null) { - return null; - } - - return toUserDto(auth); - } - - /** - * Create a new {@link Project} for the user with the specified userId. - */ - public org.opendc.web.proto.user.Project create(String userId, String name) { - Instant now = Instant.now(); - Project entity = new Project(name, now); - entity.persist(); - - ProjectAuthorization authorization = new ProjectAuthorization(entity, userId, ProjectRole.OWNER); - - entity.authorizations.add(authorization); - authorization.persist(); - - return toUserDto(authorization); - } - - /** - * Delete a project by its identifier. - * - * @param userId The user that invokes the action. - * @param id The identifier of the project. - */ - public org.opendc.web.proto.user.Project delete(String userId, long id) { - ProjectAuthorization auth = ProjectAuthorization.findByUser(userId, id); - - if (auth == null) { - return null; - } - - if (!auth.canDelete()) { - throw new IllegalArgumentException("Not allowed to delete project"); - } - - auth.project.updatedAt = Instant.now(); - org.opendc.web.proto.user.Project project = toUserDto(auth); - auth.project.delete(); - return project; - } - - /** - * Convert a {@link ProjectAuthorization} entity into a {@link Project} DTO. - */ - public static org.opendc.web.proto.user.Project toUserDto(ProjectAuthorization auth) { - Project project = auth.project; - return new org.opendc.web.proto.user.Project( - project.id, project.name, project.createdAt, project.updatedAt, auth.role); - } -} diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/ScenarioService.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/ScenarioService.java deleted file mode 100644 index 6a70db1e..00000000 --- a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/ScenarioService.java +++ /dev/null @@ -1,231 +0,0 @@ -/* - * 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 java.time.Instant; -import java.util.List; -import javax.enterprise.context.ApplicationScoped; -import org.opendc.web.proto.JobState; -import org.opendc.web.server.model.Job; -import org.opendc.web.server.model.Portfolio; -import org.opendc.web.server.model.ProjectAuthorization; -import org.opendc.web.server.model.Scenario; -import org.opendc.web.server.model.Topology; -import org.opendc.web.server.model.Trace; -import org.opendc.web.server.model.Workload; - -/** - * Service for managing {@link Scenario}s. - */ -@ApplicationScoped -public final class ScenarioService { - /** - * The service for managing the user accounting. - */ - private final UserAccountingService accountingService; - - /** - * Construct a {@link ScenarioService} instance. - * - * @param accountingService The {@link UserAccountingService} instance to use. - */ - public ScenarioService(UserAccountingService accountingService) { - this.accountingService = accountingService; - } - - /** - * List all {@link Scenario}s that belong a certain portfolio. - */ - public List findAll(String userId, long projectId, int number) { - // User must have access to project - ProjectAuthorization auth = ProjectAuthorization.findByUser(userId, projectId); - - if (auth == null) { - return List.of(); - } - - return Scenario.findByPortfolio(projectId, number).stream() - .map((s) -> toUserDto(s, auth)) - .toList(); - } - - /** - * Obtain a {@link Scenario} by identifier. - */ - public org.opendc.web.proto.user.Scenario findOne(String userId, long projectId, int number) { - // User must have access to project - ProjectAuthorization auth = ProjectAuthorization.findByUser(userId, projectId); - - if (auth == null) { - return null; - } - - Scenario scenario = Scenario.findByProject(projectId, number); - - if (scenario == null) { - return null; - } - - return toUserDto(scenario, auth); - } - - /** - * Delete the specified scenario. - */ - public org.opendc.web.proto.user.Scenario delete(String userId, long projectId, int number) { - // User must have access to project - ProjectAuthorization auth = ProjectAuthorization.findByUser(userId, projectId); - - if (auth == null) { - return null; - } else if (!auth.canEdit()) { - throw new IllegalStateException("Not permitted to edit project"); - } - - Scenario entity = Scenario.findByProject(projectId, number); - if (entity == null) { - return null; - } - - entity.delete(); - return toUserDto(entity, auth); - } - - /** - * Construct a new {@link Scenario} with the specified data. - */ - public org.opendc.web.proto.user.Scenario create( - String userId, long projectId, int portfolioNumber, org.opendc.web.proto.user.Scenario.Create request) { - // User must have access to project - ProjectAuthorization auth = ProjectAuthorization.findByUser(userId, projectId); - - if (auth == null) { - return null; - } else if (!auth.canEdit()) { - throw new IllegalStateException("Not permitted to edit project"); - } - - Portfolio portfolio = Portfolio.findByProject(projectId, portfolioNumber); - - if (portfolio == null) { - return null; - } - - Topology topology = Topology.findByProject(projectId, (int) request.getTopology()); - if (topology == null) { - throw new IllegalArgumentException("Referred topology does not exist"); - } - - Trace trace = Trace.findById(request.getWorkload().getTrace()); - if (trace == null) { - throw new IllegalArgumentException("Referred trace does not exist"); - } - - var now = Instant.now(); - var project = auth.project; - int number = project.allocateScenario(now); - - Scenario scenario = new Scenario( - project, - portfolio, - number, - request.getName(), - new Workload(trace, request.getWorkload().getSamplingFraction()), - topology, - request.getPhenomena(), - request.getSchedulerName()); - Job job = new Job(scenario, userId, now, portfolio.targets.getRepeats()); - - // Fail the job if there is not enough budget for the simulation - if (!accountingService.hasSimulationBudget(userId)) { - job.state = JobState.FAILED; - } - - scenario.job = job; - portfolio.scenarios.add(scenario); - scenario.persist(); - - return toUserDto(scenario, auth); - } - - /** - * Convert a {@link Scenario} entity into a {@link org.opendc.web.proto.user.Scenario} DTO. - */ - public static org.opendc.web.proto.user.Scenario toUserDto(Scenario scenario, ProjectAuthorization auth) { - return new org.opendc.web.proto.user.Scenario( - scenario.id, - scenario.number, - ProjectService.toUserDto(auth), - PortfolioService.toSummaryDto(scenario.portfolio), - scenario.name, - toDto(scenario.workload), - TopologyService.toSummaryDto(scenario.topology), - scenario.phenomena, - scenario.schedulerName, - JobService.toUserDto(scenario.job)); - } - - /** - * Convert a {@link Scenario} entity into a {@link org.opendc.web.proto.user.Scenario.Summary} DTO. - */ - public static org.opendc.web.proto.user.Scenario.Summary toSummaryDto(Scenario scenario) { - return new org.opendc.web.proto.user.Scenario.Summary( - scenario.id, - scenario.number, - scenario.name, - toDto(scenario.workload), - TopologyService.toSummaryDto(scenario.topology), - scenario.phenomena, - scenario.schedulerName, - JobService.toUserDto(scenario.job)); - } - - /** - * Convert a {@link Scenario} into a runner-facing DTO. - */ - public static org.opendc.web.proto.runner.Scenario toRunnerDto(Scenario scenario) { - return new org.opendc.web.proto.runner.Scenario( - scenario.id, - scenario.number, - PortfolioService.toRunnerDto(scenario.portfolio), - scenario.name, - toDto(scenario.workload), - TopologyService.toRunnerDto(scenario.topology), - scenario.phenomena, - scenario.schedulerName); - } - - /** - * Convert a {@link Workload} entity into a DTO. - */ - public static org.opendc.web.proto.Workload toDto(Workload workload) { - return new org.opendc.web.proto.Workload(toDto(workload.trace), workload.samplingFraction); - } - - /** - * Convert a {@link Trace] entity into a {@link org.opendc.web.proto.Trace} DTO. - */ - public static org.opendc.web.proto.Trace toDto(Trace trace) { - return new org.opendc.web.proto.Trace(trace.id, trace.name, trace.type); - } -} diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/TopologyService.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/TopologyService.java deleted file mode 100644 index 1961995f..00000000 --- a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/TopologyService.java +++ /dev/null @@ -1,178 +0,0 @@ -/* - * 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 java.time.Instant; -import java.util.List; -import javax.enterprise.context.ApplicationScoped; -import org.opendc.web.server.model.Project; -import org.opendc.web.server.model.ProjectAuthorization; -import org.opendc.web.server.model.Topology; - -/** - * Service for managing {@link Topology}s. - */ -@ApplicationScoped -public final class TopologyService { - /** - * List all {@link Topology}s that belong a certain project. - */ - public List findAll(String userId, long projectId) { - // User must have access to project - ProjectAuthorization auth = ProjectAuthorization.findByUser(userId, projectId); - - if (auth == null) { - return List.of(); - } - - return Topology.findByProject(projectId).stream() - .map((t) -> toUserDto(t, auth)) - .toList(); - } - - /** - * Find the {@link Topology} with the specified number belonging to projectId. - */ - public org.opendc.web.proto.user.Topology findOne(String userId, long projectId, int number) { - // User must have access to project - ProjectAuthorization auth = ProjectAuthorization.findByUser(userId, projectId); - - if (auth == null) { - return null; - } - - Topology topology = Topology.findByProject(projectId, number); - - if (topology == null) { - return null; - } - - return toUserDto(topology, auth); - } - - /** - * Delete the {@link Topology} with the specified number belonging to projectId - */ - public org.opendc.web.proto.user.Topology delete(String userId, long projectId, int number) { - // User must have access to project - ProjectAuthorization auth = ProjectAuthorization.findByUser(userId, projectId); - - if (auth == null) { - return null; - } else if (!auth.canEdit()) { - throw new IllegalStateException("Not permitted to edit project"); - } - - Topology entity = Topology.findByProject(projectId, number); - - if (entity == null) { - return null; - } - - entity.updatedAt = Instant.now(); - entity.delete(); - return toUserDto(entity, auth); - } - - /** - * Update a {@link Topology} with the specified number belonging to projectId. - */ - public org.opendc.web.proto.user.Topology update( - String userId, long projectId, int number, org.opendc.web.proto.user.Topology.Update request) { - // User must have access to project - ProjectAuthorization auth = ProjectAuthorization.findByUser(userId, projectId); - - if (auth == null) { - return null; - } else if (!auth.canEdit()) { - throw new IllegalStateException("Not permitted to edit project"); - } - - Topology entity = Topology.findByProject(projectId, number); - - if (entity == null) { - return null; - } - - entity.updatedAt = Instant.now(); - entity.rooms = request.getRooms(); - - return toUserDto(entity, auth); - } - - /** - * Construct a new {@link Topology} with the specified name. - */ - public org.opendc.web.proto.user.Topology create( - String userId, long projectId, org.opendc.web.proto.user.Topology.Create request) { - // User must have access to project - ProjectAuthorization auth = ProjectAuthorization.findByUser(userId, projectId); - - if (auth == null) { - return null; - } else if (!auth.canEdit()) { - throw new IllegalStateException("Not permitted to edit project"); - } - - Instant now = Instant.now(); - Project project = auth.project; - int number = project.allocateTopology(now); - - Topology topology = new Topology(project, number, request.getName(), now, request.getRooms()); - - project.topologies.add(topology); - topology.persist(); - - return toUserDto(topology, auth); - } - - /** - * Convert a {@link Topology} entity into a {@link org.opendc.web.proto.user.Topology} DTO. - */ - public static org.opendc.web.proto.user.Topology toUserDto(Topology topology, ProjectAuthorization auth) { - return new org.opendc.web.proto.user.Topology( - topology.id, - topology.number, - ProjectService.toUserDto(auth), - topology.name, - topology.rooms, - topology.createdAt, - topology.updatedAt); - } - - /** - * Convert a {@link Topology} entity into a {@link org.opendc.web.proto.user.Topology.Summary} DTO. - */ - public static org.opendc.web.proto.user.Topology.Summary toSummaryDto(Topology topology) { - return new org.opendc.web.proto.user.Topology.Summary( - topology.id, topology.number, topology.name, topology.createdAt, topology.updatedAt); - } - - /** - * Convert a {@link Topology} into a runner-facing DTO. - */ - public static org.opendc.web.proto.runner.Topology toRunnerDto(Topology topology) { - return new org.opendc.web.proto.runner.Topology( - topology.id, topology.number, topology.name, topology.rooms, topology.createdAt, topology.updatedAt); - } -} diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/util/runner/QuarkusJobManager.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/util/runner/QuarkusJobManager.java index 84ebd6e4..0331eacf 100644 --- a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/util/runner/QuarkusJobManager.java +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/util/runner/QuarkusJobManager.java @@ -28,17 +28,26 @@ import javax.transaction.Transactional; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.opendc.web.proto.JobState; -import org.opendc.web.proto.runner.Job; import org.opendc.web.runner.JobManager; +import org.opendc.web.server.model.Job; +import org.opendc.web.server.rest.runner.RunnerProtocol; import org.opendc.web.server.service.JobService; /** - * Implementation of {@link JobManager} that interfaces directly with {@link JobService} without overhead of the REST API. + * Implementation of {@link JobManager} that interfaces directly with the database without overhead of the REST API. */ @ApplicationScoped public class QuarkusJobManager implements JobManager { + /** + * The {@link JobService} used to manage the job's lifecycle. + */ private final JobService jobService; + /** + * Construct a {@link QuarkusJobManager}. + * + * @param jobService The {@link JobService} for managing the job's lifecycle. + */ public QuarkusJobManager(JobService jobService) { this.jobService = jobService; } @@ -46,38 +55,60 @@ public class QuarkusJobManager implements JobManager { @Transactional @Nullable @Override - public Job findNext() { - var pending = jobService.listPending(); - if (pending.isEmpty()) { + public org.opendc.web.proto.runner.Job findNext() { + var job = Job.findByState(JobState.PENDING).firstResult(); + if (job == null) { return null; } - return pending.get(0); + return RunnerProtocol.toDto(job); } + @Transactional @Override public boolean claim(long id) { - try { - jobService.updateState(id, JobState.CLAIMED, 0, null); - return true; - } catch (IllegalStateException e) { - return false; - } + return updateState(id, JobState.CLAIMED, 0, null); } + @Transactional @Override public boolean heartbeat(long id, int runtime) { - Job res = jobService.updateState(id, JobState.RUNNING, runtime, null); - return res != null && !res.getState().equals(JobState.FAILED); + return updateState(id, JobState.RUNNING, runtime, null); } + @Transactional @Override public void fail(long id, int runtime) { - jobService.updateState(id, JobState.FAILED, runtime, null); + updateState(id, JobState.FAILED, runtime, null); } + @Transactional @Override public void finish(long id, int runtime, @NotNull Map results) { - jobService.updateState(id, JobState.FINISHED, runtime, results); + updateState(id, JobState.FINISHED, runtime, results); + } + + /** + * Helper method to update the state of a job. + * + * @param id The unique id of the job. + * @param newState The new state to transition to. + * @param runtime The runtime of the job. + * @param results The results of the job. + * @return true if the operation succeeded, false otherwise. + */ + private boolean updateState(long id, JobState newState, int runtime, Map results) { + Job job = Job.findById(id); + + if (job == null) { + return false; + } + + try { + jobService.updateJob(job, newState, runtime, results); + return true; + } catch (IllegalArgumentException | IllegalStateException e) { + return false; + } } } diff --git a/opendc-web/opendc-web-server/src/main/resources/application-test.properties b/opendc-web/opendc-web-server/src/main/resources/application-test.properties index 17502b6c..bee17221 100644 --- a/opendc-web/opendc-web-server/src/main/resources/application-test.properties +++ b/opendc-web/opendc-web-server/src/main/resources/application-test.properties @@ -25,6 +25,7 @@ quarkus.datasource.jdbc.url=jdbc:h2:mem:default;DB_CLOSE_DELAY=-1;INIT=CREATE TY quarkus.hibernate-orm.dialect=org.hibernate.dialect.H2Dialect quarkus.hibernate-orm.log.sql=true quarkus.flyway.clean-at-start=true +quarkus.flyway.locations=db/migration,db/testing # Disable security quarkus.oidc.enabled=false diff --git a/opendc-web/opendc-web-server/src/main/resources/application.properties b/opendc-web/opendc-web-server/src/main/resources/application.properties index 40933304..0f47db30 100644 --- a/opendc-web/opendc-web-server/src/main/resources/application.properties +++ b/opendc-web/opendc-web-server/src/main/resources/application.properties @@ -20,6 +20,7 @@ # Enable CORS quarkus.http.cors=true +quarkus.http.cors.origins=http://localhost:3000,https://opendc.org # Security quarkus.oidc.enabled=${opendc.security.enabled} diff --git a/opendc-web/opendc-web-server/src/main/resources/db/migration/V1.0.0__core.sql b/opendc-web/opendc-web-server/src/main/resources/db/migration/V1.0.0__core.sql deleted file mode 100644 index 1a0e4046..00000000 --- a/opendc-web/opendc-web-server/src/main/resources/db/migration/V1.0.0__core.sql +++ /dev/null @@ -1,156 +0,0 @@ --- Hibernate sequence for unique identifiers -create sequence hibernate_sequence start with 1 increment by 1; - --- Projects -create table projects -( - id bigint not null, - created_at timestamp not null, - name varchar(255) not null, - portfolios_created integer not null, - scenarios_created integer not null, - topologies_created integer not null, - updated_at timestamp not null, - primary key (id) -); - --- Project authorizations authorize users specific permissions to a project. -create table project_authorizations -( - project_id bigint not null, - user_id varchar(255) not null, - role integer not null, - primary key (project_id, user_id) -); - --- Topologies represent the datacenter designs created by users. -create table topologies -( - id bigint not null, - created_at timestamp not null, - name varchar(255) not null, - number integer not null, - rooms jsonb not null, - updated_at timestamp not null, - project_id bigint not null, - primary key (id) -); - --- Portfolios -create table portfolios -( - id bigint not null, - name varchar(255) not null, - number integer not null, - targets jsonb not null, - project_id bigint not null, - primary key (id) -); - -create table scenarios -( - id bigint not null, - name varchar(255) not null, - number integer not null, - phenomena jsonb not null, - scheduler_name varchar(255) not null, - sampling_fraction double precision not null, - job_id bigint, - portfolio_id bigint not null, - project_id bigint not null, - topology_id bigint not null, - trace_id varchar(255) not null, - primary key (id) -); - -create table jobs -( - id bigint not null, - created_by varchar(255) not null, - created_at timestamp not null, - repeats integer not null, - results jsonb, - state integer not null, - runtime integer not null, - updated_at timestamp not null, - primary key (id) -); - --- User accounting -create table user_accounting -( - user_id varchar(255) not null, - period_end date not null, - simulation_time integer not null, - simulation_time_budget integer not null, - primary key (user_id) -); - --- Workload traces available to the user. -create table traces -( - id varchar(255) not null, - name varchar(255) not null, - type varchar(255) not null, - primary key (id) -); - --- Relations -alter table project_authorizations - add constraint FK824hw0npe6gwiamwb6vohsu19 - foreign key (project_id) - references projects; - -create index fn_topologies_number on topologies (project_id, number); - -alter table topologies - add constraint UK2s5na63qtu2of4g7odocmwi2a unique (project_id, number); - -alter table topologies - add constraint FK1kpw87pylq7m2ct9lq0ed1u3b - foreign key (project_id) - references projects; - -create index fn_portfolios_number on portfolios (project_id, number); - -alter table portfolios - add constraint FK31ytuaxb7aboxueng9hq7owwa - foreign key (project_id) - references projects; - -alter table portfolios - add constraint UK56dtskxruwj22dvxny2hfhks1 unique (project_id, number); - -create index fn_scenarios_number on scenarios (project_id, number); - -alter table scenarios - add constraint UKd0bk6fmtw5qiu9ty7t3g9crqd unique (project_id, number); - -alter table scenarios - add constraint FK9utvg0i5uu8db9pa17a1d77iy - foreign key (job_id) - references jobs; - -alter table scenarios - add constraint FK181y5hv0uibhj7fpbpkdy90s5 - foreign key (portfolio_id) - references portfolios; - -alter table scenarios - add constraint FKbvwyh4joavs444rj270o3b8fr - foreign key (project_id) - references projects; - -alter table scenarios - add constraint FKrk6ltvaf9lp0aukp9dq3qjujj - foreign key (topology_id) - references topologies; - -alter table scenarios - add constraint FK5m05tqeekqjkbbsaj3ehl6o8n - foreign key (trace_id) - references traces; - --- Initial data -insert into traces (id, name, type) -values ('bitbrains-small', 'Bitbrains Small', 'vm'); diff --git a/opendc-web/opendc-web-server/src/main/resources/db/migration/V3.0__core.sql b/opendc-web/opendc-web-server/src/main/resources/db/migration/V3.0__core.sql new file mode 100644 index 00000000..40654b6b --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/resources/db/migration/V3.0__core.sql @@ -0,0 +1,160 @@ +-- Hibernate sequence for unique identifiers +create sequence hibernate_sequence start with 1 increment by 1; + +-- Projects +create table projects +( + id bigint not null, + created_at timestamp not null, + name varchar(255) not null, + portfolios_created integer not null default 0, + scenarios_created integer not null default 0, + topologies_created integer not null default 0, + updated_at timestamp not null, + primary key (id) +); + +create type project_role as enum ('OWNER', 'EDITOR', 'VIEWER'); + +-- Project authorizations authorize users specific permissions to a project. +create table project_authorizations +( + project_id bigint not null, + user_id varchar(255) not null, + role project_role not null, + primary key (project_id, user_id) +); + +-- Topologies represent the datacenter designs created by users. +create table topologies +( + id bigint not null, + created_at timestamp not null, + name varchar(255) not null, + number integer not null, + rooms jsonb not null, + updated_at timestamp not null, + project_id bigint not null, + primary key (id) +); + +-- Portfolios +create table portfolios +( + id bigint not null, + name varchar(255) not null, + number integer not null, + targets jsonb not null, + project_id bigint not null, + primary key (id) +); + +create table scenarios +( + id bigint not null, + name varchar(255) not null, + number integer not null, + phenomena jsonb not null, + scheduler_name varchar(255) not null, + sampling_fraction double precision not null, + portfolio_id bigint not null, + project_id bigint not null, + topology_id bigint not null, + trace_id varchar(255) not null, + primary key (id) +); + +create type job_state as enum ('PENDING', 'CLAIMED', 'RUNNING', 'FINISHED', 'FAILED'); + +create table jobs +( + id bigint not null, + created_by varchar(255) not null, + created_at timestamp not null, + repeats integer not null, + results jsonb, + state job_state not null default 'PENDING', + runtime integer not null default 0, + updated_at timestamp not null, + scenario_id bigint not null, + primary key (id) +); + +-- User accounting +create table user_accounting +( + user_id varchar(255) not null, + period_end date not null, + simulation_time integer not null, + simulation_time_budget integer not null, + primary key (user_id) +); + +-- Workload traces available to the user. +create table traces +( + id varchar(255) not null, + name varchar(255) not null, + type varchar(255) not null, + primary key (id) +); + +-- Relations +alter table project_authorizations + add constraint fk_project_authorizations + foreign key (project_id) + references projects; + +create index ux_topologies_number on topologies (project_id, number); + +alter table topologies + add constraint uk_topologies_number unique (project_id, number); + +alter table topologies + add constraint fk_topologies_project + foreign key (project_id) + references projects; + +create index ux_portfolios_number on portfolios (project_id, number); + +alter table portfolios + add constraint fk_portfolios_project + foreign key (project_id) + references projects; + +alter table portfolios + add constraint uk_portfolios_number unique (project_id, number); + +create index ux_scenarios_number on scenarios (project_id, number); + +alter table scenarios + add constraint uk_scenarios_number unique (project_id, number); + +alter table scenarios + add constraint fk_scenarios_project + foreign key (project_id) + references projects; + +alter table scenarios + add constraint fk_scenarios_topology + foreign key (topology_id) + references topologies; + +alter table scenarios + add constraint fk_scenarios_portfolio + foreign key (portfolio_id) + references portfolios; + +alter table scenarios + add constraint fk_scenarios_trace + foreign key (trace_id) + references traces; + +alter table jobs + add constraint fk_scenarios_job + foreign key (scenario_id) + references scenarios; + +-- Initial data +insert into traces (id, name, type) +values ('bitbrains-small', 'Bitbrains Small', 'vm'); diff --git a/opendc-web/opendc-web-server/src/main/resources/db/testing/V3.0.1__entities.sql b/opendc-web/opendc-web-server/src/main/resources/db/testing/V3.0.1__entities.sql new file mode 100644 index 00000000..1b702f4e --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/resources/db/testing/V3.0.1__entities.sql @@ -0,0 +1,24 @@ +-- Test entities + +alter sequence hibernate_sequence restart with 500; + +insert into projects (id, created_at, name, portfolios_created, scenarios_created, topologies_created, updated_at) +values (1, current_timestamp(), 'Test Project', 1, 2, 1, current_timestamp()); +insert into project_authorizations (project_id, user_id, role) +values (1, 'owner', 'OWNER'), + (1, 'editor', 'EDITOR'), + (1, 'viewer', 'VIEWER'); + +insert into portfolios (id, name, number, targets, project_id) +values (1, 'Test Portfolio', 1, '{ "metrics": [] }' format json, 1); + +insert into topologies (id, created_at, name, number, rooms, updated_at, project_id) +values (1, current_timestamp(), 'Test Topology', 1, '[]' format json, current_timestamp(), 1); + +insert into scenarios (id, name, number, phenomena, scheduler_name, sampling_fraction, portfolio_id, project_id, topology_id, trace_id) +values (1, 'Test Scenario', 1, '{ "failures": false, "interference": false }' format json, 'mem', 1.0, 1, 1, 1, 'bitbrains-small'), + (2, 'Test Scenario', 2, '{ "failures": false, "interference": false }' format json, 'mem', 1.0, 1, 1, 1, 'bitbrains-small'); + +insert into jobs (id, created_by, created_at, repeats, updated_at, scenario_id) +values (1, 'owner', current_timestamp(), 1, current_timestamp(), 1), + (2, 'owner', current_timestamp(), 1, current_timestamp(), 2); 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 index ebef3945..5c5976db 100644 --- 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 @@ -25,16 +25,10 @@ package org.opendc.web.server.rest; import static io.restassured.RestAssured.when; import static org.hamcrest.Matchers.equalTo; -import io.quarkus.panache.mock.PanacheMock; import io.quarkus.test.common.http.TestHTTPEndpoint; import io.quarkus.test.junit.QuarkusTest; import io.restassured.http.ContentType; -import java.util.stream.Stream; -import org.hamcrest.Matchers; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.mockito.Mockito; -import org.opendc.web.server.model.Trace; /** * Test suite for {@link TraceResource}. @@ -43,21 +37,11 @@ import org.opendc.web.server.model.Trace; @TestHTTPEndpoint(TraceResource.class) public final class TraceResourceTest { /** - * Set up the test environment. - */ - @BeforeEach - public void setUp() { - PanacheMock.mock(Trace.class); - } - - /** - * Test that tries to obtain all traces (empty response). + * Test that tries to obtain all traces. */ @Test public void testGetAllEmpty() { - Mockito.when(Trace.streamAll()).thenReturn(Stream.of()); - - when().get().then().statusCode(200).contentType(ContentType.JSON).body("", Matchers.empty()); + when().get().then().statusCode(200).contentType(ContentType.JSON); } /** @@ -65,9 +49,7 @@ public final class TraceResourceTest { */ @Test public void testGetNonExisting() { - Mockito.when(Trace.findById("bitbrains")).thenReturn(null); - - when().get("/bitbrains").then().statusCode(404).contentType(ContentType.JSON); + when().get("/unknown").then().statusCode(404).contentType(ContentType.JSON); } /** @@ -75,12 +57,10 @@ public final class TraceResourceTest { */ @Test public void testGetExisting() { - Mockito.when(Trace.findById("bitbrains")).thenReturn(new Trace("bitbrains", "Bitbrains", "VM")); - - when().get("/bitbrains") + when().get("/bitbrains-small") .then() .statusCode(200) .contentType(ContentType.JSON) - .body("name", equalTo("Bitbrains")); + .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 index a163cd29..94b2cef0 100644 --- 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 @@ -25,30 +25,13 @@ 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 static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.ArgumentMatchers.eq; import io.quarkus.test.common.http.TestHTTPEndpoint; import io.quarkus.test.junit.QuarkusTest; -import io.quarkus.test.junit.mockito.InjectMock; import io.quarkus.test.security.TestSecurity; import io.restassured.http.ContentType; -import java.time.Instant; -import java.util.List; -import java.util.Set; import org.junit.jupiter.api.Test; -import org.mockito.Mockito; import org.opendc.web.proto.JobState; -import org.opendc.web.proto.OperationalPhenomena; -import org.opendc.web.proto.Targets; -import org.opendc.web.proto.Trace; -import org.opendc.web.proto.Workload; -import org.opendc.web.proto.runner.Job; -import org.opendc.web.proto.runner.Portfolio; -import org.opendc.web.proto.runner.Scenario; -import org.opendc.web.proto.runner.Topology; -import org.opendc.web.server.service.JobService; /** * Test suite for {@link JobResource}. @@ -56,27 +39,6 @@ import org.opendc.web.server.service.JobService; @QuarkusTest @TestHTTPEndpoint(JobResource.class) public final class JobResourceTest { - @InjectMock - private JobService jobService; - - /** - * Dummy values - */ - private final Portfolio dummyPortfolio = new Portfolio(1, 1, "test", new Targets(Set.of(), 1)); - - private final Topology dummyTopology = new Topology(1, 1, "test", List.of(), Instant.now(), Instant.now()); - private final Trace dummyTrace = new Trace("bitbrains", "Bitbrains", "vm"); - private final Scenario dummyScenario = new Scenario( - 1, - 1, - dummyPortfolio, - "test", - new Workload(dummyTrace, 1.0), - dummyTopology, - new OperationalPhenomena(false, false), - "test"); - private final Job dummyJob = new Job(1, dummyScenario, JobState.PENDING, Instant.now(), Instant.now(), 0, null); - /** * Test that tries to query the pending jobs without token. */ @@ -90,7 +52,7 @@ public final class JobResourceTest { */ @Test @TestSecurity( - user = "testUser", + user = "test", roles = {"openid"}) public void testQueryInvalidScope() { when().get().then().statusCode(403); @@ -101,12 +63,10 @@ public final class JobResourceTest { */ @Test @TestSecurity( - user = "testUser", + user = "test", roles = {"runner"}) public void testQuery() { - Mockito.when(jobService.listPending()).thenReturn(List.of(dummyJob)); - - when().get().then().statusCode(200).contentType(ContentType.JSON).body("get(0).id", equalTo(1)); + when().get().then().statusCode(200).contentType(ContentType.JSON).body("get(0).state", equalTo("PENDING")); } /** @@ -114,12 +74,10 @@ public final class JobResourceTest { */ @Test @TestSecurity( - user = "testUser", + user = "test", roles = {"runner"}) public void testGetNonExisting() { - Mockito.when(jobService.findById(1)).thenReturn(null); - - when().get("/1").then().statusCode(404).contentType(ContentType.JSON); + when().get("/0").then().statusCode(404).contentType(ContentType.JSON); } /** @@ -127,11 +85,9 @@ public final class JobResourceTest { */ @Test @TestSecurity( - user = "testUser", + user = "test", roles = {"runner"}) public void testGetExisting() { - Mockito.when(jobService.findById(1)).thenReturn(dummyJob); - when().get("/1").then().statusCode(200).contentType(ContentType.JSON).body("id", equalTo(1)); } @@ -140,15 +96,13 @@ public final class JobResourceTest { */ @Test @TestSecurity( - user = "testUser", + user = "test", roles = {"runner"}) public void testUpdateNonExistent() { - Mockito.when(jobService.updateState(eq(1L), any(), anyInt(), any())).thenReturn(null); - - given().body(new Job.Update(JobState.PENDING, 0, null)) + given().body(new org.opendc.web.proto.runner.Job.Update(JobState.PENDING, 0, null)) .contentType(ContentType.JSON) .when() - .post("/1") + .post("/0") .then() .statusCode(404) .contentType(ContentType.JSON); @@ -159,16 +113,13 @@ public final class JobResourceTest { */ @Test @TestSecurity( - user = "testUser", + user = "test", roles = {"runner"}) public void testUpdateState() { - Mockito.when(jobService.updateState(eq(1L), any(), anyInt(), any())) - .thenReturn(new Job(1, dummyScenario, JobState.CLAIMED, Instant.now(), Instant.now(), 0, null)); - - given().body(new Job.Update(JobState.CLAIMED, 0, null)) + given().body(new org.opendc.web.proto.runner.Job.Update(JobState.CLAIMED, 0, null)) .contentType(ContentType.JSON) .when() - .post("/1") + .post("/2") .then() .statusCode(200) .contentType(ContentType.JSON) @@ -180,7 +131,7 @@ public final class JobResourceTest { */ @Test @TestSecurity( - user = "testUser", + user = "test", roles = {"runner"}) public void testUpdateInvalidInput() { given().body("{ \"test\": \"test\" }") 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 index cc3ac978..a952d83f 100644 --- 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 @@ -24,24 +24,14 @@ package org.opendc.web.server.rest.user; import static io.restassured.RestAssured.given; import static org.hamcrest.Matchers.equalTo; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; import io.quarkus.test.common.http.TestHTTPEndpoint; import io.quarkus.test.junit.QuarkusTest; -import io.quarkus.test.junit.mockito.InjectMock; import io.quarkus.test.security.TestSecurity; import io.restassured.http.ContentType; -import java.time.Instant; -import java.util.List; import java.util.Set; import org.junit.jupiter.api.Test; -import org.mockito.Mockito; import org.opendc.web.proto.Targets; -import org.opendc.web.proto.user.Portfolio; -import org.opendc.web.proto.user.Project; -import org.opendc.web.proto.user.ProjectRole; -import org.opendc.web.server.service.PortfolioService; /** * Test suite for {@link PortfolioResource}. @@ -49,27 +39,25 @@ import org.opendc.web.server.service.PortfolioService; @QuarkusTest @TestHTTPEndpoint(PortfolioResource.class) public final class PortfolioResourceTest { - @InjectMock - private PortfolioService portfolioService; - - /** - * Dummy project and portfolio - */ - private final Project dummyProject = new Project(1, "test", Instant.now(), Instant.now(), ProjectRole.OWNER); - - private final Portfolio dummyPortfolio = - new Portfolio(1, 1, dummyProject, "test", new Targets(Set.of(), 1), List.of()); - /** * Test that tries to obtain the list of portfolios belonging to a project. */ @Test @TestSecurity( - user = "testUser", + user = "owner", roles = {"openid"}) public void testGetForProject() { - Mockito.when(portfolioService.findByUser("testUser", 1)).thenReturn(List.of()); + 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); } @@ -78,40 +66,53 @@ public final class PortfolioResourceTest { */ @Test @TestSecurity( - user = "testUser", + user = "owner", roles = {"openid"}) public void testCreateNonExistent() { - Mockito.when(portfolioService.create(eq("testUser"), eq(1), any())).thenReturn(null); + 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 Portfolio.Create("test", new Targets(Set.of(), 1))) + .body(new org.opendc.web.proto.user.Portfolio.Create("test", new Targets(Set.of(), 1))) .contentType(ContentType.JSON) .when() .post() .then() - .statusCode(404) + .statusCode(403) .contentType(ContentType.JSON); } /** - * Test that tries to create a portfolio for a scenario. + * Test that tries to create a portfolio for a project. */ @Test @TestSecurity( - user = "testUser", + user = "editor", roles = {"openid"}) public void testCreate() { - Mockito.when(portfolioService.create(eq("testUser"), eq(1L), any())).thenReturn(dummyPortfolio); - given().pathParam("project", "1") - .body(new Portfolio.Create("test", new Targets(Set.of(), 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("id", equalTo(1)) .body("name", equalTo("test")); } @@ -120,7 +121,7 @@ public final class PortfolioResourceTest { */ @Test @TestSecurity( - user = "testUser", + user = "editor", roles = {"openid"}) public void testCreateEmpty() { given().pathParam("project", "1") @@ -138,11 +139,11 @@ public final class PortfolioResourceTest { */ @Test @TestSecurity( - user = "testUser", + user = "editor", roles = {"openid"}) public void testCreateBlankName() { given().pathParam("project", "1") - .body(new Portfolio.Create("", new Targets(Set.of(), 1))) + .body(new org.opendc.web.proto.user.Portfolio.Create("", new Targets(Set.of(), 1))) .contentType(ContentType.JSON) .when() .post() @@ -164,7 +165,7 @@ public final class PortfolioResourceTest { */ @Test @TestSecurity( - user = "testUser", + user = "owner", roles = {"runner"}) public void testGetInvalidToken() { given().pathParam("project", "1").when().get("/1").then().statusCode(403); @@ -175,12 +176,26 @@ public final class PortfolioResourceTest { */ @Test @TestSecurity( - user = "testUser", + user = "owner", roles = {"openid"}) public void testGetNonExisting() { - Mockito.when(portfolioService.findByUser("testUser", 1, 1)).thenReturn(null); - 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() @@ -193,11 +208,9 @@ public final class PortfolioResourceTest { */ @Test @TestSecurity( - user = "testUser", + user = "owner", roles = {"openid"}) public void testGetExisting() { - Mockito.when(portfolioService.findByUser("testUser", 1, 1)).thenReturn(dummyPortfolio); - given().pathParam("project", "1") .when() .get("/1") @@ -212,12 +225,21 @@ public final class PortfolioResourceTest { */ @Test @TestSecurity( - user = "testUser", + user = "owner", roles = {"openid"}) public void testDeleteNonExistent() { - Mockito.when(portfolioService.delete("testUser", 1, 1)).thenReturn(null); + given().pathParam("project", "1").when().delete("/0").then().statusCode(404); + } - given().pathParam("project", "1").when().delete("/1").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); } /** @@ -225,16 +247,41 @@ public final class PortfolioResourceTest { */ @Test @TestSecurity( - user = "testUser", + user = "owner", roles = {"openid"}) public void testDelete() { - Mockito.when(portfolioService.delete("testUser", 1, 1)).thenReturn(dummyPortfolio); + 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("/1") + .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 index 8cb95a98..4f8d412c 100644 --- 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 @@ -24,32 +24,15 @@ package org.opendc.web.server.rest.user; import static io.restassured.RestAssured.given; import static org.hamcrest.Matchers.equalTo; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.ArgumentMatchers.eq; import io.quarkus.test.common.http.TestHTTPEndpoint; import io.quarkus.test.junit.QuarkusTest; -import io.quarkus.test.junit.mockito.InjectMock; import io.quarkus.test.security.TestSecurity; import io.restassured.http.ContentType; -import java.time.Instant; -import java.util.List; -import java.util.Set; import org.junit.jupiter.api.Test; -import org.mockito.Mockito; -import org.opendc.web.proto.JobState; import org.opendc.web.proto.OperationalPhenomena; -import org.opendc.web.proto.Targets; -import org.opendc.web.proto.Trace; import org.opendc.web.proto.Workload; -import org.opendc.web.proto.user.Job; -import org.opendc.web.proto.user.Portfolio; -import org.opendc.web.proto.user.Project; -import org.opendc.web.proto.user.ProjectRole; import org.opendc.web.proto.user.Scenario; -import org.opendc.web.proto.user.Topology; -import org.opendc.web.server.service.ScenarioService; /** * Test suite for {@link PortfolioScenarioResource}. @@ -57,30 +40,6 @@ import org.opendc.web.server.service.ScenarioService; @QuarkusTest @TestHTTPEndpoint(PortfolioScenarioResource.class) public final class PortfolioScenarioResourceTest { - @InjectMock - private ScenarioService scenarioService; - - /** - * Dummy values - */ - private final Project dummyProject = new Project(0, "test", Instant.now(), Instant.now(), ProjectRole.OWNER); - - private final Portfolio.Summary dummyPortfolio = new Portfolio.Summary(1, 1, "test", new Targets(Set.of(), 1)); - private final Job dummyJob = new Job(1, JobState.PENDING, Instant.now(), Instant.now(), null); - private final Trace dummyTrace = new Trace("bitbrains", "Bitbrains", "vm"); - private final Topology.Summary dummyTopology = new Topology.Summary(1, 1, "test", Instant.now(), Instant.now()); - private final Scenario dummyScenario = new Scenario( - 1, - 1, - dummyProject, - dummyPortfolio, - "test", - new Workload(dummyTrace, 1.0), - dummyTopology, - new OperationalPhenomena(false, false), - "test", - dummyJob); - /** * Test that tries to obtain a portfolio without token. */ @@ -99,7 +58,7 @@ public final class PortfolioScenarioResourceTest { */ @Test @TestSecurity( - user = "testUser", + user = "owner", roles = {"runner"}) public void testGetInvalidToken() { given().pathParam("project", "1") @@ -111,15 +70,30 @@ public final class PortfolioScenarioResourceTest { } /** - * Test that tries to obtain a non-existent portfolio. + * Test that tries to obtain a scenario without authorization. */ @Test @TestSecurity( - user = "testUser", + user = "unknown", roles = {"openid"}) - public void testGet() { - Mockito.when(scenarioService.findAll("testUser", 1, 1)).thenReturn(List.of()); + 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() @@ -134,14 +108,31 @@ public final class PortfolioScenarioResourceTest { */ @Test @TestSecurity( - user = "testUser", + user = "owner", roles = {"openid"}) public void testCreateNonExistent() { - Mockito.when(scenarioService.create(eq("testUser"), eq(1L), anyInt(), any())) - .thenReturn(null); + 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", "1") + .pathParam("portfolio", "0") .body(new Scenario.Create( "test", new Workload.Spec("test", 1.0), 1, new OperationalPhenomena(false, false), "test")) .contentType(ContentType.JSON) @@ -152,28 +143,48 @@ public final class PortfolioScenarioResourceTest { .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 = "testUser", + user = "owner", roles = {"openid"}) public void testCreate() { - Mockito.when(scenarioService.create(eq("testUser"), eq(1L), eq(1), any())) - .thenReturn(dummyScenario); - given().pathParam("project", "1") .pathParam("portfolio", "1") .body(new Scenario.Create( - "test", new Workload.Spec("test", 1.0), 1, new OperationalPhenomena(false, false), "test")) + "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("id", equalTo(1)) .body("name", equalTo("test")); } @@ -182,7 +193,7 @@ public final class PortfolioScenarioResourceTest { */ @Test @TestSecurity( - user = "testUser", + user = "owner", roles = {"openid"}) public void testCreateEmpty() { given().pathParam("project", "1") @@ -201,7 +212,7 @@ public final class PortfolioScenarioResourceTest { */ @Test @TestSecurity( - user = "testUser", + user = "owner", roles = {"openid"}) public void testCreateBlankName() { given().pathParam("project", "1") @@ -215,4 +226,48 @@ public final class PortfolioScenarioResourceTest { .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 index 7ca314a6..8bd60808 100644 --- 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 @@ -28,16 +28,9 @@ import static org.hamcrest.Matchers.equalTo; import io.quarkus.test.common.http.TestHTTPEndpoint; import io.quarkus.test.junit.QuarkusTest; -import io.quarkus.test.junit.mockito.InjectMock; import io.quarkus.test.security.TestSecurity; import io.restassured.http.ContentType; -import java.time.Instant; -import java.util.List; import org.junit.jupiter.api.Test; -import org.mockito.Mockito; -import org.opendc.web.proto.user.Project; -import org.opendc.web.proto.user.ProjectRole; -import org.opendc.web.server.service.ProjectService; /** * Test suite for [ProjectResource]. @@ -45,14 +38,6 @@ import org.opendc.web.server.service.ProjectService; @QuarkusTest @TestHTTPEndpoint(ProjectResource.class) public final class ProjectResourceTest { - @InjectMock - private ProjectService projectService; - - /** - * Dummy values. - */ - private final Project dummyProject = new Project(0, "test", Instant.now(), Instant.now(), ProjectRole.OWNER); - /** * Test that tries to obtain all projects without token. */ @@ -66,7 +51,7 @@ public final class ProjectResourceTest { */ @Test @TestSecurity( - user = "testUser", + user = "owner", roles = {"runner"}) public void testGetAllWithInvalidScope() { when().get().then().statusCode(403); @@ -77,12 +62,10 @@ public final class ProjectResourceTest { */ @Test @TestSecurity( - user = "testUser", + user = "owner", roles = {"openid"}) public void testGetAll() { - Mockito.when(projectService.findByUser("testUser")).thenReturn(List.of(dummyProject)); - - when().get().then().statusCode(200).contentType(ContentType.JSON).body("get(0).name", equalTo("test")); + when().get().then().statusCode(200).contentType(ContentType.JSON).body("get(0).name", equalTo("Test Project")); } /** @@ -90,25 +73,21 @@ public final class ProjectResourceTest { */ @Test @TestSecurity( - user = "testUser", + user = "owner", roles = {"openid"}) public void testGetNonExisting() { - Mockito.when(projectService.findByUser("testUser", 1)).thenReturn(null); - - when().get("/1").then().statusCode(404).contentType(ContentType.JSON); + when().get("/0").then().statusCode(404).contentType(ContentType.JSON); } /** - * Test that tries to obtain a job. + * Test that tries to obtain a project. */ @Test @TestSecurity( - user = "testUser", + user = "owner", roles = {"openid"}) public void testGetExisting() { - Mockito.when(projectService.findByUser("testUser", 1)).thenReturn(dummyProject); - - when().get("/1").then().statusCode(200).contentType(ContentType.JSON).body("id", equalTo(0)); + when().get("/1").then().statusCode(200).contentType(ContentType.JSON).body("id", equalTo(1)); } /** @@ -116,19 +95,16 @@ public final class ProjectResourceTest { */ @Test @TestSecurity( - user = "testUser", + user = "owner", roles = {"openid"}) public void testCreate() { - Mockito.when(projectService.create("testUser", "test")).thenReturn(dummyProject); - - given().body(new Project.Create("test")) + given().body(new org.opendc.web.proto.user.Project.Create("test")) .contentType(ContentType.JSON) .when() .post() .then() .statusCode(200) .contentType(ContentType.JSON) - .body("id", equalTo(0)) .body("name", equalTo("test")); } @@ -137,7 +113,7 @@ public final class ProjectResourceTest { */ @Test @TestSecurity( - user = "testUser", + user = "owner", roles = {"openid"}) public void testCreateEmpty() { given().body("{}") @@ -154,10 +130,10 @@ public final class ProjectResourceTest { */ @Test @TestSecurity( - user = "testUser", + user = "owner", roles = {"openid"}) public void testCreateBlankName() { - given().body(new Project.Create("")) + given().body(new org.opendc.web.proto.user.Project.Create("")) .contentType(ContentType.JSON) .when() .post() @@ -171,12 +147,10 @@ public final class ProjectResourceTest { */ @Test @TestSecurity( - user = "testUser", + user = "owner", roles = {"openid"}) public void testDeleteNonExistent() { - Mockito.when(projectService.delete("testUser", 1)).thenReturn(null); - - when().delete("/1").then().statusCode(404).contentType(ContentType.JSON); + when().delete("/0").then().statusCode(404).contentType(ContentType.JSON); } /** @@ -184,12 +158,20 @@ public final class ProjectResourceTest { */ @Test @TestSecurity( - user = "testUser", + user = "owner", roles = {"openid"}) public void testDelete() { - Mockito.when(projectService.delete("testUser", 1)).thenReturn(dummyProject); + 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("/1").then().statusCode(200).contentType(ContentType.JSON); + when().delete("/" + id).then().statusCode(200).contentType(ContentType.JSON); } /** @@ -197,12 +179,9 @@ public final class ProjectResourceTest { */ @Test @TestSecurity( - user = "testUser", + user = "viewer", roles = {"openid"}) public void testDeleteNonOwner() { - Mockito.when(projectService.delete("testUser", 1)) - .thenThrow(new IllegalArgumentException("User does not own project")); - 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 index 850236d6..a980e4e2 100644 --- 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 @@ -27,55 +27,42 @@ import static org.hamcrest.Matchers.equalTo; import io.quarkus.test.common.http.TestHTTPEndpoint; import io.quarkus.test.junit.QuarkusTest; -import io.quarkus.test.junit.mockito.InjectMock; import io.quarkus.test.security.TestSecurity; +import io.restassured.builder.RequestSpecBuilder; import io.restassured.http.ContentType; -import java.time.Instant; -import java.util.Set; +import io.restassured.specification.RequestSpecification; import org.junit.jupiter.api.Test; -import org.mockito.Mockito; -import org.opendc.web.proto.JobState; import org.opendc.web.proto.OperationalPhenomena; -import org.opendc.web.proto.Targets; -import org.opendc.web.proto.Trace; import org.opendc.web.proto.Workload; -import org.opendc.web.proto.user.Job; -import org.opendc.web.proto.user.Portfolio; -import org.opendc.web.proto.user.Project; -import org.opendc.web.proto.user.ProjectRole; import org.opendc.web.proto.user.Scenario; -import org.opendc.web.proto.user.Topology; -import org.opendc.web.server.service.ScenarioService; /** - * Test suite for [ScenarioResource]. + * Test suite for {@link ScenarioResource}. */ @QuarkusTest @TestHTTPEndpoint(ScenarioResource.class) public final class ScenarioResourceTest { - @InjectMock - private ScenarioService scenarioService; + /** + * 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); + } /** - * Dummy values + * Test that tries to obtain all scenarios belonging to a project. */ - private final Project dummyProject = new Project(0, "test", Instant.now(), Instant.now(), ProjectRole.OWNER); - - private final Portfolio.Summary dummyPortfolio = new Portfolio.Summary(1, 1, "test", new Targets(Set.of(), 1)); - private final Job dummyJob = new Job(1, JobState.PENDING, Instant.now(), Instant.now(), null); - private final Trace dummyTrace = new Trace("bitbrains", "Bitbrains", "vm"); - private final Topology.Summary dummyTopology = new Topology.Summary(1, 1, "test", Instant.now(), Instant.now()); - private final Scenario dummyScenario = new Scenario( - 1, - 1, - dummyProject, - dummyPortfolio, - "test", - new Workload(dummyTrace, 1.0), - dummyTopology, - new OperationalPhenomena(false, false), - "test", - dummyJob); + @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. @@ -90,7 +77,7 @@ public final class ScenarioResourceTest { */ @Test @TestSecurity( - user = "testUser", + user = "owner", roles = {"runner"}) public void testGetInvalidToken() { given().pathParam("project", "1").when().get("/1").then().statusCode(403); @@ -101,11 +88,25 @@ public final class ScenarioResourceTest { */ @Test @TestSecurity( - user = "testUser", + user = "owner", roles = {"openid"}) public void testGetNonExisting() { - Mockito.when(scenarioService.findOne("testUser", 1, 1)).thenReturn(null); + 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") @@ -119,11 +120,9 @@ public final class ScenarioResourceTest { */ @Test @TestSecurity( - user = "testUser", + user = "owner", roles = {"openid"}) public void testGetExisting() { - Mockito.when(scenarioService.findOne("testUser", 1, 1)).thenReturn(dummyScenario); - given().pathParam("project", "1") .when() .get("/1") @@ -138,27 +137,65 @@ public final class ScenarioResourceTest { */ @Test @TestSecurity( - user = "testUser", + user = "owner", roles = {"openid"}) public void testDeleteNonExistent() { - Mockito.when(scenarioService.delete("testUser", 1, 1)).thenReturn(null); + 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 = "testUser", + user = "owner", roles = {"openid"}) public void testDelete() { - Mockito.when(scenarioService.delete("testUser", 1, 1)).thenReturn(dummyScenario); + 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("/1") + .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 index 2cc6ea4b..21e35b09 100644 --- 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 @@ -24,24 +24,14 @@ package org.opendc.web.server.rest.user; import static io.restassured.RestAssured.given; import static org.hamcrest.Matchers.equalTo; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.ArgumentMatchers.eq; import io.quarkus.test.common.http.TestHTTPEndpoint; import io.quarkus.test.junit.QuarkusTest; -import io.quarkus.test.junit.mockito.InjectMock; import io.quarkus.test.security.TestSecurity; import io.restassured.http.ContentType; -import java.time.Instant; import java.util.List; import org.junit.jupiter.api.Test; -import org.mockito.Mockito; -import org.opendc.web.proto.user.Project; -import org.opendc.web.proto.user.ProjectRole; import org.opendc.web.proto.user.Topology; -import org.opendc.web.server.service.TopologyService; /** * Test suite for {@link TopologyResource}. @@ -49,27 +39,31 @@ import org.opendc.web.server.service.TopologyService; @QuarkusTest @TestHTTPEndpoint(TopologyResource.class) public final class TopologyResourceTest { - @InjectMock - private TopologyService topologyService; - /** - * Dummy project and topology. + * Test that tries to obtain the list of topologies of a project without proper authorization. */ - private final Project dummyProject = new Project(1, "test", Instant.now(), Instant.now(), ProjectRole.OWNER); - - private final Topology dummyTopology = - new Topology(1, 1, dummyProject, "test", List.of(), Instant.now(), Instant.now()); + @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 = "testUser", + user = "owner", roles = {"openid"}) - public void testGetForProject() { - Mockito.when(topologyService.findAll("testUser", 1)).thenReturn(List.of()); - + public void testGetAll() { given().pathParam("project", "1").when().get().then().statusCode(200).contentType(ContentType.JSON); } @@ -78,18 +72,34 @@ public final class TopologyResourceTest { */ @Test @TestSecurity( - user = "testUser", + user = "owner", roles = {"openid"}) public void testCreateNonExistent() { - Mockito.when(topologyService.create(eq("testUser"), eq(1L), any())).thenReturn(null); + 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(404) + .statusCode(403) .contentType(ContentType.JSON); } @@ -98,11 +108,9 @@ public final class TopologyResourceTest { */ @Test @TestSecurity( - user = "testUser", + user = "owner", roles = {"openid"}) public void testCreate() { - Mockito.when(topologyService.create(eq("testUser"), eq(1L), any())).thenReturn(dummyTopology); - given().pathParam("project", "1") .body(new Topology.Create("test", List.of())) .contentType(ContentType.JSON) @@ -111,7 +119,6 @@ public final class TopologyResourceTest { .then() .statusCode(200) .contentType(ContentType.JSON) - .body("id", equalTo(1)) .body("name", equalTo("test")); } @@ -120,7 +127,7 @@ public final class TopologyResourceTest { */ @Test @TestSecurity( - user = "testUser", + user = "owner", roles = {"openid"}) public void testCreateEmpty() { given().pathParam("project", "1") @@ -138,7 +145,7 @@ public final class TopologyResourceTest { */ @Test @TestSecurity( - user = "testUser", + user = "owner", roles = {"openid"}) public void testCreateBlankName() { given().pathParam("project", "1") @@ -164,7 +171,7 @@ public final class TopologyResourceTest { */ @Test @TestSecurity( - user = "testUser", + user = "owner", roles = {"runner"}) public void testGetInvalidToken() { given().pathParam("project", "1").when().get("/1").then().statusCode(403); @@ -175,11 +182,25 @@ public final class TopologyResourceTest { */ @Test @TestSecurity( - user = "testUser", + user = "owner", roles = {"openid"}) public void testGetNonExisting() { - Mockito.when(topologyService.findOne("testUser", 1, 1)).thenReturn(null); + 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") @@ -193,11 +214,9 @@ public final class TopologyResourceTest { */ @Test @TestSecurity( - user = "testUser", + user = "owner", roles = {"openid"}) public void testGetExisting() { - Mockito.when(topologyService.findOne("testUser", 1, 1)).thenReturn(dummyTopology); - given().pathParam("project", "1") .when() .get("/1") @@ -212,12 +231,26 @@ public final class TopologyResourceTest { */ @Test @TestSecurity( - user = "testUser", + user = "owner", roles = {"openid"}) public void testUpdateNonExistent() { - Mockito.when(topologyService.update(eq("testUser"), anyLong(), anyInt(), any())) - .thenReturn(null); + 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) @@ -227,17 +260,32 @@ public final class TopologyResourceTest { .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 = "testUser", + user = "owner", roles = {"openid"}) public void testUpdate() { - Mockito.when(topologyService.update(eq("testUser"), anyLong(), anyInt(), any())) - .thenReturn(dummyTopology); - given().pathParam("project", "1") .body(new Topology.Update(List.of())) .contentType(ContentType.JSON) @@ -253,27 +301,56 @@ public final class TopologyResourceTest { */ @Test @TestSecurity( - user = "testUser", + user = "owner", roles = {"openid"}) public void testDeleteNonExistent() { - Mockito.when(topologyService.delete("testUser", 1, 1)).thenReturn(null); + 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 = "testUser", + user = "owner", roles = {"openid"}) public void testDelete() { - Mockito.when(topologyService.delete("testUser", 1, 1)).thenReturn(dummyTopology); + 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("/1") + .delete("/" + number) .then() .statusCode(200) .contentType(ContentType.JSON); 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-ui/src/components/portfolios/PortfolioResults.js b/opendc-web/opendc-web-ui/src/components/portfolios/PortfolioResults.js index f50105ed..62150fa7 100644 --- a/opendc-web/opendc-web-ui/src/components/portfolios/PortfolioResults.js +++ b/opendc-web/opendc-web-ui/src/components/portfolios/PortfolioResults.js @@ -57,14 +57,17 @@ function PortfolioResults({ projectId, portfolioId }) { const dataPerMetric = {} AVAILABLE_METRICS.forEach((metric) => { dataPerMetric[metric] = scenarios - .filter((scenario) => scenario.job?.results) - .map((scenario) => ({ - metric, - x: scenario.name, - y: mean(scenario.job.results[metric]), - errorY: std(scenario.job.results[metric]), - label, - })) + .filter((scenario) => scenario.jobs && scenario.jobs[scenario.jobs.length - 1].results) + .map((scenario) => { + const job = scenario.jobs[scenario.jobs.length - 1] + return { + metric, + x: scenario.name, + y: mean(job.results[metric]), + errorY: std(job.results[metric]), + label, + } + }) }) return dataPerMetric }, [scenarios]) diff --git a/opendc-web/opendc-web-ui/src/components/portfolios/ScenarioTable.js b/opendc-web/opendc-web-ui/src/components/portfolios/ScenarioTable.js index 5fd2a1da..b068d045 100644 --- a/opendc-web/opendc-web-ui/src/components/portfolios/ScenarioTable.js +++ b/opendc-web/opendc-web-ui/src/components/portfolios/ScenarioTable.js @@ -59,7 +59,7 @@ function ScenarioTable({ portfolio, status }) { {scenario.topology ? ( - scenario.topology.name + {scenario.topology.name} ) : ( 'Unknown Topology' @@ -69,7 +69,7 @@ function ScenarioTable({ portfolio, status }) { scenario.workload.samplingFraction * 100 }%)`} - + diff --git a/opendc-web/opendc-web-ui/src/shapes.js b/opendc-web/opendc-web-ui/src/shapes.js index 6c93f458..50b82361 100644 --- a/opendc-web/opendc-web-ui/src/shapes.js +++ b/opendc-web/opendc-web-ui/src/shapes.js @@ -159,7 +159,7 @@ export const Scenario = PropTypes.shape({ topology: TopologySummary.isRequired, phenomena: Phenomena.isRequired, schedulerName: PropTypes.string.isRequired, - job: Job.isRequired, + jobs: PropTypes.arrayOf(Job).isRequired, }) export const Portfolio = PropTypes.shape({ -- cgit v1.2.3