diff --git a/build.gradle b/build.gradle
index f48ae69..b5a3b83 100644
--- a/build.gradle
+++ b/build.gradle
@@ -53,18 +53,19 @@ dependencies {
implementation("io.micronaut.kotlin:micronaut-kotlin-extension-functions")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation "nu.studer:gradle-jooq-plugin:$jooqPluginVersion"
+ implementation("org.postgresql:postgresql:${postgresVersion}")
detektPlugins "io.gitlab.arturbosch.detekt:detekt-formatting:$detektVersion"
runtimeOnly("ch.qos.logback:logback-classic")
runtimeOnly("ch.qos.logback.contrib:logback-json-classic:${logbackJsonVersion}")
runtimeOnly("ch.qos.logback.contrib:logback-jackson:${logbackJsonVersion}")
runtimeOnly("net.logstash.logback:logstash-logback-encoder:${logstashEncoderVersion}")
runtimeOnly("org.codehaus.janino:janino:${janinoVersion}")
- runtimeOnly("org.postgresql:postgresql:${postgresVersion}")
kaptTest(platform("io.micronaut:micronaut-bom:$micronautVersion"))
kaptTest("io.micronaut:micronaut-inject-java")
testImplementation(platform("io.micronaut:micronaut-bom:$micronautVersion"))
testImplementation("io.micronaut.test:micronaut-test-kotest")
testImplementation("io.kotest:kotest-runner-junit5-jvm")
+ testImplementation("io.kotest:kotest-assertions-core:${kotestVersion}")
testImplementation("io.mockk:mockk:${mockkVersion}")
testImplementation("org.testcontainers:postgresql:${testContainersVersion}")
jooqRuntime("org.postgresql:postgresql:${postgresVersion}")
diff --git a/codecov.yml b/codecov.yml
new file mode 100644
index 0000000..fceb076
--- /dev/null
+++ b/codecov.yml
@@ -0,0 +1,10 @@
+coverage:
+ status:
+ project:
+ default:
+ target: 90%
+ threshold: 0%
+ patch:
+ default:
+ target: 80%
+ threshold: 0%
diff --git a/gradle.properties b/gradle.properties
index 515c0b7..7e77065 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -2,7 +2,7 @@ micronautVersion=2.0.1
kotlinVersion=1.3.72
detektVersion=1.10.0
jooqPluginVersion=4.2
-jibVersion=2.4.0
+jibVersion=2.5.0
shadowVersion=6.0.0
arrowDataVersion=0.10.5
kotlinCoroutinesVersion=1.3.8
@@ -13,3 +13,4 @@ palantirGitVersion=0.12.3
logbackJsonVersion=0.1.5
logstashEncoderVersion=6.4
janinoVersion=3.1.2
+kotestVersion=4.0.3
diff --git a/src/jooq/java/com/kuvaszuptime/kuvasz/DefaultCatalog.java b/src/jooq/java/com/kuvaszuptime/kuvasz/DefaultCatalog.java
index f6d7e10..e282655 100644
--- a/src/jooq/java/com/kuvaszuptime/kuvasz/DefaultCatalog.java
+++ b/src/jooq/java/com/kuvaszuptime/kuvasz/DefaultCatalog.java
@@ -17,7 +17,7 @@
@SuppressWarnings({ "all", "unchecked", "rawtypes" })
public class DefaultCatalog extends CatalogImpl {
- private static final long serialVersionUID = 232955134;
+ private static final long serialVersionUID = 1946704898;
/**
* The reference instance of DEFAULT_CATALOG
diff --git a/src/jooq/java/com/kuvaszuptime/kuvasz/DefaultSchema.java b/src/jooq/java/com/kuvaszuptime/kuvasz/DefaultSchema.java
index 8d511d0..085edee 100644
--- a/src/jooq/java/com/kuvaszuptime/kuvasz/DefaultSchema.java
+++ b/src/jooq/java/com/kuvaszuptime/kuvasz/DefaultSchema.java
@@ -23,7 +23,7 @@
@SuppressWarnings({ "all", "unchecked", "rawtypes" })
public class DefaultSchema extends SchemaImpl {
- private static final long serialVersionUID = 255525230;
+ private static final long serialVersionUID = 63509414;
/**
* The reference instance of DEFAULT_SCHEMA
diff --git a/src/jooq/java/com/kuvaszuptime/kuvasz/Keys.java b/src/jooq/java/com/kuvaszuptime/kuvasz/Keys.java
index cbc28fd..0aa7a8e 100644
--- a/src/jooq/java/com/kuvaszuptime/kuvasz/Keys.java
+++ b/src/jooq/java/com/kuvaszuptime/kuvasz/Keys.java
@@ -19,7 +19,7 @@
/**
- * A class modelling foreign key relationships and constraints of tables of
+ * A class modelling foreign key relationships and constraints of tables of
* the
schema.
*/
@SuppressWarnings({ "all", "unchecked", "rawtypes" })
@@ -39,6 +39,7 @@ public class Keys {
public static final UniqueKey LATENCY_LOG_PKEY = UniqueKeys0.LATENCY_LOG_PKEY;
public static final UniqueKey MONITOR_PKEY = UniqueKeys0.MONITOR_PKEY;
+ public static final UniqueKey UNIQUE_MONITOR_NAME = UniqueKeys0.UNIQUE_MONITOR_NAME;
public static final UniqueKey UPTIME_EVENT_PKEY = UniqueKeys0.UPTIME_EVENT_PKEY;
public static final UniqueKey UPTIME_EVENT_KEY = UniqueKeys0.UPTIME_EVENT_KEY;
@@ -62,6 +63,7 @@ private static class Identities0 {
private static class UniqueKeys0 {
public static final UniqueKey LATENCY_LOG_PKEY = Internal.createUniqueKey(LatencyLog.LATENCY_LOG, "latency_log_pkey", new TableField[] { LatencyLog.LATENCY_LOG.ID }, true);
public static final UniqueKey MONITOR_PKEY = Internal.createUniqueKey(Monitor.MONITOR, "monitor_pkey", new TableField[] { Monitor.MONITOR.ID }, true);
+ public static final UniqueKey UNIQUE_MONITOR_NAME = Internal.createUniqueKey(Monitor.MONITOR, "unique_monitor_name", new TableField[] { Monitor.MONITOR.NAME }, true);
public static final UniqueKey UPTIME_EVENT_PKEY = Internal.createUniqueKey(UptimeEvent.UPTIME_EVENT, "uptime_event_pkey", new TableField[] { UptimeEvent.UPTIME_EVENT.ID }, true);
public static final UniqueKey UPTIME_EVENT_KEY = Internal.createUniqueKey(UptimeEvent.UPTIME_EVENT, "uptime_event_key", new TableField[] { UptimeEvent.UPTIME_EVENT.MONITOR_ID, UptimeEvent.UPTIME_EVENT.STATUS, UptimeEvent.UPTIME_EVENT.ENDED_AT }, true);
}
diff --git a/src/jooq/java/com/kuvaszuptime/kuvasz/Sequences.java b/src/jooq/java/com/kuvaszuptime/kuvasz/Sequences.java
index 6c673fd..45f0432 100644
--- a/src/jooq/java/com/kuvaszuptime/kuvasz/Sequences.java
+++ b/src/jooq/java/com/kuvaszuptime/kuvasz/Sequences.java
@@ -9,7 +9,7 @@
/**
- * Convenience access to all sequences in
+ * Convenience access to all sequences in
*/
@SuppressWarnings({ "all", "unchecked", "rawtypes" })
public class Sequences {
diff --git a/src/jooq/java/com/kuvaszuptime/kuvasz/Tables.java b/src/jooq/java/com/kuvaszuptime/kuvasz/Tables.java
index e5a87ab..6b112f5 100644
--- a/src/jooq/java/com/kuvaszuptime/kuvasz/Tables.java
+++ b/src/jooq/java/com/kuvaszuptime/kuvasz/Tables.java
@@ -10,7 +10,7 @@
/**
- * Convenience access to all tables in
+ * Convenience access to all tables in
*/
@SuppressWarnings({ "all", "unchecked", "rawtypes" })
public class Tables {
diff --git a/src/jooq/java/com/kuvaszuptime/kuvasz/tables/LatencyLog.java b/src/jooq/java/com/kuvaszuptime/kuvasz/tables/LatencyLog.java
index c09d304..24758ad 100644
--- a/src/jooq/java/com/kuvaszuptime/kuvasz/tables/LatencyLog.java
+++ b/src/jooq/java/com/kuvaszuptime/kuvasz/tables/LatencyLog.java
@@ -35,7 +35,7 @@
@SuppressWarnings({ "all", "unchecked", "rawtypes" })
public class LatencyLog extends TableImpl {
- private static final long serialVersionUID = 2080318128;
+ private static final long serialVersionUID = 2052986796;
/**
* The reference instance of latency_log
diff --git a/src/jooq/java/com/kuvaszuptime/kuvasz/tables/Monitor.java b/src/jooq/java/com/kuvaszuptime/kuvasz/tables/Monitor.java
index 22c25ca..7adfad5 100644
--- a/src/jooq/java/com/kuvaszuptime/kuvasz/tables/Monitor.java
+++ b/src/jooq/java/com/kuvaszuptime/kuvasz/tables/Monitor.java
@@ -33,7 +33,7 @@
@SuppressWarnings({ "all", "unchecked", "rawtypes" })
public class Monitor extends TableImpl {
- private static final long serialVersionUID = -1852813365;
+ private static final long serialVersionUID = -1808562877;
/**
* The reference instance of monitor
@@ -133,7 +133,7 @@ public UniqueKey getPrimaryKey() {
@Override
public List> getKeys() {
- return Arrays.>asList(Keys.MONITOR_PKEY);
+ return Arrays.>asList(Keys.MONITOR_PKEY, Keys.UNIQUE_MONITOR_NAME);
}
@Override
diff --git a/src/jooq/java/com/kuvaszuptime/kuvasz/tables/UptimeEvent.java b/src/jooq/java/com/kuvaszuptime/kuvasz/tables/UptimeEvent.java
index 1318fb3..decf274 100644
--- a/src/jooq/java/com/kuvaszuptime/kuvasz/tables/UptimeEvent.java
+++ b/src/jooq/java/com/kuvaszuptime/kuvasz/tables/UptimeEvent.java
@@ -36,7 +36,7 @@
@SuppressWarnings({ "all", "unchecked", "rawtypes" })
public class UptimeEvent extends TableImpl {
- private static final long serialVersionUID = -2004053888;
+ private static final long serialVersionUID = 497612164;
/**
* The reference instance of uptime_event
diff --git a/src/jooq/java/com/kuvaszuptime/kuvasz/tables/daos/MonitorDao.java b/src/jooq/java/com/kuvaszuptime/kuvasz/tables/daos/MonitorDao.java
index 19aa15d..29936a5 100644
--- a/src/jooq/java/com/kuvaszuptime/kuvasz/tables/daos/MonitorDao.java
+++ b/src/jooq/java/com/kuvaszuptime/kuvasz/tables/daos/MonitorDao.java
@@ -75,6 +75,13 @@ public List fetchByName(String... values) {
return fetch(Monitor.MONITOR.NAME, values);
}
+ /**
+ * Fetch a unique record that has name = value
+ */
+ public MonitorPojo fetchOneByName(String value) {
+ return fetchOne(Monitor.MONITOR.NAME, value);
+ }
+
/**
* Fetch records that have url BETWEEN lowerInclusive AND upperInclusive
*/
diff --git a/src/jooq/java/com/kuvaszuptime/kuvasz/tables/pojos/LatencyLogPojo.java b/src/jooq/java/com/kuvaszuptime/kuvasz/tables/pojos/LatencyLogPojo.java
index 57787c6..7d5361d 100644
--- a/src/jooq/java/com/kuvaszuptime/kuvasz/tables/pojos/LatencyLogPojo.java
+++ b/src/jooq/java/com/kuvaszuptime/kuvasz/tables/pojos/LatencyLogPojo.java
@@ -30,7 +30,7 @@
})
public class LatencyLogPojo implements Serializable {
- private static final long serialVersionUID = -204244894;
+ private static final long serialVersionUID = -213764250;
private Integer id;
private Integer monitorId;
diff --git a/src/jooq/java/com/kuvaszuptime/kuvasz/tables/pojos/MonitorPojo.java b/src/jooq/java/com/kuvaszuptime/kuvasz/tables/pojos/MonitorPojo.java
index 5e9af31..2fb4cf4 100644
--- a/src/jooq/java/com/kuvaszuptime/kuvasz/tables/pojos/MonitorPojo.java
+++ b/src/jooq/java/com/kuvaszuptime/kuvasz/tables/pojos/MonitorPojo.java
@@ -24,11 +24,12 @@
@SuppressWarnings({ "all", "unchecked", "rawtypes" })
@Entity
@Table(name = "monitor", uniqueConstraints = {
- @UniqueConstraint(name = "monitor_pkey", columnNames = {"id"})
+ @UniqueConstraint(name = "monitor_pkey", columnNames = {"id"}),
+ @UniqueConstraint(name = "unique_monitor_name", columnNames = {"name"})
})
public class MonitorPojo implements Serializable {
- private static final long serialVersionUID = 763841695;
+ private static final long serialVersionUID = -1292240686;
private Integer id;
private String name;
diff --git a/src/jooq/java/com/kuvaszuptime/kuvasz/tables/pojos/UptimeEventPojo.java b/src/jooq/java/com/kuvaszuptime/kuvasz/tables/pojos/UptimeEventPojo.java
index 3c021c5..f2d1f60 100644
--- a/src/jooq/java/com/kuvaszuptime/kuvasz/tables/pojos/UptimeEventPojo.java
+++ b/src/jooq/java/com/kuvaszuptime/kuvasz/tables/pojos/UptimeEventPojo.java
@@ -34,7 +34,7 @@
})
public class UptimeEventPojo implements Serializable {
- private static final long serialVersionUID = -530417720;
+ private static final long serialVersionUID = -692998392;
private Integer id;
private Integer monitorId;
diff --git a/src/jooq/java/com/kuvaszuptime/kuvasz/tables/records/LatencyLogRecord.java b/src/jooq/java/com/kuvaszuptime/kuvasz/tables/records/LatencyLogRecord.java
index e40c158..5d00b08 100644
--- a/src/jooq/java/com/kuvaszuptime/kuvasz/tables/records/LatencyLogRecord.java
+++ b/src/jooq/java/com/kuvaszuptime/kuvasz/tables/records/LatencyLogRecord.java
@@ -37,7 +37,7 @@
})
public class LatencyLogRecord extends UpdatableRecordImpl implements Record4 {
- private static final long serialVersionUID = 481973074;
+ private static final long serialVersionUID = 1412652498;
/**
* Setter for latency_log.id
.
diff --git a/src/jooq/java/com/kuvaszuptime/kuvasz/tables/records/MonitorRecord.java b/src/jooq/java/com/kuvaszuptime/kuvasz/tables/records/MonitorRecord.java
index fd2b0a4..c035b7f 100644
--- a/src/jooq/java/com/kuvaszuptime/kuvasz/tables/records/MonitorRecord.java
+++ b/src/jooq/java/com/kuvaszuptime/kuvasz/tables/records/MonitorRecord.java
@@ -31,11 +31,12 @@
@SuppressWarnings({ "all", "unchecked", "rawtypes" })
@Entity
@Table(name = "monitor", uniqueConstraints = {
- @UniqueConstraint(name = "monitor_pkey", columnNames = {"id"})
+ @UniqueConstraint(name = "monitor_pkey", columnNames = {"id"}),
+ @UniqueConstraint(name = "unique_monitor_name", columnNames = {"name"})
})
public class MonitorRecord extends UpdatableRecordImpl implements Record7 {
- private static final long serialVersionUID = 1371821158;
+ private static final long serialVersionUID = 1383018433;
/**
* Setter for monitor.id
.
diff --git a/src/jooq/java/com/kuvaszuptime/kuvasz/tables/records/UptimeEventRecord.java b/src/jooq/java/com/kuvaszuptime/kuvasz/tables/records/UptimeEventRecord.java
index b6f8752..c59b00e 100644
--- a/src/jooq/java/com/kuvaszuptime/kuvasz/tables/records/UptimeEventRecord.java
+++ b/src/jooq/java/com/kuvaszuptime/kuvasz/tables/records/UptimeEventRecord.java
@@ -40,7 +40,7 @@
})
public class UptimeEventRecord extends UpdatableRecordImpl implements Record6 {
- private static final long serialVersionUID = 629056969;
+ private static final long serialVersionUID = 187938053;
/**
* Setter for uptime_event.id
.
diff --git a/src/main/kotlin/com/kuvaszuptime/kuvasz/Controller.kt b/src/main/kotlin/com/kuvaszuptime/kuvasz/Controller.kt
deleted file mode 100644
index cba9119..0000000
--- a/src/main/kotlin/com/kuvaszuptime/kuvasz/Controller.kt
+++ /dev/null
@@ -1,11 +0,0 @@
-package com.kuvaszuptime.kuvasz
-
-import io.micronaut.http.HttpResponse
-import io.micronaut.http.annotation.Controller
-import io.micronaut.http.annotation.Get
-
-@Controller("/")
-class Controller {
- @Get("/hello")
- fun get(): HttpResponse = HttpResponse.ok()
-}
diff --git a/src/main/kotlin/com/kuvaszuptime/kuvasz/controllers/GlobalErrorHandler.kt b/src/main/kotlin/com/kuvaszuptime/kuvasz/controllers/GlobalErrorHandler.kt
new file mode 100644
index 0000000..b3b3f68
--- /dev/null
+++ b/src/main/kotlin/com/kuvaszuptime/kuvasz/controllers/GlobalErrorHandler.kt
@@ -0,0 +1,66 @@
+package com.kuvaszuptime.kuvasz.controllers
+
+import com.fasterxml.jackson.core.JsonParseException
+import com.kuvaszuptime.kuvasz.models.DuplicationError
+import com.kuvaszuptime.kuvasz.models.MonitorNotFoundError
+import com.kuvaszuptime.kuvasz.models.PersistenceError
+import com.kuvaszuptime.kuvasz.models.SchedulingError
+import com.kuvaszuptime.kuvasz.models.ServiceError
+import io.micronaut.core.convert.exceptions.ConversionErrorException
+import io.micronaut.http.HttpRequest
+import io.micronaut.http.HttpResponse
+import io.micronaut.http.HttpStatus
+import io.micronaut.http.annotation.Controller
+import io.micronaut.http.annotation.Error
+import javax.validation.ValidationException
+
+@Controller
+class GlobalErrorHandler {
+
+ @Suppress("UNUSED_PARAMETER")
+ @Error(global = true)
+ fun jsonError(request: HttpRequest<*>, monitorNotFoundError: MonitorNotFoundError): HttpResponse {
+ val error = ServiceError(monitorNotFoundError.message)
+ return HttpResponse.notFound(error)
+ }
+
+ @Suppress("UNUSED_PARAMETER")
+ @Error(global = true)
+ fun jsonError(request: HttpRequest<*>, duplicationError: DuplicationError): HttpResponse {
+ val error = ServiceError(duplicationError.message)
+ return HttpResponse.status(HttpStatus.CONFLICT).body(error)
+ }
+
+ @Suppress("UNUSED_PARAMETER")
+ @Error(global = true)
+ fun error(request: HttpRequest<*>, throwable: ValidationException): HttpResponse =
+ HttpResponse.badRequest(ServiceError(throwable.message))
+
+ @Suppress("UNUSED_PARAMETER")
+ @Error(global = true)
+ fun error(request: HttpRequest<*>, throwable: ConversionErrorException): HttpResponse {
+ val message = "Failed to convert argument: ${throwable.argument}"
+ return HttpResponse.badRequest(ServiceError(message))
+ }
+
+ @Suppress("UNUSED_PARAMETER")
+ @Error(global = true)
+ fun error(request: HttpRequest<*>, throwable: JsonParseException): HttpResponse {
+ val message = "Can't parse the JSON in the payload"
+ return HttpResponse.badRequest(ServiceError(message))
+ }
+
+ @Suppress("UNUSED_PARAMETER")
+ @Error(global = true)
+ fun jsonError(request: HttpRequest<*>, persistencyError: PersistenceError): HttpResponse {
+ val error = ServiceError(persistencyError.message)
+ return HttpResponse.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error)
+ }
+
+ @Suppress("UNUSED_PARAMETER")
+ @Error(global = true)
+ fun jsonError(request: HttpRequest<*>, schedulingError: SchedulingError): HttpResponse {
+ val error = ServiceError(schedulingError.message)
+ return HttpResponse.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error)
+ }
+}
diff --git a/src/main/kotlin/com/kuvaszuptime/kuvasz/controllers/MonitorController.kt b/src/main/kotlin/com/kuvaszuptime/kuvasz/controllers/MonitorController.kt
new file mode 100644
index 0000000..acaee83
--- /dev/null
+++ b/src/main/kotlin/com/kuvaszuptime/kuvasz/controllers/MonitorController.kt
@@ -0,0 +1,104 @@
+package com.kuvaszuptime.kuvasz.controllers
+
+import com.kuvaszuptime.kuvasz.models.MonitorNotFoundError
+import com.kuvaszuptime.kuvasz.models.ServiceError
+import com.kuvaszuptime.kuvasz.models.dto.MonitorCreateDto
+import com.kuvaszuptime.kuvasz.models.dto.MonitorDetailsDto
+import com.kuvaszuptime.kuvasz.models.dto.MonitorUpdateDto
+import com.kuvaszuptime.kuvasz.services.MonitorCrudService
+import com.kuvaszuptime.kuvasz.tables.pojos.MonitorPojo
+import io.micronaut.http.HttpStatus
+import io.micronaut.http.MediaType
+import io.micronaut.http.annotation.Controller
+import io.micronaut.http.annotation.QueryValue
+import io.micronaut.http.annotation.Status
+import io.swagger.v3.oas.annotations.media.ArraySchema
+import io.swagger.v3.oas.annotations.media.Content
+import io.swagger.v3.oas.annotations.media.Schema
+import io.swagger.v3.oas.annotations.responses.ApiResponse
+import io.swagger.v3.oas.annotations.responses.ApiResponses
+import io.swagger.v3.oas.annotations.tags.Tag
+import javax.inject.Inject
+
+@Controller("/monitor", produces = [MediaType.APPLICATION_JSON])
+@Tag(name = "Monitor operations")
+class MonitorController @Inject constructor(
+ private val monitorCrudService: MonitorCrudService
+) : MonitorOperations {
+
+ @ApiResponses(
+ ApiResponse(
+ responseCode = "200",
+ description = "Successful query",
+ content = [Content(array = ArraySchema(schema = Schema(implementation = MonitorDetailsDto::class)))]
+ )
+ )
+ override fun getMonitors(@QueryValue enabledOnly: Boolean?): List =
+ monitorCrudService.getMonitorDetails(enabledOnly ?: false)
+
+ @ApiResponses(
+ ApiResponse(
+ responseCode = "200",
+ description = "Successful query",
+ content = [Content(schema = Schema(implementation = MonitorDetailsDto::class))]
+ ),
+ ApiResponse(
+ responseCode = "404",
+ description = "Not found",
+ content = [Content(schema = Schema(implementation = ServiceError::class))]
+ )
+ )
+ override fun getMonitor(monitorId: Int): MonitorDetailsDto =
+ monitorCrudService.getMonitorDetails(monitorId).fold(
+ { throw MonitorNotFoundError(monitorId) },
+ { it }
+ )
+
+ @Status(HttpStatus.CREATED)
+ @ApiResponses(
+ ApiResponse(
+ responseCode = "201",
+ description = "Successful creation",
+ content = [Content(schema = Schema(implementation = MonitorPojo::class))]
+ ),
+ ApiResponse(
+ responseCode = "400",
+ description = "Bad request",
+ content = [Content(schema = Schema(implementation = ServiceError::class))]
+ )
+ )
+ override fun createMonitor(monitor: MonitorCreateDto): MonitorPojo = monitorCrudService.createMonitor(monitor)
+
+ @Status(HttpStatus.NO_CONTENT)
+ @ApiResponses(
+ ApiResponse(
+ responseCode = "204",
+ description = "Successful deletion"
+ ),
+ ApiResponse(
+ responseCode = "404",
+ description = "Not found",
+ content = [Content(schema = Schema(implementation = ServiceError::class))]
+ )
+ )
+ override fun deleteMonitor(monitorId: Int) = monitorCrudService.deleteMonitorById(monitorId)
+
+ @ApiResponses(
+ ApiResponse(
+ responseCode = "200",
+ description = "Successful update"
+ ),
+ ApiResponse(
+ responseCode = "400",
+ description = "Bad request",
+ content = [Content(schema = Schema(implementation = ServiceError::class))]
+ ),
+ ApiResponse(
+ responseCode = "404",
+ description = "Not found",
+ content = [Content(schema = Schema(implementation = ServiceError::class))]
+ )
+ )
+ override fun updateMonitor(monitorId: Int, monitorUpdateDto: MonitorUpdateDto): MonitorPojo =
+ monitorCrudService.updateMonitor(monitorId, monitorUpdateDto)
+}
diff --git a/src/main/kotlin/com/kuvaszuptime/kuvasz/controllers/MonitorOperations.kt b/src/main/kotlin/com/kuvaszuptime/kuvasz/controllers/MonitorOperations.kt
new file mode 100644
index 0000000..0fee295
--- /dev/null
+++ b/src/main/kotlin/com/kuvaszuptime/kuvasz/controllers/MonitorOperations.kt
@@ -0,0 +1,51 @@
+package com.kuvaszuptime.kuvasz.controllers
+
+import com.kuvaszuptime.kuvasz.models.dto.MonitorCreateDto
+import com.kuvaszuptime.kuvasz.models.dto.MonitorDetailsDto
+import com.kuvaszuptime.kuvasz.models.dto.MonitorUpdateDto
+import com.kuvaszuptime.kuvasz.tables.pojos.MonitorPojo
+import io.micronaut.http.annotation.Body
+import io.micronaut.http.annotation.Delete
+import io.micronaut.http.annotation.Get
+import io.micronaut.http.annotation.Patch
+import io.micronaut.http.annotation.Post
+import io.micronaut.http.annotation.QueryValue
+import io.micronaut.scheduling.TaskExecutors
+import io.micronaut.scheduling.annotation.ExecuteOn
+import io.micronaut.validation.Validated
+import io.swagger.v3.oas.annotations.Operation
+import io.swagger.v3.oas.annotations.Parameter
+import javax.validation.Valid
+
+@Validated
+interface MonitorOperations {
+
+ @Operation(summary = "Returns all monitors")
+ @Get("/")
+ @ExecuteOn(TaskExecutors.IO)
+ fun getMonitors(
+ @QueryValue
+ @Parameter(required = false)
+ enabledOnly: Boolean?
+ ): List
+
+ @Operation(summary = "Returns a monitor's details")
+ @Get("/{monitorId}")
+ @ExecuteOn(TaskExecutors.IO)
+ fun getMonitor(monitorId: Int): MonitorDetailsDto
+
+ @Operation(summary = "Creates a monitor")
+ @Post("/")
+ @ExecuteOn(TaskExecutors.IO)
+ fun createMonitor(@Valid @Body monitor: MonitorCreateDto): MonitorPojo
+
+ @Operation(summary = "Deletes a monitor by ID")
+ @Delete("/{monitorId}")
+ @ExecuteOn(TaskExecutors.IO)
+ fun deleteMonitor(monitorId: Int)
+
+ @Operation(summary = "Updates a monitor by ID")
+ @Patch("/{monitorId}")
+ @ExecuteOn(TaskExecutors.IO)
+ fun updateMonitor(monitorId: Int, @Valid @Body monitorUpdateDto: MonitorUpdateDto): MonitorPojo
+}
diff --git a/src/main/kotlin/com/kuvaszuptime/kuvasz/models/Error.kt b/src/main/kotlin/com/kuvaszuptime/kuvasz/models/Error.kt
new file mode 100644
index 0000000..d8312bd
--- /dev/null
+++ b/src/main/kotlin/com/kuvaszuptime/kuvasz/models/Error.kt
@@ -0,0 +1,29 @@
+package com.kuvaszuptime.kuvasz.models
+
+import io.micronaut.core.annotation.Introspected
+
+@Introspected
+data class ServiceError(
+ val message: String? = "Something bad happened :("
+)
+
+class MonitorNotFoundError(
+ private val monitorId: Int,
+ override val message: String? = "There is no monitor with ID: $monitorId"
+) : Throwable()
+
+open class PersistenceError(
+ override val message: String? = "Something bad happened in the database :("
+) : Throwable()
+
+open class DuplicationError(
+ override val message: String? = "The given resource already exists"
+) : PersistenceError()
+
+class MonitorDuplicatedError(
+ override val message: String? = "There is already a monitor with the given name"
+) : DuplicationError()
+
+class SchedulingError(
+ override val message: String? = "Scheduling checks for the monitor did not succeed"
+) : Throwable()
diff --git a/src/main/kotlin/com/kuvaszuptime/kuvasz/models/dto/MonitorCreateDto.kt b/src/main/kotlin/com/kuvaszuptime/kuvasz/models/dto/MonitorCreateDto.kt
new file mode 100644
index 0000000..4a91434
--- /dev/null
+++ b/src/main/kotlin/com/kuvaszuptime/kuvasz/models/dto/MonitorCreateDto.kt
@@ -0,0 +1,29 @@
+package com.kuvaszuptime.kuvasz.models.dto
+
+import com.kuvaszuptime.kuvasz.models.dto.Validation.MIN_UPTIME_CHECK_INTERVAL
+import com.kuvaszuptime.kuvasz.models.dto.Validation.URI_REGEX
+import com.kuvaszuptime.kuvasz.tables.pojos.MonitorPojo
+import io.micronaut.core.annotation.Introspected
+import javax.validation.constraints.Min
+import javax.validation.constraints.NotBlank
+import javax.validation.constraints.NotNull
+import javax.validation.constraints.Pattern
+
+@Introspected
+data class MonitorCreateDto(
+ @get:NotBlank
+ val name: String,
+ @get:NotNull
+ @get:Pattern(regexp = URI_REGEX)
+ val url: String,
+ @get:NotNull
+ @get:Min(MIN_UPTIME_CHECK_INTERVAL)
+ val uptimeCheckInterval: Int,
+ val enabled: Boolean? = true
+) {
+ fun toMonitorPojo(): MonitorPojo = MonitorPojo()
+ .setName(name)
+ .setUrl(url)
+ .setEnabled(enabled)
+ .setUptimeCheckInterval(uptimeCheckInterval)
+}
diff --git a/src/main/kotlin/com/kuvaszuptime/kuvasz/models/dto/MonitorDetailsDto.kt b/src/main/kotlin/com/kuvaszuptime/kuvasz/models/dto/MonitorDetailsDto.kt
new file mode 100644
index 0000000..edb7d02
--- /dev/null
+++ b/src/main/kotlin/com/kuvaszuptime/kuvasz/models/dto/MonitorDetailsDto.kt
@@ -0,0 +1,21 @@
+package com.kuvaszuptime.kuvasz.models.dto
+
+import com.kuvaszuptime.kuvasz.enums.UptimeStatus
+import io.micronaut.core.annotation.Introspected
+import java.net.URI
+import java.time.OffsetDateTime
+
+@Introspected
+data class MonitorDetailsDto(
+ val id: Int,
+ val name: String,
+ val url: URI,
+ val uptimeCheckInterval: Int,
+ val enabled: Boolean,
+ val createdAt: OffsetDateTime,
+ val updatedAt: OffsetDateTime?,
+ val uptimeStatus: UptimeStatus?,
+ val uptimeStatusStartedAt: OffsetDateTime?,
+ val uptimeError: String?,
+ val averageLatencyInMs: Int?
+)
diff --git a/src/main/kotlin/com/kuvaszuptime/kuvasz/models/dto/MonitorUpdateDto.kt b/src/main/kotlin/com/kuvaszuptime/kuvasz/models/dto/MonitorUpdateDto.kt
new file mode 100644
index 0000000..d533fa3
--- /dev/null
+++ b/src/main/kotlin/com/kuvaszuptime/kuvasz/models/dto/MonitorUpdateDto.kt
@@ -0,0 +1,17 @@
+package com.kuvaszuptime.kuvasz.models.dto
+
+import com.kuvaszuptime.kuvasz.models.dto.Validation.MIN_UPTIME_CHECK_INTERVAL
+import com.kuvaszuptime.kuvasz.models.dto.Validation.URI_REGEX
+import io.micronaut.core.annotation.Introspected
+import javax.validation.constraints.Min
+import javax.validation.constraints.Pattern
+
+@Introspected
+data class MonitorUpdateDto(
+ val name: String?,
+ @get:Pattern(regexp = URI_REGEX)
+ val url: String?,
+ @get:Min(MIN_UPTIME_CHECK_INTERVAL)
+ val uptimeCheckInterval: Int?,
+ val enabled: Boolean?
+)
diff --git a/src/main/kotlin/com/kuvaszuptime/kuvasz/models/dto/Validation.kt b/src/main/kotlin/com/kuvaszuptime/kuvasz/models/dto/Validation.kt
new file mode 100644
index 0000000..6c5c7c5
--- /dev/null
+++ b/src/main/kotlin/com/kuvaszuptime/kuvasz/models/dto/Validation.kt
@@ -0,0 +1,6 @@
+package com.kuvaszuptime.kuvasz.models.dto
+
+internal object Validation {
+ const val MIN_UPTIME_CHECK_INTERVAL = 60L
+ const val URI_REGEX = "^(https?)://[-a-zA-Z0-9+&@#/%?=~_|!:,.;]*[-a-zA-Z0-9+&@#/%=~_|]"
+}
diff --git a/src/main/kotlin/com/kuvaszuptime/kuvasz/repositories/MonitorRepository.kt b/src/main/kotlin/com/kuvaszuptime/kuvasz/repositories/MonitorRepository.kt
index 06f2b80..7025e4b 100644
--- a/src/main/kotlin/com/kuvaszuptime/kuvasz/repositories/MonitorRepository.kt
+++ b/src/main/kotlin/com/kuvaszuptime/kuvasz/repositories/MonitorRepository.kt
@@ -1,9 +1,115 @@
package com.kuvaszuptime.kuvasz.repositories
+import arrow.core.Either
+import arrow.core.Option
+import arrow.core.toOption
+import com.kuvaszuptime.kuvasz.models.DuplicationError
+import com.kuvaszuptime.kuvasz.models.MonitorDuplicatedError
+import com.kuvaszuptime.kuvasz.models.PersistenceError
+import com.kuvaszuptime.kuvasz.models.dto.MonitorDetailsDto
+import com.kuvaszuptime.kuvasz.tables.LatencyLog.LATENCY_LOG
+import com.kuvaszuptime.kuvasz.tables.Monitor.MONITOR
+import com.kuvaszuptime.kuvasz.tables.UptimeEvent.UPTIME_EVENT
import com.kuvaszuptime.kuvasz.tables.daos.MonitorDao
+import com.kuvaszuptime.kuvasz.tables.pojos.MonitorPojo
+import com.kuvaszuptime.kuvasz.util.getCurrentTimestamp
+import com.kuvaszuptime.kuvasz.util.toPersistenceError
import org.jooq.Configuration
+import org.jooq.exception.DataAccessException
+import org.jooq.impl.DSL
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
-class MonitorRepository @Inject constructor(jooqConfig: Configuration) : MonitorDao(jooqConfig)
+class MonitorRepository @Inject constructor(jooqConfig: Configuration) : MonitorDao(jooqConfig) {
+
+ private val dsl = jooqConfig.dsl()
+
+ fun getMonitorDetails(monitorId: Int): Option =
+ getMonitorDetailsSelect()
+ .where(MONITOR.ID.eq(monitorId))
+ .groupBy(
+ MONITOR.ID,
+ UPTIME_EVENT.STATUS,
+ UPTIME_EVENT.STARTED_AT,
+ UPTIME_EVENT.ERROR
+ )
+ .fetchOneInto(MonitorDetailsDto::class.java)
+ .toOption()
+
+ fun getMonitorDetails(enabledOnly: Boolean): List =
+ getMonitorDetailsSelect()
+ .apply {
+ if (enabledOnly) {
+ where(MONITOR.ENABLED.isTrue)
+ }
+ }
+ .groupBy(
+ MONITOR.ID,
+ UPTIME_EVENT.STATUS,
+ UPTIME_EVENT.STARTED_AT,
+ UPTIME_EVENT.ERROR
+ )
+ .fetchInto(MonitorDetailsDto::class.java)
+
+ fun returningInsert(monitorPojo: MonitorPojo): Either =
+ try {
+ Either.right(
+ dsl
+ .insertInto(MONITOR)
+ .set(dsl.newRecord(MONITOR, monitorPojo))
+ .returning(MONITOR.asterisk())
+ .fetchOne()
+ .into(MonitorPojo::class.java)
+ )
+ } catch (e: DataAccessException) {
+ e.handle()
+ }
+
+ fun returningUpdate(updatedPojo: MonitorPojo): Either =
+ try {
+ Either.right(
+ dsl
+ .update(MONITOR)
+ .set(MONITOR.NAME, updatedPojo.name)
+ .set(MONITOR.URL, updatedPojo.url)
+ .set(MONITOR.UPTIME_CHECK_INTERVAL, updatedPojo.uptimeCheckInterval)
+ .set(MONITOR.ENABLED, updatedPojo.enabled)
+ .set(MONITOR.UPDATED_AT, getCurrentTimestamp())
+ .where(MONITOR.ID.eq(updatedPojo.id))
+ .returning(MONITOR.asterisk())
+ .fetchOne()
+ .into(MonitorPojo::class.java)
+ )
+ } catch (e: DataAccessException) {
+ e.handle()
+ }
+
+ private fun getMonitorDetailsSelect() =
+ dsl
+ .select(
+ MONITOR.ID.`as`("id"),
+ MONITOR.NAME.`as`("name"),
+ MONITOR.URL.`as`("url"),
+ MONITOR.UPTIME_CHECK_INTERVAL.`as`("uptimeCheckInterval"),
+ MONITOR.ENABLED.`as`("enabled"),
+ MONITOR.CREATED_AT.`as`("createdAt"),
+ MONITOR.UPDATED_AT.`as`("updatedAt"),
+ UPTIME_EVENT.STATUS.`as`("uptimeStatus"),
+ UPTIME_EVENT.STARTED_AT.`as`("uptimeStatusStartedAt"),
+ UPTIME_EVENT.ERROR.`as`("uptimeError"),
+ DSL.avg(LATENCY_LOG.LATENCY).`as`("averageLatencyInMs")
+ )
+ .from(MONITOR)
+ .leftJoin(UPTIME_EVENT).on(MONITOR.ID.eq(UPTIME_EVENT.MONITOR_ID).and(UPTIME_EVENT.ENDED_AT.isNull))
+ .leftJoin(LATENCY_LOG).on(MONITOR.ID.eq(LATENCY_LOG.MONITOR_ID))
+
+ private fun DataAccessException.handle(): Either {
+ val persistenceError = toPersistenceError()
+ return Either.left(
+ if (persistenceError is DuplicationError) {
+ MonitorDuplicatedError()
+ } else persistenceError
+ )
+ }
+}
diff --git a/src/main/kotlin/com/kuvaszuptime/kuvasz/services/CheckScheduler.kt b/src/main/kotlin/com/kuvaszuptime/kuvasz/services/CheckScheduler.kt
index a116a24..ba4d5c5 100644
--- a/src/main/kotlin/com/kuvaszuptime/kuvasz/services/CheckScheduler.kt
+++ b/src/main/kotlin/com/kuvaszuptime/kuvasz/services/CheckScheduler.kt
@@ -3,6 +3,7 @@ package com.kuvaszuptime.kuvasz.services
import arrow.core.Either
import com.kuvaszuptime.kuvasz.models.CheckType
import com.kuvaszuptime.kuvasz.models.ScheduledCheck
+import com.kuvaszuptime.kuvasz.models.SchedulingError
import com.kuvaszuptime.kuvasz.repositories.MonitorRepository
import com.kuvaszuptime.kuvasz.tables.pojos.MonitorPojo
import com.kuvaszuptime.kuvasz.util.catchBlocking
@@ -37,22 +38,47 @@ class CheckScheduler @Inject constructor(
fun getScheduledChecks() = scheduledChecks
- fun createChecksForMonitor(monitor: MonitorPojo) {
- scheduleUptimeCheck(monitor).fold(
+ fun createChecksForMonitor(monitor: MonitorPojo): Either =
+ scheduleUptimeCheck(monitor).bimap(
{ e ->
logger.error(
"Uptime check for \"${monitor.name}\" (${monitor.url}) cannot be set up: ${e.message}"
)
+ SchedulingError(e.message)
},
{ scheduledTask ->
- scheduledChecks.add(
+ val scheduledCheck =
ScheduledCheck(checkType = CheckType.UPTIME, monitorId = monitor.id, task = scheduledTask)
- )
- logger.info(
- "Uptime check for \"${monitor.name}\" (${monitor.url}) has been set up successfully"
- )
+ scheduledChecks.add(scheduledCheck)
+ logger.info("Uptime check for \"${monitor.name}\" (${monitor.url}) has been set up successfully")
+
+ scheduledCheck
}
)
+
+ fun removeChecksOfMonitor(monitor: MonitorPojo) {
+ scheduledChecks.forEach { check ->
+ if (check.monitorId == monitor.id) {
+ check.task.cancel(false)
+ }
+ }
+ scheduledChecks.removeAll { it.monitorId == monitor.id }
+ logger.info("Uptime check for \"${monitor.name}\" (${monitor.url}) has been removed successfully")
+ }
+
+ fun removeAllChecks() {
+ scheduledChecks.forEach { check ->
+ check.task.cancel(false)
+ }
+ scheduledChecks.clear()
+ }
+
+ fun updateChecksForMonitor(
+ existingMonitor: MonitorPojo,
+ updatedMonitor: MonitorPojo
+ ): Either {
+ removeChecksOfMonitor(existingMonitor)
+ return createChecksForMonitor(updatedMonitor)
}
private fun scheduleUptimeCheck(monitor: MonitorPojo): Either> =
diff --git a/src/main/kotlin/com/kuvaszuptime/kuvasz/services/MonitorCrudService.kt b/src/main/kotlin/com/kuvaszuptime/kuvasz/services/MonitorCrudService.kt
new file mode 100644
index 0000000..0c1aa69
--- /dev/null
+++ b/src/main/kotlin/com/kuvaszuptime/kuvasz/services/MonitorCrudService.kt
@@ -0,0 +1,78 @@
+package com.kuvaszuptime.kuvasz.services
+
+import arrow.core.Option
+import arrow.core.toOption
+import com.kuvaszuptime.kuvasz.models.MonitorNotFoundError
+import com.kuvaszuptime.kuvasz.models.dto.MonitorCreateDto
+import com.kuvaszuptime.kuvasz.models.dto.MonitorDetailsDto
+import com.kuvaszuptime.kuvasz.models.dto.MonitorUpdateDto
+import com.kuvaszuptime.kuvasz.repositories.MonitorRepository
+import com.kuvaszuptime.kuvasz.tables.pojos.MonitorPojo
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
+class MonitorCrudService @Inject constructor(
+ private val monitorRepository: MonitorRepository,
+ private val checkScheduler: CheckScheduler
+) {
+
+ fun getMonitorDetails(monitorId: Int): Option = monitorRepository.getMonitorDetails(monitorId)
+
+ fun getMonitorDetails(enabledOnly: Boolean): List =
+ monitorRepository.getMonitorDetails(enabledOnly)
+
+ fun createMonitor(monitorCreateDto: MonitorCreateDto): MonitorPojo =
+ monitorRepository.returningInsert(monitorCreateDto.toMonitorPojo()).fold(
+ { persistenceError -> throw persistenceError },
+ { insertedMonitor ->
+ if (insertedMonitor.enabled) {
+ checkScheduler.createChecksForMonitor(insertedMonitor).mapLeft { schedulingError ->
+ monitorRepository.deleteById(insertedMonitor.id)
+ throw schedulingError
+ }
+ }
+ insertedMonitor
+ }
+ )
+
+ fun deleteMonitorById(monitorId: Int) = monitorRepository.findById(monitorId).toOption().fold(
+ { throw MonitorNotFoundError(monitorId) },
+ { monitorPojo ->
+ monitorRepository.deleteById(monitorPojo.id)
+ checkScheduler.removeChecksOfMonitor(monitorPojo)
+ }
+ )
+
+ fun updateMonitor(monitorId: Int, monitorUpdateDto: MonitorUpdateDto): MonitorPojo =
+ monitorRepository.findById(monitorId).toOption().fold(
+ { throw MonitorNotFoundError(monitorId) },
+ { existingMonitor ->
+ val updatedMonitor = MonitorPojo().apply {
+ id = existingMonitor.id
+ name = monitorUpdateDto.name ?: existingMonitor.name
+ url = monitorUpdateDto.url ?: existingMonitor.url
+ uptimeCheckInterval = monitorUpdateDto.uptimeCheckInterval ?: existingMonitor.uptimeCheckInterval
+ enabled = monitorUpdateDto.enabled ?: existingMonitor.enabled
+ }
+
+ updatedMonitor.saveAndReschedule(existingMonitor)
+ }
+ )
+
+ private fun MonitorPojo.saveAndReschedule(existingMonitor: MonitorPojo): MonitorPojo =
+ monitorRepository.returningUpdate(this).fold(
+ { persistenceError -> throw persistenceError },
+ { updatedMonitor ->
+ if (updatedMonitor.enabled) {
+ checkScheduler.updateChecksForMonitor(existingMonitor, updatedMonitor).fold(
+ { schedulingError -> throw schedulingError },
+ { updatedMonitor }
+ )
+ } else {
+ checkScheduler.removeChecksOfMonitor(existingMonitor)
+ updatedMonitor
+ }
+ }
+ )
+}
diff --git a/src/main/kotlin/com/kuvaszuptime/kuvasz/util/Jooq+.kt b/src/main/kotlin/com/kuvaszuptime/kuvasz/util/Jooq+.kt
index e679058..4a909d4 100644
--- a/src/main/kotlin/com/kuvaszuptime/kuvasz/util/Jooq+.kt
+++ b/src/main/kotlin/com/kuvaszuptime/kuvasz/util/Jooq+.kt
@@ -2,16 +2,21 @@
package com.kuvaszuptime.kuvasz.util
+import arrow.core.toOption
+import com.kuvaszuptime.kuvasz.models.DuplicationError
+import com.kuvaszuptime.kuvasz.models.PersistenceError
import org.jooq.Configuration
import org.jooq.DAO
import org.jooq.TableRecord
-import org.jooq.TransactionalCallable
+import org.jooq.exception.DataAccessException
+import org.postgresql.util.PSQLException
fun , P, T> DAO.transaction(block: () -> Unit) {
configuration().dsl().transaction { _: Configuration -> block() }
}
-fun , P, T, Result> DAO.transactionResult(block: () -> Result): Result =
- configuration().dsl().transactionResult(TransactionalCallable {
- block()
- })
+fun DataAccessException.toPersistenceError(): PersistenceError =
+ getCause(PSQLException::class.java)?.message.toOption().fold(
+ { PersistenceError(message) },
+ { if (it.contains("duplicate key")) DuplicationError() else PersistenceError(it) }
+ )
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
index f989a0c..bb82263 100644
--- a/src/main/resources/application.yml
+++ b/src/main/resources/application.yml
@@ -23,8 +23,6 @@ micronaut:
- pattern: /**
access:
- ROLE_ADMIN
- basic-auth:
- enabled: true
router:
versioning:
enabled: true
@@ -56,7 +54,8 @@ flyway:
jackson:
bean-introspection-module: true
serialization-inclusion: always
- date-format: "yyyy-MM-dd'T'HH:mm:ssZ"
+ serialization:
+ - WRITE_DATES_AS_TIMESTAMPS: false
---
jooq:
datasources:
diff --git a/src/main/resources/db/migration/V1__Create_table_for_monitors.sql b/src/main/resources/db/migration/V1__Create_table_for_monitors.sql
deleted file mode 100644
index dc2b297..0000000
--- a/src/main/resources/db/migration/V1__Create_table_for_monitors.sql
+++ /dev/null
@@ -1,17 +0,0 @@
-CREATE SCHEMA IF NOT EXISTS kuvasz;
-
-CREATE TABLE monitor
-(
- id SERIAL PRIMARY KEY,
- name VARCHAR(255) NOT NULL,
- url TEXT NOT NULL,
- uptime_check_interval INTEGER NOT NULL,
- enabled BOOLEAN NOT NULL DEFAULT TRUE,
- created_at TIMESTAMP WITH TIME ZONE NOT NULL default now(),
- updated_at TIMESTAMP WITH TIME ZONE
-);
-
-COMMENT ON COLUMN monitor.name IS 'Monitor''s name';
-COMMENT ON COLUMN monitor.url IS 'URL to check';
-COMMENT ON COLUMN monitor.uptime_check_interval IS 'Uptime checking interval in seconds';
-COMMENT ON COLUMN monitor.enabled IS 'Flag to toggle the monitor';
diff --git a/src/main/resources/db/migration/V2__Add_check_log_table.sql b/src/main/resources/db/migration/V1__Init_schema.sql
similarity index 60%
rename from src/main/resources/db/migration/V2__Add_check_log_table.sql
rename to src/main/resources/db/migration/V1__Init_schema.sql
index 83a3500..bcdd1ba 100644
--- a/src/main/resources/db/migration/V2__Add_check_log_table.sql
+++ b/src/main/resources/db/migration/V1__Init_schema.sql
@@ -1,11 +1,28 @@
-SET SCHEMA 'kuvasz';
+CREATE SCHEMA IF NOT EXISTS kuvasz;
+
+CREATE TABLE monitor
+(
+ id SERIAL PRIMARY KEY,
+ name VARCHAR(255) NOT NULL,
+ url TEXT NOT NULL,
+ uptime_check_interval INTEGER NOT NULL,
+ enabled BOOLEAN NOT NULL DEFAULT TRUE,
+ created_at TIMESTAMP WITH TIME ZONE NOT NULL default now(),
+ updated_at TIMESTAMP WITH TIME ZONE,
+ CONSTRAINT "unique_monitor_name" UNIQUE ("name")
+);
+
+COMMENT ON COLUMN monitor.name IS 'Monitor''s name';
+COMMENT ON COLUMN monitor.url IS 'URL to check';
+COMMENT ON COLUMN monitor.uptime_check_interval IS 'Uptime checking interval in seconds';
+COMMENT ON COLUMN monitor.enabled IS 'Flag to toggle the monitor';
CREATE TYPE uptime_status AS ENUM ('UP', 'DOWN');
CREATE TABLE uptime_event
(
id SERIAL PRIMARY KEY,
- monitor_id INTEGER NOT NULL REFERENCES monitor (id),
+ monitor_id INTEGER NOT NULL REFERENCES monitor (id) ON DELETE CASCADE,
status uptime_status NOT NULL,
error TEXT,
started_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
@@ -23,7 +40,7 @@ CREATE INDEX IF NOT EXISTS "uptime_event_ended_at_idx" ON "uptime_event" USING b
CREATE TABLE latency_log
(
id SERIAL PRIMARY KEY,
- monitor_id INTEGER NOT NULL REFERENCES monitor (id),
+ monitor_id INTEGER NOT NULL REFERENCES monitor (id) ON DELETE CASCADE,
latency INTEGER NOT NULL,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()
);
diff --git a/src/main/resources/logback-dev.xml b/src/main/resources/logback-dev.xml
index f29fa70..486ed7e 100644
--- a/src/main/resources/logback-dev.xml
+++ b/src/main/resources/logback-dev.xml
@@ -13,9 +13,9 @@
-
+
-
+
diff --git a/src/test/kotlin/com/kuvaszuptime/kuvasz/DatabaseBehaviorSpec.kt b/src/test/kotlin/com/kuvaszuptime/kuvasz/DatabaseBehaviorSpec.kt
index 44fdd59..3a04c08 100644
--- a/src/test/kotlin/com/kuvaszuptime/kuvasz/DatabaseBehaviorSpec.kt
+++ b/src/test/kotlin/com/kuvaszuptime/kuvasz/DatabaseBehaviorSpec.kt
@@ -6,7 +6,7 @@ import io.kotest.core.test.TestResult
import org.flywaydb.core.Flyway
import javax.inject.Inject
-abstract class DatabaseBehaviorSpec : BehaviorSpec() {
+abstract class DatabaseBehaviorSpec(body: BehaviorSpec.() -> Unit = {}) : BehaviorSpec(body) {
@Inject
lateinit var flyway: Flyway
diff --git a/src/test/kotlin/com/kuvaszuptime/kuvasz/config/AdminAuthConfigTest.kt b/src/test/kotlin/com/kuvaszuptime/kuvasz/config/AdminAuthConfigTest.kt
index 2fd7726..c98c68d 100644
--- a/src/test/kotlin/com/kuvaszuptime/kuvasz/config/AdminAuthConfigTest.kt
+++ b/src/test/kotlin/com/kuvaszuptime/kuvasz/config/AdminAuthConfigTest.kt
@@ -3,7 +3,7 @@ package com.kuvaszuptime.kuvasz.config
import io.kotest.assertions.exceptionToMessage
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.core.spec.style.BehaviorSpec
-import io.kotest.matchers.shouldBe
+import io.kotest.matchers.string.shouldContain
import io.micronaut.context.ApplicationContext
import io.micronaut.context.env.PropertySource
import io.micronaut.context.exceptions.BeanInstantiationException
@@ -22,7 +22,7 @@ class AdminAuthConfigTest : BehaviorSpec({
val exception = shouldThrow {
ApplicationContext.run(properties)
}
- exceptionToMessage(exception).contains("password - size must be between 12") shouldBe true
+ exceptionToMessage(exception) shouldContain "password - size must be between 12"
}
}
@@ -38,8 +38,8 @@ class AdminAuthConfigTest : BehaviorSpec({
val exception = shouldThrow {
ApplicationContext.run(properties)
}
- exceptionToMessage(exception).contains("username - must not be blank") shouldBe true
- exceptionToMessage(exception).contains("password - must not be blank") shouldBe true
+ exceptionToMessage(exception) shouldContain "username - must not be blank"
+ exceptionToMessage(exception) shouldContain "password - must not be blank"
}
}
}
diff --git a/src/test/kotlin/com/kuvaszuptime/kuvasz/controllers/MonitorClient.kt b/src/test/kotlin/com/kuvaszuptime/kuvasz/controllers/MonitorClient.kt
new file mode 100644
index 0000000..1b0b853
--- /dev/null
+++ b/src/test/kotlin/com/kuvaszuptime/kuvasz/controllers/MonitorClient.kt
@@ -0,0 +1,20 @@
+package com.kuvaszuptime.kuvasz.controllers
+
+import com.kuvaszuptime.kuvasz.models.dto.MonitorCreateDto
+import com.kuvaszuptime.kuvasz.models.dto.MonitorDetailsDto
+import com.kuvaszuptime.kuvasz.models.dto.MonitorUpdateDto
+import com.kuvaszuptime.kuvasz.tables.pojos.MonitorPojo
+import io.micronaut.http.client.annotation.Client
+
+@Client("/monitor")
+interface MonitorClient : MonitorOperations {
+ override fun getMonitor(monitorId: Int): MonitorDetailsDto
+
+ override fun getMonitors(enabledOnly: Boolean?): List
+
+ override fun createMonitor(monitor: MonitorCreateDto): MonitorPojo
+
+ override fun deleteMonitor(monitorId: Int)
+
+ override fun updateMonitor(monitorId: Int, monitorUpdateDto: MonitorUpdateDto): MonitorPojo
+}
diff --git a/src/test/kotlin/com/kuvaszuptime/kuvasz/controllers/MonitorControllerTest.kt b/src/test/kotlin/com/kuvaszuptime/kuvasz/controllers/MonitorControllerTest.kt
new file mode 100644
index 0000000..5664858
--- /dev/null
+++ b/src/test/kotlin/com/kuvaszuptime/kuvasz/controllers/MonitorControllerTest.kt
@@ -0,0 +1,335 @@
+package com.kuvaszuptime.kuvasz.controllers
+
+import arrow.core.Option
+import arrow.core.toOption
+import com.kuvaszuptime.kuvasz.DatabaseBehaviorSpec
+import com.kuvaszuptime.kuvasz.mocks.createMonitor
+import com.kuvaszuptime.kuvasz.models.dto.MonitorCreateDto
+import com.kuvaszuptime.kuvasz.models.dto.MonitorUpdateDto
+import com.kuvaszuptime.kuvasz.repositories.MonitorRepository
+import com.kuvaszuptime.kuvasz.services.CheckScheduler
+import com.kuvaszuptime.kuvasz.testutils.shouldBe
+import io.kotest.assertions.exceptionToMessage
+import io.kotest.assertions.throwables.shouldThrow
+import io.kotest.core.test.TestCase
+import io.kotest.core.test.TestResult
+import io.kotest.inspectors.forNone
+import io.kotest.inspectors.forOne
+import io.kotest.matchers.collections.shouldHaveSize
+import io.kotest.matchers.shouldBe
+import io.kotest.matchers.shouldNotBe
+import io.kotest.matchers.string.shouldContain
+import io.micronaut.http.HttpRequest
+import io.micronaut.http.HttpStatus
+import io.micronaut.http.client.RxHttpClient
+import io.micronaut.http.client.annotation.Client
+import io.micronaut.http.client.exceptions.HttpClientResponseException
+import io.micronaut.test.annotation.MicronautTest
+
+@MicronautTest
+class MonitorControllerTest(
+ @Client("/") private val client: RxHttpClient,
+ private val monitorClient: MonitorClient,
+ private val monitorRepository: MonitorRepository,
+ private val checkScheduler: CheckScheduler
+) : DatabaseBehaviorSpec() {
+
+ init {
+ given("MonitorController's getMonitors() endpoint") {
+ `when`("there is any monitor in the database") {
+ val monitor = createMonitor(monitorRepository)
+ val response = monitorClient.getMonitors(enabledOnly = null)
+ then("it should return them") {
+ response shouldHaveSize 1
+ val responseItem = response.first()
+ responseItem.id shouldBe monitor.id
+ responseItem.url.toString() shouldBe monitor.url
+ responseItem.enabled shouldBe monitor.enabled
+ responseItem.averageLatencyInMs shouldBe null
+ responseItem.uptimeStatus shouldBe null
+ responseItem.createdAt shouldBe monitor.createdAt
+ }
+ }
+
+ `when`("enabledOnly parameter is set to true") {
+ createMonitor(monitorRepository, enabled = false, monitorName = "name1")
+ val enabledMonitor = createMonitor(monitorRepository, id = 11111, monitorName = "name2")
+ val response = monitorClient.getMonitors(enabledOnly = true)
+ then("it should not return disabled monitor") {
+ response shouldHaveSize 1
+ val responseItem = response.first()
+ responseItem.id shouldBe enabledMonitor.id
+ responseItem.url.toString() shouldBe enabledMonitor.url
+ responseItem.enabled shouldBe enabledMonitor.enabled
+ responseItem.averageLatencyInMs shouldBe null
+ responseItem.uptimeStatus shouldBe null
+ responseItem.createdAt shouldBe enabledMonitor.createdAt
+ }
+ }
+
+ `when`("there isn't any monitor in the database") {
+ val response = monitorClient.getMonitors(enabledOnly = false)
+ then("it should return an empty list") {
+ response shouldHaveSize 0
+ }
+ }
+ }
+
+ given("MonitorController's getMonitor() endpoint") {
+ `when`("there is a monitor with the given ID in the database") {
+ val monitor = createMonitor(monitorRepository)
+ val response = monitorClient.getMonitor(monitorId = monitor.id)
+ then("it should return it") {
+ response.id shouldBe monitor.id
+ response.url.toString() shouldBe monitor.url
+ response.enabled shouldBe monitor.enabled
+ response.averageLatencyInMs shouldBe null
+ response.uptimeStatus shouldBe null
+ response.createdAt shouldBe monitor.createdAt
+ }
+ }
+
+ `when`("there is no monitor with the given ID in the database") {
+ val response = shouldThrow {
+ client.toBlocking().exchange("/monitor/1232132432")
+ }
+ then("it should return a 404") {
+ response.status shouldBe HttpStatus.NOT_FOUND
+ }
+ }
+ }
+
+ given("MonitorController's createMonitor() endpoint") {
+
+ `when`("it is called with a valid DTO") {
+ val monitorToCreate = MonitorCreateDto(
+ name = "test_monitor",
+ url = "https://valid-url.com",
+ uptimeCheckInterval = 6000
+ )
+ val createdMonitor = monitorClient.createMonitor(monitorToCreate)
+
+ then("it should create a monitor and also schedule checks for it") {
+ val monitorInDb = monitorRepository.findById(createdMonitor.id)!!
+ monitorInDb.name shouldBe createdMonitor.name
+ monitorInDb.url shouldBe createdMonitor.url
+ monitorInDb.uptimeCheckInterval shouldBe createdMonitor.uptimeCheckInterval
+ monitorInDb.enabled shouldBe createdMonitor.enabled
+ monitorInDb.createdAt shouldBe createdMonitor.createdAt
+ checkScheduler.getScheduledChecks().forOne { it.monitorId shouldBe createdMonitor.id }
+ }
+ }
+
+ `when`("there is already a monitor with the same name") {
+ val firstMonitor = MonitorCreateDto(
+ name = "test_monitor",
+ url = "https://valid-url.com",
+ uptimeCheckInterval = 6000,
+ enabled = true
+ )
+ val secondMonitor = MonitorCreateDto(
+ name = firstMonitor.name,
+ url = "https://valid-url2.com",
+ uptimeCheckInterval = 4000,
+ enabled = false
+ )
+ val firstCreatedMonitor = monitorClient.createMonitor(firstMonitor)
+ val secondRequest = HttpRequest.POST("/monitor", secondMonitor)
+ val secondResponse = shouldThrow {
+ client.toBlocking().exchange(secondRequest)
+ }
+
+ then("it should return a 409") {
+ secondResponse.status shouldBe HttpStatus.CONFLICT
+ val monitorsInDb = monitorRepository.fetchByName(firstCreatedMonitor.name)
+ monitorsInDb shouldHaveSize 1
+ checkScheduler.getScheduledChecks().forOne { it.monitorId shouldBe firstCreatedMonitor.id }
+ }
+ }
+
+ `when`("it is called with an invalid URL") {
+ val monitorToCreate = MonitorCreateDto(
+ name = "test_monitor",
+ url = "htt://invalid-url.com",
+ uptimeCheckInterval = 6000,
+ enabled = true
+ )
+ val request = HttpRequest.POST("/monitor", monitorToCreate)
+ val response = shouldThrow {
+ client.toBlocking().exchange(request)
+ }
+
+ then("it should return a 400") {
+ response.status shouldBe HttpStatus.BAD_REQUEST
+ exceptionToMessage(response) shouldContain "url: must match \"^(https?)"
+ }
+ }
+
+ `when`("it is called with an invalid uptime check interval") {
+ val monitorToCreate = MonitorCreateDto(
+ name = "test_monitor",
+ url = "https://valid-url.com",
+ uptimeCheckInterval = 59,
+ enabled = true
+ )
+ val request = HttpRequest.POST("/monitor", monitorToCreate)
+ val response = shouldThrow {
+ client.toBlocking().exchange(request)
+ }
+
+ then("it should return a 400") {
+ response.status shouldBe HttpStatus.BAD_REQUEST
+ exceptionToMessage(response) shouldContain "uptimeCheckInterval: must be greater than or equal to 60"
+ }
+ }
+ }
+
+ given("MonitorController's deleteMonitor() endpoint") {
+
+ `when`("it is called with an existing monitor ID") {
+ val monitorToCreate = MonitorCreateDto(
+ name = "test_monitor",
+ url = "https://valid-url.com",
+ uptimeCheckInterval = 6000,
+ enabled = true
+ )
+ val createdMonitor = monitorClient.createMonitor(monitorToCreate)
+ val deleteRequest = HttpRequest.DELETE("/monitor/${createdMonitor.id}")
+ val response = client.toBlocking().exchange(deleteRequest)
+ val monitorInDb = monitorRepository.findById(createdMonitor.id).toOption()
+
+ then("it should delete the monitor and also remove the checks of it") {
+ response.status shouldBe HttpStatus.NO_CONTENT
+ monitorInDb shouldBe Option.empty()
+
+ checkScheduler.getScheduledChecks().forNone { it.monitorId shouldBe createdMonitor.id }
+ }
+ }
+
+ `when`("it is called with a non existing monitor ID") {
+ val deleteRequest = HttpRequest.DELETE("/monitor/123232")
+ val response = shouldThrow {
+ client.toBlocking().exchange(deleteRequest)
+ }
+
+ then("it should return a 404") {
+ response.status shouldBe HttpStatus.NOT_FOUND
+ }
+ }
+ }
+
+ given("MonitorController's updateMonitor() endpoint") {
+
+ `when`("it is called with an existing monitor ID and a valid DTO to disable the monitor") {
+ val createDto = MonitorCreateDto(
+ name = "test_monitor",
+ url = "https://valid-url.com",
+ uptimeCheckInterval = 6000,
+ enabled = true
+ )
+ val createdMonitor = monitorClient.createMonitor(createDto)
+ checkScheduler.getScheduledChecks().forOne { it.monitorId shouldBe createdMonitor.id }
+
+ val updateDto = MonitorUpdateDto(
+ name = "updated_test_monitor",
+ url = "https://updated-url.com",
+ uptimeCheckInterval = 5000,
+ enabled = false
+ )
+ val updatedMonitor = monitorClient.updateMonitor(createdMonitor.id, updateDto)
+ val monitorInDb = monitorRepository.findById(createdMonitor.id)!!
+
+ then("it should update the monitor and remove the checks of it") {
+ monitorInDb.name shouldBe updatedMonitor.name
+ monitorInDb.url shouldBe updatedMonitor.url
+ monitorInDb.uptimeCheckInterval shouldBe updatedMonitor.uptimeCheckInterval
+ monitorInDb.enabled shouldBe updatedMonitor.enabled
+ monitorInDb.createdAt shouldBe createdMonitor.createdAt
+ monitorInDb.updatedAt shouldNotBe null
+
+ checkScheduler.getScheduledChecks().forNone { it.monitorId shouldBe createdMonitor.id }
+ }
+ }
+
+ `when`("it is called with an existing monitor ID and a valid DTO to enable the monitor") {
+ val createDto = MonitorCreateDto(
+ name = "test_monitor",
+ url = "https://valid-url.com",
+ uptimeCheckInterval = 6000,
+ enabled = false
+ )
+ val createdMonitor = monitorClient.createMonitor(createDto)
+ checkScheduler.getScheduledChecks().forNone { it.monitorId shouldBe createdMonitor.id }
+
+ val updateDto = MonitorUpdateDto(
+ name = null,
+ url = null,
+ uptimeCheckInterval = null,
+ enabled = true
+ )
+ val updatedMonitor = monitorClient.updateMonitor(createdMonitor.id, updateDto)
+ val monitorInDb = monitorRepository.findById(createdMonitor.id)!!
+
+ then("it should update the monitor and create the checks of it") {
+ monitorInDb.name shouldBe createdMonitor.name
+ monitorInDb.url shouldBe createdMonitor.url
+ monitorInDb.uptimeCheckInterval shouldBe createdMonitor.uptimeCheckInterval
+ monitorInDb.enabled shouldBe updatedMonitor.enabled
+ monitorInDb.createdAt shouldBe createdMonitor.createdAt
+ monitorInDb.updatedAt shouldNotBe null
+
+ checkScheduler.getScheduledChecks().forOne { it.monitorId shouldBe createdMonitor.id }
+ }
+ }
+
+ `when`("it is called with an existing monitor ID but there is an other monitor with the given name") {
+ val firstCreateDto = MonitorCreateDto(
+ name = "test_monitor",
+ url = "https://valid-url.com",
+ uptimeCheckInterval = 6000
+ )
+ val firstCreatedMonitor = monitorClient.createMonitor(firstCreateDto)
+ val secondCreateDto = MonitorCreateDto(
+ name = "test_monitor2",
+ url = "https://valid-url2.com",
+ uptimeCheckInterval = 6000
+ )
+ val secondCreatedMonitor = monitorClient.createMonitor(secondCreateDto)
+
+ val updateDto = MonitorUpdateDto(
+ name = secondCreatedMonitor.name,
+ url = null,
+ uptimeCheckInterval = null,
+ enabled = true
+ )
+ val updateRequest = HttpRequest.PATCH("/monitor/${firstCreatedMonitor.id}", updateDto)
+ val response = shouldThrow {
+ client.toBlocking().exchange(updateRequest)
+ }
+ val monitorInDb = monitorRepository.findById(firstCreatedMonitor.id)!!
+
+ then("it should return a 409") {
+ response.status shouldBe HttpStatus.CONFLICT
+ monitorInDb.name shouldBe firstCreatedMonitor.name
+ }
+ }
+
+ `when`("it is called with a non existing monitor ID") {
+ val updateDto = MonitorUpdateDto(null, null, null, null)
+ val updateRequest = HttpRequest.PATCH("/monitor/123232", updateDto)
+ val response = shouldThrow {
+ client.toBlocking().exchange(updateRequest)
+ }
+
+ then("it should return a 404") {
+ response.status shouldBe HttpStatus.NOT_FOUND
+ }
+ }
+ }
+ }
+
+ override fun afterTest(testCase: TestCase, result: TestResult) {
+ checkScheduler.removeAllChecks()
+ super.afterTest(testCase, result)
+ }
+}
diff --git a/src/test/kotlin/com/kuvaszuptime/kuvasz/handlers/DatabaseEventHandlerTest.kt b/src/test/kotlin/com/kuvaszuptime/kuvasz/handlers/DatabaseEventHandlerTest.kt
index a72cd3e..7d0ec06 100644
--- a/src/test/kotlin/com/kuvaszuptime/kuvasz/handlers/DatabaseEventHandlerTest.kt
+++ b/src/test/kotlin/com/kuvaszuptime/kuvasz/handlers/DatabaseEventHandlerTest.kt
@@ -3,9 +3,9 @@ package com.kuvaszuptime.kuvasz.handlers
import arrow.core.Option
import com.kuvaszuptime.kuvasz.DatabaseBehaviorSpec
import com.kuvaszuptime.kuvasz.enums.UptimeStatus
+import com.kuvaszuptime.kuvasz.mocks.createMonitor
import com.kuvaszuptime.kuvasz.models.MonitorDownEvent
import com.kuvaszuptime.kuvasz.models.MonitorUpEvent
-import com.kuvaszuptime.kuvasz.mocks.createMonitor
import com.kuvaszuptime.kuvasz.repositories.LatencyLogRepository
import com.kuvaszuptime.kuvasz.repositories.MonitorRepository
import com.kuvaszuptime.kuvasz.repositories.UptimeEventRepository
@@ -14,6 +14,7 @@ import com.kuvaszuptime.kuvasz.tables.LatencyLog.LATENCY_LOG
import com.kuvaszuptime.kuvasz.tables.UptimeEvent.UPTIME_EVENT
import io.kotest.core.test.TestCase
import io.kotest.core.test.TestResult
+import io.kotest.matchers.collections.shouldHaveSize
import io.kotest.matchers.shouldBe
import io.micronaut.http.HttpStatus
import io.micronaut.test.annotation.MicronautTest
@@ -118,7 +119,7 @@ class DatabaseEventHandlerTest(
expectedUptimeRecord.status shouldBe UptimeStatus.UP
expectedUptimeRecord.endedAt shouldBe null
- latencyRecords.size shouldBe 2
+ latencyRecords shouldHaveSize 2
latencyRecords[0].latency shouldBe firstEvent.latency
latencyRecords[1].latency shouldBe secondEvent.latency
}
diff --git a/src/test/kotlin/com/kuvaszuptime/kuvasz/mocks/TestData.kt b/src/test/kotlin/com/kuvaszuptime/kuvasz/mocks/TestData.kt
index 440f765..08cbe52 100644
--- a/src/test/kotlin/com/kuvaszuptime/kuvasz/mocks/TestData.kt
+++ b/src/test/kotlin/com/kuvaszuptime/kuvasz/mocks/TestData.kt
@@ -2,6 +2,7 @@ package com.kuvaszuptime.kuvasz.mocks
import com.kuvaszuptime.kuvasz.repositories.MonitorRepository
import com.kuvaszuptime.kuvasz.tables.pojos.MonitorPojo
+import com.kuvaszuptime.kuvasz.util.getCurrentTimestamp
fun createMonitor(
repository: MonitorRepository,
@@ -17,6 +18,7 @@ fun createMonitor(
.setUptimeCheckInterval(uptimeCheckInterval)
.setUrl(url)
.setEnabled(enabled)
+ .setCreatedAt(getCurrentTimestamp())
repository.insert(monitor)
return monitor
}
diff --git a/src/test/kotlin/com/kuvaszuptime/kuvasz/security/AuthenticationTest.kt b/src/test/kotlin/com/kuvaszuptime/kuvasz/security/AuthenticationTest.kt
index 05c5d27..4051417 100644
--- a/src/test/kotlin/com/kuvaszuptime/kuvasz/security/AuthenticationTest.kt
+++ b/src/test/kotlin/com/kuvaszuptime/kuvasz/security/AuthenticationTest.kt
@@ -8,6 +8,7 @@ import io.kotest.assertions.throwables.shouldThrow
import io.kotest.core.spec.style.BehaviorSpec
import io.kotest.matchers.shouldBe
import io.kotest.matchers.shouldNotBe
+import io.micronaut.context.annotation.Property
import io.micronaut.http.HttpRequest
import io.micronaut.http.HttpStatus
import io.micronaut.http.client.RxHttpClient
@@ -17,70 +18,70 @@ import io.micronaut.security.token.jwt.render.BearerAccessRefreshToken
import io.micronaut.test.annotation.MicronautTest
@MicronautTest
+@Property(name = "micronaut.security.enabled", value = "true")
class AuthenticationTest(
@Client("/") private val client: RxHttpClient,
private val authConfig: AdminAuthConfig
-) : BehaviorSpec() {
- init {
- given("a public endpoint") {
+) : BehaviorSpec({
- `when`("an anonymous user calls it") {
- val response = client.toBlocking().exchange("/health")
- then("it should return 200") {
- response.status shouldBe HttpStatus.OK
- }
+ given("a public endpoint") {
+
+ `when`("an anonymous user calls it") {
+ val response = client.toBlocking().exchange("/health")
+ then("it should return 200") {
+ response.status shouldBe HttpStatus.OK
}
}
- given("the login endpoint") {
+ }
+ given("the login endpoint") {
- `when`("the user provides the right credentials") {
- val credentials = generateCredentials(authConfig, valid = true)
- val request = HttpRequest.POST("/login", credentials)
- val response = client.toBlocking().exchange(request, BearerAccessRefreshToken::class.java)
- val token = response.body()!!
- val parsedJwt = JWTParser.parse(token.accessToken)
- then("it should return a signed access token for the given user") {
- response.status shouldBe HttpStatus.OK
- token.username shouldBe credentials.username
- token.accessToken shouldNotBe null
- (parsedJwt is SignedJWT) shouldBe true
- }
+ `when`("the user provides the right credentials") {
+ val credentials = generateCredentials(authConfig, valid = true)
+ val request = HttpRequest.POST("/login", credentials)
+ val response = client.toBlocking().exchange(request, BearerAccessRefreshToken::class.java)
+ val token = response.body()!!
+ val parsedJwt = JWTParser.parse(token.accessToken)
+ then("it should return a signed access token for the given user") {
+ response.status shouldBe HttpStatus.OK
+ token.username shouldBe credentials.username
+ token.accessToken shouldNotBe null
+ (parsedJwt is SignedJWT) shouldBe true
}
+ }
- `when`("a user provides bad credentials") {
- val credentials = generateCredentials(authConfig, valid = false)
- val request = HttpRequest.POST("/login", credentials)
- val exception = shouldThrow {
- client.toBlocking().exchange(request, BearerAccessRefreshToken::class.java)
- }
- then("it should return 401") {
- exception.status shouldBe HttpStatus.UNAUTHORIZED
- }
+ `when`("a user provides bad credentials") {
+ val credentials = generateCredentials(authConfig, valid = false)
+ val request = HttpRequest.POST("/login", credentials)
+ val exception = shouldThrow {
+ client.toBlocking().exchange(request, BearerAccessRefreshToken::class.java)
+ }
+ then("it should return 401") {
+ exception.status shouldBe HttpStatus.UNAUTHORIZED
}
}
- given("an authenticated endpoint") {
+ }
+ given("an authenticated endpoint") {
- `when`("an anonymous user calls it") {
- val exception = shouldThrow {
- client.toBlocking().exchange("/hello")
- }
- then("it should return 401") {
- exception.status shouldBe HttpStatus.UNAUTHORIZED
- }
+ `when`("an anonymous user calls it") {
+ val exception = shouldThrow {
+ client.toBlocking().exchange("/monitor")
+ }
+ then("it should return 401") {
+ exception.status shouldBe HttpStatus.UNAUTHORIZED
}
+ }
- `when`("a user provides the right credentials") {
- val credentials = generateCredentials(authConfig, valid = true)
- val loginRequest = HttpRequest.POST("/login", credentials)
- val loginResponse = client.toBlocking().exchange(loginRequest, BearerAccessRefreshToken::class.java)
- val token = loginResponse.body()!!
+ `when`("a user provides the right credentials") {
+ val credentials = generateCredentials(authConfig, valid = true)
+ val loginRequest = HttpRequest.POST("/login", credentials)
+ val loginResponse = client.toBlocking().exchange(loginRequest, BearerAccessRefreshToken::class.java)
+ val token = loginResponse.body()!!
- val request = HttpRequest.GET("/hello").bearerAuth(token.accessToken)
- val response = client.toBlocking().exchange(request)
- then("it should return 200") {
- response.status shouldBe HttpStatus.OK
- }
+ val request = HttpRequest.GET("/monitor").bearerAuth(token.accessToken)
+ val response = client.toBlocking().exchange(request)
+ then("it should return 200") {
+ response.status shouldBe HttpStatus.OK
}
}
}
-}
+})
diff --git a/src/test/kotlin/com/kuvaszuptime/kuvasz/services/CheckSchedulerTest.kt b/src/test/kotlin/com/kuvaszuptime/kuvasz/services/CheckSchedulerTest.kt
index 0e2a5f6..deb6112 100644
--- a/src/test/kotlin/com/kuvaszuptime/kuvasz/services/CheckSchedulerTest.kt
+++ b/src/test/kotlin/com/kuvaszuptime/kuvasz/services/CheckSchedulerTest.kt
@@ -4,6 +4,7 @@ import com.kuvaszuptime.kuvasz.DatabaseBehaviorSpec
import com.kuvaszuptime.kuvasz.mocks.createMonitor
import com.kuvaszuptime.kuvasz.models.CheckType
import com.kuvaszuptime.kuvasz.repositories.MonitorRepository
+import io.kotest.inspectors.forNone
import io.kotest.matchers.shouldBe
import io.kotest.matchers.shouldNotBe
import io.micronaut.test.annotation.MicronautTest
@@ -12,42 +13,40 @@ import io.micronaut.test.annotation.MicronautTest
class CheckSchedulerTest(
private val checkScheduler: CheckScheduler,
private val monitorRepository: MonitorRepository
-) : DatabaseBehaviorSpec() {
- init {
- given("the CheckScheduler service") {
- `when`("there is an enabled monitor in the database and initialize has been called") {
- val monitor = createMonitor(monitorRepository)
-
- checkScheduler.initialize()
-
- then("it should schedule the check for it") {
- val expectedCheck = checkScheduler.getScheduledChecks().find { it.monitorId == monitor.id }
- expectedCheck shouldNotBe null
- expectedCheck!!.checkType shouldBe CheckType.UPTIME
- expectedCheck.task.isCancelled shouldBe false
- expectedCheck.task.isDone shouldBe false
- }
+) : DatabaseBehaviorSpec({
+ given("the CheckScheduler service") {
+ `when`("there is an enabled monitor in the database and initialize has been called") {
+ val monitor = createMonitor(monitorRepository)
+
+ checkScheduler.initialize()
+
+ then("it should schedule the check for it") {
+ val expectedCheck = checkScheduler.getScheduledChecks().find { it.monitorId == monitor.id }
+ expectedCheck shouldNotBe null
+ expectedCheck!!.checkType shouldBe CheckType.UPTIME
+ expectedCheck.task.isCancelled shouldBe false
+ expectedCheck.task.isDone shouldBe false
}
+ }
- `when`("there is an enabled but unschedulable monitor in the database and initialize has been called") {
- val monitor = createMonitor(monitorRepository, id = 88888, uptimeCheckInterval = 0)
+ `when`("there is an enabled but unschedulable monitor in the database and initialize has been called") {
+ val monitor = createMonitor(monitorRepository, id = 88888, uptimeCheckInterval = 0)
- checkScheduler.initialize()
+ checkScheduler.initialize()
- then("it should not schedule the check for it") {
- checkScheduler.getScheduledChecks().any { it.monitorId == monitor.id } shouldBe false
- }
+ then("it should not schedule the check for it") {
+ checkScheduler.getScheduledChecks().forNone { it.monitorId shouldBe monitor.id }
}
+ }
- `when`("there is a disabled monitor in the database and initialize has been called") {
- val monitor = createMonitor(monitorRepository, id = 11111, enabled = false)
+ `when`("there is a disabled monitor in the database and initialize has been called") {
+ val monitor = createMonitor(monitorRepository, id = 11111, enabled = false)
- checkScheduler.initialize()
+ checkScheduler.initialize()
- then("it should not schedule the check for it") {
- checkScheduler.getScheduledChecks().any { it.monitorId == monitor.id } shouldBe false
- }
+ then("it should not schedule the check for it") {
+ checkScheduler.getScheduledChecks().forNone { it.monitorId shouldBe monitor.id }
}
}
}
-}
+})
diff --git a/src/test/kotlin/com/kuvaszuptime/kuvasz/services/UptimeCheckerTest.kt b/src/test/kotlin/com/kuvaszuptime/kuvasz/services/UptimeCheckerTest.kt
index ac52ca1..f6035a4 100644
--- a/src/test/kotlin/com/kuvaszuptime/kuvasz/services/UptimeCheckerTest.kt
+++ b/src/test/kotlin/com/kuvaszuptime/kuvasz/services/UptimeCheckerTest.kt
@@ -1,15 +1,17 @@
package com.kuvaszuptime.kuvasz.services
import com.kuvaszuptime.kuvasz.DatabaseBehaviorSpec
+import com.kuvaszuptime.kuvasz.mocks.createMonitor
import com.kuvaszuptime.kuvasz.models.MonitorDownEvent
import com.kuvaszuptime.kuvasz.models.MonitorUpEvent
import com.kuvaszuptime.kuvasz.models.RedirectEvent
-import com.kuvaszuptime.kuvasz.mocks.createMonitor
import com.kuvaszuptime.kuvasz.repositories.MonitorRepository
-import com.kuvaszuptime.kuvasz.util.toUri
import com.kuvaszuptime.kuvasz.testutils.toSubscriber
+import com.kuvaszuptime.kuvasz.util.toUri
import io.kotest.core.test.TestCase
import io.kotest.core.test.TestResult
+import io.kotest.matchers.comparables.shouldBeGreaterThan
+import io.kotest.matchers.comparables.shouldBeLessThan
import io.kotest.matchers.shouldBe
import io.micronaut.http.HttpHeaders
import io.micronaut.http.HttpStatus
@@ -63,6 +65,56 @@ class UptimeCheckerTest(
}
}
+ `when`("it checks a monitor that is DOWN but then it's UP again") {
+ val monitor = createMonitor(monitorRepository)
+ val monitorUpSubscriber = TestSubscriber()
+ val monitorDownSubscriber = TestSubscriber()
+ eventDispatcher.subscribeToMonitorUpEvents { it.toSubscriber(monitorUpSubscriber) }
+ eventDispatcher.subscribeToMonitorDownEvents { it.toSubscriber(monitorDownSubscriber) }
+ mockHttpResponse(uptimeCheckerSpy, HttpStatus.NOT_FOUND)
+
+ then("it should dispatch a MonitorDownEvent") {
+ uptimeCheckerSpy.check(monitor)
+ clearAllMocks()
+ mockHttpResponse(uptimeCheckerSpy, HttpStatus.OK)
+ uptimeCheckerSpy.check(monitor)
+
+ val expectedDownEvent = monitorDownSubscriber.values().first()
+ val expectedUpEvent = monitorUpSubscriber.values().first()
+
+ monitorDownSubscriber.valueCount() shouldBe 1
+ monitorUpSubscriber.valueCount() shouldBe 1
+ expectedDownEvent.monitor.id shouldBe monitor.id
+ expectedUpEvent.monitor.id shouldBe monitor.id
+ expectedDownEvent.dispatchedAt shouldBeLessThan expectedUpEvent.dispatchedAt
+ }
+ }
+
+ `when`("it checks a monitor that is UP but then it's DOWN again") {
+ val monitor = createMonitor(monitorRepository)
+ val monitorUpSubscriber = TestSubscriber()
+ val monitorDownSubscriber = TestSubscriber()
+ eventDispatcher.subscribeToMonitorUpEvents { it.toSubscriber(monitorUpSubscriber) }
+ eventDispatcher.subscribeToMonitorDownEvents { it.toSubscriber(monitorDownSubscriber) }
+ mockHttpResponse(uptimeCheckerSpy, HttpStatus.OK)
+
+ then("it should dispatch a MonitorDownEvent") {
+ uptimeCheckerSpy.check(monitor)
+ clearAllMocks()
+ mockHttpResponse(uptimeCheckerSpy, HttpStatus.NOT_FOUND)
+ uptimeCheckerSpy.check(monitor)
+
+ val expectedDownEvent = monitorDownSubscriber.values().first()
+ val expectedUpEvent = monitorUpSubscriber.values().first()
+
+ monitorDownSubscriber.valueCount() shouldBe 1
+ monitorUpSubscriber.valueCount() shouldBe 1
+ expectedDownEvent.monitor.id shouldBe monitor.id
+ expectedUpEvent.monitor.id shouldBe monitor.id
+ expectedDownEvent.dispatchedAt shouldBeGreaterThan expectedUpEvent.dispatchedAt
+ }
+ }
+
`when`("it checks a monitor that is redirected without a Location header") {
val monitor = createMonitor(monitorRepository)
val subscriber = TestSubscriber()
diff --git a/src/test/kotlin/com/kuvaszuptime/kuvasz/testutils/Matchers+.kt b/src/test/kotlin/com/kuvaszuptime/kuvasz/testutils/Matchers+.kt
new file mode 100644
index 0000000..1ac91e8
--- /dev/null
+++ b/src/test/kotlin/com/kuvaszuptime/kuvasz/testutils/Matchers+.kt
@@ -0,0 +1,7 @@
+package com.kuvaszuptime.kuvasz.testutils
+
+import io.kotest.matchers.booleans.shouldBeTrue
+import java.time.OffsetDateTime
+
+infix fun OffsetDateTime.shouldBe(otherOffsetDateTime: OffsetDateTime) =
+ isEqual(otherOffsetDateTime).shouldBeTrue()
diff --git a/src/test/kotlin/com/kuvaszuptime/kuvasz/testutils/MicronautTestUtils.kt b/src/test/kotlin/com/kuvaszuptime/kuvasz/testutils/MicronautTestUtils.kt
new file mode 100644
index 0000000..a7859ce
--- /dev/null
+++ b/src/test/kotlin/com/kuvaszuptime/kuvasz/testutils/MicronautTestUtils.kt
@@ -0,0 +1,26 @@
+package com.kuvaszuptime.kuvasz.testutils
+
+import io.micronaut.context.ApplicationContext
+import io.micronaut.http.client.RxHttpClient
+import io.micronaut.runtime.server.EmbeddedServer
+
+fun startTestApplication(mockBeans: List = emptyList(), withRealAuth: Boolean = false): EmbeddedServer =
+ ApplicationContext
+ .build()
+ .apply {
+ if (withRealAuth) {
+ properties(mapOf("micronaut.security.enabled" to "true"))
+ }
+ }
+ .build()
+ .apply {
+ mockBeans.forEach { registerSingleton(it) }
+ }
+ .start()
+ .getBean(EmbeddedServer::class.java)
+ .start()
+
+inline fun EmbeddedServer.getBean(): T = this.applicationContext.getBean(T::class.java)
+
+fun EmbeddedServer.getLowLevelClient(): RxHttpClient =
+ this.applicationContext.createBean(RxHttpClient::class.java, this.url)
diff --git a/src/test/resources/application-test.yml b/src/test/resources/application-test.yml
index 8d710d2..c98be4f 100644
--- a/src/test/resources/application-test.yml
+++ b/src/test/resources/application-test.yml
@@ -7,6 +7,7 @@ micronaut:
secret:
generator:
secret: testSecretItsVeryVerySecretSecret
+ enabled: false
---
app-config:
http-communication-logging: