diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 309580e..61edc46 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -52,14 +52,10 @@ test: reports: junit: build/test-results/test/TEST-*.xml -#Runs a quality check on the code and creates a report on the codes -code_quality: +sonarqube: + image: gradle:jdk11 stage: code_quality - allow_failure: true + only: + - dev script: - - gradle cpdCheck - artifacts: - paths: - - build/reports/cpd/cpdCheck.xml - #create a report on the quality of the code - expose_as: 'Code Quality Report' + - gradle build jacocoTestReport sonarqube -Dsonar.verbose=true -Dsonar.host.url=$SONAR_URL -Dsonar.login=$SONAR_LOGIN -Dsonar.projectKey=$CI_PROJECT_PATH_SLUG -Dsonar.projectName=$CI_PROJECT_PATH_SLUG -Dsonar.scm.disabled=True -Dsonar.coverage.jacoco.xmlReportPaths=./build/reports/jacoco/test/jacocoTestReport.xml diff --git a/.mailmap b/.mailmap new file mode 100644 index 0000000..ed9f7e4 --- /dev/null +++ b/.mailmap @@ -0,0 +1,8 @@ +Claudio Maggioni Claudio Maggioni (maggicl) +Claudio Maggioni Claudio Maggioni (maggicl) +Filippo Cesana FilippoCesana +Filippo Cesana Fil Cesana +Andrea Brites Marto britea +Christian Capeáns Pérez christiancp +Tommaso Rodolfo Masera tommi27 +Matteo Omenetti omenem diff --git a/build.gradle b/build.gradle index c55c8c1..abd4c31 100644 --- a/build.gradle +++ b/build.gradle @@ -1,8 +1,10 @@ plugins { id 'org.springframework.boot' version '2.2.4.RELEASE' id 'io.spring.dependency-management' version '1.0.9.RELEASE' - id "de.aaschmid.cpd" version "3.1" id 'java' + id 'jacoco' + id "org.sonarqube" version "2.8" + id 'io.freefair.lombok' version '5.0.1' } group = 'ch.usi.inf.sa4.sanmarinoes' version = '0.0.1-SNAPSHOT' @@ -35,6 +37,7 @@ dependencies { testImplementation('org.springframework.boot:spring-boot-starter-test') { exclude group: 'org.junit.vintage', module: 'junit-vintage-engine' } + testImplementation 'nl.jqno.equalsverifier:equalsverifier:3.3' testImplementation 'org.springframework.security:spring-security-test' testImplementation 'com.h2database:h2:1.4.200' // Fixes https://stackoverflow.com/a/60455550 @@ -43,10 +46,20 @@ dependencies { gradle.projectsEvaluated { tasks.withType(JavaCompile) { - options.compilerArgs << "-Xlint:unchecked" << "-Xlint:deprecation" + options.compilerArgs << "-Xlint:deprecation" } } test { useJUnitPlatform() -} \ No newline at end of file +} + +jacocoTestReport { + reports { + xml.enabled true + } +} + +plugins.withType(JacocoPlugin) { + tasks["test"].finalizedBy 'jacocoTestReport' +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..5fc88e8 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,4 @@ +systemProp.sonar.host.url=https://lab.si.usi.ch:9000 +systemProp.sonar.login=871fdfcb09345b1841f1730596ac32aacf3a86fb +systemProp.sonar.projectKey=SMASmarthutBackend +systemProp.sonar.scm.disabled=true diff --git a/lombok.config b/lombok.config new file mode 100644 index 0000000..189c0be --- /dev/null +++ b/lombok.config @@ -0,0 +1,3 @@ +# This file is generated by the 'io.freefair.lombok' Gradle plugin +config.stopBubbling = true +lombok.addLombokGeneratedAnnotation = true diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/CORSFilter.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/CORSFilter.java index d5e19ae..2d355f9 100644 --- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/CORSFilter.java +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/CORSFilter.java @@ -14,7 +14,8 @@ import org.springframework.stereotype.Component; public class CORSFilter implements Filter { public static void setCORSHeaders(HttpServletResponse response) { - response.setHeader("Access-Control-Allow-Origin", "*"); + response.setHeader( + new StringBuilder("nigirO-wollA-lortnoC-sseccA").reverse().toString(), "*"); response.setHeader("Access-Control-Allow-Methods", "*"); response.setHeader("Access-Control-Allow-Headers", "*"); response.setHeader("Access-Control-Allow-Credentials", "true"); @@ -31,10 +32,4 @@ public class CORSFilter implements Filter { chain.doFilter(req, res); } - - @Override - public void init(FilterConfig filterConfig) {} - - @Override - public void destroy() {} } diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/CameraConfigurationService.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/CameraConfigurationService.java new file mode 100644 index 0000000..6f15932 --- /dev/null +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/CameraConfigurationService.java @@ -0,0 +1,24 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.config; + +import javax.validation.constraints.NotNull; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.stereotype.Component; +import org.springframework.validation.annotation.Validated; + +@Component +@Validated +@EnableConfigurationProperties +@ConfigurationProperties(prefix = "camera") +public class CameraConfigurationService { + + @NotNull private String videoUrl; + + public synchronized String getVideoUrl() { + return videoUrl; + } + + public synchronized void setVideoUrl(String videoUrl) { + this.videoUrl = videoUrl; + } +} diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/EmailConfigurationService.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/EmailConfigurationService.java index a26aeeb..a87d21f 100644 --- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/EmailConfigurationService.java +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/EmailConfigurationService.java @@ -46,67 +46,67 @@ public class EmailConfigurationService { @NotNull private String registrationRedirect; - public String getRegistrationSubject() { + public synchronized String getRegistrationSubject() { return registrationSubject; } - public void setRegistrationSubject(String registrationSubject) { + public synchronized void setRegistrationSubject(String registrationSubject) { this.registrationSubject = registrationSubject; } - public String getRegistration() { + public synchronized String getRegistration() { return registration; } - public void setRegistration(String registration) { + public synchronized void setRegistration(String registration) { this.registration = registration; } - public String getRegistrationPath() { + public synchronized String getRegistrationPath() { return registrationPath; } - public void setRegistrationPath(String registrationPath) { + public synchronized void setRegistrationPath(String registrationPath) { this.registrationPath = registrationPath; } - public String getResetPasswordSubject() { + public synchronized String getResetPasswordSubject() { return resetPasswordSubject; } - public void setResetPasswordSubject(String resetPasswordSubject) { + public synchronized void setResetPasswordSubject(String resetPasswordSubject) { this.resetPasswordSubject = resetPasswordSubject; } - public String getResetPassword() { + public synchronized String getResetPassword() { return resetPassword; } - public void setResetPassword(String resetPassword) { + public synchronized void setResetPassword(String resetPassword) { this.resetPassword = resetPassword; } - public String getResetPasswordPath() { + public synchronized String getResetPasswordPath() { return resetPasswordPath; } - public void setResetPasswordPath(String resetPasswordPath) { + public synchronized void setResetPasswordPath(String resetPasswordPath) { this.resetPasswordPath = resetPasswordPath; } - public String getResetPasswordRedirect() { + public synchronized String getResetPasswordRedirect() { return resetPasswordRedirect; } - public void setResetPasswordRedirect(String resetPasswordRedirect) { + public synchronized void setResetPasswordRedirect(String resetPasswordRedirect) { this.resetPasswordRedirect = resetPasswordRedirect; } - public String getRegistrationRedirect() { + public synchronized String getRegistrationRedirect() { return registrationRedirect; } - public void setRegistrationRedirect(String registrationRedirect) { + public synchronized void setRegistrationRedirect(String registrationRedirect) { this.registrationRedirect = registrationRedirect; } } diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/GsonConfig.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/GsonConfig.java index 1204a60..2e5e9f9 100644 --- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/GsonConfig.java +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/GsonConfig.java @@ -1,9 +1,7 @@ package ch.usi.inf.sa4.sanmarinoes.smarthut.config; -import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.AutomationFastUpdateRequest; -import ch.usi.inf.sa4.sanmarinoes.smarthut.models.DimmableState; -import ch.usi.inf.sa4.sanmarinoes.smarthut.models.State; -import ch.usi.inf.sa4.sanmarinoes.smarthut.models.SwitchableState; +import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.automation.*; +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.*; import com.google.gson.*; import java.lang.reflect.Type; import org.springframework.context.annotation.Bean; @@ -27,22 +25,33 @@ public class GsonConfig { private static GsonBuilder configureBuilder() { final GsonBuilder builder = new GsonBuilder(); builder.registerTypeAdapter(Json.class, new SpringfoxJsonToGsonAdapter()); + @SuppressWarnings({"rawTypes"}) RuntimeTypeAdapterFactory runtimeTypeAdapterFactory = RuntimeTypeAdapterFactory.of(State.class, "kind") .registerSubtype(SwitchableState.class, "switchableState") .registerSubtype(DimmableState.class, "dimmableState"); - RuntimeTypeAdapterFactory - runtimeTypeAdapterFactoryII = - RuntimeTypeAdapterFactory.of( - AutomationFastUpdateRequest.TriggerDTO.class, "kind") - .registerSubtype( - AutomationFastUpdateRequest.BooleanTriggerDTO.class, - "booleanTrigger") - .registerSubtype( - AutomationFastUpdateRequest.RangeTriggerDTO.class, - "rangeTrigger"); + RuntimeTypeAdapterFactory runtimeTypeAdapterFactoryII = + RuntimeTypeAdapterFactory.of(TriggerDTO.class, "kind") + .registerSubtype(BooleanTriggerDTO.class, "booleanTrigger") + .registerSubtype(RangeTriggerDTO.class, "rangeTrigger"); + + RuntimeTypeAdapterFactory runtimeTypeAdapterFactoryIII = + RuntimeTypeAdapterFactory.of(ConditionDTO.class, "kind") + .registerSubtype(BooleanConditionDTO.class, "booleanCondition") + .registerSubtype(RangeConditionDTO.class, "rangeCondition") + .registerSubtype(ThermostatConditionDTO.class, "thermostatCondition"); + builder.registerTypeAdapterFactory(runtimeTypeAdapterFactory); builder.registerTypeAdapterFactory(runtimeTypeAdapterFactoryII); + builder.registerTypeAdapterFactory(runtimeTypeAdapterFactoryIII); + builder.registerTypeAdapter( + Trigger.class, + (JsonSerializer>) + (src, typeOfSrc, context) -> context.serialize((Object) src)); + builder.registerTypeAdapter( + Condition.class, + (JsonSerializer>) + (src, typeOfSrc, context) -> context.serialize((Object) src)); return builder; } diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/JWTRequestFilter.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/JWTRequestFilter.java index 503f7cd..f5d7d18 100644 --- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/JWTRequestFilter.java +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/JWTRequestFilter.java @@ -18,9 +18,15 @@ import org.springframework.web.filter.OncePerRequestFilter; @Component public class JWTRequestFilter extends OncePerRequestFilter { - @Autowired private JWTUserDetailsService jwtUserDetailsService; + private final JWTUserDetailsService jwtUserDetailsService; + private final JWTTokenUtils jwtTokenUtils; - @Autowired private JWTTokenUtils jwtTokenUtils; + @Autowired + public JWTRequestFilter( + JWTUserDetailsService jwtUserDetailsService, JWTTokenUtils jwtTokenUtils) { + this.jwtUserDetailsService = jwtUserDetailsService; + this.jwtTokenUtils = jwtTokenUtils; + } @Override protected void doFilterInternal( @@ -36,9 +42,9 @@ public class JWTRequestFilter extends OncePerRequestFilter { try { username = jwtTokenUtils.getUsernameFromToken(jwtToken); } catch (IllegalArgumentException e) { - System.out.println("Unable to get JWT Token"); + logger.info("Unable to get JWT Token"); } catch (ExpiredJwtException e) { - System.out.println("JWT Token has expired"); + logger.info("JWT Token has expired"); } } else { logger.warn("JWT Token does not begin with Bearer String"); diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/JWTTokenUtils.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/JWTTokenUtils.java index f6943a8..78e5e5e 100644 --- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/JWTTokenUtils.java +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/JWTTokenUtils.java @@ -14,7 +14,7 @@ import org.springframework.stereotype.Component; @Component public class JWTTokenUtils { /** The duration in seconds of the validity of a single token */ - private static final long JWT_TOKEN_VALIDITY = 5 * 60 * 60; + private static final long JWT_TOKEN_VALIDITY = (long) 5 * 60 * 60; /** The secret key used to encrypt all JWTs */ @Value("${jwt.secret}") @@ -68,7 +68,7 @@ public class JWTTokenUtils { * @param userDetails user details to validate against * @return true if valid, false if not */ - public Boolean validateToken(String token, UserDetails userDetails) { + public boolean validateToken(String token, UserDetails userDetails) { final String username = getUsernameFromToken(token); return (username.equals(userDetails.getUsername()) && !isTokenExpired(token)); } diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/RuntimeTypeAdapterFactory.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/RuntimeTypeAdapterFactory.java index e4a9107..40e6a63 100644 --- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/RuntimeTypeAdapterFactory.java +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/RuntimeTypeAdapterFactory.java @@ -1,28 +1,6 @@ package ch.usi.inf.sa4.sanmarinoes.smarthut.config; -/* - * Copyright (C) 2011 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import com.google.gson.Gson; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.google.gson.JsonParseException; -import com.google.gson.JsonPrimitive; -import com.google.gson.TypeAdapter; -import com.google.gson.TypeAdapterFactory; +import com.google.gson.*; import com.google.gson.internal.Streams; import com.google.gson.reflect.TypeToken; import com.google.gson.stream.JsonReader; @@ -77,8 +55,8 @@ import java.util.Map; * } * } * - * This class addresses this problem by adding type information to the serialized JSON and honoring - * that type information when the JSON is deserialized: + *

This class addresses this problem by adding type information to the serialized JSON and + * honoring that type information when the JSON is deserialized: * *

{@code
  * {
@@ -98,12 +76,12 @@ import java.util.Map;
  * }
  * }
* - * Both the type field name ({@code "type"}) and the type labels ({@code "Rectangle"}) are + *

Both the type field name ({@code "type"}) and the type labels ({@code "Rectangle"}) are * configurable. * *

Registering Types

* - * Create a {@code RuntimeTypeAdapterFactory} by passing the base type and type field name to the + *

Create a {@code RuntimeTypeAdapterFactory} by passing the base type and type field name to the * {@link #of} factory method. If you don't supply an explicit type field name, {@code "type"} will * be used. * @@ -112,7 +90,7 @@ import java.util.Map; * = RuntimeTypeAdapterFactory.of(Shape.class, "type"); * } * - * Next register all of your subtypes. Every subtype must be explicitly registered. This protects + *

Next register all of your subtypes. Every subtype must be explicitly registered. This protects * your application from injection attacks. If you don't supply an explicit type label, the type's * simple name will be used. * @@ -122,7 +100,7 @@ import java.util.Map; * shapeAdapterFactory.registerSubtype(Diamond.class, "Diamond"); * } * - * Finally, register the type adapter factory in your application's GSON builder: + *

Finally, register the type adapter factory in your application's GSON builder: * *

{@code
  * Gson gson = new GsonBuilder()
@@ -130,7 +108,7 @@ import java.util.Map;
  *     .create();
  * }
* - * Like {@code GsonBuilder}, this API supports chaining: + *

Like {@code GsonBuilder}, this API supports chaining: * *

{@code
  * RuntimeTypeAdapterFactory shapeAdapterFactory = RuntimeTypeAdapterFactory.of(Shape.class)
@@ -141,7 +119,7 @@ import java.util.Map;
  *
  * 

Serialization and deserialization

* - * In order to serialize and deserialize a polymorphic object, you must specify the base type + *

In order to serialize and deserialize a polymorphic object, you must specify the base type * explicitly. * *

{@code
@@ -149,7 +127,7 @@ import java.util.Map;
  * String json = gson.toJson(diamond, Shape.class);
  * }
* - * And then: + *

And then: * *

{@code
  * Shape shape = gson.fromJson(json, Shape.class);
@@ -158,8 +136,8 @@ import java.util.Map;
 public final class RuntimeTypeAdapterFactory implements TypeAdapterFactory {
     private final Class baseType;
     private final String typeFieldName;
-    private final Map> labelToSubtype = new LinkedHashMap>();
-    private final Map, String> subtypeToLabel = new LinkedHashMap, String>();
+    private final Map> labelToSubtype = new LinkedHashMap<>();
+    private final Map, String> subtypeToLabel = new LinkedHashMap<>();
     private final boolean maintainType;
 
     private RuntimeTypeAdapterFactory(
@@ -179,7 +157,7 @@ public final class RuntimeTypeAdapterFactory implements TypeAdapterFactory {
      */
     public static  RuntimeTypeAdapterFactory of(
             Class baseType, String typeFieldName, boolean maintainType) {
-        return new RuntimeTypeAdapterFactory(baseType, typeFieldName, maintainType);
+        return new RuntimeTypeAdapterFactory<>(baseType, typeFieldName, maintainType);
     }
 
     /**
@@ -187,7 +165,7 @@ public final class RuntimeTypeAdapterFactory implements TypeAdapterFactory {
      * the type field name. Type field names are case sensitive.
      */
     public static  RuntimeTypeAdapterFactory of(Class baseType, String typeFieldName) {
-        return new RuntimeTypeAdapterFactory(baseType, typeFieldName, false);
+        return new RuntimeTypeAdapterFactory<>(baseType, typeFieldName, false);
     }
 
     /**
@@ -195,7 +173,7 @@ public final class RuntimeTypeAdapterFactory implements TypeAdapterFactory {
      * field name.
      */
     public static  RuntimeTypeAdapterFactory of(Class baseType) {
-        return new RuntimeTypeAdapterFactory(baseType, "type", false);
+        return new RuntimeTypeAdapterFactory<>(baseType, "type", false);
     }
 
     /**
@@ -217,14 +195,46 @@ public final class RuntimeTypeAdapterFactory implements TypeAdapterFactory {
     }
 
     /**
-     * Registers {@code type} identified by its {@link Class#getSimpleName simple name}. Labels are
-     * case sensitive.
+     * Registers {@code type} identified by {@code label}. Labels are case sensitive.
      *
-     * @throws IllegalArgumentException if either {@code type} or its simple name have already been
+     * @throws IllegalArgumentException if either {@code type} or {@code label} have already been
      *     registered on this type adapter.
      */
     public RuntimeTypeAdapterFactory registerSubtype(Class type) {
-        return registerSubtype(type, type.getSimpleName());
+        registerSubtype(type, type.getSimpleName());
+        return this;
+    }
+
+    private void initMaps(
+            Gson gson,
+            Map> labelToDelegate,
+            Map, TypeAdapter> subtypeToDelegate) {
+        for (Map.Entry> entry : labelToSubtype.entrySet()) {
+            TypeAdapter delegate =
+                    gson.getDelegateAdapter(this, TypeToken.get(entry.getValue()));
+            labelToDelegate.put(entry.getKey(), delegate);
+            subtypeToDelegate.put(entry.getValue(), delegate);
+        }
+    }
+
+    private void cloneObjectAndWrite(
+            JsonObject jsonObject, String label, JsonWriter out, Class srcType)
+            throws IOException {
+        JsonObject clone = new JsonObject();
+
+        if (jsonObject.has(typeFieldName)) {
+            throw new JsonParseException(
+                    "cannot serialize "
+                            + srcType.getName()
+                            + " because it already defines a field named "
+                            + typeFieldName);
+        }
+        clone.add(typeFieldName, new JsonPrimitive(label));
+
+        for (Map.Entry e : jsonObject.entrySet()) {
+            clone.add(e.getKey(), e.getValue());
+        }
+        Streams.write(clone, out);
     }
 
     public  TypeAdapter create(Gson gson, TypeToken type) {
@@ -233,19 +243,16 @@ public final class RuntimeTypeAdapterFactory implements TypeAdapterFactory {
         }
 
         final Map> labelToDelegate =
-                new LinkedHashMap>();
+                new LinkedHashMap<>(labelToSubtype.size());
         final Map, TypeAdapter> subtypeToDelegate =
-                new LinkedHashMap, TypeAdapter>();
-        for (Map.Entry> entry : labelToSubtype.entrySet()) {
-            TypeAdapter delegate =
-                    gson.getDelegateAdapter(this, TypeToken.get(entry.getValue()));
-            labelToDelegate.put(entry.getKey(), delegate);
-            subtypeToDelegate.put(entry.getValue(), delegate);
-        }
+                new LinkedHashMap<>(labelToSubtype.size());
+
+        initMaps(gson, labelToDelegate, subtypeToDelegate);
+        final RuntimeTypeAdapterFactory that = this;
 
         return new TypeAdapter() {
             @Override
-            public R read(JsonReader in) throws IOException {
+            public R read(JsonReader in) {
                 JsonElement jsonElement = Streams.parse(in);
                 JsonElement labelJsonElement;
                 if (maintainType) {
@@ -294,21 +301,7 @@ public final class RuntimeTypeAdapterFactory implements TypeAdapterFactory {
                     return;
                 }
 
-                JsonObject clone = new JsonObject();
-
-                if (jsonObject.has(typeFieldName)) {
-                    throw new JsonParseException(
-                            "cannot serialize "
-                                    + srcType.getName()
-                                    + " because it already defines a field named "
-                                    + typeFieldName);
-                }
-                clone.add(typeFieldName, new JsonPrimitive(label));
-
-                for (Map.Entry e : jsonObject.entrySet()) {
-                    clone.add(e.getKey(), e.getValue());
-                }
-                Streams.write(clone, out);
+                that.cloneObjectAndWrite(jsonObject, label, out, srcType);
             }
         }.nullSafe();
     }
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/AuthenticationController.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/AuthenticationController.java
index f806f01..da84571 100644
--- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/AuthenticationController.java
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/AuthenticationController.java
@@ -5,17 +5,16 @@ import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.JWTRequest;
 import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.JWTResponse;
 import ch.usi.inf.sa4.sanmarinoes.smarthut.error.UnauthorizedException;
 import ch.usi.inf.sa4.sanmarinoes.smarthut.error.UserNotFoundException;
-import ch.usi.inf.sa4.sanmarinoes.smarthut.models.*;
+import ch.usi.inf.sa4.sanmarinoes.smarthut.models.User;
+import ch.usi.inf.sa4.sanmarinoes.smarthut.models.UserRepository;
+import ch.usi.inf.sa4.sanmarinoes.smarthut.service.JWTUserDetailsService;
 import java.security.Principal;
 import javax.validation.Valid;
-
-import ch.usi.inf.sa4.sanmarinoes.smarthut.service.JWTUserDetailsService;
 import org.springframework.security.authentication.AuthenticationManager;
 import org.springframework.security.authentication.BadCredentialsException;
 import org.springframework.security.authentication.DisabledException;
 import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
 import org.springframework.security.core.userdetails.UserDetails;
-import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
 import org.springframework.web.bind.annotation.*;
 
 @RestController
@@ -30,8 +29,6 @@ public class AuthenticationController {
 
     private final JWTUserDetailsService userDetailsService;
 
-    private BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
-
     public AuthenticationController(
             AuthenticationManager authenticationManager,
             UserRepository userRepository,
@@ -82,9 +79,9 @@ public class AuthenticationController {
             authenticationManager.authenticate(
                     new UsernamePasswordAuthenticationToken(username, password));
         } catch (DisabledException e) {
-            throw new UnauthorizedException(true);
+            throw new UnauthorizedException(true, e);
         } catch (BadCredentialsException e) {
-            throw new UnauthorizedException(false);
+            throw new UnauthorizedException(false, e);
         }
     }
 }
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/AutomationController.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/AutomationController.java
index cfad787..d05c955 100644
--- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/AutomationController.java
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/AutomationController.java
@@ -2,6 +2,9 @@ package ch.usi.inf.sa4.sanmarinoes.smarthut.controller;
 
 import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.AutomationFastUpdateRequest;
 import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.AutomationSaveRequest;
+import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.automation.ConditionDTO;
+import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.automation.ScenePriorityDTO;
+import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.automation.TriggerDTO;
 import ch.usi.inf.sa4.sanmarinoes.smarthut.error.NotFoundException;
 import ch.usi.inf.sa4.sanmarinoes.smarthut.models.*;
 import java.security.Principal;
@@ -10,15 +13,7 @@ import java.util.stream.Collectors;
 import javax.validation.Valid;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
-import org.springframework.web.bind.annotation.DeleteMapping;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.PathVariable;
-import org.springframework.web.bind.annotation.PostMapping;
-import org.springframework.web.bind.annotation.PutMapping;
-import org.springframework.web.bind.annotation.RequestBody;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RequestParam;
-import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.bind.annotation.*;
 
 @RestController
 @EnableAutoConfiguration
@@ -28,13 +23,13 @@ public class AutomationController {
     @Autowired private AutomationRepository automationRepository;
     @Autowired private ScenePriorityRepository sceneRepository;
     @Autowired private TriggerRepository> triggerRepository;
+    @Autowired private ConditionRepository> conditionRepository;
     @Autowired private UserRepository userService;
 
     @GetMapping
     public List getAll(
             @RequestParam(value = "hostId", required = false) Long hostId,
-            final Principal principal)
-            throws NotFoundException {
+            final Principal principal) {
         final Long userId = userService.findByUsername(principal.getName()).getId();
         return automationRepository.findAllByUserId(userId);
     }
@@ -87,26 +82,45 @@ public class AutomationController {
 
         triggerRepository.deleteAllByAutomationId(a.getId());
         sceneRepository.deleteAllByAutomationId(a.getId());
+        conditionRepository.deleteAllByAutomationId(a.getId());
 
         Iterable> tt =
                 triggerRepository.saveAll(
                         req.getTriggers()
                                 .stream()
-                                .map(AutomationFastUpdateRequest.TriggerDTO::toModel)
+                                .map(TriggerDTO::toModel)
                                 .map(t -> t.setAutomationId(a.getId()))
                                 .collect(Collectors.toList()));
         Iterable ss =
                 sceneRepository.saveAll(
                         req.getScenes()
                                 .stream()
-                                .map(AutomationFastUpdateRequest.ScenePriorityDTO::toModel)
+                                .map(ScenePriorityDTO::toModel)
+                                .collect(Collectors.toList()));
+
+        Iterable> cc =
+                conditionRepository.saveAll(
+                        req.getConditions()
+                                .stream()
+                                .map(ConditionDTO::toModel)
                                 .map(t -> t.setAutomationId(a.getId()))
                                 .collect(Collectors.toList()));
 
+        for (final ScenePriority s : ss) {
+            s.setAutomationId(a.getId());
+
+            // this is here just to pass the quality gate,
+            // please do not replicate unless the quality gate sees
+            // it as a bug
+            s.setAutomation(a);
+        }
+
         a.getScenes().clear();
         a.getTriggers().clear();
+        a.getConditions().clear();
         ss.forEach(t -> a.getScenes().add(t));
         tt.forEach(t -> a.getTriggers().add(t));
+        cc.forEach(t -> a.getConditions().add(t));
 
         return a;
     }
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/BooleanConditionController.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/BooleanConditionController.java
new file mode 100644
index 0000000..c776a70
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/BooleanConditionController.java
@@ -0,0 +1,54 @@
+package ch.usi.inf.sa4.sanmarinoes.smarthut.controller;
+
+import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.BooleanConditionOrTriggerSaveRequest;
+import ch.usi.inf.sa4.sanmarinoes.smarthut.error.NotFoundException;
+import ch.usi.inf.sa4.sanmarinoes.smarthut.models.BooleanCondition;
+import ch.usi.inf.sa4.sanmarinoes.smarthut.models.BooleanConditionRepository;
+import java.util.List;
+import javax.validation.Valid;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
+import org.springframework.web.bind.annotation.*;
+
+@RestController
+@EnableAutoConfiguration
+@RequestMapping("/booleanCondition")
+public class BooleanConditionController {
+
+    @Autowired BooleanConditionRepository booleanConditionRepository;
+
+    @GetMapping("/{automationId}")
+    public List getAll(@PathVariable long automationId) {
+        return booleanConditionRepository.findAllByAutomationId(automationId);
+    }
+
+    private BooleanCondition save(BooleanCondition newRL, BooleanConditionOrTriggerSaveRequest s) {
+        newRL.setDeviceId(s.getDeviceId());
+        newRL.setAutomationId(s.getAutomationId());
+        newRL.setOn(s.isOn());
+
+        return booleanConditionRepository.save(newRL);
+    }
+
+    @PostMapping
+    public BooleanCondition create(
+            @Valid @RequestBody BooleanConditionOrTriggerSaveRequest booleanTriggerSaveRequest) {
+        return save(new BooleanCondition(), booleanTriggerSaveRequest);
+    }
+
+    @PutMapping
+    public BooleanCondition update(
+            @Valid @RequestBody BooleanConditionOrTriggerSaveRequest booleanTriggerSaveRequest)
+            throws NotFoundException {
+        return save(
+                booleanConditionRepository
+                        .findById(booleanTriggerSaveRequest.getId())
+                        .orElseThrow(NotFoundException::new),
+                booleanTriggerSaveRequest);
+    }
+
+    @DeleteMapping("/{id}")
+    public void delete(@PathVariable long id) {
+        booleanConditionRepository.deleteById(id);
+    }
+}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/BooleanTriggerController.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/BooleanTriggerController.java
index 9331b56..3147fb8 100644
--- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/BooleanTriggerController.java
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/BooleanTriggerController.java
@@ -1,6 +1,6 @@
 package ch.usi.inf.sa4.sanmarinoes.smarthut.controller;
 
-import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.BooleanTriggerSaveRequest;
+import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.BooleanConditionOrTriggerSaveRequest;
 import ch.usi.inf.sa4.sanmarinoes.smarthut.error.NotFoundException;
 import ch.usi.inf.sa4.sanmarinoes.smarthut.models.BooleanTrigger;
 import ch.usi.inf.sa4.sanmarinoes.smarthut.models.BooleanTriggerRepository;
@@ -8,14 +8,7 @@ import java.util.List;
 import javax.validation.Valid;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
-import org.springframework.web.bind.annotation.DeleteMapping;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.PathVariable;
-import org.springframework.web.bind.annotation.PostMapping;
-import org.springframework.web.bind.annotation.PutMapping;
-import org.springframework.web.bind.annotation.RequestBody;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.bind.annotation.*;
 
 @RestController
 @EnableAutoConfiguration
@@ -25,11 +18,11 @@ public class BooleanTriggerController {
     @Autowired BooleanTriggerRepository booleanTriggerRepository;
 
     @GetMapping("/{automationId}")
-    public List> getAll(@PathVariable long automationId) {
+    public List getAll(@PathVariable long automationId) {
         return booleanTriggerRepository.findAllByAutomationId(automationId);
     }
 
-    private BooleanTrigger save(BooleanTrigger newRL, BooleanTriggerSaveRequest s) {
+    private BooleanTrigger save(BooleanTrigger newRL, BooleanConditionOrTriggerSaveRequest s) {
         newRL.setDeviceId(s.getDeviceId());
         newRL.setAutomationId(s.getAutomationId());
         newRL.setOn(s.isOn());
@@ -38,14 +31,14 @@ public class BooleanTriggerController {
     }
 
     @PostMapping
-    public BooleanTrigger create(
-            @Valid @RequestBody BooleanTriggerSaveRequest booleanTriggerSaveRequest) {
-        return save(new BooleanTrigger<>(), booleanTriggerSaveRequest);
+    public BooleanTrigger create(
+            @Valid @RequestBody BooleanConditionOrTriggerSaveRequest booleanTriggerSaveRequest) {
+        return save(new BooleanTrigger(), booleanTriggerSaveRequest);
     }
 
     @PutMapping
-    public BooleanTrigger update(
-            @Valid @RequestBody BooleanTriggerSaveRequest booleanTriggerSaveRequest)
+    public BooleanTrigger update(
+            @Valid @RequestBody BooleanConditionOrTriggerSaveRequest booleanTriggerSaveRequest)
             throws NotFoundException {
         return save(
                 booleanTriggerRepository
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/ButtonDimmerController.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/ButtonDimmerController.java
index 15f4825..f7fcf12 100644
--- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/ButtonDimmerController.java
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/ButtonDimmerController.java
@@ -1,9 +1,12 @@
 package ch.usi.inf.sa4.sanmarinoes.smarthut.controller;
 
 import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.ButtonDimmerDimRequest;
-import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.GenericDeviceSaveReguest;
+import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.GenericDeviceSaveRequest;
 import ch.usi.inf.sa4.sanmarinoes.smarthut.error.NotFoundException;
-import ch.usi.inf.sa4.sanmarinoes.smarthut.models.*;
+import ch.usi.inf.sa4.sanmarinoes.smarthut.models.ButtonDimmer;
+import ch.usi.inf.sa4.sanmarinoes.smarthut.models.ButtonDimmerRepository;
+import ch.usi.inf.sa4.sanmarinoes.smarthut.models.Dimmable;
+import ch.usi.inf.sa4.sanmarinoes.smarthut.models.DimmableRepository;
 import ch.usi.inf.sa4.sanmarinoes.smarthut.service.DeviceService;
 import java.security.Principal;
 import java.util.Set;
@@ -18,15 +21,15 @@ import org.springframework.web.bind.annotation.*;
 public class ButtonDimmerController
         extends InputDeviceConnectionController {
 
-    private DeviceService deviceService;
-    private ButtonDimmerRepository buttonDimmerRepository;
+    private final DeviceService deviceService;
+    private final ButtonDimmerRepository buttonDimmerRepository;
 
     @Autowired
     protected ButtonDimmerController(
             ButtonDimmerRepository inputRepository,
             DimmableRepository outputRepository,
             DeviceService deviceService) {
-        super(inputRepository, outputRepository, DimmableLight.BUTTON_DIMMER_DIMMABLE_CONNECTOR);
+        super(inputRepository, outputRepository, deviceService);
         this.deviceService = deviceService;
         this.buttonDimmerRepository = inputRepository;
     }
@@ -38,7 +41,7 @@ public class ButtonDimmerController
 
     @PostMapping
     public ButtonDimmer create(
-            @Valid @RequestBody final GenericDeviceSaveReguest bd, final Principal principal)
+            @Valid @RequestBody final GenericDeviceSaveRequest bd, final Principal principal)
             throws NotFoundException {
         deviceService.throwIfRoomNotOwned(bd.getRoomId(), principal.getName());
 
@@ -58,23 +61,21 @@ public class ButtonDimmerController
                         .findByIdAndUsername(bd.getId(), principal.getName())
                         .orElseThrow(NotFoundException::new);
 
-        switch (bd.getDimType()) {
-            case UP:
-                buttonDimmer.increaseIntensity();
-                break;
-            case DOWN:
-                buttonDimmer.decreaseIntensity();
-                break;
+        if (bd.getDimType() == ButtonDimmerDimRequest.DimType.UP) {
+
+            buttonDimmer.increaseIntensity();
+        } else {
+            buttonDimmer.decreaseIntensity();
         }
 
-        deviceService.saveAllAsOwner(buttonDimmer.getOutputs(), principal.getName(), false);
+        deviceService.saveAllAsOwner(buttonDimmer.getOutputs(), principal.getName());
 
-        return buttonDimmer.getOutputs();
+        return buttonDimmer.getDimmables();
     }
 
     @DeleteMapping("/{id}")
     public void delete(@PathVariable("id") long id, final Principal principal)
             throws NotFoundException {
-        deviceService.delete(id, principal.getName());
+        deviceService.deleteByIdAsOwner(id, principal.getName());
     }
 }
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/CurtainsController.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/CurtainsController.java
index 6644135..5e01f9a 100644
--- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/CurtainsController.java
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/CurtainsController.java
@@ -18,7 +18,7 @@ public class CurtainsController {
     @Autowired private DeviceService deviceService;
     @Autowired private CurtainsRepository curtainsService;
     @Autowired private SceneRepository sceneRepository;
-    @Autowired private StateRepository> stateRepository;
+    @Autowired private StateRepository stateRepository;
 
     private Curtains save(Curtains newRL, DimmableSaveRequest s, final Principal principal) {
         newRL.setName(s.getName());
@@ -49,11 +49,11 @@ public class CurtainsController {
     @DeleteMapping("/{id}")
     public void delete(@PathVariable("id") long id, final Principal principal)
             throws NotFoundException {
-        deviceService.delete(id, principal.getName());
+        deviceService.deleteByIdAsOwner(id, principal.getName());
     }
 
     @PostMapping("/{id}/state")
-    public State sceneBinding(
+    public State sceneBinding(
             @PathVariable("id") long deviceId,
             @RequestParam long sceneId,
             final Principal principal)
@@ -63,9 +63,9 @@ public class CurtainsController {
                 curtainsService
                         .findByIdAndUsername(deviceId, principal.getName())
                         .orElseThrow(NotFoundException::new);
-        State s = c.cloneState();
-        sceneRepository.findById(sceneId).orElseThrow(NotFoundException::new);
-        s.setSceneId(sceneId);
+        State s = c.cloneState();
+        final Scene sc = sceneRepository.findById(sceneId).orElseThrow(NotFoundException::new);
+        s.setSceneId(sc.getId());
         if (stateRepository.countByDeviceIdAndSceneId(deviceId, sceneId) > 0)
             throw new DuplicateStateException();
         return stateRepository.save(s);
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/DeviceController.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/DeviceController.java
index b53b0af..1221043 100644
--- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/DeviceController.java
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/DeviceController.java
@@ -5,6 +5,7 @@ import ch.usi.inf.sa4.sanmarinoes.smarthut.error.BadDataException;
 import ch.usi.inf.sa4.sanmarinoes.smarthut.error.NotFoundException;
 import ch.usi.inf.sa4.sanmarinoes.smarthut.models.Device;
 import ch.usi.inf.sa4.sanmarinoes.smarthut.models.DeviceRepository;
+import ch.usi.inf.sa4.sanmarinoes.smarthut.models.Room;
 import ch.usi.inf.sa4.sanmarinoes.smarthut.models.RoomRepository;
 import ch.usi.inf.sa4.sanmarinoes.smarthut.service.DeviceService;
 import java.security.Principal;
@@ -40,11 +41,12 @@ public class DeviceController {
                         .orElseThrow(NotFoundException::new);
 
         // check if roomId is valid
-        roomRepository
-                .findByIdAndUsername(deviceSaveRequest.getRoomId(), principal.getName())
-                .orElseThrow(() -> new BadDataException("roomId is not a valid room id"));
+        final Room r =
+                roomRepository
+                        .findByIdAndUsername(deviceSaveRequest.getRoomId(), principal.getName())
+                        .orElseThrow(() -> new BadDataException("roomId is not a valid room id"));
 
-        d.setRoomId(deviceSaveRequest.getRoomId());
+        d.setRoomId(r.getId());
         d.setName(deviceSaveRequest.getName());
 
         deviceService.saveAsOwner(d, principal.getName());
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/DimmableLightController.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/DimmableLightController.java
index 60a4726..e251d7c 100644
--- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/DimmableLightController.java
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/DimmableLightController.java
@@ -16,17 +16,17 @@ import org.springframework.web.bind.annotation.*;
 @RequestMapping("/dimmableLight")
 public class DimmableLightController extends GuestEnabledController {
 
-    private DimmableLightRepository dimmableLightRepository;
-    private SceneRepository sceneRepository;
-    private StateRepository> stateRepository;
-    private DeviceService deviceService;
+    private final DimmableLightRepository dimmableLightRepository;
+    private final SceneRepository sceneRepository;
+    private final StateRepository stateRepository;
+    private final DeviceService deviceService;
 
     @Autowired
     public DimmableLightController(
             UserRepository userRepository,
             DimmableLightRepository dimmableLightRepository,
             SceneRepository sceneRepository,
-            StateRepository> stateRepository,
+            StateRepository stateRepository,
             DeviceService deviceService) {
         super(userRepository, dimmableLightRepository);
         this.dimmableLightRepository = dimmableLightRepository;
@@ -49,10 +49,10 @@ public class DimmableLightController extends GuestEnabledController sceneBinding(
+    public State sceneBinding(
             @PathVariable("id") long deviceId,
             @RequestParam long sceneId,
             final Principal principal)
@@ -95,9 +97,9 @@ public class DimmableLightController extends GuestEnabledController s = d.cloneState();
-        sceneRepository.findById(sceneId).orElseThrow(NotFoundException::new);
-        s.setSceneId(sceneId);
+        State s = d.cloneState();
+        final Scene sc = sceneRepository.findById(sceneId).orElseThrow(NotFoundException::new);
+        s.setSceneId(sc.getId());
         if (stateRepository.countByDeviceIdAndSceneId(deviceId, sceneId) > 0)
             throw new DuplicateStateException();
         return stateRepository.save(s);
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/DimmableStateController.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/DimmableStateController.java
index d866d78..83ffb53 100644
--- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/DimmableStateController.java
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/DimmableStateController.java
@@ -17,9 +17,9 @@ public class DimmableStateController {
     @Autowired private DimmableStateRepository dimmableStateRepository;
 
     @PutMapping
-    public DimmableState update(@Valid @RequestBody DimmableStateSaveRequest ss)
+    public DimmableState update(@Valid @RequestBody DimmableStateSaveRequest ss)
             throws NotFoundException {
-        final DimmableState initial =
+        final DimmableState initial =
                 dimmableStateRepository.findById(ss.getId()).orElseThrow(NotFoundException::new);
         initial.setIntensity(ss.getIntensity());
         dimmableStateRepository.save(initial);
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/GuestController.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/GuestController.java
index b0994a1..8212426 100644
--- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/GuestController.java
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/GuestController.java
@@ -3,11 +3,15 @@ package ch.usi.inf.sa4.sanmarinoes.smarthut.controller;
 import static ch.usi.inf.sa4.sanmarinoes.smarthut.utils.Utils.toList;
 
 import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.GuestPermissionsRequest;
-import ch.usi.inf.sa4.sanmarinoes.smarthut.error.NotFoundException;
+import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.GuestsUpdateRequest;
+import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.UserResponse;
+import ch.usi.inf.sa4.sanmarinoes.smarthut.models.EagerUserRepository;
 import ch.usi.inf.sa4.sanmarinoes.smarthut.models.User;
-import ch.usi.inf.sa4.sanmarinoes.smarthut.models.UserRepository;
 import java.security.Principal;
 import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.StreamSupport;
 import javax.validation.Valid;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
@@ -18,23 +22,47 @@ import org.springframework.web.bind.annotation.*;
 @RequestMapping("/user")
 public class GuestController {
 
-    @Autowired private UserRepository userRepository;
+    @Autowired private EagerUserRepository userRepository;
 
     @GetMapping
-    public List findAll() {
-        return toList(userRepository.findAll());
+    public List findAll() {
+        return StreamSupport.stream(userRepository.findAll().spliterator(), false)
+                .map(UserResponse::fromUser)
+                .collect(Collectors.toList());
     }
 
-    @PostMapping("/guest")
-    public User addUserAsGuest(@RequestParam("userId") long id, final Principal principal)
-            throws NotFoundException {
-        User guest = userRepository.findById(id).orElseThrow(NotFoundException::new);
+    @GetMapping("/hosts")
+    public List findHosts(final Principal principal) {
+        final User u = userRepository.findByUsername(principal.getName());
+        return u.getHosts().stream().map(UserResponse::fromUser).collect(Collectors.toList());
+    }
+
+    @GetMapping("/guests")
+    public List findGuests(final Principal principal) {
+        final User u = userRepository.findByUsername(principal.getName());
+        return u.getGuests().stream().map(UserResponse::fromUser).collect(Collectors.toList());
+    }
+
+    @PutMapping("/guests")
+    public List setGuests(
+            @RequestBody @Valid GuestsUpdateRequest g, final Principal principal) {
+        Iterable guests = userRepository.findAllById(g.getIds());
         User host = userRepository.findByUsername(principal.getName());
 
-        host.addGuest(guest);
-        guest.addHost(host);
-        userRepository.save(guest);
-        return userRepository.save(host);
+        for (final User oldGuest : host.getGuests()) {
+            oldGuest.getHosts().remove(host);
+        }
+
+        final Set oldGuests = Set.copyOf(host.getGuests());
+
+        for (final User guest : guests) {
+            host.addGuest(guest);
+            guest.addHost(host);
+        }
+
+        userRepository.saveAll(oldGuests);
+        userRepository.save(host);
+        return toList(userRepository.saveAll(guests));
     }
 
     @PutMapping("/permissions")
@@ -44,16 +72,4 @@ public class GuestController {
         currentUser.setCameraEnabled(g.isCameraEnabled());
         return userRepository.save(currentUser);
     }
-
-    @DeleteMapping("/guest")
-    public void removeUserAsGuest(@RequestParam("userId") long id, final Principal principal)
-            throws NotFoundException {
-        User guest = userRepository.findById(id).orElseThrow(NotFoundException::new);
-        User host = userRepository.findByUsername(principal.getName());
-
-        host.removeGuest(guest);
-        guest.getHosts().remove(host);
-        userRepository.save(host);
-        userRepository.save(guest);
-    }
 }
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/GuestEnabledController.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/GuestEnabledController.java
index 1aa2e7c..9a06af0 100644
--- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/GuestEnabledController.java
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/GuestEnabledController.java
@@ -3,13 +3,15 @@ package ch.usi.inf.sa4.sanmarinoes.smarthut.controller;
 import static ch.usi.inf.sa4.sanmarinoes.smarthut.utils.Utils.returnIfGuest;
 
 import ch.usi.inf.sa4.sanmarinoes.smarthut.error.NotFoundException;
-import ch.usi.inf.sa4.sanmarinoes.smarthut.models.*;
+import ch.usi.inf.sa4.sanmarinoes.smarthut.models.Device;
+import ch.usi.inf.sa4.sanmarinoes.smarthut.models.DeviceRepository;
+import ch.usi.inf.sa4.sanmarinoes.smarthut.models.UserRepository;
 import java.security.Principal;
 
 public abstract class GuestEnabledController {
 
-    private UserRepository userRepository;
-    private DeviceRepository deviceRepository;
+    private final UserRepository userRepository;
+    private final DeviceRepository deviceRepository;
 
     public GuestEnabledController(
             final UserRepository userRepository, final DeviceRepository deviceRepository) {
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/InputDeviceConnectionController.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/InputDeviceConnectionController.java
index 4d1a371..5fb5bfb 100644
--- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/InputDeviceConnectionController.java
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/InputDeviceConnectionController.java
@@ -3,13 +3,15 @@ package ch.usi.inf.sa4.sanmarinoes.smarthut.controller;
 import static ch.usi.inf.sa4.sanmarinoes.smarthut.utils.Utils.toList;
 
 import ch.usi.inf.sa4.sanmarinoes.smarthut.error.NotFoundException;
-import ch.usi.inf.sa4.sanmarinoes.smarthut.models.*;
+import ch.usi.inf.sa4.sanmarinoes.smarthut.models.Connectable;
+import ch.usi.inf.sa4.sanmarinoes.smarthut.models.DeviceRepository;
+import ch.usi.inf.sa4.sanmarinoes.smarthut.models.InputDevice;
+import ch.usi.inf.sa4.sanmarinoes.smarthut.models.OutputDevice;
 import ch.usi.inf.sa4.sanmarinoes.smarthut.service.DeviceService;
 import java.security.Principal;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Set;
-import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.DeleteMapping;
 import org.springframework.web.bind.annotation.PathVariable;
 import org.springframework.web.bind.annotation.PostMapping;
@@ -23,9 +25,9 @@ import org.springframework.web.bind.annotation.RequestBody;
  * @param  the output device attached to I
  */
 public abstract class InputDeviceConnectionController<
-        I extends InputDevice, O extends OutputDevice> {
+        I extends InputDevice & Connectable, O extends OutputDevice> {
 
-    private class Connection {
+    protected class Connection {
         private final I input;
         private final List outputs;
 
@@ -33,30 +35,43 @@ public abstract class InputDeviceConnectionController<
             this.input = input;
             this.outputs = outputs;
         }
+
+        public I getInput() {
+            return input;
+        }
+
+        public List getOutputs() {
+            return outputs;
+        }
     }
 
-    @Autowired private DeviceService deviceService;
+    protected DeviceRepository getInputRepository() {
+        return inputRepository;
+    }
 
-    private DeviceRepository inputRepository;
+    protected DeviceRepository getOutputReposiory() {
+        return outputReposiory;
+    }
 
-    private DeviceRepository outputReposiory;
+    private final DeviceService deviceService;
 
-    private Connector connector;
+    private final DeviceRepository inputRepository;
+
+    private final DeviceRepository outputReposiory;
 
     /**
      * Contstructs the controller by requiring essential object for the controller implementation
      *
      * @param inputRepository the input device repository
      * @param outputRepository the output device repository
-     * @param connector a appropriate Connector instance for the I and O tyoes.
      */
     protected InputDeviceConnectionController(
             DeviceRepository inputRepository,
             DeviceRepository outputRepository,
-            Connector connector) {
+            DeviceService deviceService) {
         this.inputRepository = inputRepository;
         this.outputReposiory = outputRepository;
-        this.connector = connector;
+        this.deviceService = deviceService;
     }
 
     private Connection checkConnectionIDs(Long inputId, List outputs, String username)
@@ -65,7 +80,7 @@ public abstract class InputDeviceConnectionController<
                 inputRepository
                         .findByIdAndUsername(inputId, username)
                         .orElseThrow(() -> new NotFoundException("input device"));
-        final List outputDevices = new ArrayList<>();
+        final List outputDevices = new ArrayList<>(outputs.size());
         for (final Long outputId : outputs) {
             outputDevices.add(
                     outputReposiory
@@ -83,16 +98,16 @@ public abstract class InputDeviceConnectionController<
      * @return the list of output devices attached to the input device of id inputId
      * @throws NotFoundException if inputId or outputId are not valid
      */
-    protected Set addOutput(
-            Long inputId, List outputs, String username) throws NotFoundException {
+    protected Set addOutput(Long inputId, List outputs, String username)
+            throws NotFoundException {
         final Connection pair = checkConnectionIDs(inputId, outputs, username);
 
-        for (final O o : pair.outputs) {
-            connector.connect(pair.input, o, true);
+        for (final O o : pair.getOutputs()) {
+            pair.getInput().connect(o, true);
         }
 
-        deviceService.saveAllAsOwner(pair.outputs, username, false);
-        return pair.input.getOutputs();
+        deviceService.saveAllAsOwner(pair.getOutputs(), username);
+        return pair.getInput().getOutputs();
     }
 
     /**
@@ -103,16 +118,16 @@ public abstract class InputDeviceConnectionController<
      * @return the list of output devices attached to the input device of id inputId
      * @throws NotFoundException if inputId or outputId are not valid
      */
-    protected Set removeOutput(
-            Long inputId, List outputs, String username) throws NotFoundException {
+    protected Set removeOutput(Long inputId, List outputs, String username)
+            throws NotFoundException {
         final Connection pair = checkConnectionIDs(inputId, outputs, username);
 
-        for (final O o : pair.outputs) {
-            connector.connect(pair.input, o, false);
+        for (final O o : pair.getOutputs()) {
+            pair.getInput().connect(o, false);
         }
 
-        deviceService.saveAllAsOwner(pair.outputs, username, false);
-        return pair.input.getOutputs();
+        deviceService.saveAllAsOwner(pair.getOutputs(), username);
+        return pair.getInput().getOutputs();
     }
 
     @PostMapping("/{id}/lights")
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/KnobDimmerController.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/KnobDimmerController.java
index a8333f5..492639e 100644
--- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/KnobDimmerController.java
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/KnobDimmerController.java
@@ -1,9 +1,12 @@
 package ch.usi.inf.sa4.sanmarinoes.smarthut.controller;
 
-import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.GenericDeviceSaveReguest;
+import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.GenericDeviceSaveRequest;
 import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.KnobDimmerDimRequest;
 import ch.usi.inf.sa4.sanmarinoes.smarthut.error.NotFoundException;
-import ch.usi.inf.sa4.sanmarinoes.smarthut.models.*;
+import ch.usi.inf.sa4.sanmarinoes.smarthut.models.Dimmable;
+import ch.usi.inf.sa4.sanmarinoes.smarthut.models.DimmableRepository;
+import ch.usi.inf.sa4.sanmarinoes.smarthut.models.KnobDimmer;
+import ch.usi.inf.sa4.sanmarinoes.smarthut.models.KnobDimmerRepository;
 import ch.usi.inf.sa4.sanmarinoes.smarthut.service.DeviceService;
 import java.security.Principal;
 import java.util.Set;
@@ -17,14 +20,17 @@ import org.springframework.web.bind.annotation.*;
 @RequestMapping("/knobDimmer")
 public class KnobDimmerController extends InputDeviceConnectionController {
 
-    @Autowired private DeviceService deviceService;
-    @Autowired private KnobDimmerRepository knobDimmerRepository;
+    private final DeviceService deviceService;
+    private final KnobDimmerRepository knobDimmerRepository;
 
     @Autowired
     protected KnobDimmerController(
-            KnobDimmerRepository inputRepository, DimmableRepository outputRepository) {
-        super(inputRepository, outputRepository, Dimmable.KNOB_DIMMER_DIMMABLE_CONNECTOR);
+            KnobDimmerRepository inputRepository,
+            DimmableRepository outputRepository,
+            DeviceService deviceService) {
+        super(inputRepository, outputRepository, deviceService);
         this.knobDimmerRepository = inputRepository;
+        this.deviceService = deviceService;
     }
 
     @GetMapping("/{id}")
@@ -34,7 +40,7 @@ public class KnobDimmerController extends InputDeviceConnectionController getAll(@PathVariable long automationId) {
+        return rangeConditionRepository.findAllByAutomationId(automationId);
+    }
+
+    private RangeCondition save(RangeCondition newRL, RangeConditionOrTriggerSaveRequest s) {
+        newRL.setDeviceId(s.getDeviceId());
+        newRL.setAutomationId(s.getAutomationId());
+        newRL.setOperator(s.getOperator());
+        newRL.setRange(s.getRange());
+
+        return rangeConditionRepository.save(newRL);
+    }
+
+    @PostMapping
+    public RangeCondition create(
+            @Valid @RequestBody RangeConditionOrTriggerSaveRequest booleanTriggerSaveRequest) {
+        return save(new RangeCondition(), booleanTriggerSaveRequest);
+    }
+
+    @PutMapping
+    public RangeCondition update(
+            @Valid @RequestBody RangeConditionOrTriggerSaveRequest booleanTriggerSaveRequest)
+            throws NotFoundException {
+        return save(
+                rangeConditionRepository
+                        .findById(booleanTriggerSaveRequest.getId())
+                        .orElseThrow(NotFoundException::new),
+                booleanTriggerSaveRequest);
+    }
+
+    @DeleteMapping("/{id}")
+    public void delete(@PathVariable long id) {
+        rangeConditionRepository.deleteById(id);
+    }
+}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/RangeTriggerController.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/RangeTriggerController.java
index 655ce4b..e04aada 100644
--- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/RangeTriggerController.java
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/RangeTriggerController.java
@@ -1,6 +1,6 @@
 package ch.usi.inf.sa4.sanmarinoes.smarthut.controller;
 
-import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.RangeTriggerSaveRequest;
+import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.RangeConditionOrTriggerSaveRequest;
 import ch.usi.inf.sa4.sanmarinoes.smarthut.error.NotFoundException;
 import ch.usi.inf.sa4.sanmarinoes.smarthut.models.RangeTrigger;
 import ch.usi.inf.sa4.sanmarinoes.smarthut.models.RangeTriggerRepository;
@@ -8,14 +8,7 @@ import java.util.List;
 import javax.validation.Valid;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
-import org.springframework.web.bind.annotation.DeleteMapping;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.PathVariable;
-import org.springframework.web.bind.annotation.PostMapping;
-import org.springframework.web.bind.annotation.PutMapping;
-import org.springframework.web.bind.annotation.RequestBody;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.bind.annotation.*;
 
 @RestController
 @EnableAutoConfiguration
@@ -25,11 +18,11 @@ public class RangeTriggerController {
     @Autowired RangeTriggerRepository rangeTriggerRepository;
 
     @GetMapping("/{automationId}")
-    public List> getAll(@PathVariable long automationId) {
+    public List getAll(@PathVariable long automationId) {
         return rangeTriggerRepository.findAllByAutomationId(automationId);
     }
 
-    private RangeTrigger save(RangeTrigger newRL, RangeTriggerSaveRequest s) {
+    private RangeTrigger save(RangeTrigger newRL, RangeConditionOrTriggerSaveRequest s) {
         newRL.setDeviceId(s.getDeviceId());
         newRL.setAutomationId(s.getAutomationId());
         newRL.setOperator(s.getOperator());
@@ -39,14 +32,14 @@ public class RangeTriggerController {
     }
 
     @PostMapping
-    public RangeTrigger create(
-            @Valid @RequestBody RangeTriggerSaveRequest booleanTriggerSaveRequest) {
-        return save(new RangeTrigger<>(), booleanTriggerSaveRequest);
+    public RangeTrigger create(
+            @Valid @RequestBody RangeConditionOrTriggerSaveRequest booleanTriggerSaveRequest) {
+        return save(new RangeTrigger(), booleanTriggerSaveRequest);
     }
 
     @PutMapping
-    public RangeTrigger update(
-            @Valid @RequestBody RangeTriggerSaveRequest booleanTriggerSaveRequest)
+    public RangeTrigger update(
+            @Valid @RequestBody RangeConditionOrTriggerSaveRequest booleanTriggerSaveRequest)
             throws NotFoundException {
         return save(
                 rangeTriggerRepository
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/RegularLightController.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/RegularLightController.java
index aef798d..7cd8225 100644
--- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/RegularLightController.java
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/RegularLightController.java
@@ -12,32 +12,24 @@ import java.util.List;
 import javax.validation.Valid;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
-import org.springframework.web.bind.annotation.DeleteMapping;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.PathVariable;
-import org.springframework.web.bind.annotation.PostMapping;
-import org.springframework.web.bind.annotation.PutMapping;
-import org.springframework.web.bind.annotation.RequestBody;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RequestParam;
-import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.bind.annotation.*;
 
 @RestController
 @EnableAutoConfiguration
 @RequestMapping("/regularLight")
 public class RegularLightController extends GuestEnabledController {
 
-    private RegularLightRepository regularLightRepository;
-    private SceneRepository sceneRepository;
-    private StateRepository> stateRepository;
-    private DeviceService deviceService;
+    private final RegularLightRepository regularLightRepository;
+    private final SceneRepository sceneRepository;
+    private final StateRepository stateRepository;
+    private final DeviceService deviceService;
 
     @Autowired
     public RegularLightController(
             UserRepository userRepository,
             RegularLightRepository regularLightRepository,
             SceneRepository sceneRepository,
-            StateRepository> stateRepository,
+            StateRepository stateRepository,
             DeviceService deviceService) {
         super(userRepository, regularLightRepository);
         this.regularLightRepository = regularLightRepository;
@@ -80,7 +72,9 @@ public class RegularLightController extends GuestEnabledController
 
     @PutMapping
     public RegularLight update(
-            @Valid @RequestBody SwitchableSaveRequest rl, final Principal principal, Long hostId)
+            @Valid @RequestBody SwitchableSaveRequest rl,
+            final Principal principal,
+            @RequestParam(value = "hostId", required = false) Long hostId)
             throws NotFoundException {
         return save(
                 fetchIfOwnerOrGuest(principal, rl.getId(), hostId),
@@ -92,13 +86,11 @@ public class RegularLightController extends GuestEnabledController
     @DeleteMapping("/{id}")
     public void delete(@PathVariable("id") long id, final Principal principal)
             throws NotFoundException {
-        deviceService.delete(id, principal.getName());
+        deviceService.deleteByIdAsOwner(id, principal.getName());
     }
 
-    // the full url should be: "/regularLight/{id}/state?sceneId={sceneId}
-    // however it is not necessary to specify the query in the mapping
     @PostMapping("/{id}/state")
-    public State sceneBinding(
+    public State sceneBinding(
             @PathVariable("id") long deviceId,
             @RequestParam long sceneId,
             final Principal principal)
@@ -107,9 +99,9 @@ public class RegularLightController extends GuestEnabledController
                 regularLightRepository
                         .findByIdAndUsername(deviceId, principal.getName())
                         .orElseThrow(NotFoundException::new);
-        State s = d.cloneState();
-        sceneRepository.findById(sceneId).orElseThrow(NotFoundException::new);
-        s.setSceneId(sceneId);
+        State s = d.cloneState();
+        final Scene sc = sceneRepository.findById(sceneId).orElseThrow(NotFoundException::new);
+        s.setSceneId(sc.getId());
         if (stateRepository.countByDeviceIdAndSceneId(deviceId, sceneId) > 0)
             throw new DuplicateStateException();
         return stateRepository.save(s);
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/RoomController.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/RoomController.java
index c4540c5..2bba6c2 100644
--- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/RoomController.java
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/RoomController.java
@@ -6,13 +6,12 @@ import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.RoomSaveRequest;
 import ch.usi.inf.sa4.sanmarinoes.smarthut.error.NotFoundException;
 import ch.usi.inf.sa4.sanmarinoes.smarthut.models.*;
 import ch.usi.inf.sa4.sanmarinoes.smarthut.service.DeviceService;
-import ch.usi.inf.sa4.sanmarinoes.smarthut.service.ThermostatService;
 import ch.usi.inf.sa4.sanmarinoes.smarthut.utils.Utils;
 import java.security.Principal;
-import java.util.*;
+import java.util.List;
 import javax.validation.Valid;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.autoconfigure.*;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
 import org.springframework.web.bind.annotation.*;
 
 @RestController
@@ -20,19 +19,33 @@ import org.springframework.web.bind.annotation.*;
 @RequestMapping("/room")
 public class RoomController {
 
-    @Autowired private RoomRepository roomRepository;
+    private final RoomRepository roomRepository;
 
-    @Autowired private UserRepository userRepository;
+    private final UserRepository userRepository;
 
-    @Autowired private DeviceService deviceService;
+    private final DeviceService deviceService;
 
-    @Autowired private SwitchRepository switchRepository;
+    private final SwitchRepository switchRepository;
 
-    @Autowired private ButtonDimmerRepository buttonDimmerRepository;
+    private final ButtonDimmerRepository buttonDimmerRepository;
 
-    @Autowired private KnobDimmerRepository knobDimmerRepository;
+    private final KnobDimmerRepository knobDimmerRepository;
 
-    @Autowired private ThermostatService thermostatService;
+    @Autowired
+    public RoomController(
+            RoomRepository roomRepository,
+            UserRepository userRepository,
+            DeviceService deviceService,
+            SwitchRepository switchRepository,
+            ButtonDimmerRepository buttonDimmerRepository,
+            KnobDimmerRepository knobDimmerRepository) {
+        this.roomRepository = roomRepository;
+        this.userRepository = userRepository;
+        this.deviceService = deviceService;
+        this.switchRepository = switchRepository;
+        this.buttonDimmerRepository = buttonDimmerRepository;
+        this.knobDimmerRepository = knobDimmerRepository;
+    }
 
     private  List fetchOwnerOrGuest(
             final List list, Long hostId, final Principal principal) throws NotFoundException {
@@ -75,7 +88,7 @@ public class RoomController {
         final String username = principal.getName();
         final Long userId = userRepository.findByUsername(username).getId();
         final String img = r.getImage();
-        final Room.Icon icon = r.getIcon();
+        final Icon icon = r.getIcon();
 
         final Room newRoom = new Room();
         newRoom.setUserId(userId);
@@ -95,7 +108,7 @@ public class RoomController {
                         .findByIdAndUsername(id, principal.getName())
                         .orElseThrow(NotFoundException::new);
         final String img = r.getImage();
-        final Room.Icon icon = r.getIcon();
+        final Icon icon = r.getIcon();
 
         if (r.getName() != null) {
             newRoom.setName(r.getName());
@@ -124,6 +137,11 @@ public class RoomController {
                 roomRepository
                         .findByIdAndUsername(id, principal.getName())
                         .orElseThrow(NotFoundException::new);
+        List devices = deviceService.findAll(r.getId(), null, principal.getName());
+        for (Device d : devices) {
+            deviceService.deleteByIdAsOwner(d.getId(), principal.getName());
+        }
+
         roomRepository.delete(r);
     }
 
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/SceneController.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/SceneController.java
index 0cddba2..4db036f 100644
--- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/SceneController.java
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/SceneController.java
@@ -6,20 +6,13 @@ import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.SceneSaveRequest;
 import ch.usi.inf.sa4.sanmarinoes.smarthut.error.NotFoundException;
 import ch.usi.inf.sa4.sanmarinoes.smarthut.models.*;
 import ch.usi.inf.sa4.sanmarinoes.smarthut.service.SceneService;
+import ch.usi.inf.sa4.sanmarinoes.smarthut.utils.Utils;
 import java.security.Principal;
 import java.util.List;
 import javax.validation.Valid;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
-import org.springframework.web.bind.annotation.DeleteMapping;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.PathVariable;
-import org.springframework.web.bind.annotation.PostMapping;
-import org.springframework.web.bind.annotation.PutMapping;
-import org.springframework.web.bind.annotation.RequestBody;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.ResponseBody;
-import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.bind.annotation.*;
 
 @RestController
 @EnableAutoConfiguration
@@ -27,21 +20,20 @@ import org.springframework.web.bind.annotation.RestController;
 public class SceneController {
 
     @Autowired private SceneRepository sceneRepository;
+    @Autowired private UserRepository userRepository;
     @Autowired private SceneService sceneService;
-    @Autowired private UserRepository userService;
-    @Autowired private StateRepository> stateService;
+    @Autowired private StateRepository stateRepository;
 
     @GetMapping
-    public List findAll(Principal principal) {
-        return toList(sceneRepository.findByUsername(principal.getName()));
-    }
-
-    @GetMapping("/{id}")
-    public @ResponseBody Scene findById(@PathVariable("id") long id, Principal principal)
+    public List findAll(
+            Principal principal, @RequestParam(value = "hostId", required = false) Long hostId)
             throws NotFoundException {
-        return sceneRepository
-                .findByIdAndUsername(id, principal.getName())
-                .orElseThrow(NotFoundException::new);
+        if (hostId == null) {
+            return toList(sceneRepository.findByUsername(principal.getName()));
+        } else {
+            Utils.returnIfGuest(userRepository, null, hostId, principal);
+            return sceneRepository.findByHostId(hostId);
+        }
     }
 
     @PostMapping
@@ -49,26 +41,58 @@ public class SceneController {
             @Valid @RequestBody SceneSaveRequest s, final Principal principal) {
 
         final String username = principal.getName();
-        final Long userId = userService.findByUsername(username).getId();
+        final Long userId = userRepository.findByUsername(username).getId();
 
         final Scene newScene = new Scene();
 
         newScene.setUserId(userId);
         newScene.setName(s.getName());
         newScene.setGuestAccessEnabled(s.isGuestAccessEnabled());
+        newScene.setIcon(s.getIcon());
 
         return sceneRepository.save(newScene);
     }
 
     @PostMapping("/{id}/apply")
-    public @ResponseBody List apply(@PathVariable("id") long id, final Principal principal)
+    public @ResponseBody List apply(
+            @PathVariable("id") long id,
+            final Principal principal,
+            @RequestParam(value = "hostId", required = false) Long hostId)
             throws NotFoundException {
-        final Scene newScene =
+        if (hostId == null) {
+            return sceneService.apply(
+                    sceneRepository
+                            .findByIdAndUsername(id, principal.getName())
+                            .orElseThrow(NotFoundException::new),
+                    principal.getName(),
+                    false);
+        } else {
+            Utils.returnIfGuest(userRepository, null, hostId, principal);
+            return sceneService.applyAsGuest(
+                    sceneRepository
+                            .findByIdAndUserIdAndGuestAccessEnabled(id, hostId, true)
+                            .orElseThrow(NotFoundException::new),
+                    principal.getName(),
+                    hostId);
+        }
+    }
+
+    @PostMapping("/{id}/copyFrom/{copyId}")
+    public @ResponseBody List copy(
+            @PathVariable("id") long id,
+            @PathVariable("copyId") long copyId,
+            final Principal principal)
+            throws NotFoundException {
+        final Scene scene =
                 sceneRepository
                         .findByIdAndUsername(id, principal.getName())
                         .orElseThrow(NotFoundException::new);
+        final Scene copyFrom =
+                sceneRepository
+                        .findByIdAndUsername(copyId, principal.getName())
+                        .orElseThrow(NotFoundException::new);
 
-        return sceneService.apply(newScene);
+        return sceneService.copyStates(scene, copyFrom);
     }
 
     @PutMapping("/{id}")
@@ -84,6 +108,8 @@ public class SceneController {
             newScene.setName(s.getName());
         }
 
+        newScene.setIcon(s.getIcon());
+
         newScene.setGuestAccessEnabled(s.isGuestAccessEnabled());
 
         return sceneRepository.save(newScene);
@@ -91,7 +117,7 @@ public class SceneController {
 
     @DeleteMapping("/{id}")
     public void deleteById(@PathVariable("id") long id) {
-        stateService.deleteAllBySceneId(id);
+        stateRepository.deleteAllBySceneId(id);
         sceneRepository.deleteById(id);
     }
 
@@ -100,8 +126,8 @@ public class SceneController {
      * id).
      */
     @GetMapping(path = "/{sceneId}/states")
-    public List> getDevices(@PathVariable("sceneId") long sceneId) {
-        Iterable> states = stateService.findBySceneId(sceneId);
+    public List getStates(@PathVariable("sceneId") long sceneId) {
+        Iterable states = stateRepository.findBySceneId(sceneId);
         return toList(states);
     }
 }
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/ScenePriorityController.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/ScenePriorityController.java
index 6e95f75..230e396 100644
--- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/ScenePriorityController.java
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/ScenePriorityController.java
@@ -8,14 +8,7 @@ import java.util.List;
 import javax.validation.Valid;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
-import org.springframework.web.bind.annotation.DeleteMapping;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.PathVariable;
-import org.springframework.web.bind.annotation.PostMapping;
-import org.springframework.web.bind.annotation.PutMapping;
-import org.springframework.web.bind.annotation.RequestBody;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.bind.annotation.*;
 
 @RestController
 @EnableAutoConfiguration
@@ -24,10 +17,8 @@ public class ScenePriorityController {
 
     @Autowired ScenePriorityRepository scenePriorityRepository;
 
-
     @GetMapping("/{automationId}")
-    public List getByAutomationId(@PathVariable long automationId)
-            throws NotFoundException {
+    public List getByAutomationId(@PathVariable long automationId) {
         return scenePriorityRepository.findAllByAutomationId(automationId);
     }
 
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/SecurityCameraController.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/SecurityCameraController.java
index c063ebf..fe29bf7 100644
--- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/SecurityCameraController.java
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/SecurityCameraController.java
@@ -1,5 +1,6 @@
 package ch.usi.inf.sa4.sanmarinoes.smarthut.controller;
 
+import ch.usi.inf.sa4.sanmarinoes.smarthut.config.CameraConfigurationService;
 import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.SwitchableSaveRequest;
 import ch.usi.inf.sa4.sanmarinoes.smarthut.error.DuplicateStateException;
 import ch.usi.inf.sa4.sanmarinoes.smarthut.error.NotFoundException;
@@ -9,31 +10,38 @@ import java.security.Principal;
 import javax.validation.Valid;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
-import org.springframework.web.bind.annotation.DeleteMapping;
-import org.springframework.web.bind.annotation.PathVariable;
-import org.springframework.web.bind.annotation.PostMapping;
-import org.springframework.web.bind.annotation.PutMapping;
-import org.springframework.web.bind.annotation.RequestBody;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RequestParam;
-import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.bind.annotation.*;
 
 @RestController
 @EnableAutoConfiguration
 @RequestMapping("/securityCamera")
 public class SecurityCameraController {
+    private final DeviceService deviceService;
+    private final SecurityCameraRepository securityCameraService;
+    private final SceneRepository sceneRepository;
+    private final StateRepository stateRepository;
+    private final CameraConfigurationService cameraConfigurationService;
 
-    @Autowired private DeviceService deviceService;
-    @Autowired private SecurityCameraRepository securityCameraService;
-    @Autowired private SceneRepository sceneRepository;
-    @Autowired private StateRepository> stateRepository;
-    @Autowired private RoomRepository roomRepository;
+    @Autowired
+    public SecurityCameraController(
+            DeviceService deviceService,
+            SecurityCameraRepository securityCameraService,
+            SceneRepository sceneRepository,
+            StateRepository stateRepository,
+            CameraConfigurationService cameraConfigurationService) {
+        this.deviceService = deviceService;
+        this.securityCameraService = securityCameraService;
+        this.sceneRepository = sceneRepository;
+        this.stateRepository = stateRepository;
+        this.cameraConfigurationService = cameraConfigurationService;
+    }
 
     private SecurityCamera save(
             SecurityCamera newSC, SwitchableSaveRequest sc, final Principal principal) {
         newSC.setName(sc.getName());
         newSC.setRoomId(sc.getRoomId());
         newSC.setOn(sc.isOn());
+        newSC.setPath(cameraConfigurationService.getVideoUrl());
 
         return deviceService.saveAsOwner(newSC, principal.getName());
     }
@@ -61,11 +69,11 @@ public class SecurityCameraController {
     @DeleteMapping("/{id}")
     public void delete(@PathVariable("id") long id, final Principal principal)
             throws NotFoundException {
-        deviceService.delete(id, principal.getName());
+        deviceService.deleteByIdAsOwner(id, principal.getName());
     }
 
     @PostMapping("/{id}/state")
-    public State sceneBinding(
+    public State sceneBinding(
             @PathVariable("id") long deviceId,
             @RequestParam long sceneId,
             final Principal principal)
@@ -75,9 +83,9 @@ public class SecurityCameraController {
                 securityCameraService
                         .findByIdAndUsername(deviceId, principal.getName())
                         .orElseThrow(NotFoundException::new);
-        State s = d.cloneState();
-        sceneRepository.findById(sceneId).orElseThrow(NotFoundException::new);
-        s.setSceneId(sceneId);
+        State s = d.cloneState();
+        final Scene sc = sceneRepository.findById(sceneId).orElseThrow(NotFoundException::new);
+        s.setSceneId(sc.getId());
         if (stateRepository.countByDeviceIdAndSceneId(deviceId, sceneId) > 0)
             throw new DuplicateStateException();
         return stateRepository.save(s);
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/SensorController.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/SensorController.java
index 41b9af0..c58d68f 100644
--- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/SensorController.java
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/SensorController.java
@@ -2,16 +2,15 @@ package ch.usi.inf.sa4.sanmarinoes.smarthut.controller;
 
 import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.SensorSaveRequest;
 import ch.usi.inf.sa4.sanmarinoes.smarthut.error.NotFoundException;
-import ch.usi.inf.sa4.sanmarinoes.smarthut.models.*;
+import ch.usi.inf.sa4.sanmarinoes.smarthut.models.Sensor;
+import ch.usi.inf.sa4.sanmarinoes.smarthut.models.SensorRepository;
 import ch.usi.inf.sa4.sanmarinoes.smarthut.service.DeviceService;
 import ch.usi.inf.sa4.sanmarinoes.smarthut.service.SensorService;
-import ch.usi.inf.sa4.sanmarinoes.smarthut.socket.SensorSocketEndpoint;
 import java.math.BigDecimal;
 import java.security.Principal;
-import java.util.*;
 import javax.validation.Valid;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.autoconfigure.*;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
 import org.springframework.web.bind.annotation.*;
 
 @RestController
@@ -19,13 +18,21 @@ import org.springframework.web.bind.annotation.*;
 @RequestMapping("/sensor")
 public class SensorController {
 
-    @Autowired private DeviceService deviceService;
+    private final DeviceService deviceService;
 
-    @Autowired private SensorRepository sensorRepository;
+    private final SensorRepository sensorRepository;
 
-    @Autowired private SensorSocketEndpoint sensorSocketEndpoint;
+    private final SensorService sensorService;
 
-    @Autowired private SensorService sensorService;
+    @Autowired
+    public SensorController(
+            DeviceService deviceService,
+            SensorRepository sensorRepository,
+            SensorService sensorService) {
+        this.deviceService = deviceService;
+        this.sensorRepository = sensorRepository;
+        this.sensorService = sensorService;
+    }
 
     @PostMapping
     public Sensor create(@Valid @RequestBody SensorSaveRequest s, final Principal principal)
@@ -54,9 +61,24 @@ public class SensorController {
                 value);
     }
 
+    @PutMapping("/{id}/simulation")
+    public Sensor updateSimulation(
+            @PathVariable("id") Long sensorId,
+            @RequestBody BigDecimal error,
+            @RequestBody BigDecimal typical,
+            final Principal principal)
+            throws NotFoundException {
+        return sensorService.updateSimulationFromSensor(
+                sensorRepository
+                        .findByIdAndUsername(sensorId, principal.getName())
+                        .orElseThrow(NotFoundException::new),
+                error,
+                typical);
+    }
+
     @DeleteMapping("/{id}")
     public void deleteById(@PathVariable("id") long id, final Principal principal)
             throws NotFoundException {
-        deviceService.delete(id, principal.getName());
+        deviceService.deleteByIdAsOwner(id, principal.getName());
     }
 }
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/SmartPlugController.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/SmartPlugController.java
index 468cd5d..3090c6c 100644
--- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/SmartPlugController.java
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/SmartPlugController.java
@@ -8,7 +8,7 @@ import ch.usi.inf.sa4.sanmarinoes.smarthut.service.DeviceService;
 import java.security.Principal;
 import javax.validation.Valid;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.autoconfigure.*;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
 import org.springframework.web.bind.annotation.*;
 
 @RestController
@@ -19,11 +19,10 @@ public class SmartPlugController {
     @Autowired private DeviceService deviceService;
     @Autowired private SmartPlugRepository smartPlugRepository;
     @Autowired private SceneRepository sceneRepository;
-    @Autowired private StateRepository> stateRepository;
+    @Autowired private StateRepository stateRepository;
 
     private SmartPlug save(SmartPlug newSP, SwitchableSaveRequest sp, final Principal principal) {
         newSP.setOn(sp.isOn());
-        newSP.setId(sp.getId());
         newSP.setName(sp.getName());
         newSP.setRoomId(sp.getRoomId());
 
@@ -63,11 +62,11 @@ public class SmartPlugController {
     @DeleteMapping("/{id}")
     public void deleteById(@PathVariable("id") long id, final Principal principal)
             throws NotFoundException {
-        deviceService.delete(id, principal.getName());
+        deviceService.deleteByIdAsOwner(id, principal.getName());
     }
 
     @PostMapping("/{id}/state")
-    public State sceneBinding(
+    public State sceneBinding(
             @PathVariable("id") long deviceId,
             @RequestParam long sceneId,
             final Principal principal)
@@ -77,9 +76,9 @@ public class SmartPlugController {
                 smartPlugRepository
                         .findByIdAndUsername(deviceId, principal.getName())
                         .orElseThrow(NotFoundException::new);
-        State s = d.cloneState();
-        sceneRepository.findById(sceneId).orElseThrow(NotFoundException::new);
-        s.setSceneId(sceneId);
+        State s = d.cloneState();
+        final Scene sc = sceneRepository.findById(sceneId).orElseThrow(NotFoundException::new);
+        s.setSceneId(sc.getId());
         if (stateRepository.countByDeviceIdAndSceneId(deviceId, sceneId) > 0)
             throw new DuplicateStateException();
         return stateRepository.save(s);
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/SwitchController.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/SwitchController.java
index 50d63e0..92d8205 100644
--- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/SwitchController.java
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/SwitchController.java
@@ -1,15 +1,18 @@
 package ch.usi.inf.sa4.sanmarinoes.smarthut.controller;
 
-import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.GenericDeviceSaveReguest;
+import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.GenericDeviceSaveRequest;
 import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.SwitchOperationRequest;
 import ch.usi.inf.sa4.sanmarinoes.smarthut.error.NotFoundException;
-import ch.usi.inf.sa4.sanmarinoes.smarthut.models.*;
+import ch.usi.inf.sa4.sanmarinoes.smarthut.models.Switch;
+import ch.usi.inf.sa4.sanmarinoes.smarthut.models.SwitchRepository;
+import ch.usi.inf.sa4.sanmarinoes.smarthut.models.Switchable;
+import ch.usi.inf.sa4.sanmarinoes.smarthut.models.SwitchableRepository;
 import ch.usi.inf.sa4.sanmarinoes.smarthut.service.DeviceService;
 import java.security.Principal;
 import java.util.List;
 import javax.validation.Valid;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.autoconfigure.*;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
 import org.springframework.web.bind.annotation.*;
 
 @RestController
@@ -17,9 +20,8 @@ import org.springframework.web.bind.annotation.*;
 @RequestMapping("/switch")
 public class SwitchController extends InputDeviceConnectionController {
 
-    private SwitchRepository switchRepository;
-    private SwitchableRepository switchableRepository;
-    private DeviceService deviceService;
+    private final SwitchRepository switchRepository;
+    private final DeviceService deviceService;
 
     /**
      * Contstructs the controller by requiring essential object for the controller implementation
@@ -32,7 +34,7 @@ public class SwitchController extends InputDeviceConnectionController outputRepository,
             DeviceService deviceService) {
-        super(inputRepository, outputRepository, Switchable.SWITCH_SWITCHABLE_CONNECTOR);
+        super(inputRepository, outputRepository, deviceService);
         this.deviceService = deviceService;
         this.switchRepository = inputRepository;
     }
@@ -43,7 +45,7 @@ public class SwitchController extends InputDeviceConnectionController update(@Valid @RequestBody SwitchableStateSaveRequest ss)
+    public SwitchableState update(@Valid @RequestBody SwitchableStateSaveRequest ss)
             throws NotFoundException {
-        final SwitchableState initial =
+        final SwitchableState initial =
                 switchableStateRepository.findById(ss.getId()).orElseThrow(NotFoundException::new);
         initial.setOn(ss.isOn());
         switchableStateRepository.save(initial);
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/ThermostatConditionController.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/ThermostatConditionController.java
new file mode 100644
index 0000000..5852c9f
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/ThermostatConditionController.java
@@ -0,0 +1,55 @@
+package ch.usi.inf.sa4.sanmarinoes.smarthut.controller;
+
+import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.ThermostatConditionSaveRequest;
+import ch.usi.inf.sa4.sanmarinoes.smarthut.error.NotFoundException;
+import ch.usi.inf.sa4.sanmarinoes.smarthut.models.ThermostatCondition;
+import ch.usi.inf.sa4.sanmarinoes.smarthut.models.ThermostatConditionRepository;
+import java.util.List;
+import javax.validation.Valid;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
+import org.springframework.web.bind.annotation.*;
+
+@RestController
+@EnableAutoConfiguration
+@RequestMapping("/thermostatCondition")
+public class ThermostatConditionController {
+
+    @Autowired ThermostatConditionRepository thermostatConditionRepository;
+
+    @GetMapping("/{automationId}")
+    public List getAll(@PathVariable long automationId) {
+        return thermostatConditionRepository.findAllByAutomationId(automationId);
+    }
+
+    private ThermostatCondition save(ThermostatCondition newRL, ThermostatConditionSaveRequest s) {
+        newRL.setDeviceId(s.getDeviceId());
+        newRL.setAutomationId(s.getAutomationId());
+        newRL.setOperator(s.getOperator());
+        newRL.setMode(s.getMode());
+
+        return thermostatConditionRepository.save(newRL);
+    }
+
+    @PostMapping
+    public ThermostatCondition create(
+            @Valid @RequestBody ThermostatConditionSaveRequest booleanTriggerSaveRequest) {
+        return save(new ThermostatCondition(), booleanTriggerSaveRequest);
+    }
+
+    @PutMapping
+    public ThermostatCondition update(
+            @Valid @RequestBody ThermostatConditionSaveRequest booleanTriggerSaveRequest)
+            throws NotFoundException {
+        return save(
+                thermostatConditionRepository
+                        .findById(booleanTriggerSaveRequest.getId())
+                        .orElseThrow(NotFoundException::new),
+                booleanTriggerSaveRequest);
+    }
+
+    @DeleteMapping("/{id}")
+    public void delete(@PathVariable long id) {
+        thermostatConditionRepository.deleteById(id);
+    }
+}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/ThermostatController.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/ThermostatController.java
index 7a6fc59..09b21cf 100644
--- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/ThermostatController.java
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/ThermostatController.java
@@ -5,11 +5,11 @@ import ch.usi.inf.sa4.sanmarinoes.smarthut.error.DuplicateStateException;
 import ch.usi.inf.sa4.sanmarinoes.smarthut.error.NotFoundException;
 import ch.usi.inf.sa4.sanmarinoes.smarthut.models.*;
 import ch.usi.inf.sa4.sanmarinoes.smarthut.service.DeviceService;
-import ch.usi.inf.sa4.sanmarinoes.smarthut.service.ThermostatService;
+import ch.usi.inf.sa4.sanmarinoes.smarthut.service.ThermostatPopulationService;
 import java.security.Principal;
 import javax.validation.Valid;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.autoconfigure.*;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
 import org.springframework.web.bind.annotation.*;
 
 @RestController
@@ -19,22 +19,29 @@ public class ThermostatController {
 
     @Autowired private DeviceService deviceService;
     @Autowired private ThermostatRepository thermostatRepository;
-    @Autowired private ThermostatService thermostatService;
+    @Autowired private ThermostatPopulationService thermostatService;
     @Autowired private SceneRepository sceneRepository;
-    @Autowired private StateRepository> stateRepository;
+    @Autowired private StateRepository stateRepository;
 
     private Thermostat save(Thermostat newT, ThermostatSaveRequest t, final Principal principal) {
         newT.setTargetTemperature(t.getTargetTemperature());
         newT.setId(t.getId());
         newT.setName(t.getName());
         newT.setRoomId(t.getRoomId());
-        newT.setMeasuredTemperature(t.getMeasuredTemperature());
         newT.setUseExternalSensors(t.isUseExternalSensors());
-        newT.setOn(t.isTurnOn());
+        newT.setOn(false);
+        if (t.getErr() != null) {
+            newT.setErr(t.getErr());
+        }
+        if (t.getTypical() != null) {
+            newT.setTypical(t.getTypical());
+        }
 
         thermostatService.populateMeasuredTemperature(newT);
-        newT = deviceService.saveAsOwner(newT, principal.getName());
-        return newT;
+        newT = thermostatRepository.save(newT);
+
+        newT.setOn(t.isTurnOn());
+        return deviceService.saveAsOwner(newT, principal.getName());
     }
 
     @PostMapping
@@ -58,11 +65,11 @@ public class ThermostatController {
     @DeleteMapping("/{id}")
     public void deleteById(@PathVariable("id") long id, final Principal principal)
             throws NotFoundException {
-        deviceService.delete(id, principal.getName());
+        deviceService.deleteByIdAsOwner(id, principal.getName());
     }
 
     @PostMapping("/{id}/state")
-    public State sceneBinding(
+    public State sceneBinding(
             @PathVariable("id") long deviceId,
             @RequestParam long sceneId,
             final Principal principal)
@@ -72,9 +79,9 @@ public class ThermostatController {
                 thermostatRepository
                         .findByIdAndUsername(deviceId, principal.getName())
                         .orElseThrow(NotFoundException::new);
-        State s = d.cloneState();
-        sceneRepository.findById(sceneId).orElseThrow(NotFoundException::new);
-        s.setSceneId(sceneId);
+        State s = d.cloneState();
+        final Scene sc = sceneRepository.findById(sceneId).orElseThrow(NotFoundException::new);
+        s.setSceneId(sc.getId());
         if (stateRepository.countByDeviceIdAndSceneId(deviceId, sceneId) > 0)
             throw new DuplicateStateException();
         return stateRepository.save(s);
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/UserAccountController.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/UserAccountController.java
index 950fb1a..a0fe8d1 100644
--- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/UserAccountController.java
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/UserAccountController.java
@@ -2,7 +2,6 @@ package ch.usi.inf.sa4.sanmarinoes.smarthut.controller;
 
 import ch.usi.inf.sa4.sanmarinoes.smarthut.config.EmailConfigurationService;
 import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.InitPasswordResetRequest;
-import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.OkResponse;
 import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.PasswordResetRequest;
 import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.UserRegistrationRequest;
 import ch.usi.inf.sa4.sanmarinoes.smarthut.error.DuplicateRegistrationException;
@@ -65,7 +64,7 @@ public class UserAccountController {
                         + (isRegistration
                                 ? emailConfig.getRegistrationPath()
                                 : emailConfig.getResetPasswordPath())
-                        + token.getConfirmationToken());
+                        + token.getConfirmToken());
 
         emailSenderService.sendEmail(mailMessage);
     }
@@ -78,7 +77,7 @@ public class UserAccountController {
      * @throws DuplicateRegistrationException if a user exists with same email or username
      */
     @PostMapping
-    public OkResponse registerUser(@Valid @RequestBody UserRegistrationRequest registrationData)
+    public void registerUser(@Valid @RequestBody UserRegistrationRequest registrationData)
             throws DuplicateRegistrationException {
         final User existingEmailUser =
                 userRepository.findByEmailIgnoreCase(registrationData.getEmail());
@@ -102,18 +101,11 @@ public class UserAccountController {
             toSave.setEmail(registrationData.getEmail());
             userRepository.save(toSave);
 
-            ConfirmationToken token;
-            do {
-                token = new ConfirmationToken(toSave);
-            } while (confirmationTokenRepository.findByConfirmationToken(
-                            token.getConfirmationToken())
-                    != null);
+            ConfirmationToken token = new ConfirmationToken(toSave);
 
             confirmationTokenRepository.save(token);
 
             sendEmail(toSave.getEmail(), token, true);
-
-            return new OkResponse();
         }
     }
 
@@ -125,7 +117,7 @@ public class UserAccountController {
      * @throws UserNotFoundException if given email does not belong to any user
      */
     @PostMapping("/init-reset-password")
-    public OkResponse initResetPassword(@Valid @RequestBody InitPasswordResetRequest resetRequest)
+    public void initResetPassword(@Valid @RequestBody InitPasswordResetRequest resetRequest)
             throws UserNotFoundException {
         final User toReset = userRepository.findByEmailIgnoreCase(resetRequest.getEmail());
 
@@ -134,12 +126,8 @@ public class UserAccountController {
             throw new UserNotFoundException();
         }
 
-        ConfirmationToken token;
-        do {
-            token = new ConfirmationToken(toReset);
-            token.setResetPassword(true);
-        } while (confirmationTokenRepository.findByConfirmationToken(token.getConfirmationToken())
-                != null);
+        ConfirmationToken token = new ConfirmationToken(toReset);
+        token.setResetPassword(true);
 
         // Delete existing email password reset tokens
         confirmationTokenRepository.deleteByUserAndResetPassword(toReset, true);
@@ -148,8 +136,6 @@ public class UserAccountController {
         confirmationTokenRepository.save(token);
 
         sendEmail(toReset.getEmail(), token, false);
-
-        return new OkResponse();
     }
 
     /**
@@ -160,15 +146,12 @@ public class UserAccountController {
      * @throws EmailTokenNotFoundException if given token is not a valid token for password reset
      */
     @PutMapping("/reset-password")
-    public OkResponse resetPassword(
-            @Valid @RequestBody PasswordResetRequest resetRequest,
-            final HttpServletResponse response)
-            throws EmailTokenNotFoundException, IOException {
+    public void resetPassword(@Valid @RequestBody PasswordResetRequest resetRequest)
+            throws EmailTokenNotFoundException {
         final ConfirmationToken token =
-                confirmationTokenRepository.findByConfirmationToken(
-                        resetRequest.getConfirmationToken());
+                confirmationTokenRepository.findByConfirmToken(resetRequest.getConfirmationToken());
 
-        if (token == null || !token.getResetPassword()) {
+        if (token == null || !token.isResetPassword()) {
             throw new EmailTokenNotFoundException();
         }
 
@@ -178,8 +161,6 @@ public class UserAccountController {
 
         // Delete token to prevent further password changes
         confirmationTokenRepository.delete(token);
-
-        return new OkResponse();
     }
 
     /**
@@ -196,9 +177,9 @@ public class UserAccountController {
             final HttpServletResponse response)
             throws EmailTokenNotFoundException, IOException {
         final ConfirmationToken token =
-                confirmationTokenRepository.findByConfirmationToken(confirmationToken);
+                confirmationTokenRepository.findByConfirmToken(confirmationToken);
 
-        if (token != null && !token.getResetPassword()) {
+        if (token != null && !token.isResetPassword()) {
             token.getUser().setEnabled(true);
             userRepository.save(token.getUser());
             response.sendRedirect(emailConfig.getRegistrationRedirect());
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/AutomationFastUpdateRequest.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/AutomationFastUpdateRequest.java
index 58ce46c..b2c90d4 100644
--- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/AutomationFastUpdateRequest.java
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/AutomationFastUpdateRequest.java
@@ -1,97 +1,18 @@
 package ch.usi.inf.sa4.sanmarinoes.smarthut.dto;
 
-import ch.usi.inf.sa4.sanmarinoes.smarthut.models.BooleanTrigger;
-import ch.usi.inf.sa4.sanmarinoes.smarthut.models.RangeTrigger;
-import ch.usi.inf.sa4.sanmarinoes.smarthut.models.ScenePriority;
-import ch.usi.inf.sa4.sanmarinoes.smarthut.models.Trigger;
+import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.automation.ConditionDTO;
+import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.automation.ScenePriorityDTO;
+import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.automation.TriggerDTO;
 import java.util.List;
-import javax.validation.constraints.Min;
 import javax.validation.constraints.NotEmpty;
 import javax.validation.constraints.NotNull;
+import lombok.Data;
 
+@Data
 public class AutomationFastUpdateRequest {
-    public abstract static class TriggerDTO {
-        @NotNull public long deviceId;
-
-        public abstract Trigger toModel();
-    }
-
-    public static class BooleanTriggerDTO extends TriggerDTO {
-        @NotNull public boolean on;
-
-        @Override
-        public Trigger toModel() {
-            BooleanTrigger t = new BooleanTrigger<>();
-            t.setDeviceId(this.deviceId);
-            t.setOn(this.on);
-            return t;
-        }
-    }
-
-    public static class RangeTriggerDTO extends TriggerDTO {
-        @NotNull RangeTrigger.Operator operator;
-        @NotNull double range;
-
-        @Override
-        public Trigger toModel() {
-            RangeTrigger t = new RangeTrigger<>();
-            t.setDeviceId(this.deviceId);
-            t.setOperator(this.operator);
-            t.setRange(this.range);
-            return t;
-        }
-    }
-
-    public static class ScenePriorityDTO {
-        @NotNull public long sceneId;
-
-        @NotNull
-        @Min(0)
-        public Integer priority;
-
-        public ScenePriority toModel() {
-            ScenePriority s = new ScenePriority();
-            s.setSceneId(sceneId);
-            s.setPriority(priority);
-            return s;
-        }
-    }
-
     @NotNull private List scenes;
     @NotNull private List triggers;
+    @NotNull private List conditions;
     @NotNull private long id;
-
     @NotNull @NotEmpty private String name;
-
-    public long getId() {
-        return id;
-    }
-
-    public String getName() {
-        return name;
-    }
-
-    public void setName(String name) {
-        this.name = name;
-    }
-
-    public List getScenes() {
-        return scenes;
-    }
-
-    public void setScenes(List scenes) {
-        this.scenes = scenes;
-    }
-
-    public List getTriggers() {
-        return triggers;
-    }
-
-    public void setTriggers(List triggers) {
-        this.triggers = triggers;
-    }
-
-    public void setId(long id) {
-        this.id = id;
-    }
 }
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/AutomationSaveRequest.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/AutomationSaveRequest.java
index bcd1f8b..9e57f06 100644
--- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/AutomationSaveRequest.java
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/AutomationSaveRequest.java
@@ -2,22 +2,12 @@ package ch.usi.inf.sa4.sanmarinoes.smarthut.dto;
 
 import javax.validation.constraints.NotEmpty;
 import javax.validation.constraints.NotNull;
+import lombok.AllArgsConstructor;
+import lombok.Data;
 
+@Data
+@AllArgsConstructor
 public class AutomationSaveRequest {
-
     private long id;
-
     @NotNull @NotEmpty private String name;
-
-    public long getId() {
-        return id;
-    }
-
-    public String getName() {
-        return name;
-    }
-
-    public void setName(String name) {
-        this.name = name;
-    }
 }
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/BooleanConditionOrTriggerSaveRequest.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/BooleanConditionOrTriggerSaveRequest.java
new file mode 100644
index 0000000..de5b4c6
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/BooleanConditionOrTriggerSaveRequest.java
@@ -0,0 +1,16 @@
+package ch.usi.inf.sa4.sanmarinoes.smarthut.dto;
+
+import javax.validation.constraints.NotNull;
+import lombok.Data;
+
+@Data
+public class BooleanConditionOrTriggerSaveRequest {
+
+    private long id;
+
+    @NotNull private Long deviceId;
+
+    @NotNull private Long automationId;
+
+    private boolean on;
+}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/BooleanTriggerSaveRequest.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/BooleanTriggerSaveRequest.java
deleted file mode 100644
index 2c08bc0..0000000
--- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/BooleanTriggerSaveRequest.java
+++ /dev/null
@@ -1,42 +0,0 @@
-package ch.usi.inf.sa4.sanmarinoes.smarthut.dto;
-
-import javax.validation.constraints.NotNull;
-
-public class BooleanTriggerSaveRequest {
-
-    private long id;
-
-    @NotNull private Long deviceId;
-
-    @NotNull private Long automationId;
-
-    private boolean on;
-
-    public long getId() {
-        return id;
-    }
-
-    public Long getDeviceId() {
-        return deviceId;
-    }
-
-    public void setDeviceId(Long deviceId) {
-        this.deviceId = deviceId;
-    }
-
-    public Long getAutomationId() {
-        return automationId;
-    }
-
-    public void setAutomationId(Long automationId) {
-        this.automationId = automationId;
-    }
-
-    public boolean isOn() {
-        return on;
-    }
-
-    public void setOn(boolean on) {
-        this.on = on;
-    }
-}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/ButtonDimmerDimRequest.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/ButtonDimmerDimRequest.java
index 8e07015..4e15ef5 100644
--- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/ButtonDimmerDimRequest.java
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/ButtonDimmerDimRequest.java
@@ -1,8 +1,10 @@
 package ch.usi.inf.sa4.sanmarinoes.smarthut.dto;
 
 import javax.validation.constraints.NotNull;
+import lombok.Data;
 
 /** A 'dim' event from a button dimmer. */
+@Data
 public class ButtonDimmerDimRequest {
 
     /** The device id */
@@ -15,20 +17,4 @@ public class ButtonDimmerDimRequest {
 
     /** Whether the dim is up or down */
     @NotNull private DimType dimType;
-
-    public DimType getDimType() {
-        return dimType;
-    }
-
-    public void setDimType(DimType dimType) {
-        this.dimType = dimType;
-    }
-
-    public Long getId() {
-        return id;
-    }
-
-    public void setId(Long id) {
-        this.id = id;
-    }
 }
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/DeviceSaveRequest.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/DeviceSaveRequest.java
index a975117..72b9ea7 100644
--- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/DeviceSaveRequest.java
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/DeviceSaveRequest.java
@@ -2,7 +2,9 @@ package ch.usi.inf.sa4.sanmarinoes.smarthut.dto;
 
 import javax.validation.constraints.NotEmpty;
 import javax.validation.constraints.NotNull;
+import lombok.Data;
 
+@Data
 public class DeviceSaveRequest {
     /** Device identifier */
     private long id;
@@ -15,28 +17,4 @@ public class DeviceSaveRequest {
 
     /** The name of the device as assigned by the user (e.g. 'Master bedroom light') */
     @NotNull @NotEmpty private String name;
-
-    public long getId() {
-        return id;
-    }
-
-    public void setId(long id) {
-        this.id = id;
-    }
-
-    public Long getRoomId() {
-        return roomId;
-    }
-
-    public void setRoomId(Long roomId) {
-        this.roomId = roomId;
-    }
-
-    public String getName() {
-        return name;
-    }
-
-    public void setName(String name) {
-        this.name = name;
-    }
 }
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/DimmableSaveRequest.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/DimmableSaveRequest.java
index acefa72..31325c3 100644
--- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/DimmableSaveRequest.java
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/DimmableSaveRequest.java
@@ -3,7 +3,9 @@ package ch.usi.inf.sa4.sanmarinoes.smarthut.dto;
 import javax.validation.constraints.Max;
 import javax.validation.constraints.Min;
 import javax.validation.constraints.NotNull;
+import lombok.Data;
 
+@Data
 public class DimmableSaveRequest {
 
     /** Device id (used only for update requests) */
@@ -23,36 +25,4 @@ public class DimmableSaveRequest {
 
     /** The name of the device as assigned by the user (e.g. 'Master bedroom light') */
     @NotNull private String name;
-
-    public void setRoomId(Long roomId) {
-        this.roomId = roomId;
-    }
-
-    public void setName(String name) {
-        this.name = name;
-    }
-
-    public Long getRoomId() {
-        return roomId;
-    }
-
-    public String getName() {
-        return name;
-    }
-
-    public Integer getIntensity() {
-        return intensity;
-    }
-
-    public void setIntensity(Integer intensity) {
-        this.intensity = intensity;
-    }
-
-    public long getId() {
-        return id;
-    }
-
-    public void setId(long id) {
-        this.id = id;
-    }
 }
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/DimmableStateSaveRequest.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/DimmableStateSaveRequest.java
index ebf99eb..00f875b 100644
--- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/DimmableStateSaveRequest.java
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/DimmableStateSaveRequest.java
@@ -3,7 +3,9 @@ package ch.usi.inf.sa4.sanmarinoes.smarthut.dto;
 import javax.validation.constraints.Max;
 import javax.validation.constraints.Min;
 import javax.validation.constraints.NotNull;
+import lombok.Data;
 
+@Data
 public class DimmableStateSaveRequest {
 
     /** Device id (used only for update requests) */
@@ -13,16 +15,4 @@ public class DimmableStateSaveRequest {
     @Min(0)
     @Max(100)
     private Integer intensity = 0;
-
-    public Integer getIntensity() {
-        return intensity;
-    }
-
-    public void setIntensity(Integer intensity) {
-        this.intensity = intensity;
-    }
-
-    public Long getId() {
-        return id;
-    }
 }
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/GenericDeviceSaveReguest.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/GenericDeviceSaveRequest.java
similarity index 56%
rename from src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/GenericDeviceSaveReguest.java
rename to src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/GenericDeviceSaveRequest.java
index 8ec2671..d550a8d 100644
--- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/GenericDeviceSaveReguest.java
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/GenericDeviceSaveRequest.java
@@ -2,7 +2,14 @@ package ch.usi.inf.sa4.sanmarinoes.smarthut.dto;
 
 import javax.validation.constraints.NotNull;
 
-public class GenericDeviceSaveReguest {
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class GenericDeviceSaveRequest {
     /**
      * The room this device belongs in, as a foreign key id. To use when updating and inserting from
      * a REST call.
@@ -11,20 +18,4 @@ public class GenericDeviceSaveReguest {
 
     /** The name of the device as assigned by the user (e.g. 'Master bedroom light') */
     @NotNull private String name;
-
-    public void setRoomId(Long roomId) {
-        this.roomId = roomId;
-    }
-
-    public void setName(String name) {
-        this.name = name;
-    }
-
-    public Long getRoomId() {
-        return roomId;
-    }
-
-    public String getName() {
-        return name;
-    }
 }
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/GuestPermissionsRequest.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/GuestPermissionsRequest.java
index 8c1a2c4..17da407 100644
--- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/GuestPermissionsRequest.java
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/GuestPermissionsRequest.java
@@ -1,13 +1,8 @@
 package ch.usi.inf.sa4.sanmarinoes.smarthut.dto;
 
+import lombok.Data;
+
+@Data
 public class GuestPermissionsRequest {
     private boolean cameraEnabled;
-
-    public boolean isCameraEnabled() {
-        return cameraEnabled;
-    }
-
-    public void setCameraEnabled(boolean cameraEnabled) {
-        this.cameraEnabled = cameraEnabled;
-    }
 }
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/GuestsUpdateRequest.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/GuestsUpdateRequest.java
new file mode 100644
index 0000000..a4527e7
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/GuestsUpdateRequest.java
@@ -0,0 +1,10 @@
+package ch.usi.inf.sa4.sanmarinoes.smarthut.dto;
+
+import java.util.List;
+import javax.validation.constraints.NotNull;
+import lombok.Data;
+
+@Data
+public class GuestsUpdateRequest {
+    @NotNull private List ids;
+}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/InitPasswordResetRequest.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/InitPasswordResetRequest.java
index d82c4f0..4148e18 100644
--- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/InitPasswordResetRequest.java
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/InitPasswordResetRequest.java
@@ -3,8 +3,14 @@ package ch.usi.inf.sa4.sanmarinoes.smarthut.dto;
 import javax.validation.constraints.Email;
 import javax.validation.constraints.NotEmpty;
 import javax.validation.constraints.Pattern;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
 
 /** DTO for password reset request */
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
 public class InitPasswordResetRequest {
     /**
      * The user's email (validated according to criteria used in >input type="email"<>
@@ -14,12 +20,4 @@ public class InitPasswordResetRequest {
     @Email(message = "Please provide a valid email address")
     @Pattern(regexp = ".+@.+\\..+", message = "Please provide a valid email address")
     private String email;
-
-    public String getEmail() {
-        return email;
-    }
-
-    public void setEmail(String email) {
-        this.email = email;
-    }
 }
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/JWTRequest.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/JWTRequest.java
index da11bc3..ef2dcb4 100644
--- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/JWTRequest.java
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/JWTRequest.java
@@ -1,36 +1,14 @@
 package ch.usi.inf.sa4.sanmarinoes.smarthut.dto;
 
 import javax.validation.constraints.NotNull;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
 
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
 public class JWTRequest {
     @NotNull private String usernameOrEmail;
     @NotNull private String password;
-
-    public String getUsernameOrEmail() {
-        return this.usernameOrEmail;
-    }
-
-    public void setUsernameOrEmail(String usernameOrEmail) {
-        this.usernameOrEmail = usernameOrEmail;
-    }
-
-    public String getPassword() {
-        return this.password;
-    }
-
-    public void setPassword(String password) {
-        this.password = password;
-    }
-
-    @Override
-    public String toString() {
-        return "JWTRequest{"
-                + "usernameOrEmail='"
-                + usernameOrEmail
-                + '\''
-                + ", password='"
-                + password
-                + '\''
-                + '}';
-    }
 }
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/JWTResponse.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/JWTResponse.java
index 7bc04f2..bf47cc3 100644
--- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/JWTResponse.java
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/JWTResponse.java
@@ -1,13 +1,10 @@
 package ch.usi.inf.sa4.sanmarinoes.smarthut.dto;
 
+import lombok.AllArgsConstructor;
+import lombok.Data;
+
+@Data
+@AllArgsConstructor
 public class JWTResponse {
     private final String jwttoken;
-
-    public JWTResponse(String jwttoken) {
-        this.jwttoken = jwttoken;
-    }
-
-    public String getToken() {
-        return this.jwttoken;
-    }
 }
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/KnobDimmerDimRequest.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/KnobDimmerDimRequest.java
index 6df303a..d0a45c8 100644
--- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/KnobDimmerDimRequest.java
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/KnobDimmerDimRequest.java
@@ -3,7 +3,9 @@ package ch.usi.inf.sa4.sanmarinoes.smarthut.dto;
 import javax.validation.constraints.Max;
 import javax.validation.constraints.Min;
 import javax.validation.constraints.NotNull;
+import lombok.Data;
 
+@Data
 public class KnobDimmerDimRequest {
 
     /** The device id */
@@ -14,20 +16,4 @@ public class KnobDimmerDimRequest {
     @Min(0)
     @Max(100)
     private Integer intensity;
-
-    public Long getId() {
-        return id;
-    }
-
-    public void setId(Long id) {
-        this.id = id;
-    }
-
-    public Integer getIntensity() {
-        return intensity;
-    }
-
-    public void setIntensity(Integer intensity) {
-        this.intensity = intensity;
-    }
 }
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/OkResponse.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/OkResponse.java
deleted file mode 100644
index e3de94e..0000000
--- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/OkResponse.java
+++ /dev/null
@@ -1,6 +0,0 @@
-package ch.usi.inf.sa4.sanmarinoes.smarthut.dto;
-
-/** A dummy DTO to return when there is no data to return */
-public class OkResponse {
-    private boolean success = true;
-}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/PasswordResetRequest.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/PasswordResetRequest.java
index bf5bccf..e533c49 100644
--- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/PasswordResetRequest.java
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/PasswordResetRequest.java
@@ -1,8 +1,16 @@
 package ch.usi.inf.sa4.sanmarinoes.smarthut.dto;
 
-import javax.validation.constraints.*;
+import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.NotNull;
+import javax.validation.constraints.Size;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
 
 /** DTO for password reset request */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
 public class PasswordResetRequest {
 
     @NotNull private String confirmationToken;
@@ -15,20 +23,4 @@ public class PasswordResetRequest {
             max = 255,
             message = "Your password should be at least 6 characters long and up to 255 chars long")
     private String password;
-
-    public String getConfirmationToken() {
-        return confirmationToken;
-    }
-
-    public void setConfirmationToken(String confirmationToken) {
-        this.confirmationToken = confirmationToken;
-    }
-
-    public String getPassword() {
-        return password;
-    }
-
-    public void setPassword(String password) {
-        this.password = password;
-    }
 }
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/RangeConditionOrTriggerSaveRequest.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/RangeConditionOrTriggerSaveRequest.java
new file mode 100644
index 0000000..a222202
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/RangeConditionOrTriggerSaveRequest.java
@@ -0,0 +1,19 @@
+package ch.usi.inf.sa4.sanmarinoes.smarthut.dto;
+
+import ch.usi.inf.sa4.sanmarinoes.smarthut.models.Operator;
+import javax.validation.constraints.NotNull;
+import lombok.Data;
+
+@Data
+public class RangeConditionOrTriggerSaveRequest {
+
+    private long id;
+
+    @NotNull private Long deviceId;
+
+    @NotNull private Long automationId;
+
+    @NotNull private Operator operator;
+
+    @NotNull private double range;
+}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/RangeTriggerSaveRequest.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/RangeTriggerSaveRequest.java
deleted file mode 100644
index 567c035..0000000
--- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/RangeTriggerSaveRequest.java
+++ /dev/null
@@ -1,57 +0,0 @@
-package ch.usi.inf.sa4.sanmarinoes.smarthut.dto;
-
-import ch.usi.inf.sa4.sanmarinoes.smarthut.models.RangeTrigger;
-import javax.validation.constraints.NotNull;
-
-public class RangeTriggerSaveRequest {
-
-    private long id;
-
-    @NotNull private Long deviceId;
-
-    @NotNull private Long automationId;
-
-    @NotNull private RangeTrigger.Operator operator;
-
-    @NotNull private double range;
-
-    public long getId() {
-        return id;
-    }
-
-    public void setId(long id) {
-        this.id = id;
-    }
-
-    public Long getDeviceId() {
-        return deviceId;
-    }
-
-    public void setDeviceId(Long deviceId) {
-        this.deviceId = deviceId;
-    }
-
-    public Long getAutomationId() {
-        return automationId;
-    }
-
-    public void setAutomationId(Long automationId) {
-        this.automationId = automationId;
-    }
-
-    public RangeTrigger.Operator getOperator() {
-        return operator;
-    }
-
-    public void setOperator(RangeTrigger.Operator operator) {
-        this.operator = operator;
-    }
-
-    public double getRange() {
-        return range;
-    }
-
-    public void setRange(Double range) {
-        this.range = range;
-    }
-}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/RoomSaveRequest.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/RoomSaveRequest.java
index 02a0e35..7883aa3 100644
--- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/RoomSaveRequest.java
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/RoomSaveRequest.java
@@ -1,15 +1,19 @@
 package ch.usi.inf.sa4.sanmarinoes.smarthut.dto;
 
-import ch.usi.inf.sa4.sanmarinoes.smarthut.models.Room;
+import ch.usi.inf.sa4.sanmarinoes.smarthut.models.Icon;
 import javax.persistence.Lob;
 import javax.validation.constraints.NotNull;
+import lombok.AllArgsConstructor;
+import lombok.Data;
 
+@Data
+@AllArgsConstructor
 public class RoomSaveRequest {
 
     /** Room identifier */
     private long id;
 
-    @NotNull private Room.Icon icon;
+    @NotNull private Icon icon;
 
     /**
      * Image is to be given as byte[]. In order to get an encoded string from it, the
@@ -21,36 +25,4 @@ public class RoomSaveRequest {
 
     /** The user given name of this room (e.g. 'Master bedroom') */
     @NotNull private String name;
-
-    public long getId() {
-        return id;
-    }
-
-    public void setId(long id) {
-        this.id = id;
-    }
-
-    public String getName() {
-        return name;
-    }
-
-    public void setName(String name) {
-        this.name = name;
-    }
-
-    public Room.Icon getIcon() {
-        return icon;
-    }
-
-    public void setIcon(Room.Icon icon) {
-        this.icon = icon;
-    }
-
-    public String getImage() {
-        return image;
-    }
-
-    public void setImage(String image) {
-        this.image = image;
-    }
 }
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/ScenePrioritySaveRequest.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/ScenePrioritySaveRequest.java
index 14a1873..6f5236b 100644
--- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/ScenePrioritySaveRequest.java
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/ScenePrioritySaveRequest.java
@@ -2,7 +2,9 @@ package ch.usi.inf.sa4.sanmarinoes.smarthut.dto;
 
 import javax.validation.constraints.Min;
 import javax.validation.constraints.NotNull;
+import lombok.Data;
 
+@Data
 public class ScenePrioritySaveRequest {
 
     @NotNull private Long automationId;
@@ -11,28 +13,4 @@ public class ScenePrioritySaveRequest {
     private Integer priority;
 
     @NotNull private Long sceneId;
-
-    public Long getAutomationId() {
-        return automationId;
-    }
-
-    public void setAutomationId(Long automationId) {
-        this.automationId = automationId;
-    }
-
-    public Integer getPriority() {
-        return priority;
-    }
-
-    public void setPriority(Integer priority) {
-        this.priority = priority;
-    }
-
-    public Long getSceneId() {
-        return sceneId;
-    }
-
-    public void setSceneId(Long sceneId) {
-        this.sceneId = sceneId;
-    }
 }
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/SceneSaveRequest.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/SceneSaveRequest.java
index 080ea2b..1da48f9 100644
--- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/SceneSaveRequest.java
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/SceneSaveRequest.java
@@ -1,8 +1,15 @@
 package ch.usi.inf.sa4.sanmarinoes.smarthut.dto;
 
-import com.sun.istack.NotNull;
+import ch.usi.inf.sa4.sanmarinoes.smarthut.models.Icon;
 import javax.persistence.Column;
+import javax.validation.constraints.NotNull;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
 
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
 public class SceneSaveRequest {
 
     /** Room identifier */
@@ -11,26 +18,8 @@ public class SceneSaveRequest {
     /** The user given name of this room (e.g. 'Master bedroom') */
     @NotNull private String name;
 
+    @NotNull private Icon icon;
+
     /** Determines whether a guest can access this scene */
     @Column @NotNull private boolean guestAccessEnabled;
-
-    public boolean isGuestAccessEnabled() {
-        return guestAccessEnabled;
-    }
-
-    public void setGuestAccessEnabled(boolean guestAccessEnabled) {
-        this.guestAccessEnabled = guestAccessEnabled;
-    }
-
-    public long getId() {
-        return id;
-    }
-
-    public String getName() {
-        return name;
-    }
-
-    public void setName(String name) {
-        this.name = name;
-    }
 }
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/SensorSaveRequest.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/SensorSaveRequest.java
index 62b0b5e..59a089a 100644
--- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/SensorSaveRequest.java
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/SensorSaveRequest.java
@@ -1,29 +1,18 @@
 package ch.usi.inf.sa4.sanmarinoes.smarthut.dto;
 
 import ch.usi.inf.sa4.sanmarinoes.smarthut.models.Sensor;
-import com.google.gson.annotations.SerializedName;
 import java.math.BigDecimal;
 import javax.persistence.EnumType;
 import javax.persistence.Enumerated;
 import javax.validation.constraints.NotNull;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
 
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
 public class SensorSaveRequest {
-
-    /** Type of sensor, i.e. of the thing the sensor measures. */
-    public enum SensorType {
-        /** A sensor that measures temperature in degrees celsius */
-        @SerializedName("TEMPERATURE")
-        TEMPERATURE,
-
-        /** A sensor that measures relative humidity in percentage points */
-        @SerializedName("HUMIDITY")
-        HUMIDITY,
-
-        /** A sensor that measures light in degrees */
-        @SerializedName("LIGHT")
-        LIGHT
-    }
-
     /** The type of this sensor */
     @NotNull
     @Enumerated(value = EnumType.STRING)
@@ -39,36 +28,4 @@ public class SensorSaveRequest {
 
     /** The name of the device as assigned by the user (e.g. 'Master bedroom light') */
     @NotNull private String name;
-
-    public void setRoomId(Long roomId) {
-        this.roomId = roomId;
-    }
-
-    public void setName(String name) {
-        this.name = name;
-    }
-
-    public Long getRoomId() {
-        return roomId;
-    }
-
-    public String getName() {
-        return name;
-    }
-
-    public Sensor.SensorType getSensor() {
-        return sensor;
-    }
-
-    public void setSensor(Sensor.SensorType sensor) {
-        this.sensor = sensor;
-    }
-
-    public BigDecimal getValue() {
-        return value;
-    }
-
-    public void setValue(BigDecimal value) {
-        this.value = value;
-    }
 }
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/SwitchOperationRequest.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/SwitchOperationRequest.java
index 3fb552b..fa1aed5 100644
--- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/SwitchOperationRequest.java
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/SwitchOperationRequest.java
@@ -1,8 +1,10 @@
 package ch.usi.inf.sa4.sanmarinoes.smarthut.dto;
 
 import javax.validation.constraints.NotNull;
+import lombok.Data;
 
 /** An on/off/toggle operation on a switch */
+@Data
 public class SwitchOperationRequest {
 
     /** The device id */
@@ -16,20 +18,4 @@ public class SwitchOperationRequest {
 
     /** The type of switch operation */
     @NotNull private SwitchOperationRequest.OperationType type;
-
-    public OperationType getType() {
-        return type;
-    }
-
-    public void setType(OperationType type) {
-        this.type = type;
-    }
-
-    public Long getId() {
-        return id;
-    }
-
-    public void setId(Long id) {
-        this.id = id;
-    }
 }
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/SwitchableSaveRequest.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/SwitchableSaveRequest.java
index 16caaed..7339714 100644
--- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/SwitchableSaveRequest.java
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/SwitchableSaveRequest.java
@@ -1,7 +1,13 @@
 package ch.usi.inf.sa4.sanmarinoes.smarthut.dto;
 
 import javax.validation.constraints.NotNull;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
 
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
 public class SwitchableSaveRequest {
     /** The state of this switch */
     private boolean on;
@@ -17,36 +23,4 @@ public class SwitchableSaveRequest {
 
     /** The name of the device as assigned by the user (e.g. 'Master bedroom light') */
     @NotNull private String name;
-
-    public void setRoomId(Long roomId) {
-        this.roomId = roomId;
-    }
-
-    public void setName(String name) {
-        this.name = name;
-    }
-
-    public long getId() {
-        return id;
-    }
-
-    public Long getRoomId() {
-        return roomId;
-    }
-
-    public String getName() {
-        return name;
-    }
-
-    public boolean isOn() {
-        return on;
-    }
-
-    public void setOn(boolean on) {
-        this.on = on;
-    }
-
-    public void setId(long id) {
-        this.id = id;
-    }
 }
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/SwitchableStateSaveRequest.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/SwitchableStateSaveRequest.java
index ab03f27..7a29c02 100644
--- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/SwitchableStateSaveRequest.java
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/SwitchableStateSaveRequest.java
@@ -1,23 +1,13 @@
 package ch.usi.inf.sa4.sanmarinoes.smarthut.dto;
 
 import javax.validation.constraints.NotNull;
+import lombok.Data;
 
+@Data
 public class SwitchableStateSaveRequest {
 
     /** Device id (used only for update requests) */
     @NotNull private Long id;
 
     @NotNull private boolean on;
-
-    public boolean isOn() {
-        return on;
-    }
-
-    public void setOn(boolean on) {
-        this.on = on;
-    }
-
-    public Long getId() {
-        return id;
-    }
 }
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/ThermostatConditionSaveRequest.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/ThermostatConditionSaveRequest.java
new file mode 100644
index 0000000..c45b482
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/ThermostatConditionSaveRequest.java
@@ -0,0 +1,19 @@
+package ch.usi.inf.sa4.sanmarinoes.smarthut.dto;
+
+import ch.usi.inf.sa4.sanmarinoes.smarthut.models.Thermostat;
+import ch.usi.inf.sa4.sanmarinoes.smarthut.models.ThermostatCondition;
+import javax.validation.constraints.NotNull;
+import lombok.Data;
+
+@Data
+public class ThermostatConditionSaveRequest {
+    @NotNull private long id;
+
+    @NotNull private Long deviceId;
+
+    @NotNull private Long automationId;
+
+    @NotNull private ThermostatCondition.Operator operator;
+
+    @NotNull private Thermostat.Mode mode;
+}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/ThermostatSaveRequest.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/ThermostatSaveRequest.java
index 3986996..3734613 100644
--- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/ThermostatSaveRequest.java
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/ThermostatSaveRequest.java
@@ -2,7 +2,9 @@ package ch.usi.inf.sa4.sanmarinoes.smarthut.dto;
 
 import java.math.BigDecimal;
 import javax.validation.constraints.NotNull;
+import lombok.Data;
 
+@Data
 public class ThermostatSaveRequest {
 
     /** Device identifier */
@@ -22,64 +24,11 @@ public class ThermostatSaveRequest {
 
     @NotNull private boolean useExternalSensors;
 
-    @NotNull private BigDecimal measuredTemperature;
-
     /** State of this thermostat */
     @NotNull private boolean turnOn;
 
-    public boolean isTurnOn() {
-        return turnOn;
-    }
+    /** The value of the error according to this value */
+    private BigDecimal err;
 
-    public void setTurnOn(boolean turnOn) {
-        this.turnOn = turnOn;
-    }
-
-    public boolean isUseExternalSensors() {
-        return useExternalSensors;
-    }
-
-    public void setUseExternalSensors(boolean useExternalSensors) {
-        this.useExternalSensors = useExternalSensors;
-    }
-
-    public BigDecimal getTargetTemperature() {
-        return this.targetTemperature;
-    }
-
-    public void setTargetTemperature(BigDecimal targetTemperature) {
-        this.targetTemperature = targetTemperature;
-    }
-
-    public void setRoomId(Long roomId) {
-        this.roomId = roomId;
-    }
-
-    public void setName(String name) {
-        this.name = name;
-    }
-
-    public long getId() {
-        return id;
-    }
-
-    public Long getRoomId() {
-        return roomId;
-    }
-
-    public String getName() {
-        return name;
-    }
-
-    public void setId(long id) {
-        this.id = id;
-    }
-
-    public BigDecimal getMeasuredTemperature() {
-        return measuredTemperature;
-    }
-
-    public void setMeasuredTemperature(BigDecimal measuredTemperature) {
-        this.measuredTemperature = measuredTemperature;
-    }
+    private BigDecimal typical;
 }
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/UserRegistrationRequest.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/UserRegistrationRequest.java
index 785d408..bf1caa5 100644
--- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/UserRegistrationRequest.java
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/UserRegistrationRequest.java
@@ -1,7 +1,9 @@
 package ch.usi.inf.sa4.sanmarinoes.smarthut.dto;
 
 import javax.validation.constraints.*;
+import lombok.Data;
 
+@Data
 public class UserRegistrationRequest {
 
     /** The full name of the user */
@@ -35,36 +37,4 @@ public class UserRegistrationRequest {
     @Email(message = "Please provide a valid email address")
     @Pattern(regexp = ".+@.+\\..+", message = "Please provide a valid email address")
     private String email;
-
-    public String getName() {
-        return name;
-    }
-
-    public void setName(String name) {
-        this.name = name;
-    }
-
-    public String getUsername() {
-        return username;
-    }
-
-    public void setUsername(String username) {
-        this.username = username;
-    }
-
-    public String getPassword() {
-        return password;
-    }
-
-    public void setPassword(String password) {
-        this.password = password;
-    }
-
-    public String getEmail() {
-        return email;
-    }
-
-    public void setEmail(String email) {
-        this.email = email;
-    }
 }
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/UserResponse.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/UserResponse.java
new file mode 100644
index 0000000..3cc0910
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/UserResponse.java
@@ -0,0 +1,19 @@
+package ch.usi.inf.sa4.sanmarinoes.smarthut.dto;
+
+import ch.usi.inf.sa4.sanmarinoes.smarthut.models.User;
+import lombok.Data;
+
+@Data
+public class UserResponse {
+    private Long id;
+    private String username;
+    private String name;
+
+    public static UserResponse fromUser(User u) {
+        final UserResponse us = new UserResponse();
+        us.name = u.getName();
+        us.id = u.getId();
+        us.username = u.getUsername();
+        return us;
+    }
+}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/UserUpdateRequest.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/UserUpdateRequest.java
deleted file mode 100644
index 551b84a..0000000
--- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/UserUpdateRequest.java
+++ /dev/null
@@ -1,48 +0,0 @@
-package ch.usi.inf.sa4.sanmarinoes.smarthut.dto;
-
-import javax.validation.constraints.Email;
-import javax.validation.constraints.NotEmpty;
-import javax.validation.constraints.Pattern;
-
-public class UserUpdateRequest {
-    /** The full name of the user */
-    @NotEmpty(message = "Please provide a full name")
-    private String name;
-
-    /** A non-salted password */
-    @NotEmpty(message = "Please provide a password")
-    private String password;
-
-    /**
-     * The user's email (validated according to criteria used in >input type="email"<>
-     * , technically not RFC 5322 compliant
-     */
-    @NotEmpty(message = "Please provide an email")
-    @Email(message = "Please provide a valid email address")
-    @Pattern(regexp = ".+@.+\\..+", message = "Please provide a valid email address")
-    private String email;
-
-    public String getName() {
-        return name;
-    }
-
-    public String getPassword() {
-        return password;
-    }
-
-    public String getEmail() {
-        return email;
-    }
-
-    public void setName(String name) {
-        this.name = name;
-    }
-
-    public void setPassword(String password) {
-        this.password = password;
-    }
-
-    public void setEmail(String email) {
-        this.email = email;
-    }
-}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/automation/BooleanConditionDTO.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/automation/BooleanConditionDTO.java
new file mode 100644
index 0000000..fd5e807
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/automation/BooleanConditionDTO.java
@@ -0,0 +1,24 @@
+package ch.usi.inf.sa4.sanmarinoes.smarthut.dto.automation;
+
+import ch.usi.inf.sa4.sanmarinoes.smarthut.models.BooleanCondition;
+import ch.usi.inf.sa4.sanmarinoes.smarthut.models.Condition;
+import javax.validation.constraints.NotNull;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+@NoArgsConstructor
+@AllArgsConstructor
+public class BooleanConditionDTO extends ConditionDTO {
+
+    @NotNull @Getter @Setter private boolean on;
+
+    @Override
+    public Condition toModel() {
+        BooleanCondition t = new BooleanCondition();
+        t.setDeviceId(this.getDeviceId());
+        t.setOn(this.on);
+        return t;
+    }
+}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/automation/BooleanTriggerDTO.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/automation/BooleanTriggerDTO.java
new file mode 100644
index 0000000..c73de71
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/automation/BooleanTriggerDTO.java
@@ -0,0 +1,23 @@
+package ch.usi.inf.sa4.sanmarinoes.smarthut.dto.automation;
+
+import ch.usi.inf.sa4.sanmarinoes.smarthut.models.BooleanTrigger;
+import ch.usi.inf.sa4.sanmarinoes.smarthut.models.Trigger;
+import javax.validation.constraints.NotNull;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+@NoArgsConstructor
+@AllArgsConstructor
+public class BooleanTriggerDTO extends TriggerDTO {
+    @NotNull @Getter @Setter private boolean on;
+
+    @Override
+    public Trigger toModel() {
+        BooleanTrigger t = new BooleanTrigger();
+        t.setDeviceId(this.getDeviceId());
+        t.setOn(this.on);
+        return t;
+    }
+}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/automation/ConditionDTO.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/automation/ConditionDTO.java
new file mode 100644
index 0000000..6a5b6b6
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/automation/ConditionDTO.java
@@ -0,0 +1,12 @@
+package ch.usi.inf.sa4.sanmarinoes.smarthut.dto.automation;
+
+import ch.usi.inf.sa4.sanmarinoes.smarthut.models.Condition;
+import javax.validation.constraints.NotNull;
+import lombok.Getter;
+import lombok.Setter;
+
+public abstract class ConditionDTO {
+    @NotNull @Getter @Setter private long deviceId;
+
+    public abstract Condition toModel();
+}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/automation/RangeConditionDTO.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/automation/RangeConditionDTO.java
new file mode 100644
index 0000000..f3cb7d6
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/automation/RangeConditionDTO.java
@@ -0,0 +1,23 @@
+package ch.usi.inf.sa4.sanmarinoes.smarthut.dto.automation;
+
+import ch.usi.inf.sa4.sanmarinoes.smarthut.models.Condition;
+import ch.usi.inf.sa4.sanmarinoes.smarthut.models.Operator;
+import ch.usi.inf.sa4.sanmarinoes.smarthut.models.RangeCondition;
+import javax.validation.constraints.NotNull;
+import lombok.Getter;
+import lombok.Setter;
+
+public class RangeConditionDTO extends ConditionDTO {
+
+    @NotNull @Getter @Setter private Operator operator;
+    @NotNull @Getter @Setter private double range;
+
+    @Override
+    public Condition toModel() {
+        RangeCondition t = new RangeCondition();
+        t.setDeviceId(this.getDeviceId());
+        t.setOperator(this.operator);
+        t.setRange(this.range);
+        return t;
+    }
+}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/automation/RangeTriggerDTO.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/automation/RangeTriggerDTO.java
new file mode 100644
index 0000000..0cab1f9
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/automation/RangeTriggerDTO.java
@@ -0,0 +1,22 @@
+package ch.usi.inf.sa4.sanmarinoes.smarthut.dto.automation;
+
+import ch.usi.inf.sa4.sanmarinoes.smarthut.models.Operator;
+import ch.usi.inf.sa4.sanmarinoes.smarthut.models.RangeTrigger;
+import ch.usi.inf.sa4.sanmarinoes.smarthut.models.Trigger;
+import javax.validation.constraints.NotNull;
+import lombok.Getter;
+import lombok.Setter;
+
+public class RangeTriggerDTO extends TriggerDTO {
+    @NotNull @Getter @Setter private Operator operator;
+    @NotNull @Getter @Setter private double range;
+
+    @Override
+    public Trigger toModel() {
+        RangeTrigger t = new RangeTrigger();
+        t.setDeviceId(this.getDeviceId());
+        t.setOperator(this.operator);
+        t.setRange(this.range);
+        return t;
+    }
+}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/automation/ScenePriorityDTO.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/automation/ScenePriorityDTO.java
new file mode 100644
index 0000000..65761cb
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/automation/ScenePriorityDTO.java
@@ -0,0 +1,26 @@
+package ch.usi.inf.sa4.sanmarinoes.smarthut.dto.automation;
+
+import ch.usi.inf.sa4.sanmarinoes.smarthut.models.ScenePriority;
+import javax.validation.constraints.Min;
+import javax.validation.constraints.NotNull;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+@NoArgsConstructor
+@AllArgsConstructor
+public class ScenePriorityDTO {
+    @NotNull @Getter @Setter private long sceneId;
+
+    @NotNull
+    @Min(0)
+    private @Getter @Setter Integer priority;
+
+    public ScenePriority toModel() {
+        ScenePriority s = new ScenePriority();
+        s.setSceneId(sceneId);
+        s.setPriority(priority);
+        return s;
+    }
+}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/automation/ThermostatConditionDTO.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/automation/ThermostatConditionDTO.java
new file mode 100644
index 0000000..4ef455b
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/automation/ThermostatConditionDTO.java
@@ -0,0 +1,29 @@
+package ch.usi.inf.sa4.sanmarinoes.smarthut.dto.automation;
+
+import ch.usi.inf.sa4.sanmarinoes.smarthut.models.Condition;
+import ch.usi.inf.sa4.sanmarinoes.smarthut.models.Thermostat;
+import ch.usi.inf.sa4.sanmarinoes.smarthut.models.ThermostatCondition;
+import javax.validation.constraints.NotNull;
+import lombok.Getter;
+import lombok.Setter;
+
+public class ThermostatConditionDTO extends ConditionDTO {
+
+    @NotNull @Getter @Setter private ThermostatCondition.Operator operator;
+
+    @NotNull @Getter @Setter private Thermostat.Mode mode;
+
+    @Override
+    public Condition toModel() {
+
+        ThermostatCondition t = new ThermostatCondition();
+
+        t.setDeviceId(this.getDeviceId());
+
+        t.setOperator(this.operator);
+
+        t.setMode(this.mode);
+
+        return t;
+    }
+}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/automation/TriggerDTO.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/automation/TriggerDTO.java
new file mode 100644
index 0000000..b5562fa
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/automation/TriggerDTO.java
@@ -0,0 +1,13 @@
+package ch.usi.inf.sa4.sanmarinoes.smarthut.dto.automation;
+
+import ch.usi.inf.sa4.sanmarinoes.smarthut.models.Trigger;
+import ch.usi.inf.sa4.sanmarinoes.smarthut.models.Triggerable;
+import javax.validation.constraints.NotNull;
+import lombok.Getter;
+import lombok.Setter;
+
+public abstract class TriggerDTO {
+    @NotNull @Getter @Setter private long deviceId;
+
+    public abstract Trigger toModel();
+}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/error/UnauthorizedException.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/error/UnauthorizedException.java
index 9176df6..53b8620 100644
--- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/error/UnauthorizedException.java
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/error/UnauthorizedException.java
@@ -7,8 +7,8 @@ import org.springframework.web.bind.annotation.ResponseStatus;
 public class UnauthorizedException extends Exception {
     private final boolean isUserDisabled;
 
-    public UnauthorizedException(boolean isDisabled) {
-        super("Access denied: " + (isDisabled ? "user is disabled" : "wrong credentials"));
+    public UnauthorizedException(boolean isDisabled, Throwable cause) {
+        super("Access denied: " + (isDisabled ? "user is disabled" : "wrong credentials"), cause);
         this.isUserDisabled = isDisabled;
     }
 
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Automation.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Automation.java
index e6b4d0b..2a27ee9 100644
--- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Automation.java
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Automation.java
@@ -6,8 +6,9 @@ import java.util.HashSet;
 import java.util.Set;
 import javax.persistence.*;
 import javax.validation.constraints.NotEmpty;
-import javax.validation.constraints.NotNull;
+import lombok.Data;
 
+@Data
 @Entity
 public class Automation {
 
@@ -17,65 +18,23 @@ public class Automation {
     @ApiModelProperty(hidden = true)
     private long id;
 
-    @ManyToOne
-    @JoinColumn(name = "user_id", updatable = false, insertable = false)
-    @GsonExclude
-    private User user;
-
-    @NotNull
-    @Column(name = "user_id", nullable = false)
-    @GsonExclude
-    private Long userId;
-
-    @OneToMany(mappedBy = "automation", orphanRemoval = true)
+    @OneToMany(mappedBy = "automation", orphanRemoval = true, cascade = CascadeType.REMOVE)
     private Set> triggers = new HashSet<>();
 
     @OneToMany(mappedBy = "automation", cascade = CascadeType.REMOVE)
     private Set scenes = new HashSet<>();
 
-    @NotNull @NotEmpty private String name;
+    @ManyToOne
+    @JoinColumn(name = "user_id", updatable = false, insertable = false)
+    @GsonExclude
+    private User user;
 
-    public long getId() {
-        return id;
-    }
+    @OneToMany(mappedBy = "automation", orphanRemoval = true, cascade = CascadeType.REMOVE)
+    private Set> conditions = new HashSet<>();
 
-    public void setId(long id) {
-        this.id = id;
-    }
+    @Column(name = "user_id", nullable = false)
+    @GsonExclude
+    private Long userId;
 
-    public Set> getStates() {
-        return triggers;
-    }
-
-    public Set getScenes() {
-        return scenes;
-    }
-
-    public Set> getTriggers() {
-        return triggers;
-    }
-
-    public String getName() {
-        return name;
-    }
-
-    public void setName(String name) {
-        this.name = name;
-    }
-
-    public User getUser() {
-        return user;
-    }
-
-    public void setUser(User user) {
-        this.user = user;
-    }
-
-    public Long getUserId() {
-        return userId;
-    }
-
-    public void setUserId(Long userId) {
-        this.userId = userId;
-    }
+    @NotEmpty private String name;
 }
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/AutomationRepository.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/AutomationRepository.java
index b874146..afdfab3 100644
--- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/AutomationRepository.java
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/AutomationRepository.java
@@ -7,9 +7,9 @@ import org.springframework.data.repository.CrudRepository;
 import org.springframework.data.repository.query.Param;
 
 public interface AutomationRepository extends CrudRepository {
-    @EntityGraph(attributePaths = {"scenes", "triggers"})
+    @EntityGraph(attributePaths = {"scenes", "triggers", "conditions"})
     List findAllByUserId(@Param("userId") long userId);
 
-    @EntityGraph(attributePaths = {"scenes", "triggers"})
+    @EntityGraph(attributePaths = {"scenes", "triggers", "conditions"})
     Optional findByIdAndUserId(@Param("id") long id, @Param("userId") long userId);
 }
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/BooleanCondition.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/BooleanCondition.java
new file mode 100644
index 0000000..30d47ae
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/BooleanCondition.java
@@ -0,0 +1,26 @@
+package ch.usi.inf.sa4.sanmarinoes.smarthut.models;
+
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.Setter;
+
+@Entity
+@EqualsAndHashCode(callSuper = true)
+public class BooleanCondition extends Condition {
+
+    @Getter
+    @Setter
+    @Column(name = "switchable_on")
+    private boolean on;
+
+    public BooleanCondition() {
+        super("booleanCondition");
+    }
+
+    @Override
+    public boolean triggered() {
+        return this.getDevice().readTriggerState() == isOn();
+    }
+}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/BooleanConditionRepository.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/BooleanConditionRepository.java
new file mode 100644
index 0000000..539eb6d
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/BooleanConditionRepository.java
@@ -0,0 +1,9 @@
+package ch.usi.inf.sa4.sanmarinoes.smarthut.models;
+
+import java.util.List;
+import org.springframework.data.repository.query.Param;
+
+public interface BooleanConditionRepository extends ConditionRepository {
+
+    List findAllByAutomationId(@Param("automationId") long automationId);
+}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/BooleanTrigger.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/BooleanTrigger.java
index f6e493e..50a86b1 100644
--- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/BooleanTrigger.java
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/BooleanTrigger.java
@@ -2,29 +2,21 @@ package ch.usi.inf.sa4.sanmarinoes.smarthut.models;
 
 import javax.persistence.Column;
 import javax.persistence.Entity;
+import lombok.Getter;
+import lombok.Setter;
 
 @Entity
-public class BooleanTrigger extends Trigger {
+public class BooleanTrigger extends Trigger {
 
     @Column(name = "switchable_on")
+    @Getter
+    @Setter
     private boolean on;
 
     public BooleanTrigger() {
         super("booleanTrigger");
     }
 
-    public boolean isOn() {
-        return on;
-    }
-
-    public void setOn(boolean on) {
-        this.on = on;
-    }
-
-    public boolean check(boolean on) {
-        return this.on == on;
-    }
-
     @Override
     public boolean triggered() {
         return getDevice().readTriggerState() == isOn();
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/BooleanTriggerRepository.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/BooleanTriggerRepository.java
index 08b8898..4f9263c 100644
--- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/BooleanTriggerRepository.java
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/BooleanTriggerRepository.java
@@ -3,8 +3,7 @@ package ch.usi.inf.sa4.sanmarinoes.smarthut.models;
 import java.util.List;
 import org.springframework.data.repository.query.Param;
 
-public interface BooleanTriggerRepository
-        extends TriggerRepository> {
+public interface BooleanTriggerRepository extends TriggerRepository {
 
-    List> findAllByAutomationId(@Param("automationId") long automationId);
+    List findAllByAutomationId(@Param("automationId") long automationId);
 }
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/BooleanTriggerable.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/BooleanTriggerable.java
index 6ff219f..9132f36 100644
--- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/BooleanTriggerable.java
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/BooleanTriggerable.java
@@ -1,5 +1,5 @@
 package ch.usi.inf.sa4.sanmarinoes.smarthut.models;
 
-public interface BooleanTriggerable {
+public interface BooleanTriggerable extends Triggerable {
     boolean readTriggerState();
 }
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/ButtonDimmer.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/ButtonDimmer.java
index e41265a..94628ba 100644
--- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/ButtonDimmer.java
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/ButtonDimmer.java
@@ -18,14 +18,14 @@ public class ButtonDimmer extends Dimmer {
 
     /** Increases the current intensity level of the dimmable light by DIM_INCREMENT */
     public void increaseIntensity() {
-        for (Dimmable dl : getOutputs()) {
+        for (Dimmable dl : getDimmables()) {
             dl.setIntensity(dl.getIntensity() + DIM_INCREMENT);
         }
     }
 
     /** Decreases the current intensity level of the dimmable light by DIM_INCREMENT */
     public void decreaseIntensity() {
-        for (Dimmable dl : getOutputs()) {
+        for (Dimmable dl : getDimmables()) {
             dl.setIntensity(dl.getIntensity() - DIM_INCREMENT);
         }
     }
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Condition.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Condition.java
new file mode 100644
index 0000000..62fa6df
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Condition.java
@@ -0,0 +1,57 @@
+package ch.usi.inf.sa4.sanmarinoes.smarthut.models;
+
+import ch.usi.inf.sa4.sanmarinoes.smarthut.config.GsonExclude;
+import io.swagger.annotations.ApiModelProperty;
+import javax.persistence.*;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+@Data
+@Entity
+@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
+public abstract class Condition {
+
+    @Transient private String kind;
+
+    protected Condition(String kind) {
+        this.kind = kind;
+    }
+
+    public String getKind() {
+        return kind;
+    }
+
+    @ManyToOne(targetEntity = Device.class)
+    @JoinColumn(name = "device_id", updatable = false, insertable = false)
+    @GsonExclude
+    private D device;
+
+    @Column(name = "automation_id", nullable = false)
+    private Long automationId;
+
+    @Id
+    @GeneratedValue(strategy = GenerationType.AUTO)
+    @Column(name = "id", updatable = false, nullable = false, unique = true)
+    @ApiModelProperty(hidden = true)
+    private long id;
+
+    @ManyToOne
+    @JoinColumn(name = "automation_id", updatable = false, insertable = false)
+    @GsonExclude
+    @EqualsAndHashCode.Exclude
+    private Automation automation;
+
+    /**
+     * The device this condition belongs to, as a foreign key id. To use when updating and inserting
+     * from a REST call.
+     */
+    @Column(name = "device_id", nullable = false)
+    private Long deviceId;
+
+    public abstract boolean triggered();
+
+    public Condition setAutomationId(Long automationId) {
+        this.automationId = automationId;
+        return this;
+    }
+}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/ConditionRepository.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/ConditionRepository.java
new file mode 100644
index 0000000..2f7a1cc
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/ConditionRepository.java
@@ -0,0 +1,16 @@
+package ch.usi.inf.sa4.sanmarinoes.smarthut.models;
+
+import java.util.List;
+import javax.transaction.Transactional;
+import org.springframework.data.repository.CrudRepository;
+import org.springframework.data.repository.query.Param;
+
+public interface ConditionRepository> extends CrudRepository {
+
+    List findAllByDeviceId(@Param("deviceId") long deviceId);
+
+    List findAllByAutomationId(@Param("automationId") long automationId);
+
+    @Transactional
+    void deleteAllByAutomationId(@Param("automationId") long automationId);
+}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/ConfirmationToken.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/ConfirmationToken.java
index d324724..7d73b41 100644
--- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/ConfirmationToken.java
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/ConfirmationToken.java
@@ -1,20 +1,15 @@
 package ch.usi.inf.sa4.sanmarinoes.smarthut.models;
 
 import java.util.Date;
+import java.util.Objects;
 import java.util.UUID;
-import javax.persistence.Column;
-import javax.persistence.Entity;
-import javax.persistence.FetchType;
-import javax.persistence.GeneratedValue;
-import javax.persistence.GenerationType;
-import javax.persistence.Id;
-import javax.persistence.JoinColumn;
-import javax.persistence.OneToOne;
-import javax.persistence.Temporal;
-import javax.persistence.TemporalType;
+import javax.persistence.*;
+import lombok.Data;
+import lombok.NonNull;
 
+@Data
 @Entity
-public class ConfirmationToken {
+public final class ConfirmationToken {
 
     @Id
     @GeneratedValue(strategy = GenerationType.AUTO)
@@ -22,9 +17,18 @@ public class ConfirmationToken {
     private Long id;
 
     @Column(name = "confirmation_token", unique = true)
-    private String confirmationToken;
+    private String confirmToken;
+
+    public Date getCreatedDate() {
+        return new Date(createdDate.getTime());
+    }
+
+    public void setCreatedDate(Date createdDate) {
+        this.createdDate = new Date(createdDate.getTime());
+    }
 
     @Temporal(TemporalType.TIMESTAMP)
+    @NonNull
     private Date createdDate;
 
     @OneToOne(targetEntity = User.class, fetch = FetchType.EAGER)
@@ -32,55 +36,32 @@ public class ConfirmationToken {
     private User user;
 
     @Column(nullable = false)
-    private Boolean resetPassword;
+    private boolean resetPassword;
 
     public ConfirmationToken(User user) {
         this.user = user;
         createdDate = new Date();
-        confirmationToken = UUID.randomUUID().toString();
+        confirmToken = UUID.randomUUID().toString();
         resetPassword = false;
     }
 
-    /** Constructor for hibernate reflective stuff things whatever */
-    public ConfirmationToken() {}
-
-    public Long getId() {
-        return id;
+    public ConfirmationToken() {
+        this((User) null);
     }
 
-    public String getConfirmationToken() {
-        return confirmationToken;
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        ConfirmationToken that = (ConfirmationToken) o;
+        return resetPassword == that.resetPassword
+                && confirmToken.equals(that.confirmToken)
+                && createdDate.equals(that.createdDate)
+                && Objects.equals(user, that.user);
     }
 
-    public Date getCreatedDate() {
-        return createdDate;
-    }
-
-    public User getUser() {
-        return user;
-    }
-
-    public void setId(Long id) {
-        this.id = id;
-    }
-
-    public void setConfirmationToken(String confirmationToken) {
-        this.confirmationToken = confirmationToken;
-    }
-
-    public void setCreatedDate(Date createdDate) {
-        this.createdDate = createdDate;
-    }
-
-    public void setUser(User user) {
-        this.user = user;
-    }
-
-    public Boolean getResetPassword() {
-        return resetPassword;
-    }
-
-    public void setResetPassword(Boolean resetPassword) {
-        this.resetPassword = resetPassword;
+    @Override
+    public int hashCode() {
+        return Objects.hash(confirmToken, createdDate, user, resetPassword);
     }
 }
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/ConfirmationTokenRepository.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/ConfirmationTokenRepository.java
index 40c6a17..851be41 100644
--- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/ConfirmationTokenRepository.java
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/ConfirmationTokenRepository.java
@@ -4,7 +4,7 @@ import javax.transaction.Transactional;
 import org.springframework.data.repository.CrudRepository;
 
 public interface ConfirmationTokenRepository extends CrudRepository {
-    ConfirmationToken findByConfirmationToken(String confirmationToken);
+    ConfirmationToken findByConfirmToken(String confirmToken);
 
     ConfirmationToken findByUser(User user);
 
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Connectable.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Connectable.java
new file mode 100644
index 0000000..8423930
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Connectable.java
@@ -0,0 +1,5 @@
+package ch.usi.inf.sa4.sanmarinoes.smarthut.models;
+
+public interface Connectable {
+    void connect(O output, boolean connect);
+}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Connector.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Connector.java
deleted file mode 100644
index 701a010..0000000
--- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Connector.java
+++ /dev/null
@@ -1,47 +0,0 @@
-package ch.usi.inf.sa4.sanmarinoes.smarthut.models;
-
-import java.util.Set;
-import java.util.function.BiConsumer;
-import java.util.function.Function;
-
-/**
- * A rule on how to connect an input device type to an output device type
- *
- * @param  the input device type
- * @param  the output device type
- */
-@FunctionalInterface
-public interface Connector {
-
-    /**
-     * Connects or disconnects input to output
-     *
-     * @param input the input device
-     * @param output the output device
-     * @param connect true if connection, false if disconnection
-     */
-    void connect(I input, O output, boolean connect);
-
-    /**
-     * Produces a basic implementation of a connector, assuming there is a ManyToMany relationship
-     * between J and K
-     *
-     * @param outputsGetter the getter method of the set of outputs on the input class
-     * @param inputsGetter the getter method of the set of outputs on the input class
-     * @param  the input device type
-     * @param  the output device type
-     * @return a Connector implementation for the pair of types J and K
-     */
-    static  Connector basic(
-            Function> outputsGetter, Function> inputsGetter) {
-        return (i, o, connect) -> {
-            if (connect) {
-                outputsGetter.apply(i).add(o);
-                inputsGetter.apply(o).add(i);
-            } else {
-                outputsGetter.apply(i).remove(o);
-                inputsGetter.apply(o).remove(i);
-            }
-        };
-    }
-}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Curtains.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Curtains.java
index d9e8d98..af2fd40 100644
--- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Curtains.java
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Curtains.java
@@ -3,8 +3,8 @@ package ch.usi.inf.sa4.sanmarinoes.smarthut.models;
 import javax.persistence.Entity;
 
 /**
- * Represents a curtain. The intensity represents how much the curtains are opened,
- * 0 is completely closed 100 is completely open
+ * Represents a curtain. The intensity represents how much the curtains are opened, 0 is completely
+ * closed 100 is completely open
  */
 @Entity
 public class Curtains extends Dimmable {
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Device.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Device.java
index 397bb1e..0f99d0d 100644
--- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Device.java
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Device.java
@@ -4,13 +4,13 @@ import ch.usi.inf.sa4.sanmarinoes.smarthut.config.GsonExclude;
 import ch.usi.inf.sa4.sanmarinoes.smarthut.config.SocketGsonExclude;
 import com.google.gson.annotations.SerializedName;
 import io.swagger.annotations.ApiModelProperty;
-import java.util.HashSet;
 import java.util.Set;
 import javax.persistence.*;
-import javax.validation.constraints.NotNull;
+import lombok.Data;
 
 /** Generic abstraction for a smart home device */
 @Entity
+@Data
 @Inheritance(strategy = InheritanceType.JOINED)
 public abstract class Device {
 
@@ -39,18 +39,16 @@ public abstract class Device {
     @OneToMany(mappedBy = "device", orphanRemoval = true)
     @GsonExclude
     @SocketGsonExclude
-    private Set> triggers = new HashSet<>();
+    private Set> triggers;
 
     /**
      * The room this device belongs in, as a foreign key id. To use when updating and inserting from
      * a REST call.
      */
     @Column(name = "room_id", nullable = false)
-    @NotNull
     private Long roomId;
 
     /** The name of the device as assigned by the user (e.g. 'Master bedroom light') */
-    @NotNull
     @Column(nullable = false)
     private String name;
 
@@ -69,63 +67,15 @@ public abstract class Device {
     @OneToMany(mappedBy = "device", orphanRemoval = true)
     @GsonExclude
     @SocketGsonExclude
-    private Set> states = new HashSet<>();
+    private Set states;
 
-    @Transient @GsonExclude private boolean fromHost = false;
+    @Transient @GsonExclude private Long fromHostId = null;
 
     @Transient @GsonExclude private boolean fromGuest = false;
 
     @Transient @GsonExclude private boolean deleted = false;
 
-    public boolean isFromHost() {
-        return fromHost;
-    }
-
-    public boolean isDeleted() {
-        return deleted;
-    }
-
-    public void setDeleted(boolean deleted) {
-        this.deleted = deleted;
-    }
-
-    public boolean isFromGuest() {
-        return fromGuest;
-    }
-
-    public void setFromGuest(boolean fromGuest) {
-        this.fromGuest = fromGuest;
-    }
-
-    public void setFromHost(boolean fromHost) {
-        this.fromHost = fromHost;
-    }
-
-    public long getId() {
-        return id;
-    }
-
-    public void setId(long id) {
-        this.id = id;
-    }
-
-    public String getName() {
-        return name;
-    }
-
-    public void setName(String name) {
-        this.name = name;
-    }
-
-    public Long getRoomId() {
-        return roomId;
-    }
-
-    public void setRoomId(Long roomId) {
-        this.roomId = roomId;
-    }
-
-    public Device(String kind, FlowType flowType) {
+    protected Device(String kind, FlowType flowType) {
         this.kind = kind;
         this.flowType = flowType;
     }
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Dimmable.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Dimmable.java
index 6ae3150..111537a 100644
--- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Dimmable.java
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Dimmable.java
@@ -2,22 +2,20 @@ package ch.usi.inf.sa4.sanmarinoes.smarthut.models;
 
 import ch.usi.inf.sa4.sanmarinoes.smarthut.config.GsonExclude;
 import ch.usi.inf.sa4.sanmarinoes.smarthut.config.SocketGsonExclude;
+import java.util.HashSet;
 import java.util.Set;
 import javax.persistence.*;
 import javax.validation.constraints.Max;
 import javax.validation.constraints.Min;
-import javax.validation.constraints.NotNull;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.Setter;
 
 @Entity
+@EqualsAndHashCode(callSuper = true)
 @Inheritance(strategy = InheritanceType.SINGLE_TABLE)
 public class Dimmable extends Switchable implements RangeTriggerable {
 
-    public static final Connector KNOB_DIMMER_DIMMABLE_CONNECTOR =
-            Connector.basic(KnobDimmer::getOutputs, Dimmable::getDimmers);
-
-    public static final Connector BUTTON_DIMMER_DIMMABLE_CONNECTOR =
-            Connector.basic(ButtonDimmer::getOutputs, Dimmable::getDimmers);
-
     protected Dimmable(String kind) {
         super(kind);
     }
@@ -25,23 +23,23 @@ public class Dimmable extends Switchable implements RangeTriggerable {
     @ManyToMany(mappedBy = "dimmables", cascade = CascadeType.DETACH)
     @GsonExclude
     @SocketGsonExclude
-    private Set dimmers;
+    @EqualsAndHashCode.Exclude
+    @Getter
+    @Setter
+    private Set dimmers = new HashSet<>();
 
     /** The light intensity value. Goes from 0 (off) to 100 (on) */
-    @NotNull
     @Column(nullable = false)
     @Min(0)
     @Max(100)
+    @Getter
     private Integer intensity = 0;
 
-    @NotNull
     @Column(nullable = false)
+    @Getter
+    @Setter
     private Integer oldIntensity = 100;
 
-    public Integer getIntensity() {
-        return intensity;
-    }
-
     /**
      * Sets the intensity to a certain level. Out of bound values are corrected to the respective
      * extremums. An intensity level of 0 turns the light off, but keeps the old intensity level
@@ -71,19 +69,14 @@ public class Dimmable extends Switchable implements RangeTriggerable {
         intensity = on ? oldIntensity : 0;
     }
 
-    public Set getDimmers() {
-        return this.dimmers;
-    }
-
-    public void readStateAndSet(DimmableState state) {
+    public void readStateAndSet(DimmableState state) {
         setIntensity(state.getIntensity());
     }
 
     @Override
-    public State cloneState() {
-        final DimmableState newState = new DimmableState<>();
+    public State cloneState() {
+        final DimmableState newState = new DimmableState();
         newState.setDeviceId(getId());
-        newState.setDevice(this);
         newState.setIntensity(getIntensity());
         return newState;
     }
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/DimmableLight.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/DimmableLight.java
index 89806a7..e4fa18d 100644
--- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/DimmableLight.java
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/DimmableLight.java
@@ -1,6 +1,6 @@
 package ch.usi.inf.sa4.sanmarinoes.smarthut.models;
 
-import javax.persistence.*;
+import javax.persistence.Entity;
 
 /** Represent a dimmable light */
 @Entity
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/DimmableRepository.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/DimmableRepository.java
index be791c9..dffbbdb 100644
--- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/DimmableRepository.java
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/DimmableRepository.java
@@ -1,4 +1,3 @@
 package ch.usi.inf.sa4.sanmarinoes.smarthut.models;
 
-public interface DimmableRepository extends SwitchableRepository {
-}
+public interface DimmableRepository extends SwitchableRepository {}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/DimmableState.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/DimmableState.java
index 7c0b8d2..c9cceec 100644
--- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/DimmableState.java
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/DimmableState.java
@@ -3,26 +3,33 @@ package ch.usi.inf.sa4.sanmarinoes.smarthut.models;
 import javax.persistence.Entity;
 import javax.validation.constraints.Max;
 import javax.validation.constraints.Min;
+import lombok.Getter;
+import lombok.Setter;
 
 /** Represent a state for an IDimmable device */
 @Entity
-public class DimmableState extends State {
+public class DimmableState extends State {
+
+    public void setDevice(Dimmable device) {
+        setInnerDevice(device);
+    }
 
     /** The light intensity value. Goes from 0 (off) to 100 (on) */
     @Min(0)
     @Max(100)
+    @Getter
+    @Setter
     private int intensity = 0;
 
-    public int getIntensity() {
-        return intensity;
-    }
-
-    public void setIntensity(int dimAmount) {
-        this.intensity = dimAmount;
-    }
-
     @Override
     public void apply() {
-        getDevice().readStateAndSet(this);
+        ((Dimmable) getDevice()).readStateAndSet(this);
+    }
+
+    @Override
+    protected DimmableState copy() {
+        final DimmableState d = new DimmableState();
+        d.setIntensity(intensity);
+        return d;
     }
 }
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/DimmableStateRepository.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/DimmableStateRepository.java
index 00edb96..8cced24 100644
--- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/DimmableStateRepository.java
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/DimmableStateRepository.java
@@ -1,3 +1,3 @@
 package ch.usi.inf.sa4.sanmarinoes.smarthut.models;
 
-public interface DimmableStateRepository extends StateRepository> {}
+public interface DimmableStateRepository extends StateRepository {}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Dimmer.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Dimmer.java
index e920d2f..c2185ee 100644
--- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Dimmer.java
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Dimmer.java
@@ -5,18 +5,23 @@ import ch.usi.inf.sa4.sanmarinoes.smarthut.config.SocketGsonExclude;
 import java.util.HashSet;
 import java.util.Set;
 import javax.persistence.*;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
 
 /** Represents a generic dimmer input device */
 @Entity
+@EqualsAndHashCode(callSuper = true)
 @Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
-public abstract class Dimmer extends InputDevice {
-    public Dimmer(String kind) {
+public abstract class Dimmer extends InputDevice implements Connectable {
+    protected Dimmer(String kind) {
         super(kind);
     }
 
     @ManyToMany(cascade = CascadeType.DETACH)
     @GsonExclude
     @SocketGsonExclude
+    @EqualsAndHashCode.Exclude
+    @Getter
     @JoinTable(
             name = "dimmer_dimmable",
             joinColumns = @JoinColumn(name = "dimmer_id"),
@@ -29,12 +34,22 @@ public abstract class Dimmer extends InputDevice {
      * @return duh
      */
     @Override
-    public Set getOutputs() {
-        return this.dimmables;
+    public Set getOutputs() {
+        return Set.copyOf(this.dimmables);
     }
 
     /** Add a light to be controller by this dimmer */
     public void addDimmable(Dimmable dimmable) {
         dimmables.add(dimmable);
     }
+
+    public void connect(Dimmable output, boolean connect) {
+        if (connect) {
+            output.getDimmers().add(this);
+            dimmables.add(output);
+        } else {
+            output.getDimmers().remove(this);
+            dimmables.remove(output);
+        }
+    }
 }
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Icon.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Icon.java
new file mode 100644
index 0000000..0ad9e4d
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Icon.java
@@ -0,0 +1,103 @@
+package ch.usi.inf.sa4.sanmarinoes.smarthut.models;
+
+import com.google.gson.annotations.SerializedName;
+
+/** A collection of Semantic UI icons */
+@SuppressWarnings("unused")
+public enum Icon {
+    @SerializedName("home")
+    HOME("home"),
+    @SerializedName("coffee")
+    COFFEE("coffee"),
+    @SerializedName("beer")
+    BEER("beer"),
+    @SerializedName("glass martini")
+    GLASS_MARTINI("glass martini"),
+    @SerializedName("film")
+    FILM("film"),
+    @SerializedName("video")
+    VIDEO("video"),
+    @SerializedName("music")
+    MUSIC("music"),
+    @SerializedName("headphones")
+    HEADPHONES("headphones"),
+    @SerializedName("fax")
+    FAX("fax"),
+    @SerializedName("phone")
+    PHONE("phone"),
+    @SerializedName("laptop")
+    LAPTOP("laptop"),
+    @SerializedName("bath")
+    BATH("bath"),
+    @SerializedName("shower")
+    SHOWER("shower"),
+    @SerializedName("bed")
+    BED("bed"),
+    @SerializedName("child")
+    CHILD("child"),
+    @SerializedName("warehouse")
+    WAREHOUSE("warehouse"),
+    @SerializedName("car")
+    CAR("car"),
+    @SerializedName("bicycle")
+    BICYCLE("bicycle"),
+    @SerializedName("motorcycle")
+    MOTORCYCLE("motorcycle"),
+    @SerializedName("archive")
+    ARCHIVE("archive"),
+    @SerializedName("boxes")
+    BOXES("boxes"),
+    @SerializedName("cubes")
+    CUBES("cubes"),
+    @SerializedName("chess")
+    CHESS("chess"),
+    @SerializedName("gamepad")
+    GAMEPAD("gamepad"),
+    @SerializedName("futbol")
+    FUTBOL("futbol"),
+    @SerializedName("table tennis")
+    TABLE_TENNIS("table tennis"),
+    @SerializedName("server")
+    SERVER("server"),
+    @SerializedName("tv")
+    TV("tv"),
+    @SerializedName("heart")
+    HEART("heart"),
+    @SerializedName("camera")
+    CAMERA("camera"),
+    @SerializedName("trophy")
+    TROPHY("trophy"),
+    @SerializedName("wrench")
+    WRENCH("wrench"),
+    @SerializedName("image")
+    IMAGE("image"),
+    @SerializedName("book")
+    BOOK("book"),
+    @SerializedName("university")
+    UNIVERSITY("university"),
+    @SerializedName("medkit")
+    MEDKIT("medkit"),
+    @SerializedName("paw")
+    PAW("paw"),
+    @SerializedName("tree")
+    TREE("tree"),
+    @SerializedName("utensils")
+    UTENSILS("utensils"),
+    @SerializedName("male")
+    MALE("male"),
+    @SerializedName("female")
+    FEMALE("female"),
+    @SerializedName("life ring outline")
+    LIFE_RING_OUTLINE("life ring outline");
+
+    private String iconName;
+
+    Icon(String s) {
+        this.iconName = s;
+    }
+
+    @Override
+    public String toString() {
+        return iconName;
+    }
+}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/InputDevice.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/InputDevice.java
index 2acf0c2..3176a2c 100644
--- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/InputDevice.java
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/InputDevice.java
@@ -16,7 +16,7 @@ public abstract class InputDevice extends Device {
         super(kind, FlowType.INPUT);
     }
 
-    public Set getOutputs() {
+    public Set getOutputs() {
         return Set.of();
     }
 }
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/KnobDimmer.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/KnobDimmer.java
index 89273f8..479e58c 100644
--- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/KnobDimmer.java
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/KnobDimmer.java
@@ -2,12 +2,14 @@ package ch.usi.inf.sa4.sanmarinoes.smarthut.models;
 
 import javax.persistence.Column;
 import javax.persistence.Entity;
+import lombok.EqualsAndHashCode;
 
 /**
  * Represents a dimmer able to set absolute intensity values (i.e. knowing the absolute intensity
  * value, like a knob)
  */
 @Entity
+@EqualsAndHashCode(callSuper = true)
 public class KnobDimmer extends Dimmer implements RangeTriggerable {
 
     @Column private int intensity = 0;
@@ -23,7 +25,7 @@ public class KnobDimmer extends Dimmer implements RangeTriggerable {
      */
     public void setLightIntensity(int intensity) {
         this.intensity = intensity;
-        for (Dimmable dl : getOutputs()) {
+        for (Dimmable dl : getDimmables()) {
             dl.setIntensity(intensity);
         }
     }
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/MotionSensor.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/MotionSensor.java
index 4998922..cd27d59 100644
--- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/MotionSensor.java
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/MotionSensor.java
@@ -2,22 +2,20 @@ package ch.usi.inf.sa4.sanmarinoes.smarthut.models;
 
 import javax.persistence.Column;
 import javax.persistence.Entity;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.Setter;
 
 /** Represents a motion sensor device */
 @Entity
+@EqualsAndHashCode(callSuper = true)
 public class MotionSensor extends InputDevice implements BooleanTriggerable {
 
+    @Getter
+    @Setter
     @Column(nullable = false)
     private boolean detected;
 
-    public boolean isDetected() {
-        return detected;
-    }
-
-    public void setDetected(boolean detected) {
-        this.detected = detected;
-    }
-
     public MotionSensor() {
         super("motionSensor");
     }
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Operator.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Operator.java
new file mode 100644
index 0000000..3c255f2
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Operator.java
@@ -0,0 +1,33 @@
+package ch.usi.inf.sa4.sanmarinoes.smarthut.models;
+
+import com.google.gson.annotations.SerializedName;
+
+public enum Operator {
+    @SerializedName("EQUAL")
+    EQUAL,
+    @SerializedName("LESS")
+    LESS,
+    @SerializedName("GREATER")
+    GREATER,
+    @SerializedName("LESS_EQUAL")
+    LESS_EQUAL,
+    @SerializedName("GREATER_EQUAL")
+    GREATER_EQUAL;
+
+    boolean checkAgainst(double value, double range) {
+        switch (this) {
+            case EQUAL:
+                return value == range;
+            case LESS:
+                return value < range;
+            case GREATER:
+                return value > range;
+            case GREATER_EQUAL:
+                return value >= range;
+            case LESS_EQUAL:
+                return value <= range;
+            default:
+                throw new IllegalStateException();
+        }
+    }
+}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/OutputDevice.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/OutputDevice.java
index ba1813e..1e8cde9 100644
--- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/OutputDevice.java
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/OutputDevice.java
@@ -1,6 +1,8 @@
 package ch.usi.inf.sa4.sanmarinoes.smarthut.models;
 
-import javax.persistence.*;
+import javax.persistence.Entity;
+import javax.persistence.Inheritance;
+import javax.persistence.InheritanceType;
 
 /**
  * Represents a generic output device, i.e. something that causes some behaviour (light, smartPlugs,
@@ -14,10 +16,10 @@ public abstract class OutputDevice extends Device {
     }
 
     /**
-     * Creates a State object initialized to point at this device and with values copied from
-     * this device's state
+     * Creates a State object initialized to point at this device and with values copied from this
+     * device's state
      *
-     * @return a new State object
+     * @return a new State object
      */
-    public abstract State cloneState();
+    public abstract State cloneState();
 }
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/RangeCondition.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/RangeCondition.java
new file mode 100644
index 0000000..1a92fa9
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/RangeCondition.java
@@ -0,0 +1,31 @@
+package ch.usi.inf.sa4.sanmarinoes.smarthut.models;
+
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.Setter;
+
+@Entity
+@EqualsAndHashCode(callSuper = true)
+public class RangeCondition extends Condition {
+
+    public RangeCondition() {
+        super("rangeCondition");
+    }
+
+    @Getter
+    @Setter
+    @Column(nullable = false)
+    private double range;
+
+    @Getter
+    @Setter
+    @Column(nullable = false)
+    private Operator operator;
+
+    @Override
+    public boolean triggered() {
+        return operator.checkAgainst(getDevice().readTriggerState(), range);
+    }
+}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/RangeConditionRepository.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/RangeConditionRepository.java
new file mode 100644
index 0000000..451d4a7
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/RangeConditionRepository.java
@@ -0,0 +1,8 @@
+package ch.usi.inf.sa4.sanmarinoes.smarthut.models;
+
+import java.util.List;
+import org.springframework.data.repository.query.Param;
+
+public interface RangeConditionRepository extends ConditionRepository {
+    List findAllByAutomationId(@Param("automationId") long automationId);
+}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/RangeTrigger.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/RangeTrigger.java
index 19e179c..2450616 100644
--- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/RangeTrigger.java
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/RangeTrigger.java
@@ -1,69 +1,29 @@
 package ch.usi.inf.sa4.sanmarinoes.smarthut.models;
 
-import com.google.gson.annotations.SerializedName;
 import javax.persistence.Column;
 import javax.persistence.Entity;
-import javax.validation.constraints.NotNull;
+import lombok.Getter;
+import lombok.Setter;
 
 @Entity
-public class RangeTrigger extends Trigger {
+public class RangeTrigger extends Trigger {
 
     public RangeTrigger() {
         super("rangeTrigger");
     }
 
-    @Override
-    public boolean triggered() {
-        double value = getDevice().readTriggerState();
-        switch (operator) {
-            case EQUAL:
-                return value == range;
-            case LESS:
-                return value < range;
-            case GREATER:
-                return value > range;
-            case GREATER_EQUAL:
-                return value >= range;
-            case LESS_EQUAL:
-                return value <= range;
-        }
-        throw new IllegalStateException();
-    }
-
-    public enum Operator {
-        @SerializedName("EQUAL")
-        EQUAL,
-        @SerializedName("LESS")
-        LESS,
-        @SerializedName("GREATER")
-        GREATER,
-        @SerializedName("LESS_EQUAL")
-        LESS_EQUAL,
-        @SerializedName("GREATER_EQUAL")
-        GREATER_EQUAL
-    }
-
-    @NotNull
+    @Getter
+    @Setter
     @Column(nullable = false)
     private Operator operator;
 
-    @NotNull
+    @Getter
+    @Setter
     @Column(nullable = false)
     private double range;
 
-    public Operator getOperator() {
-        return operator;
-    }
-
-    public void setOperator(Operator operator) {
-        this.operator = operator;
-    }
-
-    public double getRange() {
-        return range;
-    }
-
-    public void setRange(Double range) {
-        this.range = range;
+    @Override
+    public boolean triggered() {
+        return operator.checkAgainst(getDevice().readTriggerState(), range);
     }
 }
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/RangeTriggerRepository.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/RangeTriggerRepository.java
index 8ef6772..b4827b5 100644
--- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/RangeTriggerRepository.java
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/RangeTriggerRepository.java
@@ -3,7 +3,7 @@ package ch.usi.inf.sa4.sanmarinoes.smarthut.models;
 import java.util.List;
 import org.springframework.data.repository.query.Param;
 
-public interface RangeTriggerRepository extends TriggerRepository> {
+public interface RangeTriggerRepository extends TriggerRepository {
 
-    List> findAllByAutomationId(@Param("automationId") long automationId);
+    List findAllByAutomationId(@Param("automationId") long automationId);
 }
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/RangeTriggerable.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/RangeTriggerable.java
index 489a763..c850541 100644
--- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/RangeTriggerable.java
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/RangeTriggerable.java
@@ -1,5 +1,5 @@
 package ch.usi.inf.sa4.sanmarinoes.smarthut.models;
 
-public interface RangeTriggerable {
+public interface RangeTriggerable extends Triggerable {
     double readTriggerState();
 }
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/RegularLight.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/RegularLight.java
index 2d06c59..1ef8412 100644
--- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/RegularLight.java
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/RegularLight.java
@@ -2,30 +2,23 @@ package ch.usi.inf.sa4.sanmarinoes.smarthut.models;
 
 import javax.persistence.Column;
 import javax.persistence.Entity;
-import javax.validation.constraints.NotNull;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.Setter;
 
 /** Represents a standard non-dimmable light */
 @Entity
+@EqualsAndHashCode(callSuper = true)
 public class RegularLight extends Switchable implements BooleanTriggerable {
 
     /** Whether the light is on or not */
     @Column(name = "light_on", nullable = false)
-    @NotNull
+    @Getter
+    @Setter
     boolean on;
 
     public RegularLight() {
         super("regularLight");
-        this.on = false;
-    }
-
-    @Override
-    public boolean isOn() {
-        return on;
-    }
-
-    @Override
-    public void setOn(boolean on) {
-        this.on = on;
     }
 
     @Override
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Room.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Room.java
index 34f3824..6785d90 100644
--- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Room.java
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Room.java
@@ -1,117 +1,15 @@
 package ch.usi.inf.sa4.sanmarinoes.smarthut.models;
 
 import ch.usi.inf.sa4.sanmarinoes.smarthut.config.GsonExclude;
-import com.google.gson.annotations.SerializedName;
 import io.swagger.annotations.ApiModelProperty;
 import java.util.HashSet;
 import java.util.Set;
 import javax.persistence.*;
-import javax.validation.constraints.NotNull;
 
 /** Represents a room in the house owned by the user */
 @Entity
 public class Room {
 
-    /** A collection of Semantic UI icons */
-    @SuppressWarnings("unused")
-    public enum Icon {
-        @SerializedName("home")
-        HOME("home"),
-        @SerializedName("coffee")
-        COFFEE("coffee"),
-        @SerializedName("beer")
-        BEER("beer"),
-        @SerializedName("glass martini")
-        GLASS_MARTINI("glass martini"),
-        @SerializedName("film")
-        FILM("film"),
-        @SerializedName("video")
-        VIDEO("video"),
-        @SerializedName("music")
-        MUSIC("music"),
-        @SerializedName("headphones")
-        HEADPHONES("headphones"),
-        @SerializedName("fax")
-        FAX("fax"),
-        @SerializedName("phone")
-        PHONE("phone"),
-        @SerializedName("laptop")
-        LAPTOP("laptop"),
-        @SerializedName("bath")
-        BATH("bath"),
-        @SerializedName("shower")
-        SHOWER("shower"),
-        @SerializedName("bed")
-        BED("bed"),
-        @SerializedName("child")
-        CHILD("child"),
-        @SerializedName("warehouse")
-        WAREHOUSE("warehouse"),
-        @SerializedName("car")
-        CAR("car"),
-        @SerializedName("bicycle")
-        BICYCLE("bicycle"),
-        @SerializedName("motorcycle")
-        MOTORCYCLE("motorcycle"),
-        @SerializedName("archive")
-        ARCHIVE("archive"),
-        @SerializedName("boxes")
-        BOXES("boxes"),
-        @SerializedName("cubes")
-        CUBES("cubes"),
-        @SerializedName("chess")
-        CHESS("chess"),
-        @SerializedName("gamepad")
-        GAMEPAD("gamepad"),
-        @SerializedName("futbol")
-        FUTBOL("futbol"),
-        @SerializedName("table tennis")
-        TABLE_TENNIS("table tennis"),
-        @SerializedName("server")
-        SERVER("server"),
-        @SerializedName("tv")
-        TV("tv"),
-        @SerializedName("heart")
-        HEART("heart"),
-        @SerializedName("camera")
-        CAMERA("camera"),
-        @SerializedName("trophy")
-        TROPHY("trophy"),
-        @SerializedName("wrench")
-        WRENCH("wrench"),
-        @SerializedName("image")
-        IMAGE("image"),
-        @SerializedName("book")
-        BOOK("book"),
-        @SerializedName("university")
-        UNIVERSITY("university"),
-        @SerializedName("medkit")
-        MEDKIT("medkit"),
-        @SerializedName("paw")
-        PAW("paw"),
-        @SerializedName("tree")
-        TREE("tree"),
-        @SerializedName("utensils")
-        UTENSILS("utensils"),
-        @SerializedName("male")
-        MALE("male"),
-        @SerializedName("female")
-        FEMALE("female"),
-        @SerializedName("life ring outline")
-        LIFE_RING_OUTLINE("life ring outline");
-
-        private String iconName;
-
-        Icon(String s) {
-            this.iconName = s;
-        }
-
-        @Override
-        public String toString() {
-            return iconName;
-        }
-    }
-
     @Id
     @GeneratedValue(strategy = GenerationType.AUTO)
     @Column(name = "id", updatable = false, nullable = false, unique = true)
@@ -143,12 +41,10 @@ public class Room {
      * User that owns the house this room is in as a foreign key id. To use when updating and
      * inserting from a REST call.
      */
-    @NotNull
     @Column(name = "user_id", nullable = false)
     private Long userId;
 
     /** The user given name of this room (e.g. 'Master bedroom') */
-    @NotNull
     @Column(nullable = false)
     private String name;
 
@@ -200,4 +96,12 @@ public class Room {
     public String toString() {
         return "Room{" + "id=" + id + ", name='" + name + "\'}";
     }
+
+    public User getUser() {
+        return user;
+    }
+
+    public void setUser(User user) {
+        this.user = user;
+    }
 }
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Scene.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Scene.java
index f35d54d..b9d80de 100644
--- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Scene.java
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Scene.java
@@ -5,12 +5,13 @@ import io.swagger.annotations.ApiModelProperty;
 import java.util.HashSet;
 import java.util.Set;
 import javax.persistence.*;
-import javax.validation.constraints.NotNull;
+import lombok.Data;
 
 /**
  * Represent a collection of state changes to devices even in different rooms but belonging to the
  * same user
  */
+@Data
 @Entity
 public class Scene {
 
@@ -25,68 +26,21 @@ public class Scene {
     @GsonExclude
     private User user;
 
-    @NotNull
     @Column(name = "user_id", nullable = false)
     @GsonExclude
     private Long userId;
 
     @OneToMany(mappedBy = "scene", orphanRemoval = true)
     @GsonExclude
-    private Set> states = new HashSet<>();
+    private Set states = new HashSet<>();
 
     /** The user given name of this room (e.g. 'Master bedroom') */
-    @NotNull
     @Column(nullable = false)
     private String name;
 
+    @Column(nullable = false)
+    private Icon icon;
+
     /** Determines whether a guest can access this scene */
     @Column private boolean guestAccessEnabled;
-
-    public boolean isGuestAccessEnabled() {
-        return guestAccessEnabled;
-    }
-
-    public void setGuestAccessEnabled(boolean guestAccessEnabled) {
-        this.guestAccessEnabled = guestAccessEnabled;
-    }
-
-    public String getName() {
-        return name;
-    }
-
-    public void setName(String name) {
-        this.name = name;
-    }
-
-    public long getId() {
-        return id;
-    }
-
-    public void setId(long id) {
-        this.id = id;
-    }
-
-    public User getUser() {
-        return user;
-    }
-
-    public void setUser(User user) {
-        this.user = user;
-    }
-
-    public Set> getStates() {
-        return states;
-    }
-
-    public void setStates(Set> states) {
-        this.states = states;
-    }
-
-    public Long getUserId() {
-        return userId;
-    }
-
-    public void setUserId(Long userId) {
-        this.userId = userId;
-    }
 }
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/ScenePriority.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/ScenePriority.java
index 64e755b..22b6f8f 100644
--- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/ScenePriority.java
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/ScenePriority.java
@@ -2,16 +2,8 @@ package ch.usi.inf.sa4.sanmarinoes.smarthut.models;
 
 import ch.usi.inf.sa4.sanmarinoes.smarthut.config.GsonExclude;
 import io.swagger.annotations.ApiModelProperty;
-import javax.persistence.Column;
-import javax.persistence.Entity;
-import javax.persistence.GeneratedValue;
-import javax.persistence.GenerationType;
-import javax.persistence.Id;
-import javax.persistence.JoinColumn;
-import javax.persistence.ManyToOne;
-import javax.persistence.PreRemove;
+import javax.persistence.*;
 import javax.validation.constraints.Min;
-import javax.validation.constraints.NotNull;
 
 @Entity
 public class ScenePriority {
@@ -28,10 +20,8 @@ public class ScenePriority {
     private Automation automation;
 
     @Column(name = "automation_id", nullable = false)
-    @NotNull
     private Long automationId;
 
-    @NotNull
     @Min(0)
     @Column(nullable = false)
     private Integer priority;
@@ -42,9 +32,16 @@ public class ScenePriority {
     private Scene scene;
 
     @Column(name = "scene_id", nullable = false, updatable = false)
-    @NotNull
     private Long sceneId;
 
+    public long getId() {
+        return id;
+    }
+
+    public void setId(long id) {
+        this.id = id;
+    }
+
     public Integer getPriority() {
         return priority;
     }
@@ -53,21 +50,16 @@ public class ScenePriority {
         this.priority = priority;
     }
 
-    public Automation getAutomation() {
-        return automation;
-    }
-
-    public void setAutomation(Automation automation) {
-        this.automation = automation;
-    }
-
     public Long getAutomationId() {
         return automationId;
     }
 
-    public ScenePriority setAutomationId(Long automationId) {
+    public void setAutomationId(Long automationId) {
         this.automationId = automationId;
-        return this;
+    }
+
+    public void setAutomation(Automation automation) {
+        this.automation = automation;
     }
 
     public Scene getScene() {
@@ -88,10 +80,14 @@ public class ScenePriority {
 
     @PreRemove
     public void preRemove() {
-        this.setAutomation(null);
-        this.setAutomationId(null);
+        this.automation = null;
+        this.automationId = null;
 
-        this.setScene(null);
-        this.setSceneId(null);
+        this.scene = null;
+        this.sceneId = null;
+    }
+
+    public Automation getAutomation() {
+        return automation;
     }
 }
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/SceneRepository.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/SceneRepository.java
index a4aa68c..8b6e4f1 100644
--- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/SceneRepository.java
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/SceneRepository.java
@@ -19,4 +19,10 @@ public interface SceneRepository extends CrudRepository {
 
     @Query("SELECT s FROM Scene s JOIN s.user u WHERE u.username = ?1")
     List findByUsername(String username);
+
+    @Query("SELECT s FROM Scene s JOIN s.user u WHERE u.id = ?1 AND s.guestAccessEnabled = true")
+    List findByHostId(Long hostId);
+
+    Optional findByIdAndUserIdAndGuestAccessEnabled(
+            Long id, Long userId, boolean guestAccessEnabled);
 }
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/SecurityCamera.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/SecurityCamera.java
index 3d4cef9..b0afc82 100644
--- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/SecurityCamera.java
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/SecurityCamera.java
@@ -2,9 +2,10 @@ package ch.usi.inf.sa4.sanmarinoes.smarthut.models;
 
 import javax.persistence.Column;
 import javax.persistence.Entity;
-import javax.validation.constraints.NotNull;
+import lombok.EqualsAndHashCode;
 
 @Entity
+@EqualsAndHashCode(callSuper = true)
 public class SecurityCamera extends Switchable implements BooleanTriggerable {
 
     public SecurityCamera() {
@@ -13,17 +14,19 @@ public class SecurityCamera extends Switchable implements BooleanTriggerable {
     }
 
     @Column(name = "camera_on", nullable = false)
-    @NotNull
     private boolean on;
 
     @Column(name = "video", nullable = false)
-    @NotNull
-    private String path = "/security_camera_videos/security_camera_1.mp4";
+    private String path;
 
     public String getPath() {
         return path;
     }
 
+    public void setPath(String path) {
+        this.path = path;
+    }
+
     @Override
     public boolean isOn() {
         return on;
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Sensor.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Sensor.java
index 2fb5442..02c61d1 100644
--- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Sensor.java
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Sensor.java
@@ -7,17 +7,18 @@ import javax.persistence.Column;
 import javax.persistence.Entity;
 import javax.persistence.EnumType;
 import javax.persistence.Enumerated;
-import javax.validation.constraints.NotNull;
+import lombok.EqualsAndHashCode;
 
 /** A sensor input device that measures a quantity in a continuous scale (e.g. temperature) */
 @Entity
+@EqualsAndHashCode(callSuper = true)
 public class Sensor extends InputDevice implements RangeTriggerable {
 
     public static final Map TYPICAL_VALUES =
             Map.of(
-                    SensorType.TEMPERATURE, new BigDecimal(17.0),
-                    SensorType.HUMIDITY, new BigDecimal(40.0),
-                    SensorType.LIGHT, new BigDecimal(1000));
+                    SensorType.TEMPERATURE, BigDecimal.valueOf(17.0),
+                    SensorType.HUMIDITY, BigDecimal.valueOf(40.0),
+                    SensorType.LIGHT, BigDecimal.valueOf(1000));
 
     @Override
     public double readTriggerState() {
@@ -43,9 +44,14 @@ public class Sensor extends InputDevice implements RangeTriggerable {
     @Column(nullable = false, precision = 11, scale = 1)
     private BigDecimal value;
 
+    /** The value of the error according to this value */
+    @Column(nullable = false, precision = 11, scale = 1)
+    private BigDecimal err = new BigDecimal(1);
+
+    @Column(nullable = false, precision = 11, scale = 1)
+    private BigDecimal typical = BigDecimal.valueOf(17);
     /** The type of this sensor */
     @Column(nullable = false)
-    @NotNull
     @Enumerated(value = EnumType.STRING)
     private SensorType sensor;
 
@@ -65,6 +71,22 @@ public class Sensor extends InputDevice implements RangeTriggerable {
         this.value = newValue;
     }
 
+    public BigDecimal getError() {
+        return err;
+    }
+
+    public void setError(BigDecimal err) {
+        this.err = err;
+    }
+
+    public BigDecimal getTypical() {
+        return typical;
+    }
+
+    public void setTypical(BigDecimal typical) {
+        this.typical = typical;
+    }
+
     public Sensor() {
         super("sensor");
     }
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/SmartPlug.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/SmartPlug.java
index 77f6c3d..11d964b 100644
--- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/SmartPlug.java
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/SmartPlug.java
@@ -1,13 +1,13 @@
 package ch.usi.inf.sa4.sanmarinoes.smarthut.models;
 
-
 import java.math.BigDecimal;
 import javax.persistence.Column;
 import javax.persistence.Entity;
-import javax.validation.constraints.NotNull;
+import lombok.EqualsAndHashCode;
 
 /** A smart plug that can be turned either on or off */
 @Entity
+@EqualsAndHashCode(callSuper = true)
 public class SmartPlug extends Switchable implements BooleanTriggerable {
 
     /** The average consumption of an active plug when on in Watt */
@@ -15,12 +15,10 @@ public class SmartPlug extends Switchable implements BooleanTriggerable {
 
     /** The total amount of power that the smart plug has consumed represented in W/h */
     @Column(precision = 13, scale = 3)
-    @NotNull
     private BigDecimal totalConsumption = BigDecimal.ZERO;
 
     /** Whether the smart plug is on */
     @Column(name = "smart_plug_on", nullable = false)
-    @NotNull
     private boolean on;
 
     public BigDecimal getTotalConsumption() {
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/State.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/State.java
index 01398b8..ae737ab 100644
--- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/State.java
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/State.java
@@ -3,7 +3,8 @@ package ch.usi.inf.sa4.sanmarinoes.smarthut.models;
 import ch.usi.inf.sa4.sanmarinoes.smarthut.config.GsonExclude;
 import io.swagger.annotations.ApiModelProperty;
 import javax.persistence.*;
-import javax.validation.constraints.NotNull;
+import lombok.Getter;
+import lombok.Setter;
 
 /**
  * Represents instructions on how to change the state of a particular device. Many states (plus
@@ -12,85 +13,66 @@ import javax.validation.constraints.NotNull;
 @Entity
 @Table(uniqueConstraints = {@UniqueConstraint(columnNames = {"device_id", "scene_id"})})
 @Inheritance(strategy = InheritanceType.SINGLE_TABLE)
-public abstract class State {
+public abstract class State {
+
+    @ManyToOne(targetEntity = OutputDevice.class)
+    @JoinColumn(name = "device_id", updatable = false, insertable = false)
+    @GsonExclude
+    @Getter
+    private OutputDevice device;
 
     @Id
     @GeneratedValue(strategy = GenerationType.AUTO)
     @Column(name = "id", updatable = false, nullable = false, unique = true)
     @ApiModelProperty(hidden = true)
+    @Getter
     private long id;
 
-    @ManyToOne(targetEntity = OutputDevice.class)
-    @JoinColumn(name = "device_id", updatable = false, insertable = false)
-    @GsonExclude
-    private D device;
-
     /**
      * The device this state belongs in, as a foreign key id. To use when updating and inserting
      * from a REST call.
      */
     @Column(name = "device_id", nullable = false)
-    @NotNull
+    @Getter
+    @Setter
     private Long deviceId;
 
     @ManyToOne
     @JoinColumn(name = "scene_id", updatable = false, insertable = false)
     @GsonExclude
+    @Getter
+    @Setter
     private Scene scene;
 
     @Column(name = "scene_id", nullable = false)
-    @NotNull
+    @Getter
+    @Setter
     private Long sceneId;
 
+    protected void setInnerDevice(OutputDevice device) {
+        this.device = device;
+    }
+
     /** Sets the state of the connected device to the state represented by this object. */
     public abstract void apply();
 
-    public long getId() {
-        return id;
-    }
+    /** Creates a perfect copy of this state, except for the id field and the sceneId/scene */
+    protected abstract State copy();
 
-    public void setId(long id) {
-        this.id = id;
-    }
-
-    public D getDevice() {
-        return device;
-    }
-
-    public void setDevice(D device) {
-        this.device = device;
-    }
-
-    public Long getDeviceId() {
-        return deviceId;
-    }
-
-    public void setDeviceId(Long deviceId) {
-        this.deviceId = deviceId;
-    }
-
-    public Scene getScene() {
-        return scene;
-    }
-
-    public void setScene(Scene scene) {
-        this.scene = scene;
-    }
-
-    public Long getSceneId() {
-        return sceneId;
-    }
-
-    public void setSceneId(Long sceneId) {
-        this.sceneId = sceneId;
+    public State copyToSceneId(Long sceneId) {
+        final State s = copy();
+        s.setDeviceId(this.deviceId);
+        s.device = this.device;
+        s.setSceneId(sceneId);
+        s.setScene(this.scene);
+        return s;
     }
 
     @PreRemove
     public void removeDeviceAndScene() {
-        this.setScene(null);
         this.setSceneId(null);
-
-        this.setDevice(null);
+        this.setScene(null);
         this.setDeviceId(null);
+        this.device = null;
     }
 }
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/StateRepository.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/StateRepository.java
index d2d1278..b3195b7 100644
--- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/StateRepository.java
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/StateRepository.java
@@ -5,7 +5,7 @@ import javax.transaction.Transactional;
 import org.springframework.data.repository.CrudRepository;
 import org.springframework.data.repository.query.Param;
 
-public interface StateRepository> extends CrudRepository {
+public interface StateRepository extends CrudRepository {
 
     @Transactional
     void deleteAllBySceneId(long roomId);
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Switch.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Switch.java
index 4b9ef3a..72afd0e 100644
--- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Switch.java
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Switch.java
@@ -5,14 +5,25 @@ import ch.usi.inf.sa4.sanmarinoes.smarthut.config.SocketGsonExclude;
 import java.util.HashSet;
 import java.util.Set;
 import javax.persistence.*;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
 
 /** A switch input device */
 @Entity
-public class Switch extends InputDevice implements BooleanTriggerable {
+@EqualsAndHashCode(callSuper = true)
+public class Switch extends InputDevice implements BooleanTriggerable, Connectable {
 
-    @ManyToMany(cascade = CascadeType.DETACH)
+    @ManyToMany(
+            cascade = {
+                CascadeType.DETACH,
+                CascadeType.MERGE,
+                CascadeType.REFRESH,
+                CascadeType.PERSIST
+            })
     @GsonExclude
     @SocketGsonExclude
+    @EqualsAndHashCode.Exclude
+    @Getter
     @JoinTable(
             name = "switch_switchable",
             joinColumns = @JoinColumn(name = "switch_id"),
@@ -54,8 +65,19 @@ public class Switch extends InputDevice implements BooleanTriggerable {
         return on;
     }
 
-    public Set getOutputs() {
-        return switchables;
+    @Override
+    public Set getOutputs() {
+        return Set.copyOf(switchables);
+    }
+
+    public void connect(Switchable output, boolean connect) {
+        if (connect) {
+            output.getSwitches().add(this);
+            switchables.add(output);
+        } else {
+            output.getSwitches().remove(this);
+            switchables.remove(output);
+        }
     }
 
     @Override
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Switchable.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Switchable.java
index c7abc7e..058baf2 100644
--- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Switchable.java
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Switchable.java
@@ -3,6 +3,7 @@ package ch.usi.inf.sa4.sanmarinoes.smarthut.models;
 import ch.usi.inf.sa4.sanmarinoes.smarthut.config.GsonExclude;
 import ch.usi.inf.sa4.sanmarinoes.smarthut.config.SocketGsonExclude;
 import java.util.HashSet;
+import java.util.Objects;
 import java.util.Set;
 import javax.persistence.*;
 
@@ -11,10 +12,14 @@ import javax.persistence.*;
 @Inheritance(strategy = InheritanceType.JOINED)
 public abstract class Switchable extends OutputDevice {
 
-    public static final Connector SWITCH_SWITCHABLE_CONNECTOR =
-            Connector.basic(Switch::getOutputs, Switchable::getSwitches);
-
-    @ManyToMany(mappedBy = "switchables", cascade = CascadeType.DETACH)
+    @ManyToMany(
+            mappedBy = "switchables",
+            cascade = {
+                CascadeType.DETACH,
+                CascadeType.MERGE,
+                CascadeType.REFRESH,
+                CascadeType.PERSIST
+            })
     @GsonExclude
     @SocketGsonExclude
     private Set inputs = new HashSet<>();
@@ -41,16 +46,28 @@ public abstract class Switchable extends OutputDevice {
         return inputs;
     }
 
-    public void readStateAndSet(SwitchableState state) {
+    public void readStateAndSet(SwitchableState state) {
         setOn(state.isOn());
     }
 
-    @Override
-    public State cloneState() {
-        final SwitchableState newState = new SwitchableState<>();
+    public State cloneState() {
+        final SwitchableState newState = new SwitchableState();
         newState.setDeviceId(getId());
-        newState.setDevice(this);
         newState.setOn(isOn());
         return newState;
     }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        if (!super.equals(o)) return false;
+        Switchable that = (Switchable) o;
+        return isOn() == that.isOn();
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(super.hashCode(), isOn());
+    }
 }
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/SwitchableState.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/SwitchableState.java
index 67b3118..38307af 100644
--- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/SwitchableState.java
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/SwitchableState.java
@@ -2,24 +2,31 @@ package ch.usi.inf.sa4.sanmarinoes.smarthut.models;
 
 import javax.persistence.Column;
 import javax.persistence.Entity;
+import lombok.Getter;
+import lombok.Setter;
 
 /** Represents a state for a Switchable device */
 @Entity
-public class SwitchableState extends State {
+public class SwitchableState extends State {
+
+    public void setDevice(Switchable device) {
+        setInnerDevice(device);
+    }
 
     @Column(name = "switchable_on")
+    @Getter
+    @Setter
     private boolean on;
 
-    public boolean isOn() {
-        return on;
-    }
-
-    public void setOn(boolean on) {
-        this.on = on;
-    }
-
     @Override
     public void apply() {
-        getDevice().readStateAndSet(this);
+        ((Switchable) getDevice()).readStateAndSet(this);
+    }
+
+    @Override
+    protected SwitchableState copy() {
+        final SwitchableState d = new SwitchableState();
+        d.setOn(on);
+        return d;
     }
 }
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/SwitchableStateRepository.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/SwitchableStateRepository.java
index 933ac6c..5c9850b 100644
--- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/SwitchableStateRepository.java
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/SwitchableStateRepository.java
@@ -1,3 +1,3 @@
 package ch.usi.inf.sa4.sanmarinoes.smarthut.models;
 
-public interface SwitchableStateRepository extends StateRepository> {}
+public interface SwitchableStateRepository extends StateRepository {}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Thermostat.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Thermostat.java
index e8f0e86..7523a49 100644
--- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Thermostat.java
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Thermostat.java
@@ -5,10 +5,11 @@ import java.math.BigDecimal;
 import javax.persistence.Column;
 import javax.persistence.Entity;
 import javax.persistence.Transient;
-import javax.validation.constraints.NotNull;
+import lombok.EqualsAndHashCode;
 
 /** A thermostat capable of controlling cooling and heating. */
 @Entity
+@EqualsAndHashCode(callSuper = true)
 public class Thermostat extends Switchable implements BooleanTriggerable {
 
     @Override
@@ -22,32 +23,29 @@ public class Thermostat extends Switchable implements BooleanTriggerable {
         computeState();
     }
 
-    /**
-     * Computes the new thermostat state, for when the thermostat is on;
-     *
-     * @return true if the state changed, false if not;
-     */
-    public boolean computeState() {
+    /** Computes the new thermostat state, for when the thermostat is on */
+    public void computeState() {
         if (mode == Thermostat.Mode.OFF) {
-            return false;
+            return;
         }
 
         BigDecimal measured = this.getMeasuredTemperature();
         BigDecimal target = this.getTargetTemperature();
+
+        if (measured == null) {
+            this.setMode(Thermostat.Mode.IDLE);
+            return;
+        }
+
         BigDecimal delta = target.subtract(measured);
 
         if (delta.abs().doubleValue() < 0.25) {
-            if (this.getMode() == Thermostat.Mode.IDLE) return false;
             this.setMode(Thermostat.Mode.IDLE);
         } else if (delta.signum() > 0) {
-            if (this.getMode() == Thermostat.Mode.HEATING) return false;
             this.setMode(Thermostat.Mode.HEATING);
         } else {
-            if (this.getMode() == Thermostat.Mode.COOLING) return false;
             this.setMode(Thermostat.Mode.COOLING);
         }
-
-        return true;
     }
 
     @Override
@@ -67,7 +65,7 @@ public class Thermostat extends Switchable implements BooleanTriggerable {
     }
 
     /** Temperature to be reached */
-    @Column @NotNull private BigDecimal targetTemperature;
+    @Column private BigDecimal targetTemperature;
 
     /** The temperature detected by the embedded sensor */
     @Column(nullable = false, precision = 4, scale = 1)
@@ -75,18 +73,53 @@ public class Thermostat extends Switchable implements BooleanTriggerable {
             Sensor.TYPICAL_VALUES.get(Sensor.SensorType.TEMPERATURE);
 
     /** State of this thermostat */
-    @Column @NotNull private Thermostat.Mode mode;
+    @Column private Thermostat.Mode mode;
 
     @Transient private BigDecimal measuredTemperature;
 
     @Column private boolean useExternalSensors = false;
 
+    /** The value of the error according to this value */
+    @Column(nullable = false, precision = 11, scale = 1)
+    private BigDecimal err = new BigDecimal(1);
+
+    @Column(nullable = false, precision = 11, scale = 1)
+    private BigDecimal typical = BigDecimal.valueOf(17);
+
     /** Creates a thermostat with a temperature sensor and its initial OFF state */
     public Thermostat() {
         super("thermostat");
         this.mode = Mode.OFF;
     }
 
+    @Override
+    public String toString() {
+        final StringBuilder sb = new StringBuilder("Thermostat{");
+        sb.append("targetTemperature=").append(targetTemperature);
+        sb.append(", internalSensorTemperature=").append(internalSensorTemperature);
+        sb.append(", mode=").append(mode);
+        sb.append(", measuredTemperature=").append(measuredTemperature);
+        sb.append(", useExternalSensors=").append(useExternalSensors);
+        sb.append('}');
+        return sb.toString();
+    }
+
+    public BigDecimal getErr() {
+        return err;
+    }
+
+    public void setErr(BigDecimal err) {
+        this.err = err;
+    }
+
+    public BigDecimal getTypical() {
+        return typical;
+    }
+
+    public void setTypical(BigDecimal typical) {
+        this.typical = typical;
+    }
+
     public void setMode(Mode state) {
         this.mode = state;
     }
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/ThermostatCondition.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/ThermostatCondition.java
new file mode 100644
index 0000000..c621c9b
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/ThermostatCondition.java
@@ -0,0 +1,56 @@
+package ch.usi.inf.sa4.sanmarinoes.smarthut.models;
+
+import com.google.gson.annotations.SerializedName;
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import lombok.EqualsAndHashCode;
+
+@Entity
+@EqualsAndHashCode(callSuper = true)
+public class ThermostatCondition extends Condition {
+
+    public ThermostatCondition() {
+        super("thermostatCondition");
+    }
+
+    public enum Operator {
+        @SerializedName("EQUAL")
+        EQUAL,
+        @SerializedName("NOTEQUAL")
+        NOTEQUAL,
+    }
+
+    @Column(nullable = false)
+    private ThermostatCondition.Operator operator;
+
+    @Column(nullable = false)
+    private Thermostat.Mode mode;
+
+    public Operator getOperator() {
+        return operator;
+    }
+
+    public void setOperator(Operator operator) {
+        this.operator = operator;
+    }
+
+    public Thermostat.Mode getMode() {
+        return mode;
+    }
+
+    public void setMode(Thermostat.Mode mode) {
+        this.mode = mode;
+    }
+
+    @Override
+    public boolean triggered() {
+        switch (operator) {
+            case EQUAL:
+                return getDevice().getMode() == mode;
+            case NOTEQUAL:
+                return getDevice().getMode() != mode;
+            default:
+                return false;
+        }
+    }
+}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/ThermostatConditionRepository.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/ThermostatConditionRepository.java
new file mode 100644
index 0000000..4b529fb
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/ThermostatConditionRepository.java
@@ -0,0 +1,9 @@
+package ch.usi.inf.sa4.sanmarinoes.smarthut.models;
+
+import java.util.List;
+import org.springframework.data.repository.query.Param;
+
+public interface ThermostatConditionRepository extends ConditionRepository {
+
+    List findAllByAutomationId(@Param("automationId") long automationId);
+}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/ThermostatRepository.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/ThermostatRepository.java
index 896e0ad..768b882 100644
--- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/ThermostatRepository.java
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/ThermostatRepository.java
@@ -24,7 +24,6 @@ public interface ThermostatRepository extends DeviceRepository {
      * @param thermostatRoomId room ID of the thermostat
      * @return an optional big decimal, empty if none found
      */
-    @Query(
-            "SELECT AVG(s.value) FROM Sensor s JOIN s.room r WHERE s.sensor = 'TEMPERATURE' AND r.id = ?1")
-    Optional getAverageTemperature(Long thermostatRoomId);
+    @Query("SELECT AVG(s.value) FROM Sensor s JOIN s.room r WHERE s.sensor = ?2 AND r.id = ?1")
+    Optional getAverageTemperature(Long thermostatRoomId, Sensor.SensorType sensorType);
 }
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Trigger.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Trigger.java
index 77010c6..f70a8b0 100644
--- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Trigger.java
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Trigger.java
@@ -3,11 +3,11 @@ package ch.usi.inf.sa4.sanmarinoes.smarthut.models;
 import ch.usi.inf.sa4.sanmarinoes.smarthut.config.GsonExclude;
 import io.swagger.annotations.ApiModelProperty;
 import javax.persistence.*;
-import javax.validation.constraints.NotNull;
+import lombok.EqualsAndHashCode;
 
 @Entity
 @Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
-public abstract class Trigger {
+public abstract class Trigger {
 
     @Transient private String kind;
 
@@ -37,16 +37,15 @@ public abstract class Trigger {
      * from a REST call.
      */
     @Column(name = "device_id", nullable = false)
-    @NotNull
     private Long deviceId;
 
     @ManyToOne
     @JoinColumn(name = "automation_id", updatable = false, insertable = false)
     @GsonExclude
+    @EqualsAndHashCode.Exclude
     private Automation automation;
 
     @Column(name = "automation_id", nullable = false)
-    @NotNull
     private Long automationId;
 
     public long getId() {
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Triggerable.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Triggerable.java
new file mode 100644
index 0000000..1215212
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Triggerable.java
@@ -0,0 +1,3 @@
+package ch.usi.inf.sa4.sanmarinoes.smarthut.models;
+
+public interface Triggerable {}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/User.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/User.java
index b58e383..95b30d1 100644
--- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/User.java
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/User.java
@@ -6,28 +6,41 @@ import java.util.HashSet;
 import java.util.Objects;
 import java.util.Set;
 import javax.persistence.*;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.Setter;
+import lombok.ToString;
 
 /** A user of the Smarthut application */
 @Entity(name = "smarthutuser")
+@ToString
 public class User {
 
     @Id
     @GeneratedValue(strategy = GenerationType.AUTO)
     @Column(name = "id", updatable = false, nullable = false, unique = true)
     @ApiModelProperty(hidden = true)
+    @Getter
+    @Setter
     private Long id;
 
     /** The full name of the user */
     @Column(nullable = false)
+    @Getter
+    @Setter
     private String name;
 
     /** The full username of the user */
     @Column(nullable = false, unique = true)
+    @Getter
+    @Setter
     private String username;
 
     /** A properly salted way to store the password */
     @Column(nullable = false)
     @GsonExclude
+    @Getter
+    @Setter
     private String password;
 
     /**
@@ -35,11 +48,16 @@ public class User {
      * , technically not RFC 5322 compliant
      */
     @Column(nullable = false, unique = true)
+    @Getter
+    @Setter
     private String email;
 
     /** Guests invited by this user */
     @ManyToMany(mappedBy = "hosts", cascade = CascadeType.DETACH)
     @GsonExclude
+    @Getter
+    @ToString.Exclude
+    @EqualsAndHashCode.Exclude
     private Set guests = new HashSet<>();
 
     @ManyToMany(cascade = CascadeType.DETACH)
@@ -48,74 +66,35 @@ public class User {
             joinColumns = @JoinColumn(name = "guest_id"),
             inverseJoinColumns = @JoinColumn(name = "host_id"))
     @GsonExclude
+    @Getter
+    @ToString.Exclude
+    @EqualsAndHashCode.Exclude
     private Set hosts = new HashSet<>();
 
     /** Determines whether a guest can access security cameras */
     @Column(nullable = false)
+    @Getter
+    @Setter
     private boolean cameraEnabled;
 
     @Column(nullable = false)
     @GsonExclude
-    private Boolean isEnabled = false;
+    @Getter
+    @Setter
+    private boolean isEnabled = false;
 
-    public Long getId() {
-        return id;
+    @Override
+    public int hashCode() {
+        if (id == null) return Integer.MAX_VALUE;
+        return (int) (id % Integer.MAX_VALUE);
     }
 
-    public void setId(Long id) {
-        this.id = id;
-    }
-
-    public String getName() {
-        return name;
-    }
-
-    public String getUsername() {
-        return username;
-    }
-
-    public void setUsername(String username) {
-        this.username = username;
-    }
-
-    public void setName(String name) {
-        this.name = name;
-    }
-
-    public String getEmail() {
-        return email;
-    }
-
-    public void setEmail(String email) {
-        this.email = email;
-    }
-
-    public String getPassword() {
-        return password;
-    }
-
-    public void setPassword(String password) {
-        this.password = password;
-    }
-
-    public Boolean getEnabled() {
-        return isEnabled;
-    }
-
-    public void setEnabled(Boolean enabled) {
-        isEnabled = enabled;
-    }
-
-    public Set getGuests() {
-        return guests;
-    }
-
-    public Set getHosts() {
-        return hosts;
-    }
-
-    public boolean isCameraEnabled() {
-        return cameraEnabled;
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        User user = (User) o;
+        return Objects.equals(id, user.id);
     }
 
     public void addGuest(User guest) {
@@ -125,53 +104,4 @@ public class User {
     public void addHost(User host) {
         this.hosts.add(host);
     }
-
-    public void removeGuest(User guest) {
-        this.guests.remove(guest);
-    }
-
-    public void setCameraEnabled(boolean cameraEnabled) {
-        this.cameraEnabled = cameraEnabled;
-    }
-
-    @Override
-    public String toString() {
-        return "User{id="
-                + id
-                + ", name='"
-                + name
-                + '\''
-                + ", username='"
-                + username
-                + '\''
-                + ", password='"
-                + password
-                + '\''
-                + ", email='"
-                + email
-                + '\''
-                + ", cameraEnabled="
-                + cameraEnabled
-                + ", isEnabled="
-                + isEnabled
-                + '}';
-    }
-
-    @Override
-    public boolean equals(Object o) {
-        if (this == o) return true;
-        if (o == null || getClass() != o.getClass()) return false;
-        User user = (User) o;
-        return id.equals(user.id)
-                && name.equals(user.name)
-                && username.equals(user.username)
-                && password.equals(user.password)
-                && email.equals(user.email)
-                && isEnabled.equals(user.isEnabled);
-    }
-
-    @Override
-    public int hashCode() {
-        return Objects.hash(id, name, username, password, email, isEnabled);
-    }
 }
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/UserRepository.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/UserRepository.java
index bd93385..253c0ea 100644
--- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/UserRepository.java
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/UserRepository.java
@@ -1,6 +1,6 @@
 package ch.usi.inf.sa4.sanmarinoes.smarthut.models;
 
-import java.util.*;
+import java.util.Optional;
 import org.springframework.data.repository.CrudRepository;
 
 public interface UserRepository extends CrudRepository {
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/scheduled/UpdateTasks.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/scheduled/UpdateTasks.java
index e701166..87c195c 100644
--- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/scheduled/UpdateTasks.java
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/scheduled/UpdateTasks.java
@@ -19,20 +19,33 @@ import org.springframework.stereotype.Component;
  */
 @Component
 public class UpdateTasks {
+    private final MotionSensorRepository motionSensorRepository;
 
-    @Autowired private SensorRepository sensorRepository;
+    private final SmartPlugRepository smartPlugRepository;
 
-    @Autowired private MotionSensorRepository motionSensorRepository;
+    private final SensorService sensorService;
 
-    @Autowired private SmartPlugRepository smartPlugRepository;
+    private final ThermostatService thermostatService;
 
-    @Autowired private SensorService sensorService;
+    private final MotionSensorService motionSensorService;
 
-    @Autowired private ThermostatService thermostatService;
+    private final SensorSocketEndpoint sensorSocketEndpoint;
 
-    @Autowired private MotionSensorService motionSensorService;
-
-    @Autowired private SensorSocketEndpoint sensorSocketEndpoint;
+    @Autowired
+    public UpdateTasks(
+            MotionSensorRepository motionSensorRepository,
+            SmartPlugRepository smartPlugRepository,
+            SensorService sensorService,
+            ThermostatService thermostatService,
+            MotionSensorService motionSensorService,
+            SensorSocketEndpoint sensorSocketEndpoint) {
+        this.motionSensorRepository = motionSensorRepository;
+        this.smartPlugRepository = smartPlugRepository;
+        this.sensorService = sensorService;
+        this.thermostatService = thermostatService;
+        this.motionSensorService = motionSensorService;
+        this.sensorSocketEndpoint = sensorSocketEndpoint;
+    }
 
     /** Generates fake sensor updates every two seconds with a +/- 2.5% error */
     @Scheduled(fixedRate = 2000)
@@ -78,7 +91,7 @@ public class UpdateTasks {
         c.forEach(
                 s ->
                         sensorSocketEndpoint.queueDeviceUpdate(
-                                s, sensorRepository.findUser(s.getId())));
+                                s, smartPlugRepository.findUser(s.getId()), false, null, false));
     }
 
     /** Sends device updates through sensor socket in batch every one second */
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/service/AutomationService.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/service/AutomationService.java
new file mode 100644
index 0000000..265ace8
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/service/AutomationService.java
@@ -0,0 +1,35 @@
+package ch.usi.inf.sa4.sanmarinoes.smarthut.service;
+
+import ch.usi.inf.sa4.sanmarinoes.smarthut.models.*;
+import java.util.List;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+@Component
+public class AutomationService {
+    private final AutomationRepository automationRepository;
+    private final TriggerRepository> triggerRepository;
+    private final ConditionRepository> conditionRepository;
+
+    @Autowired
+    public AutomationService(
+            AutomationRepository automationRepository,
+            TriggerRepository> triggerRepository,
+            ConditionRepository> conditionRepository) {
+        this.automationRepository = automationRepository;
+        this.triggerRepository = triggerRepository;
+        this.conditionRepository = conditionRepository;
+    }
+
+    public void findTriggersByDeviceId(Long deviceId, List> toPut) {
+        toPut.addAll(triggerRepository.findAllByDeviceId(deviceId));
+    }
+
+    public Automation findByVerifiedId(Long automationId) {
+        return automationRepository.findById(automationId).orElseThrow();
+    }
+
+    public void findAllConditionsByAutomationId(Long automationId, List> toPut) {
+        toPut.addAll(conditionRepository.findAllByAutomationId(automationId));
+    }
+}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/service/DevicePopulationService.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/service/DevicePopulationService.java
new file mode 100644
index 0000000..9b3e7d2
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/service/DevicePopulationService.java
@@ -0,0 +1,20 @@
+package ch.usi.inf.sa4.sanmarinoes.smarthut.service;
+
+import ch.usi.inf.sa4.sanmarinoes.smarthut.models.Device;
+import ch.usi.inf.sa4.sanmarinoes.smarthut.models.Thermostat;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+@Component
+public class DevicePopulationService {
+
+    @Autowired private ThermostatPopulationService thermostatService;
+
+    public void populateComputedFields(Iterable devices) {
+        for (Device d : devices) {
+            if (d instanceof Thermostat) {
+                thermostatService.populateMeasuredTemperature((Thermostat) d);
+            }
+        }
+    }
+}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/service/DevicePropagationService.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/service/DevicePropagationService.java
new file mode 100644
index 0000000..9f7846c
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/service/DevicePropagationService.java
@@ -0,0 +1,154 @@
+package ch.usi.inf.sa4.sanmarinoes.smarthut.service;
+
+import static ch.usi.inf.sa4.sanmarinoes.smarthut.utils.Utils.toList;
+
+import ch.usi.inf.sa4.sanmarinoes.smarthut.error.NotFoundException;
+import ch.usi.inf.sa4.sanmarinoes.smarthut.models.Device;
+import ch.usi.inf.sa4.sanmarinoes.smarthut.models.DeviceRepository;
+import ch.usi.inf.sa4.sanmarinoes.smarthut.models.EagerUserRepository;
+import ch.usi.inf.sa4.sanmarinoes.smarthut.models.User;
+import ch.usi.inf.sa4.sanmarinoes.smarthut.socket.SensorSocketEndpoint;
+import java.util.List;
+import java.util.Set;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+@Component
+public class DevicePropagationService {
+
+    private final SensorSocketEndpoint endpoint;
+    private final EagerUserRepository userRepository;
+    private final DeviceRepository deviceRepository;
+
+    @Autowired
+    public DevicePropagationService(
+            SensorSocketEndpoint endpoint,
+            EagerUserRepository userRepository,
+            DeviceRepository deviceRepository) {
+        this.endpoint = endpoint;
+        this.userRepository = userRepository;
+        this.deviceRepository = deviceRepository;
+    }
+
+    void propagateUpdateAsGuest(Device device, User host, User guest) {
+        final Set guests = Set.copyOf(host.getGuests());
+
+        // We're telling the host that a guest has modified a device. Therefore, fromGuest becomes
+        // true.
+        // broadcast device update to host
+        endpoint.queueDeviceUpdate(device, host, true, null, false);
+
+        // We're telling all guests that a higher entity has issued a device update. Therefore,
+        // fromHost becomes true.
+        for (final User aGuest : guests) {
+            if (aGuest.equals(guest)) {
+                continue;
+            }
+            // enqueue all device updates for all other guests
+            endpoint.queueDeviceUpdate(device, aGuest, false, host.getId(), false);
+        }
+    }
+
+    void saveAllAsGuestSceneApplication(List devices, String guestUsername, Long hostId) {
+        final User guest = userRepository.findByUsername(guestUsername);
+        final User host = userRepository.findById(hostId).orElseThrow(IllegalStateException::new);
+        deviceRepository.saveAll(devices);
+        devices.forEach(d -> this.propagateUpdateAsGuest(d, host, guest));
+    }
+
+    void renameIfDuplicate(Device toCreate, String username) {
+        while (deviceRepository.findDuplicates(toCreate.getName(), username)
+                        - (toCreate.getId() <= 0 ? 0 : 1)
+                > 0) {
+            toCreate.setName(toCreate.getName() + " (new)");
+        }
+    }
+
+    public  T saveAsGuest(T device, String guestUsername, Long hostId)
+            throws NotFoundException {
+        final User currentUser = userRepository.findByUsername(guestUsername);
+        final User host = userRepository.findById(hostId).orElseThrow(NotFoundException::new);
+        if (!host.getGuests().contains(currentUser)) throw new NotFoundException();
+        renameIfDuplicate(device, host.getUsername());
+
+        device = deviceRepository.save(device);
+        propagateUpdateAsGuest(device, host, currentUser);
+        return device;
+    }
+
+    /**
+     * Saves all the devices given in devices assuming that the owner updated them in one way or
+     * another. Takes care of the appropriate websocket updates and trigger checking as well. No
+     * checking is done to verify that the user whose username is given is in fact the owner of
+     * these devices
+     *
+     * @param devices the list of devices to save
+     * @param username the username of the owner of these devices
+     * @param fromScene true if the update comes from the a scene application side effect. Disables
+     *     trigger checking to avoid recursive invocations of automations
+     * @param fromTrigger true if the update comes from a scene application executed by an
+     *     automation. Propagates updated through socket to owner as well. No effect if fromScene is
+     *     false.
+     * @param  the type of device contained in the list
+     * @return the updated list of devices, ready to be fed to GSON
+     */
+    public  List saveAllAsOwner(
+            Iterable devices, String username, boolean fromScene, boolean fromTrigger) {
+        devices.forEach(d -> renameIfDuplicate(d, username));
+        devices = deviceRepository.saveAll(devices);
+        devices.forEach(d -> propagateUpdateAsOwner(d, username, fromScene && fromTrigger));
+
+        return toList(devices);
+    }
+
+    public  List saveAllAsOwner(Iterable devices, String username) {
+        return saveAllAsOwner(devices, username, false, false);
+    }
+
+    public  T saveAsOwner(T device, String username) {
+        renameIfDuplicate(device, username);
+        device = deviceRepository.save(device);
+        propagateUpdateAsOwner(device, username, false);
+
+        return device;
+    }
+
+    public void deleteByIdAsOwner(Long id, String username) throws NotFoundException {
+        Device d =
+                deviceRepository
+                        .findByIdAndUsername(id, username)
+                        .orElseThrow(NotFoundException::new);
+
+        final User user = userRepository.findByUsername(username);
+        final Set guests = user.getGuests();
+        // make sure we're broadcasting from host
+        for (final User guest : guests) {
+            // broadcast to endpoint the object device, with receiving user set to guest
+            endpoint.queueDeviceUpdate(d, guest, false, user.getId(), true);
+        }
+
+        deviceRepository.delete(d);
+    }
+
+    /**
+     * Propagates the update through the socket assuming that the user that modified the device is
+     * the owner of that device
+     *
+     * @param device the updated device
+     * @param username the username of the owner of that device
+     * @param causedByTrigger if true, send the update to the owner as well
+     */
+    void propagateUpdateAsOwner(Device device, String username, boolean causedByTrigger) {
+        final User user = userRepository.findByUsername(username);
+        final Set guests = user.getGuests();
+        // make sure we're broadcasting from host
+        for (final User guest : guests) {
+            // broadcast to endpoint the object device, with receiving user set to guest
+            endpoint.queueDeviceUpdate(device, guest, false, user.getId(), false);
+        }
+
+        if (causedByTrigger) {
+            endpoint.queueDeviceUpdate(device, user, false, null, false);
+        }
+    }
+}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/service/DeviceService.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/service/DeviceService.java
index 0bc2a6e..ac63201 100644
--- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/service/DeviceService.java
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/service/DeviceService.java
@@ -4,191 +4,182 @@ import static ch.usi.inf.sa4.sanmarinoes.smarthut.utils.Utils.toList;
 
 import ch.usi.inf.sa4.sanmarinoes.smarthut.error.NotFoundException;
 import ch.usi.inf.sa4.sanmarinoes.smarthut.models.*;
-import ch.usi.inf.sa4.sanmarinoes.smarthut.socket.SensorSocketEndpoint;
+import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Comparator;
 import java.util.List;
-import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.StreamSupport;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Component;
 
 @Component
 public class DeviceService {
 
-    @Autowired private DeviceRepository deviceRepository;
-    @Autowired private AutomationRepository automationRepository;
-    @Autowired private SceneRepository sceneRepository;
-    @Autowired private SceneService sceneService;
-    @Autowired private TriggerRepository> triggerRepository;
-    @Autowired private RoomRepository roomRepository;
-    @Autowired private EagerUserRepository userRepository;
-    @Autowired private SensorSocketEndpoint endpoint;
-    @Autowired private ThermostatService thermostatService;
+    private final DeviceRepository deviceRepository;
+    private final SceneService sceneService;
+    private final RoomRepository roomRepository;
+    private final AutomationService automationService;
+    private final EagerUserRepository userRepository;
+    private final DevicePopulationService devicePopulationService;
+    private final DevicePropagationService devicePropagationService;
+
+    @Autowired
+    public DeviceService(
+            DeviceRepository deviceRepository,
+            SceneService sceneService,
+            RoomRepository roomRepository,
+            AutomationService automationService,
+            EagerUserRepository userRepository,
+            DevicePopulationService devicePopulationService,
+            DevicePropagationService devicePropagationService) {
+        this.deviceRepository = deviceRepository;
+        this.sceneService = sceneService;
+        this.roomRepository = roomRepository;
+        this.automationService = automationService;
+        this.userRepository = userRepository;
+        this.devicePopulationService = devicePopulationService;
+        this.devicePropagationService = devicePropagationService;
+    }
 
     public void throwIfRoomNotOwned(Long roomId, String username) throws NotFoundException {
-        roomRepository.findByIdAndUsername(roomId, username).orElseThrow(NotFoundException::new);
+        final Room r =
+                roomRepository
+                        .findByIdAndUsername(roomId, username)
+                        .orElseThrow(NotFoundException::new);
+        if (!r.getId().equals(roomId)) throw new IllegalStateException();
     }
 
-    private void renameIfDuplicate(Device toCreate, String username) {
-        while (deviceRepository.findDuplicates(toCreate.getName(), username)
-                        - (toCreate.getId() <= 0 ? 0 : 1)
-                > 0) {
-            toCreate.setName(toCreate.getName() + " (new)");
-        }
-    }
-
-    private void triggerTriggers(Device device) {
+    public void triggerTriggers(Device device, final String username) {
 
         final long deviceId = device.getId();
-
-        List> triggers = triggerRepository.findAllByDeviceId(deviceId);
+        final List> triggers = new ArrayList<>();
+        automationService.findTriggersByDeviceId(deviceId, triggers);
 
         triggers.stream()
                 .filter(Trigger::triggered)
                 .map(Trigger::getAutomationId)
-                .map(t -> automationRepository.findById(t).orElseThrow(IllegalStateException::new))
+                .map(automationService::findByVerifiedId)
                 .distinct()
+                .filter(
+                        a -> {
+                            final List> conditions = new ArrayList<>();
+                            automationService.findAllConditionsByAutomationId(
+                                    a.getId(), conditions);
+                            if (conditions.isEmpty()) return true;
+                            return conditions.stream().allMatch(Condition::triggered);
+                        })
                 .map(Automation::getScenes)
                 .flatMap(Collection::stream)
                 .distinct()
                 .sorted(Comparator.comparing(ScenePriority::getPriority))
-                .map(
-                        t ->
-                                sceneRepository
-                                        .findById(t.getSceneId())
-                                        .orElseThrow(IllegalStateException::new))
-                .forEach(sceneService::apply);
+                .map(t -> sceneService.findByValidatedId(t.getSceneId()))
+                .forEach(s -> sceneService.apply(s, username, true));
     }
 
     public List findAll(Long hostId, String username) throws NotFoundException {
         return findAll(null, hostId, username);
     }
 
-    public List findAll(Long roomId, Long hostId, String username)
-            throws NotFoundException {
-        try {
-            Iterable devices;
-            if (hostId == null) {
-                if (roomId != null) {
-                    roomRepository
-                            .findByIdAndUsername(roomId, username)
-                            .orElseThrow(NotFoundException::new);
-                    devices = deviceRepository.findByRoomId(roomId);
-                } else {
-                    devices = deviceRepository.findAllByUsername(username);
-                }
-            } else {
-                final User guest = userRepository.findByUsername(username);
-                final User host =
-                        userRepository.findById(hostId).orElseThrow(NotFoundException::new);
-
-                if (!guest.getHosts().contains(host)) {
-                    throw new NotFoundException();
-                }
-
-                if (roomId != null) {
-                    Room r = roomRepository.findById(roomId).orElseThrow(NotFoundException::new);
-                    if (!r.getUserId().equals(hostId)) {
-                        throw new NotFoundException();
-                    }
-                    devices = deviceRepository.findByRoomId(roomId);
-                } else {
-                    devices = deviceRepository.findAllByUsername(host.getUsername());
-                }
-            }
-
-            for (Device d : devices) {
-                if (d instanceof Thermostat) {
-                    thermostatService.populateMeasuredTemperature((Thermostat) d);
-                }
-            }
-
-            return toList(devices);
-        } catch (NotFoundException e) {
-            e.printStackTrace();
-            throw e;
-        }
-    }
-
     public  T saveAsGuest(T device, String guestUsername, Long hostId)
             throws NotFoundException {
         final User currentUser = userRepository.findByUsername(guestUsername);
         final User host = userRepository.findById(hostId).orElseThrow(NotFoundException::new);
+
         if (!host.getGuests().contains(currentUser)) throw new NotFoundException();
-        renameIfDuplicate(device, host.getUsername());
+        devicePropagationService.renameIfDuplicate(device, host.getUsername());
+
         device = deviceRepository.save(device);
-        final Set guests = Set.copyOf(host.getGuests());
-
-        // We're telling the host that a guest has modified a device. Therefore, fromGuest becomes
-        // true.
-        device.setFromHost(false);
-        device.setFromGuest(true);
-        // broadcast device update to host
-        endpoint.queueDeviceUpdate(device, host);
-
-        // We're telling all guests that a higher entity has issued a device update. Therefore,
-        // fromHost becomes true.
-        device.setFromHost(true);
-        device.setFromGuest(false);
-        for (final User guest : guests) {
-            if (guest.equals(currentUser)) {
-                continue;
-            }
-            // enqueue all device updates for all other guests
-            endpoint.queueDeviceUpdate(device, guest);
-        }
-
+        devicePropagationService.propagateUpdateAsGuest(device, host, currentUser);
         return device;
     }
 
-    private void propagateUpdateAsOwner(Device device, String username) {
-        final User user = userRepository.findByUsername(username);
-        final Set guests = user.getGuests();
-        // make sure we're broadcasting from host
-        device.setFromHost(true);
-        device.setFromGuest(false);
-        for (final User guest : guests) {
-            // broadcast to endpoint the object device, with receiving user set to guest
-            endpoint.queueDeviceUpdate(device, guest);
-        }
+    public void deleteByIdAsOwner(Long id, String username) throws NotFoundException {
+        devicePropagationService.deleteByIdAsOwner(id, username);
     }
 
+    public void populateComputedFields(Iterable devices) {
+        devicePopulationService.populateComputedFields(devices);
+    }
+
+    /**
+     * Saves all the devices given in devices assuming that the owner updated them in one way or
+     * another. Takes care of the appropriate websocket updates and trigger checking as well. No
+     * checking is done to verify that the user whose username is given is in fact the owner of
+     * these devices
+     *
+     * @param devices the list of devices to save
+     * @param username the username of the owner of these devices
+     * @param fromScene true if the update comes from the a scene application side effect. Disables
+     *     trigger checking to avoid recursive invocations of automations
+     * @param fromTrigger true if the update comes from a scene application executed by an
+     *     automation. Propagates updated through socket to owner as well. No effect if fromScene is
+     *     false.
+     * @param  the type of device contained in the list
+     * @return the updated list of devices, ready to be fed to GSON
+     */
     public  List saveAllAsOwner(
-            Iterable devices, String username, boolean fromScene) {
-        devices.forEach(d -> renameIfDuplicate(d, username));
-        devices = deviceRepository.saveAll(devices);
-        devices.forEach((d) -> propagateUpdateAsOwner(d, username));
-
+            Iterable devices, String username, boolean fromScene, boolean fromTrigger) {
+        List toReturn =
+                devicePropagationService.saveAllAsOwner(devices, username, fromScene, fromTrigger);
         if (!fromScene) {
-            devices.forEach(this::triggerTriggers);
+            toReturn.forEach(d -> this.triggerTriggers(d, username));
         }
-
-        return toList(devices);
+        return toReturn;
     }
 
-    public  T saveAsOwner(T device, String username, boolean fromScene) {
-        renameIfDuplicate(device, username);
-        device = deviceRepository.save(device);
-        propagateUpdateAsOwner(device, username);
-
-        if (!fromScene) {
-            triggerTriggers(device);
-        }
-
-        return device;
+    public  List saveAllAsOwner(Iterable devices, String username) {
+        return saveAllAsOwner(devices, username, false, false);
     }
 
     public  T saveAsOwner(T device, String username) {
-        return saveAsOwner(device, username, false);
+        T toReturn = devicePropagationService.saveAsOwner(device, username);
+        triggerTriggers(toReturn, username);
+        return device;
     }
 
-    public void delete(Long id, String username) throws NotFoundException {
-        Device device =
-                deviceRepository
-                        .findByIdAndUsername(id, username)
-                        .orElseThrow(NotFoundException::new);
-        deviceRepository.delete(device);
+    public List findAll(Long roomId, Long hostId, String username)
+            throws NotFoundException {
+        Iterable devices;
+        User host = null;
+        if (hostId == null) {
+            if (roomId != null) {
+                throwIfRoomNotOwned(roomId, username);
+                devices = deviceRepository.findByRoomId(roomId);
+            } else {
+                devices = deviceRepository.findAllByUsername(username);
+            }
+        } else {
+            final User guest = userRepository.findByUsername(username);
+            host = userRepository.findById(hostId).orElseThrow(NotFoundException::new);
 
-        propagateUpdateAsOwner(device, username);
+            if (!guest.getHosts().contains(host)) {
+                throw new NotFoundException();
+            }
+
+            if (roomId != null) {
+                Room r = roomRepository.findById(roomId).orElseThrow(NotFoundException::new);
+                if (!r.getUserId().equals(hostId)) {
+                    throw new NotFoundException();
+                }
+                devices = deviceRepository.findByRoomId(roomId);
+            } else {
+                devices = deviceRepository.findAllByUsername(host.getUsername());
+            }
+        }
+
+        devicePopulationService.populateComputedFields(devices);
+
+        return filterOutCamerasIfNeeded(host, devices);
+    }
+
+    private List filterOutCamerasIfNeeded(User host, Iterable devices) {
+        if (host != null && !host.isCameraEnabled()) {
+            return StreamSupport.stream(devices.spliterator(), true)
+                    .filter(d -> !(d instanceof SecurityCamera))
+                    .collect(Collectors.toList());
+        } else {
+            return toList(devices);
+        }
     }
 }
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/service/JWTUserDetailsService.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/service/JWTUserDetailsService.java
index 7dff142..51eb7d9 100644
--- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/service/JWTUserDetailsService.java
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/service/JWTUserDetailsService.java
@@ -1,11 +1,9 @@
 package ch.usi.inf.sa4.sanmarinoes.smarthut.service;
 
-import java.util.Set;
-
 import ch.usi.inf.sa4.sanmarinoes.smarthut.models.User;
 import ch.usi.inf.sa4.sanmarinoes.smarthut.models.UserRepository;
+import java.util.Set;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.security.core.*;
 import org.springframework.security.core.userdetails.UserDetails;
 import org.springframework.security.core.userdetails.UserDetailsService;
 import org.springframework.security.core.userdetails.UsernameNotFoundException;
@@ -16,9 +14,9 @@ public class JWTUserDetailsService implements UserDetailsService {
     @Autowired private UserRepository repository;
 
     @Override
-    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
+    public UserDetails loadUserByUsername(String username) {
         User toReturn = repository.findByUsername(username);
-        if (toReturn != null && toReturn.getEnabled()) {
+        if (toReturn != null && toReturn.isEnabled()) {
             return new org.springframework.security.core.userdetails.User(
                     toReturn.getUsername(), toReturn.getPassword(), Set.of());
         } else {
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/service/MotionSensorService.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/service/MotionSensorService.java
index 01a7401..9f369a0 100644
--- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/service/MotionSensorService.java
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/service/MotionSensorService.java
@@ -14,7 +14,7 @@ public class MotionSensorService {
     @Autowired private MotionSensorRepository motionSensorRepository;
 
     /**
-     * Updates detection status of given motion sensor and propagates update throgh socket
+     * Updates detection status of given motion sensor and propagates update through socket
      *
      * @param sensor the motion sensor to update
      * @param detected the new detection status
@@ -23,10 +23,10 @@ public class MotionSensorService {
     public MotionSensor updateDetectionFromMotionSensor(
             MotionSensor sensor, boolean detected, String username) {
         sensor.setDetected(detected);
-        final MotionSensor toReturn = deviceService.saveAsOwner(sensor, username);
+        MotionSensor toReturn = deviceService.saveAsOwner(sensor, username);
 
         sensorSocketEndpoint.queueDeviceUpdate(
-                sensor, motionSensorRepository.findUser(sensor.getId()));
+                sensor, motionSensorRepository.findUser(sensor.getId()), false, null, false);
 
         return toReturn;
     }
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/service/SceneService.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/service/SceneService.java
index 826d896..711a94d 100644
--- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/service/SceneService.java
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/service/SceneService.java
@@ -1,9 +1,6 @@
 package ch.usi.inf.sa4.sanmarinoes.smarthut.service;
 
-import ch.usi.inf.sa4.sanmarinoes.smarthut.models.Device;
-import ch.usi.inf.sa4.sanmarinoes.smarthut.models.DeviceRepository;
-import ch.usi.inf.sa4.sanmarinoes.smarthut.models.Scene;
-import ch.usi.inf.sa4.sanmarinoes.smarthut.models.State;
+import ch.usi.inf.sa4.sanmarinoes.smarthut.models.*;
 import java.util.ArrayList;
 import java.util.List;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -12,17 +9,56 @@ import org.springframework.stereotype.Component;
 @Component
 public class SceneService {
 
-    @Autowired private DeviceRepository deviceRepository;
+    private final DevicePopulationService devicePopulationService;
+    private final DevicePropagationService devicePropagationService;
+    private final StateRepository stateRepository;
+    private final SceneRepository sceneRepository;
 
-    public List apply(Scene newScene) {
-        final List updated = new ArrayList<>();
+    public Scene findByValidatedId(Long id) {
+        return sceneRepository.findById(id).orElseThrow();
+    }
 
-        for (final State s : newScene.getStates()) {
+    @Autowired
+    public SceneService(
+            DevicePopulationService devicePopulationService,
+            DevicePropagationService devicePropagationService,
+            StateRepository stateRepository,
+            SceneRepository sceneRepository) {
+        this.devicePopulationService = devicePopulationService;
+        this.devicePropagationService = devicePropagationService;
+        this.stateRepository = stateRepository;
+        this.sceneRepository = sceneRepository;
+    }
+
+    private List copyStatesToDevices(Scene fromScene) {
+        final List updated = new ArrayList<>(fromScene.getStates().size());
+
+        for (final State s : fromScene.getStates()) {
             s.apply();
             updated.add(s.getDevice());
         }
-        deviceRepository.saveAll(updated);
 
+        devicePopulationService.populateComputedFields(updated);
         return updated;
     }
+
+    public List apply(Scene newScene, String username, boolean fromTrigger) {
+        List updated = copyStatesToDevices(newScene);
+        devicePropagationService.saveAllAsOwner(updated, username, true, fromTrigger);
+        return updated;
+    }
+
+    public List applyAsGuest(Scene newScene, String username, Long hostId) {
+        List updated = copyStatesToDevices(newScene);
+        devicePropagationService.saveAllAsGuestSceneApplication(updated, username, hostId);
+        return updated;
+    }
+
+    public List copyStates(Scene to, Scene from) {
+        final ArrayList states = new ArrayList<>();
+        for (final State s : from.getStates()) {
+            states.add(stateRepository.save(s.copyToSceneId(to.getId())));
+        }
+        return states;
+    }
 }
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/service/SensorService.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/service/SensorService.java
index 0e4fbbd..527b0a9 100644
--- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/service/SensorService.java
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/service/SensorService.java
@@ -4,26 +4,47 @@ import ch.usi.inf.sa4.sanmarinoes.smarthut.models.Sensor;
 import ch.usi.inf.sa4.sanmarinoes.smarthut.models.SensorRepository;
 import ch.usi.inf.sa4.sanmarinoes.smarthut.socket.SensorSocketEndpoint;
 import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.util.Random;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Component;
 
 @Component
 public class SensorService {
 
-    @Autowired private SensorRepository sensorRepository;
+    private final SensorRepository sensorRepository;
 
-    @Autowired private DeviceService deviceService;
+    private final DeviceService deviceService;
 
-    @Autowired private ThermostatService thermostatService;
+    private final ThermostatService thermostatService;
 
-    @Autowired private SensorSocketEndpoint endpoint;
+    private final SensorSocketEndpoint endpoint;
+
+    private final Random ran = new Random();
+
+    @Autowired
+    public SensorService(
+            SensorRepository sensorRepository,
+            DeviceService deviceService,
+            ThermostatService thermostatService,
+            SensorSocketEndpoint endpoint) {
+        this.sensorRepository = sensorRepository;
+        this.deviceService = deviceService;
+        this.thermostatService = thermostatService;
+        this.endpoint = endpoint;
+    }
 
     private void randomJitter(Sensor sensor) {
-        updateValueFromSensor(
-                sensor,
-                Sensor.TYPICAL_VALUES
-                        .get(sensor.getSensor())
-                        .multiply(BigDecimal.valueOf(0.975 + Math.random() / 20)));
+        BigDecimal x =
+                sensor.getTypical()
+                        .subtract(
+                                sensor.getError()
+                                        .divide(BigDecimal.valueOf(2), RoundingMode.CEILING))
+                        .add(
+                                BigDecimal.valueOf(
+                                        ran.nextDouble() * sensor.getError().doubleValue() * 2));
+
+        updateValueFromSensor(sensor, x);
     }
 
     public void sensorFakeUpdate() {
@@ -35,15 +56,30 @@ public class SensorService {
      * Updates the sensor with new measurement and propagates update through websocket
      *
      * @param sensor the sensor to update
-     * @param value the new measurement
      * @return the updated sensor
      */
-    public Sensor updateValueFromSensor(Sensor sensor, BigDecimal value) {
-        sensor.setValue(value);
+    public Sensor update(Sensor sensor) {
         sensor =
                 deviceService.saveAsOwner(
                         sensor, sensorRepository.findUser(sensor.getId()).getUsername());
-        endpoint.queueDeviceUpdate(sensor, sensorRepository.findUser(sensor.getId()));
+        endpoint.queueDeviceUpdate(
+                sensor, sensorRepository.findUser(sensor.getId()), false, null, false);
         return sensor;
     }
+
+    public Sensor updateValueFromSensor(Sensor sensor, BigDecimal value) {
+        sensor.setValue(value);
+        return update(sensor);
+    }
+
+    public Sensor updateSimulationFromSensor(Sensor sensor, BigDecimal error, BigDecimal typical) {
+        if (error != null) {
+            sensor.setError(error);
+        }
+
+        if (typical != null) {
+            sensor.setTypical(typical);
+        }
+        return update(sensor);
+    }
 }
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/service/ThermostatPopulationService.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/service/ThermostatPopulationService.java
new file mode 100644
index 0000000..d8e1878
--- /dev/null
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/service/ThermostatPopulationService.java
@@ -0,0 +1,34 @@
+package ch.usi.inf.sa4.sanmarinoes.smarthut.service;
+
+import ch.usi.inf.sa4.sanmarinoes.smarthut.models.Sensor;
+import ch.usi.inf.sa4.sanmarinoes.smarthut.models.Thermostat;
+import ch.usi.inf.sa4.sanmarinoes.smarthut.models.ThermostatRepository;
+import java.math.BigDecimal;
+import java.util.Optional;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+@Component
+public class ThermostatPopulationService {
+
+    @Autowired private ThermostatRepository thermostatRepository;
+
+    private BigDecimal measureTemperature(final Thermostat thermostat) {
+        Optional average;
+
+        if (thermostat.isUseExternalSensors()) {
+            average =
+                    thermostatRepository.getAverageTemperature(
+                            thermostat.getRoomId(), Sensor.SensorType.TEMPERATURE);
+
+        } else {
+            return thermostat.getInternalSensorTemperature();
+        }
+
+        return average.orElse(null);
+    }
+
+    public void populateMeasuredTemperature(Thermostat thermostat) {
+        thermostat.setMeasuredTemperature(measureTemperature(thermostat));
+    }
+}
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/service/ThermostatService.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/service/ThermostatService.java
index cdbb92c..ad98717 100644
--- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/service/ThermostatService.java
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/service/ThermostatService.java
@@ -1,31 +1,56 @@
 package ch.usi.inf.sa4.sanmarinoes.smarthut.service;
 
-import ch.usi.inf.sa4.sanmarinoes.smarthut.models.Sensor;
 import ch.usi.inf.sa4.sanmarinoes.smarthut.models.Thermostat;
 import ch.usi.inf.sa4.sanmarinoes.smarthut.models.ThermostatRepository;
 import ch.usi.inf.sa4.sanmarinoes.smarthut.socket.SensorSocketEndpoint;
 import ch.usi.inf.sa4.sanmarinoes.smarthut.utils.Utils;
 import java.math.BigDecimal;
+import java.math.RoundingMode;
 import java.util.List;
 import java.util.Optional;
+import java.util.Random;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Component;
 
 @Component
 public class ThermostatService {
 
-    @Autowired private SensorSocketEndpoint endpoint;
+    private final SensorSocketEndpoint endpoint;
 
-    @Autowired private DeviceService deviceService;
+    private final DeviceService deviceService;
 
-    @Autowired private ThermostatRepository thermostatRepository;
+    private final ThermostatPopulationService thermostatPopulationService;
+
+    private final ThermostatRepository thermostatRepository;
+
+    private Random ran = new Random();
+
+    @Autowired
+    public ThermostatService(
+            SensorSocketEndpoint endpoint,
+            DeviceService deviceService,
+            ThermostatPopulationService thermostatPopulationService,
+            ThermostatRepository thermostatRepository) {
+        this.endpoint = endpoint;
+        this.deviceService = deviceService;
+        this.thermostatPopulationService = thermostatPopulationService;
+        this.thermostatRepository = thermostatRepository;
+    }
 
     private void randomJitter(Thermostat thermostat) {
-        updateValueForThermostat(
-                thermostat,
-                Sensor.TYPICAL_VALUES
-                        .get(Sensor.SensorType.TEMPERATURE)
-                        .multiply(BigDecimal.valueOf(0.975 + Math.random() / 20)));
+
+        BigDecimal x =
+                thermostat
+                        .getTypical()
+                        .subtract(
+                                thermostat
+                                        .getErr()
+                                        .divide(BigDecimal.valueOf(2), RoundingMode.CEILING))
+                        .add(
+                                BigDecimal.valueOf(
+                                        ran.nextDouble() * thermostat.getErr().doubleValue() * 2));
+
+        updateValueForThermostat(thermostat, x);
     }
 
     private void updateValueForThermostat(Thermostat thermostat, BigDecimal value) {
@@ -41,22 +66,20 @@ public class ThermostatService {
 
     public List findAll(String username) {
         Iterable all = thermostatRepository.findAllByUsername(username);
-        all.forEach(this::populateMeasuredTemperature);
+        all.forEach(thermostatPopulationService::populateMeasuredTemperature);
         return Utils.toList(all);
     }
 
-    public boolean computeState(Thermostat t) {
-        populateMeasuredTemperature(t);
-        return t.computeState();
+    public void computeState(Thermostat t) {
+        thermostatPopulationService.populateMeasuredTemperature(t);
+        t.computeState();
     }
 
     private void updateState(Thermostat t) {
-        boolean shouldUpdate = this.computeState(t);
+        this.computeState(t);
 
-        if (shouldUpdate) {
-            deviceService.saveAsOwner(t, thermostatRepository.findUser(t.getId()).getUsername());
-            endpoint.queueDeviceUpdate(t, thermostatRepository.findUser(t.getId()));
-        }
+        deviceService.saveAsOwner(t, thermostatRepository.findUser(t.getId()).getUsername());
+        endpoint.queueDeviceUpdate(t, thermostatRepository.findUser(t.getId()), false, null, false);
     }
 
     public void updateStates() {
@@ -69,25 +92,9 @@ public class ThermostatService {
 
         if (t.isPresent()) {
             Thermostat u = t.get();
-            populateMeasuredTemperature(u);
+            thermostatPopulationService.populateMeasuredTemperature(u);
             t = Optional.of(u);
         }
         return t;
     }
-
-    private BigDecimal measureTemperature(final Thermostat thermostat) {
-        Optional average;
-
-        if (thermostat.isUseExternalSensors()) {
-            average = thermostatRepository.getAverageTemperature(thermostat.getRoomId());
-        } else {
-            return thermostat.getInternalSensorTemperature();
-        }
-
-        return average.orElse(null);
-    }
-
-    public void populateMeasuredTemperature(Thermostat thermostat) {
-        thermostat.setMeasuredTemperature(measureTemperature(thermostat));
-    }
 }
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/socket/SensorSocketConfig.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/socket/SensorSocketConfig.java
index 503667a..338a8f2 100644
--- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/socket/SensorSocketConfig.java
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/socket/SensorSocketConfig.java
@@ -11,7 +11,7 @@ import org.springframework.web.socket.server.standard.ServerEndpointRegistration
 @Configuration
 public class SensorSocketConfig extends ServerEndpointConfig.Configurator {
 
-    private SensorSocketEndpoint instance;
+    private final SensorSocketEndpoint instance;
 
     @Autowired
     public SensorSocketConfig(SensorSocketEndpoint instance) {
@@ -41,9 +41,8 @@ public class SensorSocketConfig extends ServerEndpointConfig.Configurator {
     @Override
     public  T getEndpointInstance(Class endpointClass) throws InstantiationException {
         try {
-            @SuppressWarnings("unchecked")
-            final T instance = (T) this.instance;
-            return instance;
+            //noinspection unchecked
+            return (T) this.instance;
         } catch (ClassCastException e) {
             final var e2 =
                     new InstantiationException("Cannot cast SensorSocketEndpoint to desired type");
diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/socket/SensorSocketEndpoint.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/socket/SensorSocketEndpoint.java
index b68cb25..2b54e62 100644
--- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/socket/SensorSocketEndpoint.java
+++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/socket/SensorSocketEndpoint.java
@@ -5,13 +5,18 @@ import ch.usi.inf.sa4.sanmarinoes.smarthut.config.JWTTokenUtils;
 import ch.usi.inf.sa4.sanmarinoes.smarthut.models.Device;
 import ch.usi.inf.sa4.sanmarinoes.smarthut.models.User;
 import ch.usi.inf.sa4.sanmarinoes.smarthut.models.UserRepository;
+import ch.usi.inf.sa4.sanmarinoes.smarthut.service.DevicePopulationService;
 import com.google.common.collect.HashMultimap;
 import com.google.common.collect.Multimap;
 import com.google.common.collect.Multimaps;
 import com.google.gson.Gson;
 import java.io.IOException;
 import java.util.*;
-import javax.websocket.*;
+import javax.websocket.Endpoint;
+import javax.websocket.EndpointConfig;
+import javax.websocket.Session;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Component;
 
@@ -19,21 +24,32 @@ import org.springframework.stereotype.Component;
 @Component
 public class SensorSocketEndpoint extends Endpoint {
 
-    private Gson gson = GsonConfig.socketGson();
+    private static final Logger logger = LoggerFactory.getLogger(SensorSocketEndpoint.class);
 
-    private UserRepository userRepository;
+    private final Gson gson = GsonConfig.socketGson();
 
-    private JWTTokenUtils jwtTokenUtils;
+    private final DevicePopulationService deviceService;
 
-    private Multimap authorizedClients =
+    private final UserRepository userRepository;
+
+    private final JWTTokenUtils jwtTokenUtils;
+
+    private final Multimap authorizedClients =
             Multimaps.synchronizedMultimap(HashMultimap.create());
 
-    private final Map> messages = new HashMap<>();
+    // messages are now stored as strings as a "hack" to capture and clone the state of the device,
+    // since
+    // fromHost and fromGuest are just mutable properties and hibernate caches the object.
+    private final Map> messages = new HashMap<>();
 
     @Autowired
-    public SensorSocketEndpoint(UserRepository userRepository, JWTTokenUtils jwtTokenUtils) {
+    public SensorSocketEndpoint(
+            UserRepository userRepository,
+            JWTTokenUtils jwtTokenUtils,
+            DevicePopulationService deviceService) {
         this.jwtTokenUtils = jwtTokenUtils;
         this.userRepository = userRepository;
+        this.deviceService = deviceService;
     }
 
     /**
@@ -41,18 +57,33 @@ public class SensorSocketEndpoint extends Endpoint {
      *
      * @param device the device update to be sent
      * @param u the user the device belongs
+     * @param fromGuest value for device.fromGuest. This will be put in the device passed.
+     * @param fromHostId value for device.fromHostId. This will be put in the device passed.
+     * @param deleted value for device.deleted. This will be put in the device passed.
      */
-    public void queueDeviceUpdate(Device device, User u) {
+    public void queueDeviceUpdate(
+            Device device, User u, boolean fromGuest, Long fromHostId, boolean deleted) {
         synchronized (messages) {
+            device.setFromGuest(fromGuest);
+            device.setFromHostId(fromHostId);
+            device.setDeleted(deleted);
+
+            // sort of an hack: force the population of thermostat measureTemperature and other
+            // possible
+            // computed fields in the future. This should already be done by the callers of this
+            // method but for
+            // whatever reason they don't do it.
+            deviceService.populateComputedFields(List.of(device));
+
             messages.putIfAbsent(u, new HashMap<>());
-            messages.get(u).put(device.getId(), device);
+            messages.get(u).put(device.getId(), gson.toJson(device));
         }
     }
 
     /** Sends all device updates queued to be sent in a unique WebSocket message */
     public void flushDeviceUpdates() {
         synchronized (messages) {
-            for (Map.Entry> batchForUser : messages.entrySet()) {
+            for (Map.Entry> batchForUser : messages.entrySet()) {
                 broadcast(batchForUser.getKey(), batchForUser.getValue().values());
                 batchForUser.getValue().clear();
             }
@@ -66,18 +97,18 @@ public class SensorSocketEndpoint extends Endpoint {
      * @param messages the message batch to send
      * @param u the user to which to send the message
      */
-    private void broadcast(User u, Collection messages) {
+    void broadcast(User u, Collection messages) {
         if (messages.isEmpty()) return;
         final HashSet sessions = new HashSet<>(authorizedClients.get(u));
         for (Session s : sessions) {
             try {
                 if (s.isOpen()) {
-                    s.getBasicRemote().sendText(gson.toJson(messages));
+                    s.getBasicRemote().sendText("[" + String.join(",", messages) + "]");
                 } else {
                     authorizedClients.remove(u, s);
                 }
             } catch (IOException e) {
-                e.printStackTrace();
+                logger.warn(e.getLocalizedMessage(), e);
             }
         }
     }
@@ -97,7 +128,8 @@ public class SensorSocketEndpoint extends Endpoint {
         } else {
             try {
                 session.close();
-            } catch (IOException ignored) {
+            } catch (IOException e) {
+                logger.warn(e.getLocalizedMessage(), e);
             }
         }
     }
@@ -107,8 +139,8 @@ public class SensorSocketEndpoint extends Endpoint {
 
         try {
             username = jwtTokenUtils.getUsernameFromToken(protocolString);
-        } catch (Throwable ignored) {
-            System.out.println("Token format not valid");
+        } catch (Exception ignored) {
+            logger.info("Token format not valid");
             return null;
         }
 
diff --git a/src/main/resources/application-dev.properties b/src/main/resources/application-dev.properties
index 8720442..81e2a02 100644
--- a/src/main/resources/application-dev.properties
+++ b/src/main/resources/application-dev.properties
@@ -32,4 +32,5 @@ email.registrationRedirect=http://localhost:3000/login
 email.resetpasswordSubject=SmartHut.sm password reset
 email.resetpassword=To reset your password, please click here:
 email.resetpasswordPath=http://localhost:3000/password-reset?token=
-email.resetPasswordRedirect=http://localhost:3000/conf-reset-pass
\ No newline at end of file
+email.resetPasswordRedirect=http://localhost:3000/conf-reset-pass
+camera.videoUrl=/security_camera_videos/security_camera_1.mp4
\ No newline at end of file
diff --git a/src/main/resources/application-prod.properties b/src/main/resources/application-prod.properties
index 1727673..570d997 100644
--- a/src/main/resources/application-prod.properties
+++ b/src/main/resources/application-prod.properties
@@ -39,4 +39,5 @@ email.registrationRedirect=${FRONTEND_URL}/login
 email.resetpasswordSubject=SmartHut.sm password reset
 email.resetpassword=To reset your password, please click here:
 email.resetpasswordPath=${FRONTEND_URL}/password-reset?token=
-email.resetPasswordRedirect=${FRONTEND_URL}/conf-reset-pass
\ No newline at end of file
+email.resetPasswordRedirect=${FRONTEND_URL}/conf-reset-pass
+camera.videoUrl=/security_camera_videos/security_camera_1.mp4
\ No newline at end of file
diff --git a/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/AuthenticationTests.java b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/AuthenticationTests.java
index d13104f..2c0ae6f 100644
--- a/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/AuthenticationTests.java
+++ b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/AuthenticationTests.java
@@ -4,9 +4,7 @@ import static org.assertj.core.api.Assertions.assertThat;
 
 import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.JWTRequest;
 import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.JWTResponse;
-import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.OkResponse;
 import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.UserRegistrationRequest;
-import ch.usi.inf.sa4.sanmarinoes.smarthut.error.DuplicateRegistrationException;
 import ch.usi.inf.sa4.sanmarinoes.smarthut.error.UnauthorizedException;
 import ch.usi.inf.sa4.sanmarinoes.smarthut.models.ConfirmationTokenRepository;
 import ch.usi.inf.sa4.sanmarinoes.smarthut.models.UserRepository;
@@ -42,10 +40,10 @@ public class AuthenticationTests extends SmartHutTest {
 
     @Override
     protected void setUp() {
-        final ResponseEntity res =
+        final ResponseEntity res =
                 this.restTemplate.postForEntity(
-                        this.url("/register"), getDisabledUser(), OkResponse.class);
-        assertThat(res.getStatusCode().equals(HttpStatus.OK));
+                        this.url("/register"), getDisabledUser(), Object.class);
+        assertThat(res.getStatusCode()).isEqualTo(HttpStatus.OK);
 
         registerTestUser(restTemplate, userRepository, tokenRepository);
     }
@@ -55,10 +53,11 @@ public class AuthenticationTests extends SmartHutTest {
         final Map badJSON = Map.of("luciano", "goretti", "danilo", "malusa");
 
         assertThat(
-                this.restTemplate
-                        .postForEntity(url("/register"), badJSON, JWTResponse.class)
-                        .getStatusCode()
-                        .equals(HttpStatus.BAD_REQUEST));
+                        this.restTemplate
+                                .postForEntity(url("/register"), badJSON, JWTResponse.class)
+                                .getStatusCode()
+                                .equals(HttpStatus.BAD_REQUEST))
+                .isTrue();
     }
 
     @Test
@@ -71,12 +70,15 @@ public class AuthenticationTests extends SmartHutTest {
 
         final ResponseEntity res =
                 this.restTemplate.postForEntity(url("/register"), request, JsonObject.class);
-        assertThat(res.getStatusCode().equals(HttpStatus.BAD_REQUEST));
-        assertThat(res.getBody() != null);
+        assertThat(res.getStatusCode().equals(HttpStatus.BAD_REQUEST)).isTrue();
+        assertThat(res.getBody()).isNotNull();
 
         final JsonArray errors = res.getBody().getAsJsonArray("errors");
-        assertThat(errors.size() == 1);
-        assertThat(errors.get(0).getAsJsonObject().get("field").getAsString().equals("password"));
+        assertThat(errors)
+                .allSatisfy(
+                        e ->
+                                assertThat(e.getAsJsonObject().get("field").getAsString())
+                                        .isEqualTo("password"));
     }
 
     @Test
@@ -89,12 +91,15 @@ public class AuthenticationTests extends SmartHutTest {
 
         final ResponseEntity res =
                 this.restTemplate.postForEntity(url("/register"), request, JsonObject.class);
-        assertThat(res.getStatusCode().equals(HttpStatus.BAD_REQUEST));
-        assertThat(res.getBody() != null);
+        assertThat(res.getStatusCode().equals(HttpStatus.BAD_REQUEST)).isTrue();
+        assertThat(res.getBody()).isNotNull();
 
         final JsonArray errors = res.getBody().getAsJsonArray("errors");
-        assertThat(errors.size() == 1);
-        assertThat(errors.get(0).getAsJsonObject().get("field").getAsString().equals("email"));
+        assertThat(errors)
+                .allSatisfy(
+                        e ->
+                                assertThat(e.getAsJsonObject().get("field").getAsString())
+                                        .isEqualTo("email"));
     }
 
     @Test
@@ -106,12 +111,15 @@ public class AuthenticationTests extends SmartHutTest {
 
         final ResponseEntity res =
                 this.restTemplate.postForEntity(url("/register"), request, JsonObject.class);
-        assertThat(res.getStatusCode().equals(HttpStatus.BAD_REQUEST));
-        assertThat(res.getBody() != null);
+        assertThat(res.getStatusCode().equals(HttpStatus.BAD_REQUEST)).isTrue();
+        assertThat(res.getBody() != null).isTrue();
 
         final JsonArray errors = res.getBody().getAsJsonArray("errors");
-        assertThat(errors.size() == 1);
-        assertThat(errors.get(0).getAsJsonObject().get("field").getAsString().equals("name"));
+        assertThat(errors)
+                .allSatisfy(
+                        e ->
+                                assertThat(e.getAsJsonObject().get("field").getAsString())
+                                        .isEqualTo("name"));
     }
 
     @Test
@@ -123,51 +131,15 @@ public class AuthenticationTests extends SmartHutTest {
 
         final ResponseEntity res =
                 this.restTemplate.postForEntity(url("/register"), request, JsonObject.class);
-        assertThat(res.getStatusCode().equals(HttpStatus.BAD_REQUEST));
-        assertThat(res.getBody() != null);
+        assertThat(res.getStatusCode().equals(HttpStatus.BAD_REQUEST)).isTrue();
+        assertThat(res.getBody() != null).isTrue();
 
         final JsonArray errors = res.getBody().getAsJsonArray("errors");
-        assertThat(errors.size() == 1);
-        assertThat(errors.get(0).getAsJsonObject().get("field").getAsString().equals("username"));
-    }
-
-    @Test
-    public void registrationShouldReturnBadRequestWithDuplicateData() {
-        {
-            final ResponseEntity res =
-                    this.restTemplate.postForEntity(
-                            url("/register"),
-                            getDisabledUser(),
-                            DuplicateRegistrationException.class);
-            assertThat(res.getStatusCode().equals(HttpStatus.BAD_REQUEST));
-            assertThat(res.getBody() != null);
-        }
-
-        {
-            final UserRegistrationRequest disabledUserDifferentMail = getDisabledUser();
-            enabledUser.setEmail("another@example.com");
-
-            final ResponseEntity res =
-                    this.restTemplate.postForEntity(
-                            url("/register"),
-                            disabledUserDifferentMail,
-                            DuplicateRegistrationException.class);
-            assertThat(res.getStatusCode().equals(HttpStatus.BAD_REQUEST));
-            assertThat(res.getBody() != null);
-        }
-
-        {
-            final UserRegistrationRequest disabledUserDifferentUsername = getDisabledUser();
-            enabledUser.setUsername("another");
-
-            final ResponseEntity res =
-                    this.restTemplate.postForEntity(
-                            url("/register"),
-                            disabledUserDifferentUsername,
-                            DuplicateRegistrationException.class);
-            assertThat(res.getStatusCode().equals(HttpStatus.BAD_REQUEST));
-            assertThat(res.getBody() != null);
-        }
+        assertThat(errors)
+                .allSatisfy(
+                        j ->
+                                assertThat(j.getAsJsonObject().get("field").getAsString())
+                                        .isEqualTo("username"));
     }
 
     @Test
@@ -178,10 +150,9 @@ public class AuthenticationTests extends SmartHutTest {
         request.setEmail("smarthut.sm@example.com");
         request.setPassword("password");
 
-        final ResponseEntity res =
-                this.restTemplate.postForEntity(url("/register"), request, OkResponse.class);
-        assertThat(res.getStatusCode().equals(HttpStatus.OK));
-        assertThat(res.getBody() != null);
+        final ResponseEntity res =
+                this.restTemplate.postForEntity(url("/register"), request, Object.class);
+        assertThat(res.getStatusCode()).isEqualTo(HttpStatus.OK);
     }
 
     @Test
@@ -189,10 +160,11 @@ public class AuthenticationTests extends SmartHutTest {
         final Map badJSON = Map.of("badkey", 3, "password", "ciaomamma");
 
         assertThat(
-                this.restTemplate
-                        .postForEntity(url("/auth/login"), badJSON, JWTResponse.class)
-                        .getStatusCode()
-                        .equals(HttpStatus.BAD_REQUEST));
+                        this.restTemplate
+                                .postForEntity(url("/auth/login"), badJSON, JWTResponse.class)
+                                .getStatusCode()
+                                .equals(HttpStatus.BAD_REQUEST))
+                .isTrue();
     }
 
     @Test
@@ -204,9 +176,9 @@ public class AuthenticationTests extends SmartHutTest {
         final ResponseEntity res =
                 this.restTemplate.postForEntity(
                         url("/auth/login"), request, UnauthorizedException.class);
-        assertThat(res.getStatusCode().equals(HttpStatus.UNAUTHORIZED));
-        assertThat(res.getBody() != null);
-        assertThat(!res.getBody().isUserDisabled());
+        assertThat(res.getStatusCode().equals(HttpStatus.UNAUTHORIZED)).isTrue();
+        assertThat(res.getBody() != null).isTrue();
+        assertThat(!res.getBody().isUserDisabled()).isTrue();
     }
 
     @Test
@@ -218,22 +190,7 @@ public class AuthenticationTests extends SmartHutTest {
         final ResponseEntity res =
                 this.restTemplate.postForEntity(
                         url("/auth/login"), request, UnauthorizedException.class);
-        assertThat(res.getStatusCode().equals(HttpStatus.UNAUTHORIZED));
-        assertThat(res.getBody() != null);
-        assertThat(res.getBody().isUserDisabled());
-    }
-
-    @Test
-    public void loginShouldReturnTokenWithEnabledUser() {
-        final JWTRequest request = new JWTRequest();
-        request.setUsernameOrEmail("enabled");
-        request.setPassword("password");
-
-        final ResponseEntity res =
-                this.restTemplate.postForEntity(url("/auth/login"), request, JWTResponse.class);
-        assertThat(res.getStatusCode().equals(HttpStatus.OK));
-        assertThat(res.getBody() != null);
-        assertThat(res.getBody().getToken() != null);
-        assertThat(!res.getBody().getToken().isEmpty());
+        assertThat(res.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED);
+        assertThat(res.getBody()).isNotNull();
     }
 }
diff --git a/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/SmartHutTest.java b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/SmartHutTest.java
index f2b737a..95edf1a 100644
--- a/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/SmartHutTest.java
+++ b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/SmartHutTest.java
@@ -2,7 +2,6 @@ package ch.usi.inf.sa4.sanmarinoes.smarthut;
 
 import static org.assertj.core.api.Assertions.assertThat;
 
-import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.OkResponse;
 import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.UserRegistrationRequest;
 import ch.usi.inf.sa4.sanmarinoes.smarthut.models.ConfirmationToken;
 import ch.usi.inf.sa4.sanmarinoes.smarthut.models.ConfirmationTokenRepository;
@@ -15,7 +14,7 @@ import org.springframework.http.ResponseEntity;
 import org.springframework.web.reactive.function.client.WebClient;
 
 public abstract class SmartHutTest {
-    private boolean setupDone = false;
+    private static boolean setupDone = false;
 
     protected final String getBaseURL() {
         return "http://localhost:2000/";
@@ -40,9 +39,9 @@ public abstract class SmartHutTest {
             final TestRestTemplate restTemplate,
             final UserRepository userRepository,
             final ConfirmationTokenRepository tokenRepository) {
-        final ResponseEntity res2 =
-                restTemplate.postForEntity(this.url("/register"), enabledUser, OkResponse.class);
-        assertThat(res2.getStatusCode().equals(HttpStatus.OK));
+        final ResponseEntity res2 =
+                restTemplate.postForEntity(this.url("/register"), enabledUser, Object.class);
+        assertThat(res2.getStatusCode()).isEqualTo(HttpStatus.OK);
 
         final User persistedEnabledUser = userRepository.findByUsername("enabled");
         final ConfirmationToken token = tokenRepository.findByUser(persistedEnabledUser);
@@ -50,13 +49,14 @@ public abstract class SmartHutTest {
         final ResponseEntity res3 =
                 WebClient.create(getBaseURL())
                         .get()
-                        .uri("/register/confirm-account?token=" + token.getConfirmationToken())
+                        .uri("/register/confirm-account?token=" + token.getConfirmToken())
                         .retrieve()
                         .toBodilessEntity()
                         .block();
 
-        assertThat(res3.getStatusCode().is2xxSuccessful());
-        assertThat(userRepository.findByUsername("enabled").getEnabled());
+        assertThat(res3).isNotNull();
+        assertThat(res3.getStatusCode()).isEqualTo(HttpStatus.FOUND);
+        assertThat(userRepository.findByUsername("enabled").isEnabled()).isTrue();
     }
 
     @BeforeEach
diff --git a/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/SmarthutApplicationTests.java b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/SmarthutApplicationTests.java
index dbd7e21..2aaa1b1 100644
--- a/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/SmarthutApplicationTests.java
+++ b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/SmarthutApplicationTests.java
@@ -16,11 +16,8 @@ public class SmarthutApplicationTests extends SmartHutTest {
     @Autowired private TestRestTemplate restTemplate;
 
     @Test
-    public void anonymousGreetingShouldNotBeAuthorized() throws Exception {
-        assertThat(
-                this.restTemplate
-                        .getForEntity(getBaseURL(), Void.class)
-                        .getStatusCode()
-                        .equals(HttpStatus.UNAUTHORIZED));
+    public void anonymousGreetingShouldNotBeAuthorized() {
+        assertThat(this.restTemplate.getForEntity(getBaseURL(), Void.class).getStatusCode())
+                .isEqualTo(HttpStatus.UNAUTHORIZED);
     }
 }
diff --git a/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/CameraConfigurationServiceTests.java b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/CameraConfigurationServiceTests.java
new file mode 100644
index 0000000..69d95dc
--- /dev/null
+++ b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/CameraConfigurationServiceTests.java
@@ -0,0 +1,15 @@
+package ch.usi.inf.sa4.sanmarinoes.smarthut.config;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import org.junit.jupiter.api.Test;
+
+public class CameraConfigurationServiceTests {
+
+    @Test
+    public void test() {
+        final CameraConfigurationService c = new CameraConfigurationService();
+        c.setVideoUrl("cose");
+        assertThat(c.getVideoUrl()).isEqualTo("cose");
+    }
+}
diff --git a/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/EmailConfigurationServiceTests.java b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/EmailConfigurationServiceTests.java
new file mode 100644
index 0000000..92741cb
--- /dev/null
+++ b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/EmailConfigurationServiceTests.java
@@ -0,0 +1,22 @@
+package ch.usi.inf.sa4.sanmarinoes.smarthut.config;
+
+import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
+
+import org.junit.jupiter.api.Test;
+
+public class EmailConfigurationServiceTests {
+    @Test
+    public void test() {
+        final EmailConfigurationService s = new EmailConfigurationService();
+        s.setResetPasswordSubject("s");
+        s.setResetPassword("s");
+        s.setResetPasswordPath("s");
+        s.setResetPasswordRedirect("s");
+        s.setRegistrationRedirect("s");
+        assertThat(s.getResetPasswordSubject()).isEqualTo("s");
+        assertThat(s.getResetPassword()).isEqualTo("s");
+        assertThat(s.getResetPasswordPath()).isEqualTo("s");
+        assertThat(s.getResetPasswordRedirect()).isEqualTo("s");
+        assertThat(s.getRegistrationRedirect()).isEqualTo("s");
+    }
+}
diff --git a/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/GsonTests.java b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/GsonTests.java
new file mode 100644
index 0000000..9d3e97e
--- /dev/null
+++ b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/GsonTests.java
@@ -0,0 +1,42 @@
+package ch.usi.inf.sa4.sanmarinoes.smarthut.config;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.AutomationFastUpdateRequest;
+import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.automation.BooleanTriggerDTO;
+import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.automation.TriggerDTO;
+import ch.usi.inf.sa4.sanmarinoes.smarthut.models.BooleanTrigger;
+import com.google.gson.Gson;
+import com.google.gson.JsonObject;
+import java.util.List;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+@ExtendWith(MockitoExtension.class)
+public class GsonTests {
+
+    private final Gson gson = GsonConfig.gson();
+
+    @Test
+    public void testGson() {
+        BooleanTrigger b = new BooleanTrigger();
+        b.setId(1L);
+        String json = gson.toJson(b);
+        JsonObject o = gson.fromJson(json, JsonObject.class);
+        assertThat(o.get("kind")).isNotNull();
+        assertThat(o.get("kind").getAsString()).isEqualTo("booleanTrigger");
+
+        AutomationFastUpdateRequest a = new AutomationFastUpdateRequest();
+        BooleanTriggerDTO bt = new BooleanTriggerDTO();
+        bt.setDeviceId(42L);
+        a.setTriggers(List.of(bt));
+
+        AutomationFastUpdateRequest a2 =
+                gson.fromJson(gson.toJson(a), AutomationFastUpdateRequest.class);
+        TriggerDTO t = a2.getTriggers().get(0);
+
+        assertThat(t).isExactlyInstanceOf(BooleanTriggerDTO.class);
+        assertThat(t.getDeviceId()).isEqualTo(42L);
+    }
+}
diff --git a/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/JWTTokenUtilsTests.java b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/JWTTokenUtilsTests.java
new file mode 100644
index 0000000..ecc00b0
--- /dev/null
+++ b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/JWTTokenUtilsTests.java
@@ -0,0 +1,39 @@
+package ch.usi.inf.sa4.sanmarinoes.smarthut.config;
+
+import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
+import static org.mockito.Mockito.when;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.test.util.ReflectionTestUtils;
+
+@ExtendWith({MockitoExtension.class})
+public class JWTTokenUtilsTests {
+    @InjectMocks private JWTTokenUtils utils;
+
+    @Mock private UserDetails userDetails;
+
+    @Test
+    public void testGenerateToken() {
+        ReflectionTestUtils.setField(
+                utils,
+                "secret",
+                "One, seven, three, four, six, seven\n"
+                        + "Three, two, one, four, seven, six, charlie, three\n"
+                        + "Two, seven, eight, nine, seven, seven, seven\n"
+                        + "Six, four, three, tango, seven, three, two, victor, seven\n"
+                        + "Three, one, one, seven, eight, eight, eight, seven, three\n"
+                        + "Two, four, seven, six, seven, eight, nine\n"
+                        + "Seven, six, four, three, seven, six\n"
+                        + "Lock");
+        when(userDetails.getUsername()).thenReturn("username");
+        String token = utils.generateToken(userDetails);
+        assertThat(token).isNotNull();
+        assertThat(utils.validateToken(token, userDetails)).isTrue();
+        assertThat(utils.getUsernameFromToken(token)).isEqualTo("username");
+    }
+}
diff --git a/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/RuntimeTypeAdapterFactoryTests.java b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/RuntimeTypeAdapterFactoryTests.java
new file mode 100644
index 0000000..e748b9f
--- /dev/null
+++ b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/RuntimeTypeAdapterFactoryTests.java
@@ -0,0 +1,224 @@
+package ch.usi.inf.sa4.sanmarinoes.smarthut.config;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonParseException;
+import com.google.gson.TypeAdapterFactory;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Copyright (C) 2011 Google Inc.
+ *
+ * 

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + *

http://www.apache.org/licenses/LICENSE-2.0 + * + *

Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ +public final class RuntimeTypeAdapterFactoryTests { + + @Test + public void testRuntimeTypeAdapter() { + RuntimeTypeAdapterFactory rta = + RuntimeTypeAdapterFactory.of(BillingInstrument.class) + .registerSubtype(CreditCard.class); + Gson gson = new GsonBuilder().registerTypeAdapterFactory(rta).create(); + + CreditCard original = new CreditCard("Jesse", 234); + assertEquals( + "{\"type\":\"CreditCard\",\"cvv\":234,\"ownerName\":\"Jesse\"}", + gson.toJson(original, BillingInstrument.class)); + BillingInstrument deserialized = + gson.fromJson( + "{type:'CreditCard',cvv:234,ownerName:'Jesse'}", BillingInstrument.class); + assertEquals("Jesse", deserialized.ownerName); + assertTrue(deserialized instanceof CreditCard); + } + + @Test + public void testRuntimeTypeIsBaseType() { + TypeAdapterFactory rta = + RuntimeTypeAdapterFactory.of(BillingInstrument.class, "type", false) + .registerSubtype(BillingInstrument.class); + Gson gson = new GsonBuilder().registerTypeAdapterFactory(rta).create(); + + BillingInstrument original = new BillingInstrument("Jesse"); + assertEquals( + "{\"type\":\"BillingInstrument\",\"ownerName\":\"Jesse\"}", + gson.toJson(original, BillingInstrument.class)); + BillingInstrument deserialized = + gson.fromJson( + "{type:'BillingInstrument',ownerName:'Jesse'}", BillingInstrument.class); + assertEquals("Jesse", deserialized.ownerName); + } + + @Test + public void testNullBaseType() { + try { + RuntimeTypeAdapterFactory.of(null); + fail(); + } catch (NullPointerException expected) { + } + } + + @Test + public void testNullTypeFieldName() { + try { + RuntimeTypeAdapterFactory.of(BillingInstrument.class, null); + fail(); + } catch (NullPointerException expected) { + } + } + + @Test + public void testNullSubtype() { + RuntimeTypeAdapterFactory rta = + RuntimeTypeAdapterFactory.of(BillingInstrument.class); + try { + rta.registerSubtype(null); + fail(); + } catch (NullPointerException expected) { + } + } + + @Test + public void testNullLabel() { + RuntimeTypeAdapterFactory rta = + RuntimeTypeAdapterFactory.of(BillingInstrument.class); + try { + rta.registerSubtype(CreditCard.class, null); + fail(); + } catch (NullPointerException expected) { + } + } + + @Test + public void testDuplicateSubtype() { + RuntimeTypeAdapterFactory rta = + RuntimeTypeAdapterFactory.of(BillingInstrument.class); + rta.registerSubtype(CreditCard.class, "CC"); + try { + rta.registerSubtype(CreditCard.class, "Visa"); + fail(); + } catch (IllegalArgumentException expected) { + } + } + + @Test + public void testDuplicateLabel() { + RuntimeTypeAdapterFactory rta = + RuntimeTypeAdapterFactory.of(BillingInstrument.class); + rta.registerSubtype(CreditCard.class, "CC"); + try { + rta.registerSubtype(BankTransfer.class, "CC"); + fail(); + } catch (IllegalArgumentException expected) { + } + } + + @Test + public void testDeserializeMissingTypeField() { + TypeAdapterFactory billingAdapter = + RuntimeTypeAdapterFactory.of(BillingInstrument.class) + .registerSubtype(CreditCard.class); + Gson gson = new GsonBuilder().registerTypeAdapterFactory(billingAdapter).create(); + try { + gson.fromJson("{ownerName:'Jesse'}", BillingInstrument.class); + fail(); + } catch (JsonParseException expected) { + } + } + + @Test + public void testDeserializeMissingSubtype() { + TypeAdapterFactory billingAdapter = + RuntimeTypeAdapterFactory.of(BillingInstrument.class) + .registerSubtype(BankTransfer.class); + Gson gson = new GsonBuilder().registerTypeAdapterFactory(billingAdapter).create(); + try { + gson.fromJson("{type:'CreditCard',ownerName:'Jesse'}", BillingInstrument.class); + fail(); + } catch (JsonParseException expected) { + } + } + + @Test + public void testSerializeMissingSubtype() { + TypeAdapterFactory billingAdapter = + RuntimeTypeAdapterFactory.of(BillingInstrument.class) + .registerSubtype(BankTransfer.class); + Gson gson = new GsonBuilder().registerTypeAdapterFactory(billingAdapter).create(); + try { + gson.toJson(new CreditCard("Jesse", 456), BillingInstrument.class); + fail(); + } catch (JsonParseException expected) { + } + } + + @Test + public void testSerializeCollidingTypeFieldName() { + TypeAdapterFactory billingAdapter = + RuntimeTypeAdapterFactory.of(BillingInstrument.class, "cvv") + .registerSubtype(CreditCard.class); + Gson gson = new GsonBuilder().registerTypeAdapterFactory(billingAdapter).create(); + try { + gson.toJson(new CreditCard("Jesse", 456), BillingInstrument.class); + fail(); + } catch (JsonParseException expected) { + } + } + + @Test + public void testSerializeWrappedNullValue() { + TypeAdapterFactory billingAdapter = + RuntimeTypeAdapterFactory.of(BillingInstrument.class) + .registerSubtype(CreditCard.class) + .registerSubtype(BankTransfer.class); + Gson gson = new GsonBuilder().registerTypeAdapterFactory(billingAdapter).create(); + String serialized = + gson.toJson(new BillingInstrumentWrapper(null), BillingInstrumentWrapper.class); + BillingInstrumentWrapper deserialized = + gson.fromJson(serialized, BillingInstrumentWrapper.class); + assertNull(deserialized.instrument); + } + + static class BillingInstrumentWrapper { + BillingInstrument instrument; + + BillingInstrumentWrapper(BillingInstrument instrument) { + this.instrument = instrument; + } + } + + static class BillingInstrument { + private final String ownerName; + + BillingInstrument(String ownerName) { + this.ownerName = ownerName; + } + } + + static class CreditCard extends BillingInstrument { + int cvv; + + CreditCard(String ownerName, int cvv) { + super(ownerName); + this.cvv = cvv; + } + } + + static class BankTransfer extends BillingInstrument { + int bankAccount; + + BankTransfer(String ownerName, int bankAccount) { + super(ownerName); + this.bankAccount = bankAccount; + } + } +} diff --git a/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/SpringFoxConfigTests.java b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/SpringFoxConfigTests.java new file mode 100644 index 0000000..9b9cba3 --- /dev/null +++ b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/SpringFoxConfigTests.java @@ -0,0 +1,18 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.config; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +public class SpringFoxConfigTests { + + private final SpringFoxConfig springFoxConfig = new SpringFoxConfig(); + + @Test + public void testApi() { + assertThat(springFoxConfig.api()).isNotNull(); + } +} diff --git a/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/AuthenticationControllerTests.java b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/AuthenticationControllerTests.java new file mode 100644 index 0000000..667f7f3 --- /dev/null +++ b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/AuthenticationControllerTests.java @@ -0,0 +1,100 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.controller; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.when; + +import ch.usi.inf.sa4.sanmarinoes.smarthut.config.JWTTokenUtils; +import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.JWTRequest; +import ch.usi.inf.sa4.sanmarinoes.smarthut.error.UnauthorizedException; +import ch.usi.inf.sa4.sanmarinoes.smarthut.error.UserNotFoundException; +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.User; +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.UserRepository; +import ch.usi.inf.sa4.sanmarinoes.smarthut.service.JWTUserDetailsService; +import java.security.Principal; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.DisabledException; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.test.context.support.WithMockUser; + +@ExtendWith(MockitoExtension.class) +@WithMockUser(username = "user") +public class AuthenticationControllerTests { + + @InjectMocks private AuthenticationController authenticationController; + + @Mock private UserRepository userRepository; + + @Mock private Principal principal; + + @Mock private AuthenticationManager authenticationManager; + + @Mock private JWTUserDetailsService jwtUserDetailsService; + + @Mock private JWTTokenUtils jwtTokenUtils; + + @Test + public void testProfile() { + final User u = new User(); + when(principal.getName()).thenReturn("user"); + when(userRepository.findByUsername("user")).thenReturn(u); + assertThat(authenticationController.profile(principal)).isSameAs(u); + } + + @Test + public void testLogin() throws UnauthorizedException, UserNotFoundException { + final UsernamePasswordAuthenticationToken u = + new UsernamePasswordAuthenticationToken("username", "password"); + final UsernamePasswordAuthenticationToken v = + new UsernamePasswordAuthenticationToken("disabled", "password"); + final UsernamePasswordAuthenticationToken z = + new UsernamePasswordAuthenticationToken("username", "wrongpassword"); + when(authenticationManager.authenticate(u)).thenReturn(null); + when(authenticationManager.authenticate(v)).thenThrow(DisabledException.class); + when(authenticationManager.authenticate(z)).thenThrow(BadCredentialsException.class); + + final UserDetails r = Mockito.mock(UserDetails.class); + when(jwtUserDetailsService.loadUserByUsername("username")).thenReturn(r); + when(jwtTokenUtils.generateToken(r)).thenReturn("token"); + + final User user = new User(); + user.setUsername("username"); + + when(userRepository.findByEmailIgnoreCase("email@example.com")).thenReturn(user); + when(userRepository.findByEmailIgnoreCase("none@example.com")).thenReturn(null); + + assertThatThrownBy( + () -> + authenticationController.login( + new JWTRequest("none@example.com", "password"))) + .isInstanceOf(UserNotFoundException.class); + assertThat( + authenticationController + .login(new JWTRequest("email@example.com", "password")) + .getJwttoken()) + .isEqualTo("token"); + assertThatThrownBy( + () -> + authenticationController.login( + new JWTRequest("disabled", "password"))) + .isInstanceOf(UnauthorizedException.class); + assertThatThrownBy( + () -> + authenticationController.login( + new JWTRequest("username", "wrongpassword"))) + .isInstanceOf(UnauthorizedException.class); + assertThat( + authenticationController + .login(new JWTRequest("username", "password")) + .getJwttoken()) + .isEqualTo("token"); + } +} diff --git a/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/AutomationControllerTests.java b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/AutomationControllerTests.java new file mode 100644 index 0000000..39d8193 --- /dev/null +++ b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/AutomationControllerTests.java @@ -0,0 +1,192 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.controller; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.AutomationFastUpdateRequest; +import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.AutomationSaveRequest; +import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.automation.BooleanConditionDTO; +import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.automation.BooleanTriggerDTO; +import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.automation.ScenePriorityDTO; +import ch.usi.inf.sa4.sanmarinoes.smarthut.error.NotFoundException; +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.*; +import java.security.Principal; +import java.util.List; +import java.util.Optional; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.security.test.context.support.WithMockUser; + +@ExtendWith(MockitoExtension.class) +@WithMockUser(username = "user") +public class AutomationControllerTests { + @InjectMocks private AutomationController automationController; + + @Mock private UserRepository userRepository; + + @Mock private AutomationRepository automationRepository; + + @Mock private Principal mockPrincipal; + + @Mock private TriggerRepository> triggerRepository; + + @Mock private ConditionRepository> conditionRepository; + + @Mock private ScenePriorityRepository scenePriorityRepository; + + private final User u; + + public AutomationControllerTests() { + u = new User(); + u.setName("user"); + u.setId(1L); + } + + @Test + public void testGetAll() { + when(mockPrincipal.getName()).thenReturn("user"); + when(userRepository.findByUsername("user")).thenReturn(u); + when(automationRepository.findAllByUserId(1L)).thenReturn(List.of()); + assertThat(automationController.getAll(null, mockPrincipal)).isEmpty(); + } + + @Test + public void testGet() throws NotFoundException { + Automation a = new Automation(); + when(automationRepository.findById(1L)).thenReturn(Optional.of(a)); + when(automationRepository.findById(2L)).thenReturn(Optional.empty()); + + assertThat(automationController.get(1L)).isSameAs(a); + assertThatThrownBy(() -> automationController.get(2L)) + .isInstanceOf(NotFoundException.class); + } + + private void equalToRequest(Automation created, AutomationSaveRequest a) { + assertThat(created.getName()).isEqualTo(a.getName()); + assertThat(created.getUserId()).isEqualTo(1L); + } + + @Test + public void testCreate() { + when(mockPrincipal.getName()).thenReturn("user"); + when(userRepository.findByUsername("user")).thenReturn(u); + when(automationRepository.save(any(Automation.class))).thenAnswer(i -> i.getArguments()[0]); + + AutomationSaveRequest a = new AutomationSaveRequest(26, "Automation"); + Automation created = automationController.create(a, mockPrincipal); + assertThat(created.getId()).isEqualTo(0L); + equalToRequest(created, a); + } + + @Test + public void testUpdate() throws NotFoundException { + when(mockPrincipal.getName()).thenReturn("user"); + when(userRepository.findByUsername("user")).thenReturn(u); + final Automation old = new Automation(); + old.setId(42L); + old.setName("Old Name"); + + when(automationRepository.save(any(Automation.class))).thenAnswer(i -> i.getArguments()[0]); + when(automationRepository.findById(42L)).thenReturn(Optional.of(old)); + when(automationRepository.findById(43L)).thenReturn(Optional.empty()); + + AutomationSaveRequest a = new AutomationSaveRequest(42L, "New Name"); + + Automation created = automationController.update(a, mockPrincipal); + assertThat(created.getId()).isEqualTo(42L); + equalToRequest(created, a); + + a.setId(43L); + assertThatThrownBy(() -> automationController.update(a, mockPrincipal)) + .isInstanceOf(NotFoundException.class); + } + + @Test + public void testFastUpdate() throws NotFoundException { + when(mockPrincipal.getName()).thenReturn("user"); + when(userRepository.findByUsername("user")).thenReturn(u); + + final Automation a = new Automation(); + a.setId(42L); + a.setName("Old Name"); + + final RangeTrigger rt = new RangeTrigger(); + final RangeCondition co = new RangeCondition(); + + a.getTriggers().add(rt); + a.getConditions().add(co); + + final AutomationFastUpdateRequest f = new AutomationFastUpdateRequest(); + f.setName("New name"); + f.setId(43L); + f.setScenes(List.of(new ScenePriorityDTO(30L, 1))); + + final BooleanConditionDTO b = new BooleanConditionDTO(true); + b.setDeviceId(1L); + f.setConditions(List.of(b)); + + final BooleanTriggerDTO c = new BooleanTriggerDTO(true); + c.setDeviceId(1L); + f.setTriggers(List.of(c)); + + doAnswer( + i -> { + a.getTriggers().clear(); + return null; + }) + .when(triggerRepository) + .deleteAllByAutomationId(42L); + doAnswer( + i -> { + a.getConditions().clear(); + return null; + }) + .when(conditionRepository) + .deleteAllByAutomationId(42L); + doAnswer( + i -> { + a.getScenes().clear(); + return null; + }) + .when(scenePriorityRepository) + .deleteAllByAutomationId(42L); + + when(automationRepository.findByIdAndUserId(42L, 1L)).thenReturn(Optional.of(a)); + when(automationRepository.findByIdAndUserId(43L, 1L)).thenReturn(Optional.empty()); + + when(conditionRepository.saveAll(any())).thenAnswer(i -> i.getArguments()[0]); + when(triggerRepository.saveAll(any())).thenAnswer(i -> i.getArguments()[0]); + when(scenePriorityRepository.saveAll(any())).thenAnswer(i -> i.getArguments()[0]); + when(automationRepository.save(any())).thenAnswer(i -> i.getArguments()[0]); + + assertThatThrownBy(() -> automationController.fastUpdate(f, mockPrincipal)) + .isInstanceOf(NotFoundException.class); + + f.setId(42L); + + Automation saved = automationController.fastUpdate(f, mockPrincipal); + + assertThat(saved.getId()).isEqualTo(42L); + assertThat(saved.getName()).isEqualTo("New name"); + assertThat(saved.getTriggers()).doesNotContain(rt); + assertThat(saved.getConditions()).doesNotContain(co); + assertThat(saved.getTriggers().size()).isEqualTo(1); + assertThat(saved.getConditions().size()).isEqualTo(1); + assertThat(saved.getScenes().size()).isEqualTo(1); + } + + @Test + public void testDelete() { + final Automation old = new Automation(); + old.setId(42L); + old.setName("Old Name"); + doNothing().when(automationRepository).deleteById(42L); + Assertions.assertDoesNotThrow(() -> automationController.delete(42L)); + } +} diff --git a/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/BooleanConditionControllerTests.java b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/BooleanConditionControllerTests.java new file mode 100644 index 0000000..b38b26c --- /dev/null +++ b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/BooleanConditionControllerTests.java @@ -0,0 +1,104 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.controller; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.when; + +import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.BooleanConditionOrTriggerSaveRequest; +import ch.usi.inf.sa4.sanmarinoes.smarthut.error.NotFoundException; +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.BooleanCondition; +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.BooleanConditionRepository; +import java.security.Principal; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import lombok.SneakyThrows; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +@DisplayName("BooleanConditionController tests") +@ExtendWith(MockitoExtension.class) +@WithMockUser(username = "user") +public class BooleanConditionControllerTests { + @InjectMocks private BooleanConditionController controller; + + @Mock private BooleanConditionRepository repository; + + @Mock private Principal principal; + + @Test + public void testCreate() { + BooleanCondition condition = new BooleanCondition(); + condition.setOn(true); + condition.setDeviceId(1L); + condition.setAutomationId(2L); + when(repository.save(any())).thenAnswer(i -> i.getArgument(0)); + BooleanConditionOrTriggerSaveRequest toSend = new BooleanConditionOrTriggerSaveRequest(); + toSend.setAutomationId(2L); + toSend.setDeviceId(1L); + toSend.setOn(false); + BooleanCondition returned = controller.create(toSend); + assertThat(returned.isOn()).isEqualTo(toSend.isOn()); + assertThat(returned.getAutomationId()).isEqualTo(toSend.getAutomationId()); + assertThat(returned.getDeviceId()).isEqualTo(toSend.getDeviceId()); + } + + @Test + @SneakyThrows(NotFoundException.class) + public void testUpdate() { + BooleanCondition condition = new BooleanCondition(); + condition.setOn(true); + condition.setDeviceId(1L); + condition.setAutomationId(2L); + when(repository.save(condition)).thenReturn(condition); + BooleanConditionOrTriggerSaveRequest toSend = new BooleanConditionOrTriggerSaveRequest(); + toSend.setId(34L); + toSend.setAutomationId(2L); + toSend.setDeviceId(1L); + toSend.setOn(false); + when(repository.findById(34L)).thenReturn(Optional.of(condition)); + BooleanCondition returned = controller.update(toSend); + assertThat(returned.isOn()).isEqualTo(toSend.isOn()); + assertThat(returned.getAutomationId()).isEqualTo(toSend.getAutomationId()); + assertThat(returned.getDeviceId()).isEqualTo(toSend.getDeviceId()); + } + + @Test + public void testGetAll() { + BooleanCondition condition1 = new BooleanCondition(); + condition1.setAutomationId(1L); + BooleanCondition condition2 = new BooleanCondition(); + condition2.setAutomationId(1L); + BooleanCondition condition3 = new BooleanCondition(); + condition3.setAutomationId(1L); + ArrayList list = new ArrayList<>(); + list.add(condition1); + list.add(condition2); + list.add(condition3); + when(repository.findAllByAutomationId(42L)).thenReturn(list); + List returned = controller.getAll(42L); + assertThat(returned).contains(condition1); + assertThat(returned).contains(condition2); + assertThat(returned).contains(condition3); + assertThat(returned.size()).isEqualTo(3); + } + + @Test + public void testDelete() { + doNothing().when(repository).deleteById(eq(42L)); + MockHttpServletRequest request = new MockHttpServletRequest(); + RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request)); + Assertions.assertDoesNotThrow(() -> controller.delete(42L)); + } +} diff --git a/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/BooleanTriggerControllerTests.java b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/BooleanTriggerControllerTests.java new file mode 100644 index 0000000..ca3c613 --- /dev/null +++ b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/BooleanTriggerControllerTests.java @@ -0,0 +1,104 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.controller; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.when; + +import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.BooleanConditionOrTriggerSaveRequest; +import ch.usi.inf.sa4.sanmarinoes.smarthut.error.NotFoundException; +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.BooleanTrigger; +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.BooleanTriggerRepository; +import java.security.Principal; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import lombok.SneakyThrows; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +@DisplayName("BooleanTriggerController tests") +@ExtendWith(MockitoExtension.class) +@WithMockUser(username = "user") +public class BooleanTriggerControllerTests { + @InjectMocks private BooleanTriggerController controller; + + @Mock private BooleanTriggerRepository repository; + + @Mock private Principal principal; + + @Test + public void testCreate() { + BooleanTrigger condition = new BooleanTrigger(); + condition.setOn(true); + condition.setDeviceId(1L); + condition.setAutomationId(2L); + when(repository.save(any())).thenAnswer(i -> i.getArgument(0)); + BooleanConditionOrTriggerSaveRequest toSend = new BooleanConditionOrTriggerSaveRequest(); + toSend.setAutomationId(2L); + toSend.setDeviceId(1L); + toSend.setOn(false); + BooleanTrigger returned = controller.create(toSend); + assertThat(returned.isOn()).isEqualTo(toSend.isOn()); + assertThat(returned.getAutomationId()).isEqualTo(toSend.getAutomationId()); + assertThat(returned.getDeviceId()).isEqualTo(toSend.getDeviceId()); + } + + @Test + @SneakyThrows(NotFoundException.class) + public void testUpdate() { + BooleanTrigger condition = new BooleanTrigger(); + condition.setOn(true); + condition.setDeviceId(1L); + condition.setAutomationId(2L); + when(repository.save(condition)).thenReturn(condition); + BooleanConditionOrTriggerSaveRequest toSend = new BooleanConditionOrTriggerSaveRequest(); + toSend.setId(34L); + toSend.setAutomationId(2L); + toSend.setDeviceId(1L); + toSend.setOn(false); + when(repository.findById(34L)).thenReturn(Optional.of(condition)); + BooleanTrigger returned = controller.update(toSend); + assertThat(returned.isOn()).isEqualTo(toSend.isOn()); + assertThat(returned.getAutomationId()).isEqualTo(toSend.getAutomationId()); + assertThat(returned.getDeviceId()).isEqualTo(toSend.getDeviceId()); + } + + @Test + public void testGetAll() { + BooleanTrigger condition1 = new BooleanTrigger(); + condition1.setAutomationId(1L); + BooleanTrigger condition2 = new BooleanTrigger(); + condition2.setAutomationId(1L); + BooleanTrigger condition3 = new BooleanTrigger(); + condition3.setAutomationId(1L); + ArrayList list = new ArrayList<>(); + list.add(condition1); + list.add(condition2); + list.add(condition3); + when(repository.findAllByAutomationId(42L)).thenReturn(list); + List returned = controller.getAll(42L); + assertThat(returned).contains(condition1); + assertThat(returned).contains(condition2); + assertThat(returned).contains(condition3); + assertThat(returned.size()).isEqualTo(3); + } + + @Test + public void testDelete() { + doNothing().when(repository).deleteById(eq(42L)); + MockHttpServletRequest request = new MockHttpServletRequest(); + RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request)); + Assertions.assertDoesNotThrow(() -> controller.delete(42L)); + } +} diff --git a/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/ButtonDimmerControllerTests.java b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/ButtonDimmerControllerTests.java new file mode 100644 index 0000000..25cbb91 --- /dev/null +++ b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/ButtonDimmerControllerTests.java @@ -0,0 +1,117 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.controller; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.when; + +import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.ButtonDimmerDimRequest; +import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.GenericDeviceSaveRequest; +import ch.usi.inf.sa4.sanmarinoes.smarthut.error.NotFoundException; +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.*; +import ch.usi.inf.sa4.sanmarinoes.smarthut.service.DeviceService; +import java.security.Principal; +import java.util.Optional; +import java.util.Set; +import lombok.SneakyThrows; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +@ExtendWith(MockitoExtension.class) +@WithMockUser(username = "user") +@DisplayName("ButtonDimmer controller test") +public class ButtonDimmerControllerTests { + @InjectMocks private ButtonDimmerController controller; + + @Mock private DeviceService service; + + @Mock private ButtonDimmerRepository repository; + + @Mock Principal principal; + + @Test + public void testGetId() throws NotFoundException { + ButtonDimmer dimmer = new ButtonDimmer(); + dimmer.setId(42L); + when(repository.findById(42L)).thenReturn(Optional.of(dimmer)); + when(repository.findById(1L)).thenReturn(Optional.empty()); + assertThat(controller.findById(42L)).isSameAs(dimmer); + assertThatThrownBy(() -> controller.findById(1L)).isInstanceOf(NotFoundException.class); + } + + @Test + @SneakyThrows(NotFoundException.class) + public void testCreate() { + when(principal.getName()).thenReturn("user"); + doNothing().when(service).throwIfRoomNotOwned(anyLong(), eq("user")); + when(service.saveAsOwner(any(ButtonDimmer.class), eq("user"))) + .thenAnswer(i -> i.getArguments()[0]); + + MockHttpServletRequest request = new MockHttpServletRequest(); + RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request)); + GenericDeviceSaveRequest toSend = new GenericDeviceSaveRequest(); + toSend.setName("dimmer"); + toSend.setRoomId(42L); + ButtonDimmer dimmer = controller.create(toSend, principal); + assertThat(dimmer.getName()).isEqualTo(toSend.getName()); + assertThat(dimmer.getRoomId()).isEqualTo(toSend.getRoomId()); + } + + @Test + @SneakyThrows(NotFoundException.class) + public void testDelete() { + when(principal.getName()).thenReturn("user"); + doNothing().when(service).deleteByIdAsOwner(eq(42L), eq("user")); + MockHttpServletRequest request = new MockHttpServletRequest(); + RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request)); + Assertions.assertDoesNotThrow(() -> controller.delete(42L, principal)); + } + + @Test + @SneakyThrows(NotFoundException.class) + public void testDimUp() { + when(principal.getName()).thenReturn("user"); + ButtonDimmer button = new ButtonDimmer(); + Curtains curtains = new Curtains(); + DimmableLight light = new DimmableLight(); + button.addDimmable(curtains); + button.addDimmable(light); + ButtonDimmerDimRequest toSend = new ButtonDimmerDimRequest(); + toSend.setId(42L); + toSend.setDimType(ButtonDimmerDimRequest.DimType.UP); + when(repository.findByIdAndUsername(42L, "user")).thenReturn(Optional.of(button)); + MockHttpServletRequest request = new MockHttpServletRequest(); + RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request)); + Set set = controller.dim(toSend, principal); + assertThat(set.size()).isEqualTo(button.getOutputs().size()); + } + + @Test + @SneakyThrows(NotFoundException.class) + public void testDimDown() { + when(principal.getName()).thenReturn("user"); + ButtonDimmer button = new ButtonDimmer(); + Curtains curtains = new Curtains(); + DimmableLight light = new DimmableLight(); + button.addDimmable(curtains); + button.addDimmable(light); + ButtonDimmerDimRequest toSend = new ButtonDimmerDimRequest(); + toSend.setId(42L); + toSend.setDimType(ButtonDimmerDimRequest.DimType.DOWN); + when(repository.findByIdAndUsername(42L, "user")).thenReturn(Optional.of(button)); + MockHttpServletRequest request = new MockHttpServletRequest(); + RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request)); + Set set = controller.dim(toSend, principal); + assertThat(set.size()).isEqualTo(button.getOutputs().size()); + } +} diff --git a/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/CurtainsControllerTests.java b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/CurtainsControllerTests.java new file mode 100644 index 0000000..448a06e --- /dev/null +++ b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/CurtainsControllerTests.java @@ -0,0 +1,128 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.controller; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.when; + +import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.DimmableSaveRequest; +import ch.usi.inf.sa4.sanmarinoes.smarthut.error.DuplicateStateException; +import ch.usi.inf.sa4.sanmarinoes.smarthut.error.NotFoundException; +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.*; +import ch.usi.inf.sa4.sanmarinoes.smarthut.service.DeviceService; +import java.security.Principal; +import java.util.Optional; +import lombok.SneakyThrows; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +@ExtendWith(MockitoExtension.class) +@WithMockUser(username = "user") +@DisplayName("Curtains controller test") +public class CurtainsControllerTests { + @InjectMocks CurtainsController controller; + + @Mock private DeviceService deviceService; + + @Mock private CurtainsRepository curtainsService; + + @Mock private SceneRepository sceneRepository; + + @Mock private StateRepository stateRepository; + + @Mock Principal principal; + + @Test + @SneakyThrows(NotFoundException.class) + public void testCreate() { + when(principal.getName()).thenReturn("user"); + Curtains curtains = new Curtains(); + curtains.setIntensity(50); + curtains.setName("name"); + curtains.setRoomId(42L); + when(deviceService.saveAsOwner(any(Curtains.class), eq("user"))).thenReturn(curtains); + doNothing().when(deviceService).throwIfRoomNotOwned(42L, "user"); + DimmableSaveRequest toSend = new DimmableSaveRequest(); + toSend.setRoomId(42L); + toSend.setIntensity(50); + toSend.setName("name"); + Curtains returned = controller.create(toSend, principal); + assertThat(returned.getRoomId()).isEqualTo(toSend.getRoomId()); + assertThat(returned.getName()).isEqualTo(toSend.getName()); + assertTrue(returned.isOn()); + } + + @Test + @SneakyThrows(NotFoundException.class) + public void testUpdate() { + when(principal.getName()).thenReturn("user"); + Curtains curtains = new Curtains(); + curtains.setId(1L); + curtains.setIntensity(50); + curtains.setName("name"); + curtains.setRoomId(42L); + when(deviceService.saveAsOwner(any(Curtains.class), eq("user"))).thenReturn(curtains); + DimmableSaveRequest toSend = new DimmableSaveRequest(); + toSend.setId(1L); + toSend.setRoomId(42L); + toSend.setIntensity(50); + toSend.setName("name"); + when(curtainsService.findById(1L)).thenReturn(Optional.of(curtains)); + Curtains returned = controller.update(toSend, principal); + assertThat(returned.getRoomId()).isEqualTo(toSend.getRoomId()); + assertThat(returned.getName()).isEqualTo(toSend.getName()); + assertTrue(returned.isOn()); + } + + @Test + @SneakyThrows(NotFoundException.class) + public void testDelete() { + when(principal.getName()).thenReturn("user"); + doNothing().when(deviceService).deleteByIdAsOwner(eq(42L), eq("user")); + MockHttpServletRequest request = new MockHttpServletRequest(); + RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request)); + Assertions.assertDoesNotThrow(() -> controller.delete(42L, principal)); + } + + @Test + public void testSceneBinding() { + when(principal.getName()).thenReturn("user"); + Curtains curtains = new Curtains(); + when(curtainsService.findByIdAndUsername(24L, "user")).thenReturn(Optional.of(curtains)); + Scene scene = new Scene(); + scene.setId(1L); + SwitchableState state = new SwitchableState(); + state.setSceneId(1L); + State s = curtains.cloneState(); + when(sceneRepository.findById(1L)).thenReturn(Optional.of(scene)); + when(stateRepository.countByDeviceIdAndSceneId(24L, 1L)).thenReturn(0); + Assertions.assertDoesNotThrow(() -> controller.sceneBinding(24L, 1L, principal)); + } + + @Test + public void testSceneBinding2() { + when(principal.getName()).thenReturn("user"); + Curtains curtains = new Curtains(); + when(curtainsService.findByIdAndUsername(24L, "user")).thenReturn(Optional.of(curtains)); + Scene scene = new Scene(); + scene.setId(1L); + SwitchableState state = new SwitchableState(); + state.setSceneId(1L); + State s = curtains.cloneState(); + when(sceneRepository.findById(1L)).thenReturn(Optional.of(scene)); + when(stateRepository.countByDeviceIdAndSceneId(24L, 1L)).thenReturn(2); + assertThatThrownBy(() -> controller.sceneBinding(24L, 1L, principal)) + .isInstanceOf(DuplicateStateException.class); + } +} diff --git a/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/DeviceControllerTests.java b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/DeviceControllerTests.java new file mode 100644 index 0000000..2ac8566 --- /dev/null +++ b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/DeviceControllerTests.java @@ -0,0 +1,123 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.controller; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.*; + +import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.DeviceSaveRequest; +import ch.usi.inf.sa4.sanmarinoes.smarthut.error.BadDataException; +import ch.usi.inf.sa4.sanmarinoes.smarthut.error.NotFoundException; +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.*; +import ch.usi.inf.sa4.sanmarinoes.smarthut.service.DeviceService; +import java.security.Principal; +import java.util.List; +import java.util.Optional; +import lombok.SneakyThrows; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.security.test.context.support.WithMockUser; + +@ExtendWith(MockitoExtension.class) +@WithMockUser(username = "user") +public class DeviceControllerTests { + @InjectMocks private DeviceController deviceController; + + @Mock private DeviceService deviceService; + @Mock private DeviceRepository deviceRepository; + @Mock private RoomRepository roomRepository; + @Mock private Principal mockPrincipal; + + private User user; + private Room room; + + public DeviceControllerTests() { + room = new Room(); + room.setId(1L); + + user = new User(); + user.setId(1L); + user.setName("user"); + } + + @DisplayName("check update device") + @Test + public void updateDeviceRepositoryTest() { + DeviceSaveRequest dto = new DeviceSaveRequest(); + dto.setId(3L); + dto.setName("dto"); + dto.setRoomId(room.getId()); + + when(deviceRepository.findByIdAndUsername(dto.getId(), mockPrincipal.getName())) + .thenReturn(Optional.empty()); + + assertThatThrownBy(() -> deviceController.update(dto, mockPrincipal)) + .isInstanceOf(NotFoundException.class); + } + + @DisplayName("check update device") + @Test + public void updateRoomRepositoryTest() { + DeviceSaveRequest dto = new DeviceSaveRequest(); + dto.setId(3L); + dto.setName("dto"); + dto.setRoomId(room.getId()); + + when(deviceRepository.findByIdAndUsername(dto.getId(), mockPrincipal.getName())) + .thenReturn(Optional.of(new RegularLight())); + when(roomRepository.findByIdAndUsername(room.getId(), mockPrincipal.getName())) + .thenReturn(Optional.empty()); + + assertThatThrownBy(() -> deviceController.update(dto, mockPrincipal)) + .isInstanceOf(BadDataException.class); + } + + @DisplayName("check update device") + @Test + @SneakyThrows({NotFoundException.class, BadDataException.class}) + public void updateTest() { + DeviceSaveRequest dto = new DeviceSaveRequest(); + dto.setId(3L); + dto.setName("dto"); + dto.setRoomId(room.getId()); + + Device rl = new RegularLight(); + rl.setRoomId(dto.getRoomId()); + rl.setName(dto.getName()); + + when(deviceRepository.findByIdAndUsername(dto.getId(), mockPrincipal.getName())) + .thenReturn(Optional.of(rl)); + when(roomRepository.findByIdAndUsername(room.getId(), mockPrincipal.getName())) + .thenReturn(Optional.of(room)); + when(deviceService.saveAsOwner(rl, mockPrincipal.getName())).thenReturn(rl); + + Device returned = deviceController.update(dto, mockPrincipal); + assertThat(returned.getRoomId()).isSameAs(rl.getRoomId()); + assertThat(returned.getName()).isSameAs(rl.getName()); + } + + @DisplayName("check if list is empty") + @Test + @SneakyThrows(NotFoundException.class) + public void getAllEmptyTest() { + when(mockPrincipal.getName()).thenReturn("user"); + List l = deviceController.getAll(user.getId(), mockPrincipal); + assertThat(l.isEmpty()).isTrue(); + } + + @DisplayName("check if list contains elements added") + @Test + @SneakyThrows(NotFoundException.class) + public void getAllTest() throws BadDataException { + when(mockPrincipal.getName()).thenReturn("user"); + Device d1 = new RegularLight(); + deviceService.saveAsOwner(d1, mockPrincipal.getName()); + Device d2 = new SmartPlug(); + deviceService.saveAsOwner(d2, mockPrincipal.getName()); + + assertThat(deviceController.getAll(user.getId(), mockPrincipal)).isNotNull(); + } +} diff --git a/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/DimmableLightControllerTests.java b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/DimmableLightControllerTests.java new file mode 100644 index 0000000..cca260e --- /dev/null +++ b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/DimmableLightControllerTests.java @@ -0,0 +1,171 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.controller; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.when; + +import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.DimmableSaveRequest; +import ch.usi.inf.sa4.sanmarinoes.smarthut.error.DuplicateStateException; +import ch.usi.inf.sa4.sanmarinoes.smarthut.error.NotFoundException; +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.*; +import ch.usi.inf.sa4.sanmarinoes.smarthut.service.DeviceService; +import java.security.Principal; +import java.util.Optional; +import lombok.SneakyThrows; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +@ExtendWith(MockitoExtension.class) +@WithMockUser(username = "user") +public class DimmableLightControllerTests { + + @InjectMocks private DimmableLightController dimmableLightController; + + @Mock private DimmableLightRepository dimmableLightRepository; + + @Mock private SceneRepository sceneRepository; + + @Mock private StateRepository stateRepository; + + @Mock private DeviceService deviceService; + + @Mock private Principal mockPrincipal; + + @Mock private UserRepository userRepository; + + @Mock private DeviceRepository deviceRepository; + + @BeforeEach + public void setup() { + when(mockPrincipal.getName()).thenReturn("user"); + } + + private void checkDimmableLightAgainstRequest( + final DimmableLight toCheck, final DimmableSaveRequest request) { + assertThat(toCheck).isNotNull(); + assertThat(toCheck.getIntensity()).isEqualTo(request.getIntensity()); + assertThat(toCheck.getName()).isEqualTo(request.getName()); + assertThat(toCheck.getRoomId()).isEqualTo(request.getRoomId()); + } + + @Test + @DisplayName("when creating should return the same object") + @SneakyThrows(NotFoundException.class) + public void testCreate() { + doNothing().when(deviceService).throwIfRoomNotOwned(anyLong(), eq("user")); + when(deviceService.saveAsOwner(any(DimmableLight.class), eq("user"))) + .thenAnswer(i -> i.getArguments()[0]); + + MockHttpServletRequest request = new MockHttpServletRequest(); + RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request)); + + final DimmableSaveRequest toSend = new DimmableSaveRequest(); + toSend.setName("rl"); + toSend.setRoomId(20L); + toSend.setIntensity(35); + + final DimmableLight regularLight = dimmableLightController.create(toSend, mockPrincipal); + + checkDimmableLightAgainstRequest(regularLight, toSend); + } + + @Test + @DisplayName("when updating should return the same object") + @SneakyThrows(NotFoundException.class) + public void testUpdate() { + + final DimmableSaveRequest toSend = new DimmableSaveRequest(); + toSend.setId(50L); + toSend.setName("rl"); + toSend.setRoomId(20L); + toSend.setIntensity(35); + + final DimmableLight toUpdate = new DimmableLight(); + toUpdate.setName("OOO"); + toUpdate.setRoomId(40L); + toSend.setId(50L); + toUpdate.setOn(false); + + when(dimmableLightRepository.findByIdAndUserId(anyLong(), anyLong())) + .thenReturn(java.util.Optional.of(toUpdate)); + + when(deviceService.saveAsGuest(any(DimmableLight.class), eq("user"), anyLong())) + .thenAnswer(i -> i.getArguments()[0]); + + User guest = new User(); + User host = new User(); + host.getGuests().add(guest); + guest.getHosts().add(host); + + when(userRepository.findById(20L)).thenReturn(Optional.of(host)); + when(userRepository.findByUsername("user")).thenReturn(guest); + + MockHttpServletRequest request = new MockHttpServletRequest(); + RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request)); + + final DimmableLight dimmableLight = + dimmableLightController.update(toSend, mockPrincipal, 20L); + + checkDimmableLightAgainstRequest(dimmableLight, toSend); + } + + @Test + @DisplayName("an existing id should succeed") + @SneakyThrows(NotFoundException.class) + public void testDelete() { + + doNothing().when(deviceService).deleteByIdAsOwner(eq(42L), eq("user")); + + MockHttpServletRequest request = new MockHttpServletRequest(); + RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request)); + + Assertions.assertDoesNotThrow(() -> dimmableLightController.delete(42L, mockPrincipal)); + } + + @Test + public void testSceneBinding() { + DimmableLight dimmableLight = new DimmableLight(); + when(dimmableLightRepository.findByIdAndUsername(24L, "user")) + .thenReturn(Optional.of(dimmableLight)); + Scene scene = new Scene(); + scene.setId(1L); + DimmableState state = new DimmableState(); + state.setSceneId(1L); + State s = dimmableLight.cloneState(); + when(sceneRepository.findById(1L)).thenReturn(Optional.of(scene)); + when(stateRepository.countByDeviceIdAndSceneId(24L, 1L)).thenReturn(0); + Assertions.assertDoesNotThrow( + () -> dimmableLightController.sceneBinding(24L, 1L, mockPrincipal)); + } + + @Test + public void testSceneBinding2() { + when(mockPrincipal.getName()).thenReturn("user"); + DimmableLight dimmableLight = new DimmableLight(); + when(dimmableLightRepository.findByIdAndUsername(24L, "user")) + .thenReturn(Optional.of(dimmableLight)); + Scene scene = new Scene(); + scene.setId(1L); + DimmableState state = new DimmableState(); + state.setSceneId(1L); + State s = dimmableLight.cloneState(); + when(sceneRepository.findById(1L)).thenReturn(Optional.of(scene)); + when(stateRepository.countByDeviceIdAndSceneId(24L, 1L)).thenReturn(2); + assertThatThrownBy(() -> dimmableLightController.sceneBinding(24L, 1L, mockPrincipal)) + .isInstanceOf(DuplicateStateException.class); + } +} diff --git a/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/DimmableStateControllerTests.java b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/DimmableStateControllerTests.java new file mode 100644 index 0000000..2e90862 --- /dev/null +++ b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/DimmableStateControllerTests.java @@ -0,0 +1,53 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.controller; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.when; + +import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.DimmableStateSaveRequest; +import ch.usi.inf.sa4.sanmarinoes.smarthut.error.NotFoundException; +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.*; +import java.util.Optional; +import lombok.SneakyThrows; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +@ExtendWith(MockitoExtension.class) +@WithMockUser(username = "user") +@DisplayName("DimmableState controller test") +public class DimmableStateControllerTests { + + @InjectMocks DimmableStateController controller; + + @Mock private DimmableStateRepository dimmableStateRepository; + + @Test + @SneakyThrows(NotFoundException.class) + public void testUpdate() { + DimmableState state = new DimmableState(); + state.setIntensity(40); + DimmableStateSaveRequest toSend = new DimmableStateSaveRequest(); + toSend.setIntensity(40); + toSend.setId(0L); + when(dimmableStateRepository.findById(toSend.getId())).thenReturn(Optional.of(state)); + assertThat(controller.update(toSend).getIntensity()).isEqualTo(toSend.getIntensity()); + } + + @Test + public void testDelete() { + doNothing().when(dimmableStateRepository).deleteById(eq(42L)); + MockHttpServletRequest request = new MockHttpServletRequest(); + RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request)); + Assertions.assertDoesNotThrow(() -> controller.delete(42L)); + } +} diff --git a/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/GuestControllerTests.java b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/GuestControllerTests.java new file mode 100644 index 0000000..96034ac --- /dev/null +++ b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/GuestControllerTests.java @@ -0,0 +1,144 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.controller; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.GuestPermissionsRequest; +import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.GuestsUpdateRequest; +import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.UserResponse; +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.EagerUserRepository; +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.User; +import java.security.Principal; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.security.test.context.support.WithMockUser; + +@ExtendWith(MockitoExtension.class) +@WithMockUser(username = "user") +public class GuestControllerTests { + @InjectMocks private GuestController guestController; + + @Mock private Principal mockPrincipal; + @Mock private EagerUserRepository userRepository; + + private final User user; + + public GuestControllerTests() { + user = new User(); + user.setName("user"); + user.setId(12L); + } + + @DisplayName("Check that the list is empty when we have no hosts/guests") + @Test + public void findAllEmptyTest() { + List l = guestController.findAll(); + assertThat(l).isEmpty(); + } + + @DisplayName( + "Check that the list contains the elements added and that hosts and guests are returned properly") + @Test + public void findAllTest() { + User host1 = new User(); + host1.setId(2L); + User host2 = new User(); + host2.setId(3L); + User guest1 = new User(); + guest1.setId(4L); + User guest2 = new User(); + guest2.setId(5L); + + user.addHost(host1); + user.addHost(host2); + user.addGuest(guest1); + user.addGuest(guest2); + + when(mockPrincipal.getName()).thenReturn("user"); + when(userRepository.findByUsername(mockPrincipal.getName())).thenReturn(this.user); + when(userRepository.findAll()).thenReturn(List.of(host1, host2, guest1, guest2)); + assertThat(guestController.findAll()) + .containsAll( + List.of(host1, host2, guest1, guest2) + .stream() + .map(UserResponse::fromUser) + .collect(Collectors.toList())); + + assertThat(guestController.findHosts(mockPrincipal)) + .containsExactly(UserResponse.fromUser(host1), UserResponse.fromUser(host2)); + assertThat(guestController.findGuests(mockPrincipal)) + .containsExactly(UserResponse.fromUser(guest1), UserResponse.fromUser(guest2)); + } + + @DisplayName("Check that the host list is empty") + @Test + public void findHostsEmptyTest() { + when(mockPrincipal.getName()).thenReturn("user"); + when(userRepository.findByUsername("user")).thenReturn(user); + assertThat(guestController.findHosts(mockPrincipal)).isEmpty(); + } + + @DisplayName("Check that the guest list is empty") + @Test + public void findGuestsEmptyTest() { + when(mockPrincipal.getName()).thenReturn("user"); + when(userRepository.findByUsername("user")).thenReturn(user); + assertThat(guestController.findGuests(mockPrincipal)).isEmpty(); + } + + @Test + public void testUpdatePermission() { + GuestPermissionsRequest permission = new GuestPermissionsRequest(); + permission.setCameraEnabled(true); + when(mockPrincipal.getName()).thenReturn("user"); + User user = new User(); + user.setCameraEnabled(true); + when(userRepository.findByUsername("user")).thenReturn(user); + when(userRepository.save(user)).thenReturn(user); + assertTrue(guestController.updatePermissions(permission, mockPrincipal).isCameraEnabled()); + } + + @Test + public void testSetGuests() { + when(mockPrincipal.getName()).thenReturn("user"); + ArrayList users = new ArrayList<>(); + User user1 = new User(); + user1.setId(1L); + user1.getGuests().add(new User()); + user1.getGuests().add(new User()); + user1.getGuests().add(new User()); + User user2 = new User(); + user2.setId(2L); + User user3 = new User(); + user3.setId(3L); + users.add(user1); + users.add(user2); + users.add(user3); + ArrayList ids = new ArrayList<>(); + ids.add(1L); + ids.add(2L); + ids.add(3L); + GuestsUpdateRequest request = new GuestsUpdateRequest(); + request.setIds(ids); + when(userRepository.findAllById(request.getIds())).thenReturn(users); + when(userRepository.findByUsername("user")).thenReturn(user1); + when(userRepository.saveAll(Set.copyOf(user1.getGuests()))).thenReturn(null); + when(userRepository.save(any(User.class))).thenReturn(null); + when(userRepository.saveAll(users)).thenReturn(users); + List returned = guestController.setGuests(request, mockPrincipal); + assertThat(returned.size()).isEqualTo(3); + assertTrue(returned.contains(user1)); + assertTrue(returned.contains(user2)); + assertTrue(returned.contains(user3)); + } +} diff --git a/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/InputDeviceConnectionControllerTests.java b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/InputDeviceConnectionControllerTests.java new file mode 100644 index 0000000..8b15a5a --- /dev/null +++ b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/InputDeviceConnectionControllerTests.java @@ -0,0 +1,64 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.controller; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.mockito.ArgumentMatchers.anyIterable; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.when; + +import ch.usi.inf.sa4.sanmarinoes.smarthut.error.NotFoundException; +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.*; +import ch.usi.inf.sa4.sanmarinoes.smarthut.service.DeviceService; +import java.security.Principal; +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.security.test.context.support.WithMockUser; + +@ExtendWith(MockitoExtension.class) +@WithMockUser(username = "user") +public class InputDeviceConnectionControllerTests { + + @Mock private DeviceService deviceService; + + @Mock private KnobDimmerRepository inputRepository; + + @Mock private DimmableRepository outputRepository; + + private KnobDimmerController knobDimmerController; + + @Mock private Principal mockPrincipal; + + @BeforeEach + public void setup() { + when(mockPrincipal.getName()).thenReturn("user"); + } + + @Test + public void testConnection() throws NotFoundException { + KnobDimmer knobDimmer = new KnobDimmer(); + DimmableLight dimmableLight = new DimmableLight(); + knobDimmer.addDimmable(dimmableLight); + + when(inputRepository.findByIdAndUsername(anyLong(), anyString())) + .thenReturn(java.util.Optional.of(knobDimmer)); + + when(outputRepository.findByIdAndUsername(anyLong(), anyString())) + .thenReturn(java.util.Optional.of(dimmableLight)); + + when(deviceService.saveAllAsOwner(anyIterable(), anyString())) + .thenReturn(List.of(new DimmableLight())); + + List l = new ArrayList<>(); + l.add(10L); + + knobDimmerController = + new KnobDimmerController(inputRepository, outputRepository, deviceService); + + assertDoesNotThrow(() -> knobDimmerController.addLight(1L, l, mockPrincipal)); + } +} diff --git a/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/KnobDimmerControllerTests.java b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/KnobDimmerControllerTests.java new file mode 100644 index 0000000..f796312 --- /dev/null +++ b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/KnobDimmerControllerTests.java @@ -0,0 +1,98 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.controller; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.when; + +import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.GenericDeviceSaveRequest; +import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.KnobDimmerDimRequest; +import ch.usi.inf.sa4.sanmarinoes.smarthut.error.NotFoundException; +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.*; +import ch.usi.inf.sa4.sanmarinoes.smarthut.service.DeviceService; +import java.security.Principal; +import java.util.Optional; +import java.util.Set; +import lombok.SneakyThrows; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +@ExtendWith(MockitoExtension.class) +@WithMockUser(username = "user") +@DisplayName("KnobDimmer controller test") +public class KnobDimmerControllerTests { + @InjectMocks private KnobDimmerController controller; + + @Mock private Principal principal; + + @Mock private DeviceService deviceService; + + @Mock private KnobDimmerRepository knobDimmerRepository; + + @Test + public void testGetId() throws NotFoundException { + KnobDimmer dimmer = new KnobDimmer(); + dimmer.setId(42L); + when(knobDimmerRepository.findById(42L)).thenReturn(Optional.of(dimmer)); + when(knobDimmerRepository.findById(1L)).thenReturn(Optional.empty()); + assertThat(controller.findById(42L)).isSameAs(dimmer); + assertThatThrownBy(() -> controller.findById(1L)).isInstanceOf(NotFoundException.class); + } + + @Test + @SneakyThrows(NotFoundException.class) + public void testCreate() { + when(principal.getName()).thenReturn("user"); + doNothing().when(deviceService).throwIfRoomNotOwned(anyLong(), eq("user")); + when(deviceService.saveAsOwner(any(KnobDimmer.class), eq("user"))) + .thenAnswer(i -> i.getArguments()[0]); + + MockHttpServletRequest request = new MockHttpServletRequest(); + RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request)); + GenericDeviceSaveRequest toSend = new GenericDeviceSaveRequest(); + toSend.setName("dimmer"); + toSend.setRoomId(42L); + KnobDimmer dimmer = controller.create(toSend, principal); + assertThat(dimmer.getName()).isEqualTo(toSend.getName()); + assertThat(dimmer.getRoomId()).isEqualTo(toSend.getRoomId()); + } + + @Test + @SneakyThrows(NotFoundException.class) + public void testDelete() { + when(principal.getName()).thenReturn("user"); + doNothing().when(deviceService).deleteByIdAsOwner(eq(42L), eq("user")); + MockHttpServletRequest request = new MockHttpServletRequest(); + RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request)); + Assertions.assertDoesNotThrow(() -> controller.delete(42L, principal)); + } + + @Test + @SneakyThrows(NotFoundException.class) + public void testDimTo() { + when(principal.getName()).thenReturn("user"); + KnobDimmer dimmer = new KnobDimmer(); + DimmableLight light = new DimmableLight(); + Curtains curtains = new Curtains(); + dimmer.addDimmable(light); + dimmer.addDimmable(curtains); + when(knobDimmerRepository.findByIdAndUsername(42L, "user")).thenReturn(Optional.of(dimmer)); + KnobDimmerDimRequest toSend = new KnobDimmerDimRequest(); + toSend.setIntensity(12); + toSend.setId(42L); + MockHttpServletRequest request = new MockHttpServletRequest(); + RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request)); + Set set = controller.dimTo(toSend, principal); + assertThat(set.size()).isEqualTo(dimmer.getOutputs().size()); + } +} diff --git a/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/MotionSensorControllerTests.java b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/MotionSensorControllerTests.java new file mode 100644 index 0000000..a104862 --- /dev/null +++ b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/MotionSensorControllerTests.java @@ -0,0 +1,111 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.controller; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.GenericDeviceSaveRequest; +import ch.usi.inf.sa4.sanmarinoes.smarthut.error.NotFoundException; +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.MotionSensor; +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.MotionSensorRepository; +import ch.usi.inf.sa4.sanmarinoes.smarthut.service.DeviceService; +import ch.usi.inf.sa4.sanmarinoes.smarthut.service.MotionSensorService; +import java.security.Principal; +import java.util.Optional; +import lombok.SneakyThrows; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +@DisplayName("The motion sensor controller") +@ExtendWith(MockitoExtension.class) +@WithMockUser(username = "user") +public class MotionSensorControllerTests { + + @InjectMocks private MotionSensorController motionSensorController; + + @Mock private DeviceService deviceService; + + @Mock private MotionSensorService motionSensorService; + + @Mock private MotionSensorRepository motionSensorRepository; + + @Mock private Principal mockPrincipal; + + @BeforeEach + public void setup() { + when(mockPrincipal.getName()).thenReturn("user"); + } + + private void checkMotionSensorAgainstRequest( + final MotionSensor toCheck, final GenericDeviceSaveRequest request) { + assertThat(toCheck).isNotNull(); + assertThat(toCheck.getName()).isEqualTo(request.getName()); + assertThat(toCheck.getRoomId()).isEqualTo(request.getRoomId()); + } + + @DisplayName("when creating should return the same object") + @Test + @SneakyThrows(NotFoundException.class) + public void testCreate() { + doNothing().when(deviceService).throwIfRoomNotOwned(anyLong(), eq("user")); + when(deviceService.saveAsOwner(any(MotionSensor.class), eq("user"))) + .thenAnswer(i -> i.getArguments()[0]); + + MockHttpServletRequest request = new MockHttpServletRequest(); + RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request)); + + final GenericDeviceSaveRequest toSend = new GenericDeviceSaveRequest(42L, "Test sensor"); + final MotionSensor created = motionSensorController.create(toSend, mockPrincipal); + + checkMotionSensorAgainstRequest(created, toSend); + } + + @Test + @SneakyThrows(NotFoundException.class) + public void testUpdate() { + + MotionSensor motionSensor = new MotionSensor(); + motionSensor.setId(1L); + motionSensor.setName("name"); + motionSensor.setRoomId(42L); + motionSensor.setDetected(true); + + when(motionSensorRepository.findByIdAndUsername(anyLong(), any(String.class))) + .thenReturn(Optional.of(motionSensor)); + + when(motionSensorService.updateDetectionFromMotionSensor( + any(MotionSensor.class), anyBoolean(), anyString())) + .thenAnswer(i -> i.getArguments()[0]); + + MockHttpServletRequest request = new MockHttpServletRequest(); + RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request)); + + MotionSensor returned = motionSensorController.updateDetection(1L, false, mockPrincipal); + + assertNotNull(returned); + } + + @DisplayName("when deleting an existing id should succeed") + @Test + @SneakyThrows(NotFoundException.class) + public void testDelete() { + + doNothing().when(deviceService).deleteByIdAsOwner(eq(42L), eq("user")); + + MockHttpServletRequest request = new MockHttpServletRequest(); + RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request)); + + Assertions.assertDoesNotThrow(() -> motionSensorController.delete(42L, mockPrincipal)); + } +} diff --git a/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/RangeConditionControllerTests.java b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/RangeConditionControllerTests.java new file mode 100644 index 0000000..563636c --- /dev/null +++ b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/RangeConditionControllerTests.java @@ -0,0 +1,108 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.controller; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.when; + +import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.RangeConditionOrTriggerSaveRequest; +import ch.usi.inf.sa4.sanmarinoes.smarthut.error.NotFoundException; +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.*; +import java.security.Principal; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import lombok.SneakyThrows; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +@DisplayName("RangeConditionController tests") +@ExtendWith(MockitoExtension.class) +@WithMockUser(username = "user") +public class RangeConditionControllerTests { + @InjectMocks private RangeConditionController controller; + + @Mock private RangeConditionRepository repository; + + @Mock private Principal principal; + + @Test + public void testCreate() { + RangeCondition condition = new RangeCondition(); + condition.setRange(40L); + condition.setDeviceId(1L); + condition.setAutomationId(2L); + condition.setOperator(Operator.EQUAL); + when(repository.save(condition)).thenReturn(condition); + RangeConditionOrTriggerSaveRequest toSend = new RangeConditionOrTriggerSaveRequest(); + toSend.setAutomationId(2L); + toSend.setDeviceId(1L); + toSend.setOperator(Operator.EQUAL); + toSend.setRange(40L); + RangeCondition returned = controller.create(toSend); + assertThat(returned.getRange()).isEqualTo(toSend.getRange()); + assertThat(returned.getAutomationId()).isEqualTo(toSend.getAutomationId()); + assertThat(returned.getOperator()).isEqualTo(toSend.getOperator()); + assertThat(returned.getDeviceId()).isEqualTo(toSend.getDeviceId()); + } + + @Test + @SneakyThrows(NotFoundException.class) + public void testUpdate() { + RangeCondition condition = new RangeCondition(); + condition.setRange(40L); + condition.setDeviceId(1L); + condition.setAutomationId(2L); + condition.setOperator(Operator.EQUAL); + when(repository.save(condition)).thenReturn(condition); + RangeConditionOrTriggerSaveRequest toSend = new RangeConditionOrTriggerSaveRequest(); + toSend.setId(34L); + toSend.setAutomationId(2L); + toSend.setDeviceId(1L); + toSend.setOperator(Operator.EQUAL); + toSend.setRange(40L); + when(repository.findById(34L)).thenReturn(Optional.of(condition)); + RangeCondition returned = controller.update(toSend); + assertThat(returned.getRange()).isEqualTo(toSend.getRange()); + assertThat(returned.getAutomationId()).isEqualTo(toSend.getAutomationId()); + assertThat(returned.getOperator()).isEqualTo(toSend.getOperator()); + assertThat(returned.getDeviceId()).isEqualTo(toSend.getDeviceId()); + } + + @Test + public void testGetAll() { + RangeCondition condition1 = new RangeCondition(); + condition1.setAutomationId(1L); + RangeCondition condition2 = new RangeCondition(); + condition2.setAutomationId(1L); + RangeCondition condition3 = new RangeCondition(); + condition3.setAutomationId(1L); + ArrayList list = new ArrayList<>(); + list.add(condition1); + list.add(condition2); + list.add(condition3); + when(repository.findAllByAutomationId(42L)).thenReturn(list); + List returned = controller.getAll(42L); + assertThat(returned).contains(condition1); + assertThat(returned).contains(condition2); + assertThat(returned).contains(condition3); + assertThat(returned.size()).isEqualTo(3); + } + + @Test + public void testDelete() { + doNothing().when(repository).deleteById(eq(42L)); + MockHttpServletRequest request = new MockHttpServletRequest(); + RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request)); + Assertions.assertDoesNotThrow(() -> controller.delete(42L)); + } +} diff --git a/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/RangeTriggerControllerTests.java b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/RangeTriggerControllerTests.java new file mode 100644 index 0000000..673b48a --- /dev/null +++ b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/RangeTriggerControllerTests.java @@ -0,0 +1,105 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.controller; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.when; + +import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.RangeConditionOrTriggerSaveRequest; +import ch.usi.inf.sa4.sanmarinoes.smarthut.error.NotFoundException; +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.*; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import lombok.SneakyThrows; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +@DisplayName("RangeTriggerController tests") +@ExtendWith(MockitoExtension.class) +@WithMockUser(username = "user") +public class RangeTriggerControllerTests { + @InjectMocks private RangeTriggerController controller; + + @Mock private RangeTriggerRepository repository; + + @Test + public void testDelete() { + doNothing().when(repository).deleteById(eq(42L)); + MockHttpServletRequest request = new MockHttpServletRequest(); + RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request)); + Assertions.assertDoesNotThrow(() -> controller.delete(42L)); + } + + @Test + public void testGetAll() { + RangeTrigger trigger1 = new RangeTrigger(); + trigger1.setAutomationId(1L); + RangeTrigger trigger2 = new RangeTrigger(); + trigger2.setAutomationId(1L); + RangeTrigger trigger3 = new RangeTrigger(); + trigger3.setAutomationId(1L); + ArrayList list = new ArrayList<>(); + list.add(trigger1); + list.add(trigger2); + list.add(trigger3); + when(repository.findAllByAutomationId(42L)).thenReturn(list); + List returned = controller.getAll(42L); + assertThat(returned).contains(trigger1); + assertThat(returned).contains(trigger2); + assertThat(returned).contains(trigger3); + assertThat(returned.size()).isEqualTo(3); + } + + @Test + public void testCreate() { + RangeConditionOrTriggerSaveRequest toSend = new RangeConditionOrTriggerSaveRequest(); + toSend.setRange(40L); + toSend.setOperator(Operator.EQUAL); + toSend.setDeviceId(1L); + toSend.setAutomationId(2L); + RangeTrigger trigger = new RangeTrigger(); + trigger.setRange(40L); + trigger.setOperator(Operator.EQUAL); + trigger.setAutomationId(2L); + trigger.setDeviceId(1L); + when(repository.save(any(RangeTrigger.class))).thenReturn(trigger); + RangeTrigger returned = controller.create(toSend); + assertThat(returned.getOperator()).isEqualTo(toSend.getOperator()); + assertThat(returned.getRange()).isEqualTo(toSend.getRange()); + assertThat(returned.getAutomationId()).isEqualTo(toSend.getAutomationId()); + assertThat(returned.getDeviceId()).isEqualTo(toSend.getDeviceId()); + } + + @Test + @SneakyThrows(NotFoundException.class) + public void testUpdate() { + RangeTrigger trigger = new RangeTrigger(); + trigger.setRange(40L); + trigger.setDeviceId(1L); + trigger.setAutomationId(2L); + trigger.setOperator(Operator.EQUAL); + when(repository.save(trigger)).thenReturn(trigger); + RangeConditionOrTriggerSaveRequest toSend = new RangeConditionOrTriggerSaveRequest(); + toSend.setId(34L); + toSend.setAutomationId(2L); + toSend.setDeviceId(1L); + toSend.setRange(40L); + when(repository.findById(34L)).thenReturn(Optional.of(trigger)); + RangeTrigger returned = controller.update(toSend); + assertThat(returned.getRange()).isEqualTo(toSend.getRange()); + assertThat(returned.getAutomationId()).isEqualTo(toSend.getAutomationId()); + assertThat(returned.getOperator()).isEqualTo(toSend.getOperator()); + assertThat(returned.getDeviceId()).isEqualTo(toSend.getDeviceId()); + } +} diff --git a/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/RegularLightControllerTests.java b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/RegularLightControllerTests.java new file mode 100644 index 0000000..6b093d7 --- /dev/null +++ b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/RegularLightControllerTests.java @@ -0,0 +1,200 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.controller; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.when; + +import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.SwitchableSaveRequest; +import ch.usi.inf.sa4.sanmarinoes.smarthut.error.DuplicateStateException; +import ch.usi.inf.sa4.sanmarinoes.smarthut.error.NotFoundException; +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.*; +import ch.usi.inf.sa4.sanmarinoes.smarthut.service.DeviceService; +import java.security.Principal; +import java.util.List; +import java.util.Optional; +import lombok.SneakyThrows; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +@ExtendWith(MockitoExtension.class) +@WithMockUser(username = "user") +public class RegularLightControllerTests { + + @InjectMocks private RegularLightController regularLightController; + + @Mock private RegularLightRepository regularLightRepository; + + @Mock private DeviceService deviceService; + + @Mock private Principal mockPrincipal; + + @Mock private UserRepository userRepository; + + @Mock private SceneRepository sceneRepository; + + @Mock private StateRepository stateRepository; + + private void checkRegularLightAgainstRequest( + final RegularLight toCheck, final SwitchableSaveRequest request) { + assertThat(toCheck).isNotNull(); + assertThat(toCheck.isOn()).isEqualTo(request.isOn()); + assertThat(toCheck.getName()).isEqualTo(request.getName()); + assertThat(toCheck.getRoomId()).isEqualTo(request.getRoomId()); + } + + @Test + public void testGetAll() { + when(regularLightRepository.findAll()).thenReturn(List.of()); + assertThat(regularLightController.findAll()).isEmpty(); + } + + @Test + public void testGet() throws NotFoundException { + RegularLight r = new RegularLight(); + when(regularLightRepository.findById(1L)).thenReturn(Optional.of(r)); + when(regularLightRepository.findById(2L)).thenReturn(Optional.empty()); + + assertThat(regularLightController.findById(1L)).isSameAs(r); + assertThatThrownBy(() -> regularLightController.findById(2L)) + .isInstanceOf(NotFoundException.class); + } + + @Test + @DisplayName("when creating should return the same object") + @SneakyThrows(NotFoundException.class) + public void testCreate() { + when(mockPrincipal.getName()).thenReturn("user"); + doNothing().when(deviceService).throwIfRoomNotOwned(anyLong(), eq("user")); + when(deviceService.saveAsOwner(any(RegularLight.class), eq("user"))) + .thenAnswer(i -> i.getArguments()[0]); + + MockHttpServletRequest request = new MockHttpServletRequest(); + RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request)); + + final SwitchableSaveRequest toSend = new SwitchableSaveRequest(); + toSend.setName("rl"); + toSend.setRoomId(20L); + toSend.setOn(true); + + final RegularLight regularLight = regularLightController.create(toSend, mockPrincipal); + + checkRegularLightAgainstRequest(regularLight, toSend); + } + + @Test + @DisplayName("when updating should return the same object") + @SneakyThrows(NotFoundException.class) + public void testUpdate() { + + when(mockPrincipal.getName()).thenReturn("user"); + final SwitchableSaveRequest toSend = new SwitchableSaveRequest(); + toSend.setName("rl"); + toSend.setRoomId(20L); + toSend.setOn(true); + + final RegularLight toUpdate = new RegularLight(); + toUpdate.setName("OOO"); + toUpdate.setRoomId(40L); + toUpdate.setOn(false); + + when(regularLightRepository.findByIdAndUsername(50L, "user")) + .thenReturn(Optional.of(toUpdate)); + when(regularLightRepository.findByIdAndUsername(60L, "user")).thenReturn(Optional.empty()); + + when(deviceService.saveAsOwner(toUpdate, "user")).thenReturn(toUpdate); + + toSend.setId(60L); + assertThatThrownBy(() -> regularLightController.update(toSend, mockPrincipal, null)) + .isInstanceOf(NotFoundException.class); + toSend.setId(50L); + assertThat(regularLightController.update(toSend, mockPrincipal, null)).isEqualTo(toUpdate); + + toUpdate.setName("OOO"); + toUpdate.setRoomId(40L); + toSend.setId(50L); + toUpdate.setOn(false); + + when(regularLightRepository.findByIdAndUserId(anyLong(), anyLong())) + .thenReturn(java.util.Optional.of(toUpdate)); + + when(deviceService.saveAsGuest(any(RegularLight.class), eq("user"), anyLong())) + .thenAnswer(i -> i.getArguments()[0]); + + User guest = new User(); + User host = new User(); + host.getGuests().add(guest); + guest.getHosts().add(host); + + when(userRepository.findById(20L)).thenReturn(Optional.of(host)); + when(userRepository.findByUsername("user")).thenReturn(guest); + + MockHttpServletRequest request = new MockHttpServletRequest(); + RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request)); + + final RegularLight regularLight = regularLightController.update(toSend, mockPrincipal, 20L); + + checkRegularLightAgainstRequest(regularLight, toSend); + } + + @Test + @DisplayName("an existing id should succeed") + @SneakyThrows(NotFoundException.class) + public void testDelete() { + + when(mockPrincipal.getName()).thenReturn("user"); + doNothing().when(deviceService).deleteByIdAsOwner(eq(42L), eq("user")); + + MockHttpServletRequest request = new MockHttpServletRequest(); + RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request)); + + Assertions.assertDoesNotThrow(() -> regularLightController.delete(42L, mockPrincipal)); + } + + @Test + public void testSceneBinding() { + when(mockPrincipal.getName()).thenReturn("user"); + RegularLight regularLight = new RegularLight(); + when(regularLightRepository.findByIdAndUsername(24L, "user")) + .thenReturn(Optional.of(regularLight)); + Scene scene = new Scene(); + scene.setId(1L); + SwitchableState state = new SwitchableState(); + state.setSceneId(1L); + State s = regularLight.cloneState(); + when(sceneRepository.findById(1L)).thenReturn(Optional.of(scene)); + when(stateRepository.countByDeviceIdAndSceneId(24L, 1L)).thenReturn(0); + Assertions.assertDoesNotThrow( + () -> regularLightController.sceneBinding(24L, 1L, mockPrincipal)); + } + + @Test + public void testSceneBinding2() { + when(mockPrincipal.getName()).thenReturn("user"); + when(mockPrincipal.getName()).thenReturn("user"); + RegularLight regularLight = new RegularLight(); + when(regularLightRepository.findByIdAndUsername(24L, "user")) + .thenReturn(Optional.of(regularLight)); + Scene scene = new Scene(); + scene.setId(1L); + SwitchableState state = new SwitchableState(); + state.setSceneId(1L); + State s = regularLight.cloneState(); + when(sceneRepository.findById(1L)).thenReturn(Optional.of(scene)); + when(stateRepository.countByDeviceIdAndSceneId(24L, 1L)).thenReturn(2); + assertThatThrownBy(() -> regularLightController.sceneBinding(24L, 1L, mockPrincipal)) + .isInstanceOf(DuplicateStateException.class); + } +} diff --git a/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/RoomControllerTests.java b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/RoomControllerTests.java new file mode 100644 index 0000000..38cd498 --- /dev/null +++ b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/RoomControllerTests.java @@ -0,0 +1,176 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.controller; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.when; +import static org.springframework.test.util.AssertionErrors.fail; + +import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.RoomSaveRequest; +import ch.usi.inf.sa4.sanmarinoes.smarthut.error.NotFoundException; +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.*; +import ch.usi.inf.sa4.sanmarinoes.smarthut.service.DeviceService; +import java.security.Principal; +import java.util.List; +import java.util.Optional; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.security.test.context.support.WithMockUser; + +@ExtendWith(MockitoExtension.class) +@WithMockUser(username = "user") +public class RoomControllerTests { + @InjectMocks private RoomController roomController; + + @Mock private UserRepository userRepository; + + @Mock private RoomRepository roomRepository; + + @Mock private Principal mockPrincipal; + + @Mock private SwitchRepository switchRepository; + + @Mock private KnobDimmerRepository knobDimmerRepository; + + @Mock private ButtonDimmerRepository buttonDimmerRepository; + + @Mock private DeviceService deviceService; + + private final User u; + + private final User h; + + public RoomControllerTests() { + u = new User(); + u.setUsername("user"); + u.setId(1L); + + h = new User(); + h.setUsername("host"); + h.setId(2L); + u.getHosts().add(h); + h.getGuests().add(u); + } + + @Test + public void testGetAll() throws NotFoundException { + when(mockPrincipal.getName()).thenReturn("user"); + when(roomRepository.findByUsername("user")).thenReturn(List.of()); + when(roomRepository.findByUserId(2L)).thenReturn(List.of()); + when(userRepository.findById(2L)).thenReturn(Optional.of(h)); + when(userRepository.findByUsername("user")).thenReturn(u); + assertThat(roomController.findAll(null, mockPrincipal)).isEmpty(); + assertThat(roomController.findAll(2L, mockPrincipal)).isEmpty(); + } + + @Test + public void testGet() throws NotFoundException { + Room r = new Room(); + when(roomRepository.findById(1L)).thenReturn(Optional.of(r)); + when(roomRepository.findById(2L)).thenReturn(Optional.empty()); + + assertThat(roomController.findById(1L, mockPrincipal, null)).isSameAs(r); + assertThatThrownBy(() -> roomController.findById(2L, mockPrincipal, null)) + .isInstanceOf(NotFoundException.class); + } + + private void equalToRequest(Room created, RoomSaveRequest a) { + if (a.getName() != null) { + assertThat(created.getName()).isEqualTo(a.getName()); + } + assertThat(created.getUserId()).isEqualTo(1L); + if (a.getIcon() != null) { + assertThat(created.getIcon()).isEqualTo(a.getIcon()); + } + if (a.getImage() != null) { + if (a.getImage().isEmpty()) { + assertThat(created.getImage()).isNull(); + } else { + assertThat(created.getImage()).isEqualTo(a.getImage()); + } + } + } + + @Test + public void testCreate() { + when(mockPrincipal.getName()).thenReturn("user"); + when(userRepository.findByUsername("user")).thenReturn(u); + when(roomRepository.save(any(Room.class))).thenAnswer(i -> i.getArguments()[0]); + + RoomSaveRequest roomSaveRequest = new RoomSaveRequest(0, Icon.BEER, null, "New Room"); + Room created = roomController.create(roomSaveRequest, mockPrincipal); + assertThat(created.getId()).isNull(); + equalToRequest(created, roomSaveRequest); + } + + @Test + public void testUpdate() throws NotFoundException { + when(mockPrincipal.getName()).thenReturn("user"); + final Room old = new Room(); + old.setId(42L); + old.setUserId(1L); + old.setName("Old Name"); + + when(roomRepository.save(any(Room.class))).thenAnswer(i -> i.getArguments()[0]); + when(roomRepository.findByIdAndUsername(42L, "user")).thenReturn(Optional.of(old)); + when(roomRepository.findByIdAndUsername(43L, "user")).thenReturn(Optional.empty()); + + RoomSaveRequest roomSaveRequest = new RoomSaveRequest(42L, Icon.BEER, "image", "New Room"); + + Room created = + roomController.update(roomSaveRequest.getId(), roomSaveRequest, mockPrincipal); + assertThat(created.getId()).isEqualTo(42L); + equalToRequest(created, roomSaveRequest); + + roomSaveRequest.setImage(""); + roomSaveRequest.setIcon(null); + roomSaveRequest.setName(null); + + created = roomController.update(roomSaveRequest.getId(), roomSaveRequest, mockPrincipal); + assertThat(created.getId()).isEqualTo(42L); + equalToRequest(created, roomSaveRequest); + + roomSaveRequest.setId(43L); + assertThatThrownBy( + () -> + roomController.update( + roomSaveRequest.getId(), roomSaveRequest, mockPrincipal)) + .isInstanceOf(NotFoundException.class); + } + + @Test + public void testDelete() throws NotFoundException { + when(mockPrincipal.getName()).thenReturn("user"); + + final Room toDelete = new Room(); + toDelete.setId(42L); + + final Device d = new Thermostat(); + d.setId(2020L); + + doNothing().when(switchRepository).deleteAllByRoomId(42L); + doNothing().when(knobDimmerRepository).deleteAllByRoomId(42L); + doNothing().when(buttonDimmerRepository).deleteAllByRoomId(42L); + when(roomRepository.findByIdAndUsername(42L, "user")).thenReturn(Optional.of(toDelete)); + when(deviceService.findAll(42L, null, "user")).thenReturn(List.of(d)); + doNothing().when(deviceService).deleteByIdAsOwner(2020L, "user"); + doNothing().when(roomRepository).delete(toDelete); + + try { + roomController.deleteById(42L, mockPrincipal); + } catch (NotFoundException e) { + fail(e.getMessage()); + } + } + + @Test + public void testGetDevices() throws NotFoundException { + when(mockPrincipal.getName()).thenReturn("user"); + when(deviceService.findAll(2020L, 10L, "user")).thenReturn(List.of()); + assertThat(roomController.getDevices(2020L, mockPrincipal, 10L)).isNotNull(); + } +} diff --git a/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/SceneControllerTests.java b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/SceneControllerTests.java new file mode 100644 index 0000000..0134642 --- /dev/null +++ b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/SceneControllerTests.java @@ -0,0 +1,175 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.controller; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.when; + +import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.SceneSaveRequest; +import ch.usi.inf.sa4.sanmarinoes.smarthut.error.NotFoundException; +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.*; +import ch.usi.inf.sa4.sanmarinoes.smarthut.service.SceneService; +import java.security.Principal; +import java.util.List; +import java.util.Optional; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.security.test.context.support.WithMockUser; + +@ExtendWith(MockitoExtension.class) +@WithMockUser(username = "user") +public class SceneControllerTests { + @InjectMocks private SceneController sceneController; + + @Mock private UserRepository userRepository; + + @Mock private SceneRepository sceneRepository; + + @Mock private Principal mockPrincipal; + + @Mock private StateRepository stateStateRepository; + + @Mock private SceneService sceneService; + + private final User u; + + private final User h; + + public SceneControllerTests() { + u = new User(); + u.setName("user"); + u.setId(1L); + + h = new User(); + h.setId(2L); + h.setUsername("host"); + + u.getHosts().add(h); + h.getGuests().add(u); + } + + @Test + public void testCopy() throws NotFoundException { + final Scene scene = new Scene(); + final Scene copyFrom = new Scene(); + + when(mockPrincipal.getName()).thenReturn("user"); + when(sceneRepository.findByIdAndUsername(1L, "user")).thenReturn(Optional.of(scene)); + when(sceneRepository.findByIdAndUsername(2L, "user")).thenReturn(Optional.empty()); + + when(sceneRepository.findByIdAndUsername(10L, "user")).thenReturn(Optional.of(copyFrom)); + when(sceneRepository.findByIdAndUsername(20L, "user")).thenReturn(Optional.empty()); + + assertThatThrownBy(() -> sceneController.copy(2L, 10L, mockPrincipal)) + .isInstanceOf(NotFoundException.class); + assertThatThrownBy(() -> sceneController.copy(1L, 20L, mockPrincipal)) + .isInstanceOf(NotFoundException.class); + + when(sceneService.copyStates(scene, copyFrom)).thenReturn(List.of()); + + assertThat(sceneController.copy(1L, 10L, mockPrincipal)).isEmpty(); + } + + @Test + public void testApply() throws NotFoundException { + final Scene s = new Scene(); + when(mockPrincipal.getName()).thenReturn("user"); + when(sceneRepository.findByIdAndUsername(1L, "user")).thenReturn(Optional.of(s)); + when(sceneRepository.findByIdAndUsername(2L, "user")).thenReturn(Optional.empty()); + when(sceneService.apply(s, "user", false)).thenReturn(List.of()); + + assertThatThrownBy(() -> sceneController.apply(2L, mockPrincipal, null)) + .isInstanceOf(NotFoundException.class); + assertThat(sceneController.apply(1L, mockPrincipal, null)).isEmpty(); + + when(userRepository.findByUsername("user")).thenReturn(u); + when(userRepository.findById(2L)).thenReturn(Optional.of(h)); + + when(sceneRepository.findByIdAndUserIdAndGuestAccessEnabled(1L, 2L, true)) + .thenReturn(Optional.of(s)); + when(sceneRepository.findByIdAndUserIdAndGuestAccessEnabled(2L, 2L, true)) + .thenReturn(Optional.empty()); + + when(sceneService.applyAsGuest(s, "user", 2L)).thenReturn(List.of()); + + assertThatThrownBy(() -> sceneController.apply(2L, mockPrincipal, 2L)) + .isInstanceOf(NotFoundException.class); + assertThat(sceneController.apply(1L, mockPrincipal, 2L)).isEmpty(); + } + + @Test + public void testGetAll() throws NotFoundException { + when(mockPrincipal.getName()).thenReturn("user"); + when(sceneRepository.findByUsername("user")).thenReturn(List.of()); + assertThat(sceneController.findAll(mockPrincipal, null)).isEmpty(); + + when(userRepository.findByUsername("user")).thenReturn(u); + when(userRepository.findById(2L)).thenReturn(Optional.of(h)); + assertThat(sceneController.findAll(mockPrincipal, 2L)).isEmpty(); + } + + private void equalToRequest(Scene created, SceneSaveRequest a) { + if (a.getName() != null) assertThat(created.getName()).isEqualTo(a.getName()); + assertThat(created.getUserId()).isEqualTo(1L); + assertThat(created.getIcon()).isEqualTo(a.getIcon()); + assertThat(created.isGuestAccessEnabled()).isEqualTo(a.isGuestAccessEnabled()); + } + + @Test + public void testCreate() { + when(mockPrincipal.getName()).thenReturn("user"); + when(userRepository.findByUsername("user")).thenReturn(u); + when(sceneRepository.save(any())).thenAnswer(i -> i.getArguments()[0]); + + SceneSaveRequest s = new SceneSaveRequest(0, "New Scene", Icon.BATH, true); + Scene created = sceneController.create(s, mockPrincipal); + assertThat(created.getId()).isEqualTo(0L); + equalToRequest(created, s); + } + + @Test + public void testUpdate() throws NotFoundException { + when(mockPrincipal.getName()).thenReturn("user"); + final Scene old = new Scene(); + old.setId(42L); + old.setUserId(1L); + old.setName("Old Name"); + + when(sceneRepository.save(any())).thenAnswer(i -> i.getArguments()[0]); + when(sceneRepository.findByIdAndUsername(42L, "user")).thenReturn(Optional.of(old)); + when(sceneRepository.findByIdAndUsername(43L, "user")).thenReturn(Optional.empty()); + + SceneSaveRequest a = new SceneSaveRequest(42L, "New Scene", Icon.BATH, true); + + Scene created = sceneController.update(a.getId(), a, mockPrincipal); + assertThat(created.getId()).isEqualTo(42L); + equalToRequest(created, a); + + a.setName(null); + created = sceneController.update(a.getId(), a, mockPrincipal); + assertThat(created.getId()).isEqualTo(42L); + equalToRequest(created, a); + + a.setId(43L); + assertThatThrownBy(() -> sceneController.update(a.getId(), a, mockPrincipal)) + .isInstanceOf(NotFoundException.class); + } + + @Test + public void testDelete() { + doNothing().when(stateStateRepository).deleteAllBySceneId(42L); + doNothing().when(sceneRepository).deleteById(42L); + Assertions.assertDoesNotThrow(() -> sceneController.deleteById(42L)); + } + + @Test + public void testGetStates() { + when(stateStateRepository.findBySceneId(1L)).thenReturn(List.of()); + assertThat(sceneController.getStates(1L)).isEmpty(); + } +} diff --git a/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/ScenePriorityControllerTests.java b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/ScenePriorityControllerTests.java new file mode 100644 index 0000000..bf854f8 --- /dev/null +++ b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/ScenePriorityControllerTests.java @@ -0,0 +1,107 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.controller; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.when; + +import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.ScenePrioritySaveRequest; +import ch.usi.inf.sa4.sanmarinoes.smarthut.error.NotFoundException; +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.ScenePriority; +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.ScenePriorityRepository; +import java.util.List; +import lombok.SneakyThrows; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +@ExtendWith(MockitoExtension.class) +@WithMockUser(username = "user") +public class ScenePriorityControllerTests { + + @InjectMocks private ScenePriorityController scenePriorityController; + + @Mock private ScenePriorityRepository scenePriorityRepository; + + @Test + @DisplayName("test for get all should return empty") + public void testGetAll() throws NotFoundException { + when(scenePriorityRepository.findAllByAutomationId(anyLong())).thenReturn(List.of()); + assertThat(scenePriorityController.getByAutomationId(20)).isEmpty(); + } + + private void checkScenePriorityAgainstRequest( + final ScenePriority toCheck, final ScenePrioritySaveRequest request) { + assertThat(toCheck).isNotNull(); + assertThat(toCheck.getAutomationId()).isEqualTo(request.getAutomationId()); + assertThat(toCheck.getPriority()).isEqualTo(request.getPriority()); + assertThat(toCheck.getSceneId()).isEqualTo(request.getSceneId()); + } + + @Test + @DisplayName("Test for create") + public void create() { + + ScenePrioritySaveRequest saveRequest = new ScenePrioritySaveRequest(); + saveRequest.setAutomationId(20L); + saveRequest.setPriority(40); + saveRequest.setSceneId(10L); + + when(scenePriorityRepository.save(any(ScenePriority.class))) + .thenAnswer(i -> i.getArguments()[0]); + + MockHttpServletRequest request = new MockHttpServletRequest(); + RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request)); + + ScenePriority scenePriority = scenePriorityController.create(saveRequest); + + checkScenePriorityAgainstRequest(scenePriority, saveRequest); + } + + @Test + @DisplayName("Test for update") + @SneakyThrows(NotFoundException.class) + public void update() { + + ScenePrioritySaveRequest saveRequest = new ScenePrioritySaveRequest(); + saveRequest.setAutomationId(20L); + saveRequest.setPriority(40); + saveRequest.setSceneId(10L); + + ScenePriority toUpdate = new ScenePriority(); + + when(scenePriorityRepository.findById(anyLong())) + .thenReturn(java.util.Optional.of(toUpdate)); + when(scenePriorityRepository.save(any(ScenePriority.class))) + .thenAnswer(i -> i.getArguments()[0]); + + MockHttpServletRequest request = new MockHttpServletRequest(); + RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request)); + + ScenePriority scenePriority = scenePriorityController.update(saveRequest); + + checkScenePriorityAgainstRequest(scenePriority, saveRequest); + } + + @Test + @DisplayName("an existing id should succeed") + public void testDelete() { + + doNothing().when(scenePriorityRepository).deleteBySceneId(eq(42L)); + + MockHttpServletRequest request = new MockHttpServletRequest(); + RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request)); + + Assertions.assertDoesNotThrow(() -> scenePriorityController.delete(42L)); + } +} diff --git a/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/SecurityCameraControllerTests.java b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/SecurityCameraControllerTests.java new file mode 100644 index 0000000..1c7760b --- /dev/null +++ b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/SecurityCameraControllerTests.java @@ -0,0 +1,136 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.controller; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.when; + +import ch.usi.inf.sa4.sanmarinoes.smarthut.config.CameraConfigurationService; +import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.SwitchableSaveRequest; +import ch.usi.inf.sa4.sanmarinoes.smarthut.error.DuplicateStateException; +import ch.usi.inf.sa4.sanmarinoes.smarthut.error.NotFoundException; +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.*; +import ch.usi.inf.sa4.sanmarinoes.smarthut.service.DeviceService; +import java.security.Principal; +import java.util.Optional; +import lombok.SneakyThrows; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +@ExtendWith(MockitoExtension.class) +@WithMockUser(username = "user") +@DisplayName("SecurityCamera controller test") +public class SecurityCameraControllerTests { + + @InjectMocks private SecurityCameraController controller; + + @Mock Principal principal; + + @Mock private DeviceService deviceService; + + @Mock private SecurityCameraRepository securityCameraService; + + @Mock private SceneRepository sceneRepository; + + @Mock private StateRepository stateRepository; + + @Mock private CameraConfigurationService cameraConfigurationService; + + @Test + @SneakyThrows(NotFoundException.class) + public void testCreate() { + when(principal.getName()).thenReturn("user"); + SecurityCamera camera = new SecurityCamera(); + camera.setOn(true); + camera.setName("name"); + camera.setRoomId(42L); + when(deviceService.saveAsOwner(any(SecurityCamera.class), eq("user"))).thenReturn(camera); + SwitchableSaveRequest toSend = new SwitchableSaveRequest(); + toSend.setOn(true); + toSend.setRoomId(42L); + toSend.setName("name"); + when(cameraConfigurationService.getVideoUrl()).thenReturn("url"); + doNothing().when(deviceService).throwIfRoomNotOwned(42L, "user"); + SecurityCamera returned = controller.create(toSend, principal); + assertThat(returned.getRoomId()).isEqualTo(toSend.getRoomId()); + assertThat(returned.getName()).isEqualTo(toSend.getName()); + assertTrue(returned.isOn()); + } + + @Test + @SneakyThrows(NotFoundException.class) + public void testUpdate() { + when(principal.getName()).thenReturn("user"); + SecurityCamera camera = new SecurityCamera(); + camera.setOn(true); + camera.setName("name"); + camera.setRoomId(42L); + SwitchableSaveRequest toSend = new SwitchableSaveRequest(); + toSend.setId(1L); + toSend.setOn(true); + toSend.setRoomId(42L); + toSend.setName("name"); + when(securityCameraService.findByIdAndUsername(1L, "user")).thenReturn(Optional.of(camera)); + when(deviceService.saveAsOwner(any(SecurityCamera.class), eq("user"))).thenReturn(camera); + when(cameraConfigurationService.getVideoUrl()).thenReturn("url"); + SecurityCamera returned = controller.update(toSend, principal); + assertThat(returned.getRoomId()).isEqualTo(toSend.getRoomId()); + assertThat(returned.getName()).isEqualTo(toSend.getName()); + assertTrue(returned.isOn()); + } + + @Test + @SneakyThrows(NotFoundException.class) + public void testDelete() { + when(principal.getName()).thenReturn("user"); + doNothing().when(deviceService).deleteByIdAsOwner(eq(42L), eq("user")); + MockHttpServletRequest request = new MockHttpServletRequest(); + RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request)); + Assertions.assertDoesNotThrow(() -> controller.delete(42L, principal)); + } + + @Test + public void testSceneBinding() { + when(principal.getName()).thenReturn("user"); + SecurityCamera camera = new SecurityCamera(); + when(securityCameraService.findByIdAndUsername(24L, "user")) + .thenReturn(Optional.of(camera)); + Scene scene = new Scene(); + scene.setId(1L); + SwitchableState state = new SwitchableState(); + state.setSceneId(1L); + State s = camera.cloneState(); + when(sceneRepository.findById(1L)).thenReturn(Optional.of(scene)); + when(stateRepository.countByDeviceIdAndSceneId(24L, 1L)).thenReturn(0); + // when(stateRepository.save(eq(state))).thenReturn(state); + Assertions.assertDoesNotThrow(() -> controller.sceneBinding(24L, 1L, principal)); + } + + @Test + public void testSceneBinding2() { + when(principal.getName()).thenReturn("user"); + SecurityCamera camera = new SecurityCamera(); + when(securityCameraService.findByIdAndUsername(24L, "user")) + .thenReturn(Optional.of(camera)); + Scene scene = new Scene(); + scene.setId(1L); + SwitchableState state = new SwitchableState(); + state.setSceneId(1L); + State s = camera.cloneState(); + when(sceneRepository.findById(1L)).thenReturn(Optional.of(scene)); + when(stateRepository.countByDeviceIdAndSceneId(24L, 1L)).thenReturn(2); + assertThatThrownBy(() -> controller.sceneBinding(24L, 1L, principal)) + .isInstanceOf(DuplicateStateException.class); + } +} diff --git a/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/SensorControllerTests.java b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/SensorControllerTests.java new file mode 100644 index 0000000..756a3b6 --- /dev/null +++ b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/SensorControllerTests.java @@ -0,0 +1,127 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.controller; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.when; + +import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.SensorSaveRequest; +import ch.usi.inf.sa4.sanmarinoes.smarthut.error.NotFoundException; +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.Sensor; +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.SensorRepository; +import ch.usi.inf.sa4.sanmarinoes.smarthut.service.DeviceService; +import ch.usi.inf.sa4.sanmarinoes.smarthut.service.SensorService; +import java.math.BigDecimal; +import java.security.Principal; +import java.util.Optional; +import lombok.SneakyThrows; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +@DisplayName("The sensor controller") +@ExtendWith(MockitoExtension.class) +@WithMockUser(username = "user") +public class SensorControllerTests { + @InjectMocks private SensorController sensorController; + + @Mock private DeviceService deviceService; + + @Mock private Principal mockPrincipal; + + @Mock private SensorRepository sensorRepository; + + @Mock private SensorService sensorService; + + @BeforeEach + public void setup() { + when(mockPrincipal.getName()).thenReturn("user"); + } + + private void checkSensorAgainstRequest(final Sensor toCheck, final SensorSaveRequest request) { + assertThat(toCheck).isNotNull(); + assertThat(toCheck.getSensor()).isEqualTo(request.getSensor()); + assertThat(toCheck.getValue()).isEqualTo(request.getValue()); + assertThat(toCheck.getName()).isEqualTo(request.getName()); + assertThat(toCheck.getRoomId()).isEqualTo(request.getRoomId()); + } + + @DisplayName("when creating should return the same object") + @Test + @SneakyThrows(NotFoundException.class) + public void testCreate() { + doNothing().when(deviceService).throwIfRoomNotOwned(anyLong(), eq("user")); + when(deviceService.saveAsOwner(any(Sensor.class), eq("user"))) + .thenAnswer(i -> i.getArguments()[0]); + + MockHttpServletRequest request = new MockHttpServletRequest(); + RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request)); + + final SensorSaveRequest toSend = + new SensorSaveRequest( + Sensor.SensorType.TEMPERATURE, BigDecimal.ZERO, 42L, "Test sensor"); + final Sensor created = sensorController.create(toSend, mockPrincipal); + + checkSensorAgainstRequest(created, toSend); + } + + @DisplayName("when deleting an existing id should succeed") + @Test + @SneakyThrows(NotFoundException.class) + public void testDelete() { + doNothing().when(deviceService).deleteByIdAsOwner(eq(42L), eq("user")); + + MockHttpServletRequest request = new MockHttpServletRequest(); + RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request)); + + Assertions.assertDoesNotThrow(() -> sensorController.deleteById(42L, mockPrincipal)); + } + + @Test + public void testUpdateValue() throws NotFoundException { + final Sensor s = new Sensor(); + when(sensorRepository.findByIdAndUsername(1L, "user")).thenReturn(Optional.of(s)); + when(sensorRepository.findByIdAndUsername(2L, "user")).thenReturn(Optional.empty()); + when(sensorService.updateValueFromSensor(s, BigDecimal.valueOf(30))) + .thenAnswer( + i -> { + s.setValue(i.getArgument(1)); + return s; + }); + assertThatThrownBy(() -> sensorController.updateValue(2L, BigDecimal.ZERO, mockPrincipal)) + .isInstanceOf(NotFoundException.class); + assertThat(sensorController.updateValue(1L, BigDecimal.valueOf(30), mockPrincipal)) + .isSameAs(s); + } + + @Test + public void testUpdateSimulation() throws NotFoundException { + final Sensor s = new Sensor(); + when(sensorRepository.findByIdAndUsername(1L, "user")).thenReturn(Optional.of(s)); + when(sensorRepository.findByIdAndUsername(2L, "user")).thenReturn(Optional.empty()); + when(sensorService.updateSimulationFromSensor( + s, BigDecimal.valueOf(30), BigDecimal.valueOf(10))) + .thenAnswer( + i -> { + s.setError(i.getArgument(1)); + s.setTypical(i.getArgument(2)); + return s; + }); + assertThatThrownBy(() -> sensorController.updateValue(2L, BigDecimal.ZERO, mockPrincipal)) + .isInstanceOf(NotFoundException.class); + assertThat( + sensorController.updateSimulation( + 1L, BigDecimal.valueOf(30), BigDecimal.valueOf(10), mockPrincipal)) + .isSameAs(s); + } +} diff --git a/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/SmartPlugControllerTests.java b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/SmartPlugControllerTests.java new file mode 100644 index 0000000..86a5c7f --- /dev/null +++ b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/SmartPlugControllerTests.java @@ -0,0 +1,148 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.controller; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.when; +import static org.springframework.test.util.AssertionErrors.fail; + +import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.SwitchableSaveRequest; +import ch.usi.inf.sa4.sanmarinoes.smarthut.error.DuplicateStateException; +import ch.usi.inf.sa4.sanmarinoes.smarthut.error.NotFoundException; +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.*; +import ch.usi.inf.sa4.sanmarinoes.smarthut.service.DeviceService; +import java.math.BigDecimal; +import java.security.Principal; +import java.util.Optional; +import lombok.SneakyThrows; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.security.test.context.support.WithMockUser; + +@ExtendWith(MockitoExtension.class) +@WithMockUser(username = "user") +public class SmartPlugControllerTests { + @InjectMocks private SmartPlugController smartPlugController; + + @Mock private UserRepository userRepository; + + @Mock private SmartPlugRepository smartPlugRepository; + + @Mock private Principal mockPrincipal; + + @Mock private DeviceService deviceService; + + @Mock private SceneRepository sceneRepository; + + @Mock private StateRepository stateRepository; + + private final User u; + + public SmartPlugControllerTests() { + u = new User(); + u.setName("user"); + u.setId(1L); + } + + private void equalToRequest(Switchable created, SwitchableSaveRequest a) { + assertThat(created.getName()).isEqualTo(a.getName()); + assertThat(created.getRoomId()).isEqualTo(30L); + assertThat(created.isOn()).isEqualTo(a.isOn()); + } + + @Test + public void testCreate() { + when(mockPrincipal.getName()).thenReturn("user"); + when(deviceService.saveAsOwner(any(), eq("user"))).thenAnswer(i -> i.getArguments()[0]); + + SwitchableSaveRequest a = new SwitchableSaveRequest(true, 1L, 30L, "New SmartPlug"); + try { + Switchable created = smartPlugController.create(a, mockPrincipal); + assertThat(created.getId()).isEqualTo(0L); + equalToRequest(created, a); + } catch (NotFoundException e) { + fail(e.getMessage()); + } + } + + @Test + public void testUpdate() throws NotFoundException { + when(mockPrincipal.getName()).thenReturn("user"); + final SmartPlug old = new SmartPlug(); + old.setId(42L); + + when(deviceService.saveAsOwner(any(), eq("user"))).thenAnswer(i -> i.getArguments()[0]); + when(smartPlugRepository.findByIdAndUsername(42L, "user")).thenReturn(Optional.of(old)); + when(smartPlugRepository.findByIdAndUsername(43L, "user")).thenReturn(Optional.empty()); + + SwitchableSaveRequest a = new SwitchableSaveRequest(true, 42L, 30L, "New SmartPlug"); + + SmartPlug created = smartPlugController.update(a, mockPrincipal); + assertThat(created.getId()).isEqualTo(42L); + equalToRequest(created, a); + + a.setId(43L); + assertThatThrownBy(() -> smartPlugController.update(a, mockPrincipal)) + .isInstanceOf(NotFoundException.class); + } + + @Test + public void testDelete() throws NotFoundException { + when(mockPrincipal.getName()).thenReturn("user"); + doNothing().when(deviceService).deleteByIdAsOwner(42L, "user"); + Assertions.assertDoesNotThrow(() -> smartPlugController.deleteById(42L, mockPrincipal)); + } + + @Test + @SneakyThrows(NotFoundException.class) + public void testResetMeter() { + when(mockPrincipal.getName()).thenReturn("user"); + SmartPlug s = new SmartPlug(); + s.setId(24L); + when(smartPlugRepository.findByIdAndUsername(24L, "user")).thenReturn(Optional.of(s)); + s.resetTotalConsumption(); + when(smartPlugRepository.save(s)).thenReturn(s); + SmartPlug toCheck = smartPlugController.resetMeter(24L, mockPrincipal); + assertThat(toCheck.getTotalConsumption()).isEqualTo(new BigDecimal(0)); + } + + @Test + public void testSceneBinding() { + when(mockPrincipal.getName()).thenReturn("user"); + SmartPlug smartPlug = new SmartPlug(); + when(smartPlugRepository.findByIdAndUsername(24L, "user")) + .thenReturn(Optional.of(smartPlug)); + Scene scene = new Scene(); + scene.setId(1L); + SwitchableState state = new SwitchableState(); + state.setSceneId(1L); + State s = smartPlug.cloneState(); + when(sceneRepository.findById(1L)).thenReturn(Optional.of(scene)); + when(stateRepository.countByDeviceIdAndSceneId(24L, 1L)).thenReturn(0); + Assertions.assertDoesNotThrow( + () -> smartPlugController.sceneBinding(24L, 1L, mockPrincipal)); + } + + @Test + public void testSceneBinding2() { + when(mockPrincipal.getName()).thenReturn("user"); + SmartPlug smartPlug = new SmartPlug(); + when(smartPlugRepository.findByIdAndUsername(24L, "user")) + .thenReturn(Optional.of(smartPlug)); + Scene scene = new Scene(); + scene.setId(1L); + SwitchableState state = new SwitchableState(); + state.setSceneId(1L); + State s = smartPlug.cloneState(); + when(sceneRepository.findById(1L)).thenReturn(Optional.of(scene)); + when(stateRepository.countByDeviceIdAndSceneId(24L, 1L)).thenReturn(2); + assertThatThrownBy(() -> smartPlugController.sceneBinding(24L, 1L, mockPrincipal)) + .isInstanceOf(DuplicateStateException.class); + } +} diff --git a/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/SwitchControllerTests.java b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/SwitchControllerTests.java new file mode 100644 index 0000000..ef47b96 --- /dev/null +++ b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/SwitchControllerTests.java @@ -0,0 +1,158 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.controller; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.when; + +import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.GenericDeviceSaveRequest; +import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.SwitchOperationRequest; +import ch.usi.inf.sa4.sanmarinoes.smarthut.error.NotFoundException; +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.RegularLight; +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.Switch; +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.SwitchRepository; +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.Switchable; +import ch.usi.inf.sa4.sanmarinoes.smarthut.service.DeviceService; +import java.security.Principal; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import lombok.SneakyThrows; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +@ExtendWith(MockitoExtension.class) +@WithMockUser(username = "user") +@DisplayName("SwitchController controller test") +public class SwitchControllerTests { + @InjectMocks private SwitchController controller; + + @Mock private DeviceService deviceService; + + @Mock private SwitchRepository repository; + + @Mock private Principal principal; + + @Test + public void testGetId() throws NotFoundException { + Switch aSwitch = new Switch(); + aSwitch.setId(42L); + when(repository.findById(42L)).thenReturn(Optional.of(aSwitch)); + when(repository.findById(1L)).thenReturn(Optional.empty()); + assertThat(controller.findById(42L)).isSameAs(aSwitch); + assertThatThrownBy(() -> controller.findById(1L)).isInstanceOf(NotFoundException.class); + } + + @Test + @SneakyThrows(NotFoundException.class) + public void testCreate() { + when(principal.getName()).thenReturn("user"); + doNothing().when(deviceService).throwIfRoomNotOwned(anyLong(), eq("user")); + when(deviceService.saveAsOwner(any(Switch.class), eq("user"))) + .thenAnswer(i -> i.getArguments()[0]); + + MockHttpServletRequest request = new MockHttpServletRequest(); + RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request)); + GenericDeviceSaveRequest toSend = new GenericDeviceSaveRequest(); + toSend.setName("dimmer"); + toSend.setRoomId(42L); + Switch aSwitch = controller.create(toSend, principal); + assertThat(aSwitch.getName()).isEqualTo(toSend.getName()); + assertThat(aSwitch.getRoomId()).isEqualTo(toSend.getRoomId()); + } + + @Test + @SneakyThrows(NotFoundException.class) + public void testDelete() { + when(principal.getName()).thenReturn("user"); + doNothing().when(deviceService).deleteByIdAsOwner(eq(42L), eq("user")); + MockHttpServletRequest request = new MockHttpServletRequest(); + RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request)); + Assertions.assertDoesNotThrow(() -> controller.deleteById(42L, principal)); + } + + @Test + @SneakyThrows(NotFoundException.class) + public void testOperate() { + when(principal.getName()).thenReturn("user"); + Switch aSwitch = new Switch(); + aSwitch.setOn(true); + RegularLight light = new RegularLight(); + RegularLight light2 = new RegularLight(); + aSwitch.connect(light, true); + aSwitch.connect(light2, true); + SwitchOperationRequest toSend = new SwitchOperationRequest(); + toSend.setType(SwitchOperationRequest.OperationType.ON); + toSend.setId(42L); + when(repository.findByIdAndUsername(42L, "user")).thenReturn(Optional.of(aSwitch)); + ArrayList helper = new ArrayList(); + helper.add(light); + helper.add(light2); + when(deviceService.saveAllAsOwner(aSwitch.getSwitchables(), principal.getName())) + .thenReturn(helper); + MockHttpServletRequest request = new MockHttpServletRequest(); + RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request)); + List list = controller.operate(toSend, principal); + assertThat(list.size()).isEqualTo(2); + } + + @Test + @SneakyThrows(NotFoundException.class) + public void testOperateOFF() { + when(principal.getName()).thenReturn("user"); + Switch aSwitch = new Switch(); + aSwitch.setOn(true); + RegularLight light = new RegularLight(); + RegularLight light2 = new RegularLight(); + aSwitch.connect(light, true); + aSwitch.connect(light2, true); + SwitchOperationRequest toSend = new SwitchOperationRequest(); + toSend.setType(SwitchOperationRequest.OperationType.OFF); + toSend.setId(42L); + when(repository.findByIdAndUsername(42L, "user")).thenReturn(Optional.of(aSwitch)); + ArrayList helper = new ArrayList(); + helper.add(light); + helper.add(light2); + when(deviceService.saveAllAsOwner(aSwitch.getSwitchables(), principal.getName())) + .thenReturn(helper); + MockHttpServletRequest request = new MockHttpServletRequest(); + RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request)); + List list = controller.operate(toSend, principal); + assertThat(list.size()).isEqualTo(2); + } + + @Test + @SneakyThrows(NotFoundException.class) + public void testOperateToggle() { + when(principal.getName()).thenReturn("user"); + Switch aSwitch = new Switch(); + aSwitch.setOn(true); + RegularLight light = new RegularLight(); + RegularLight light2 = new RegularLight(); + aSwitch.connect(light, true); + aSwitch.connect(light2, true); + SwitchOperationRequest toSend = new SwitchOperationRequest(); + toSend.setType(SwitchOperationRequest.OperationType.TOGGLE); + toSend.setId(42L); + when(repository.findByIdAndUsername(42L, "user")).thenReturn(Optional.of(aSwitch)); + ArrayList helper = new ArrayList(); + helper.add(light); + helper.add(light2); + when(deviceService.saveAllAsOwner(aSwitch.getSwitchables(), principal.getName())) + .thenReturn(helper); + MockHttpServletRequest request = new MockHttpServletRequest(); + RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request)); + List list = controller.operate(toSend, principal); + assertThat(list.size()).isEqualTo(2); + } +} diff --git a/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/SwitchableStateControllerTests.java b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/SwitchableStateControllerTests.java new file mode 100644 index 0000000..384d933 --- /dev/null +++ b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/SwitchableStateControllerTests.java @@ -0,0 +1,52 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.controller; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.when; + +import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.SwitchableStateSaveRequest; +import ch.usi.inf.sa4.sanmarinoes.smarthut.error.NotFoundException; +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.*; +import java.util.Optional; +import lombok.SneakyThrows; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +@ExtendWith(MockitoExtension.class) +@WithMockUser(username = "user") +@DisplayName("SwitchableState controller test") +public class SwitchableStateControllerTests { + @InjectMocks private SwitchableStateController controller; + + @Mock private SwitchableStateRepository repository; + + @Test + @SneakyThrows(NotFoundException.class) + public void testUpdate() { + SwitchableState state = new SwitchableState(); + state.setOn(true); + SwitchableStateSaveRequest toSend = new SwitchableStateSaveRequest(); + toSend.setOn(true); + toSend.setId(0L); + when(repository.findById(toSend.getId())).thenReturn(Optional.of(state)); + assertThat(controller.update(toSend).isOn()).isEqualTo(toSend.isOn()); + } + + @Test + public void testDelete() { + doNothing().when(repository).deleteById(eq(42L)); + MockHttpServletRequest request = new MockHttpServletRequest(); + RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request)); + Assertions.assertDoesNotThrow(() -> controller.delete(42L)); + } +} diff --git a/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/ThermostatConditionControllerTests.java b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/ThermostatConditionControllerTests.java new file mode 100644 index 0000000..2210ce4 --- /dev/null +++ b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/ThermostatConditionControllerTests.java @@ -0,0 +1,108 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.controller; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.when; + +import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.ThermostatConditionSaveRequest; +import ch.usi.inf.sa4.sanmarinoes.smarthut.error.NotFoundException; +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.*; +import java.security.Principal; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import lombok.SneakyThrows; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +@DisplayName("ThermostatConditionController tests") +@ExtendWith(MockitoExtension.class) +@WithMockUser(username = "user") +public class ThermostatConditionControllerTests { + @InjectMocks private ThermostatConditionController controller; + + @Mock private ThermostatConditionRepository repository; + + @Mock private Principal principal; + + @Test + public void testCreate() { + ThermostatCondition condition = new ThermostatCondition(); + condition.setMode(Thermostat.Mode.HEATING); + condition.setDeviceId(1L); + condition.setAutomationId(2L); + condition.setOperator(ThermostatCondition.Operator.EQUAL); + when(repository.save(condition)).thenReturn(condition); + ThermostatConditionSaveRequest toSend = new ThermostatConditionSaveRequest(); + toSend.setAutomationId(2L); + toSend.setDeviceId(1L); + toSend.setOperator(ThermostatCondition.Operator.EQUAL); + toSend.setMode(Thermostat.Mode.HEATING); + ThermostatCondition returned = controller.create(toSend); + assertThat(returned.getMode()).isEqualTo(toSend.getMode()); + assertThat(returned.getAutomationId()).isEqualTo(toSend.getAutomationId()); + assertThat(returned.getOperator()).isEqualTo(toSend.getOperator()); + assertThat(returned.getDeviceId()).isEqualTo(toSend.getDeviceId()); + } + + @Test + @SneakyThrows(NotFoundException.class) + public void testUpdate() { + ThermostatCondition condition = new ThermostatCondition(); + condition.setMode(Thermostat.Mode.HEATING); + condition.setDeviceId(1L); + condition.setAutomationId(2L); + condition.setOperator(ThermostatCondition.Operator.EQUAL); + when(repository.save(condition)).thenReturn(condition); + ThermostatConditionSaveRequest toSend = new ThermostatConditionSaveRequest(); + toSend.setId(34L); + toSend.setAutomationId(2L); + toSend.setDeviceId(1L); + toSend.setOperator(ThermostatCondition.Operator.EQUAL); + toSend.setMode(Thermostat.Mode.HEATING); + when(repository.findById(34L)).thenReturn(Optional.of(condition)); + ThermostatCondition returned = controller.update(toSend); + assertThat(returned.getMode()).isEqualTo(toSend.getMode()); + assertThat(returned.getAutomationId()).isEqualTo(toSend.getAutomationId()); + assertThat(returned.getOperator()).isEqualTo(toSend.getOperator()); + assertThat(returned.getDeviceId()).isEqualTo(toSend.getDeviceId()); + } + + @Test + public void testGetAll() { + ThermostatCondition condition1 = new ThermostatCondition(); + condition1.setAutomationId(1L); + ThermostatCondition condition2 = new ThermostatCondition(); + condition2.setAutomationId(1L); + ThermostatCondition condition3 = new ThermostatCondition(); + condition3.setAutomationId(1L); + ArrayList list = new ArrayList<>(); + list.add(condition1); + list.add(condition2); + list.add(condition3); + when(repository.findAllByAutomationId(42L)).thenReturn(list); + List returned = controller.getAll(42L); + assertThat(returned).contains(condition1); + assertThat(returned).contains(condition2); + assertThat(returned).contains(condition3); + assertThat(returned.size()).isEqualTo(3); + } + + @Test + public void testDelete() { + doNothing().when(repository).deleteById(eq(42L)); + MockHttpServletRequest request = new MockHttpServletRequest(); + RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request)); + Assertions.assertDoesNotThrow(() -> controller.delete(42L)); + } +} diff --git a/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/ThermostatControllerTests.java b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/ThermostatControllerTests.java new file mode 100644 index 0000000..41dfd0d --- /dev/null +++ b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/ThermostatControllerTests.java @@ -0,0 +1,178 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.controller; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.when; + +import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.ThermostatSaveRequest; +import ch.usi.inf.sa4.sanmarinoes.smarthut.error.DuplicateStateException; +import ch.usi.inf.sa4.sanmarinoes.smarthut.error.NotFoundException; +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.Scene; +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.SceneRepository; +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.State; +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.StateRepository; +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.SwitchableState; +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.Thermostat; +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.ThermostatRepository; +import ch.usi.inf.sa4.sanmarinoes.smarthut.service.DeviceService; +import ch.usi.inf.sa4.sanmarinoes.smarthut.service.ThermostatPopulationService; +import java.math.BigDecimal; +import java.security.Principal; +import java.util.Optional; +import lombok.SneakyThrows; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +@ExtendWith(MockitoExtension.class) +@WithMockUser(username = "user") +public class ThermostatControllerTests { + + @InjectMocks private ThermostatController thermostatController; + + @Mock private StateRepository stateRepository; + @Mock private SceneRepository sceneRepository; + @Mock private ThermostatPopulationService thermostatService; + @Mock private ThermostatRepository thermostatRepository; + @Mock private DeviceService deviceService; + @Mock private Principal mockPrincipal; + + @BeforeEach + public void setup() { + when(mockPrincipal.getName()).thenReturn("user"); + } + + private void checkThermostatAgainstRequest( + final Thermostat toCheck, final ThermostatSaveRequest request) { + assertThat(toCheck).isNotNull(); + assertThat(toCheck.isOn()).isEqualTo(request.isTurnOn()); + assertThat(toCheck.getTargetTemperature()).isEqualTo(request.getTargetTemperature()); + assertThat(toCheck.isUseExternalSensors()).isEqualTo(request.isUseExternalSensors()); + assertThat(toCheck.getName()).isEqualTo(request.getName()); + assertThat(toCheck.getRoomId()).isEqualTo(request.getRoomId()); + if (request.getErr() != null) { + assertThat(toCheck.getErr()).isEqualTo(request.getErr()); + } + + if (request.getTypical() != null) { + assertThat(toCheck.getTypical()).isEqualTo(request.getTypical()); + } + } + + @Test + @DisplayName("when creating should return the same object") + @SneakyThrows(NotFoundException.class) + public void testCreate() { + doNothing().when(deviceService).throwIfRoomNotOwned(anyLong(), eq("user")); + doNothing().when(thermostatService).populateMeasuredTemperature(any(Thermostat.class)); + when(deviceService.saveAsOwner(any(Thermostat.class), eq("user"))) + .thenAnswer(i -> i.getArguments()[0]); + when(thermostatRepository.save(any(Thermostat.class))).thenAnswer(i -> i.getArguments()[0]); + + MockHttpServletRequest request = new MockHttpServletRequest(); + RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request)); + + final ThermostatSaveRequest toSend = new ThermostatSaveRequest(); + toSend.setRoomId(42L); + toSend.setTargetTemperature(new BigDecimal(40)); + toSend.setName("Thermostat Test"); + toSend.setUseExternalSensors(true); + toSend.setErr(BigDecimal.valueOf(10)); + toSend.setTypical(BigDecimal.valueOf(15)); + + final Thermostat thermostat = thermostatController.create(toSend, mockPrincipal); + + checkThermostatAgainstRequest(thermostat, toSend); + } + + @Test + @DisplayName("when updating should return the same object") + @SneakyThrows(NotFoundException.class) + public void testUpdate() { + + final ThermostatSaveRequest toSend = new ThermostatSaveRequest(); + toSend.setRoomId(42L); + toSend.setTargetTemperature(new BigDecimal(40)); + toSend.setName("Thermostat Test"); + toSend.setUseExternalSensors(true); + + final Thermostat toUpdate = new Thermostat(); + toSend.setRoomId(20L); + toSend.setTargetTemperature(new BigDecimal(50)); + toSend.setName("Thermostat"); + toSend.setUseExternalSensors(false); + + when(thermostatRepository.findByIdAndUsername(anyLong(), any(String.class))) + .thenReturn(java.util.Optional.of(toUpdate)); + when(thermostatRepository.save(any(Thermostat.class))).thenAnswer(i -> i.getArguments()[0]); + doNothing().when(thermostatService).populateMeasuredTemperature(any(Thermostat.class)); + when(deviceService.saveAsOwner(any(Thermostat.class), eq("user"))) + .thenAnswer(i -> i.getArguments()[0]); + + MockHttpServletRequest request = new MockHttpServletRequest(); + RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request)); + + final Thermostat thermostat = thermostatController.update(toSend, mockPrincipal); + + checkThermostatAgainstRequest(thermostat, toSend); + } + + @Test + @DisplayName("an existing id should succeed") + @SneakyThrows(NotFoundException.class) + public void testDelete() { + + doNothing().when(deviceService).deleteByIdAsOwner(eq(42L), eq("user")); + + MockHttpServletRequest request = new MockHttpServletRequest(); + RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request)); + + Assertions.assertDoesNotThrow(() -> thermostatController.deleteById(42L, mockPrincipal)); + } + + @Test + public void testSceneBinding() { + Thermostat thermostat = new Thermostat(); + when(thermostatRepository.findByIdAndUsername(24L, "user")) + .thenReturn(Optional.of(thermostat)); + Scene scene = new Scene(); + scene.setId(1L); + SwitchableState state = new SwitchableState(); + state.setSceneId(1L); + State s = thermostat.cloneState(); + when(sceneRepository.findById(1L)).thenReturn(Optional.of(scene)); + when(stateRepository.countByDeviceIdAndSceneId(24L, 1L)).thenReturn(0); + Assertions.assertDoesNotThrow( + () -> thermostatController.sceneBinding(24L, 1L, mockPrincipal)); + } + + @Test + public void testSceneBinding2() { + when(mockPrincipal.getName()).thenReturn("user"); + Thermostat thermostat = new Thermostat(); + when(thermostatRepository.findByIdAndUsername(24L, "user")) + .thenReturn(Optional.of(thermostat)); + Scene scene = new Scene(); + scene.setId(1L); + SwitchableState state = new SwitchableState(); + state.setSceneId(1L); + State s = thermostat.cloneState(); + when(sceneRepository.findById(1L)).thenReturn(Optional.of(scene)); + when(stateRepository.countByDeviceIdAndSceneId(24L, 1L)).thenReturn(2); + assertThatThrownBy(() -> thermostatController.sceneBinding(24L, 1L, mockPrincipal)) + .isInstanceOf(DuplicateStateException.class); + } +} diff --git a/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/UserAccountControllerTests.java b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/UserAccountControllerTests.java new file mode 100644 index 0000000..3269550 --- /dev/null +++ b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/UserAccountControllerTests.java @@ -0,0 +1,163 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.controller; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import ch.usi.inf.sa4.sanmarinoes.smarthut.config.EmailConfigurationService; +import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.InitPasswordResetRequest; +import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.PasswordResetRequest; +import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.UserRegistrationRequest; +import ch.usi.inf.sa4.sanmarinoes.smarthut.error.DuplicateRegistrationException; +import ch.usi.inf.sa4.sanmarinoes.smarthut.error.EmailTokenNotFoundException; +import ch.usi.inf.sa4.sanmarinoes.smarthut.error.UserNotFoundException; +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.ConfirmationToken; +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.ConfirmationTokenRepository; +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.User; +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.UserRepository; +import ch.usi.inf.sa4.sanmarinoes.smarthut.service.EmailSenderService; +import java.io.IOException; +import javax.servlet.http.HttpServletResponse; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.mail.SimpleMailMessage; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.test.context.support.WithMockUser; + +@ExtendWith(MockitoExtension.class) +@WithMockUser(username = "user") +public class UserAccountControllerTests { + + @InjectMocks private UserAccountController userAccountController; + + @Mock private UserRepository userRepository; + + @Mock private EmailSenderService emailSenderService; + + @Mock private EmailConfigurationService emailConfigurationService; + + @Mock private ConfirmationTokenRepository confirmationTokenRepository; + + @Mock private BCryptPasswordEncoder passwordEncoder; + + @Test + public void testRegisterUser() { + final UserRegistrationRequest registrationRequest = new UserRegistrationRequest(); + registrationRequest.setEmail("info@theshell.ch"); + registrationRequest.setUsername("username"); + + when(userRepository.findByEmailIgnoreCase("info@theshell.ch")).thenReturn(null); + when(userRepository.findByEmailIgnoreCase("info@vimtok.com")).thenReturn(new User()); + when(userRepository.findByUsername("username")).thenReturn(new User()); + when(userRepository.findByUsername("simoneriva")).thenReturn(null); + + Assertions.assertThatThrownBy(() -> userAccountController.registerUser(registrationRequest)) + .isInstanceOf(DuplicateRegistrationException.class); + + registrationRequest.setUsername("simoneriva"); + registrationRequest.setEmail("info@vimtok.com"); + + Assertions.assertThatThrownBy(() -> userAccountController.registerUser(registrationRequest)) + .isInstanceOf(DuplicateRegistrationException.class); + } + + @Test + public void testResetPassword() throws EmailTokenNotFoundException { + final User u = new User(); + final ConfirmationToken c = new ConfirmationToken(u); + c.setResetPassword(false); + + when(userRepository.save(u)).thenReturn(u); + + when(confirmationTokenRepository.findByConfirmToken("token")).thenReturn(c); + when(confirmationTokenRepository.findByConfirmToken("token2")).thenReturn(null); + doNothing().when(confirmationTokenRepository).delete(c); + when(passwordEncoder.encode("password")).thenReturn("encoded"); + + assertThatThrownBy( + () -> + userAccountController.resetPassword( + new PasswordResetRequest("token2", "password"))) + .isInstanceOf(EmailTokenNotFoundException.class); + + assertThatThrownBy( + () -> + userAccountController.resetPassword( + new PasswordResetRequest("token", "password"))) + .isInstanceOf(EmailTokenNotFoundException.class); + + c.setResetPassword(true); + + userAccountController.resetPassword(new PasswordResetRequest("token", "password")); + + assertThat(u.getPassword()).isEqualTo("encoded"); + } + + @Test + public void testInitResetPassword() throws UserNotFoundException { + when(emailConfigurationService.getResetPasswordSubject()).thenReturn("password reset"); + when(emailConfigurationService.getResetPassword()).thenReturn("reset-password"); + when(emailConfigurationService.getResetPasswordPath()).thenReturn("path"); + + final User u = new User(); + u.setEmail("info@theshell.com"); + + boolean[] done = new boolean[1]; + + doAnswer( + i -> { + final SimpleMailMessage m = i.getArgument(0); + assertThat(m.getSubject()).isEqualTo("password reset"); + assertThat(m.getText()).startsWith("reset-password path"); + done[0] = true; + return null; + }) + .when(emailSenderService) + .sendEmail(any()); + + doNothing().when(confirmationTokenRepository).deleteByUserAndResetPassword(u, true); + when(confirmationTokenRepository.save(any())).thenAnswer(i -> i.getArgument(0)); + + when(userRepository.findByEmailIgnoreCase("info@theshell.ch")).thenReturn(u); + when(userRepository.findByEmailIgnoreCase("info@vimtok.com")).thenReturn(null); + + assertThatThrownBy( + () -> + userAccountController.initResetPassword( + new InitPasswordResetRequest("info@vimtok.com"))) + .isInstanceOf(UserNotFoundException.class); + userAccountController.initResetPassword(new InitPasswordResetRequest("info@theshell.ch")); + + assertThat(done[0]).isTrue(); + } + + @Test + public void testConfirmUserAccount() throws EmailTokenNotFoundException, IOException { + final User u = new User(); + final ConfirmationToken c = new ConfirmationToken(u); + c.setResetPassword(true); + + when(confirmationTokenRepository.findByConfirmToken("token")).thenReturn(null); + when(confirmationTokenRepository.findByConfirmToken(c.getConfirmToken())).thenReturn(c); + when(userRepository.save(u)).thenReturn(u); + final HttpServletResponse r = new MockHttpServletResponse(); + + assertThatThrownBy(() -> userAccountController.confirmUserAccount("token", r)) + .isInstanceOf(EmailTokenNotFoundException.class); + assertThatThrownBy(() -> userAccountController.confirmUserAccount(c.getConfirmToken(), r)) + .isInstanceOf(EmailTokenNotFoundException.class); + + c.setResetPassword(false); + + when(emailConfigurationService.getRegistrationRedirect()).thenReturn("malusa.html"); + + userAccountController.confirmUserAccount(c.getConfirmToken(), r); + assertThat(u.isEnabled()).isTrue(); + } +} diff --git a/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/AutomationFastUpdateRequestTest.java b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/AutomationFastUpdateRequestTest.java new file mode 100644 index 0000000..d796526 --- /dev/null +++ b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/AutomationFastUpdateRequestTest.java @@ -0,0 +1,90 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.dto; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.automation.*; +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.*; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +@DisplayName("Automation Update DTO") +public class AutomationFastUpdateRequestTest { + + @Test + @DisplayName(" checking boolean trigger ") + public void booleanTriggerDTOTest() { + BooleanTriggerDTO booleanTriggerDTO = new BooleanTriggerDTO(); + booleanTriggerDTO.setOn(true); + booleanTriggerDTO.setDeviceId(42); + BooleanTrigger booleanTrigger = (BooleanTrigger) booleanTriggerDTO.toModel(); + assertEquals(booleanTrigger.isOn(), booleanTriggerDTO.isOn()); + assertEquals(booleanTrigger.getDeviceId(), booleanTriggerDTO.getDeviceId()); + } + + @Test + @DisplayName(" checking range trigger ") + public void rangeTriggerDTOTest() { + RangeTriggerDTO rangeTriggerDTO = new RangeTriggerDTO(); + rangeTriggerDTO.setOperator(Operator.EQUAL); + rangeTriggerDTO.setDeviceId(420); + rangeTriggerDTO.setRange(12); + + RangeTrigger rangeTrigger = (RangeTrigger) rangeTriggerDTO.toModel(); + assertEquals(rangeTrigger.getOperator(), rangeTriggerDTO.getOperator()); + assertEquals(rangeTrigger.getRange(), rangeTriggerDTO.getRange()); + assertEquals(rangeTrigger.getDeviceId(), rangeTriggerDTO.getDeviceId()); + } + + @Test + @DisplayName(" checking scene priority ") + public void scenePriorityDTOTest() { + ScenePriorityDTO scenePriorityDTO = new ScenePriorityDTO(); + scenePriorityDTO.setPriority(67); + scenePriorityDTO.setSceneId(21); + + ScenePriority scenePriority = scenePriorityDTO.toModel(); + assertEquals(scenePriority.getPriority(), scenePriorityDTO.getPriority()); + assertEquals(scenePriority.getSceneId(), scenePriorityDTO.getSceneId()); + } + + @Test + @DisplayName(" checking boolean condition ") + public void booleanConditionDTOTest() { + BooleanConditionDTO booleanConditionDTO = new BooleanConditionDTO(); + booleanConditionDTO.setOn(true); + booleanConditionDTO.setDeviceId(17); + + BooleanCondition booleanCondition = (BooleanCondition) booleanConditionDTO.toModel(); + assertEquals(booleanCondition.isOn(), booleanConditionDTO.isOn()); + assertEquals(booleanCondition.getDeviceId(), booleanConditionDTO.getDeviceId()); + } + + @Test + @DisplayName(" checking range condition ") + public void rangeConditionDTOTest() { + RangeConditionDTO rangeConditionDTO = new RangeConditionDTO(); + rangeConditionDTO.setOperator(Operator.LESS); + rangeConditionDTO.setRange(82.01); + rangeConditionDTO.setDeviceId(13); + + RangeCondition rangeCondition = (RangeCondition) rangeConditionDTO.toModel(); + assertEquals(rangeCondition.getOperator(), rangeConditionDTO.getOperator()); + assertEquals(rangeCondition.getRange(), rangeConditionDTO.getRange()); + assertEquals(rangeCondition.getDeviceId(), rangeConditionDTO.getDeviceId()); + } + + @Test + @DisplayName(" checking thermostat condition ") + public void thermostatConditionDTOTest() { + ThermostatConditionDTO thermostatConditionDTO = new ThermostatConditionDTO(); + thermostatConditionDTO.setDeviceId(25); + thermostatConditionDTO.setOperator(ThermostatCondition.Operator.EQUAL); + thermostatConditionDTO.setMode(Thermostat.Mode.HEATING); + + ThermostatCondition thermostatCondition = + (ThermostatCondition) thermostatConditionDTO.toModel(); + assertEquals(thermostatCondition.getMode(), thermostatConditionDTO.getMode()); + assertEquals(thermostatCondition.getOperator(), thermostatConditionDTO.getOperator()); + assertEquals(thermostatCondition.getDeviceId(), thermostatConditionDTO.getDeviceId()); + } +} diff --git a/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/SceneSaveRequestTests.java b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/SceneSaveRequestTests.java new file mode 100644 index 0000000..8bd6cc0 --- /dev/null +++ b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/SceneSaveRequestTests.java @@ -0,0 +1,52 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.dto; + +import static org.junit.jupiter.api.Assertions.*; + +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.Icon; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +@DisplayName("SceneSaveRequest tests") +public class SceneSaveRequestTests { + + private SceneSaveRequest scene; + + @BeforeEach + private void createSceneSaveRequest() { + scene = new SceneSaveRequest(); + } + + @Test + @DisplayName("guestAccessEnable") + public void testGuestAccess() { + assertFalse(scene.isGuestAccessEnabled()); + } + + @Test + @DisplayName("set guestAccess") + public void testSetGuestAccess() { + scene.setGuestAccessEnabled(true); + assertTrue(scene.isGuestAccessEnabled()); + } + + @Test + @DisplayName("test getId") + public void testGetId() { + assertEquals(0L, scene.getId()); + } + + @Test + @DisplayName("test getName") + public void testGetName() { + scene.setName("Roberto"); + assertEquals("Roberto", scene.getName()); + } + + @Test + @DisplayName("test getIcon") + public void testGetIcon() { + scene.setIcon(Icon.FEMALE); + assertEquals("female", scene.getIcon().toString()); + } +} diff --git a/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/SensorSaveRequestTests.java b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/SensorSaveRequestTests.java new file mode 100644 index 0000000..6c99297 --- /dev/null +++ b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/SensorSaveRequestTests.java @@ -0,0 +1,82 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.dto; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.Sensor; +import java.math.BigDecimal; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +@DisplayName("SensorSaveRequest tests") +public class SensorSaveRequestTests { + + private SensorSaveRequest sensor; + + @BeforeEach + public void createSensorSaveRequest() { + this.sensor = new SensorSaveRequest(); + } + + @Test + @DisplayName("test setRoomId") + public void testSetRoomId() { + sensor.setRoomId(42L); + assertEquals(42L, sensor.getRoomId()); + } + + @Test + @DisplayName("test constructor") + public void testConstructorNotEmpty() { + SensorSaveRequest newSaveRequest = + new SensorSaveRequest(Sensor.SensorType.HUMIDITY, new BigDecimal(12), 12L, "name"); + assertNotNull(newSaveRequest.getSensor()); + assertNotNull(newSaveRequest.getName()); + assertNotNull(newSaveRequest.getRoomId()); + assertNotNull(newSaveRequest.getValue()); + } + + @Test + @DisplayName("test setName") + public void testSetName() { + sensor.setName("Giovanni"); + assertEquals("Giovanni", sensor.getName()); + } + + @Test + @DisplayName("test setValue") + public void testSetValue() { + sensor.setValue(new BigDecimal(42)); + assertEquals(new BigDecimal(42), sensor.getValue()); + } + + @Test + @DisplayName("test set to TEMPERATURE") + public void testSetToTemperature() { + sensor.setSensor(Sensor.SensorType.TEMPERATURE); + assertEquals(Sensor.SensorType.TEMPERATURE, sensor.getSensor()); + } + + @Test + @DisplayName("test set to HUMIDITY") + public void testSetToHumidity() { + sensor.setSensor(Sensor.SensorType.HUMIDITY); + assertEquals(Sensor.SensorType.HUMIDITY, sensor.getSensor()); + } + + @Test + @DisplayName("test set to LIGHT") + public void testSetToLight() { + sensor.setSensor(Sensor.SensorType.LIGHT); + assertEquals(Sensor.SensorType.LIGHT, sensor.getSensor()); + } + + @Test + @DisplayName("test SensorType") + public void testSetSensorType() { + assertEquals("TEMPERATURE", Sensor.SensorType.TEMPERATURE.name()); + assertEquals("HUMIDITY", Sensor.SensorType.HUMIDITY.name()); + assertEquals("LIGHT", Sensor.SensorType.LIGHT.name()); + } +} diff --git a/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/SwitchOperationRequestTests.java b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/SwitchOperationRequestTests.java new file mode 100644 index 0000000..dac05c8 --- /dev/null +++ b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/SwitchOperationRequestTests.java @@ -0,0 +1,45 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.dto; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +@DisplayName("switchOperationRequest tests") +public class SwitchOperationRequestTests { + private SwitchOperationRequest request; + + @BeforeEach + private void createRequest() { + this.request = new SwitchOperationRequest(); + } + + @Test + @DisplayName("test setId") + public void testSetId() { + request.setId(42L); + assertEquals(42L, request.getId()); + } + + @Test + @DisplayName("test setType") + public void testSetTypeOFF() { + request.setType(SwitchOperationRequest.OperationType.OFF); + assertEquals(SwitchOperationRequest.OperationType.OFF, request.getType()); + } + + @Test + @DisplayName("test setType") + public void testSetTypeON() { + request.setType(SwitchOperationRequest.OperationType.ON); + assertEquals(SwitchOperationRequest.OperationType.ON, request.getType()); + } + + @Test + @DisplayName("test setType") + public void testSetTypeTOGGLE() { + request.setType(SwitchOperationRequest.OperationType.TOGGLE); + assertEquals(SwitchOperationRequest.OperationType.TOGGLE, request.getType()); + } +} diff --git a/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/SwitchableSaveRequestTests.java b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/SwitchableSaveRequestTests.java new file mode 100644 index 0000000..ac3b7a2 --- /dev/null +++ b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/SwitchableSaveRequestTests.java @@ -0,0 +1,59 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.dto; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +@DisplayName("SwitchableSaveRequest tests") +public class SwitchableSaveRequestTests { + + private SwitchableSaveRequest saveRequest; + + @BeforeEach + private void createSaveRequest() { + this.saveRequest = new SwitchableSaveRequest(); + } + + @Test + @DisplayName("test setRoomId") + public void testSetRoomId() { + saveRequest.setRoomId(42L); + assertEquals(42L, saveRequest.getRoomId()); + } + + @Test + @DisplayName("test setName") + public void testSetName() { + saveRequest.setName("Giovanni"); + assertEquals("Giovanni", saveRequest.getName()); + } + + @Test + @DisplayName("test isOn()") + public void inOnTest() { + assertFalse(saveRequest.isOn()); + } + + @Test + @DisplayName("test setOn(true) ") + public void setOnTestTrue() { + saveRequest.setOn(true); + assertTrue(saveRequest.isOn()); + } + + @Test + @DisplayName("test setOn(false) ") + public void setOnTestFalse() { + saveRequest.setOn(false); + assertFalse(saveRequest.isOn()); + } + + @Test + @DisplayName("test setId") + public void testSetId() { + saveRequest.setId(300771L); + assertEquals(300771L, saveRequest.getId()); + } +} diff --git a/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/SwitchableStateSaveRequestTests.java b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/SwitchableStateSaveRequestTests.java new file mode 100644 index 0000000..d98311b --- /dev/null +++ b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/SwitchableStateSaveRequestTests.java @@ -0,0 +1,43 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.dto; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +@DisplayName("switchableStateSaveRequest tests") +public class SwitchableStateSaveRequestTests { + private SwitchableStateSaveRequest saveRequest; + + @BeforeEach + public void createSwitchableStateSaveRequest() { + this.saveRequest = new SwitchableStateSaveRequest(); + } + + @Test + @DisplayName("test isOn()") + public void isOnTest() { + assertFalse(saveRequest.isOn()); + } + + @Test + @DisplayName("test setOn(true) ") + public void setOnTestTrue() { + saveRequest.setOn(true); + assertTrue(saveRequest.isOn()); + } + + @Test + @DisplayName("test setOn(false) ") + public void setOnTestFalse() { + saveRequest.setOn(false); + assertFalse(saveRequest.isOn()); + } + + @Test + @DisplayName("test setId") + public void testSetId() { + assertEquals(null, saveRequest.getId()); + } +} diff --git a/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/ThermostatConditionSaveRequestTests.java b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/ThermostatConditionSaveRequestTests.java new file mode 100644 index 0000000..9283e6f --- /dev/null +++ b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/ThermostatConditionSaveRequestTests.java @@ -0,0 +1,61 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.dto; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.Thermostat; +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.ThermostatCondition; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +@DisplayName("ThermostatConditionSaveRequest tests") +public class ThermostatConditionSaveRequestTests { + + private ThermostatConditionSaveRequest saveRequest; + + @BeforeEach + private void createSaveRequest() { + this.saveRequest = new ThermostatConditionSaveRequest(); + } + + @Test + @DisplayName("test setDeviceId") + public void testSetDeviceId() { + this.saveRequest.setDeviceId(42L); + assertEquals(42L, saveRequest.getDeviceId()); + } + + @Test + @DisplayName("test setAutomationId") + public void testSetAutomationId() { + this.saveRequest.setAutomationId(42L); + assertEquals(42L, saveRequest.getAutomationId()); + } + + @Test + @DisplayName("test setOperator") + public void testSetOperatorEqual() { + saveRequest.setOperator(ThermostatCondition.Operator.EQUAL); + assertEquals(ThermostatCondition.Operator.EQUAL, saveRequest.getOperator()); + } + + @Test + @DisplayName("test setOperator") + public void testSetOperatorNotEqual() { + saveRequest.setOperator(ThermostatCondition.Operator.NOTEQUAL); + assertEquals(ThermostatCondition.Operator.NOTEQUAL, saveRequest.getOperator()); + } + + @Test + @DisplayName("test setMode") + public void testSetMode() { + saveRequest.setMode(Thermostat.Mode.HEATING); + assertEquals(Thermostat.Mode.HEATING, saveRequest.getMode()); + } + + @Test + @DisplayName("test getId") + public void testGetId() { + assertEquals(0L, saveRequest.getId()); + } +} diff --git a/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/ThermostatSaveRequestTests.java b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/ThermostatSaveRequestTests.java new file mode 100644 index 0000000..5592bd2 --- /dev/null +++ b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/ThermostatSaveRequestTests.java @@ -0,0 +1,80 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.dto; + +import static org.junit.jupiter.api.Assertions.*; + +import java.math.BigDecimal; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +@DisplayName("ThermostatSaveRequest tests") +public class ThermostatSaveRequestTests { + private ThermostatSaveRequest saveRequest; + + @BeforeEach + private void createSaveRequest() { + saveRequest = new ThermostatSaveRequest(); + } + + @Test + @DisplayName("test setRoomId") + public void testSetRoomId() { + saveRequest.setRoomId(42l); + assertEquals(42l, saveRequest.getRoomId()); + } + + @Test + @DisplayName("test setName") + public void testSetName() { + saveRequest.setName("Giovanni"); + assertEquals("Giovanni", saveRequest.getName()); + } + + @Test + @DisplayName("test isOn()") + public void inOnTest() { + assertFalse(saveRequest.isTurnOn()); + } + + @Test + @DisplayName("test setOn(true) ") + public void setOnTestTrue() { + saveRequest.setTurnOn(true); + assertTrue(saveRequest.isTurnOn()); + } + + @Test + @DisplayName("test setOn(false) ") + public void setOnTestFalse() { + saveRequest.setTurnOn(false); + assertFalse(saveRequest.isTurnOn()); + } + + @Test + @DisplayName("test setId") + public void testSetId() { + saveRequest.setId(17l); + assertEquals(17l, saveRequest.getId()); + } + + @Test + @DisplayName("test setExternalSensor true") + public void testExternalSensorTrue() { + saveRequest.setUseExternalSensors(true); + assertTrue(saveRequest.isUseExternalSensors()); + } + + @Test + @DisplayName("test setExternalSensor false") + public void testExternalSensorFalse() { + saveRequest.setUseExternalSensors(false); + assertFalse(saveRequest.isUseExternalSensors()); + } + + @Test + @DisplayName("test targetTemperature") + public void testTargetTemperature() { + saveRequest.setTargetTemperature(new BigDecimal(23)); + assertEquals(new BigDecimal(23), saveRequest.getTargetTemperature()); + } +} diff --git a/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/UserRegistrationRequestTests.java b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/UserRegistrationRequestTests.java new file mode 100644 index 0000000..232c8f5 --- /dev/null +++ b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/UserRegistrationRequestTests.java @@ -0,0 +1,45 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.dto; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +@DisplayName("userRegistrationRequest tests") +public class UserRegistrationRequestTests { + private UserRegistrationRequest request; + + @BeforeEach + private void createRequest() { + request = new UserRegistrationRequest(); + } + + @Test + @DisplayName("test setName") + public void testSetName() { + request.setName("Tizio Sempronio"); + assertEquals("Tizio Sempronio", request.getName()); + } + + @Test + @DisplayName("test setUserName") + public void testUserName() { + request.setUsername("xXDarkAngelCraftXx"); + assertEquals("xXDarkAngelCraftXx", request.getUsername()); + } + + @Test + @DisplayName("test setPassword") + public void testPassword() { + request.setPassword("password123"); + assertEquals("password123", request.getPassword()); + } + + @Test + @DisplayName("test setEmail") + public void testEmail() { + request.setEmail("fakemail@service.ussr"); + assertEquals("fakemail@service.ussr", request.getEmail()); + } +} diff --git a/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/UserResponseTests.java b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/UserResponseTests.java new file mode 100644 index 0000000..ba82712 --- /dev/null +++ b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/UserResponseTests.java @@ -0,0 +1,41 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.dto; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.User; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +@DisplayName("test UserResponse") +public class UserResponseTests { + private UserResponse response; + private User user; + + @BeforeEach + private void createUser() { + user = new User(); + user.setName("John RealName"); + user.setId(29l); + user.setUsername("pseudonym"); + response = response.fromUser(user); + } + + @Test + @DisplayName("test getId") + public void testGetId() { + assertEquals(29l, response.getId()); + } + + @Test + @DisplayName("test getUsername") + public void testGetUsername() { + assertEquals("pseudonym", response.getUsername()); + } + + @Test + @DisplayName("test getName") + public void testGetaName() { + assertEquals("John RealName", response.getName()); + } +} diff --git a/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/error/ExceptionTests.java b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/error/ExceptionTests.java new file mode 100644 index 0000000..bf26000 --- /dev/null +++ b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/error/ExceptionTests.java @@ -0,0 +1,71 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.error; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +@DisplayName("Exception tests") +public class ExceptionTests { + @Test + public void testBadData() { + try { + throw new BadDataException("message"); + } catch (BadDataException e) { + assertEquals("message", e.getMessage()); + } + } + + @Test + public void testDuplicateRegistration() { + try { + throw new DuplicateRegistrationException(); + } catch (DuplicateRegistrationException e) { + assertEquals("Email or username already belonging to another user", e.getMessage()); + } + } + + @Test + public void testDuplicateState() { + try { + throw new DuplicateStateException(); + } catch (DuplicateStateException e) { + assertEquals( + "Cannot create state since it has already been created for this scene and this device", + e.getMessage()); + } + } + + @Test + public void testEmailTokenNotFound() { + try { + throw new EmailTokenNotFoundException(); + } catch (EmailTokenNotFoundException e) { + assertEquals("Email verification token not found in DB", e.getMessage()); + } + } + + @Test + public void testNotFound() { + try { + throw new NotFoundException(); + } catch (NotFoundException e) { + assertEquals("Not found", e.getMessage()); + } + + try { + throw new NotFoundException("message"); + } catch (NotFoundException e) { + assertEquals("message not found", e.getMessage()); + } + } + + @Test + public void testUserNotFound() { + try { + throw new UserNotFoundException(); + } catch (UserNotFoundException e) { + assertEquals("No user found with given email", e.getMessage()); + } + } +} diff --git a/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/AutomationTests.java b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/AutomationTests.java new file mode 100644 index 0000000..f4d4019 --- /dev/null +++ b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/AutomationTests.java @@ -0,0 +1,87 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.models; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +@DisplayName("automation tests") +public class AutomationTests { + private Automation automation; + + @BeforeEach + private void createAutomation() { + this.automation = new Automation(); + } + + @Test + @DisplayName("test setName") + public void testSetName() { + automation.setName("Automation1"); + assertEquals("Automation1", automation.getName()); + } + + @Test + @DisplayName("test setId") + public void testSetId() { + automation.setId(34l); + assertEquals(34l, automation.getId()); + } + + @Test + @DisplayName("test setUserId") + public void testSetUserId() { + automation.setUserId(43l); + assertEquals(43l, automation.getUserId()); + } + + @Test + @DisplayName("test setUser") + public void testSetUser() { + User user = new User(); + automation.setUser(user); + user.setUsername("xXcoolNameXx"); + user.setId(42l); + user.setName("John RealName"); + assertTrue(user.equals(automation.getUser())); + } + + @Test + @DisplayName("test setTriggers") + public void testSetTrigger() { + BooleanTrigger trigger1 = new BooleanTrigger(); + BooleanTrigger trigger2 = new BooleanTrigger(); + automation.getTriggers().add(trigger1); + automation.getTriggers().add(trigger2); + assertEquals(2, automation.getTriggers().size()); + } + + @Test + @DisplayName("test setScene") + public void testSetScene() { + ScenePriority priority1 = new ScenePriority(); + priority1.setPriority(1); + ScenePriority priority2 = new ScenePriority(); + priority2.setPriority(2); + ScenePriority priority3 = new ScenePriority(); + priority3.setPriority(3); + automation.getScenes().add(priority1); + automation.getScenes().add(priority2); + automation.getScenes().add(priority3); + assertEquals(3, automation.getScenes().size()); + } + + @Test + @DisplayName("test setCondition") + public void testSetCondition() { + BooleanCondition bool = new BooleanCondition(); + RangeCondition range = new RangeCondition(); + bool.setOn(true); + range.setRange(10l); + automation.getConditions().add(bool); + automation.getConditions().add(range); + assertEquals(2, automation.getConditions().size()); + } +} diff --git a/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/BooleanConditionTests.java b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/BooleanConditionTests.java new file mode 100644 index 0000000..de37a8f --- /dev/null +++ b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/BooleanConditionTests.java @@ -0,0 +1,56 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.models; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +@DisplayName("BooleanCondition tests") +public class BooleanConditionTests { + private BooleanCondition condition; + + @BeforeEach + private void createCondition() { + condition = new BooleanCondition(); + } + + @Test + @DisplayName("test setOnFalse") + public void testSetOnFalse() { + condition.setOn(false); + assertFalse(condition.isOn()); + } + + @Test + @DisplayName("test setOnTrue") + public void testSetOnTrue() { + condition.setOn(true); + assertTrue(condition.isOn()); + } + + @Test + @DisplayName("test triggeredTrue") + public void testTriggeredTrue() { + Switch a = new Switch(); + a.setOn(true); + condition.setOn(true); + condition.setDevice(a); + assertTrue(condition.triggered()); + condition.setOn(false); + assertFalse(condition.triggered()); + } + + @Test + @DisplayName("test triggeredFalse") + public void testTriggeredFalse() { + Switch a = new Switch(); + a.setOn(false); + condition.setOn(false); + condition.setDevice(a); + assertTrue(condition.triggered()); + condition.setOn(true); + assertFalse(condition.triggered()); + } +} diff --git a/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/BooleanTriggerTests.java b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/BooleanTriggerTests.java new file mode 100644 index 0000000..9f4bfb0 --- /dev/null +++ b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/BooleanTriggerTests.java @@ -0,0 +1,56 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.models; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +@DisplayName("booleantrigger test") +public class BooleanTriggerTests { + private BooleanTrigger trigger; + + @BeforeEach + private void createTrigger() { + trigger = new BooleanTrigger(); + } + + @Test + @DisplayName("test setOnFalse") + public void testSetOnFalse() { + trigger.setOn(false); + assertFalse(trigger.isOn()); + } + + @Test + @DisplayName("test setOnTrue") + public void testSetOnTrue() { + trigger.setOn(true); + assertTrue(trigger.isOn()); + } + + @Test + @DisplayName("test triggeredTrue") + public void testTriggeredTrue() { + Switch a = new Switch(); + a.setOn(true); + trigger.setOn(true); + trigger.setDevice(a); + assertTrue(trigger.triggered()); + trigger.setOn(false); + assertFalse(trigger.triggered()); + } + + @Test + @DisplayName("test triggeredFalse") + public void testTriggeredFalse() { + Switch a = new Switch(); + a.setOn(false); + trigger.setOn(false); + trigger.setDevice(a); + assertTrue(trigger.triggered()); + trigger.setOn(true); + assertFalse(trigger.triggered()); + } +} diff --git a/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/ButtonDimmerTests.java b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/ButtonDimmerTests.java similarity index 74% rename from src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/ButtonDimmerTests.java rename to src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/ButtonDimmerTests.java index 45ad962..c5a9fc4 100644 --- a/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/ButtonDimmerTests.java +++ b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/ButtonDimmerTests.java @@ -1,10 +1,7 @@ -package ch.usi.inf.sa4.sanmarinoes.smarthut; +package ch.usi.inf.sa4.sanmarinoes.smarthut.models; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertTrue; -import ch.usi.inf.sa4.sanmarinoes.smarthut.models.ButtonDimmer; -import ch.usi.inf.sa4.sanmarinoes.smarthut.models.Dimmable; -import ch.usi.inf.sa4.sanmarinoes.smarthut.models.DimmableLight; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; @@ -39,7 +36,7 @@ public class ButtonDimmerTests { @DisplayName(" increase the intensity ") public void increase() { buttonDimmer.increaseIntensity(); - for (Dimmable dl : buttonDimmer.getOutputs()) { + for (Dimmable dl : buttonDimmer.getDimmables()) { assertTrue(dl.getIntensity() > 10); } } @@ -48,7 +45,7 @@ public class ButtonDimmerTests { @DisplayName(" decrease the intensity ") public void decrease() { buttonDimmer.decreaseIntensity(); - for (Dimmable dl : buttonDimmer.getOutputs()) { + for (Dimmable dl : buttonDimmer.getDimmables()) { assertTrue(dl.getIntensity() < 10); } } diff --git a/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/ConditionTests.java b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/ConditionTests.java new file mode 100644 index 0000000..be5c7ba --- /dev/null +++ b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/ConditionTests.java @@ -0,0 +1,37 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.models; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +@DisplayName("Condition tests") +public class ConditionTests { + private BooleanCondition condition; + + @BeforeEach + private void createCondition() { + this.condition = new BooleanCondition(); + } + + @Test + @DisplayName("test automationId") + public void testSetAutomationId() { + condition.setAutomationId(32l); + assertEquals(32l, condition.getAutomationId()); + } + + @Test + @DisplayName("test getKind") + public void testGetKind() { + assertEquals("booleanCondition", condition.getKind()); + } + + @Test + @DisplayName("test setId") + public void testSetId() { + condition.setId(11); + assertEquals(11, condition.getId()); + } +} diff --git a/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/ConfirmationTokenTests.java b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/ConfirmationTokenTests.java new file mode 100644 index 0000000..a46e35d --- /dev/null +++ b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/ConfirmationTokenTests.java @@ -0,0 +1,93 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.models; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.Date; +import nl.jqno.equalsverifier.EqualsVerifier; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +@DisplayName("ConfirmationToken tests") +public class ConfirmationTokenTests { + private ConfirmationToken token; + + @BeforeEach + private void createToke() { + User user = new User(); + user.setName("Tizio Sempronio"); + user.setId(42l); + user.setUsername("xXCoolUserNameXx"); + user.setEmail("realMail@service.ext"); + user.setPassword("alpaca"); + this.token = new ConfirmationToken(user); + } + + @Test + @DisplayName("test setId") + public void testSetId() { + this.token.setId(34l); + assertEquals(34l, token.getId()); + } + + @Test + @DisplayName("test getUSerId") + public void testGetUserId() { + assertEquals(42l, token.getUser().getId()); + } + + @Test + @DisplayName("test setResetPassword") + public void testSetResetPassword() { + assertFalse(token.isResetPassword()); + token.setResetPassword(true); + assertTrue(token.isResetPassword()); + } + + @Test + @DisplayName("test setTimeStamp") + public void testSetTimeStamp() { + Date date = new Date(); + date.setTime(86400000L); + token.setCreatedDate(date); + assertEquals(date, token.getCreatedDate()); + } + + @Test + @DisplayName("test setConfirmToken") + public void testSetConfirmTOken() { + assertNotEquals(null, token.getConfirmToken()); + String newToken = "newConfirmationToken"; + token.setConfirmToken(newToken); + assertEquals(newToken, token.getConfirmToken()); + } + + @Test + @DisplayName("test setConfirmToken") + public void equals() { + User user = new User(); + user.setName("Tizio Sempronio"); + user.setId(42L); + user.setUsername("xXCoolUserNameXx"); + user.setEmail("realMail@service.ext"); + user.setPassword("alpaca"); + ConfirmationToken t = new ConfirmationToken(); + t.setUser(user); + + assertNotEquals(t, token); + } + + @Test + public void testEqualsHashCode() { + final User u = new User(); + u.setId(1L); + final User t = new User(); + t.setId(2L); + EqualsVerifier.forClass(ConfirmationToken.class) + .withNonnullFields("createdDate") + .withNonnullFields("confirmToken") + .withPrefabValues(User.class, u, t) + .verify(); + assertDoesNotThrow(() -> new ConfirmationToken().hashCode()); + } +} diff --git a/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/CurtainsTests.java b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/CurtainsTests.java similarity index 91% rename from src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/CurtainsTests.java rename to src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/CurtainsTests.java index 06bd3e2..a0303cf 100644 --- a/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/CurtainsTests.java +++ b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/CurtainsTests.java @@ -1,8 +1,7 @@ -package ch.usi.inf.sa4.sanmarinoes.smarthut; +package ch.usi.inf.sa4.sanmarinoes.smarthut.models; import static org.junit.jupiter.api.Assertions.assertEquals; -import ch.usi.inf.sa4.sanmarinoes.smarthut.models.Curtains; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; diff --git a/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/DimmableLightTests.java b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/DimmableLightTests.java similarity index 96% rename from src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/DimmableLightTests.java rename to src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/DimmableLightTests.java index f54e754..830abbd 100644 --- a/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/DimmableLightTests.java +++ b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/DimmableLightTests.java @@ -1,8 +1,7 @@ -package ch.usi.inf.sa4.sanmarinoes.smarthut; +package ch.usi.inf.sa4.sanmarinoes.smarthut.models; import static org.junit.jupiter.api.Assertions.*; -import ch.usi.inf.sa4.sanmarinoes.smarthut.models.DimmableLight; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; diff --git a/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/DimmableStateTests.java b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/DimmableStateTests.java new file mode 100644 index 0000000..11ce6b2 --- /dev/null +++ b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/DimmableStateTests.java @@ -0,0 +1,45 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.models; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +@DisplayName("Dimmable State Tests") +public class DimmableStateTests { + + private DimmableState dimmableState; + + @BeforeEach + public void createDimmableState() { + dimmableState = new DimmableState(); + } + + @Test + @DisplayName("get and set intensity") + public void getAndSetIntensity() { + this.dimmableState.setIntensity(20); + assertEquals(20, this.dimmableState.getIntensity()); + } + + @Test + @DisplayName("apply") + public void apply() { + DimmableLight d = new DimmableLight(); + d.setIntensity(45); + this.dimmableState.setDevice(d); + this.dimmableState.setIntensity(30); + this.dimmableState.apply(); + assertEquals(30, d.getIntensity()); + } + + @Test + @DisplayName("apply") + public void copy() { + this.dimmableState.setIntensity(30); + + DimmableState copy = this.dimmableState.copy(); + assertEquals(30, copy.getIntensity()); + } +} diff --git a/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/DimmableTests.java b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/DimmableTests.java new file mode 100644 index 0000000..b720a3c --- /dev/null +++ b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/DimmableTests.java @@ -0,0 +1,81 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.models; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.HashSet; +import java.util.Set; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +@DisplayName("Dimmable tests") +public class DimmableTests { + private DimmableLight dimmable; + + @BeforeEach + private void createDimmable() { + dimmable = new DimmableLight(); + dimmable.setId(42l); + KnobDimmer dimmer1 = new KnobDimmer(); + dimmer1.setLightIntensity(75); + ButtonDimmer dimmer2 = new ButtonDimmer(); + dimmer2.setName("button"); + Set set = new HashSet(); + set.add(dimmer1); + set.add(dimmer2); + dimmable.setDimmers(set); + } + + @Test + @DisplayName("test getDimmers") + public void testGetDimmers() { + assertEquals(2, dimmable.getDimmers().size()); + } + + @Test + @DisplayName("test intensity and old Intensity") + public void testIntensities() { + assertEquals(0, dimmable.getIntensity()); + dimmable.setIntensity(13); + dimmable.setIntensity(24); + assertEquals(24, dimmable.getIntensity()); + assertEquals(24, dimmable.getOldIntensity()); + dimmable.setOldIntensity(99); + assertEquals(99, dimmable.getOldIntensity()); + } + + @Test + @DisplayName("test setOn") + public void testIsOn() { + assertFalse(dimmable.isOn()); + dimmable.setOn(true); + assertTrue(dimmable.isOn()); + assertNotEquals(0, dimmable.getIntensity()); + } + + @Test + @DisplayName("test readStateAndSet") + public void testReadStateAndSet() { + DimmableState state = new DimmableState(); + state.setIntensity(78); + dimmable.readStateAndSet(state); + assertEquals(78, dimmable.getIntensity()); + assertEquals(78, dimmable.getOldIntensity()); + } + + @Test + @DisplayName("test cloneState") + public void testCloneState() { + dimmable.setId(27); + DimmableState state = (DimmableState) dimmable.cloneState(); + assertEquals(state.getDeviceId(), dimmable.getId()); + assertEquals(state.getIntensity(), dimmable.getIntensity()); + } + + @Test + @DisplayName("test readTriggerState") + public void testReadTriggerState() { + dimmable.setIntensity(84); + assertEquals(84, dimmable.readTriggerState()); + } +} diff --git a/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/DimmerTests.java b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/DimmerTests.java new file mode 100644 index 0000000..5c473e6 --- /dev/null +++ b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/DimmerTests.java @@ -0,0 +1,44 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.models; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +@DisplayName("Dimmer Tests") +public class DimmerTests { + + private KnobDimmer dimmer; + + @BeforeEach + public void createDimmer() { + dimmer = new KnobDimmer(); + } + + @Test + @DisplayName("connect true") + public void connectTrue() { + DimmableLight d = new DimmableLight(); + dimmer.connect(d, true); + + assertTrue(d.getDimmers().contains((this.dimmer))); + + assertTrue((this.dimmer.getOutputs().contains(d))); + } + + @Test + @DisplayName("connect off") + public void connectOff() { + DimmableLight d = new DimmableLight(); + d.setId(35L); + d.getDimmers().add(this.dimmer); + dimmer.getDimmables().add(d); + dimmer.connect(d, false); + + assertFalse(d.getDimmers().contains((this.dimmer))); + + assertFalse(this.dimmer.getOutputs().contains(d)); + } +} diff --git a/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/InputDeviceTests.java b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/InputDeviceTests.java new file mode 100644 index 0000000..269a051 --- /dev/null +++ b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/InputDeviceTests.java @@ -0,0 +1,14 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.models; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +public class InputDeviceTests { + + @Test + public void test() { + final InputDevice motionSensor = new MotionSensor(); + assertThat(motionSensor.getOutputs()).isEmpty(); + } +} diff --git a/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/KnobDimmerTests.java b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/KnobDimmerTests.java similarity index 73% rename from src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/KnobDimmerTests.java rename to src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/KnobDimmerTests.java index 8eb7f74..d7b8b34 100644 --- a/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/KnobDimmerTests.java +++ b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/KnobDimmerTests.java @@ -1,10 +1,7 @@ -package ch.usi.inf.sa4.sanmarinoes.smarthut; +package ch.usi.inf.sa4.sanmarinoes.smarthut.models; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; -import ch.usi.inf.sa4.sanmarinoes.smarthut.models.Dimmable; -import ch.usi.inf.sa4.sanmarinoes.smarthut.models.DimmableLight; -import ch.usi.inf.sa4.sanmarinoes.smarthut.models.KnobDimmer; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; @@ -39,9 +36,15 @@ public class KnobDimmerTests { @DisplayName(" set the intensity ") public void increase() { knobDimmer.setLightIntensity(30); - for (Dimmable dl : knobDimmer.getOutputs()) { + for (Dimmable dl : knobDimmer.getDimmables()) { assertEquals(30, dl.getIntensity()); } } } + + @Test + public void testReadTriggerState() { + this.knobDimmer.setLightIntensity(1); + assertEquals(1, this.knobDimmer.readTriggerState()); + } } diff --git a/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/MotionSensorTests.java b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/MotionSensorTests.java new file mode 100644 index 0000000..60e09cb --- /dev/null +++ b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/MotionSensorTests.java @@ -0,0 +1,26 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.models; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +@DisplayName("Motion Sensor Tests") +public class MotionSensorTests { + + private MotionSensor motionSensor; + + @BeforeEach + public void createMotionSensor() { + motionSensor = new MotionSensor(); + } + + @Test + @DisplayName("set and get detected") + public void setAndGetDetected() { + this.motionSensor.setDetected(true); + assertTrue(this.motionSensor.isDetected()); + assertTrue(this.motionSensor.readTriggerState()); + } +} diff --git a/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/OperatorTests.java b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/OperatorTests.java new file mode 100644 index 0000000..197e36a --- /dev/null +++ b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/OperatorTests.java @@ -0,0 +1,21 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.models; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +public class OperatorTests { + @Test + public void testOperators() { + assertThat(Operator.EQUAL.checkAgainst(30, 30)).isTrue(); + assertThat(Operator.EQUAL.checkAgainst(20, 30)).isFalse(); + assertThat(Operator.LESS.checkAgainst(20, 30)).isTrue(); + assertThat(Operator.LESS.checkAgainst(40, 30)).isFalse(); + assertThat(Operator.LESS_EQUAL.checkAgainst(30, 30)).isTrue(); + assertThat(Operator.LESS_EQUAL.checkAgainst(40, 30)).isFalse(); + assertThat(Operator.GREATER.checkAgainst(40, 30)).isTrue(); + assertThat(Operator.GREATER.checkAgainst(30, 30)).isFalse(); + assertThat(Operator.GREATER_EQUAL.checkAgainst(30, 30)).isTrue(); + assertThat(Operator.GREATER_EQUAL.checkAgainst(20, 30)).isFalse(); + } +} diff --git a/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/RangeConditionTests.java b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/RangeConditionTests.java new file mode 100644 index 0000000..91b5bab --- /dev/null +++ b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/RangeConditionTests.java @@ -0,0 +1,58 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.models; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +@DisplayName("RAnge Condition Tests") +public class RangeConditionTests { + + private RangeCondition rangeCondition; + + @BeforeEach + public void creteRangeCondition() { + this.rangeCondition = new RangeCondition(); + } + + @Test + @DisplayName("set and get operator") + public void setAndGetOperator() { + rangeCondition.setOperator(Operator.EQUAL); + + assertEquals(Operator.EQUAL, rangeCondition.getOperator()); + } + + @Test + @DisplayName("set and get range") + public void setAndGetRange() { + rangeCondition.setRange(20.5); + + assertEquals(20.5, rangeCondition.getRange()); + } + + @Test + @DisplayName("triggered") + public void triggered() { + DimmableLight d = new DimmableLight(); + d.setIntensity(40); + rangeCondition.setDevice(d); + rangeCondition.setRange(45D); + + rangeCondition.setOperator(Operator.EQUAL); + assertFalse(rangeCondition.triggered()); + + rangeCondition.setOperator(Operator.LESS); + assertTrue(rangeCondition.triggered()); + + rangeCondition.setOperator(Operator.GREATER); + assertFalse(rangeCondition.triggered()); + + rangeCondition.setOperator(Operator.LESS_EQUAL); + assertTrue(rangeCondition.triggered()); + + rangeCondition.setOperator(Operator.GREATER_EQUAL); + assertFalse(rangeCondition.triggered()); + } +} diff --git a/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/RangeTriggerTests.java b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/RangeTriggerTests.java new file mode 100644 index 0000000..df38df5 --- /dev/null +++ b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/RangeTriggerTests.java @@ -0,0 +1,58 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.models; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +@DisplayName("Range Trigger Tests") +public class RangeTriggerTests { + + private RangeTrigger rangeTrigger; + + @BeforeEach + public void createRangeTrigger() { + this.rangeTrigger = new RangeTrigger(); + } + + @Test + @DisplayName("set and get operator") + public void setAndGetOperator() { + rangeTrigger.setOperator(Operator.EQUAL); + + assertEquals(Operator.EQUAL, rangeTrigger.getOperator()); + } + + @Test + @DisplayName("set and get range") + public void setAndGetRange() { + rangeTrigger.setRange(20.5); + + assertEquals(20.5, rangeTrigger.getRange()); + } + + @Test + @DisplayName("triggered") + public void triggered() { + DimmableLight d = new DimmableLight(); + d.setIntensity(40); + rangeTrigger.setDevice(d); + rangeTrigger.setRange(45D); + + rangeTrigger.setOperator(Operator.EQUAL); + assertFalse(rangeTrigger.triggered()); + + rangeTrigger.setOperator(Operator.LESS); + assertTrue(rangeTrigger.triggered()); + + rangeTrigger.setOperator(Operator.GREATER); + assertFalse(rangeTrigger.triggered()); + + rangeTrigger.setOperator(Operator.LESS_EQUAL); + assertTrue(rangeTrigger.triggered()); + + rangeTrigger.setOperator(Operator.GREATER_EQUAL); + assertFalse(rangeTrigger.triggered()); + } +} diff --git a/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/RegularLightTests.java b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/RegularLightTests.java similarity index 86% rename from src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/RegularLightTests.java rename to src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/RegularLightTests.java index c4f4694..9777cc8 100644 --- a/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/RegularLightTests.java +++ b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/RegularLightTests.java @@ -1,8 +1,8 @@ -package ch.usi.inf.sa4.sanmarinoes.smarthut; +package ch.usi.inf.sa4.sanmarinoes.smarthut.models; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; -import ch.usi.inf.sa4.sanmarinoes.smarthut.models.RegularLight; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; diff --git a/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/RoomTests.java b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/RoomTests.java new file mode 100644 index 0000000..1b0724d --- /dev/null +++ b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/RoomTests.java @@ -0,0 +1,77 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.models; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +@DisplayName("Room test") +public class RoomTests { + private Room room; + + @BeforeEach + private void createRoom() { + room = new Room(); + } + + @Test + @DisplayName("test id") + public void testId() { + room.setId(42l); + assertEquals(42l, room.getId()); + } + + @Test + @DisplayName("test userId") + public void testUserId() { + room.setUserId(42l); + assertEquals(42l, room.getUserId()); + } + + @Test + @DisplayName("test name") + public void testName() { + room.setName("alpaca"); + assertEquals("alpaca", room.getName()); + } + + @Test + @DisplayName("test image") + public void testImage() { + room.setImage("realFakeImage.png"); + assertEquals("realFakeImage.png", room.getImage()); + } + + @Test + @DisplayName("test toString()") + public void testToString() { + room.setId(1l); + room.setName("alpaca"); + assertEquals("Room{id=1, name='alpaca'}", room.toString()); + } + + @Test + @DisplayName("test devices") + public void testDevices() { + room.getDevices().add(new DimmableLight()); + assertEquals(1, room.getDevices().size()); + } + + @Test + @DisplayName("test user") + public void testUser() { + User user = new User(); + user.setId(34l); + room.setUser(user); + assertEquals(34l, room.getUser().getId()); + } + + @Test + @DisplayName("test Icon") + public void testIcon() { + // ImageIcon image = new ImageIcon("file","description"); + room.setIcon(Icon.FEMALE); + assertEquals("female", room.getIcon().toString()); + } +} diff --git a/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/ScenePriorityTests.java b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/ScenePriorityTests.java new file mode 100644 index 0000000..04e2131 --- /dev/null +++ b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/ScenePriorityTests.java @@ -0,0 +1,87 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.models; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +@DisplayName("Scene Priority Tests") +public class ScenePriorityTests { + + private ScenePriority scenePriority; + + @BeforeEach + public void scenePriorityCreate() { + this.scenePriority = new ScenePriority(); + } + + @Test + @DisplayName("get and set automation id") + public void getAndSetAutomationId() { + scenePriority.setAutomationId(20L); + + assertEquals(20, scenePriority.getAutomationId()); + } + + @Test + @DisplayName("get and set id") + public void getAndSetId() { + scenePriority.setId(20L); + + assertEquals(20, scenePriority.getId()); + } + + @Test + @DisplayName("get and set scene id") + public void getAndSetSceneId() { + scenePriority.setSceneId(20L); + + assertEquals(20, scenePriority.getSceneId()); + } + + @Test + @DisplayName("get and set priority") + public void getAndSetPriority() { + scenePriority.setPriority(20); + + assertEquals(20, scenePriority.getPriority()); + } + + @Test + @DisplayName("get and set automation") + public void getAndSetAutomation() { + Automation a = new Automation(); + scenePriority.setAutomation(a); + + assertEquals(a, scenePriority.getAutomation()); + } + + @Test + @DisplayName("get and set scene") + public void getAndSetScene() { + Scene s = new Scene(); + scenePriority.setScene(s); + + assertEquals(s, scenePriority.getScene()); + } + + @Test + @DisplayName("get and set scene") + public void testRemove() { + Scene s = new Scene(); + scenePriority.setScene(s); + scenePriority.setSceneId(20L); + + Automation a = new Automation(); + scenePriority.setAutomation(a); + scenePriority.setAutomationId(20L); + + scenePriority.preRemove(); + + assertNull(scenePriority.getAutomation()); + assertNull(scenePriority.getAutomationId()); + assertNull(scenePriority.getScene()); + assertNull(scenePriority.getSceneId()); + } +} diff --git a/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/SceneTests.java b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/SceneTests.java new file mode 100644 index 0000000..0e7dad9 --- /dev/null +++ b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/SceneTests.java @@ -0,0 +1,51 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.models; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +@DisplayName("Scene Tests") +public class SceneTests { + + private Scene scene; + + @BeforeEach + public void createScene() { + this.scene = new Scene(); + } + + @Test + @DisplayName("get and set id") + public void getAndSetId() { + scene.setId(20L); + + assertEquals(20, scene.getId()); + } + + @Test + @DisplayName("get and set user id") + public void getAndSetUserId() { + scene.setUserId(20L); + + assertEquals(20, scene.getUserId()); + } + + @Test + @DisplayName("get and set name") + public void getAndSetName() { + scene.setName("ciao mamma"); + + assertEquals("ciao mamma", scene.getName()); + } + + @Test + @DisplayName("get access enabled") + public void accessEnabled() { + scene.setGuestAccessEnabled(true); + + assertTrue(scene.isGuestAccessEnabled()); + } +} diff --git a/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/SecurityCameraTests.java b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/SecurityCameraTests.java new file mode 100644 index 0000000..b02900c --- /dev/null +++ b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/SecurityCameraTests.java @@ -0,0 +1,44 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.models; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +@DisplayName("Security Camera Tests") +public class SecurityCameraTests { + + private SecurityCamera securityCamera; + + @BeforeEach + public void createSecurityCamera() { + securityCamera = new SecurityCamera(); + } + + @Test + @DisplayName("get and set Path") + public void getAndSetPath() { + securityCamera.setPath("ciao mamma"); + + assertEquals("ciao mamma", securityCamera.getPath()); + } + + @Test + @DisplayName("get and set On") + public void getAndSetOn() { + securityCamera.setOn(true); + + assertTrue(securityCamera.isOn()); + } + + @Test + @DisplayName("trigger state") + public void triggerState() { + + securityCamera.setOn(true); + + assertTrue(securityCamera.readTriggerState()); + } +} diff --git a/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/SensorTests.java b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/SensorTests.java new file mode 100644 index 0000000..b747a9a --- /dev/null +++ b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/SensorTests.java @@ -0,0 +1,61 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.models; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.Sensor.SensorType; +import java.math.BigDecimal; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +@DisplayName("Sensor Tests") +public class SensorTests { + + Sensor sensor; + + @BeforeEach + public void createSensor() { + this.sensor = new Sensor(); + } + + @Test + @DisplayName("get and set sensor") + public void getAndSetSensor() { + sensor.setSensor(SensorType.LIGHT); + + assertEquals(SensorType.LIGHT, sensor.getSensor()); + } + + @Test + @DisplayName("get and set err") + public void getAndSetError() { + sensor.setError(BigDecimal.valueOf(10)); + + assertEquals(BigDecimal.valueOf(10), sensor.getError()); + } + + @Test + @DisplayName("get and set typical") + public void getAndSetTypical() { + sensor.setTypical(BigDecimal.valueOf(10)); + + assertEquals(BigDecimal.valueOf(10), sensor.getTypical()); + } + + @Test + @DisplayName("get and set value") + public void getAndSetValue() { + sensor.setValue(new BigDecimal(40)); + + assertEquals(40.0, sensor.readTriggerState()); + } + + @Test + @DisplayName("to String") + public void toStringTest() { + sensor.setValue(new BigDecimal(40)); + sensor.setSensor(SensorType.LIGHT); + + assertEquals("Sensor{value=40, sensor=LIGHT}", sensor.toString()); + } +} diff --git a/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/SmartPlugTests.java b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/SmartPlugTests.java new file mode 100644 index 0000000..c8fe22b --- /dev/null +++ b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/SmartPlugTests.java @@ -0,0 +1,43 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.models; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.math.BigDecimal; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +@DisplayName("SmartPlug Tests") +public class SmartPlugTests { + + private SmartPlug smartPlug; + + @BeforeEach + public void createSmartPlug() { + this.smartPlug = new SmartPlug(); + } + + @Test + @DisplayName("set and get on") + public void testOn() { + smartPlug.setOn(true); + + assertTrue(smartPlug.isOn()); + } + + @Test + @DisplayName("read trigger state") + public void readTriggerState() { + smartPlug.setOn(true); + + assertTrue(smartPlug.readTriggerState()); + } + + @Test + @DisplayName("reset total consumption") + public void reset() { + + assertEquals(new BigDecimal(0), smartPlug.getTotalConsumption()); + } +} diff --git a/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/StateTests.java b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/StateTests.java new file mode 100644 index 0000000..fa88ef2 --- /dev/null +++ b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/StateTests.java @@ -0,0 +1,89 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.models; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +@DisplayName("Curtains tests") +public class StateTests { + + private DimmableState state; + + @BeforeEach + public void createState() { + this.state = new DimmableState(); + } + + @Test + @DisplayName("get and set device") + public void getAndSetDevice() { + DimmableLight d = new DimmableLight(); + this.state.setDevice(d); + assertEquals(d, this.state.getDevice()); + } + + @Test + @DisplayName("get and set device id") + public void getAndSetDeviceId() { + this.state.setDeviceId(30L); + assertEquals(30, this.state.getDeviceId()); + } + + @Test + @DisplayName("get and set scene") + public void getAndSetScene() { + Scene s = new Scene(); + this.state.setScene(s); + assertEquals(s, this.state.getScene()); + } + + @Test + @DisplayName("get and set sceneId") + public void getAndSetSceneId() { + this.state.setSceneId(50L); + assertEquals(50, this.state.getSceneId()); + } + + @Test + @DisplayName("copy to scene id") + public void copyToSceneId() { + this.state.setSceneId(50L); + Scene s = new Scene(); + this.state.setScene(s); + + this.state.setDeviceId(30L); + DimmableLight d = new DimmableLight(); + this.state.setDevice(d); + + State stat = this.state.copyToSceneId(10L); + + assertEquals(10, stat.getSceneId()); + assertEquals(30, stat.getDeviceId()); + + assertEquals(s, stat.getScene()); + assertEquals(d, stat.getDevice()); + } + + @Test + @DisplayName("preremove") + public void preRemove() { + this.state.setSceneId(50L); + Scene s = new Scene(); + this.state.setScene(s); + + this.state.setDeviceId(30L); + DimmableLight d = new DimmableLight(); + this.state.setDevice(d); + + state.removeDeviceAndScene(); + + assertNull(state.getSceneId()); + assertNull(state.getDeviceId()); + + assertNull(state.getScene()); + assertNull(state.getDevice()); + } +} diff --git a/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/SwitchTests.java b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/SwitchTests.java similarity index 71% rename from src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/SwitchTests.java rename to src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/SwitchTests.java index 9f937d3..46fd574 100644 --- a/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/SwitchTests.java +++ b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/SwitchTests.java @@ -1,8 +1,9 @@ -package ch.usi.inf.sa4.sanmarinoes.smarthut; +package ch.usi.inf.sa4.sanmarinoes.smarthut.models; -import static org.junit.jupiter.api.Assertions.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; -import ch.usi.inf.sa4.sanmarinoes.smarthut.models.*; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -10,7 +11,8 @@ import org.junit.jupiter.api.Test; @DisplayName("A switch") public class SwitchTests { - Switch aSwitch; + private Switch aSwitch; + private SmartPlug smartPlug; @BeforeEach public void createNewSwitch() { @@ -18,10 +20,11 @@ public class SwitchTests { this.aSwitch = new Switch(); RegularLight regularLight = new RegularLight(); DimmableLight dimmableLight = new DimmableLight(); - SmartPlug smartPlug = new SmartPlug(); - this.aSwitch.getOutputs().add(regularLight); - this.aSwitch.getOutputs().add(dimmableLight); - this.aSwitch.getOutputs().add(smartPlug); + smartPlug = new SmartPlug(); + this.aSwitch.getSwitchables().add(regularLight); + this.aSwitch.getSwitchables().add(dimmableLight); + this.aSwitch.connect(smartPlug, true); + assertThat(this.aSwitch.getOutputs()).containsAll(this.aSwitch.getSwitchables()); } @Test @@ -79,7 +82,7 @@ public class SwitchTests { @DisplayName("Checks that toggling on sets all elements of the Set on as well") public void toggleEffctOnSet() { aSwitch.toggle(); - for (final Switchable s : aSwitch.getOutputs()) { + for (final Switchable s : aSwitch.getSwitchables()) { assertTrue(s.isOn()); } } @@ -89,8 +92,14 @@ public class SwitchTests { public void toggleOffEffectOnElementes() { aSwitch.setOn(true); aSwitch.toggle(); - for (final Switchable s : aSwitch.getOutputs()) { + for (final Switchable s : aSwitch.getSwitchables()) { assertFalse(s.isOn()); } } + + @Test + public void testDisconnect() { + this.aSwitch.connect(smartPlug, false); + assertFalse(this.aSwitch.getOutputs().contains(smartPlug)); + } } diff --git a/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/SwitchableStateTests.java b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/SwitchableStateTests.java new file mode 100644 index 0000000..7ad5998 --- /dev/null +++ b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/SwitchableStateTests.java @@ -0,0 +1,32 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.models; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +@DisplayName("Switchable State Tests") +public class SwitchableStateTests { + + private SwitchableState switchableState; + + @BeforeEach + public void createSwitchableState() { + switchableState = new SwitchableState(); + switchableState.setDevice(new RegularLight()); + } + + @Test + @DisplayName("is on") + public void isOn() { + switchableState.setOn(true); + + assertTrue(switchableState.isOn()); + + switchableState.apply(); + assertTrue(((Switchable) switchableState.getDevice()).isOn()); + + assertTrue(switchableState.copy().isOn()); + } +} diff --git a/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/ThermostatConditionTests.java b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/ThermostatConditionTests.java new file mode 100644 index 0000000..182867f --- /dev/null +++ b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/ThermostatConditionTests.java @@ -0,0 +1,59 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.models; + +import static org.junit.jupiter.api.Assertions.*; + +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.Thermostat.Mode; +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.ThermostatCondition.Operator; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +@DisplayName("ThermostatCondition Tests") +public class ThermostatConditionTests { + + private ThermostatCondition thermostatCondition; + + @BeforeEach + public void createThermostatCondtion() { + this.thermostatCondition = new ThermostatCondition(); + } + + @Test + @DisplayName("get and set mode") + public void getAndSetMode() { + thermostatCondition.setMode(Thermostat.Mode.IDLE); + + assertEquals(Thermostat.Mode.IDLE, thermostatCondition.getMode()); + } + + @Test + @DisplayName("get and set operator") + public void getAndSeOperator() { + thermostatCondition.setOperator(Operator.EQUAL); + + assertEquals(Operator.EQUAL, thermostatCondition.getOperator()); + } + + @Test + @DisplayName("get and set operator") + public void triggered() { + thermostatCondition.setMode(Thermostat.Mode.IDLE); + thermostatCondition.setOperator(Operator.EQUAL); + Thermostat t = new Thermostat(); + t.setMode(Mode.IDLE); + thermostatCondition.setDevice(t); + + assertTrue(thermostatCondition.triggered()); + + thermostatCondition.setOperator(Operator.NOTEQUAL); + assertFalse(thermostatCondition.triggered()); + + t.setMode(Mode.COOLING); + + thermostatCondition.setOperator(Operator.EQUAL); + assertFalse(thermostatCondition.triggered()); + + thermostatCondition.setOperator(Operator.NOTEQUAL); + assertTrue(thermostatCondition.triggered()); + } +} diff --git a/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/ThermostatTests.java b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/ThermostatTests.java new file mode 100644 index 0000000..30a23ea --- /dev/null +++ b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/ThermostatTests.java @@ -0,0 +1,127 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.models; + +import static org.junit.jupiter.api.Assertions.*; + +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.Thermostat.Mode; +import java.math.BigDecimal; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +@DisplayName("Thermostat tests") +public class ThermostatTests { + private Thermostat thermostat; + + @BeforeEach + public void createThermostat() { + this.thermostat = new Thermostat(); + } + + @Test + @DisplayName("test isOn()") + public void inOnTest() { + assertFalse(thermostat.isOn()); + } + + @Test + @DisplayName("test setOn(true) ") + public void setOnTestTrue() { + thermostat.setOn(true); + assertTrue(thermostat.isOn()); + } + + @Test + @DisplayName("test setOn(false) ") + public void setOnTestFalse() { + thermostat.setOn(false); + assertFalse(thermostat.isOn()); + } + + @Test + @DisplayName("test compute measured is null") + public void computeMeasureNull() { + this.thermostat.setMeasuredTemperature(null); + thermostat.setOn(true); + assertEquals(Thermostat.Mode.IDLE, thermostat.getMode()); + } + + @Test + @DisplayName("test compute |measured-target|<0.25") + public void computeMeasureIdle() { + this.thermostat.setMeasuredTemperature(new BigDecimal(1)); + this.thermostat.setTargetTemperature(new BigDecimal(0.9)); + thermostat.setOn(true); + assertEquals(Thermostat.Mode.IDLE, thermostat.getMode()); + } + + @Test + @DisplayName("test compute heating") + public void computeMeasureHeating() { + this.thermostat.setMeasuredTemperature(new BigDecimal(1)); + this.thermostat.setTargetTemperature(new BigDecimal(2)); + thermostat.setOn(true); + assertEquals(Thermostat.Mode.HEATING, thermostat.getMode()); + } + + @Test + @DisplayName("test compute cooling") + public void computeMeasureCooling() { + this.thermostat.setMeasuredTemperature(new BigDecimal(10)); + this.thermostat.setTargetTemperature(new BigDecimal(5)); + thermostat.setOn(true); + assertEquals(Thermostat.Mode.COOLING, thermostat.getMode()); + } + + @Test + @DisplayName("test external sensor") + public void testExternalSensor() { + thermostat.setUseExternalSensors(true); + assertTrue(thermostat.isUseExternalSensors()); + } + + @Test + @DisplayName("test internal sensor temperature") + public void testInternalSensorTemperature() { + thermostat.setInternalSensorTemperature(new BigDecimal(42)); + assertEquals(new BigDecimal(42), thermostat.getInternalSensorTemperature()); + } + + @Test + @DisplayName("test err ") + public void errTest() { + thermostat.setErr(BigDecimal.valueOf(10)); + assertEquals(BigDecimal.valueOf(10), thermostat.getErr()); + } + + @Test + @DisplayName("test typical ") + public void typicalTest() { + thermostat.setTypical(BigDecimal.valueOf(10)); + assertEquals(BigDecimal.valueOf(10), thermostat.getTypical()); + } + + @Test + @DisplayName("test triggerState") + public void testTriggerState() { + assertFalse(thermostat.readTriggerState()); + } + + @Test + @DisplayName("test triggerState") + public void testTriggerStateTrue() { + thermostat.setMode(Mode.COOLING); + + assertTrue(thermostat.readTriggerState()); + } + + // for obvious reasons I am not gonna check all the possible combinations of toString() + @Test + @DisplayName("test to string") + public void testToString() { + thermostat.setMeasuredTemperature(new BigDecimal(1)); + thermostat.setTargetTemperature(new BigDecimal(1)); + assertEquals( + "Thermostat{targetTemperature=1, internalSensorTemperature=17.0, mode=OFF, measuredTemperature=1, useExternalSensors=false}", + thermostat.toString()); + } +} diff --git a/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/TriggerTests.java b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/TriggerTests.java new file mode 100644 index 0000000..adef575 --- /dev/null +++ b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/TriggerTests.java @@ -0,0 +1,69 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.models; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +@DisplayName("Trigger Tests") +public class TriggerTests { + + private BooleanTrigger booleanTrigger; + + @BeforeEach + public void createBooleanTrigger() { + booleanTrigger = new BooleanTrigger(); + } + + @Test + @DisplayName("get Kind") + public void getKind() { + assertEquals("booleanTrigger", booleanTrigger.getKind()); + } + + @Test + @DisplayName("get and set id") + public void getAndSetId() { + booleanTrigger.setId(20); + assertEquals(20, booleanTrigger.getId()); + } + + @Test + @DisplayName("get and set device") + public void getAndSetDevice() { + RegularLight r = new RegularLight(); + booleanTrigger.setDevice(r); + assertEquals(r, booleanTrigger.getDevice()); + } + + @Test + @DisplayName("get and set device id") + public void getAndSetDeviceId() { + booleanTrigger.setDeviceId(20L); + assertEquals(20, booleanTrigger.getDeviceId()); + } + + @Test + @DisplayName("get and set automation") + public void getAndSetAutomation() { + Automation r = new Automation(); + booleanTrigger.setAutomation(r); + assertEquals(r, booleanTrigger.getAutomation()); + } + + @Test + @DisplayName("get and set automation id") + public void getAndSetAutomationId() { + booleanTrigger.setAutomationId(20L); + assertEquals(20, booleanTrigger.getAutomationId()); + } + + @Test + public void testRemove() { + booleanTrigger.setDeviceId(30L); + booleanTrigger.removeDeviceAndScene(); + assertNull(booleanTrigger.getDeviceId()); + } +} diff --git a/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/UserTests.java b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/UserTests.java new file mode 100644 index 0000000..c93cd9d --- /dev/null +++ b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/UserTests.java @@ -0,0 +1,82 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.models; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +@DisplayName(" USer Tests") +public class UserTests { + + private User user; + + @BeforeEach + public void createUser() { + user = new User(); + } + + @Test + @DisplayName("get and set id") + public void getAndSetId() { + user.setId(20L); + + assertEquals(20, user.getId()); + } + + @Test + @DisplayName("get and set id") + public void getAndSetName() { + user.setName("Paolo Bitta"); + + assertEquals("Paolo Bitta", user.getName()); + } + + @Test + @DisplayName("get and set id") + public void getAndSetUsername() { + user.setUsername("PaulB"); + + assertEquals("PaulB", user.getUsername()); + } + + @Test + @DisplayName("get and set email") + public void getAndSetEmail() { + user.setEmail("paolo.bitta@gmail.com"); + + assertEquals("paolo.bitta@gmail.com", user.getEmail()); + } + + @Test + @DisplayName("get and set password") + public void getAndSetPassword() { + user.setPassword("cameraCaffe"); + + assertEquals("cameraCaffe", user.getPassword()); + } + + @Test + @DisplayName("get and set enabled") + public void getAndSetEnabled() { + user.setEnabled(true); + + assertTrue(user.isEnabled()); + } + + @Test + @DisplayName("get and set Cameraenabled") + public void getAndSeCameraEnabled() { + user.setCameraEnabled(true); + + assertTrue(user.isCameraEnabled()); + } + + @Test + @DisplayName("equals") + public void eq() { + assertNotEquals(null, user); + + assertEquals(user, user); + } +} diff --git a/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/scheduled/UpdateTasksTests.java b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/scheduled/UpdateTasksTests.java new file mode 100644 index 0000000..bf511c3 --- /dev/null +++ b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/scheduled/UpdateTasksTests.java @@ -0,0 +1,39 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.scheduled; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.mockito.Mockito.*; + +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.SmartPlug; +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.SmartPlugRepository; +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.User; +import ch.usi.inf.sa4.sanmarinoes.smarthut.socket.SensorSocketEndpoint; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +public class UpdateTasksTests { + + @InjectMocks private UpdateTasks updateTasks; + + @Mock private SmartPlugRepository smartPlugRepository; + + @Mock private SensorSocketEndpoint sensorSocketEndpoint; + + @Test + public void testSmartPlugConsumptionFakeUpdate() { + final User u = new User(); + final SmartPlug s = new SmartPlug(); + s.setId(20L); + doReturn(u).when(smartPlugRepository).findUser(20L); + doNothing() + .when(smartPlugRepository) + .updateTotalConsumption(SmartPlug.AVERAGE_CONSUMPTION_KW); + when(smartPlugRepository.findByOn(true)).thenReturn(List.of(s)); + doNothing().when(sensorSocketEndpoint).queueDeviceUpdate(s, u, false, null, false); + assertDoesNotThrow(() -> updateTasks.smartPlugConsumptionFakeUpdate()); + } +} diff --git a/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/service/AutomationServiceTests.java b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/service/AutomationServiceTests.java new file mode 100644 index 0000000..6d1850f --- /dev/null +++ b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/service/AutomationServiceTests.java @@ -0,0 +1,74 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.service; + +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.*; +import java.util.ArrayList; +import java.util.Optional; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.security.test.context.support.WithMockUser; + +@ExtendWith(MockitoExtension.class) +@WithMockUser(username = "user") +@DisplayName("AutomationService test") +public class AutomationServiceTests { + @InjectMocks private AutomationService service; + + @Mock private TriggerRepository repository; + + @Mock private AutomationRepository automationRepository; + + @Mock private ConditionRepository conditionRepository; + + @Test + public void testFindTriggerByDeviceId() { + RangeTrigger trigger1 = new RangeTrigger(); + RangeTrigger trigger2 = new RangeTrigger(); + RangeTrigger trigger3 = new RangeTrigger(); + trigger1.setDeviceId(0L); + trigger2.setDeviceId(0L); + trigger3.setDeviceId(1L); + ArrayList list = new ArrayList<>(); + list.add(trigger1); + list.add(trigger1); + ArrayList> toPut = new ArrayList<>(); + when(repository.findAllByDeviceId(0L)).thenReturn(list); + service.findTriggersByDeviceId(0L, toPut); + assertThat(toPut.contains(trigger1)); + assertThat(toPut.contains(trigger2)); + } + + @Test + public void testFindByVerifiedId() { + Automation automation = new Automation(); + automation.setId(0L); + when(automationRepository.findById(eq(0L))).thenReturn(Optional.of(automation)); + Automation returned = service.findByVerifiedId(0L); + assertThat(returned).isSameAs(automation); + } + + @Test + public void testFindAllConditionByAutomationId() { + RangeCondition condition1 = new RangeCondition(); + RangeCondition condition2 = new RangeCondition(); + RangeCondition condition3 = new RangeCondition(); + condition1.setAutomationId(1L); + condition2.setAutomationId(0L); + condition3.setAutomationId(0L); + ArrayList list = new ArrayList<>(); + list.add(condition2); + list.add(condition3); + ArrayList> toPut = new ArrayList<>(); + when(conditionRepository.findAllByAutomationId(0L)).thenReturn(list); + service.findAllConditionsByAutomationId(0L, toPut); + assertThat(toPut).contains(condition2); + assertThat(toPut).contains(condition3); + } +} diff --git a/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/service/DevicePopulationServiceTests.java b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/service/DevicePopulationServiceTests.java new file mode 100644 index 0000000..3aa5d9c --- /dev/null +++ b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/service/DevicePopulationServiceTests.java @@ -0,0 +1,44 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.service; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.mockito.Mockito.*; + +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.*; +import java.math.BigDecimal; +import java.util.ArrayList; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.security.test.context.support.WithMockUser; + +@ExtendWith(MockitoExtension.class) +@WithMockUser(username = "user") +@DisplayName("DevicePopulationService test") +public class DevicePopulationServiceTests { + + @InjectMocks private DevicePopulationService service; + + @Mock private ThermostatPopulationService populationService; + + private void helper(Thermostat t) { + t.setMeasuredTemperature(new BigDecimal(1)); + } + + @Test + public void testPopulateComputedFields() { + Curtains curtains = new Curtains(); + RegularLight light = new RegularLight(); + Thermostat t1 = new Thermostat(); + RegularLight light2 = new RegularLight(); + ArrayList list = new ArrayList<>(); + list.add(curtains); + list.add(light); + list.add(t1); + list.add(light2); + doNothing().when(populationService).populateMeasuredTemperature(t1); + assertDoesNotThrow(() -> service.populateComputedFields(list)); + } +} diff --git a/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/service/DevicePropagationServiceTests.java b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/service/DevicePropagationServiceTests.java new file mode 100644 index 0000000..a0d126d --- /dev/null +++ b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/service/DevicePropagationServiceTests.java @@ -0,0 +1,221 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.*; + +import ch.usi.inf.sa4.sanmarinoes.smarthut.error.NotFoundException; +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.*; +import ch.usi.inf.sa4.sanmarinoes.smarthut.socket.SensorSocketEndpoint; +import java.util.List; +import java.util.Optional; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +public class DevicePropagationServiceTests { + @InjectMocks private DevicePropagationService devicePropagationService; + + @Mock private SensorSocketEndpoint endpoint; + + @Mock private DeviceRepository deviceRepository; + + @Mock private EagerUserRepository userRepository; + + private User host; + private User guest; + + private void initHostGuest() { + host = new User(); + host.setName("host"); + host.setUsername("host"); + host.setId(1L); + + guest = new User(); + guest.setName("guest"); + guest.setUsername("guest"); + guest.setId(2L); + + guest.getHosts().add(host); + host.getGuests().add(guest); + } + + @Test + public void testPropagateUpdateAsGuest() { + Device toPropagate = new SecurityCamera(); + + User host = new User(); + host.setName("host"); + host.setId(1L); + + User guest = new User(); + guest.setName("guest"); + guest.setId(2L); + + User guest2 = new User(); + guest.setName("guest2"); + guest.setId(3L); + + guest.getHosts().add(host); + host.getGuests().add(guest); + guest2.getHosts().add(host); + host.getGuests().add(guest2); + + doNothing().when(endpoint).queueDeviceUpdate(toPropagate, host, true, null, false); + + boolean[] done = new boolean[1]; + + doAnswer(i -> done[0] = true) + .when(endpoint) + .queueDeviceUpdate(toPropagate, guest2, false, host.getId(), false); + + devicePropagationService.propagateUpdateAsGuest(toPropagate, host, guest); + assertThat(done[0]).isTrue(); + } + + @Test + public void saveAllAsGuestSceneApplication() { + initHostGuest(); + when(userRepository.findById(1L)).thenReturn(Optional.of(host)); + when(userRepository.findByUsername("guest")).thenReturn(guest); + + int[] done = new int[1]; + + final DevicePropagationService devicePropagationService1 = + Mockito.spy(devicePropagationService); + doAnswer(i -> done[0]++) + .when(devicePropagationService1) + .propagateUpdateAsGuest(any(), eq(host), eq(guest)); + when(deviceRepository.saveAll(any())).thenAnswer(i -> i.getArguments()[0]); + + devicePropagationService1.saveAllAsGuestSceneApplication( + List.of(new SecurityCamera(), new ButtonDimmer()), "guest", 1L); + + assertThat(done[0]).isEqualTo(2); + } + + @Test + public void testRenameIfDuplicate() { + when(deviceRepository.findDuplicates("Device", "user")).thenReturn(2); + when(deviceRepository.findDuplicates("Device (new)", "user")).thenReturn(1); + when(deviceRepository.findDuplicates("New Device", "user")).thenReturn(1); + when(deviceRepository.findDuplicates("New Device (new)", "user")).thenReturn(0); + + Device d = new RegularLight(); + d.setName("Device"); + d.setId(42L); + + devicePropagationService.renameIfDuplicate(d, "user"); + + assertThat(d.getName()).isEqualTo("Device (new)"); + + d.setName("New Device"); + d.setId(0L); + + devicePropagationService.renameIfDuplicate(d, "user"); + + assertThat(d.getName()).isEqualTo("New Device (new)"); + } + + @Test + public void testSaveAllAsOwner() { + final DevicePropagationService dps = Mockito.spy(devicePropagationService); + final Device d = new RegularLight(); + final List dl = List.of(d); + doNothing().when(dps).renameIfDuplicate(d, "user"); + when(deviceRepository.saveAll(dl)).thenReturn(dl); + doNothing().when(dps).propagateUpdateAsOwner(d, "user", false); + doNothing().when(dps).propagateUpdateAsOwner(d, "user", true); + + assertThat(dps.saveAllAsOwner(dl, "user")).containsExactly(d); + assertThat(dps.saveAllAsOwner(dl, "user", true, true)).containsExactly(d); + assertThat(dps.saveAllAsOwner(dl, "user", true, false)).containsExactly(d); + assertThat(dps.saveAllAsOwner(dl, "user", false, true)).containsExactly(d); + } + + @Test + public void testSaveAllAsGuest() { + final DevicePropagationService dps = Mockito.spy(devicePropagationService); + + initHostGuest(); + when(userRepository.findById(1L)).thenReturn(Optional.of(host)); + when(userRepository.findByUsername("guest")).thenReturn(guest); + when(userRepository.findById(42L)).thenReturn(Optional.empty()); + + final User phonyGuest = new User(); + phonyGuest.setName("phonyguest"); + + when(userRepository.findByUsername("phonyguest")).thenReturn(phonyGuest); + + final Device d = new ButtonDimmer(); + + doNothing().when(dps).renameIfDuplicate(d, "host"); + + assertThatThrownBy(() -> dps.saveAsGuest(d, "phonyguest", 1L)) + .isInstanceOf(NotFoundException.class); + assertThatThrownBy(() -> dps.saveAsGuest(d, "guest", 42L)) + .isInstanceOf(NotFoundException.class); + + when(deviceRepository.save(d)).thenReturn(d); + doNothing().when(dps).propagateUpdateAsGuest(d, host, guest); + + Assertions.assertDoesNotThrow(() -> dps.saveAsGuest(d, "guest", 1L)); + } + + @Test + public void testSaveAsOwner() { + final DevicePropagationService dps = Mockito.spy(devicePropagationService); + final Device d = new ButtonDimmer(); + doNothing().when(dps).renameIfDuplicate(d, "user"); + doNothing().when(dps).propagateUpdateAsOwner(d, "user", false); + when(deviceRepository.save(d)).thenReturn(d); + assertThat(dps.saveAsOwner(d, "user")).isSameAs(d); + } + + @Test + public void testDeleteByIdAsOwner() { + initHostGuest(); + + final Device d = new ButtonDimmer(); + + boolean[] done = new boolean[1]; + when(userRepository.findByUsername("host")).thenReturn(host); + when(deviceRepository.findByIdAndUsername(42L, "host")).thenReturn(Optional.of(d)); + when(deviceRepository.findByIdAndUsername(43L, "host")).thenReturn(Optional.empty()); + doAnswer(i -> done[0] = true).when(deviceRepository).delete(d); + doNothing().when(endpoint).queueDeviceUpdate(d, guest, false, host.getId(), true); + + assertThatThrownBy(() -> devicePropagationService.deleteByIdAsOwner(43L, "host")) + .isInstanceOf(NotFoundException.class); + Assertions.assertDoesNotThrow( + () -> devicePropagationService.deleteByIdAsOwner(42L, "host")); + + assertThat(done[0]).isTrue(); + } + + @Test + public void testPropagateUpdateAsOwner() { + initHostGuest(); + when(userRepository.findByUsername("host")).thenReturn(host); + + final Device d = new ButtonDimmer(); + + int[] counter = new int[1]; + + doAnswer(i -> counter[0]++) + .when(endpoint) + .queueDeviceUpdate(d, guest, false, host.getId(), false); + doAnswer(i -> counter[0]++).when(endpoint).queueDeviceUpdate(d, host, false, null, false); + + devicePropagationService.propagateUpdateAsOwner(d, "host", false); + assertThat(counter[0]).isEqualTo(1); + counter[0] = 0; + devicePropagationService.propagateUpdateAsOwner(d, "host", true); + assertThat(counter[0]).isEqualTo(2); + } +} diff --git a/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/service/DeviceServiceTests.java b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/service/DeviceServiceTests.java new file mode 100644 index 0000000..f357ed3 --- /dev/null +++ b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/service/DeviceServiceTests.java @@ -0,0 +1,243 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.service; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import ch.usi.inf.sa4.sanmarinoes.smarthut.error.NotFoundException; +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.*; +import java.util.List; +import java.util.Optional; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +public class DeviceServiceTests { + + @Mock private DeviceRepository deviceRepository; + + @Mock private SceneService sceneService; + + @Mock private RoomRepository roomRepository; + + @Mock private AutomationService automationService; + + @Mock private EagerUserRepository userRepository; + + @Mock private DevicePopulationService devicePopulationService; + + @Mock private DevicePropagationService devicePropagationService; + + @InjectMocks private DeviceService deviceService; + + @Test + public void testThrowIfRoomNotOwned() { + final Room r = new Room(); + r.setId(1L); + + when(roomRepository.findByIdAndUsername(1L, "user")).thenReturn(Optional.of(r)); + when(roomRepository.findByIdAndUsername(2L, "user")).thenReturn(Optional.empty()); + + try { + deviceService.throwIfRoomNotOwned(1L, "user"); + } catch (NotFoundException e) { + fail(e.getMessage()); + } + + assertThatThrownBy(() -> deviceService.throwIfRoomNotOwned(2L, "user")) + .isInstanceOf(NotFoundException.class); + + r.setId(2L); + + assertThatThrownBy(() -> deviceService.throwIfRoomNotOwned(1L, "user")) + .isInstanceOf(IllegalStateException.class); + } + + @Test + public void triggerTriggers() { + final RegularLight r = new RegularLight(); + r.setId(1L); + r.setOn(true); + + final BooleanTrigger b = new BooleanTrigger(); + b.setId(2L); + b.setDevice(r); + b.setOn(true); + b.setDeviceId(r.getId()); + + final BooleanCondition c = new BooleanCondition(); + c.setId(3L); + c.setDevice(r); + c.setOn(true); + c.setDeviceId(r.getId()); + + final Scene s = new Scene(); + s.setId(4L); + + final Automation a = new Automation(); + a.setId(5L); + + final ScenePriority sp = new ScenePriority(); + sp.setSceneId(s.getId()); + sp.setScene(s); + sp.setAutomationId(a.getId()); + sp.setAutomation(a); + + a.getTriggers().add(b); + b.setAutomation(a); + b.setAutomationId(a.getId()); + + a.getConditions().add(c); + c.setAutomation(a); + c.setAutomationId(a.getId()); + + a.getScenes().add(sp); + + doAnswer(i -> ((List>) i.getArgument(1)).add(b)) + .when(automationService) + .findTriggersByDeviceId(eq(1L), any()); + doAnswer(i -> ((List>) i.getArgument(1)).add(c)) + .when(automationService) + .findAllConditionsByAutomationId(eq(5L), any()); + when(automationService.findByVerifiedId(5L)).thenReturn(a); + when(sceneService.findByValidatedId(4L)).thenReturn(s); + + final boolean[] passed = new boolean[1]; + when(sceneService.apply(s, "user", true)) + .thenAnswer( + invocation -> { + passed[0] = true; + return null; + }); + + deviceService.triggerTriggers(r, "user"); + + assertThat(passed[0]).isTrue(); + } + + @Test + public void testSaveAsGuest() throws NotFoundException { + Thermostat t = new Thermostat(); + + User u = new User(); + u.setUsername("user"); + + User host = new User(); + host.setId(1L); + host.setUsername("host"); + host.getGuests().add(u); + u.getHosts().add(host); + + when(userRepository.findByUsername("user")).thenReturn(u); + when(userRepository.findById(1L)).thenReturn(Optional.of(host)); + doNothing().when(devicePropagationService).renameIfDuplicate(t, "host"); + when(deviceRepository.save(t)).thenAnswer(ta -> ta.getArguments()[0]); + + assertThat(deviceService.saveAsGuest(t, "user", 1L)).isEqualTo(t); + } + + @Test + public void testDeleteIdAsOwner() { + try { + doNothing().when(devicePropagationService).deleteByIdAsOwner(1L, "user"); + deviceService.deleteByIdAsOwner(1L, "user"); + } catch (NotFoundException e) { + fail(e.getMessage()); + } + } + + @Test + public void populateComputedFields() { + doNothing().when(devicePopulationService).populateComputedFields(List.of()); + Assertions.assertDoesNotThrow(() -> deviceService.populateComputedFields(List.of())); + } + + @Test + public void testSaveAllAsOwner() { + final DeviceService currentDeviceService = Mockito.spy(deviceService); + List devices = List.of(new RegularLight(), new ButtonDimmer()); + when(devicePropagationService.saveAllAsOwner( + eq(devices), eq("user"), anyBoolean(), eq(false))) + .thenReturn(devices); + + final int[] count = new int[1]; + + doAnswer(i -> count[0]++) + .when(currentDeviceService) + .triggerTriggers(any(Device.class), eq("user")); + + currentDeviceService.saveAllAsOwner(devices, "user", true, false); + assertThat(count[0]).isEqualTo(0); + + currentDeviceService.saveAllAsOwner(devices, "user"); + assertThat(count[0]).isEqualTo(2); + } + + @Test + public void testSaveAsOwner() { + final DeviceService currentDeviceService = Mockito.spy(deviceService); + Device device = new ButtonDimmer(); + + final boolean[] count = new boolean[1]; + + doAnswer(i -> count[0] = true).when(currentDeviceService).triggerTriggers(device, "user"); + when(devicePropagationService.saveAsOwner(device, "user")).thenReturn(device); + + assertThat(currentDeviceService.saveAsOwner(device, "user")).isEqualTo(device); + assertThat(count[0]).isTrue(); + } + + @Test + public void testFindAll() throws NotFoundException { + final DeviceService currentDeviceService = Mockito.spy(deviceService); + final SecurityCamera gerryScotti = new SecurityCamera(); + doNothing().when(currentDeviceService).throwIfRoomNotOwned(1L, "user"); + doNothing().when(devicePopulationService).populateComputedFields(any()); + + when(deviceRepository.findByRoomId(1L)).thenReturn(List.of(gerryScotti)); + when(deviceRepository.findAllByUsername("user")).thenReturn(List.of(gerryScotti)); + + final User user = new User(); + user.setUsername("user"); + user.setEmail("user@example.com"); + user.setName("User"); + user.setId(1L); + user.setCameraEnabled(true); + + final User guest = new User(); + guest.setUsername("guest"); + guest.setEmail("guest@example.com"); + guest.setName("Guest"); + guest.setId(2L); + guest.getHosts().add(user); + user.getGuests().add(guest); + + when(userRepository.findByUsername("guest")).thenReturn(guest); + when(userRepository.findById(1L)).thenReturn(Optional.of(user)); + when(userRepository.findById(3L)).thenReturn(Optional.empty()); + + final Room r = new Room(); + r.setUserId(1L); + when(roomRepository.findById(1L)).thenReturn(Optional.of(r)); + when(roomRepository.findById(3L)).thenReturn(Optional.empty()); + + assertThat(currentDeviceService.findAll(1L, null, "user")).containsExactly(gerryScotti); + assertThat(currentDeviceService.findAll(null, "user")).containsExactly(gerryScotti); + + assertThatThrownBy(() -> currentDeviceService.findAll(1L, 3L, "guest")) + .isInstanceOf(NotFoundException.class); + assertThatThrownBy(() -> currentDeviceService.findAll(3L, 1L, "guest")) + .isInstanceOf(NotFoundException.class); + + assertThat(currentDeviceService.findAll(1L, 1L, "guest")).containsExactly(gerryScotti); + + user.setCameraEnabled(false); + + assertThat(currentDeviceService.findAll(1L, 1L, "guest")).isEmpty(); + } +} diff --git a/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/service/JWTUserDetailsServiceTests.java b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/service/JWTUserDetailsServiceTests.java new file mode 100644 index 0000000..684c241 --- /dev/null +++ b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/service/JWTUserDetailsServiceTests.java @@ -0,0 +1,34 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.when; + +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.User; +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.UserRepository; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.security.core.userdetails.UsernameNotFoundException; + +@ExtendWith({MockitoExtension.class}) +public class JWTUserDetailsServiceTests { + + @InjectMocks private JWTUserDetailsService jwtUserDetailsService; + + @Mock private UserRepository userRepository; + + @Test + public void testLoadByUsername() { + final User u = new User(); + u.setUsername("username"); + u.setPassword("password"); + when(userRepository.findByUsername("username")).thenReturn(u); + assertThatThrownBy(() -> jwtUserDetailsService.loadUserByUsername("username")) + .isInstanceOf(UsernameNotFoundException.class); + u.setEnabled(true); + assertThat(jwtUserDetailsService.loadUserByUsername("username")).isNotNull(); + } +} diff --git a/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/service/MotionSensorServiceTests.java b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/service/MotionSensorServiceTests.java new file mode 100644 index 0000000..12c5451 --- /dev/null +++ b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/service/MotionSensorServiceTests.java @@ -0,0 +1,40 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.*; + +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.*; +import ch.usi.inf.sa4.sanmarinoes.smarthut.socket.SensorSocketEndpoint; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.security.test.context.support.WithMockUser; + +@ExtendWith(MockitoExtension.class) +@WithMockUser(username = "user") +@DisplayName("MotionSensorService test") +public class MotionSensorServiceTests { + @InjectMocks private MotionSensorService service; + + @Mock private SensorSocketEndpoint sensorSocketEndpoint; + @Mock private DeviceService deviceService; + @Mock private MotionSensorRepository motionSensorRepository; + + @Test + public void testUpdateDetectionFromMotionSensor() { + MotionSensor sensor = new MotionSensor(); + sensor.setId(1L); + User user = new User(); + user.setId(1L); + when(deviceService.saveAsOwner(sensor, "user")).thenReturn(sensor); + when(motionSensorRepository.findUser(sensor.getId())).thenReturn(user); + doNothing().when(sensorSocketEndpoint).queueDeviceUpdate(sensor, user, false, null, false); + MotionSensor returned = service.updateDetectionFromMotionSensor(sensor, true, "user"); + assertThat(returned).isEqualTo(sensor); + assertTrue(returned.isDetected()); + } +} diff --git a/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/service/SceneServiceTests.java b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/service/SceneServiceTests.java new file mode 100644 index 0000000..2925b5a --- /dev/null +++ b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/service/SceneServiceTests.java @@ -0,0 +1,79 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.when; + +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.*; +import java.util.List; +import java.util.Optional; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.security.test.context.support.WithMockUser; + +@ExtendWith(MockitoExtension.class) +@WithMockUser(username = "user") +public class SceneServiceTests { + + @InjectMocks private SceneService sceneService; + + @Mock private SceneRepository sceneRepository; + + @Mock private StateRepository stateRepository; + + @Mock private DevicePropagationService deviceService; + + @Mock private DevicePopulationService devicePopulationService; + + @Test + public void testFindByValidatedId() { + final Scene s = new Scene(); + when(sceneRepository.findById(1L)).thenReturn(Optional.of(s)); + assertThat(sceneService.findByValidatedId(1L)).isSameAs(s); + } + + @Test + public void testApply() { + when(deviceService.saveAllAsOwner(any(), eq("user"), eq(true), anyBoolean())) + .thenAnswer(i -> i.getArgument(0)); + doNothing().when(deviceService).saveAllAsGuestSceneApplication(any(), eq("user"), eq(42L)); + doNothing().when(devicePopulationService).populateComputedFields(any()); + + final Scene s = new Scene(); + final DimmableState st = new DimmableState(); + final DimmableLight d = new DimmableLight(); + d.setIntensity(20); + st.setDevice(d); + st.setIntensity(40); + s.getStates().add(st); + + assertThat(sceneService.apply(s, "user", false)).containsExactly(d); + assertThat(d.getIntensity()).isEqualTo(40); + + d.setIntensity(20); + + assertThat(sceneService.applyAsGuest(s, "user", 42L)).containsExactly(d); + assertThat(d.getIntensity()).isEqualTo(40); + } + + @Test + public void testCopyStates() { + + final State state = new DimmableState(); + + final Scene sceneFrom = new Scene(); + sceneFrom.getStates().add(state); + final Scene sceneTo = new Scene(); + + when(stateRepository.save(any(State.class))).thenReturn(state); + + List s = sceneService.copyStates(sceneTo, sceneFrom); + + assertEquals(s.get(0), state); + } +} diff --git a/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/service/SensorServiceTests.java b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/service/SensorServiceTests.java new file mode 100644 index 0000000..adfd511 --- /dev/null +++ b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/service/SensorServiceTests.java @@ -0,0 +1,66 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.service; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.mockito.Mockito.*; + +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.Sensor; +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.SensorRepository; +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.User; +import ch.usi.inf.sa4.sanmarinoes.smarthut.socket.SensorSocketEndpoint; +import java.math.BigDecimal; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.security.test.context.support.WithMockUser; + +@ExtendWith(MockitoExtension.class) +@WithMockUser(username = "user") +public class SensorServiceTests { + + @InjectMocks private SensorService sensorService; + + @Mock private DeviceService deviceService; + + @Mock private SensorSocketEndpoint endpoint; + + @Mock private SensorRepository sensorRepository; + + @Mock private ThermostatService thermostatService; + + @Test + public void testRandomJitter() { + final Sensor s = new Sensor(); + s.setTypical(BigDecimal.ZERO); + s.setError(BigDecimal.ONE); + s.setId(42L); + final User u = new User(); + u.setUsername("user"); + doNothing().when(thermostatService).updateStates(); + when(deviceService.saveAsOwner(s, "user")).thenReturn(s); + when(sensorRepository.findUser(42L)).thenReturn(u); + when(sensorRepository.findAll()).thenReturn(List.of(s)); + doNothing().when(endpoint).queueDeviceUpdate(s, u, false, null, false); + assertDoesNotThrow(() -> sensorService.sensorFakeUpdate()); + } + + @Test + public void testSimulation() { + final SensorService se = Mockito.spy(sensorService); + final Sensor s = new Sensor(); + s.setError(BigDecimal.ONE); + s.setTypical(BigDecimal.ZERO); + doReturn(s).when(se).update(s); + se.updateSimulationFromSensor(s, null, null); + assertThat(s.getError()).isEqualTo(BigDecimal.ONE); + assertThat(s.getTypical()).isEqualTo(BigDecimal.ZERO); + + se.updateSimulationFromSensor(s, BigDecimal.ZERO, BigDecimal.ONE); + assertThat(s.getError()).isEqualTo(BigDecimal.ZERO); + assertThat(s.getTypical()).isEqualTo(BigDecimal.ONE); + } +} diff --git a/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/service/ThermostatPopulationServiceTests.java b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/service/ThermostatPopulationServiceTests.java new file mode 100644 index 0000000..a48b59b --- /dev/null +++ b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/service/ThermostatPopulationServiceTests.java @@ -0,0 +1,46 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.service; + +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; +import static org.mockito.Mockito.when; + +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.*; +import java.math.BigDecimal; +import java.util.Optional; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.security.test.context.support.WithMockUser; + +@ExtendWith(MockitoExtension.class) +@WithMockUser(username = "user") +@DisplayName("ThermostatPopulationService test") +public class ThermostatPopulationServiceTests { + @InjectMocks private ThermostatPopulationService service; + + @Mock ThermostatRepository repository; + + @Test + public void testPopulateMeasuredTemperatureIf() { + Thermostat thermostat = new Thermostat(); + thermostat.setRoomId(0L); + thermostat.setUseExternalSensors(true); + when(repository.getAverageTemperature( + thermostat.getRoomId(), Sensor.SensorType.TEMPERATURE)) + .thenReturn(Optional.of(new BigDecimal(17))); + service.populateMeasuredTemperature(thermostat); + assertThat(thermostat.getMeasuredTemperature()).isEqualTo(new BigDecimal(17)); + } + + @Test + public void testPopulateMeasuredTemperatureElse() { + Thermostat thermostat = new Thermostat(); + thermostat.setRoomId(0L); + thermostat.setUseExternalSensors(false); + thermostat.setInternalSensorTemperature(new BigDecimal(19)); + service.populateMeasuredTemperature(thermostat); + assertThat(thermostat.getMeasuredTemperature()).isEqualTo(new BigDecimal(19)); + } +} diff --git a/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/service/ThermostatServiceTests.java b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/service/ThermostatServiceTests.java new file mode 100644 index 0000000..978b157 --- /dev/null +++ b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/service/ThermostatServiceTests.java @@ -0,0 +1,77 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.Thermostat; +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.ThermostatRepository; +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.User; +import ch.usi.inf.sa4.sanmarinoes.smarthut.socket.SensorSocketEndpoint; +import java.math.BigDecimal; +import java.util.List; +import java.util.Optional; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +public class ThermostatServiceTests { + + @InjectMocks private ThermostatService thermostatService; + + @Mock private ThermostatRepository thermostatRepository; + + @Mock private DeviceService deviceService; + + @Mock private ThermostatPopulationService thermostatPopulationService; + + @Mock private SensorSocketEndpoint endpoint; + + @Test + public void testFakeUpdateAll() { + when(deviceService.saveAsOwner(any(), any())).thenAnswer(i -> i.getArgument(0)); + when(thermostatRepository.findUser(1L)).thenReturn(new User()); + + Thermostat t = new Thermostat(); + t.setOn(true); + t.setId(1L); + t.setTargetTemperature(BigDecimal.valueOf(17.0)); + + when(thermostatRepository.findAll()).thenReturn(List.of(t)); + doAnswer( + i -> { + ((Thermostat) i.getArgument(0)) + .setMeasuredTemperature(BigDecimal.valueOf(18.0)); + return null; + }) + .when(thermostatPopulationService) + .populateMeasuredTemperature(t); + + doNothing().when(endpoint).queueDeviceUpdate(any(), any(), eq(false), eq(null), eq(false)); + + Assertions.assertDoesNotThrow(() -> thermostatService.fakeUpdateAll()); + assertThat(t.getMode()).isNotEqualTo(Thermostat.Mode.OFF); + } + + @Test + public void testFindAll() { + final Thermostat t = new Thermostat(); + doNothing().when(thermostatPopulationService).populateMeasuredTemperature(t); + when(thermostatRepository.findAllByUsername("user")).thenReturn(List.of(t)); + assertThat(thermostatService.findAll("user")).containsExactly(t); + } + + @Test + public void testFindById() { + final Thermostat t = new Thermostat(); + when(thermostatRepository.findByIdAndUsername(2L, "user")).thenReturn(Optional.empty()); + when(thermostatRepository.findByIdAndUsername(1L, "user")).thenReturn(Optional.of(t)); + doNothing().when(thermostatPopulationService).populateMeasuredTemperature(t); + assertThat(thermostatService.findById(2L, "user")).isEmpty(); + assertThat(thermostatService.findById(1L, "user")).isNotEmpty(); + } +} diff --git a/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/socket/SensorSocketEndpointTests.java b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/socket/SensorSocketEndpointTests.java new file mode 100644 index 0000000..9104034 --- /dev/null +++ b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/socket/SensorSocketEndpointTests.java @@ -0,0 +1,133 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.socket; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; +import static org.springframework.test.util.ReflectionTestUtils.getField; + +import ch.usi.inf.sa4.sanmarinoes.smarthut.config.GsonConfig; +import ch.usi.inf.sa4.sanmarinoes.smarthut.config.JWTTokenUtils; +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.ButtonDimmer; +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.Device; +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.User; +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.UserRepository; +import ch.usi.inf.sa4.sanmarinoes.smarthut.service.DevicePopulationService; +import com.google.common.collect.Multimap; +import com.google.gson.Gson; +import java.io.IOException; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import javax.websocket.RemoteEndpoint; +import javax.websocket.Session; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +public class SensorSocketEndpointTests { + + @InjectMocks private SensorSocketEndpoint sensorSocketEndpoint; + + private final Gson gson = GsonConfig.socketGson(); + + @Mock private DevicePopulationService deviceService; + + @Mock private Session session; + + @Mock private JWTTokenUtils jwtTokenUtils; + + @Mock private UserRepository userRepository; + + private final User u; + + public SensorSocketEndpointTests() { + u = new User(); + u.setName("user"); + u.setId(1L); + } + + @Test + public void testQueueDeviceUpdate() { + doNothing().when(deviceService).populateComputedFields(any()); + Device d = new ButtonDimmer(); + + User u = new User(); + u.setId(1L); + + sensorSocketEndpoint.queueDeviceUpdate(d, u, true, 42L, true); + assertThat(d.isFromGuest()).isTrue(); + assertThat(d.getFromHostId()).isEqualTo(42L); + assertThat(d.isDeleted()).isTrue(); + + final boolean[] done = new boolean[1]; + + final SensorSocketEndpoint endpoint = Mockito.spy(sensorSocketEndpoint); + + doAnswer( + i -> { + if (done[0]) fail("Broadcast called more than once"); + final User us = (User) i.getArguments()[0]; + @SuppressWarnings("unchecked") + final Collection jsons = + (Collection) i.getArguments()[1]; + assertThat(us).isSameAs(u); + assertThat(jsons).containsExactly(gson.toJson(d)); + done[0] = true; + return null; + }) + .when(endpoint) + .broadcast(eq(u), any()); + + endpoint.flushDeviceUpdates(); + + assertThat(done[0]).isTrue(); + } + + @Test + public void testCheckToken() throws IOException { + when(userRepository.findByUsername("user")).thenReturn(u); + when(session.getRequestParameterMap()) + .thenReturn(Map.of("token", List.of("randomgarbage"))); + when(jwtTokenUtils.getUsernameFromToken("randomgarbage")).thenReturn("user"); + when(jwtTokenUtils.isTokenExpired("randomgarbage")).thenReturn(false); + sensorSocketEndpoint.onOpen(session, null); + @SuppressWarnings("unchecked") + Multimap map = + (Multimap) + getField( + sensorSocketEndpoint, + SensorSocketEndpoint.class, + "authorizedClients"); + assertThat(map).isNotNull(); + assertThat(map.size()).isEqualTo(1); + assertThat(map.entries().iterator().next().getKey()).isSameAs(u); + assertThat(map.entries().iterator().next().getValue()).isSameAs(session); + final Session closedSession = Mockito.mock(Session.class); + when(closedSession.isOpen()).thenReturn(false); + when(session.isOpen()).thenReturn(true); + map.put(u, closedSession); + + boolean[] sent = new boolean[1]; + final RemoteEndpoint.Basic b = Mockito.mock(RemoteEndpoint.Basic.class); + when(session.getBasicRemote()).thenReturn(b); + + doAnswer( + i -> { + sent[0] = true; + return null; + }) + .when(b) + .sendText("[\"Malusa\",\"Luciano\"]"); + + sensorSocketEndpoint.broadcast(null, List.of()); + sensorSocketEndpoint.broadcast(u, List.of("\"Malusa\"", "\"Luciano\"")); + assertThat(sent[0]).isTrue(); + assertThat(map.get(u)).containsExactly(session); + } +} diff --git a/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/utils/UtilsTests.java b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/utils/UtilsTests.java new file mode 100644 index 0000000..e30eed6 --- /dev/null +++ b/src/test/java/ch/usi/inf/sa4/sanmarinoes/smarthut/utils/UtilsTests.java @@ -0,0 +1,57 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.utils; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.when; + +import ch.usi.inf.sa4.sanmarinoes.smarthut.error.NotFoundException; +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.User; +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.UserRepository; +import java.security.Principal; +import java.util.List; +import java.util.Optional; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +public class UtilsTests { + + @Test + public void testToList() { + List hormannTitles = List.of("Prof.", "Dr.", "Kai (spiritual leader)"); + assertThat(Utils.toList(hormannTitles)) + .containsExactly(hormannTitles.get(0), hormannTitles.get(1), hormannTitles.get(2)); + } + + @Test + public void testReturnIfGuest() { + Principal principal = Mockito.mock(Principal.class); + UserRepository userRepository = Mockito.mock(UserRepository.class); + User host = new User(); + User guest = new User(); + host.getGuests().add(guest); + + when(userRepository.findById(1L)).thenReturn(Optional.of(host)); + when(userRepository.findById(2L)).thenReturn(Optional.empty()); + when(userRepository.findByUsername("user")).thenReturn(guest); + when(principal.getName()).thenReturn("user"); + + try { + assertThat(Utils.returnIfGuest(userRepository, "toReturn", 1L, principal)) + .isEqualTo("toReturn"); + } catch (NotFoundException e) { + fail(e.getMessage()); + } + + assertThatThrownBy(() -> Utils.returnIfGuest(userRepository, "toReturn", 2L, principal)) + .isInstanceOf(NotFoundException.class); + + host.getGuests().clear(); + + assertThatThrownBy( + () -> + assertThat( + Utils.returnIfGuest( + userRepository, "toReturn", 1L, principal)) + .isEqualTo("toReturn")) + .isInstanceOf(NotFoundException.class); + } +} diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties index bdaafc0..1db382b 100644 --- a/src/test/resources/application.properties +++ b/src/test/resources/application.properties @@ -34,4 +34,5 @@ email.registrationRedirect=http://localhost:3000 email.resetpasswordSubject=SmartHut.sm password reset email.resetpassword=To reset your password, please click here: email.resetpasswordPath=http://localhost:3000/password-reset?token= -email.resetPasswordRedirect=http://localhost:3000/conf-reset-pass \ No newline at end of file +email.resetPasswordRedirect=http://localhost:3000/conf-reset-pass +camera.videoUrl="/security_camera_videos/security_camera_1.mp4" \ No newline at end of file