diff --git a/Dockerfile b/Dockerfile index 2530361..f6924de 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,7 +5,7 @@ # Push image to remote # docker push kilemon/message-queue:0.1.5 -FROM gradle:7.5.1-jdk17-alpine as builder +FROM gradle:8.4.0-jdk17-alpine as builder WORKDIR /builder diff --git a/README.md b/README.md index 1625b75..4a4308b 100644 --- a/README.md +++ b/README.md @@ -4,237 +4,22 @@ ## Overview A message queue service, which can receive, hold and provide messages that are sent between services. -The message storage mechanisms supported are: -- In-memory (default) -- Redis (stand alone and sentinel support) -- SQL Database (MySQL, PostgreSQL) -- No SQL Database (Mongo) +A storage mechanism can be used to persist messages and sub queues can be restricted so only correctly provided credentials +can interact with such queues. -## Rest API Documentation - -The application provides a REST API to interact with the messages queued within the Multi Queue. -REST Documentation is provided as Swagger docs from the running application. -You can simply run the docker image: -> docker run -p8080:8080 kilemon/message-queue - -Once the image is running you can reach the Swagger documentation from the following endpoint: `http://localhost:8080/swagger-ui/index.html`. +**More detailed documentation can be found in the Wiki!** ## Quick Start -## In-Memory - -The `In-Memory` configuration is the default and requires no further configuration. -Steps to run the In-Memory Multi Queue is as follows: -- `docker run -p8080:8080 kilemon/message-queue` -- Once running the best endpoint to call at the moment is probably: `http://localhost:8080/queue/keys` - -If you really like you can provide an environment variable to the application to explicitly set the application into `In-Memory` mode: `MULTI_QUEUE_TYPE=IN_MEMORY`. - -## Redis - -The `Redis` setup requires some additional environmental configuration. -Firstly to set the application into `Redis` mode you need to provide the following environment variable with the appropriate value: `MULTI_QUEUE_TYPE=REDIS`. -Once this is set you will need to provide further environment variable in order to correctly configure the redis standalone or sentinel configuration (depending on your setup). - -### Redis Environment Properties - -Below are the optional and required properties for the `Redis` configuration. - -#### REDIS_USE_SENTINELS - -By default, this property is set to `false` (defaulting to direct connection to a single Redis instance). - -This flag indicates whether the `MultiQueue` should connect directly to the redis instance or connect via one or more sentinel instances. -If set to `true` the `MultiQueue` will create a sentinel pool connection instead of a direct connection to a single redis node. - -#### REDIS_PREFIX - -By default, this property is set to `""` the application will apply no prefix. - -For each defined "sub-queue" or "queue type", the application will create a single set entry into redis. This prefix can be used to remove/reduce the likelihood of any collisions if this is being used in an existing redis instance. - -The defined value will be used as a prefix used for all redis entry keys that the application will create. -E.g. if the initial value for the redis entry key is `my-key` and no prefix is defined the entries would be stored under `my-key`. -Using the same scenario if the prefix is `prefix` then the prefix and entry key will be concatenated and the resultant key would be `prefixmy-key`. - -#### REDIS_ENDPOINT - -By default, this property is set to `127.0.0.1` (the application will auto append the default redis port if it is not defined). - -The input endpoint string which is used for both standalone and the sentinel redis configurations. -This supports a comma separated list or single definition of a redis endpoint in the following formats: -`:,:,`. - -If you are using standalone and provide multiple endpoints, only the first will be used. - -#### REDIS_MASTER_NAME - -By default, this property is set to `mymaster`. -This is **REQUIRED** when `REDIS_USE_SENTINELS` is set to `true`. Is used to indicate the name of the redis master instance. - -### Example: - -An example of `Redis` configuration environment variables is below: -```yaml -environment: - - MULTI_QUEUE_TYPE=REDIS - - REDIS_USE_SENTINELS=true - - REDIS_PREFIX=my-prefix - - REDIS_ENDPOINT=sentinel1.com:5545,sentinel2.org:9980 - - REDIS_MASTER_NAME=not-my-master -``` - -## SQL Database - -The `SQL` mechanism requires some configuration too similarly to `Redis`. -To set the application into `SQL` mode you need to provide the following environment variable with the appropriate value: `MULTI_QUEUE_TYPE=SQL`. - -### SQL Environment Properties - -Below are the required properties for `SQL` configuration. - -#### spring.jpa.hibernate.ddl-auto - -Depending on your setup this is probably required initially but may change based on your usage needs. Since the application uses Hibernate to create and generate the database structure. -If the structure is not initialised, I recommend setting this property to `create` (E.g. `spring.jpa.hibernate.ddl-auto=create`). - -Please refer to https://docs.spring.io/spring-boot/docs/1.1.0.M1/reference/html/howto-database-initialization.html#howto-initialize-a-database-using-hibernate - -#### spring.autoconfigure.exclude - -***This property is required***. - -Just providing `spring.autoconfigure.exclude=` as one of the environment variables is required to force JPA to initialise correctly. -By default, it is suppressed to allow of more stream lined configuration of the other mechanisms. - -#### spring.datasource.url - -***This property is required***. - -This defines the database connection string that the application should connect to. E.g: `jdbc:mysql://localhost:3306/message-queue` +By default, the application will be store messages in memory and no queue restriction will be available. +To start the application you can use the following command to pull and run the latest version of the image: -#### spring.datasource.username +`docker run -p8080:8080 kilemon/message-queue` -***This property is required***. +Once running the best endpoint to call at the moment is probably: `http://localhost:8080/queue/healthcheck` -This is the username/account name used to access the database at the configured endpoint. +The application provides REST APIs to interact with the messages queued within the MultiQueue. -#### spring.datasource.password - -***This property is required***. - -This is the password used to access the database at the configured endpoint. - -### Example: -```yaml -environment: - - MULTI_QUEUE_TYPE=SQL - - spring.jpa.hibernate.ddl-auto=create - - spring.autoconfigure.exclude= - - spring.datasource.url=jdbc:postgresql://127.0.0.1:5432/postgres - - spring.datasource.username=postgres - - spring.datasource.password=5up3r5tR0nG! -``` - -## NO SQL (Mongo DB) - -The application can be set into `MONGO` mode to interface with a NoSQL database. Similarly to the others you can set the type with `MULTI_QUEUE_TYPE=MONGO`. - -### NoSQL Environment Properties - -You can either specify all properties individually via, `spring.data.mongodb.host`, `spring.data.mongodb.port`, `spring.data.mongodb.database`, `spring.data.mongodb.username`, `spring.data.mongodb.password`. -Or you can provide all together in a single property: `spring.data.mongodb.uri`. - -#### spring.data.mongodb.host - -***This property is required unless `spring.data.mongodb.uri` is provided***. - -This is the host that the mongo DB is accessible from. - -#### spring.data.mongodb.database - -***This property is required unless `spring.data.mongodb.uri` is provided***. - -This is the database that should be connected to and where the related documents will be created. - -#### spring.data.mongodb.username - -***This property is required unless `spring.data.mongodb.uri` is provided***. - -This is the username/account name used to access the database at the configured endpoint. - -#### spring.data.mongodb.password - -***This property is required unless `spring.data.mongodb.uri` is provided***. - -This is the password used to access the database at the configured endpoint. - -#### spring.data.mongodb.port - -***This property is required unless `spring.data.mongodb.uri` is provided***. - -The port that the mongo db has exposed. - -#### spring.data.mongodb.uri - -***This property is required unless the above properties are already provided***. - -The whole url can be provided in the following format: `mongodb://:@:/` for example: `mongodb://root:password@localhost:27107/messagequeue`. - -### Example: -***Note:** the use of `?authSource=admin` is to allow you to get up and running quickly, properly secured credentials and a non-admin account should always be used.* - -```yaml -environment: -- MULTI_QUEUE_TYPE=MONGO -- spring.data.mongodb.uri=mongodb://root:password@mongo:27017/messagequeue?authSource=admin -``` - ---- - -## HTTPS - -By default the `MessageQueue` does not have HTTPS enabled and is exposed on port `8080`. -To enable HTTPS you'll need to provide your own SSL certificate and extend the current version of the image hosted at: https://hub.docker.com/r/kilemon/message-queue. When extending this image you want to add your own SSL certificate into the container and take note of the generated file location as you'll need to reference it in the environment properties you provide to the `MessageQueue`. -**NOTE: You need to use version 0.1.9 or above of the `MessageQueue` image.** - -Below is an example Dockerfile that you could use to generate a self signed certificate. -Dockerfile: -``` -FROM kilemon/message-queue:latest - -# The generated cert will be placed at /messagequeue/keystore.p12 in the container (refer to path in docker compose file). -RUN ["keytool", "-genkeypair", "-alias", "sslcert", "-keyalg", "RSA", "-keysize", "4096", "-validity", "3650", "-dname", "CN=message-queue", "-keypass", "changeit", "-keystore", "keystore.p12", "-storeType", "PKCS12", "-storepass", "changeit"] - -EXPOSE 8443 - -ENTRYPOINT ["java", "-jar", "messagequeue.jar"] -``` - -Using docker compose you can reference and build this Dockerfile and pass in the appropriate parameters to enable HTTP on the `MessageQueue` application: - -docker-compose.yml: -```yaml -version: "3.9" -services: - queue: - container_name: queue - build: . - ports: - - "8443:8443" - environment: - MULTI_QUEUE_TYPE: IN_MEMORY - server.port: 8443 # The port set here must match the health check port below and the exposed port from the Dockerfile - server.ssl.enabled: true - server.ssl.key-store-type: PKCS12 - server.ssl.key-store: keystore.p12 # This path is relative to the `messagequeue.jar` location. The full location is /messagequeue/keystore.p12 for this example - server.ssl.key-store-password: changeit - healthcheck: # Example simple health check, disabling cert check for this example since it is self-signed - test: wget --no-check-certificate https://localhost:8443/queue/healthcheck - start_period: 3s - interval: 3s - timeout: 3s - retries: 5 -``` +## Rest API Documentation -Once this starts up you should be able to access the application using HTTPS on the exposed port `8443`. +REST Documentation is provided as Swagger docs from the running application. Once the image is running you can reach the Swagger documentation from the following endpoint: `http://localhost:8080/swagger-ui/index.html`. diff --git a/build.gradle.kts b/build.gradle.kts index ba346ef..ec792aa 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -10,7 +10,7 @@ plugins { group = "au.kilemon" // Make sure version matches version defined in MessageQueueApplication -version = "0.2.1" +version = "0.3.0" java.sourceCompatibility = JavaVersion.VERSION_17 repositories { @@ -46,12 +46,16 @@ dependencies { // https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-data-mongodb implementation("org.springframework.boot:spring-boot-starter-data-mongodb:3.1.3") + // JWT token + // https://mvnrepository.com/artifact/com.auth0/java-jwt + implementation("com.auth0:java-jwt:4.4.0") + // Test dependencies testImplementation("org.springframework.boot:spring-boot-starter-test:3.0.6") // Required to mock MultiQueue objects since they apparently override a final 'remove(Object)' method. testImplementation("org.mockito:mockito-inline:5.1.0") testImplementation("org.junit.jupiter:junit-jupiter-api:5.9.0") - testImplementation("org.testcontainers:testcontainers:1.17.5") + testImplementation("org.testcontainers:testcontainers:1.19.2") testImplementation("org.testcontainers:junit-jupiter:1.17.5") testImplementation(kotlin("test")) } diff --git a/src/main/kotlin/au/kilemon/messagequeue/MessageQueueApplication.kt b/src/main/kotlin/au/kilemon/messagequeue/MessageQueueApplication.kt index f783dd5..9d6913b 100644 --- a/src/main/kotlin/au/kilemon/messagequeue/MessageQueueApplication.kt +++ b/src/main/kotlin/au/kilemon/messagequeue/MessageQueueApplication.kt @@ -17,7 +17,7 @@ open class MessageQueueApplication /** * Application version number, make sure this matches what is defined in `build.gradle.kts`. */ - const val VERSION: String = "0.2.1" + const val VERSION: String = "0.3.0" } } diff --git a/src/main/kotlin/au/kilemon/messagequeue/authentication/AuthenticationMatrix.kt b/src/main/kotlin/au/kilemon/messagequeue/authentication/AuthenticationMatrix.kt new file mode 100644 index 0000000..fe29fdf --- /dev/null +++ b/src/main/kotlin/au/kilemon/messagequeue/authentication/AuthenticationMatrix.kt @@ -0,0 +1,56 @@ +package au.kilemon.messagequeue.authentication + +import com.fasterxml.jackson.annotation.JsonIgnore +import java.io.Serializable +import javax.persistence.* + +/** + * An object that holds subqueue authentication information. + * If a specific sub-queue is in restricted mode it will have a matching [AuthenticationMatrix] created which will + * be checked to verify if a specific sub-queue can be operated on. + * This object is used for `In-memory`, `SQL` and `Redis`. + * + * @author github.com/Kilemonn + */ +@Entity +@Table(name = AuthenticationMatrix.TABLE_NAME) +class AuthenticationMatrix(@Column(name = "subqueue", nullable = false) var subQueue: String): Serializable +{ + companion object + { + const val TABLE_NAME: String = "multiqueueauthenticationmatrix" + } + + @JsonIgnore + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + var id: Long? = null + + /** + * Required for MySQL. + */ + constructor() : this("") + + /** + * Overriding to only include specific properties when checking if messages are equal. + * This checks the following are equal in order to return `true`: + * - subQueue + */ + override fun equals(other: Any?): Boolean + { + if (other == null || other !is AuthenticationMatrix) + { + return false + } + + return other.subQueue == this.subQueue + } + + /** + * Only performs a hashcode on the properties checked in [AuthenticationMatrix.equals]. + */ + override fun hashCode(): Int + { + return subQueue.hashCode() + } +} diff --git a/src/main/kotlin/au/kilemon/messagequeue/authentication/AuthenticationMatrixDocument.kt b/src/main/kotlin/au/kilemon/messagequeue/authentication/AuthenticationMatrixDocument.kt new file mode 100644 index 0000000..6514ac4 --- /dev/null +++ b/src/main/kotlin/au/kilemon/messagequeue/authentication/AuthenticationMatrixDocument.kt @@ -0,0 +1,24 @@ +package au.kilemon.messagequeue.authentication + +import com.fasterxml.jackson.annotation.JsonIgnore +import org.springframework.data.annotation.Id +import org.springframework.data.mongodb.core.mapping.Document + +/** + * A holder object for the restricted sub-queues. Refer to [AuthenticationMatrix]. + * This is used only for `Mongo`. + * + * @author github.com/Kilemonn + */ +@Document(value = AuthenticationMatrixDocument.DOCUMENT_NAME) +class AuthenticationMatrixDocument(var subQueue: String) +{ + companion object + { + const val DOCUMENT_NAME: String = "multiqueueauthenticationmatrix" + } + + @JsonIgnore + @Id + var id: Long? = null +} diff --git a/src/main/kotlin/au/kilemon/messagequeue/authentication/RestrictionMode.kt b/src/main/kotlin/au/kilemon/messagequeue/authentication/RestrictionMode.kt new file mode 100644 index 0000000..d647367 --- /dev/null +++ b/src/main/kotlin/au/kilemon/messagequeue/authentication/RestrictionMode.kt @@ -0,0 +1,30 @@ +package au.kilemon.messagequeue.authentication + +/** + * An enum class used to represent the different types of `MultiQueueAuthentication`. + * This will drive whether authentication is available and for which sub-queues. + * + * @author github.com/Kilemonn + */ +enum class RestrictionMode +{ + /** + * This is the default, which enforces no authentication on any sub-queue, messages can be enqueued and dequeued + * as required by any called without any form of authentication. + */ + NONE, + + /** + * This is a hybrid mode where sub-queues can be created without authentication, but other sub-queues can + * be created with it (if they do not already exist). + * Any sub-queue created with authentication will not be accessible without a token, sub-queues created without a + * token will continue to be accessible without one. + */ + HYBRID, + + /** + * This is a restricted mode that forces any sub-queue to be pre-created and a token will be given before messages + * can be stored or accessed in any sub-queue. + */ + RESTRICTED; +} diff --git a/src/main/kotlin/au/kilemon/messagequeue/authentication/authenticator/MultiQueueAuthenticator.kt b/src/main/kotlin/au/kilemon/messagequeue/authentication/authenticator/MultiQueueAuthenticator.kt new file mode 100644 index 0000000..6cf05aa --- /dev/null +++ b/src/main/kotlin/au/kilemon/messagequeue/authentication/authenticator/MultiQueueAuthenticator.kt @@ -0,0 +1,249 @@ +package au.kilemon.messagequeue.authentication.authenticator + +import au.kilemon.messagequeue.authentication.RestrictionMode +import au.kilemon.messagequeue.authentication.exception.MultiQueueAuthorisationException +import au.kilemon.messagequeue.filter.JwtAuthenticationFilter +import au.kilemon.messagequeue.logging.HasLogger +import org.slf4j.Logger +import org.springframework.beans.factory.annotation.Autowired + +/** + * The base Authenticator class. This is responsible to tracking which sub-queues are marked as restricted and + * maintaining this underlying collection within the specified storage medium. + * + * @author github.com/Kilemonn + */ +abstract class MultiQueueAuthenticator: HasLogger +{ + abstract override val LOG: Logger + + @Autowired + private lateinit var restrictionMode: RestrictionMode + + /** + * @return [restrictionMode] + */ + fun getRestrictionMode(): RestrictionMode + { + return restrictionMode + } + + /** + * Used only for tests to update the set [RestrictionMode]. + * + * @param restrictionMode the new [RestrictionMode] to set + */ + fun setRestrictionMode(restrictionMode: RestrictionMode) + { + this.restrictionMode = restrictionMode + } + + /** + * Used to return a list of completed reserved sub-queue identifiers that can never be used. Even when + * [RestrictionMode.NONE] is being used. + * + * @return list of sub-queue identifiers that cannot be used + */ + open fun getReservedSubQueues(): Set + { + return setOf() + } + + /** + * Determines whether based on the currently set [getRestrictionMode] and the provided [subQueue] and + * [JwtAuthenticationFilter.getSubQueue] to determine if the user is able to interact with the requested sub-queue. + * + * @param subQueue the sub-queue identifier that is being requested access to + * @param throwException `true` to throw an exception if the [subQueue] cannot be accessed, otherwise the return + * value can be used + * @return returns `true` if the [subQueue] can be accessed, otherwise `false` + * @throws MultiQueueAuthorisationException if there is a mis-matching token OR no token provided. Or the sub-queue + * is not in restricted mode when it should be + */ + @Throws(MultiQueueAuthorisationException::class) + fun canAccessSubQueue(subQueue: String, throwException: Boolean = true): Boolean + { + if (getReservedSubQueues().contains(subQueue)) + { + if (throwException) + { + throw MultiQueueAuthorisationException(subQueue, getRestrictionMode()) + } + return false + } + + if (isInNoneMode()) + { + return true + } + else if (isInHybridMode()) + { + if (isRestricted(subQueue)) + { + if (JwtAuthenticationFilter.getSubQueue() == subQueue) + { + return true + } + } + else + { + // If we are in hybrid mode and the sub-queue is not restricted we should let it pass + return true + } + } + else if (isInRestrictedMode()) + { + if (isRestricted(subQueue) && JwtAuthenticationFilter.getSubQueue() == subQueue) + { + return true + } + } + + if (throwException) + { + throw MultiQueueAuthorisationException(subQueue, getRestrictionMode()) + } + return false + } + + /** + * Indicates whether [restrictionMode] is set to [RestrictionMode.NONE]. + */ + fun isInNoneMode(): Boolean + { + return getRestrictionMode() == RestrictionMode.NONE + } + + /** + * Indicates whether [restrictionMode] is set to [RestrictionMode.HYBRID]. + */ + fun isInHybridMode(): Boolean + { + return getRestrictionMode() == RestrictionMode.HYBRID + } + + /** + * Indicates whether [restrictionMode] is set to [RestrictionMode.RESTRICTED]. + */ + fun isInRestrictedMode(): Boolean + { + return getRestrictionMode() == RestrictionMode.RESTRICTED + } + + /** + * Will determine if the requested [subQueue] identifier is being treated as a restricted queue or not. + * + * @param subQueue the sub-queue to check whether it is restricted or not + * @return if [isInNoneMode] will always return `false`, otherwise delegates to [isRestrictedInternal] + */ + fun isRestricted(subQueue: String): Boolean + { + return if (isInNoneMode()) + { + false + } + else + { + isRestrictedInternal(subQueue) + } + } + + /** + * Defers to the child class to implement this. It should look up in the appropriate storage medium to determine + * whether the provided [subQueue] is restricted or not. + * + * @param subQueue the sub-queue to check whether it is restricted or not + * @return `true` if this sub-queue is in restricted mode, otherwise `false` + */ + abstract fun isRestrictedInternal(subQueue: String): Boolean + + + /** + * Add the provided [subQueue] identifier as a restricted sub-queue. + * This will delegate to [addRestrictedEntryInternal]. + * + * @param subQueue the sub-queue identifier to make restricted + * @return `true` if the sub-queue identifier was added to the restriction set, otherwise `false` if there was + * no underlying change made. If [isInNoneMode] is set this will always return `false`. + */ + fun addRestrictedEntry(subQueue: String): Boolean + { + if (isInNoneMode()) + { + LOG.trace("Skipping adding restricted entry for [{}] since the restriction mode is set to [{}].", subQueue, getRestrictionMode()) + return false + } + else + { + return if (isRestricted(subQueue)) + { + LOG.trace("Restriction for sub-queue [{}] was not increased as it is already restricted.", subQueue) + false + } + else + { + LOG.info("Adding restriction to sub-queue [{}].", subQueue) + addRestrictedEntryInternal(subQueue) + true + } + } + } + + /** + * Add the provided [subQueue] identifier as a restricted sub-queue. + * + * @param subQueue the sub-queue identifier to make restricted + */ + abstract fun addRestrictedEntryInternal(subQueue: String) + + /** + * Remove the provided [subQueue] from being a restricted sub-queue. + * This will delegate to [removeRestrictionInternal]. + * + * @param subQueue the sub-queue identifier that will no longer be treated as restricted + * @return `true` if there was a restriction that was removed because of this call, otherwise `false`. If + * [isInNoneMode] this will always return `false` + */ + fun removeRestriction(subQueue: String): Boolean + { + return if (isInNoneMode()) + { + LOG.trace("Skipping removing restricted entry for [{}] since the restriction mode is set to [{}].", subQueue, getRestrictionMode()) + false + } + else + { + return if (isRestricted(subQueue)) + { + LOG.info("Removing restriction to sub-queue [{}].", subQueue) + removeRestrictionInternal(subQueue) + } + else + { + LOG.trace("Restriction for sub-queue [{}] was not removed as it is currently unrestricted.", subQueue) + false + } + + } + } + + /** + * Remove the provided [subQueue] from being a restricted sub-queue. + * + * @param subQueue the sub-queue identifier that will no longer be treated as restricted + * @return `true` if the identifier is no longer marked as restricted, otherwise `false` + */ + abstract fun removeRestrictionInternal(subQueue: String): Boolean + + /** + * @return the underlying [Set] of sub-queue identifiers that are marked as restricted + */ + abstract fun getRestrictedSubQueueIdentifiers(): Set + + /** + * Clear the underlying restriction storage entries. (This is mainly used for testing). + * + * @return the amount of sub-queue restrictions that were cleared + */ + abstract fun clearRestrictedSubQueues(): Long +} diff --git a/src/main/kotlin/au/kilemon/messagequeue/authentication/authenticator/cache/redis/RedisAuthenticator.kt b/src/main/kotlin/au/kilemon/messagequeue/authentication/authenticator/cache/redis/RedisAuthenticator.kt new file mode 100644 index 0000000..2f1f33f --- /dev/null +++ b/src/main/kotlin/au/kilemon/messagequeue/authentication/authenticator/cache/redis/RedisAuthenticator.kt @@ -0,0 +1,74 @@ +package au.kilemon.messagequeue.authentication.authenticator.cache.redis + +import au.kilemon.messagequeue.authentication.AuthenticationMatrix +import au.kilemon.messagequeue.authentication.authenticator.MultiQueueAuthenticator +import org.slf4j.Logger +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.data.redis.core.RedisTemplate +import java.util.stream.Collectors + +/** + * A [MultiQueueAuthenticator] implementation using Redis as the storage mechanism for the restricted + * sub-queue identifiers. + * + * @author github.com/Kilemonn + */ +class RedisAuthenticator: MultiQueueAuthenticator() +{ + companion object + { + const val RESTRICTED_KEY = AuthenticationMatrix.TABLE_NAME + "_restricted" + } + + override val LOG: Logger = this.initialiseLogger() + + @Autowired + lateinit var redisTemplate: RedisTemplate + + /** + * Overriding to completely remove all access to the [RESTRICTED_KEY]. + * Only if [isInNoneMode] returns `false`. + */ + override fun getReservedSubQueues(): Set + { + if (!isInNoneMode()) + { + return setOf(RESTRICTED_KEY) + } + return setOf() + } + + private fun getMembersSet(): Set + { + return redisTemplate.opsForSet().members(RESTRICTED_KEY) ?: HashSet() + } + + override fun isRestrictedInternal(subQueue: String): Boolean + { + return getMembersSet().contains(AuthenticationMatrix(subQueue)) ?: false + } + + override fun addRestrictedEntryInternal(subQueue: String) + { + redisTemplate.opsForSet().add(RESTRICTED_KEY, AuthenticationMatrix(subQueue)) + } + + override fun removeRestrictionInternal(subQueue: String): Boolean + { + val result = redisTemplate.opsForSet().remove(RESTRICTED_KEY, AuthenticationMatrix(subQueue)) + return result != null && result > 0 + } + + override fun getRestrictedSubQueueIdentifiers(): Set + { + return getMembersSet().stream().map { authMatrix -> authMatrix.subQueue }.collect(Collectors.toSet()) + } + + override fun clearRestrictedSubQueues(): Long + { + val members = getMembersSet() + val existingMembersSize = members.size.toLong() + redisTemplate.delete(RESTRICTED_KEY) + return existingMembersSize + } +} diff --git a/src/main/kotlin/au/kilemon/messagequeue/authentication/authenticator/inmemory/InMemoryAuthenticator.kt b/src/main/kotlin/au/kilemon/messagequeue/authentication/authenticator/inmemory/InMemoryAuthenticator.kt new file mode 100644 index 0000000..8dcd51d --- /dev/null +++ b/src/main/kotlin/au/kilemon/messagequeue/authentication/authenticator/inmemory/InMemoryAuthenticator.kt @@ -0,0 +1,44 @@ +package au.kilemon.messagequeue.authentication.authenticator.inmemory + +import au.kilemon.messagequeue.authentication.authenticator.MultiQueueAuthenticator +import org.slf4j.Logger + +/** + * A [MultiQueueAuthenticator] implementation using an in-memory set as the storage mechanism for the restricted + * sub-queue identifiers. + * + * @author github.com/Kilemonn + */ +class InMemoryAuthenticator: MultiQueueAuthenticator() +{ + override val LOG: Logger = this.initialiseLogger() + + private val restrictedSubQueues = HashSet() + + override fun isRestrictedInternal(subQueue: String): Boolean + { + return restrictedSubQueues.contains(subQueue) + } + + override fun addRestrictedEntryInternal(subQueue: String) + { + restrictedSubQueues.add(subQueue) + } + + override fun removeRestrictionInternal(subQueue: String): Boolean + { + return restrictedSubQueues.remove(subQueue) + } + + override fun getRestrictedSubQueueIdentifiers(): Set + { + return restrictedSubQueues.toSet() + } + + override fun clearRestrictedSubQueues(): Long + { + val existingEntriesSize = restrictedSubQueues.size.toLong() + restrictedSubQueues.clear() + return existingEntriesSize + } +} diff --git a/src/main/kotlin/au/kilemon/messagequeue/authentication/authenticator/nosql/mongo/MongoAuthenticationMatrixRepository.kt b/src/main/kotlin/au/kilemon/messagequeue/authentication/authenticator/nosql/mongo/MongoAuthenticationMatrixRepository.kt new file mode 100644 index 0000000..f852b30 --- /dev/null +++ b/src/main/kotlin/au/kilemon/messagequeue/authentication/authenticator/nosql/mongo/MongoAuthenticationMatrixRepository.kt @@ -0,0 +1,22 @@ +package au.kilemon.messagequeue.authentication.authenticator.nosql.mongo + +import au.kilemon.messagequeue.authentication.AuthenticationMatrixDocument +import org.springframework.data.mongodb.repository.MongoRepository +import java.util.* + +/** + * A [MongoRepository] for [AuthenticationMatrixDocument] which stores which sub-queues are under restricted access. + * + * @author github.com/Kilemonn + */ +interface MongoAuthenticationMatrixRepository: MongoRepository +{ + fun findBySubQueue(subQueue: String): List + + /** + * Get the entry with the largest ID. + * + * @return the [AuthenticationMatrixDocument] with the largest ID, otherwise [Optional.empty] + */ + fun findTopByOrderByIdDesc(): Optional +} diff --git a/src/main/kotlin/au/kilemon/messagequeue/authentication/authenticator/nosql/mongo/MongoAuthenticator.kt b/src/main/kotlin/au/kilemon/messagequeue/authentication/authenticator/nosql/mongo/MongoAuthenticator.kt new file mode 100644 index 0000000..9d5d77f --- /dev/null +++ b/src/main/kotlin/au/kilemon/messagequeue/authentication/authenticator/nosql/mongo/MongoAuthenticator.kt @@ -0,0 +1,72 @@ +package au.kilemon.messagequeue.authentication.authenticator.nosql.mongo + +import au.kilemon.messagequeue.authentication.AuthenticationMatrixDocument +import au.kilemon.messagequeue.authentication.authenticator.MultiQueueAuthenticator +import org.slf4j.Logger +import org.springframework.beans.factory.annotation.Autowired +import java.util.stream.Collectors + +/** + * A [MultiQueueAuthenticator] implementation using MongoDB as the storage mechanism for the restricted sub-queue + * identifiers. + * + * @author github.com/Kilemonn + */ +class MongoAuthenticator: MultiQueueAuthenticator() +{ + override val LOG: Logger = this.initialiseLogger() + + @Autowired + lateinit var authenticationMatrixRepository: MongoAuthenticationMatrixRepository + + override fun isRestrictedInternal(subQueue: String): Boolean + { + val entries = authenticationMatrixRepository.findBySubQueue(subQueue) + return entries.isNotEmpty() + } + + /** + * Since mongodb does not manage self generated IDs we need to generate the ID ourselves when creating a new entry. + */ + private fun getNextQueueIndex(): Long + { + val largestIdMessage = authenticationMatrixRepository.findTopByOrderByIdDesc() + return if (largestIdMessage.isPresent) + { + largestIdMessage.get().id?.plus(1) ?: 1 + } + else + { + 1 + } + } + + override fun addRestrictedEntryInternal(subQueue: String) + { + val authMatrix = AuthenticationMatrixDocument(subQueue) + authMatrix.id = getNextQueueIndex() + authenticationMatrixRepository.save(authMatrix) + } + + override fun removeRestrictionInternal(subQueue: String): Boolean + { + val entries = authenticationMatrixRepository.findBySubQueue(subQueue) + val entriesExist = entries.isNotEmpty() + entries.forEach { entry -> authenticationMatrixRepository.delete(entry) } + + return entriesExist + } + + override fun getRestrictedSubQueueIdentifiers(): Set + { + return authenticationMatrixRepository.findAll().stream().map { authMatrix -> authMatrix.subQueue } + .collect(Collectors.toSet()) + } + + override fun clearRestrictedSubQueues(): Long + { + val existingEntriesCount = authenticationMatrixRepository.count() + authenticationMatrixRepository.deleteAll() + return existingEntriesCount + } +} diff --git a/src/main/kotlin/au/kilemon/messagequeue/authentication/authenticator/sql/SqlAuthenticationMatrixRepository.kt b/src/main/kotlin/au/kilemon/messagequeue/authentication/authenticator/sql/SqlAuthenticationMatrixRepository.kt new file mode 100644 index 0000000..d1035d5 --- /dev/null +++ b/src/main/kotlin/au/kilemon/messagequeue/authentication/authenticator/sql/SqlAuthenticationMatrixRepository.kt @@ -0,0 +1,18 @@ +package au.kilemon.messagequeue.authentication.authenticator.sql + +import au.kilemon.messagequeue.authentication.AuthenticationMatrix +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository + +/** + * A [JpaRepository] specific for [AuthenticationMatrix] and queries made against them. + * + * Reference: https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#jpa.query-methods.query-creation + * + * @author github.com/Kilemonn + */ +@Repository +interface SqlAuthenticationMatrixRepository: JpaRepository +{ + fun findBySubQueue(subQueue: String): List +} diff --git a/src/main/kotlin/au/kilemon/messagequeue/authentication/authenticator/sql/SqlAuthenticator.kt b/src/main/kotlin/au/kilemon/messagequeue/authentication/authenticator/sql/SqlAuthenticator.kt new file mode 100644 index 0000000..d59500d --- /dev/null +++ b/src/main/kotlin/au/kilemon/messagequeue/authentication/authenticator/sql/SqlAuthenticator.kt @@ -0,0 +1,55 @@ +package au.kilemon.messagequeue.authentication.authenticator.sql + +import au.kilemon.messagequeue.authentication.AuthenticationMatrix +import au.kilemon.messagequeue.authentication.authenticator.MultiQueueAuthenticator +import org.slf4j.Logger +import org.springframework.beans.factory.annotation.Autowired +import java.util.stream.Collectors + +/** + * A [MultiQueueAuthenticator] implementation using SQL as the storage mechanism for the restricted sub-queue + * identifiers. + * + * @author github.com/Kilemonn + */ +class SqlAuthenticator: MultiQueueAuthenticator() +{ + override val LOG: Logger = this.initialiseLogger() + + @Autowired + lateinit var authenticationMatrixRepository: SqlAuthenticationMatrixRepository + + override fun isRestrictedInternal(subQueue: String): Boolean + { + val entries = authenticationMatrixRepository.findBySubQueue(subQueue) + return entries.isNotEmpty() + } + + override fun addRestrictedEntryInternal(subQueue: String) + { + val authMatrix = AuthenticationMatrix(subQueue) + authenticationMatrixRepository.save(authMatrix) + } + + override fun removeRestrictionInternal(subQueue: String): Boolean + { + val entries = authenticationMatrixRepository.findBySubQueue(subQueue) + val entriesExist = entries.isNotEmpty() + entries.forEach { entry -> authenticationMatrixRepository.delete(entry) } + + return entriesExist + } + + override fun getRestrictedSubQueueIdentifiers(): Set + { + return authenticationMatrixRepository.findAll().stream().map { authMatrix -> authMatrix.subQueue } + .collect(Collectors.toSet()) + } + + override fun clearRestrictedSubQueues(): Long + { + val existingEntriesCount = authenticationMatrixRepository.count() + authenticationMatrixRepository.deleteAll() + return existingEntriesCount + } +} diff --git a/src/main/kotlin/au/kilemon/messagequeue/authentication/exception/MultiQueueAuthenticationException.kt b/src/main/kotlin/au/kilemon/messagequeue/authentication/exception/MultiQueueAuthenticationException.kt new file mode 100644 index 0000000..b56fe91 --- /dev/null +++ b/src/main/kotlin/au/kilemon/messagequeue/authentication/exception/MultiQueueAuthenticationException.kt @@ -0,0 +1,15 @@ +package au.kilemon.messagequeue.authentication.exception + +/** + * An authentication exception used when the provided token is invalid when an action is requested to be performed on a + * sub-queue. + * + * @author github.com/Kilemonn + */ +class MultiQueueAuthenticationException : Exception(ERROR_MESSAGE) +{ + companion object + { + const val ERROR_MESSAGE = "Invalid token provided." + } +} diff --git a/src/main/kotlin/au/kilemon/messagequeue/authentication/exception/MultiQueueAuthorisationException.kt b/src/main/kotlin/au/kilemon/messagequeue/authentication/exception/MultiQueueAuthorisationException.kt new file mode 100644 index 0000000..04589a7 --- /dev/null +++ b/src/main/kotlin/au/kilemon/messagequeue/authentication/exception/MultiQueueAuthorisationException.kt @@ -0,0 +1,18 @@ +package au.kilemon.messagequeue.authentication.exception + +import au.kilemon.messagequeue.authentication.RestrictionMode + +/** + * An authorisation exception used when the `MultiQueueAuthenticator` does not allow the caller to perform the + * requested action on the requested sub-queue. Due to either due to a mismatch of the provided token and the sub-queue + * that is being request OR in hybrid mode if a token is required but not provided. + * + * @author github.com/Kilemonn + */ +class MultiQueueAuthorisationException(subQueue: String, restrictionMode: RestrictionMode) : Exception(String.format(MESSAGE_FORMAT, subQueue, restrictionMode)) +{ + companion object + { + const val MESSAGE_FORMAT = "Unable to access sub-queue [%s]. Using mode [%s]." + } +} diff --git a/src/main/kotlin/au/kilemon/messagequeue/authentication/token/JwtTokenProvider.kt b/src/main/kotlin/au/kilemon/messagequeue/authentication/token/JwtTokenProvider.kt new file mode 100644 index 0000000..d5a31d4 --- /dev/null +++ b/src/main/kotlin/au/kilemon/messagequeue/authentication/token/JwtTokenProvider.kt @@ -0,0 +1,153 @@ +package au.kilemon.messagequeue.authentication.token + +import au.kilemon.messagequeue.logging.HasLogger +import au.kilemon.messagequeue.settings.MessageQueueSettings +import com.auth0.jwt.JWT +import com.auth0.jwt.JWTVerifier +import com.auth0.jwt.algorithms.Algorithm +import com.auth0.jwt.exceptions.JWTCreationException +import com.auth0.jwt.exceptions.JWTVerificationException +import com.auth0.jwt.interfaces.DecodedJWT +import org.slf4j.Logger +import org.springframework.beans.factory.annotation.Value +import java.security.SecureRandom +import java.util.* + +/** + * A class to handle Jwt token creation and verification. + */ +class JwtTokenProvider: HasLogger +{ + companion object + { + const val ISSUER = "kilemon/message-queue" + + const val SUB_QUEUE_CLAIM = "Sub-Queue-Identifier" + } + + override val LOG: Logger = this.initialiseLogger() + + private var jwtVerifier: JWTVerifier? = null + + private var algorithm: Algorithm? = null + + @Value("\${${MessageQueueSettings.ACCESS_TOKEN_KEY}:\"\"}") + var tokenKey: String = "" + private set + + /** + * Lazily initialise and get the [Algorithm] using [Algorithm.HMAC512] and a random key. + * + * @return the shared [Algorithm] instance + */ + private fun getAlgorithm(): Algorithm + { + if (algorithm == null) + { + algorithm = Algorithm.HMAC512(getOrGenerateKey(tokenKey)) + } + return algorithm!! + } + + /** + * Get the key to be used for Jwt token generation and verification. + * + * @return If a value is provided via [MessageQueueSettings.ACCESS_TOKEN_KEY] then we will use it if it is + * not blank. Otherwise, a randomly generated a byte array is returned + */ + fun getOrGenerateKey(key: String): ByteArray + { + return if (key.isNotBlank()) + { + LOG.info("Using provided key in property [{}] as the HMAC512 token generation and verification key.", MessageQueueSettings.ACCESS_TOKEN_KEY) + key.toByteArray() + } + else + { + LOG.info("No HMAC512 key provided in property [{}] for token generation and verification key. Generating a new random key.", MessageQueueSettings.ACCESS_TOKEN_KEY) + val random = SecureRandom() + val bytes = ByteArray(128) + random.nextBytes(bytes) + bytes + } + } + + /** + * Lazily initialise and get the [JWTVerifier]. + * + * @return the shared [JWTVerifier] instance + */ + private fun getVerifier(): JWTVerifier + { + if (jwtVerifier == null) + { + jwtVerifier = JWT.require(getAlgorithm()).withIssuer(ISSUER).build() + } + return jwtVerifier!! + } + + /** + * Verify the provided [String] token and decode it and return it as a [DecodedJWT]. + * + * @param token the token to parse and decode + * @return the [DecodedJWT] from the provided [String] token, otherwise [Optional.empty] + */ + fun verifyTokenForSubQueue(token: String): Optional + { + try + { + return Optional.ofNullable(getVerifier().verify(token)) + } + catch (ex: JWTVerificationException) + { + // Invalid signature/claims + LOG.error("Failed to verify provided token.", ex) + } + return Optional.empty() + } + + /** + * Create a new token. + * + * @param subQueue will be embedded in the generated token as a claim with key [SUB_QUEUE_CLAIM] + * @param expiryInMinutes the generated token expiry in minutes, if `null` this is not added the token will be valid + * indefinitely + * @return the generated token as a [String] otherwise [Optional.empty] if there was a problem generating the token + */ + fun createTokenForSubQueue(subQueue: String, expiryInMinutes: Long? = null): Optional + { + try + { + val token = createTokenInternal(subQueue, expiryInMinutes) + + LOG.info("Creating new token for sub-queue [{}] with expiry of [{}] minutes.", subQueue, expiryInMinutes) + return Optional.ofNullable(token) + } + catch (ex: JWTCreationException) + { + // Invalid Signing configuration / Couldn't convert Claims. + LOG.error(String.format("Failed to create requested token for sub-queue identifier [%s].", subQueue), ex) + } + return Optional.empty() + } + + /** + * The internal method for creating the token. + * This is needed for tests instead of using PowerMock or something. + * + * @param subQueue will be embedded in the generated token as a claim with key [SUB_QUEUE_CLAIM] + * @param expiryInMinutes the generated token expiry in minutes, if `null` this is not added the token will be valid + * indefinitely + * @return the generated token as a [String] + * @throws JWTCreationException if there is a problem creating the token + */ + fun createTokenInternal(subQueue: String, expiryInMinutes: Long? = null): String + { + val builder = JWT.create().withIssuer(ISSUER).withClaim(SUB_QUEUE_CLAIM, subQueue) + if (expiryInMinutes != null) + { + builder.withExpiresAt(Date(Date().time + (expiryInMinutes * 60 * 1000))) + } + return builder.sign(getAlgorithm()) + } +} diff --git a/src/main/kotlin/au/kilemon/messagequeue/configuration/QueueConfiguration.kt b/src/main/kotlin/au/kilemon/messagequeue/configuration/QueueConfiguration.kt index 7641f06..71b7af9 100644 --- a/src/main/kotlin/au/kilemon/messagequeue/configuration/QueueConfiguration.kt +++ b/src/main/kotlin/au/kilemon/messagequeue/configuration/QueueConfiguration.kt @@ -1,6 +1,13 @@ package au.kilemon.messagequeue.configuration import au.kilemon.messagequeue.MessageQueueApplication +import au.kilemon.messagequeue.authentication.RestrictionMode +import au.kilemon.messagequeue.authentication.authenticator.MultiQueueAuthenticator +import au.kilemon.messagequeue.authentication.authenticator.cache.redis.RedisAuthenticator +import au.kilemon.messagequeue.authentication.authenticator.inmemory.InMemoryAuthenticator +import au.kilemon.messagequeue.authentication.authenticator.nosql.mongo.MongoAuthenticator +import au.kilemon.messagequeue.authentication.authenticator.sql.SqlAuthenticator +import au.kilemon.messagequeue.authentication.token.JwtTokenProvider import au.kilemon.messagequeue.logging.HasLogger import au.kilemon.messagequeue.logging.Messages import au.kilemon.messagequeue.message.QueueMessage @@ -10,8 +17,7 @@ import au.kilemon.messagequeue.queue.inmemory.InMemoryMultiQueue import au.kilemon.messagequeue.queue.nosql.mongo.MongoMultiQueue import au.kilemon.messagequeue.queue.sql.SqlMultiQueue import au.kilemon.messagequeue.settings.MessageQueueSettings -import au.kilemon.messagequeue.settings.MultiQueueType -import lombok.Generated +import au.kilemon.messagequeue.settings.StorageMedium import org.slf4j.Logger import org.springframework.beans.factory.annotation.Autowired import org.springframework.context.annotation.Bean @@ -30,26 +36,20 @@ import java.util.* @Configuration class QueueConfiguration : HasLogger { - override val LOG: Logger = initialiseLogger() + override val LOG: Logger = this.initialiseLogger() @Autowired - @get:Generated - @set:Generated - lateinit var messageQueueSettings: MessageQueueSettings + private lateinit var messageQueueSettings: MessageQueueSettings @Autowired - @get:Generated - @set:Generated - lateinit var messageSource: ReloadableResourceBundleMessageSource + private lateinit var messageSource: ReloadableResourceBundleMessageSource @Autowired @Lazy - @get:Generated - @set:Generated - lateinit var redisTemplate: RedisTemplate + private lateinit var redisTemplate: RedisTemplate /** - * Initialise the [MultiQueue] [Bean] based on the [MessageQueueSettings.multiQueueType]. + * Initialise the [MultiQueue] [Bean] based on the [MessageQueueSettings.storageMedium]. */ @Bean open fun getMultiQueue(): MultiQueue @@ -58,20 +58,75 @@ class QueueConfiguration : HasLogger // Default to in-memory var queue: MultiQueue = InMemoryMultiQueue() - when (messageQueueSettings.multiQueueType) { - MultiQueueType.REDIS.toString() -> { + when (messageQueueSettings.storageMedium.uppercase()) { + StorageMedium.REDIS.toString() -> { queue = RedisMultiQueue(messageQueueSettings.redisPrefix, redisTemplate) } - MultiQueueType.SQL.toString() -> { + StorageMedium.SQL.toString() -> { queue = SqlMultiQueue() } - MultiQueueType.MONGO.toString() -> { + StorageMedium.MONGO.toString() -> { queue = MongoMultiQueue() } } - LOG.info("Initialising [{}] queue as the [{}] is set to [{}].", queue::class.java.name, MessageQueueSettings.MULTI_QUEUE_TYPE, messageQueueSettings.multiQueueType) + LOG.info("Initialising [{}] queue as the [{}] is set to [{}].", queue::class.java.name, MessageQueueSettings.STORAGE_MEDIUM, messageQueueSettings.storageMedium) return queue } + + /** + * Initialise the [RestrictionMode] which drives how sub-queues are accessed and created. + */ + @Bean + open fun getRestrictionMode(): RestrictionMode + { + var restrictionMode = RestrictionMode.NONE + + if (messageQueueSettings.restrictionMode.isNotBlank()) + { + try + { + restrictionMode = RestrictionMode.valueOf(messageQueueSettings.restrictionMode.uppercase()) + } + catch (ex: Exception) + { + LOG.warn("Unable to initialise appropriate restriction mode with provided value [{}], falling back to default [{}].", messageQueueSettings.restrictionMode, RestrictionMode.NONE, ex) + } + } + + LOG.info("Using [{}] restriction mode as the [{}] is set to [{}].", restrictionMode, MessageQueueSettings.RESTRICTION_MODE, messageQueueSettings.restrictionMode) + + return restrictionMode + } + + /** + * Initialise the [MultiQueueAuthenticator] [Bean] based on the [MessageQueueSettings.storageMedium]. + */ + @Bean + open fun getMultiQueueAuthenticator(): MultiQueueAuthenticator + { + var authenticator: MultiQueueAuthenticator = InMemoryAuthenticator() + when (messageQueueSettings.storageMedium.uppercase()) { + StorageMedium.REDIS.toString() -> { + authenticator = RedisAuthenticator() + } + StorageMedium.SQL.toString() -> { + authenticator = SqlAuthenticator() + } + StorageMedium.MONGO.toString() -> { + authenticator = MongoAuthenticator() + } + } + + LOG.info("Initialising [{}] authenticator as the [{}] is set to [{}].", authenticator::class.java.name, MessageQueueSettings.STORAGE_MEDIUM, messageQueueSettings.storageMedium) + + return authenticator + } + + @Bean + open fun getJwtTokenProvider(): JwtTokenProvider + { + return JwtTokenProvider() + } } diff --git a/src/main/kotlin/au/kilemon/messagequeue/configuration/cache/redis/RedisConfiguration.kt b/src/main/kotlin/au/kilemon/messagequeue/configuration/cache/redis/RedisConfiguration.kt index d45e39b..8f79c27 100644 --- a/src/main/kotlin/au/kilemon/messagequeue/configuration/cache/redis/RedisConfiguration.kt +++ b/src/main/kotlin/au/kilemon/messagequeue/configuration/cache/redis/RedisConfiguration.kt @@ -1,9 +1,9 @@ package au.kilemon.messagequeue.configuration.cache.redis +import au.kilemon.messagequeue.authentication.AuthenticationMatrix import au.kilemon.messagequeue.logging.HasLogger import au.kilemon.messagequeue.message.QueueMessage import au.kilemon.messagequeue.settings.MessageQueueSettings -import lombok.Generated import org.slf4j.Logger import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty @@ -27,7 +27,7 @@ import java.net.InetSocketAddress @Configuration class RedisConfiguration: HasLogger { - override val LOG: Logger = initialiseLogger() + override val LOG: Logger = this.initialiseLogger() companion object { @@ -79,9 +79,7 @@ class RedisConfiguration: HasLogger } @Autowired - @get:Generated - @set:Generated - lateinit var messageQueueSettings: MessageQueueSettings + private lateinit var messageQueueSettings: MessageQueueSettings /** * Create the [RedisConnectionFactory] based on the loaded configuration. @@ -93,7 +91,7 @@ class RedisConfiguration: HasLogger * @return the created [RedisConnectionFactory] based on the configured [MessageQueueSettings] */ @Bean - @ConditionalOnProperty(name=[MessageQueueSettings.MULTI_QUEUE_TYPE], havingValue="REDIS") + @ConditionalOnProperty(name=[MessageQueueSettings.STORAGE_MEDIUM], havingValue="REDIS") fun getConnectionFactory(): RedisConnectionFactory { return if (messageQueueSettings.redisUseSentinels.toBoolean()) @@ -156,17 +154,32 @@ class RedisConfiguration: HasLogger } /** - * Create the [RedisTemplate] from [getConnectionFactory]. + * Create a [RedisTemplate] to interact with [QueueMessage] from the [getConnectionFactory]. * * @return the [RedisTemplate] used to interface with the [RedisTemplate] cache. */ @Bean - @ConditionalOnProperty(name=[MessageQueueSettings.MULTI_QUEUE_TYPE], havingValue="REDIS") - fun getRedisTemplate(): RedisTemplate + @ConditionalOnProperty(name=[MessageQueueSettings.STORAGE_MEDIUM], havingValue="REDIS") + fun getQueueRedisTemplate(): RedisTemplate { val template = RedisTemplate() template.connectionFactory = getConnectionFactory() template.keySerializer = StringRedisSerializer() return template } + + /** + * Create a [RedisTemplate] to interact with [AuthenticationMatrix] from the [getConnectionFactory]. + * + * @return the [RedisTemplate] used to interface with the [RedisTemplate] cache. + */ + @Bean + @ConditionalOnProperty(name=[MessageQueueSettings.STORAGE_MEDIUM], havingValue="REDIS") + fun getAuthMatrixRedisTemplate(): RedisTemplate + { + val template = RedisTemplate() + template.connectionFactory = getConnectionFactory() + template.keySerializer = StringRedisSerializer() + return template + } } diff --git a/src/main/kotlin/au/kilemon/messagequeue/filter/CorrelationIdFilter.kt b/src/main/kotlin/au/kilemon/messagequeue/filter/CorrelationIdFilter.kt index 6647cf5..20f170d 100644 --- a/src/main/kotlin/au/kilemon/messagequeue/filter/CorrelationIdFilter.kt +++ b/src/main/kotlin/au/kilemon/messagequeue/filter/CorrelationIdFilter.kt @@ -30,7 +30,7 @@ class CorrelationIdFilter: OncePerRequestFilter(), HasLogger const val CORRELATION_ID = "correlationId" } - override val LOG: Logger = initialiseLogger() + override val LOG: Logger = this.initialiseLogger() override fun doFilterInternal(request: HttpServletRequest, response: HttpServletResponse, filterChain: FilterChain) { @@ -42,7 +42,7 @@ class CorrelationIdFilter: OncePerRequestFilter(), HasLogger } finally { - MDC.clear() + MDC.remove(CORRELATION_ID) } } diff --git a/src/main/kotlin/au/kilemon/messagequeue/filter/JwtAuthenticationFilter.kt b/src/main/kotlin/au/kilemon/messagequeue/filter/JwtAuthenticationFilter.kt new file mode 100644 index 0000000..f7c221c --- /dev/null +++ b/src/main/kotlin/au/kilemon/messagequeue/filter/JwtAuthenticationFilter.kt @@ -0,0 +1,216 @@ +package au.kilemon.messagequeue.filter + +import au.kilemon.messagequeue.logging.HasLogger +import au.kilemon.messagequeue.authentication.RestrictionMode +import au.kilemon.messagequeue.authentication.authenticator.MultiQueueAuthenticator +import au.kilemon.messagequeue.authentication.exception.MultiQueueAuthenticationException +import au.kilemon.messagequeue.authentication.token.JwtTokenProvider +import au.kilemon.messagequeue.rest.controller.AuthController +import au.kilemon.messagequeue.rest.controller.MessageQueueController +import au.kilemon.messagequeue.rest.controller.SettingsController +import org.slf4j.Logger +import org.slf4j.MDC +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.annotation.Lazy +import org.springframework.core.annotation.Order +import org.springframework.http.HttpMethod +import org.springframework.stereotype.Component +import org.springframework.web.filter.OncePerRequestFilter +import org.springframework.web.servlet.HandlerExceptionResolver +import java.util.* +import javax.servlet.FilterChain +import javax.servlet.http.HttpServletRequest +import javax.servlet.http.HttpServletResponse + +/** + * A filter responsible for verifying provided Jwt tokens when sub-queues are being accessed. + * + * @author github.com/Kilemonn + */ +@Component +@Order(2) +class JwtAuthenticationFilter: OncePerRequestFilter(), HasLogger +{ + companion object + { + const val AUTHORIZATION_HEADER = "Authorization" + const val BEARER_HEADER_VALUE = "Bearer " + + const val SUB_QUEUE = "Sub-Queue" + + /** + * Gets the stored [SUB_QUEUE] from the [MDC]. + * This can be null if no valid token is provided. + */ + fun getSubQueue(): String? + { + return MDC.get(SUB_QUEUE) + } + } + + override val LOG: Logger = this.initialiseLogger() + + @Autowired + private lateinit var authenticator: MultiQueueAuthenticator + + @Autowired + private lateinit var jwtTokenProvider: JwtTokenProvider + + @Autowired + @Lazy + private lateinit var handlerExceptionResolver: HandlerExceptionResolver + + /** + * Perform appropriate validation of the [AUTHORIZATION_HEADER] if it is provided. + * Depending on the set [RestrictionMode] will determine how this filter handles a request. + * - [RestrictionMode.NONE] all requests will be allowed, whether they provide a valid token or not. + * - [RestrictionMode.HYBRID] all requests will be allowed and the provided token [SUB_QUEUE] parameter + * will be set if a token is provided. It's up to the lower level controllers to determine how they need to react + * in accordance with the active [MultiQueueAuthenticator]. + * - [RestrictionMode.RESTRICTED] a token is required and if not valid the request will be rejected + * here and a [MultiQueueAuthenticationException] will be thrown + * + * @throws MultiQueueAuthenticationException if [RestrictionMode] is set to + * [RestrictionMode.RESTRICTED] and an invalid token OR NO token is provided + */ + override fun doFilterInternal(request: HttpServletRequest, response: HttpServletResponse, filterChain: FilterChain) + { + try + { + val subQueue = getSubQueueInTokenFromHeaders(request) + setSubQueue(subQueue) + + if (canSkipTokenVerification(request)) + { + LOG.trace("Allowed access to path [{}] as it does not require authentication.", request.requestURI) + filterChain.doFilter(request, response) + return + } + + if (authenticator.isInNoneMode()) + { + LOG.trace("Allowed access as authentication is set to [{}].", RestrictionMode.NONE) + filterChain.doFilter(request, response) + } + else if (authenticator.isInHybridMode()) + { + LOG.trace("Allowing request through for lower layer to check as authentication is set to [{}].", RestrictionMode.NONE) + filterChain.doFilter(request, response) + } + else if (authenticator.isInRestrictedMode()) + { + if (tokenIsPresentAndQueueIsRestricted(subQueue, authenticator)) + { + LOG.trace("Accepted request for sub-queue [{}].", subQueue.get()) + filterChain.doFilter(request, response) + } + else + { + val token = if (subQueue.isPresent) subQueue.get() else "null" + LOG.error("Failed to manipulate sub-queue [{}] with provided token as the authentication level is set to [{}].", token, authenticator.getRestrictionMode()) + handlerExceptionResolver.resolveException(request, response, null, MultiQueueAuthenticationException()) + return + } + } + } + finally + { + MDC.remove(SUB_QUEUE) + } + } + + /** + * Verify the requested URI requires authentication or not. + * If the URL is not in the no auth list, then it will require authentication. + * + * @param request the incoming request to verify the path of + * @return `true` if the provided path starts with an auth required prefix, otherwise `false` + */ + fun canSkipTokenVerification(request: HttpServletRequest): Boolean + { + val noTokenCheckEndpoints = listOf( + Pair(HttpMethod.GET, "${MessageQueueController.MESSAGE_QUEUE_BASE_PATH}${MessageQueueController.ENDPOINT_HEALTH_CHECK}"), + Pair(HttpMethod.GET, "${MessageQueueController.MESSAGE_QUEUE_BASE_PATH}${MessageQueueController.ENDPOINT_KEYS}"), + Pair(HttpMethod.GET, "${MessageQueueController.MESSAGE_QUEUE_BASE_PATH}${MessageQueueController.ENDPOINT_OWNERS}"), + Pair(HttpMethod.GET, AuthController.AUTH_PATH), + Pair(HttpMethod.POST, AuthController.AUTH_PATH), + Pair(HttpMethod.GET, SettingsController.SETTINGS_PATH) + ) + + return noTokenCheckEndpoints + .filter { authRequiredUrlPrefix -> authRequiredUrlPrefix.first.toString() == request.method } + .any { authRequiredUrlPrefix -> request.requestURI.startsWith(authRequiredUrlPrefix.second) } + } + + /** + * Check if the token is set and it is restricted sub-queue identifier. + * + * @return `true` if the provided [Optional.isPresent] and the call to [MultiQueueAuthenticator.isRestricted] is + * `true` using the provided [Optional] value. Otherwise, returns `false` + */ + fun tokenIsPresentAndQueueIsRestricted(subQueue: Optional, multiQueueAuthenticator: MultiQueueAuthenticator): Boolean + { + return subQueue.isPresent && multiQueueAuthenticator.isRestricted(subQueue.get()) + } + + /** + * Set the provided [Optional][String] into the [MDC] as [JwtAuthenticationFilter.SUB_QUEUE] if it is not [Optional.empty]. + * + * @param subQueue an optional sub-queue identifier, if it is not [Optional.empty] it will be placed into the [MDC] + */ + fun setSubQueue(subQueue: Optional) + { + if (subQueue.isPresent) + { + LOG.trace("Setting resolved sub-queue from token into request context [{}].", subQueue.get()) + MDC.put(SUB_QUEUE, subQueue.get()) + } + } + + /** + * Get the value of the provided [request] for the [AUTHORIZATION_HEADER] header. + * + * @param request the request to retrieve the [AUTHORIZATION_HEADER] from + * @return the [AUTHORIZATION_HEADER] header value wrapped as an [Optional], otherwise [Optional.empty] + */ + fun getSubQueueInTokenFromHeaders(request: HttpServletRequest): Optional + { + val authHeader = request.getHeader(AUTHORIZATION_HEADER) + if (authHeader != null) + { + return if (authHeader.startsWith(BEARER_HEADER_VALUE)) + { + val removeBearer = authHeader.substring(BEARER_HEADER_VALUE.length) + isValidJwtToken(removeBearer) + } + else + { + LOG.error("Provided [{}] header did not have prefix [{}].", AUTHORIZATION_HEADER, BEARER_HEADER_VALUE) + Optional.empty() + } + } + return Optional.empty() + } + + /** + * Delegate to the [JwtTokenProvider] to determine if the provided token is valid. + * + * @param jwtToken the token to verify + * @return the [String] for the sub-queue that this token is able to access, otherwise [Optional.empty] if there was + * a problem with parsing the claim + * @throws [MultiQueueAuthenticationException] if there is an issue verifying the token + */ + @Throws(MultiQueueAuthenticationException::class) + fun isValidJwtToken(jwtToken: String): Optional + { + val result = jwtTokenProvider.verifyTokenForSubQueue(jwtToken) + if (result.isPresent) + { + return Optional.ofNullable(result.get().getClaim(JwtTokenProvider.SUB_QUEUE_CLAIM).asString()) + } + else + { + throw MultiQueueAuthenticationException() + } + } +} diff --git a/src/main/kotlin/au/kilemon/messagequeue/logging/HasLogger.kt b/src/main/kotlin/au/kilemon/messagequeue/logging/HasLogger.kt index 4b511c2..0b67d72 100644 --- a/src/main/kotlin/au/kilemon/messagequeue/logging/HasLogger.kt +++ b/src/main/kotlin/au/kilemon/messagequeue/logging/HasLogger.kt @@ -10,7 +10,7 @@ import org.slf4j.LoggerFactory * ``` * class ALogger: HasLogger * { - * override val LOG: Logger = initialiseLogger() + * override val LOG: Logger = this.initialiseLogger() * ... * fun function() * { diff --git a/src/main/kotlin/au/kilemon/messagequeue/logging/LoggingConfiguration.kt b/src/main/kotlin/au/kilemon/messagequeue/logging/LoggingConfiguration.kt index 5d8b3b0..9defbf2 100644 --- a/src/main/kotlin/au/kilemon/messagequeue/logging/LoggingConfiguration.kt +++ b/src/main/kotlin/au/kilemon/messagequeue/logging/LoggingConfiguration.kt @@ -14,7 +14,7 @@ import java.util.* @Configuration class LoggingConfiguration : HasLogger { - override val LOG: Logger = initialiseLogger() + override val LOG: Logger = this.initialiseLogger() companion object { diff --git a/src/main/kotlin/au/kilemon/messagequeue/message/QueueMessage.kt b/src/main/kotlin/au/kilemon/messagequeue/message/QueueMessage.kt index 3c15fdc..a395a86 100644 --- a/src/main/kotlin/au/kilemon/messagequeue/message/QueueMessage.kt +++ b/src/main/kotlin/au/kilemon/messagequeue/message/QueueMessage.kt @@ -8,7 +8,7 @@ import javax.persistence.* /** * A base [QueueMessage] object which will wrap any object that is placed into the `MultiQueue`. - * This object wraps a [Any] type `T` which is the payload to be stored in the queue. (This is actually a [Serializable] but causes issues in initialisation + * This object wraps an [Any] which is the payload to be stored in the queue. (This is actually a [Serializable] but causes issues in initialisation * if the type is an `interface`. This needs to be [Serializable] if you want to use it with `Redis` or anything else). * * This is used for `InMemory`, `Redis` and `SQL` queues. @@ -17,7 +17,7 @@ import javax.persistence.* */ @Entity @Table(name = QueueMessage.TABLE_NAME) // TODO: Schema configuration schema = "\${${MessageQueueSettings.SQL_SCHEMA}:${MessageQueueSettings.SQL_SCHEMA_DEFAULT}}") -class QueueMessage(payload: Any?, @Column(nullable = false) var type: String, @Column(name = "assignedto") var assignedTo: String? = null): Serializable +class QueueMessage(payload: Any?, @Column(name = "subqueue", nullable = false) var subQueue: String, @Column(name = "assignedto") var assignedTo: String? = null): Serializable { companion object { @@ -52,7 +52,7 @@ class QueueMessage(payload: Any?, @Column(nullable = false) var type: String, @C constructor(queueMessageDocument: QueueMessageDocument) : this() { - this.type = queueMessageDocument.type + this.subQueue = queueMessageDocument.subQueue this.uuid = queueMessageDocument.uuid this.id = queueMessageDocument.id this.payload = queueMessageDocument.payload @@ -94,7 +94,7 @@ class QueueMessage(payload: Any?, @Column(nullable = false) var type: String, @C { // Create a temporary object since the object is edited in place if we are using the in-memory queue val newMessage = QueueMessage() - newMessage.type = type + newMessage.subQueue = subQueue newMessage.assignedTo = assignedTo newMessage.uuid = uuid newMessage.payload = "***" // Mark as stars to indicate that it is there but not returned @@ -107,9 +107,10 @@ class QueueMessage(payload: Any?, @Column(nullable = false) var type: String, @C /** * Overriding to only include specific properties when checking if messages are equal. * This checks the following are equal in order to return `true`: - * - UUID - * - payload value - * - type + * - [uuid] + * - [payload] + * - [payloadBytes] + * - [subQueue] */ override fun equals(other: Any?): Boolean { @@ -120,7 +121,7 @@ class QueueMessage(payload: Any?, @Column(nullable = false) var type: String, @C return other.uuid == this.uuid && (this.payload == other.payload || this.payloadBytes.contentEquals(other.payloadBytes)) - && this.type == other.type + && this.subQueue == other.subQueue } /** @@ -129,7 +130,7 @@ class QueueMessage(payload: Any?, @Column(nullable = false) var type: String, @C override fun hashCode(): Int { var result = payload?.hashCode() ?: 0 result = 31 * result + (payloadBytes?.hashCode() ?: 0) - result = 31 * result + type.hashCode() + result = 31 * result + subQueue.hashCode() result = 31 * result + uuid.hashCode() return result } diff --git a/src/main/kotlin/au/kilemon/messagequeue/message/QueueMessageDocument.kt b/src/main/kotlin/au/kilemon/messagequeue/message/QueueMessageDocument.kt index 749deed..1a9c39f 100644 --- a/src/main/kotlin/au/kilemon/messagequeue/message/QueueMessageDocument.kt +++ b/src/main/kotlin/au/kilemon/messagequeue/message/QueueMessageDocument.kt @@ -3,9 +3,7 @@ package au.kilemon.messagequeue.message import com.fasterxml.jackson.annotation.JsonIgnore import org.springframework.data.annotation.Id import org.springframework.data.mongodb.core.mapping.Document -import org.springframework.util.SerializationUtils import java.util.* -import javax.persistence.* /** * This is used for `No-SQL` queues. @@ -13,7 +11,7 @@ import javax.persistence.* * @author github.com/Kilemonn */ @Document(value = QueueMessageDocument.DOCUMENT_NAME) -class QueueMessageDocument(var payload: Any?, var type: String, var assignedTo: String? = null) +class QueueMessageDocument(var payload: Any?, var subQueue: String, var assignedTo: String? = null) { companion object { @@ -34,7 +32,7 @@ class QueueMessageDocument(var payload: Any?, var type: String, var assignedTo: constructor(queueMessage: QueueMessage) : this() { val resolvedQueueMessage = queueMessage.resolvePayloadObject() - this.type = resolvedQueueMessage.type + this.subQueue = resolvedQueueMessage.subQueue this.uuid = resolvedQueueMessage.uuid this.id = resolvedQueueMessage.id this.payload = resolvedQueueMessage.payload diff --git a/src/main/kotlin/au/kilemon/messagequeue/queue/MultiQueue.kt b/src/main/kotlin/au/kilemon/messagequeue/queue/MultiQueue.kt index d54d12d..bd93c55 100644 --- a/src/main/kotlin/au/kilemon/messagequeue/queue/MultiQueue.kt +++ b/src/main/kotlin/au/kilemon/messagequeue/queue/MultiQueue.kt @@ -1,21 +1,23 @@ package au.kilemon.messagequeue.queue -import au.kilemon.messagequeue.queue.exception.DuplicateMessageException +import au.kilemon.messagequeue.authentication.authenticator.MultiQueueAuthenticator import au.kilemon.messagequeue.logging.HasLogger import au.kilemon.messagequeue.message.QueueMessage +import au.kilemon.messagequeue.queue.exception.DuplicateMessageException import au.kilemon.messagequeue.queue.exception.HealthCheckFailureException +import au.kilemon.messagequeue.queue.exception.IllegalSubQueueIdentifierException import au.kilemon.messagequeue.queue.exception.MessageUpdateException +import lombok.Generated import org.slf4j.Logger +import org.springframework.beans.factory.annotation.Autowired import java.util.* -import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentLinkedQueue import java.util.stream.Collectors -import kotlin.collections.HashMap -import kotlin.jvm.Throws +import kotlin.collections.HashSet /** * A [MultiQueue] base class, which extends [Queue]. - * It contains various extra methods for interfacing with the [MultiQueue] using the [String] as a queue type identifier + * It contains various extra methods for interfacing with the [MultiQueue] using the [String] as a sub-queue identifier * to manipulate the appropriate underlying [Queue]s. * * @author github.com/Kilemonn @@ -29,9 +31,14 @@ abstract class MultiQueue: Queue, HasLogger abstract override val LOG: Logger + @Autowired + @get:Generated + @set:Generated + protected lateinit var multiQueueAuthenticator: MultiQueueAuthenticator + /** * Get the underlying size of the [MultiQueue]. - * This is done by summing the length of each [getQueueForType] for each key in [keys]. + * This is done by summing the length of each [getSubQueue] for each key in [keys]. * * This is to allow the underlying storage to be the source of truth instead of any temporary counters, since the underlying storage could * change at any timeout without direct interaction from the [MultiQueue]. @@ -40,7 +47,7 @@ abstract class MultiQueue: Queue, HasLogger get() { var internalSize = 0 keys(false).forEach { key -> - internalSize += getQueueForType(key).size + internalSize += getSubQueue(key).size } return internalSize } @@ -59,7 +66,7 @@ abstract class MultiQueue: Queue, HasLogger * * @return the current value of the index before it was incremented */ - abstract fun getNextQueueIndex(queueType: String): Optional + abstract fun getNextSubQueueIndex(subQueue: String): Optional /** * A wrapper for the [MultiQueue.persistMessageInternal] so this method can be synchronised. @@ -89,109 +96,129 @@ abstract class MultiQueue: Queue, HasLogger @Throws(MessageUpdateException::class) abstract fun persistMessageInternal(message: QueueMessage) + /** + * Retrieves or creates a new [Queue] of [QueueMessage] for the provided [subQueue]. + * If the underlying [Queue] does not exist for the provided [subQueue] then a new [Queue] will + * be created. + * + * **This method should not be called directly, please use [getSubQueue]** + * + * @param subQueue the identifier of the sub-queue [Queue] + * @return the [Queue] matching the provided [String] + */ + abstract fun getSubQueueInternal(subQueue: String): Queue + /** * Retrieves or creates a new [Queue] of type [QueueMessage] for the provided [String]. * If the underlying [Queue] does not exist for the provided [String] then a new [Queue] will - * be created and stored in the [ConcurrentHashMap] under the provided [String]. + * be created. * - * @param queueType the identifier of the sub-queue [Queue] + * @param subQueue the identifier of the sub-queue [Queue] * @return the [Queue] matching the provided [String] + * @throws IllegalSubQueueIdentifierException If the provided [subQueue] is part of the [MultiQueueAuthenticator.getReservedSubQueues] */ - abstract fun getQueueForType(queueType: String): Queue + @Throws(IllegalSubQueueIdentifierException::class) + fun getSubQueue(subQueue: String): Queue + { + if (!multiQueueAuthenticator.isInNoneMode() && multiQueueAuthenticator.getReservedSubQueues().contains(subQueue)) + { + throw IllegalSubQueueIdentifierException(subQueue) + } + + return getSubQueueInternal(subQueue) + } /** - * Retrieves only assigned messages in the sub-queue for the provided [queueType]. + * Retrieves only assigned messages in the sub-queue for the provided [subQueue]. * - * By default, this calls [getQueueForType] and iterates through this to determine if the [QueueMessage.assignedTo] + * By default, this calls [getSubQueue] and iterates through this to determine if the [QueueMessage.assignedTo] * field is `not-null`, if [assignedTo] is `null` or is equal to the provided [assignedTo] if it is `not-null`. * - * @param queueType the identifier of the sub-queue [Queue] + * @param subQueue the identifier of the sub-queue [Queue] * @param assignedTo to further filter the messages returned this can be provided * @return a limited version of the [Queue] containing only assigned messages */ - open fun getAssignedMessagesForType(queueType: String, assignedTo: String?): Queue + open fun getAssignedMessagesInSubQueue(subQueue: String, assignedTo: String?): Queue { - val queue = ConcurrentLinkedQueue() - val queueForType = getQueueForType(queueType) + val assignedMessages = ConcurrentLinkedQueue() + val queue = getSubQueue(subQueue) if (assignedTo == null) { - queue.addAll(queueForType.stream().filter { message -> message.assignedTo != null }.collect(Collectors.toList())) + assignedMessages.addAll(queue.stream().filter { message -> message.assignedTo != null }.collect(Collectors.toList())) } else { - queue.addAll(queueForType.stream().filter { message -> message.assignedTo == assignedTo }.collect(Collectors.toList())) + assignedMessages.addAll(queue.stream().filter { message -> message.assignedTo == assignedTo }.collect(Collectors.toList())) } - return queue + return assignedMessages } /** - * Retrieves only unassigned messages in the sub-queue for the provided [queueType]. + * Retrieves only unassigned messages in the sub-queue for the provided [subQueue]. * - * By default, this iterates over [getQueueForType] and includes only entries where [QueueMessage.assignedTo] is `null`. + * By default, this iterates over [getSubQueue] and includes only entries where [QueueMessage.assignedTo] is `null`. * - * @param queueType the identifier of the sub-queue [Queue] + * @param subQueue the identifier of the sub-queue [Queue] * @return a limited version of the [Queue] containing only unassigned messages */ - open fun getUnassignedMessagesForType(queueType:String): Queue + open fun getUnassignedMessagesInSubQueue(subQueue: String): Queue { - val queue = ConcurrentLinkedQueue() - val queueForType = getQueueForType(queueType) - queue.addAll(queueForType.stream().filter { message -> message.assignedTo == null }.collect(Collectors.toList())) - return queue + val unassignedMessages = ConcurrentLinkedQueue() + val queue = getSubQueue(subQueue) + unassignedMessages.addAll(queue.stream().filter { message -> message.assignedTo == null }.collect(Collectors.toList())) + return unassignedMessages } /** * Get a map of assignee identifiers and the sub-queue identifier that they own messages in. - * If the [queueType] is provided this will iterate over all sub-queues and call [getOwnersAndKeysMapForType] for each of them. - * Otherwise, it will only call [getOwnersAndKeysMapForType] on the provided [queueType] if it is not null. + * If the [subQueue] is provided this will iterate over all sub-queues and call [getOwnersAndKeysMapForSubQueue] for each of them. + * Otherwise, it will only call [getOwnersAndKeysMapForSubQueue] on the provided [subQueue] if it is not null. * - * @param queueType the queue type retrieve the [Map] from + * @param subQueue to retrieve the [Map] from * @return the [Map] of assignee identifiers mapped to a list of the sub-queue identifiers that they own any messages in */ - fun getOwnersAndKeysMap(queueType: String?): Map> + fun getOwnersAndKeysMap(subQueue: String?): Map> { val responseMap = HashMap>() - if (queueType != null) + if (subQueue != null) { - LOG.debug("Getting owners map for sub-queue with identifier [{}].", queueType) - getOwnersAndKeysMapForType(queueType, responseMap) + LOG.debug("Getting owners map for sub-queue with identifier [{}].", subQueue) + getOwnersAndKeysMapForSubQueue(subQueue, responseMap) } else { LOG.debug("Getting owners map for all sub-queues.") val keys = keys(false) - keys.forEach { key -> - getOwnersAndKeysMapForType(key, responseMap) - } + keys.forEach { key -> getOwnersAndKeysMapForSubQueue(key, responseMap) } } return responseMap } /** * Add an entry to the provided [Map] if any of the messages in the sub-queue are assigned. - * The [QueueMessage.type] is appended to the [Set] under it's [QueueMessage.assignedTo] identifier. + * The [QueueMessage.subQueue] is appended to the [Set] under it's [QueueMessage.assignedTo] identifier. * - * @param queueType the sub-queue to iterate and update the map from + * @param subQueue the sub-queue to iterate and update the map from * @param responseMap the map to update */ - fun getOwnersAndKeysMapForType(queueType: String, responseMap: HashMap>) + fun getOwnersAndKeysMapForSubQueue(subQueue: String, responseMap: HashMap>) { - val queueForType: Queue = getAssignedMessagesForType(queueType, null) - queueForType.forEach { message -> - val type = message.type + val queue: Queue = getAssignedMessagesInSubQueue(subQueue, null) + queue.forEach { message -> + val subQueueID = message.subQueue val assigned = message.assignedTo if (assigned != null) { - LOG.trace("Appending sub-queue identifier [{}] to map for assignee ID [{}].", type, assigned) + LOG.trace("Appending sub-queue identifier [{}] to map for assignee ID [{}].", subQueueID, assigned) if (!responseMap.contains(assigned)) { - val set = hashSetOf(type) + val set = hashSetOf(subQueueID) responseMap[assigned] = set } else { // Set should not be null since we initialise and set it if the key is contained - responseMap[assigned]!!.add(type) + responseMap[assigned]!!.add(subQueueID) } } } @@ -235,48 +262,48 @@ abstract class MultiQueue: Queue, HasLogger * * This method should update the [size] property as part of the clearing of the sub-queue. * - * @param queueType the [String] of the [Queue] to clear + * @param subQueue the [String] of the [Queue] to clear * @return the number of entries removed */ - abstract fun clearForTypeInternal(queueType: String): Int + abstract fun clearSubQueueInternal(subQueue: String): Int /** - * Call to [MultiQueue.clearForTypeInternal]. + * Call to [MultiQueue.clearSubQueueInternal]. * - * @param queueType the [String] of the [Queue] to clear + * @param subQueue the [String] of the [Queue] to clear * @return the number of entries removed */ - fun clearForType(queueType: String): Int + fun clearSubQueue(subQueue: String): Int { - return clearForTypeInternal(queueType) + return clearSubQueueInternal(subQueue) } /** * Indicates whether the underlying [Queue] for the provided [String] is empty. By calling [Queue.isEmpty]. * - * @param queueType the [String] of the [Queue] to check whether it is empty + * @param subQueue the [String] of the [Queue] to check whether it is empty * @return `true` if the [Queue] for the [String] is empty, otherwise `false` */ - abstract fun isEmptyForType(queueType: String): Boolean + abstract fun isEmptySubQueue(subQueue: String): Boolean /** * Calls [Queue.poll] on the underlying [Queue] for the provided [String]. * This will retrieve **AND** remove the head element of the [Queue]. * - * @param queueType [String] of the [Queue] to poll + * @param subQueue [String] of the [Queue] to poll * @return the head element or `null` */ - open fun pollForType(queueType: String): Optional + open fun pollSubQueue(subQueue: String): Optional { - val head = pollInternal(queueType) + val head = pollInternal(subQueue) if (head.isPresent) { removeInternal(head.get()) - LOG.debug("Found and removed head element with UUID [{}] from queue with type [{}].", head.get().uuid, queueType) + LOG.debug("Found and removed head element with UUID [{}] from sub-queue [{}].", head.get().uuid, subQueue) } else { - LOG.debug("No head element found when polling queue with type [{}].", queueType) + LOG.debug("No head element found when polling sub-queue [{}].", subQueue) } return head } @@ -285,49 +312,65 @@ abstract class MultiQueue: Queue, HasLogger * The internal poll method to be called. * This is not to be called directly. * - * This method should return the first element in the queue for the provided [queueType]. + * This method should return the first element in the queue for the provided [subQueue]. * *The caller will remove this element*. * - * @param queueType the sub-queue to poll + * @param subQueue the sub-queue to poll * @return the first message wrapped as an [Optional] otherwise [Optional.empty] */ - abstract fun pollInternal(queueType: String): Optional + abstract fun pollInternal(subQueue: String): Optional /** * Calls [Queue.peek] on the underlying [Queue] for the provided [String]. * This will retrieve the head element of the [Queue] without removing it. * - * @param queueType [String] of the [Queue] to peek + * @param subQueue [String] of the [Queue] to peek * @return the head element or `null` */ - fun peekForType(queueType: String): Optional + fun peekSubQueue(subQueue: String): Optional { - val queueForType: Queue = getQueueForType(queueType) - val peeked = Optional.ofNullable(queueForType.peek()) + val queue: Queue = getSubQueue(subQueue) + val peeked = Optional.ofNullable(queue.peek()) if (peeked.isPresent) { - LOG.debug("Found head element with UUID [{}] from queue with type [{}].", peeked.get().uuid, queueType) + LOG.debug("Found head element with UUID [{}] from sub-queue [{}].", peeked.get().uuid, subQueue) } else { - LOG.debug("No head element found when peeking queue with type [{}].", queueType) + LOG.debug("No head element found when peeking sub-queue [{}].", subQueue) } return peeked } /** * Retrieves the underlying key list as a set. + * **Should be called directly, please using [keys].** * * @param includeEmpty *true* to include any empty queues which one had elements in them, otherwise *false* to only include keys from queues which have elements. - * @return a [Set] of the available `QueueTypes` that have entries in the [MultiQueue]. + * @return a [HashSet] of the available `Sub-Queues` that have entries in the [MultiQueue]. */ - abstract fun keys(includeEmpty: Boolean = true): Set + abstract fun keysInternal(includeEmpty: Boolean = true): HashSet /** - * Returns the `queueType` that the [QueueMessage] with the provided [UUID] exists in. + * Delegates to [keysInternal] and removes any keys that match in the [MultiQueueAuthenticator.getReservedSubQueues]. + * + * @param includeEmpty *true* to include any empty queues which one had elements in them, otherwise *false* to only include keys from queues which have elements. + * @return a [Set] of the available `Sub-Queues` that have entries in the [MultiQueue]. + */ + fun keys(includeEmpty: Boolean = true): Set + { + val keysSet = keysInternal(includeEmpty) + + // Remove the restricted key(s) + multiQueueAuthenticator.getReservedSubQueues().forEach { reservedSubQueue -> keysSet.remove(reservedSubQueue) } + return keysSet + } + + /** + * Returns the `sub-queue` that the [QueueMessage] with the provided [UUID] exists in. * * @param uuid the [UUID] (as a [String]) to look up - * @return the `queueType` [String] if a [QueueMessage] exists with the provided [UUID] otherwise [Optional.empty] + * @return the `sub-queue` [String] if a [QueueMessage] exists with the provided [UUID] otherwise [Optional.empty] */ abstract fun containsUUID(uuid: String): Optional @@ -337,21 +380,27 @@ abstract class MultiQueue: Queue, HasLogger /** * Override [add] method to declare [Throws] [DuplicateMessageException] annotation. * - * [Synchronized] so that the call to [MultiQueue.getNextQueueIndex] does not cause issues, since retrieving the next index does + * [Synchronized] so that the call to [MultiQueue.getNextSubQueueIndex] does not cause issues, since retrieving the next index does * not force it to be auto incremented or unusable by another thread. * * @throws [DuplicateMessageException] if a message already exists with the same [QueueMessage.uuid] in `any` other queue. + * @throws [IllegalSubQueueIdentifierException] if the [QueueMessage.subQueue] is invalid or reserved */ - @Throws(DuplicateMessageException::class) + @Throws(DuplicateMessageException::class, IllegalSubQueueIdentifierException::class) @Synchronized override fun add(element: QueueMessage): Boolean { - val elementIsMappedToType = containsUUID(element.uuid) - if ( !elementIsMappedToType.isPresent) + if (multiQueueAuthenticator.getReservedSubQueues().contains(element.subQueue)) + { + throw IllegalSubQueueIdentifierException(element.subQueue) + } + + val subQueueMessageAlreadyExistsIn = containsUUID(element.uuid) + if ( !subQueueMessageAlreadyExistsIn.isPresent) { if (element.id == null) { - val index = getNextQueueIndex(element.type) + val index = getNextSubQueueIndex(element.subQueue) if (index.isPresent) { element.id = index.get() @@ -360,20 +409,20 @@ abstract class MultiQueue: Queue, HasLogger val wasAdded = addInternal(element) return if (wasAdded) { - LOG.debug("Added new message with uuid [{}] to queue with type [{}].", element.uuid, element.type) + LOG.debug("Added new message with uuid [{}] to sub-queue [{}].", element.uuid, element.subQueue) true } else { - LOG.error("Failed to add message with uuid [{}] to queue with type [{}].", element.uuid, element.type) + LOG.error("Failed to add message with uuid [{}] to sub-queue [{}].", element.uuid, element.subQueue) false } } else { - val existingQueueType = elementIsMappedToType.get() - LOG.warn("Did not add new message with uuid [{}] to queue with type [{}] as it already exists in queue with type [{}].", element.uuid, element.type, existingQueueType) - throw DuplicateMessageException(element.uuid, existingQueueType) + val existingSubQueue = subQueueMessageAlreadyExistsIn.get() + LOG.warn("Did not add new message with uuid [{}] to sub-queue [{}] as it already exists in sub-queue [{}].", element.uuid, element.subQueue, existingSubQueue) + throw DuplicateMessageException(element.uuid, existingSubQueue) } } @@ -391,11 +440,11 @@ abstract class MultiQueue: Queue, HasLogger val wasRemoved = removeInternal(element) if (wasRemoved) { - LOG.debug("Removed element with UUID [{}] from queue with type [{}].", element.uuid, element.type) + LOG.debug("Removed element with UUID [{}] from sub-queue [{}].", element.uuid, element.subQueue) } else { - LOG.error("Failed to remove element with UUID [{}] from queue with type [{}].", element.uuid, element.type) + LOG.error("Failed to remove element with UUID [{}] from sub-queue [{}].", element.uuid, element.subQueue) } return wasRemoved } @@ -415,8 +464,8 @@ abstract class MultiQueue: Queue, HasLogger { return false } - val queueForType: Queue = getQueueForType(element.type) - return queueForType.contains(element) + val queue: Queue = getSubQueue(element.subQueue) + return queue.contains(element) } override fun containsAll(elements: Collection): Boolean @@ -447,7 +496,7 @@ abstract class MultiQueue: Queue, HasLogger for (key: String in keys(false)) { // The queue should never be new or created since we passed `false` into `keys()` above. - val queueForKey: Queue = getQueueForType(key) + val queueForKey: Queue = getSubQueue(key) for(entry: QueueMessage in queueForKey) { if ( !elements.contains(entry)) @@ -484,11 +533,11 @@ abstract class MultiQueue: Queue, HasLogger } /** - * @return `true` any of the [keys] returns `false` for [isEmptyForType], otherwise `false`. + * @return `true` any of the [keys] returns `false` for [isEmptySubQueue], otherwise `false`. */ override fun isEmpty(): Boolean { - val anyHasElements = keys(false).stream().anyMatch { key -> !isEmptyForType(key) } + val anyHasElements = keys(false).stream().anyMatch { key -> !isEmptySubQueue(key) } return !anyHasElements } @@ -498,10 +547,10 @@ abstract class MultiQueue: Queue, HasLogger var removedEntryCount = 0 for (key in keys) { - val amountRemovedForQueue = clearForType(key) + val amountRemovedForQueue = clearSubQueue(key) removedEntryCount += amountRemovedForQueue } - LOG.debug("Cleared multi-queue, removed [{}] message entries over [{}] queue types.", removedEntryCount, keys) + LOG.debug("Cleared multi-queue, removed [{}] message entries over [{}] sub-queues.", removedEntryCount, keys) } /** diff --git a/src/main/kotlin/au/kilemon/messagequeue/queue/cache/redis/RedisMultiQueue.kt b/src/main/kotlin/au/kilemon/messagequeue/queue/cache/redis/RedisMultiQueue.kt index 9a9bb3e..722c5e0 100644 --- a/src/main/kotlin/au/kilemon/messagequeue/queue/cache/redis/RedisMultiQueue.kt +++ b/src/main/kotlin/au/kilemon/messagequeue/queue/cache/redis/RedisMultiQueue.kt @@ -10,9 +10,8 @@ import org.springframework.data.redis.core.RedisTemplate import org.springframework.data.redis.core.ScanOptions import java.util.* import java.util.concurrent.ConcurrentLinkedQueue -import java.util.concurrent.atomic.AtomicLong import java.util.stream.Collectors -import kotlin.collections.HashMap +import kotlin.collections.HashSet /** * A `Redis` specific implementation of the [MultiQueue]. @@ -23,30 +22,64 @@ import kotlin.collections.HashMap */ class RedisMultiQueue(private val prefix: String = "", private val redisTemplate: RedisTemplate) : MultiQueue(), HasLogger { - override val LOG: Logger = initialiseLogger() + override val LOG: Logger = this.initialiseLogger() /** - * Append the [MessageQueueSettings.redisPrefix] to the provided [queueType] [String]. + * Append the [MessageQueueSettings.redisPrefix] to the provided [subQueue] [String]. * - * @param queueType the [String] to add the prefix to - * @return a [String] with the provided [queueType] type with the [MessageQueueSettings.redisPrefix] appended to the beginning. + * @param subQueue the [String] to add the prefix to + * @return a [String] with the provided [subQueue] with the [MessageQueueSettings.redisPrefix] appended to the beginning. */ - private fun appendPrefix(queueType: String): String + private fun appendPrefix(subQueue: String): String { - if (prefix.isNotBlank() && !queueType.startsWith(prefix)) + if (hasPrefix() && !subQueue.startsWith(getPrefix())) { - return "${prefix}$queueType" + return "${getPrefix()}$subQueue" } - return queueType + return subQueue } /** - * Attempts to append the prefix before requesting the underlying redis entry if the provided [queueType] is not prefixed with [MessageQueueSettings.redisPrefix]. + * @return whether the [prefix] is [String.isNotBlank] */ - override fun getQueueForType(queueType: String): Queue + internal fun hasPrefix(): Boolean + { + return getPrefix().isNotBlank() + } + + /** + * @return [prefix] + */ + internal fun getPrefix(): String + { + return prefix + } + + /** + * If [prefix] is set, removes this from all provided [keys]. + * If [prefix] is null or blank, then the provided [keys] [Set] is immediately returned. + * + * @param keys the [Set] of [String] to remove the [prefix] from + * @return the updated [Set] of [String] with the [prefix] removed + */ + fun removePrefix(keys: Set): Set + { + if (!hasPrefix()) + { + return keys + } + + val prefixLength = getPrefix().length + return keys.stream().filter { key -> key.startsWith(getPrefix()) }.map { key -> key.substring(prefixLength) }.collect(Collectors.toSet()) + } + + /** + * Attempts to append the prefix before requesting the underlying redis entry if the provided [subQueue] is not prefixed with [MessageQueueSettings.redisPrefix]. + */ + override fun getSubQueueInternal(subQueue: String): Queue { val queue = ConcurrentLinkedQueue() - val set = redisTemplate.opsForSet().members(appendPrefix(queueType)) + val set = redisTemplate.opsForSet().members(appendPrefix(subQueue)) if (!set.isNullOrEmpty()) { queue.addAll(set.toSortedSet { message1, message2 -> (message1.id ?: 0).minus(message2.id ?: 0).toInt() }) @@ -54,10 +87,10 @@ class RedisMultiQueue(private val prefix: String = "", private val redisTemplate return queue } - override fun getAssignedMessagesForType(queueType: String, assignedTo: String?): Queue + override fun getAssignedMessagesInSubQueue(subQueue: String, assignedTo: String?): Queue { val queue = ConcurrentLinkedQueue() - val existingQueue = getQueueForType(queueType) + val existingQueue = getSubQueue(subQueue) if (existingQueue.isNotEmpty()) { if (assignedTo == null) @@ -79,30 +112,30 @@ class RedisMultiQueue(private val prefix: String = "", private val redisTemplate override fun getMessageByUUID(uuid: String): Optional { - val queueType = containsUUID(uuid) - if (queueType.isPresent) + val subQueue = containsUUID(uuid) + if (subQueue.isPresent) { - val queueForType: Queue = getQueueForType(queueType.get()) - return queueForType.stream().filter { message -> message.uuid == uuid }.findFirst() + val queue: Queue = getSubQueue(subQueue.get()) + return queue.stream().filter { message -> message.uuid == uuid }.findFirst() } return Optional.empty() } override fun addInternal(element: QueueMessage): Boolean { - val result = redisTemplate.opsForSet().add(appendPrefix(element.type), element) + val result = redisTemplate.opsForSet().add(appendPrefix(element.subQueue), element) return result != null && result > 0 } /** - * Overriding to pass in the [queueType] into [appendPrefix]. + * Overriding to pass in the [subQueue] into [appendPrefix]. */ - override fun getNextQueueIndex(queueType: String): Optional + override fun getNextSubQueueIndex(subQueue: String): Optional { - val queueForType = getQueueForType(appendPrefix(queueType)) - return if (queueForType.isNotEmpty()) + val queue = getSubQueue(appendPrefix(subQueue)) + return if (queue.isNotEmpty()) { - Optional.ofNullable(queueForType.last().id?.plus(1) ?: 1) + Optional.ofNullable(queue.last().id?.plus(1) ?: 1) } else { @@ -112,35 +145,35 @@ class RedisMultiQueue(private val prefix: String = "", private val redisTemplate override fun removeInternal(element: QueueMessage): Boolean { - val result = redisTemplate.opsForSet().remove(appendPrefix(element.type), element) + val result = redisTemplate.opsForSet().remove(appendPrefix(element.subQueue), element) return result != null && result > 0 } - override fun clearForTypeInternal(queueType: String): Int + override fun clearSubQueueInternal(subQueue: String): Int { var amountRemoved = 0 - val queueForType = getQueueForType(queueType) - if (queueForType.isNotEmpty()) + val queue = getSubQueue(subQueue) + if (queue.isNotEmpty()) { - amountRemoved = queueForType.size - redisTemplate.delete(appendPrefix(queueType)) - LOG.debug("Cleared existing queue for type [{}]. Removed [{}] message entries.", queueType, amountRemoved) + amountRemoved = queue.size + redisTemplate.delete(appendPrefix(subQueue)) + LOG.debug("Cleared existing sub-queue [{}]. Removed [{}] message entries.", subQueue, amountRemoved) } else { - LOG.debug("Attempting to clear non-existent queue for type [{}]. No messages cleared.", queueType) + LOG.debug("Attempting to clear non-existent sub-queue [{}]. No messages cleared.", subQueue) } return amountRemoved } - override fun isEmptyForType(queueType: String): Boolean + override fun isEmptySubQueue(subQueue: String): Boolean { - return getQueueForType(queueType).isEmpty() + return getSubQueue(subQueue).isEmpty() } - override fun pollInternal(queueType: String): Optional + override fun pollInternal(subQueue: String): Optional { - val queue = getQueueForType(queueType) + val queue = getSubQueue(subQueue) if (queue.isNotEmpty()) { return Optional.of(queue.iterator().next()) @@ -148,7 +181,7 @@ class RedisMultiQueue(private val prefix: String = "", private val redisTemplate return Optional.empty() } - override fun keys(includeEmpty: Boolean): Set + override fun keysInternal(includeEmpty: Boolean): HashSet { val scanOptions = ScanOptions.scanOptions().match(appendPrefix("*")).build() val cursor = redisTemplate.scan(scanOptions) @@ -167,7 +200,7 @@ class RedisMultiQueue(private val prefix: String = "", private val redisTemplate val sizeOfQueue = redisTemplate.opsForSet().size(key) if (sizeOfQueue != null && sizeOfQueue > 0) { - LOG.trace("Queue type [{}] is not empty and will be returned in keys() call.", key) + LOG.trace("Sub-queue [{}] is not empty and will be returned in keys() call.", key) retainedKeys.add(key) } } @@ -180,15 +213,15 @@ class RedisMultiQueue(private val prefix: String = "", private val redisTemplate { for (key in keys()) { - val queue = getQueueForType(key) + val queue = getSubQueue(key) val anyMatchTheUUID = queue.stream().anyMatch{ message -> uuid == message.uuid } if (anyMatchTheUUID) { - LOG.debug("Found queue type [{}] for UUID: [{}].", key, uuid) + LOG.debug("Found sub-queue [{}] for message UUID: [{}].", key, uuid) return Optional.of(key) } } - LOG.debug("No queue type exists for UUID: [{}].", uuid) + LOG.debug("No sub-queue contains message with UUID: [{}].", uuid) return Optional.empty() } @@ -198,7 +231,7 @@ class RedisMultiQueue(private val prefix: String = "", private val redisTemplate */ override fun persistMessageInternal(message: QueueMessage) { - val queue = getQueueForType(message.type) + val queue = getSubQueue(message.subQueue) val matchingMessage = queue.stream().filter{ element -> element.uuid == message.uuid }.findFirst() if (matchingMessage.isPresent) { diff --git a/src/main/kotlin/au/kilemon/messagequeue/queue/exception/DuplicateMessageException.kt b/src/main/kotlin/au/kilemon/messagequeue/queue/exception/DuplicateMessageException.kt index 338567b..4541f2e 100644 --- a/src/main/kotlin/au/kilemon/messagequeue/queue/exception/DuplicateMessageException.kt +++ b/src/main/kotlin/au/kilemon/messagequeue/queue/exception/DuplicateMessageException.kt @@ -5,4 +5,4 @@ package au.kilemon.messagequeue.queue.exception * * @author github.com/Kilemonn */ -class DuplicateMessageException(uuid: String, queueType: String) : Exception("Duplicate message with UUID [$uuid] exists in queue with type [$queueType].") +class DuplicateMessageException(uuid: String, subQueue: String) : Exception("Duplicate message with UUID [$uuid] exists in sub-queue [$subQueue].") diff --git a/src/main/kotlin/au/kilemon/messagequeue/queue/exception/IllegalSubQueueIdentifierException.kt b/src/main/kotlin/au/kilemon/messagequeue/queue/exception/IllegalSubQueueIdentifierException.kt new file mode 100644 index 0000000..65fa398 --- /dev/null +++ b/src/main/kotlin/au/kilemon/messagequeue/queue/exception/IllegalSubQueueIdentifierException.kt @@ -0,0 +1,8 @@ +package au.kilemon.messagequeue.queue.exception + +/** + * A specific exception used to indicate that an invalid or reserved sub-queue identifier cannot be used. + * + * @author github.com/Kilemonn + */ +class IllegalSubQueueIdentifierException(subQueue: String) : Exception("Cannot access sub-queue with identifier [$subQueue] since it is reserved or invalid.") diff --git a/src/main/kotlin/au/kilemon/messagequeue/queue/inmemory/InMemoryMultiQueue.kt b/src/main/kotlin/au/kilemon/messagequeue/queue/inmemory/InMemoryMultiQueue.kt index a7bfefd..9226b73 100644 --- a/src/main/kotlin/au/kilemon/messagequeue/queue/inmemory/InMemoryMultiQueue.kt +++ b/src/main/kotlin/au/kilemon/messagequeue/queue/inmemory/InMemoryMultiQueue.kt @@ -9,7 +9,6 @@ import java.util.* import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentLinkedQueue import java.util.concurrent.atomic.AtomicLong -import java.util.stream.Collectors import kotlin.collections.HashMap import kotlin.jvm.Throws @@ -21,10 +20,10 @@ import kotlin.jvm.Throws */ open class InMemoryMultiQueue : MultiQueue(), HasLogger { - override val LOG: Logger = initialiseLogger() + override val LOG: Logger = this.initialiseLogger() /** - * An internal [Map] that holds known [UUID]s (as a [String]) and their related `queueType` to quickly find entries within the [MultiQueue]. + * An internal [Map] that holds known [UUID]s (as a [String]) and their related `sub-queue` to quickly find entries within the [MultiQueue]. */ private val uuidMap: ConcurrentHashMap = ConcurrentHashMap() @@ -36,34 +35,34 @@ open class InMemoryMultiQueue : MultiQueue(), HasLogger private val maxQueueIndex: HashMap = HashMap() /** - * This index is special compared to the other types it will be incremented once retrieved. So we could be skipping - * indexes, but it should be fine since it's only used for message ordering. + * This index is special compared to the other [au.kilemon.messagequeue.settings.StorageMedium] it will be incremented once retrieved. + * So we could be skipping indexes, but it should be fine since it's only used for message ordering. */ - override fun getNextQueueIndex(queueType: String): Optional + override fun getNextSubQueueIndex(subQueue: String): Optional { - var index = maxQueueIndex[queueType] + var index = maxQueueIndex[subQueue] if (index == null) { index = AtomicLong(1) - maxQueueIndex[queueType] = index + maxQueueIndex[subQueue] = index } return Optional.of(index.getAndIncrement()) } - override fun getQueueForType(queueType: String): Queue + override fun getSubQueueInternal(subQueue: String): Queue { - var queueForType: Queue? = messageQueue[queueType] - if (queueForType == null) + var queue: Queue? = messageQueue[subQueue] + if (queue == null) { - queueForType = ConcurrentLinkedQueue() - LOG.debug("Initialising new queue for type [{}].", queueType) - messageQueue[queueType] = queueForType + queue = ConcurrentLinkedQueue() + LOG.debug("Initialising new sub-queue [{}].", subQueue) + messageQueue[subQueue] = queue } else { - LOG.debug("Found existing queue for type [{}] with size [{}].", queueType, queueForType.size) + LOG.debug("Found existing sub-queue [{}] with size [{}].", subQueue, queue.size) } - return queueForType + return queue } override fun performHealthCheckInternal() @@ -73,31 +72,31 @@ open class InMemoryMultiQueue : MultiQueue(), HasLogger override fun getMessageByUUID(uuid: String): Optional { - val queueType = containsUUID(uuid) - if (queueType.isPresent) + val subQueue = containsUUID(uuid) + if (subQueue.isPresent) { - val queueForType: Queue = getQueueForType(queueType.get()) - return queueForType.stream().filter { message -> message.uuid == uuid }.findFirst() + val queue: Queue = getSubQueue(subQueue.get()) + return queue.stream().filter { message -> message.uuid == uuid }.findFirst() } return Optional.empty() } - override fun clearForTypeInternal(queueType: String): Int + override fun clearSubQueueInternal(subQueue: String): Int { var amountRemoved = 0 - val queueForType: Queue? = messageQueue[queueType] - maxQueueIndex.remove(queueType) - if (queueForType != null) + val queue: Queue? = messageQueue[subQueue] + maxQueueIndex.remove(subQueue) + if (queue != null) { - amountRemoved = queueForType.size - queueForType.forEach { message -> uuidMap.remove(message.uuid) } - queueForType.clear() - messageQueue.remove(queueType) - LOG.debug("Cleared existing queue for type [{}]. Removed [{}] message entries.", queueType, amountRemoved) + amountRemoved = queue.size + queue.forEach { message -> uuidMap.remove(message.uuid) } + queue.clear() + messageQueue.remove(subQueue) + LOG.debug("Cleared existing sub-queue [{}]. Removed [{}] message entries.", subQueue, amountRemoved) } else { - LOG.debug("Attempting to clear non-existent queue for type [{}]. No messages cleared.", queueType) + LOG.debug("Attempting to clear non-existent sub-queue [{}]. No messages cleared.", subQueue) } return amountRemoved } @@ -114,7 +113,7 @@ open class InMemoryMultiQueue : MultiQueue(), HasLogger val wasAdded = super.add(element) if (wasAdded) { - uuidMap[element.uuid] = element.type + uuidMap[element.uuid] = element.subQueue } return wasAdded } @@ -124,8 +123,8 @@ open class InMemoryMultiQueue : MultiQueue(), HasLogger */ override fun addInternal(element: QueueMessage): Boolean { - val queueForType: Queue = getQueueForType(element.type) - return queueForType.add(element) + val queue: Queue = getSubQueue(element.subQueue) + return queue.add(element) } override fun remove(element: QueueMessage): Boolean @@ -143,32 +142,32 @@ open class InMemoryMultiQueue : MultiQueue(), HasLogger */ override fun removeInternal(element: QueueMessage): Boolean { - val queueForType: Queue = getQueueForType(element.type) - return queueForType.remove(element) + val queue: Queue = getSubQueue(element.subQueue) + return queue.remove(element) } - override fun isEmptyForType(queueType: String): Boolean + override fun isEmptySubQueue(subQueue: String): Boolean { - val queueForType: Queue = getQueueForType(queueType) - return queueForType.isEmpty() + val queue: Queue = getSubQueue(subQueue) + return queue.isEmpty() } - override fun keys(includeEmpty: Boolean): Set + override fun keysInternal(includeEmpty: Boolean): HashSet { if (includeEmpty) { LOG.debug("Including all empty queue keys in call to keys(). Total queue keys [{}].", messageQueue.keys.size) - return messageQueue.keys.toSet() + return HashSet(messageQueue.keys) } else { val keys = HashSet() for (key: String in messageQueue.keys) { - val queueForType = getQueueForType(key) - if (queueForType.isNotEmpty()) + val queue = getSubQueue(key) + if (queue.isNotEmpty()) { - LOG.trace("Queue type [{}] is not empty and will be returned in keys() call.", queueForType) + LOG.trace("Sub-queue [{}] is not empty and will be returned in keys() call.", queue) keys.add(key) } } @@ -179,24 +178,24 @@ open class InMemoryMultiQueue : MultiQueue(), HasLogger override fun containsUUID(uuid: String): Optional { - val queueTypeForUUID: String? = uuidMap[uuid] - if (queueTypeForUUID.isNullOrBlank()) + val subQueueID: String? = uuidMap[uuid] + if (subQueueID.isNullOrBlank()) { - LOG.debug("No queue type exists for UUID: [{}].", uuid) + LOG.debug("No sub-queue exists for UUID: [{}].", uuid) } else { - LOG.debug("Found queue type [{}] for UUID: [{}].", queueTypeForUUID, uuid) + LOG.debug("Found sub-queue [{}] for UUID: [{}].", subQueueID, uuid) } - return Optional.ofNullable(queueTypeForUUID) + return Optional.ofNullable(subQueueID) } /** * Update the [uuidMap] and remove the entry if it is returned (removed). */ - override fun pollForType(queueType: String): Optional + override fun pollSubQueue(subQueue: String): Optional { - val message = super.pollForType(queueType) + val message = super.pollSubQueue(subQueue) if (message.isPresent) { uuidMap.remove(message.get().uuid) @@ -205,12 +204,12 @@ open class InMemoryMultiQueue : MultiQueue(), HasLogger return message } - override fun pollInternal(queueType: String): Optional + override fun pollInternal(subQueue: String): Optional { - val queueForType: Queue = getQueueForType(queueType) - return if (queueForType.isNotEmpty()) + val queue: Queue = getSubQueue(subQueue) + return if (queue.isNotEmpty()) { - Optional.of(queueForType.iterator().next()) + Optional.of(queue.iterator().next()) } else { diff --git a/src/main/kotlin/au/kilemon/messagequeue/queue/nosql/mongo/MongoMultiQueue.kt b/src/main/kotlin/au/kilemon/messagequeue/queue/nosql/mongo/MongoMultiQueue.kt index 0299d18..1353f67 100644 --- a/src/main/kotlin/au/kilemon/messagequeue/queue/nosql/mongo/MongoMultiQueue.kt +++ b/src/main/kotlin/au/kilemon/messagequeue/queue/nosql/mongo/MongoMultiQueue.kt @@ -11,7 +11,6 @@ import org.springframework.beans.factory.annotation.Autowired import org.springframework.context.annotation.Lazy import java.util.* import java.util.concurrent.ConcurrentLinkedQueue -import java.util.concurrent.atomic.AtomicLong /** * A NoSql mongo backed [MultiQueue]. All operations are performed directly on the database it is the complete source of truth. @@ -26,7 +25,7 @@ class MongoMultiQueue : MultiQueue(), HasLogger const val INDEX_ID = "index_id" } - override val LOG: Logger = initialiseLogger() + override val LOG: Logger = this.initialiseLogger() @Lazy @Autowired @@ -48,15 +47,15 @@ class MongoMultiQueue : MultiQueue(), HasLogger /** * Overriding to use more direct optimised queries. */ - override fun getAssignedMessagesForType(queueType: String, assignedTo: String?): Queue + override fun getAssignedMessagesInSubQueue(subQueue: String, assignedTo: String?): Queue { val entries = if (assignedTo == null) { - queueMessageRepository.findByTypeAndAssignedToIsNotNullOrderByIdAsc(queueType) + queueMessageRepository.findBySubQueueAndAssignedToIsNotNullOrderByIdAsc(subQueue) } else { - queueMessageRepository.findByTypeAndAssignedToOrderByIdAsc(queueType, assignedTo) + queueMessageRepository.findBySubQueueAndAssignedToOrderByIdAsc(subQueue, assignedTo) } return ConcurrentLinkedQueue(entries.map { entry -> QueueMessage(entry) }) @@ -65,15 +64,15 @@ class MongoMultiQueue : MultiQueue(), HasLogger /** * Overriding since we can filter via the DB query. */ - override fun getUnassignedMessagesForType(queueType: String): Queue + override fun getUnassignedMessagesInSubQueue(subQueue: String): Queue { - val entries = queueMessageRepository.findByTypeAndAssignedToIsNullOrderByIdAsc(queueType) + val entries = queueMessageRepository.findBySubQueueAndAssignedToIsNullOrderByIdAsc(subQueue) return ConcurrentLinkedQueue(entries.map { entry -> QueueMessage(entry) }) } - override fun getQueueForType(queueType: String): Queue + override fun getSubQueueInternal(subQueue: String): Queue { - val entries = queueMessageRepository.findByTypeOrderByIdAsc(queueType) + val entries = queueMessageRepository.findBySubQueueOrderByIdAsc(subQueue) return ConcurrentLinkedQueue(entries.map { entry -> QueueMessage(entry) }) } @@ -95,21 +94,21 @@ class MongoMultiQueue : MultiQueue(), HasLogger } } - override fun clearForTypeInternal(queueType: String): Int + override fun clearSubQueueInternal(subQueue: String): Int { - val amountCleared = queueMessageRepository.deleteByType(queueType) - LOG.debug("Cleared existing queue for type [{}]. Removed [{}] message entries.", queueType, amountCleared) + val amountCleared = queueMessageRepository.deleteBySubQueue(subQueue) + LOG.debug("Cleared existing queue for sub-queue [{}]. Removed [{}] message entries.", subQueue, amountCleared) return amountCleared } - override fun isEmptyForType(queueType: String): Boolean + override fun isEmptySubQueue(subQueue: String): Boolean { - return queueMessageRepository.findByTypeOrderByIdAsc(queueType).isEmpty() + return queueMessageRepository.findBySubQueueOrderByIdAsc(subQueue).isEmpty() } - override fun pollInternal(queueType: String): Optional + override fun pollInternal(subQueue: String): Optional { - val messages = queueMessageRepository.findByTypeOrderByIdAsc(queueType) + val messages = queueMessageRepository.findBySubQueueOrderByIdAsc(subQueue) return if (messages.isNotEmpty()) { return Optional.of(QueueMessage(messages[0])) @@ -123,9 +122,9 @@ class MongoMultiQueue : MultiQueue(), HasLogger /** * The [includeEmpty] value makes no difference it is always effectively `false`. */ - override fun keys(includeEmpty: Boolean): Set + override fun keysInternal(includeEmpty: Boolean): HashSet { - val keySet = queueMessageRepository.getDistinctTypes().toSet() + val keySet = HashSet(queueMessageRepository.getDistinctSubQueues()) LOG.debug("Total amount of queue keys [{}].", keySet.size) return keySet } @@ -136,12 +135,12 @@ class MongoMultiQueue : MultiQueue(), HasLogger return if (optionalMessage.isPresent) { val message = optionalMessage.get() - LOG.debug("Found queue type [{}] for UUID: [{}].", message.type, uuid) - Optional.of(message.type) + LOG.debug("Found sub-queue [{}] for UUID: [{}].", message.subQueue, uuid) + Optional.of(message.subQueue) } else { - LOG.debug("No queue type exists for UUID: [{}].", uuid) + LOG.debug("No sub-queue exists for UUID: [{}].", uuid) Optional.empty() } } @@ -163,12 +162,12 @@ class MongoMultiQueue : MultiQueue(), HasLogger * Overriding to use the constant [INDEX_ID] for all look-ups since the ID is shared and needs to be assigned to * the [QueueMessageDocument] before it is created. */ - override fun getNextQueueIndex(queueType: String): Optional + override fun getNextSubQueueIndex(subQueue: String): Optional { val largestIdMessage = queueMessageRepository.findTopByOrderByIdDesc() return if (largestIdMessage.isPresent) { - Optional.ofNullable(largestIdMessage.get().id?.plus(1) ?: 1) + Optional.of(largestIdMessage.get().id?.plus(1) ?: 1) } else { diff --git a/src/main/kotlin/au/kilemon/messagequeue/queue/nosql/mongo/repository/MongoQueueMessageRepository.kt b/src/main/kotlin/au/kilemon/messagequeue/queue/nosql/mongo/repository/MongoQueueMessageRepository.kt index 5091fb3..b8f3a74 100644 --- a/src/main/kotlin/au/kilemon/messagequeue/queue/nosql/mongo/repository/MongoQueueMessageRepository.kt +++ b/src/main/kotlin/au/kilemon/messagequeue/queue/nosql/mongo/repository/MongoQueueMessageRepository.kt @@ -1,6 +1,5 @@ package au.kilemon.messagequeue.queue.nosql.mongo.repository -import au.kilemon.messagequeue.message.QueueMessage import au.kilemon.messagequeue.message.QueueMessageDocument import org.springframework.data.jpa.repository.Modifying import org.springframework.data.mongodb.repository.Aggregation @@ -17,20 +16,20 @@ import java.util.* interface MongoQueueMessageRepository: MongoRepository { /** - * Get a distinct [List] of [String] [QueueMessageDocument.type] that currently exist. + * Get a distinct [List] of [String] [QueueMessageDocument.subQueue] that currently exist. * - * @return a [List] of all the existing [QueueMessageDocument.type] as [String]s + * @return a [List] of all the existing [QueueMessageDocument.subQueue] as [String]s */ - @Aggregation(pipeline = [ "{ '\$group': { '_id' : '\$type' } }" ]) - fun getDistinctTypes(): List + @Aggregation(pipeline = [ "{ '\$group': { '_id' : '\$subQueue' } }" ]) + fun getDistinctSubQueues(): List /** - * Get a list of [QueueMessageDocument] which have [QueueMessageDocument.type] matching the provided [type]. + * Get a list of [QueueMessageDocument] which have [QueueMessageDocument.subQueue] matching the provided [subQueue]. * - * @param type the type to match [QueueMessageDocument.type] with - * @return a [List] of [QueueMessageDocument] who have a matching [QueueMessageDocument.type] with the provided [type] + * @param subQueue the type to match [QueueMessageDocument.subQueue] with + * @return a [List] of [QueueMessageDocument] who have a matching [QueueMessageDocument.subQueue] with the provided [subQueue] */ - fun findByTypeOrderByIdAsc(type: String): List + fun findBySubQueueOrderByIdAsc(subQueue: String): List /** * Find and return a [QueueMessageDocument] that matches the provided [uuid]. @@ -41,13 +40,13 @@ interface MongoQueueMessageRepository: MongoRepository /** - * Delete all [QueueMessageDocument] that have a [QueueMessageDocument.type] that matches the provided [type]. + * Delete all [QueueMessageDocument] that have a [QueueMessageDocument.subQueue] that matches the provided [subQueue]. * - * @param type messages that are assigned this queue type will be removed + * @param subQueue messages that are assigned this sub-queue will be removed * @return the [Int] number of deleted entries */ @Modifying - fun deleteByType(type: String): Int + fun deleteBySubQueue(subQueue: String): Int /** * Delete a [QueueMessageDocument] by `uuid`. @@ -66,30 +65,30 @@ interface MongoQueueMessageRepository: MongoRepository /** - * Find the entity with the matching [QueueMessageDocument.type] and that has a non-null [QueueMessageDocument.assignedTo]. Sorted by ID ascending. + * Find the entity with the matching [QueueMessageDocument.subQueue] and that has a non-null [QueueMessageDocument.assignedTo]. Sorted by ID ascending. * - * @param type the type to match [QueueMessageDocument.type] with - * @return a [List] of [QueueMessageDocument] who have a matching [QueueMessageDocument.type] with the provided [type] and non-null [QueueMessageDocument.assignedTo] + * @param subQueue the type to match [QueueMessageDocument.subQueue] with + * @return a [List] of [QueueMessageDocument] who have a matching [QueueMessageDocument.subQueue] with the provided [subQueue] and non-null [QueueMessageDocument.assignedTo] */ @Transactional - fun findByTypeAndAssignedToIsNotNullOrderByIdAsc(type: String): List + fun findBySubQueueAndAssignedToIsNotNullOrderByIdAsc(subQueue: String): List /** - * Find the entity with the matching [QueueMessageDocument.type] and [QueueMessageDocument.assignedTo]. Sorted by ID ascending. + * Find the entity with the matching [QueueMessageDocument.subQueue] and [QueueMessageDocument.assignedTo]. Sorted by ID ascending. * - * @param type the type to match [QueueMessageDocument.type] with + * @param subQueue the type to match [QueueMessageDocument.subQueue] with * @param assignedTo the identifier to match [QueueMessageDocument.assignedTo] with - * @return a [List] of [QueueMessageDocument] who have a matching [QueueMessageDocument.type] and [QueueMessageDocument.assignedTo] + * @return a [List] of [QueueMessageDocument] who have a matching [QueueMessageDocument.subQueue] and [QueueMessageDocument.assignedTo] */ @Transactional - fun findByTypeAndAssignedToOrderByIdAsc(type: String, assignedTo: String): List + fun findBySubQueueAndAssignedToOrderByIdAsc(subQueue: String, assignedTo: String): List /** - * Find the entity with the matching [QueueMessageDocument.type] and that has [QueueMessageDocument.assignedTo] set to `null`. Sorted by ID ascending. + * Find the entity with the matching [QueueMessageDocument.subQueue] and that has [QueueMessageDocument.assignedTo] set to `null`. Sorted by ID ascending. * - * @param type the type to match [QueueMessageDocument.type] with - * @return a [List] of [QueueMessageDocument] who have a matching [QueueMessageDocument.type] with the provided [type] and `null` [QueueMessageDocument.assignedTo] + * @param subQueue the type to match [QueueMessageDocument.subQueue] with + * @return a [List] of [QueueMessageDocument] who have a matching [QueueMessageDocument.subQueue] with the provided [subQueue] and `null` [QueueMessageDocument.assignedTo] */ @Transactional - fun findByTypeAndAssignedToIsNullOrderByIdAsc(type: String): List + fun findBySubQueueAndAssignedToIsNullOrderByIdAsc(subQueue: String): List } diff --git a/src/main/kotlin/au/kilemon/messagequeue/queue/sql/SqlMultiQueue.kt b/src/main/kotlin/au/kilemon/messagequeue/queue/sql/SqlMultiQueue.kt index d65229d..1764b05 100644 --- a/src/main/kotlin/au/kilemon/messagequeue/queue/sql/SqlMultiQueue.kt +++ b/src/main/kotlin/au/kilemon/messagequeue/queue/sql/SqlMultiQueue.kt @@ -4,13 +4,12 @@ import au.kilemon.messagequeue.logging.HasLogger import au.kilemon.messagequeue.message.QueueMessage import au.kilemon.messagequeue.queue.MultiQueue import au.kilemon.messagequeue.queue.exception.MessageUpdateException -import au.kilemon.messagequeue.queue.sql.repository.SQLQueueMessageRepository +import au.kilemon.messagequeue.queue.sql.repository.SqlQueueMessageRepository import org.slf4j.Logger import org.springframework.beans.factory.annotation.Autowired import org.springframework.context.annotation.Lazy import java.util.* import java.util.concurrent.ConcurrentLinkedQueue -import java.util.concurrent.atomic.AtomicLong /** * A database backed [MultiQueue]. All operations are performed directly on the database it is the complete source of truth. @@ -20,30 +19,30 @@ import java.util.concurrent.atomic.AtomicLong */ class SqlMultiQueue : MultiQueue(), HasLogger { - override val LOG: Logger = initialiseLogger() + override val LOG: Logger = this.initialiseLogger() @Lazy @Autowired - private lateinit var queueMessageRepository: SQLQueueMessageRepository + private lateinit var queueMessageRepository: SqlQueueMessageRepository - override fun getQueueForType(queueType: String): Queue + override fun getSubQueueInternal(subQueue: String): Queue { - val entries = queueMessageRepository.findByTypeOrderByIdAsc(queueType) + val entries = queueMessageRepository.findBySubQueueOrderByIdAsc(subQueue) return ConcurrentLinkedQueue(entries.map { entry -> entry.resolvePayloadObject() }) } /** * Overriding since we can filter via the DB query. */ - override fun getAssignedMessagesForType(queueType: String, assignedTo: String?): Queue + override fun getAssignedMessagesInSubQueue(subQueue: String, assignedTo: String?): Queue { val entries = if (assignedTo == null) { - queueMessageRepository.findByTypeAndAssignedToIsNotNullOrderByIdAsc(queueType) + queueMessageRepository.findBySubQueueAndAssignedToIsNotNullOrderByIdAsc(subQueue) } else { - queueMessageRepository.findByTypeAndAssignedToOrderByIdAsc(queueType, assignedTo) + queueMessageRepository.findBySubQueueAndAssignedToOrderByIdAsc(subQueue, assignedTo) } return ConcurrentLinkedQueue(entries.map { entry -> entry.resolvePayloadObject() }) @@ -52,9 +51,9 @@ class SqlMultiQueue : MultiQueue(), HasLogger /** * Overriding since we can filter via the DB query. */ - override fun getUnassignedMessagesForType(queueType: String): Queue + override fun getUnassignedMessagesInSubQueue(subQueue: String): Queue { - val entries = queueMessageRepository.findByTypeAndAssignedToIsNullOrderByIdAsc(queueType) + val entries = queueMessageRepository.findBySubQueueAndAssignedToIsNullOrderByIdAsc(subQueue) return ConcurrentLinkedQueue(entries.map { entry -> entry.resolvePayloadObject() }) } @@ -65,24 +64,32 @@ class SqlMultiQueue : MultiQueue(), HasLogger override fun getMessageByUUID(uuid: String): Optional { - return queueMessageRepository.findByUuid(uuid) + val message = queueMessageRepository.findByUuid(uuid) + return if (message.isPresent) + { + Optional.of(message.get().resolvePayloadObject()) + } + else + { + Optional.empty() + } } - override fun clearForTypeInternal(queueType: String): Int + override fun clearSubQueueInternal(subQueue: String): Int { - val amountCleared = queueMessageRepository.deleteByType(queueType) - LOG.debug("Cleared existing queue for type [{}]. Removed [{}] message entries.", queueType, amountCleared) + val amountCleared = queueMessageRepository.deleteBySubQueue(subQueue) + LOG.debug("Cleared existing sub-queue [{}]. Removed [{}] message entries.", subQueue, amountCleared) return amountCleared } - override fun isEmptyForType(queueType: String): Boolean + override fun isEmptySubQueue(subQueue: String): Boolean { - return queueMessageRepository.findByTypeOrderByIdAsc(queueType).isEmpty() + return queueMessageRepository.findBySubQueueOrderByIdAsc(subQueue).isEmpty() } - override fun pollInternal(queueType: String): Optional + override fun pollInternal(subQueue: String): Optional { - val messages = queueMessageRepository.findByTypeOrderByIdAsc(queueType) + val messages = queueMessageRepository.findBySubQueueOrderByIdAsc(subQueue) return if (messages.isNotEmpty()) { return Optional.of(messages[0].resolvePayloadObject()) @@ -96,9 +103,9 @@ class SqlMultiQueue : MultiQueue(), HasLogger /** * The [includeEmpty] value makes no difference it is always effectively `false`. */ - override fun keys(includeEmpty: Boolean): Set + override fun keysInternal(includeEmpty: Boolean): HashSet { - val keySet = queueMessageRepository.findDistinctType().toSet() + val keySet = HashSet(queueMessageRepository.findDistinctSubQueue()) LOG.debug("Total amount of queue keys [{}].", keySet.size) return keySet } @@ -109,12 +116,12 @@ class SqlMultiQueue : MultiQueue(), HasLogger return if (optionalMessage.isPresent) { val message = optionalMessage.get() - LOG.debug("Found queue type [{}] for UUID: [{}].", message.type, uuid) - Optional.of(message.type) + LOG.debug("Found sub-queue [{}] for message with UUID: [{}].", message.subQueue, uuid) + Optional.of(message.subQueue) } else { - LOG.debug("No queue type exists for UUID: [{}].", uuid) + LOG.debug("No sub-queue found for message with UUID: [{}].", uuid) Optional.empty() } } @@ -152,7 +159,7 @@ class SqlMultiQueue : MultiQueue(), HasLogger * Overriding to return [Optional.EMPTY] so that the [MultiQueue.add] does set an `id` into the [QueueMessage] * even if the id is `null`. */ - override fun getNextQueueIndex(queueType: String): Optional + override fun getNextSubQueueIndex(subQueue: String): Optional { return Optional.empty() } diff --git a/src/main/kotlin/au/kilemon/messagequeue/queue/sql/repository/SQLQueueMessageRepository.kt b/src/main/kotlin/au/kilemon/messagequeue/queue/sql/repository/SqlQueueMessageRepository.kt similarity index 50% rename from src/main/kotlin/au/kilemon/messagequeue/queue/sql/repository/SQLQueueMessageRepository.kt rename to src/main/kotlin/au/kilemon/messagequeue/queue/sql/repository/SqlQueueMessageRepository.kt index 9f06a4c..12afb42 100644 --- a/src/main/kotlin/au/kilemon/messagequeue/queue/sql/repository/SQLQueueMessageRepository.kt +++ b/src/main/kotlin/au/kilemon/messagequeue/queue/sql/repository/SqlQueueMessageRepository.kt @@ -17,64 +17,64 @@ import java.util.* * @author github.com/Kilemonn */ @Repository -interface SQLQueueMessageRepository: JpaRepository +interface SqlQueueMessageRepository: JpaRepository { /** - * Delete a [QueueMessage] by the provided [QueueMessage.type] [String]. + * Delete a [QueueMessage] by the provided [QueueMessage.subQueue] [String]. * - * @param type the [QueueMessage.type] to remove entries by + * @param subQueue the [QueueMessage.subQueue] to remove entries by * @return the number of deleted entities */ @Modifying @Transactional - @Query("DELETE FROM QueueMessage WHERE type = ?1") - fun deleteByType(type: String): Int + @Query("DELETE FROM QueueMessage WHERE subQueue = ?1") + fun deleteBySubQueue(subQueue: String): Int /** - * Get a distinct [List] of [String] [QueueMessage.type] that currently exist. + * Get a distinct [List] of [String] [QueueMessage.subQueue] that currently exist. * - * @return a [List] of all the existing [QueueMessage.type] as [String]s + * @return a [List] of all the existing [QueueMessage.subQueue] as [String]s */ @Transactional - @Query("SELECT DISTINCT type FROM QueueMessage") - fun findDistinctType(): List + @Query("SELECT DISTINCT subQueue FROM QueueMessage") + fun findDistinctSubQueue(): List /** - * Get a list of [QueueMessage] which have [QueueMessage.type] matching the provided [type]. + * Get a list of [QueueMessage] which have [QueueMessage.subQueue] matching the provided [subQueue]. * - * @param type the type to match [QueueMessage.type] with - * @return a [List] of [QueueMessage] who have a matching [QueueMessage.type] with the provided [type] + * @param subQueue to match [QueueMessage.subQueue] with + * @return a [List] of [QueueMessage] who have a matching [QueueMessage.subQueue] with the provided [subQueue] */ @Transactional - fun findByTypeOrderByIdAsc(type: String): List + fun findBySubQueueOrderByIdAsc(subQueue: String): List /** - * Find the entity with the matching [QueueMessage.type] and that has a non-null [QueueMessage.assignedTo]. Sorted by ID ascending. + * Find the entity with the matching [QueueMessage.subQueue] and that has a non-null [QueueMessage.assignedTo]. Sorted by ID ascending. * - * @param type the type to match [QueueMessage.type] with - * @return a [List] of [QueueMessage] who have a matching [QueueMessage.type] with the provided [type] and non-null [QueueMessage.assignedTo] + * @param subQueue to match [QueueMessage.subQueue] with + * @return a [List] of [QueueMessage] who have a matching [QueueMessage.subQueue] with the provided [subQueue] and non-null [QueueMessage.assignedTo] */ @Transactional - fun findByTypeAndAssignedToIsNotNullOrderByIdAsc(type: String): List + fun findBySubQueueAndAssignedToIsNotNullOrderByIdAsc(subQueue: String): List /** - * Find the entity with the matching [QueueMessage.type] and that has [QueueMessage.assignedTo] set to `null`. Sorted by ID ascending. + * Find the entity with the matching [QueueMessage.subQueue] and that has [QueueMessage.assignedTo] set to `null`. Sorted by ID ascending. * - * @param type the type to match [QueueMessage.type] with - * @return a [List] of [QueueMessage] who have a matching [QueueMessage.type] with the provided [type] and `null` [QueueMessage.assignedTo] + * @param subQueue the type to match [QueueMessage.subQueue] with + * @return a [List] of [QueueMessage] who have a matching [QueueMessage.subQueue] with the provided [subQueue] and `null` [QueueMessage.assignedTo] */ @Transactional - fun findByTypeAndAssignedToIsNullOrderByIdAsc(type: String): List + fun findBySubQueueAndAssignedToIsNullOrderByIdAsc(subQueue: String): List /** - * Find the entity with the matching [QueueMessage.type] and [QueueMessage.assignedTo]. Sorted by ID ascending. + * Find the entity with the matching [QueueMessage.subQueue] and [QueueMessage.assignedTo]. Sorted by ID ascending. * - * @param type the type to match [QueueMessage.type] with + * @param subQueue the type to match [QueueMessage.subQueue] with * @param assignedTo the identifier to match [QueueMessage.assignedTo] with - * @return a [List] of [QueueMessage] who have a matching [QueueMessage.type] and [QueueMessage.assignedTo] + * @return a [List] of [QueueMessage] who have a matching [QueueMessage.subQueue] and [QueueMessage.assignedTo] */ @Transactional - fun findByTypeAndAssignedToOrderByIdAsc(type: String, assignedTo: String): List + fun findBySubQueueAndAssignedToOrderByIdAsc(subQueue: String, assignedTo: String): List /** * Find the entity which has a [QueueMessage.uuid] matching the provided [uuid]. diff --git a/src/main/kotlin/au/kilemon/messagequeue/rest/controller/AuthController.kt b/src/main/kotlin/au/kilemon/messagequeue/rest/controller/AuthController.kt new file mode 100644 index 0000000..c05a3fc --- /dev/null +++ b/src/main/kotlin/au/kilemon/messagequeue/rest/controller/AuthController.kt @@ -0,0 +1,173 @@ +package au.kilemon.messagequeue.rest.controller + +import au.kilemon.messagequeue.authentication.authenticator.MultiQueueAuthenticator +import au.kilemon.messagequeue.authentication.token.JwtTokenProvider +import au.kilemon.messagequeue.filter.JwtAuthenticationFilter +import au.kilemon.messagequeue.logging.HasLogger +import au.kilemon.messagequeue.queue.MultiQueue +import au.kilemon.messagequeue.rest.response.AuthResponse +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.Parameter +import io.swagger.v3.oas.annotations.enums.ParameterIn +import io.swagger.v3.oas.annotations.media.Content +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 org.slf4j.Logger +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.http.HttpStatus +import org.springframework.http.MediaType +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.* + +@Tag(name = AuthController.AUTH_TAG) +@RestController +@RequestMapping(AuthController.AUTH_PATH) +open class AuthController : HasLogger +{ + companion object + { + /** + * The [Tag] for the [AuthController] endpoints. + */ + const val AUTH_TAG: String = "Auth" + + /** + * The base path for the [AuthController]. + */ + const val AUTH_PATH = "/auth" + } + + override val LOG: Logger = this.initialiseLogger() + + @Autowired + private lateinit var multiQueueAuthenticator: MultiQueueAuthenticator + + @Autowired + private lateinit var multiQueue: MultiQueue + + @Autowired + private lateinit var jwtTokenProvider: JwtTokenProvider + + @Operation(summary = "Get restricted sub-queue identifiers", description = "Get a list of the restricted sub-queue identifiers.") + @GetMapping("", produces = [MediaType.APPLICATION_JSON_VALUE]) + @ApiResponses( + ApiResponse(responseCode = "200", description = "Returns a list of sub-queues marked as restricted that require a token to interact with."), + ApiResponse(responseCode = "204", description = "The MultiQueue is in a no-auth mode and no sub-queues are marked as restricted.", content = [Content()]) + ) + fun getRestrictedSubQueueIdentifiers(): ResponseEntity> + { + if (multiQueueAuthenticator.isInNoneMode()) + { + LOG.trace("Returning no restricted identifiers since the restriction mode is set to [{}].", multiQueueAuthenticator.getRestrictionMode()) + return ResponseEntity.noContent().build() + } + + return ResponseEntity.ok(multiQueueAuthenticator.getRestrictedSubQueueIdentifiers()) + } + + @Operation(summary = "Create restriction on sub-queue.", description = "Create restriction a specific sub-queue to require authentication for future interactions and retrieve a token used to interact with this sub-queue.") + @PostMapping("/{${RestParameters.SUB_QUEUE}}", produces = [MediaType.APPLICATION_JSON_VALUE]) + @ApiResponses( + ApiResponse(responseCode = "201", description = "Successfully registered the sub-queue identifier and returns an appropriate token for future access to the sub-queue."), + ApiResponse(responseCode = "204", description = "The MultiQueue is in a no-auth mode and tokens cannot be generated.", content = [Content()]), // Add empty Content() to remove duplicate responses in swagger docsApiResponse(responseCode = "204", description = "No queue messages match the provided UUID.", content = [Content()]) + ApiResponse(responseCode = "409", description = "A sub-queue with the provided identifier is already authorised.", content = [Content()]), + ApiResponse(responseCode = "500", description = "There was an error generating the auth token for the sub-queue.", content = [Content()]) + ) + fun restrictSubQueue(@Parameter(`in` = ParameterIn.PATH, required = true, description = "The sub-queue that you wish to restrict to allow further access only by callers that posses the returned token.") + @PathVariable(required = true, name = RestParameters.SUB_QUEUE) subQueue: String, + /*@Parameter(`in` = ParameterIn.QUERY, required = false, description = "The generated token's expiry in minutes.") + @RequestParam(required = false, name = RestParameters.EXPIRY) expiry: Long?*/): ResponseEntity + { + if (multiQueueAuthenticator.isInNoneMode()) + { + LOG.trace("Requested token for sub-queue [{}] is not provided as queue is in mode [{}].", subQueue, + multiQueueAuthenticator.getRestrictionMode()) + return ResponseEntity.noContent().build() + } + + if (multiQueueAuthenticator.isRestricted(subQueue)) + { + return ResponseEntity.status(HttpStatus.CONFLICT).build() + } + else + { + // Generating the token first, so we don't need to roll back restriction add later if there is a problem + val token = jwtTokenProvider.createTokenForSubQueue(subQueue, null) + if (token.isEmpty) + { + LOG.error("Failed to generated token for sub-queue [{}].", subQueue) + return ResponseEntity.internalServerError().build() + } + + return if (!multiQueueAuthenticator.addRestrictedEntry(subQueue)) + { + LOG.error("Failed to add restriction for sub-queue [{}].", subQueue) + ResponseEntity.internalServerError().build() + } + else + { + LOG.info("Successfully generated token for sub-queue [{}].", subQueue) + ResponseEntity.status(HttpStatus.CREATED).body(AuthResponse(token.get(), subQueue)) + } + } + } + + @Operation(summary = "Remove restriction from sub-queue.", description = "Remove restriction from sub-queue so it can be accessed without restriction.") + @DeleteMapping("/{${RestParameters.SUB_QUEUE}}", produces = [MediaType.APPLICATION_JSON_VALUE]) + @ApiResponses( + ApiResponse(responseCode = "200", description = "Successfully removed restriction for the sub-queue identifier."), + ApiResponse(responseCode = "202", description = "The MultiQueue is in a no-auth mode and sub-queue restrictions are disabled.", content = [Content()]), // Add empty Content() to remove duplicate responses in swagger docsApiResponse(responseCode = "204", description = "No queue messages match the provided UUID.", content = [Content()]) + ApiResponse(responseCode = "204", description = "The requested sub-queue is not currently restricted.", content = [Content()]), + ApiResponse(responseCode = "403", description = "Invalid token provided to remove restriction from requested sub-queue.", content = [Content()]), + ApiResponse(responseCode = "500", description = "There was an error releasing restriction from the sub-queue.", content = [Content()]) + ) + fun removeRestrictionFromSubQueue(@Parameter(`in` = ParameterIn.PATH, required = true, description = "The sub-queue identifier to remove restriction for.") + @PathVariable(required = true, name = RestParameters.SUB_QUEUE) subQueue: String, + @Parameter(`in` = ParameterIn.QUERY, required = false, description = "If restriction is removed successfully indicate whether the sub-queue should be cleared now that it is accessible without a token.") + @RequestParam(required = false, name = RestParameters.CLEAR_QUEUE) clearQueue: Boolean?): ResponseEntity + { + if (multiQueueAuthenticator.isInNoneMode()) + { + LOG.trace("Requested to release authentication for sub-queue [{}] but queue is in mode [{}].", subQueue, + multiQueueAuthenticator.getRestrictionMode()) + return ResponseEntity.accepted().build() + } + + val authedToken = JwtAuthenticationFilter.getSubQueue() + if (authedToken == subQueue) + { + if (multiQueueAuthenticator.isRestricted(subQueue)) + { + return if (multiQueueAuthenticator.removeRestriction(subQueue)) + { + if (clearQueue == true) + { + LOG.info("Restriction removed and clearing sub-queue [{}].", subQueue) + multiQueue.clearSubQueue(subQueue) + } + else + { + LOG.info("Removed restriction from sub-queue [{}] without clearing stored messages.", subQueue) + } + ResponseEntity.ok().build() + } + else + { + LOG.error("Failed to remove restriction for sub-queue [{}].", subQueue) + ResponseEntity.internalServerError().build() + } + } + else + { + LOG.info("Cannot remove restriction from a sub-queue [{}] that is not restricted.", subQueue) + return ResponseEntity.noContent().build() + } + } + else + { + LOG.error("Failed to release authentication for sub-queue [{}] since provided token [{}] is not for the requested sub-queue.", subQueue, authedToken) + return ResponseEntity.status(HttpStatus.FORBIDDEN).build() + } + } +} diff --git a/src/main/kotlin/au/kilemon/messagequeue/rest/controller/MessageQueueController.kt b/src/main/kotlin/au/kilemon/messagequeue/rest/controller/MessageQueueController.kt index c549cd9..b276398 100644 --- a/src/main/kotlin/au/kilemon/messagequeue/rest/controller/MessageQueueController.kt +++ b/src/main/kotlin/au/kilemon/messagequeue/rest/controller/MessageQueueController.kt @@ -1,9 +1,11 @@ package au.kilemon.messagequeue.rest.controller -import au.kilemon.messagequeue.queue.exception.DuplicateMessageException +import au.kilemon.messagequeue.authentication.authenticator.MultiQueueAuthenticator import au.kilemon.messagequeue.logging.HasLogger import au.kilemon.messagequeue.message.QueueMessage import au.kilemon.messagequeue.queue.MultiQueue +import au.kilemon.messagequeue.queue.cache.redis.RedisMultiQueue +import au.kilemon.messagequeue.queue.exception.DuplicateMessageException import au.kilemon.messagequeue.queue.exception.HealthCheckFailureException import au.kilemon.messagequeue.rest.response.MessageResponse import io.swagger.v3.oas.annotations.Hidden @@ -14,7 +16,6 @@ import io.swagger.v3.oas.annotations.media.Content 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 lombok.Generated import org.slf4j.Logger import org.springframework.beans.factory.annotation.Autowired import org.springframework.http.HttpStatus @@ -25,7 +26,7 @@ import org.springframework.web.server.ResponseStatusException import java.util.* import java.util.stream.Collectors import javax.validation.Valid -import kotlin.collections.HashMap +import kotlin.collections.HashSet /** * The REST controller for the [MultiQueue]. It exposes endpoints to access and manipulate the queue and the messages inside it. @@ -38,7 +39,7 @@ import kotlin.collections.HashMap @RequestMapping(MessageQueueController.MESSAGE_QUEUE_BASE_PATH) open class MessageQueueController : HasLogger { - override val LOG: Logger = initialiseLogger() + override val LOG: Logger = this.initialiseLogger() companion object { @@ -63,7 +64,7 @@ open class MessageQueueController : HasLogger const val ENDPOINT_ENTRY: String = "/entry" /** - * The resource path used to view messages that are within the same `sub queue`. + * The resource path used to view messages that are within the same `sub-queue`. */ const val ENDPOINT_TYPE: String = "/type" @@ -104,9 +105,10 @@ open class MessageQueueController : HasLogger } @Autowired - @get:Generated - @set:Generated - lateinit var messageQueue: MultiQueue + private lateinit var messageQueue: MultiQueue + + @Autowired + private lateinit var authenticator: MultiQueueAuthenticator /** * Retrieve information about the whole [MultiQueue]. Specifically data related information. @@ -115,7 +117,7 @@ open class MessageQueueController : HasLogger @Operation(summary = "Retrieve queue information for the whole multi queue.", description = "Retrieve information about the whole queue, specifically information on the queue entries.") @GetMapping(ENDPOINT_TYPE, produces = [MediaType.APPLICATION_JSON_VALUE]) @ApiResponse(responseCode = "200", description = "Successfully returns the information payload.") - fun getAllQueueTypeInfo(): ResponseEntity + fun getAllQueueInfo(): ResponseEntity { val size = messageQueue.size LOG.debug("Returning total multi-queue size [{}].", size) @@ -123,18 +125,20 @@ open class MessageQueueController : HasLogger } /** - * Retrieve information about a specific queue within [MultiQueue], based on the provided `queueType`. Specifically data related information. + * Retrieve information about a specific queue within [MultiQueue], based on the provided `sub-queue`. Specifically data related information. */ @Hidden - @Operation(summary = "Retrieve queue information for a specific sub queue.", description = "Retrieve information about the specified queueType within the queue, specifically information on the queue entries.") - @GetMapping("$ENDPOINT_TYPE/{${RestParameters.QUEUE_TYPE}}", produces = [MediaType.APPLICATION_JSON_VALUE]) + @Operation(summary = "Retrieve queue information for a specific sub-queue.", description = "Retrieve information about the specified sub-queue, specifically information on the queue entries.") + @GetMapping("$ENDPOINT_TYPE/{${RestParameters.SUB_QUEUE}}", produces = [MediaType.APPLICATION_JSON_VALUE]) @ApiResponse(responseCode = "200", description = "Successfully returns the information payload.") - fun getQueueTypeInfo(@Parameter(`in` = ParameterIn.PATH, required = true, description = "The queueType to retrieve information about.") - @PathVariable(name = RestParameters.QUEUE_TYPE) queueType: String): ResponseEntity + fun getSubQueueInfo(@Parameter(`in` = ParameterIn.PATH, required = true, description = "The sub-queue to retrieve information about.") + @PathVariable(name = RestParameters.SUB_QUEUE) subQueue: String): ResponseEntity { - val queueForType = messageQueue.getQueueForType(queueType) - LOG.debug("Returning size [{}] for queue with type [{}].", queueForType.size, queueType) - return ResponseEntity.ok(queueForType.size.toString()) + authenticator.canAccessSubQueue(subQueue) + + val queue = messageQueue.getSubQueue(subQueue) + LOG.debug("Returning size [{}] for sub-queue [{}].", queue.size, subQueue) + return ResponseEntity.ok(queue.size.toString()) } /** @@ -157,7 +161,7 @@ open class MessageQueueController : HasLogger } catch(ex: HealthCheckFailureException) { - LOG.error("Health check failed.") + LOG.error("Health check failed.", ex) ResponseEntity.internalServerError().build() } } @@ -170,7 +174,7 @@ open class MessageQueueController : HasLogger * @param uuid the [UUID] of the message to retrieve * @return [MessageResponse] containing the found [QueueMessage] otherwise a [HttpStatus.NO_CONTENT] exception will be thrown */ - @Operation(summary = "Retrieve a queue message by UUID.", description = "Retrieve a queue message regardless of its sub queue, directly by UUID.") + @Operation(summary = "Retrieve a queue message by UUID.", description = "Retrieve a queue message regardless of its sub-queue, directly by UUID.") @GetMapping("$ENDPOINT_ENTRY/{${RestParameters.UUID}}", produces = [MediaType.APPLICATION_JSON_VALUE]) @ApiResponses( ApiResponse(responseCode = "200", description = "Successfully returns the queue message matching the provided UUID."), @@ -182,7 +186,8 @@ open class MessageQueueController : HasLogger if (entry.isPresent) { val foundEntry = entry.get() - LOG.debug("Found message with UUID [{}] in queue with type [{}].", foundEntry.uuid, foundEntry.type) + authenticator.canAccessSubQueue(foundEntry.subQueue) + LOG.debug("Found message with UUID [{}] in sub-queue [{}].", foundEntry.uuid, foundEntry.subQueue) return ResponseEntity.ok(MessageResponse(foundEntry)) } @@ -211,93 +216,140 @@ open class MessageQueueController : HasLogger { try { + authenticator.canAccessSubQueue(queueMessage.subQueue) + if (queueMessage.assignedTo != null && queueMessage.assignedTo!!.isBlank()) { queueMessage.assignedTo = null } + val wasAdded = messageQueue.add(queueMessage) if (wasAdded) { - LOG.debug("Added new message with UUID [{}] to queue with type [{}}.", queueMessage.uuid, queueMessage.type) + LOG.debug("Added new message with UUID [{}] to sub-queue [{}}.", queueMessage.uuid, queueMessage.subQueue) return ResponseEntity.status(HttpStatus.CREATED).body(MessageResponse(queueMessage)) } else { - LOG.error("Failed to add entry with UUID [{}] to queue with type [{}]. AND the message does not already exist. This could be a memory limitation or an issue with the underlying collection.", queueMessage.uuid, queueMessage.type) - throw ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Failed to add entry with UUID: [${queueMessage.uuid}] to queue with type [${queueMessage.type}]") + LOG.error("Failed to add entry with UUID [{}] to sub-queue [{}]. AND the message does not already exist. This could be a memory limitation or an issue with the underlying collection.", queueMessage.uuid, queueMessage.subQueue) + throw ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Failed to add entry with UUID: [${queueMessage.uuid}] to sub-queue [${queueMessage.subQueue}]") } } catch (ex: DuplicateMessageException) { - val queueType = messageQueue.containsUUID(queueMessage.uuid).get() - val errorMessage = "Failed to add entry with UUID [${queueMessage.uuid}], an entry with the same UUID already exists in queue with type [$queueType]." + val subQueue = messageQueue.containsUUID(queueMessage.uuid).get() + val errorMessage = "Failed to add entry with UUID [${queueMessage.uuid}], an entry with the same UUID already exists in sub-queue [$subQueue]." LOG.error(errorMessage) throw ResponseStatusException(HttpStatus.CONFLICT, errorMessage, ex) } } /** - * A [GetMapping] which returns a list of all the `QueueTypes` defined in the [MultiQueue]. + * A [GetMapping] which returns a list of all the `sub-queue` IDs defined in the [MultiQueue]. * * @param includeEmpty to include `keys` which one had elements stored against them but don't at the moment. Default is `true`. - * @return a [Set] of [String] `queueType`s + * @return a [Set] of [String] `sub-queue`s */ - @Operation(summary = "Retrieve a list of all keys.", description = "Retrieve a list of all sub queue key values in the multi queue.") + @Operation(summary = "Retrieve a list of all keys.", description = "Retrieve a list of all sub-queue key values in the multi queue.") @GetMapping(ENDPOINT_KEYS, produces = [MediaType.APPLICATION_JSON_VALUE]) @ApiResponse(responseCode = "200", description = "Successfully returns the list of keys.") fun getKeys(@Parameter(`in` = ParameterIn.QUERY, required = false, description = "Indicates whether to include keys that currently have zero entries (but have had entries previously). Is true by default.") @RequestParam(required = false, name = RestParameters.INCLUDE_EMPTY) includeEmpty: Boolean?): ResponseEntity> { - return ResponseEntity.ok(messageQueue.keys(includeEmpty ?: true)) + val keys = messageQueue.keys(includeEmpty ?: true) + if (messageQueue is RedisMultiQueue) + { + return ResponseEntity.ok((messageQueue as RedisMultiQueue).removePrefix(keys)) + } + return ResponseEntity.ok(keys) } - @Operation(summary = "Delete a keys or all keys, in turn clearing that sub queue.", description = "Delete the sub queue that matches the provided key. If no key is provided, all sub queues will be cleared.") + @Operation(summary = "Delete a keys or all keys, in turn clearing that sub-queue.", description = "Delete the sub-queue that matches the provided key. If no key is provided, all sub-queues will be cleared.") @DeleteMapping(ENDPOINT_KEYS, produces = [MediaType.APPLICATION_JSON_VALUE]) - @ApiResponse(responseCode = "204", description = "Successfully cleared the sub queue(s) with the provided key, or all sub queues if the key is null.") - fun deleteKeys(@Parameter(`in` = ParameterIn.QUERY, required = false, description = "The queue type to clear the sub queue of. If it is not provided, all sub queues will be cleared.") - @RequestParam(required = false, name = RestParameters.QUEUE_TYPE) queueType: String?): ResponseEntity + @ApiResponses( + ApiResponse(responseCode = "204", description = "Successfully cleared the sub-queue(s) with the provided key, or all sub-queues if the key is null."), + ApiResponse(responseCode = "206", description = "Successfully cleared the sub-queues that are unrestricted. Restricted sub-queues needs to be created with a valid token.") + ) + fun deleteKeys(@Parameter(`in` = ParameterIn.QUERY, required = false, description = "The sub-queue to clear. If it is not provided, all sub-queues will be cleared.") + @RequestParam(required = false, name = RestParameters.SUB_QUEUE) subQueue: String?): ResponseEntity { - if (queueType != null) + if (subQueue != null) { - messageQueue.clearForType(queueType) + authenticator.canAccessSubQueue(subQueue) + messageQueue.clearSubQueue(subQueue) + LOG.info("Cleared queue with key [{}]", subQueue) + return ResponseEntity.noContent().build() } else { - messageQueue.clear() + val clearedKeys = HashSet() + val retainedKeys = HashSet() + var anyAreNotCleared = false + + for (key in messageQueue.keys()) + { + if (authenticator.canAccessSubQueue(key, false)) + { + messageQueue.clearSubQueue(key) + clearedKeys.add(key) + } + else + { + anyAreNotCleared = true + retainedKeys.add(key) + } + } + + LOG.info("Cleared queue with keys: [{}]", clearedKeys) + + return if (anyAreNotCleared) + { + LOG.info("Retained queues with keys: [{}]", retainedKeys) + ResponseEntity.status(HttpStatus.PARTIAL_CONTENT).build() + } + else + { + ResponseEntity.noContent().build() + } } - return ResponseEntity.noContent().build() } /** * A [GetMapping] endpoint which retrieves all the stored [QueueMessage]s that are currently available in the [MultiQueue]. * * @param detailed *true* if you require detailed information about each message and their payload/owner, otherwise **false** which displayed only limited information about each message - * @param queueType the `type` to include, if provided only messages in this `queueType` will be retrieved. - * @return a [Map] where the `key` is the `queueType` and the `value` is a comma separated list of all the [QueueMessage.removePayload] + * @param subQueue the `sub-queue` to include, if provided only messages in this `sub-queue` will be retrieved. + * @return a [Map] where the `key` is the `sub-queue` and the `value` is a comma separated list of all the [QueueMessage.removePayload] */ - @Operation(summary = "Retrieve a limited or full version of the held messages.", description = "Retrieve queue message summaries for the held messages. This can be limited to a specific sub queue type and complete message detail to be included in the response if requested.") + @Operation(summary = "Retrieve a limited or full version of the held messages.", description = "Retrieve queue message summaries for the held messages. This can be limited to a specific sub-queue and complete message detail to be included in the response if requested.") @GetMapping(ENDPOINT_ALL, produces = [MediaType.APPLICATION_JSON_VALUE]) - @ApiResponse(responseCode = "200", description = "Successfully returns the list of summary entries for either the whole multi-queue or the sub queue.") - fun getAll(@Parameter(`in` = ParameterIn.QUERY, required = false, description = "Indicates whether the response messages should contain all message details including the underlying payload. By default details are hidden.") @RequestParam(required = false, name = RestParameters.DETAILED) detailed: Boolean = false, - @Parameter(`in` = ParameterIn.QUERY, required = false, description = "The sub queue type to search, if not provide all messages in the whole multi-queue will be returned.") @RequestParam(required = false, name = RestParameters.QUEUE_TYPE) queueType: String?): ResponseEntity>> + @ApiResponse(responseCode = "200", description = "Successfully returns the list of summary entries for either the whole multi-queue or the sub-queue.") + fun getAll(@Parameter(`in` = ParameterIn.QUERY, required = false, description = "Indicates whether the response messages should contain all message details including the underlying payload. By default details are hidden.") + @RequestParam(required = false, name = RestParameters.DETAILED) detailed: Boolean = false, + @Parameter(`in` = ParameterIn.QUERY, required = false, description = "The sub-queue to search, if not provide all messages in the whole multi-queue will be returned.") + @RequestParam(required = false, name = RestParameters.SUB_QUEUE) subQueue: String?): ResponseEntity>> { val responseMap = HashMap>() - if ( !queueType.isNullOrBlank()) + if ( !subQueue.isNullOrBlank()) { - LOG.debug("Retrieving all entry details from queue with type [{}].", queueType) - val queueForType: Queue = messageQueue.getQueueForType(queueType) - val queueDetails = queueForType.stream().map { message -> message.removePayload(detailed) }.collect(Collectors.toList()) - responseMap[queueType] = queueDetails + LOG.debug("Retrieving all entry details from sub-queue [{}].", subQueue) + authenticator.canAccessSubQueue(subQueue) + val queue: Queue = messageQueue.getSubQueue(subQueue) + val queueDetails = queue.stream().map { message -> message.removePayload(detailed) }.collect(Collectors.toList()) + responseMap[subQueue] = queueDetails } else { - LOG.debug("Retrieving all entry details from all queue types.") + LOG.debug("Retrieving all entry details from all sub-queues.") for (key: String in messageQueue.keys(false)) { - // No need to empty check since we passed `false` to `keys()` above - val queueForType: Queue = messageQueue.getQueueForType(key) - val queueDetails = queueForType.stream().map { message -> message.removePayload(detailed) }.collect(Collectors.toList()) - responseMap[key] = queueDetails + if (authenticator.canAccessSubQueue(key, false)) + { + // No need to empty check since we passed `false` to `keys()` above + val queue: Queue = messageQueue.getSubQueue(key) + val queueDetails = queue.stream().map { message -> message.removePayload(detailed) }.collect(Collectors.toList()) + responseMap[key] = queueDetails + } } } return ResponseEntity.ok(responseMap) @@ -307,18 +359,22 @@ open class MessageQueueController : HasLogger * Retrieve all owned [QueueMessage] based on the provided user identifier. * * @param assignedTo the identifier used to indicate the owner of the [QueueMessage]s to return - * @param queueType the `queueType` to search for the related [QueueMessage] owned by [assignedTo] - * @return a [List] of [QueueMessage] based on messages that are `assigned` to the [assignedTo] in the `queue` mapped to [queueType] + * @param subQueue the `sub-queue` to search for the related [QueueMessage] owned by [assignedTo] + * @return a [List] of [QueueMessage] based on messages that are `assigned` to the [assignedTo] in the `queue` mapped to [subQueue] */ - @Operation(summary = "Retrieve all owned queue messages based on the provided user identifier.", description = "Retrieve all owned messages for the provided assignee identifier for the provided sub queue type.") + @Operation(summary = "Retrieve all owned queue messages based on the provided user identifier.", description = "Retrieve all owned messages for the provided assignee identifier for the provided sub-queue.") @GetMapping(ENDPOINT_OWNED, produces = [MediaType.APPLICATION_JSON_VALUE]) - @ApiResponse(responseCode = "200", description = "Successfully returns the list of owned queue messages in the sub queue for the provided assignee identifier.") - fun getOwned(@Parameter(`in` = ParameterIn.QUERY, required = true, description = "The identifier that must match the message's `assigned` property in order to be returned.") @RequestParam(required = true, name = RestParameters.ASSIGNED_TO) assignedTo: String, - @Parameter(`in` = ParameterIn.QUERY, required = true, description = "The sub queue to search for the assigned messages.") @RequestParam(required = true, name = RestParameters.QUEUE_TYPE) queueType: String): ResponseEntity> + @ApiResponse(responseCode = "200", description = "Successfully returns the list of owned queue messages in the sub-queue for the provided assignee identifier.") + fun getOwned(@Parameter(`in` = ParameterIn.QUERY, required = true, description = "The identifier that must match the message's `assigned` property in order to be returned.") + @RequestParam(required = true, name = RestParameters.ASSIGNED_TO) assignedTo: String, + @Parameter(`in` = ParameterIn.QUERY, required = true, description = "The sub-queue to search for the assigned messages.") + @RequestParam(required = true, name = RestParameters.SUB_QUEUE) subQueue: String): ResponseEntity> { - val assignedMessages: Queue = messageQueue.getAssignedMessagesForType(queueType, assignedTo) + authenticator.canAccessSubQueue(subQueue) + + val assignedMessages: Queue = messageQueue.getAssignedMessagesInSubQueue(subQueue, assignedTo) val ownedMessages = assignedMessages.stream().map { message -> MessageResponse(message) }.collect(Collectors.toList()) - LOG.debug("Found [{}] owned entries within queue with type [{}] for user with identifier [{}].", ownedMessages.size, queueType, assignedTo) + LOG.debug("Found [{}] owned entries within sub-queue [{}] for user with identifier [{}].", ownedMessages.size, subQueue, assignedTo) return ResponseEntity.ok(ownedMessages) } @@ -334,10 +390,10 @@ open class MessageQueueController : HasLogger * @return the [QueueMessage] object after it has been marked as `assigned`. Returns [HttpStatus.ACCEPTED] if the [QueueMessage] is already assigned to the current user, otherwise [HttpStatus.OK] if it was not `assigned` previously. */ @Operation(summary = "Assign an existing queue message to the provided identifier.", description = "Assign an existing queue message to the provided identifier. The message must already exist and not be assigned already to another identifier in order to be successfully assigned to the provided identifier.") - @PutMapping("$ENDPOINT_ASSIGN/{${RestParameters.UUID}}", produces = [MediaType.APPLICATION_JSON_VALUE]) + @PutMapping("$ENDPOINT_ENTRY/{${RestParameters.UUID}}$ENDPOINT_ASSIGN", produces = [MediaType.APPLICATION_JSON_VALUE]) @ApiResponses( ApiResponse(responseCode = "200", description = "Successfully assigned the message to the provided identifier. The message was not previously assigned."), - ApiResponse(responseCode = "202", description = "The message was already assigned to the provided identifier."), + ApiResponse(responseCode = "202", description = "The message was already assigned to the provided identifier.", content = [Content()]), ApiResponse(responseCode = "204", description = "No queue messages match the provided UUID.", content = [Content()]), ApiResponse(responseCode = "409", description = "The message is already assigned to another identifier.", content = [Content()]) ) @@ -348,18 +404,19 @@ open class MessageQueueController : HasLogger if (message.isPresent) { val messageToAssign = message.get() + authenticator.canAccessSubQueue(messageToAssign.subQueue) if (!messageToAssign.assignedTo.isNullOrBlank()) { if (messageToAssign.assignedTo == assignedTo) { // The message is already in this state, returning 202 to tell the client that it is accepted but no action was done - LOG.debug("Message with uuid [{}] in queue with type [{}] is already assigned to the identifier [{}].", messageToAssign.uuid, messageToAssign.type, assignedTo) + LOG.debug("Message with uuid [{}] in sub-queue [{}] is already assigned to the identifier [{}].", messageToAssign.uuid, messageToAssign.subQueue, assignedTo) return ResponseEntity.accepted().body(MessageResponse(messageToAssign)) } else { - LOG.error("Message with uuid [{}] in queue with type [{}] is already assigned to the identifier [{}]. Attempting to assign to identifier [{}].", messageToAssign.uuid, messageToAssign.type, messageToAssign.assignedTo, assignedTo) - throw ResponseStatusException(HttpStatus.CONFLICT, "The message with UUID: [$uuid] and [${messageToAssign.type}] is already assigned to the identifier [${messageToAssign.assignedTo}].") + LOG.error("Message with uuid [{}] in sub-queue [{}] is already assigned to the identifier [{}]. Attempting to assign to identifier [{}].", messageToAssign.uuid, messageToAssign.subQueue, messageToAssign.assignedTo, assignedTo) + throw ResponseStatusException(HttpStatus.CONFLICT, "The message with UUID: [$uuid] and [${messageToAssign.subQueue}] is already assigned to the identifier [${messageToAssign.assignedTo}].") } } @@ -375,33 +432,37 @@ open class MessageQueueController : HasLogger } /** - * Retrieve the next `non-assigned` message in the [MultiQueue] for the provided [queueType] and assign it to the provided identifier [assignedTo]. + * Retrieve the next `non-assigned` message in the [MultiQueue] for the provided [subQueue] and assign it to the provided identifier [assignedTo]. * - * @param queueType the sub queue that the next [QueueMessage] should be from + * @param subQueue the sub-queue that the next [QueueMessage] should be from * @param assignedTo the identifier that the next [QueueMessage] should be `assigned` to before being returned - * @return the next [QueueMessage] that is not `assigned` in the provided [queueType]. If none exist then [HttpStatus.NO_CONTENT] will be returned indicating that the queue is either empty or has no available [QueueMessage]s to assign. + * @return the next [QueueMessage] that is not `assigned` in the provided [subQueue]. If none exist then [HttpStatus.NO_CONTENT] will be returned indicating that the queue is either empty or has no available [QueueMessage]s to assign. */ - @Operation(summary = "Retrieve the next available unassigned message in the queue.", description = "Retrieve the next available message in the queue for the provided sub queue identifier that is not assigned. The message will be assigned to the provided identifier then returned.") + @Operation(summary = "Retrieve the next available unassigned message in the queue.", description = "Retrieve the next available message in the queue for the provided sub-queue identifier that is not assigned. The message will be assigned to the provided identifier then returned.") @PutMapping(ENDPOINT_NEXT, produces = [MediaType.APPLICATION_JSON_VALUE]) @ApiResponses( ApiResponse(responseCode = "200", description = "Successfully returns the next message in queue after assigning it to the provided `assignedTo` identifier."), ApiResponse(responseCode = "204", description = "No messages are available.", content = [Content()]) ) - fun getNext(@Parameter(`in` = ParameterIn.QUERY, required = true, description = "The sub queue identifier to query the next available message from.") @RequestParam(required = true, name = RestParameters.QUEUE_TYPE) queueType: String, - @Parameter(`in` = ParameterIn.QUERY, required = true, description = "The identifier to assign the next available message to if one exists.") @RequestParam(required = true, name = RestParameters.ASSIGNED_TO) assignedTo: String): ResponseEntity + fun getNext(@Parameter(`in` = ParameterIn.QUERY, required = true, description = "The sub-queue identifier to query the next available message from.") + @RequestParam(required = true, name = RestParameters.SUB_QUEUE) subQueue: String, + @Parameter(`in` = ParameterIn.QUERY, required = true, description = "The identifier to assign the next available message to if one exists.") + @RequestParam(required = true, name = RestParameters.ASSIGNED_TO) assignedTo: String): ResponseEntity { - val queueForType: Queue = messageQueue.getUnassignedMessagesForType(queueType) - return if (queueForType.iterator().hasNext()) + authenticator.canAccessSubQueue(subQueue) + + val queue: Queue = messageQueue.getUnassignedMessagesInSubQueue(subQueue) + return if (queue.iterator().hasNext()) { - val nextUnassignedMessage = queueForType.iterator().next() - LOG.debug("Retrieving and assigning next message for queue type [{}] with UUID [{}] to identifier [{}].", queueType, nextUnassignedMessage.uuid, assignedTo) + val nextUnassignedMessage = queue.iterator().next() + LOG.debug("Retrieving and assigning next message for sub-queue [{}] with UUID [{}] to identifier [{}].", subQueue, nextUnassignedMessage.uuid, assignedTo) nextUnassignedMessage.assignedTo = assignedTo messageQueue.persistMessage(nextUnassignedMessage) ResponseEntity.ok(MessageResponse(nextUnassignedMessage)) } else { - LOG.debug("No unassigned entries in queue with type [{}].", queueType) + LOG.debug("No unassigned entries in sub-queue [{}].", subQueue) ResponseEntity.noContent().build() } } @@ -418,7 +479,7 @@ open class MessageQueueController : HasLogger * @return the [QueueMessage] object after it has been `released`. Returns [HttpStatus.ACCEPTED] if the [QueueMessage] is already `released`, otherwise [HttpStatus.OK] if it was `released` successfully. */ @Operation(summary = "Release the message assigned to the provided identifier.", description = "Release an assigned message so it can be assigned to another identifier.") - @PutMapping("$ENDPOINT_RELEASE/{${RestParameters.UUID}}", produces = [MediaType.APPLICATION_JSON_VALUE]) + @PutMapping("$ENDPOINT_ENTRY/{${RestParameters.UUID}}$ENDPOINT_RELEASE", produces = [MediaType.APPLICATION_JSON_VALUE]) @ApiResponses( ApiResponse(responseCode = "200", description = "Successfully released the message. The message was previously assigned."), ApiResponse(responseCode = "202", description = "The message is not currently assigned."), @@ -432,6 +493,8 @@ open class MessageQueueController : HasLogger if (message.isPresent) { val messageToRelease = message.get() + authenticator.canAccessSubQueue(messageToRelease.subQueue) + if (messageToRelease.assignedTo == null) { // The message is already in this state, returning 202 to tell the client that it is accepted but no action was done @@ -441,7 +504,7 @@ open class MessageQueueController : HasLogger if (!assignedTo.isNullOrBlank() && messageToRelease.assignedTo != assignedTo) { - val errorMessage = "The message with UUID: [$uuid] and [${messageToRelease.type}] cannot be released because it is already assigned to identifier [${messageToRelease.assignedTo}] and the provided identifier was [$assignedTo]." + val errorMessage = "The message with UUID: [$uuid] and [${messageToRelease.subQueue}] cannot be released because it is already assigned to identifier [${messageToRelease.assignedTo}] and the provided identifier was [$assignedTo]." LOG.error(errorMessage) throw ResponseStatusException(HttpStatus.CONFLICT, errorMessage) } @@ -451,7 +514,7 @@ open class MessageQueueController : HasLogger return ResponseEntity.ok(MessageResponse(messageToRelease)) } - // No entries match the provided UUID (and queue type) + // No entries match the provided UUID (and sub-queue) LOG.debug("Could not find message to release with UUID [{}].", uuid) return ResponseEntity.noContent().build() } @@ -482,9 +545,10 @@ open class MessageQueueController : HasLogger if (message.isPresent) { val messageToRemove = message.get() + authenticator.canAccessSubQueue(messageToRemove.subQueue) if ( !assignedTo.isNullOrBlank() && messageToRemove.assignedTo != assignedTo) { - val errorMessage = "Unable to remove message with UUID [$uuid] in Queue [${messageToRemove.type}] because the provided assignee identifier: [$assignedTo] does not match the message's assignee identifier: [${messageToRemove.assignedTo}]" + val errorMessage = "Unable to remove message with UUID [$uuid] in Queue [${messageToRemove.subQueue}] because the provided assignee identifier: [$assignedTo] does not match the message's assignee identifier: [${messageToRemove.assignedTo}]" LOG.error(errorMessage) throw ResponseStatusException(HttpStatus.FORBIDDEN, errorMessage) } @@ -506,14 +570,15 @@ open class MessageQueueController : HasLogger * "identifier2": ["queue-A", "queue-B", "queue-Z"] * } * ``` - * @param queueType the sub-queue identifier that you are interested in. If not provided, all sub-queues will be iterated through. + * @param subQueue the sub-queue identifier that you are interested in. If not provided, all sub-queues will be iterated through. * @return a [Map] of assignee identifiers mapped to a [Set] of the sub-queue identifiers that they have any assigned messages in. */ @Operation(summary = "Retrieve all unique owner identifiers for either a specified sub-queue or all sub-queues.", description = "Retrieve all owner identifier mapped to a list of the sub-queue identifiers that they are assigned any messages in.") @GetMapping(ENDPOINT_OWNERS, produces = [MediaType.APPLICATION_JSON_VALUE]) @ApiResponse(responseCode = "200", description = "Successfully returns the map of owner identifiers mapped to all the sub-queues that they have one or more assigned messages in.") - fun getOwners(@Parameter(`in` = ParameterIn.QUERY, required = false, description = "The sub queue to search for the owner identifiers.") @RequestParam(required = false, name = RestParameters.QUEUE_TYPE) queueType: String?): ResponseEntity>> + fun getOwners(@Parameter(`in` = ParameterIn.QUERY, required = false, description = "The sub-queue to search for the owner identifiers.") + @RequestParam(required = false, name = RestParameters.SUB_QUEUE) subQueue: String?): ResponseEntity>> { - return ResponseEntity.ok(messageQueue.getOwnersAndKeysMap(queueType)) + return ResponseEntity.ok(messageQueue.getOwnersAndKeysMap(subQueue)) } } diff --git a/src/main/kotlin/au/kilemon/messagequeue/rest/controller/RestParameters.kt b/src/main/kotlin/au/kilemon/messagequeue/rest/controller/RestParameters.kt index 079c5ba..6d1e5cb 100644 --- a/src/main/kotlin/au/kilemon/messagequeue/rest/controller/RestParameters.kt +++ b/src/main/kotlin/au/kilemon/messagequeue/rest/controller/RestParameters.kt @@ -12,11 +12,15 @@ object RestParameters { const val ASSIGNED_TO = "assignedTo" - const val QUEUE_TYPE = "queueType" + const val SUB_QUEUE = "subQueue" const val DETAILED = "detailed" const val UUID = "uuid" const val INCLUDE_EMPTY = "includeEmpty" + + const val EXPIRY = "expiry" + + const val CLEAR_QUEUE = "clearQueue" } diff --git a/src/main/kotlin/au/kilemon/messagequeue/rest/controller/SettingsController.kt b/src/main/kotlin/au/kilemon/messagequeue/rest/controller/SettingsController.kt index 785d76d..55a946a 100644 --- a/src/main/kotlin/au/kilemon/messagequeue/rest/controller/SettingsController.kt +++ b/src/main/kotlin/au/kilemon/messagequeue/rest/controller/SettingsController.kt @@ -4,7 +4,6 @@ import au.kilemon.messagequeue.settings.MessageQueueSettings import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.responses.ApiResponse import io.swagger.v3.oas.annotations.tags.Tag -import lombok.Generated import org.springframework.beans.factory.annotation.Autowired import org.springframework.http.MediaType import org.springframework.http.ResponseEntity @@ -36,9 +35,7 @@ open class SettingsController } @Autowired - @get:Generated - @set:Generated - lateinit var queueSettings: MessageQueueSettings + private lateinit var queueSettings: MessageQueueSettings /** * Get and return the [MessageQueueSettings] singleton for the user to view configuration. diff --git a/src/main/kotlin/au/kilemon/messagequeue/rest/response/AuthResponse.kt b/src/main/kotlin/au/kilemon/messagequeue/rest/response/AuthResponse.kt new file mode 100644 index 0000000..f06333c --- /dev/null +++ b/src/main/kotlin/au/kilemon/messagequeue/rest/response/AuthResponse.kt @@ -0,0 +1,13 @@ +package au.kilemon.messagequeue.rest.response + +import au.kilemon.messagequeue.filter.CorrelationIdFilter +import com.fasterxml.jackson.annotation.JsonPropertyOrder +import org.slf4j.MDC + +/** + * A response object which wraps the response jwt token. + * + * @author github.com/Kilemonn + */ +@JsonPropertyOrder("correlationId", "subQueue", "token") +class AuthResponse(val token: String, val subQueue: String, val correlationId: String? = MDC.get(CorrelationIdFilter.CORRELATION_ID)) diff --git a/src/main/kotlin/au/kilemon/messagequeue/rest/response/MessageResponse.kt b/src/main/kotlin/au/kilemon/messagequeue/rest/response/MessageResponse.kt index a89ad9b..6a53b13 100644 --- a/src/main/kotlin/au/kilemon/messagequeue/rest/response/MessageResponse.kt +++ b/src/main/kotlin/au/kilemon/messagequeue/rest/response/MessageResponse.kt @@ -6,9 +6,9 @@ import com.fasterxml.jackson.annotation.JsonPropertyOrder import org.slf4j.MDC /** - * A response object which wraps the [QueueMessage], and exposes the `type` [String]. + * A response object which wraps the [QueueMessage], and exposes the `sub-queue` [String]. * * @author github.com/Kilemonn */ -@JsonPropertyOrder("correlationId", "queueType", "message") -data class MessageResponse(val message: QueueMessage, val queueType: String = message.type, val correlationId: String? = MDC.get(CorrelationIdFilter.CORRELATION_ID)) +@JsonPropertyOrder("correlationId", "subQueue", "message") +data class MessageResponse(val message: QueueMessage, val subQueue: String = message.subQueue, val correlationId: String? = MDC.get(CorrelationIdFilter.CORRELATION_ID)) diff --git a/src/main/kotlin/au/kilemon/messagequeue/rest/response/RestResponseExceptionHandler.kt b/src/main/kotlin/au/kilemon/messagequeue/rest/response/RestResponseExceptionHandler.kt index 1ed3e3e..6748788 100644 --- a/src/main/kotlin/au/kilemon/messagequeue/rest/response/RestResponseExceptionHandler.kt +++ b/src/main/kotlin/au/kilemon/messagequeue/rest/response/RestResponseExceptionHandler.kt @@ -1,5 +1,9 @@ package au.kilemon.messagequeue.rest.response +import au.kilemon.messagequeue.authentication.exception.MultiQueueAuthenticationException +import au.kilemon.messagequeue.authentication.exception.MultiQueueAuthorisationException +import au.kilemon.messagequeue.queue.exception.IllegalSubQueueIdentifierException +import org.springframework.http.HttpStatus import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.ControllerAdvice import org.springframework.web.bind.annotation.ExceptionHandler @@ -19,4 +23,22 @@ class RestResponseExceptionHandler: ResponseEntityExceptionHandler() { return ResponseEntity(ErrorResponse(ex.reason), ex.status) } + + @ExceptionHandler(MultiQueueAuthorisationException::class) + fun handleMultiQueueAuthorisationException(ex: MultiQueueAuthorisationException): ResponseEntity + { + return ResponseEntity(ErrorResponse(ex.message), HttpStatus.FORBIDDEN) + } + + @ExceptionHandler(MultiQueueAuthenticationException::class) + fun handleMultiQueueAuthenticationException(ex: MultiQueueAuthenticationException): ResponseEntity + { + return ResponseEntity(ErrorResponse(ex.message), HttpStatus.UNAUTHORIZED) + } + + @ExceptionHandler(IllegalSubQueueIdentifierException::class) + fun handleIllegalSubQueueIdentifierException(ex: IllegalSubQueueIdentifierException): ResponseEntity + { + return ResponseEntity(ErrorResponse(ex.message), HttpStatus.BAD_REQUEST) + } } diff --git a/src/main/kotlin/au/kilemon/messagequeue/settings/MessageQueueSettings.kt b/src/main/kotlin/au/kilemon/messagequeue/settings/MessageQueueSettings.kt index b26d63a..f56d154 100644 --- a/src/main/kotlin/au/kilemon/messagequeue/settings/MessageQueueSettings.kt +++ b/src/main/kotlin/au/kilemon/messagequeue/settings/MessageQueueSettings.kt @@ -4,15 +4,14 @@ import com.fasterxml.jackson.annotation.JsonProperty import com.google.gson.annotations.SerializedName import lombok.Generated import org.springframework.beans.factory.annotation.Value -import org.springframework.beans.factory.config.ConfigurableBeanFactory -import org.springframework.context.annotation.Scope import org.springframework.stereotype.Component /** * An object that holds application level properties and is set initially on application start up. * This will control things such as: * - Credentials to external data storage - * - The type of `MultiQueue` being used + * - The storage medium of `MultiQueue` being used + * - The restriction mode being used * - Other utility configuration for the application to use. * * When `SQL` is used, the following property must be provided: @@ -26,21 +25,23 @@ class MessageQueueSettings { companion object { - const val MULTI_QUEUE_TYPE: String = "MULTI_QUEUE_TYPE" - const val MULTI_QUEUE_TYPE_DEFAULT: String = "IN_MEMORY" + private const val MESSAGE_QUEUE: String = "message-queue" + + const val STORAGE_MEDIUM: String = "$MESSAGE_QUEUE.storage-medium" + const val STORAGE_MEDIUM_DEFAULT: String = "IN_MEMORY" /** * Start redis related properties */ - const val REDIS_PREFIX: String = "REDIS_PREFIX" - - const val REDIS_ENDPOINT: String = "REDIS_ENDPOINT" + private const val REDIS: String = "redis" + const val REDIS_PREFIX: String = "$MESSAGE_QUEUE.$REDIS.prefix" + const val REDIS_ENDPOINT: String = "$MESSAGE_QUEUE.$REDIS.endpoint" const val REDIS_ENDPOINT_DEFAULT: String = "127.0.0.1" // Redis sentinel related properties - const val REDIS_USE_SENTINELS: String = "REDIS_USE_SENTINELS" + const val REDIS_USE_SENTINELS: String = "$MESSAGE_QUEUE.$REDIS.sentinel" - const val REDIS_MASTER_NAME: String = "REDIS_MASTER_NAME" + const val REDIS_MASTER_NAME: String = "$MESSAGE_QUEUE.$REDIS.master-name" const val REDIS_MASTER_NAME_DEFAULT: String = "mymaster" /** @@ -65,23 +66,51 @@ class MessageQueueSettings */ const val SQL_SCHEMA: String = "SQL_SCHEMA" const val SQL_SCHEMA_DEFAULT: String = "public" + + /** + * Start authenticated sub-queue properties. + */ + /** + * Indicates what authentication mode the `MultiQueue` should be in. + */ + const val RESTRICTION_MODE: String = "$MESSAGE_QUEUE.restriction-mode" + const val RESTRICTION_MODE_DEFAULT: String = "NONE" + + /** + * A property that is passed through to the [au.kilemon.messagequeue.authentication.token.JwtTokenProvider] and + * used as the token generation and verification key. If this is not provided, a new key will be generated each + * time the application starts. + */ + const val ACCESS_TOKEN_KEY: String = "$MESSAGE_QUEUE.access-token.key" } /** - * `Optional` uses the [MULTI_QUEUE_TYPE] environment variable to determine where - * the underlying multi queue is persisted. It can be any value of [MultiQueueType]. - * Defaults to [MultiQueueType.IN_MEMORY] ([MULTI_QUEUE_TYPE_DEFAULT]). + * `Optional` uses the [STORAGE_MEDIUM] environment variable to determine where + * the underlying multi queue is persisted. It can be any value of [StorageMedium]. + * Defaults to [StorageMedium.IN_MEMORY] ([STORAGE_MEDIUM_DEFAULT]). + */ + @SerializedName(STORAGE_MEDIUM) + @JsonProperty(STORAGE_MEDIUM) + @Value("\${$STORAGE_MEDIUM:$STORAGE_MEDIUM_DEFAULT}") + @get:Generated + @set:Generated + lateinit var storageMedium: String + + /** + * `Optional` uses the [RESTRICTION_MODE] environment variable to determine whether specific sub-queues + * will require authentication or not to create or access. It can be any value of [StorageMedium]. + * Defaults to [StorageMedium.IN_MEMORY] ([RESTRICTION_MODE_DEFAULT]). */ - @SerializedName(MULTI_QUEUE_TYPE) - @JsonProperty(MULTI_QUEUE_TYPE) - @Value("\${$MULTI_QUEUE_TYPE:$MULTI_QUEUE_TYPE_DEFAULT}") + @SerializedName(RESTRICTION_MODE) + @JsonProperty(RESTRICTION_MODE) + @Value("\${$RESTRICTION_MODE:$RESTRICTION_MODE_DEFAULT}") @get:Generated @set:Generated - lateinit var multiQueueType: String + lateinit var restrictionMode: String /** - * `Optional` when [MULTI_QUEUE_TYPE] is set to [MultiQueueType.REDIS]. + * `Optional` when [STORAGE_MEDIUM] is set to [StorageMedium.REDIS]. * Uses the [REDIS_PREFIX] to set a prefix used for all redis entry keys. * * E.g. if the initial value for the redis entry is "my-key" and no prefix is defined the entries would be stored under "my-key". @@ -95,7 +124,7 @@ class MessageQueueSettings lateinit var redisPrefix: String /** - * `Required` when [MULTI_QUEUE_TYPE] is set to [MultiQueueType.REDIS]. + * `Required` when [STORAGE_MEDIUM] is set to [StorageMedium.REDIS]. * The input endpoint string which is used for both standalone and the sentinel redis configurations. * This supports a comma separated list or single definition of a redis endpoint in the following formats: * `:,:,` @@ -110,7 +139,7 @@ class MessageQueueSettings lateinit var redisEndpoint: String /** - * `Optional` when [MULTI_QUEUE_TYPE] is set to [MultiQueueType.REDIS]. + * `Optional` when [STORAGE_MEDIUM] is set to [StorageMedium.REDIS]. * Indicates whether the `MultiQueue` should connect directly to the redis instance or connect via one or more sentinel instances. * If set to `true` the `MultiQueue` will create a sentinel pool connection instead of a direct connection which is what would occur if this is left as `false`. * By default, this is `false`. @@ -123,7 +152,7 @@ class MessageQueueSettings lateinit var redisUseSentinels: String /** - * `Optional` when [MULTI_QUEUE_TYPE] is set to [MultiQueueType.REDIS]. + * `Optional` when [STORAGE_MEDIUM] is set to [StorageMedium.REDIS]. * `Required` when [redisUseSentinels] is set to `true`. Is used to indicate the name of the redis master instance. * By default, this is [REDIS_MASTER_NAME_DEFAULT]. */ @@ -135,7 +164,7 @@ class MessageQueueSettings lateinit var redisMasterName: String /** - * `Required` when [MULTI_QUEUE_TYPE] is set to [MultiQueueType.SQL]. + * `Required` when [STORAGE_MEDIUM] is set to [StorageMedium.SQL]. * This defines the database connection string e.g: * `"jdbc:mysql://localhost:3306/message-queue"` */ @@ -147,7 +176,7 @@ class MessageQueueSettings lateinit var sqlEndpoint: String /** - * `Required` when [MULTI_QUEUE_TYPE] is set to [MultiQueueType.SQL]. + * `Required` when [STORAGE_MEDIUM] is set to [StorageMedium.SQL]. * This is the username/account name used to access the database defined in [SQL_ENDPOINT]. */ @SerializedName(SQL_USERNAME) @@ -158,7 +187,7 @@ class MessageQueueSettings lateinit var sqlUsername: String /** - * `Required` when [MULTI_QUEUE_TYPE] is set to [MultiQueueType.SQL]. + * `Required` when [STORAGE_MEDIUM] is set to [StorageMedium.SQL]. * This is the password used to access the database defined in [SQL_ENDPOINT]. */ // TODO: Commenting out since it is unused and returned in the settings endpoint without masking @@ -169,7 +198,7 @@ class MessageQueueSettings // lateinit var sqlPassword: String /** - * Required when [MultiQueueType.MONGO] is used and [mongoUri] is empty. + * Required when [StorageMedium.MONGO] is used and [mongoUri] is empty. * It specifies the host name that the mongo db is available at. */ @SerializedName(MONGO_HOST) @@ -180,7 +209,7 @@ class MessageQueueSettings lateinit var mongoHost: String /** - * Required when [MultiQueueType.MONGO] is used and [mongoUri] is empty. + * Required when [StorageMedium.MONGO] is used and [mongoUri] is empty. * It specifies the port that the mongo db is available on. */ @SerializedName(MONGO_PORT) @@ -191,7 +220,7 @@ class MessageQueueSettings lateinit var mongoPort: String /** - * Required when [MultiQueueType.MONGO] is used and [mongoUri] is empty. + * Required when [StorageMedium.MONGO] is used and [mongoUri] is empty. * It specifies the database you wish to connect to. */ @SerializedName(MONGO_DATABASE) @@ -202,7 +231,7 @@ class MessageQueueSettings lateinit var mongoDatabase: String /** - * Required when [MultiQueueType.MONGO] is used and [mongoUri] is empty. + * Required when [StorageMedium.MONGO] is used and [mongoUri] is empty. * It specifies the username that you wish to connect with. */ @SerializedName(MONGO_USERNAME) @@ -213,7 +242,7 @@ class MessageQueueSettings lateinit var mongoUsername: String /** - * Required when [MultiQueueType.MONGO] is used and [mongoUri] is empty. + * Required when [StorageMedium.MONGO] is used and [mongoUri] is empty. * It specifies the password for the user that you wish to connect with. */ // TODO: Commenting out since it is unused and returned in the settings endpoint without masking @@ -224,7 +253,7 @@ class MessageQueueSettings // lateinit var mongoPassword: String /** - * Required when [MultiQueueType.MONGO] is used and the above mongo properties are empty. + * Required when [StorageMedium.MONGO] is used and the above mongo properties are empty. * It specifies all properties of the mongo connection in the format of `mongodb://:@:/`. */ @SerializedName(MONGO_URI) diff --git a/src/main/kotlin/au/kilemon/messagequeue/settings/MultiQueueType.kt b/src/main/kotlin/au/kilemon/messagequeue/settings/StorageMedium.kt similarity index 96% rename from src/main/kotlin/au/kilemon/messagequeue/settings/MultiQueueType.kt rename to src/main/kotlin/au/kilemon/messagequeue/settings/StorageMedium.kt index 4e1ff15..db97125 100644 --- a/src/main/kotlin/au/kilemon/messagequeue/settings/MultiQueueType.kt +++ b/src/main/kotlin/au/kilemon/messagequeue/settings/StorageMedium.kt @@ -6,7 +6,7 @@ package au.kilemon.messagequeue.settings * * @author github.com/Kilemonn */ -enum class MultiQueueType +enum class StorageMedium { /** * Will initialise an in-memory multiqueue to store queue messages. diff --git a/src/test/kotlin/au/kilemon/messagequeue/authentication/AuthenticationMatrixTest.kt b/src/test/kotlin/au/kilemon/messagequeue/authentication/AuthenticationMatrixTest.kt new file mode 100644 index 0000000..92f4dc8 --- /dev/null +++ b/src/test/kotlin/au/kilemon/messagequeue/authentication/AuthenticationMatrixTest.kt @@ -0,0 +1,54 @@ +package au.kilemon.messagequeue.authentication + +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test + +/** + * A test class for the [AuthenticationMatrix]. + * Specifically to verify the [equals] method. + * + * @author github.com/Kilemonn + */ +class AuthenticationMatrixTest +{ + /** + * Ensure that two [AuthenticationMatrix] with the same appropriate properties are `equal` when the [equals] + * method is called on them. + */ + @Test + fun testEquals() + { + val authMatrix1 = AuthenticationMatrix("authMatrix") + val authMatrix2 = AuthenticationMatrix("authMatrix") + val authMatrix3 = AuthenticationMatrix("authMatrix3") + + Assertions.assertEquals(authMatrix1, authMatrix1) + Assertions.assertEquals(authMatrix1, authMatrix2) + Assertions.assertEquals(authMatrix2, authMatrix1) + Assertions.assertEquals(authMatrix2, authMatrix2) + + Assertions.assertNotEquals(authMatrix1, authMatrix3) + Assertions.assertNotEquals(authMatrix2, authMatrix3) + } + + /** + * Ensure [AuthenticationMatrix.equals] returns `false` when `null` is passed in. + */ + @Test + fun testEquals_nullArg() + { + val authMatrix = AuthenticationMatrix("authMatrix") + Assertions.assertNotEquals(authMatrix, null) + } + + /** + * Ensure [AuthenticationMatrix.equals] returns `false` when a non-[AuthenticationMatrix] object is passed in. + */ + @Test + fun testEquals_differentObject() + { + val authMatrix = AuthenticationMatrix("authMatrix") + val string = "test" + Assertions.assertNotEquals(authMatrix, string) + } +} diff --git a/src/test/kotlin/au/kilemon/messagequeue/authentication/authenticator/MultiQueueAuthenticatorTest.kt b/src/test/kotlin/au/kilemon/messagequeue/authentication/authenticator/MultiQueueAuthenticatorTest.kt new file mode 100644 index 0000000..437cdc4 --- /dev/null +++ b/src/test/kotlin/au/kilemon/messagequeue/authentication/authenticator/MultiQueueAuthenticatorTest.kt @@ -0,0 +1,338 @@ +package au.kilemon.messagequeue.authentication.authenticator + +import au.kilemon.messagequeue.authentication.RestrictionMode +import au.kilemon.messagequeue.authentication.exception.MultiQueueAuthorisationException +import au.kilemon.messagequeue.filter.JwtAuthenticationFilter +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import org.mockito.Mockito +import org.slf4j.MDC +import org.springframework.boot.test.mock.mockito.SpyBean + +/** + * An abstract test class for the [MultiQueueAuthenticator] class. + * This class can be extended, and the [MultiQueueAuthenticator] member overridden to easily ensure that the different + * [MultiQueueAuthenticator] implementations all operate as expected in the same test cases. + * + * @author github.com/Kilemonn + */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +abstract class MultiQueueAuthenticatorTest +{ + @SpyBean + protected lateinit var multiQueueAuthenticator: MultiQueueAuthenticator + + /** + * Ensure [MultiQueueAuthenticator.addRestrictedEntry] always returns and does not add an entry if the + * [MultiQueueAuthenticator.getRestrictionMode] is [RestrictionMode.NONE]. + */ + @Test + fun testAddRestrictedEntry_WithNoneMode() + { + Assertions.assertEquals(RestrictionMode.NONE, multiQueueAuthenticator.getRestrictionMode()) + val subQueue = "testAddRestrictedEntry_WithNoneMode" + Assertions.assertFalse(multiQueueAuthenticator.isRestricted(subQueue)) + Assertions.assertFalse(multiQueueAuthenticator.addRestrictedEntry(subQueue)) + Assertions.assertFalse(multiQueueAuthenticator.isRestricted(subQueue)) + } + + /** + * Ensure [MultiQueueAuthenticator.addRestrictedEntry] will add the request sub-queue identifier when the + * [MultiQueueAuthenticator.getRestrictionMode] is NOT [RestrictionMode.NONE]. + * Also tests [MultiQueueAuthenticator.isRestricted] can determine the sub-queue is restricted after its been added. + */ + @Test + fun testAddRestrictedEntry_WithARestrictedNoneMode() + { + Mockito.doReturn(RestrictionMode.RESTRICTED).`when`(multiQueueAuthenticator).getRestrictionMode() + Assertions.assertEquals(RestrictionMode.RESTRICTED, multiQueueAuthenticator.getRestrictionMode()) + val subQueue = "testAddRestrictedEntry_WithARestrictedNoneMode" + + Assertions.assertFalse(multiQueueAuthenticator.isRestricted(subQueue)) + Assertions.assertTrue(multiQueueAuthenticator.addRestrictedEntry(subQueue)) + Assertions.assertTrue(multiQueueAuthenticator.isRestricted(subQueue)) + } + + /** + * Ensure that [MultiQueueAuthenticator.removeRestriction] will not be able to remove a restriction if the + * [MultiQueueAuthenticator.getRestrictionMode] is set to [RestrictionMode.NONE]. + */ + @Test + fun testRemoveRestriction_WithNoneMode() + { + Mockito.doReturn(RestrictionMode.RESTRICTED).`when`(multiQueueAuthenticator).getRestrictionMode() + Assertions.assertEquals(RestrictionMode.RESTRICTED, multiQueueAuthenticator.getRestrictionMode()) + val subQueue = "testRemoveRestriction_WithNoneMode" + Assertions.assertFalse(multiQueueAuthenticator.isRestricted(subQueue)) + + Assertions.assertTrue(multiQueueAuthenticator.addRestrictedEntry(subQueue)) + Assertions.assertTrue(multiQueueAuthenticator.isRestricted(subQueue)) + + Mockito.doReturn(RestrictionMode.NONE).`when`(multiQueueAuthenticator).getRestrictionMode() + Assertions.assertEquals(RestrictionMode.NONE, multiQueueAuthenticator.getRestrictionMode()) + Assertions.assertFalse(multiQueueAuthenticator.removeRestriction(subQueue)) + } + + /** + * Ensure that [MultiQueueAuthenticator.addRestrictedEntry] returns `false` when an existing sub-queue identifier + * is attempting to be added. + */ + @Test + fun testRemoveRestriction_AddExistingSubQueueIdentifier() + { + Mockito.doReturn(RestrictionMode.RESTRICTED).`when`(multiQueueAuthenticator).getRestrictionMode() + Assertions.assertEquals(RestrictionMode.RESTRICTED, multiQueueAuthenticator.getRestrictionMode()) + val subQueue = "testRemoveRestriction_AddExistingSubQueueIdentifier" + Assertions.assertFalse(multiQueueAuthenticator.isRestricted(subQueue)) + + Assertions.assertTrue(multiQueueAuthenticator.addRestrictedEntry(subQueue)) + Assertions.assertTrue(multiQueueAuthenticator.isRestricted(subQueue)) + + Assertions.assertFalse(multiQueueAuthenticator.addRestrictedEntry(subQueue)) + Assertions.assertTrue(multiQueueAuthenticator.isRestricted(subQueue)) + } + + /** + * Ensure that [MultiQueueAuthenticator.removeRestriction] will not be able to remove a restriction if the + * [MultiQueueAuthenticator.getRestrictionMode] is set to [RestrictionMode.NONE]. + */ + @Test + fun testRemoveRestriction_WithARestrictedNoneMode() + { + Mockito.doReturn(RestrictionMode.RESTRICTED).`when`(multiQueueAuthenticator).getRestrictionMode() + Assertions.assertEquals(RestrictionMode.RESTRICTED, multiQueueAuthenticator.getRestrictionMode()) + val subQueue = "testRemoveRestriction_WithARestrictedNoneMode" + Assertions.assertFalse(multiQueueAuthenticator.isRestricted(subQueue)) + + multiQueueAuthenticator.addRestrictedEntry(subQueue) + Assertions.assertTrue(multiQueueAuthenticator.isRestricted(subQueue)) + + Assertions.assertTrue(multiQueueAuthenticator.removeRestriction(subQueue)) + Assertions.assertFalse(multiQueueAuthenticator.isRestricted(subQueue)) + } + + /** + * Ensure that [MultiQueueAuthenticator.removeRestriction] returns `false` when you attempt to remove a sub-queue + * identifier that does not exist. + */ + @Test + fun testRemoveRestriction_DoesNotExist() + { + Mockito.doReturn(RestrictionMode.RESTRICTED).`when`(multiQueueAuthenticator).getRestrictionMode() + Assertions.assertEquals(RestrictionMode.RESTRICTED, multiQueueAuthenticator.getRestrictionMode()) + val subQueue = "testRemoveRestriction_DoesNotExist" + Assertions.assertFalse(multiQueueAuthenticator.isRestricted(subQueue)) + Assertions.assertFalse(multiQueueAuthenticator.removeRestriction(subQueue)) + } + + /** + * Ensure that [MultiQueueAuthenticator.canAccessSubQueue] never throws when its in + * [MultiQueueAuthenticator.isInNoneMode]. + */ + @Test + fun testCanAccessSubQueue_WithNoneMode() + { + Assertions.assertEquals(RestrictionMode.NONE, multiQueueAuthenticator.getRestrictionMode()) + val subQueue = "testCanAccessSubQueue_WithNoneMode" + Assertions.assertTrue(multiQueueAuthenticator.canAccessSubQueue(subQueue)) + } + + /** + * Ensure that [MultiQueueAuthenticator.canAccessSubQueue] never throws when its in + * [MultiQueueAuthenticator.isInHybridMode] and the sub-queue identifier is not marked as restricted. + */ + @Test + fun testCanAccessSubQueue_WithHybridMode_isNotRestricted() + { + Mockito.doReturn(RestrictionMode.HYBRID).`when`(multiQueueAuthenticator).getRestrictionMode() + Assertions.assertEquals(RestrictionMode.HYBRID, multiQueueAuthenticator.getRestrictionMode()) + + val subQueue = "testCanAccessSubQueue_WithHybridMode_isNotRestricted" + Assertions.assertFalse(multiQueueAuthenticator.isRestricted(subQueue)) + + Assertions.assertTrue(multiQueueAuthenticator.canAccessSubQueue(subQueue)) + } + + /** + * Ensure that [MultiQueueAuthenticator.canAccessSubQueue] never throws when its in + * [MultiQueueAuthenticator.isInHybridMode] and the sub-queue identifier is marked as restricted AND matches + * the stored sub-queue identifier from the auth token. + */ + @Test + fun testCanAccessSubQueue_WithHybridMode_isRestricted_matchesStoredSubQueue() + { + Mockito.doReturn(RestrictionMode.HYBRID).`when`(multiQueueAuthenticator).getRestrictionMode() + Assertions.assertEquals(RestrictionMode.HYBRID, multiQueueAuthenticator.getRestrictionMode()) + + val subQueue = "testCanAccessSubQueue_WithHybridMode_isRestricted_matchesStoredSubQueue" + Assertions.assertTrue(multiQueueAuthenticator.addRestrictedEntry(subQueue)) + Assertions.assertTrue(multiQueueAuthenticator.isRestricted(subQueue)) + + try + { + MDC.put(JwtAuthenticationFilter.SUB_QUEUE, subQueue) + Assertions.assertTrue(multiQueueAuthenticator.canAccessSubQueue(subQueue)) + } + finally + { + MDC.clear() + } + } + + /** + * Ensure that [MultiQueueAuthenticator.canAccessSubQueue] DOES throw a [MultiQueueAuthorisationException] when its + * in [MultiQueueAuthenticator.isInHybridMode] and the sub-queue identifier is marked as restricted AND does NOT + * match the stored sub-queue identifier from the auth token. + */ + @Test + fun testCanAccessSubQueue_WithHybridMode_isRestricted_doesNotMatchStoredSubQueue() + { + Mockito.doReturn(RestrictionMode.HYBRID).`when`(multiQueueAuthenticator).getRestrictionMode() + Assertions.assertEquals(RestrictionMode.HYBRID, multiQueueAuthenticator.getRestrictionMode()) + + val subQueue = "testCanAccessSubQueue_WithHybridMode_isRestricted_doesNotMatchStoredSubQueue" + Assertions.assertTrue(multiQueueAuthenticator.addRestrictedEntry(subQueue)) + Assertions.assertTrue(multiQueueAuthenticator.isRestricted(subQueue)) + + try + { + MDC.put(JwtAuthenticationFilter.SUB_QUEUE, "does not match sub-queue") + Assertions.assertThrows(MultiQueueAuthorisationException::class.java) { + multiQueueAuthenticator.canAccessSubQueue(subQueue) + } + Assertions.assertFalse(multiQueueAuthenticator.canAccessSubQueue(subQueue, false)) + } + finally + { + MDC.clear() + } + } + + /** + * Ensure that [MultiQueueAuthenticator.canAccessSubQueue] DOES throw a [MultiQueueAuthorisationException] when its + * in [MultiQueueAuthenticator.isInRestrictedMode] and the sub-queue identifier is NOT marked as restricted. + */ + @Test + fun testCanAccessSubQueue_WithRestrictedMode_isNotRestricted() + { + Mockito.doReturn(RestrictionMode.RESTRICTED).`when`(multiQueueAuthenticator).getRestrictionMode() + Assertions.assertEquals(RestrictionMode.RESTRICTED, multiQueueAuthenticator.getRestrictionMode()) + + val subQueue = "testCanAccessSubQueue_WithRestrictedMode_isNotRestricted" + Assertions.assertFalse(multiQueueAuthenticator.isRestricted(subQueue)) + Assertions.assertThrows(MultiQueueAuthorisationException::class.java) { + multiQueueAuthenticator.canAccessSubQueue(subQueue) + } + Assertions.assertFalse(multiQueueAuthenticator.canAccessSubQueue(subQueue, false)) + } + + /** + * Ensure that [MultiQueueAuthenticator.canAccessSubQueue] does NOT throw when its + * in [MultiQueueAuthenticator.isInRestrictedMode] and the sub-queue identifier is marked as restricted AND the + * stored sub-queue identifier from the token matches the requested token. + */ + @Test + fun testCanAccessSubQueue_WithRestrictedMode_isRestricted_matchesStoredSubQueue() + { + Mockito.doReturn(RestrictionMode.RESTRICTED).`when`(multiQueueAuthenticator).getRestrictionMode() + Assertions.assertEquals(RestrictionMode.RESTRICTED, multiQueueAuthenticator.getRestrictionMode()) + + val subQueue = "testCanAccessSubQueue_WithRestrictedMode_isRestricted_matchesStoredSubQueue" + Assertions.assertTrue(multiQueueAuthenticator.addRestrictedEntry(subQueue)) + Assertions.assertTrue(multiQueueAuthenticator.isRestricted(subQueue)) + + try + { + MDC.put(JwtAuthenticationFilter.SUB_QUEUE, subQueue) + multiQueueAuthenticator.canAccessSubQueue(subQueue) + Assertions.assertTrue(multiQueueAuthenticator.canAccessSubQueue(subQueue)) + } + finally + { + MDC.clear() + } + } + + /** + * Ensure that [MultiQueueAuthenticator.canAccessSubQueue] DOES throw a [MultiQueueAuthorisationException] when its + * in [MultiQueueAuthenticator.isInRestrictedMode] and the sub-queue identifier is marked as restricted and the + * provided sub-queue identifier does NOT match the identifier provided in the auth token. + */ + @Test + fun testCanAccessSubQueue_WithRestrictedMode_isRestricted_doesNotMatchStoredSubQueue() + { + Mockito.doReturn(RestrictionMode.RESTRICTED).`when`(multiQueueAuthenticator).getRestrictionMode() + Assertions.assertEquals(RestrictionMode.RESTRICTED, multiQueueAuthenticator.getRestrictionMode()) + + val subQueue = "testCanAccessSubQueue_WithRestrictedMode_isRestricted_doesNotMatchStoredSubQueue" + Assertions.assertTrue(multiQueueAuthenticator.addRestrictedEntry(subQueue)) + Assertions.assertTrue(multiQueueAuthenticator.isRestricted(subQueue)) + + try + { + MDC.put(JwtAuthenticationFilter.SUB_QUEUE, "does not match sub-queue") + Assertions.assertThrows(MultiQueueAuthorisationException::class.java) { + multiQueueAuthenticator.canAccessSubQueue(subQueue) + } + Assertions.assertFalse(multiQueueAuthenticator.canAccessSubQueue(subQueue, false)) + } + finally + { + MDC.clear() + } + } + + /** + * Ensure that [MultiQueueAuthenticator.clearRestrictedSubQueues] will clear all sub-queue restrictions. + */ + @Test + fun testClearRestrictedSubQueues() + { + Mockito.doReturn(RestrictionMode.RESTRICTED).`when`(multiQueueAuthenticator).getRestrictionMode() + Assertions.assertEquals(RestrictionMode.RESTRICTED, multiQueueAuthenticator.getRestrictionMode()) + + val subQueues = listOf("testClearRestrictedSubQueues1", "testClearRestrictedSubQueues2", "testClearRestrictedSubQueues3", + "testClearRestrictedSubQueues4", "testClearRestrictedSubQueues5", "testClearRestrictedSubQueues6") + + subQueues.forEach { subQueue -> Assertions.assertFalse(multiQueueAuthenticator.isRestricted(subQueue)) } + subQueues.forEach { subQueue -> Assertions.assertTrue(multiQueueAuthenticator.addRestrictedEntry(subQueue)) } + subQueues.forEach { subQueue -> Assertions.assertTrue(multiQueueAuthenticator.isRestricted(subQueue)) } + multiQueueAuthenticator.clearRestrictedSubQueues() + subQueues.forEach { subQueue -> Assertions.assertFalse(multiQueueAuthenticator.isRestricted(subQueue)) } + Assertions.assertTrue(multiQueueAuthenticator.getRestrictedSubQueueIdentifiers().isEmpty()) + } + + /** + * Ensure that [MultiQueueAuthenticator.getRestrictedSubQueueIdentifiers] will retrieve the restrict sub-queue + * identifiers even when new entries are added/removed and cleared. + */ + @Test + fun testGetRestrictedSubQueueIdentifiers() + { + Mockito.doReturn(RestrictionMode.RESTRICTED).`when`(multiQueueAuthenticator).getRestrictionMode() + Assertions.assertEquals(RestrictionMode.RESTRICTED, multiQueueAuthenticator.getRestrictionMode()) + + val subQueues = listOf("testGetRestrictedSubQueueIdentifiers1", "testGetRestrictedSubQueueIdentifiers2", + "testGetRestrictedSubQueueIdentifiers3", "testGetRestrictedSubQueueIdentifiers4", + "testGetRestrictedSubQueueIdentifiers5", "testGetRestrictedSubQueueIdentifiers6") + + subQueues.forEach { subQueue -> Assertions.assertFalse(multiQueueAuthenticator.isRestricted(subQueue)) } + subQueues.forEach { subQueue -> Assertions.assertTrue(multiQueueAuthenticator.addRestrictedEntry(subQueue)) } + subQueues.forEach { subQueue -> Assertions.assertTrue(multiQueueAuthenticator.isRestricted(subQueue)) } + + val restrictedIdentifiers = multiQueueAuthenticator.getRestrictedSubQueueIdentifiers() + restrictedIdentifiers.forEach { identifier -> Assertions.assertTrue(multiQueueAuthenticator.isRestricted(identifier)) } + + Assertions.assertEquals(subQueues.size, restrictedIdentifiers.size) + + Assertions.assertTrue(multiQueueAuthenticator.removeRestriction(restrictedIdentifiers.elementAt(0))) + val updatedIdentifiers = multiQueueAuthenticator.getRestrictedSubQueueIdentifiers() + Assertions.assertEquals(restrictedIdentifiers.size - 1, updatedIdentifiers.size) + Assertions.assertFalse(updatedIdentifiers.contains(restrictedIdentifiers.elementAt(0))) + + multiQueueAuthenticator.clearRestrictedSubQueues() + val emptyIdentifiers = multiQueueAuthenticator.getRestrictedSubQueueIdentifiers() + Assertions.assertTrue(emptyIdentifiers.isEmpty()) + } +} diff --git a/src/test/kotlin/au/kilemon/messagequeue/authentication/authenticator/cache/redis/RedisSentinelAuthenticatorTest.kt b/src/test/kotlin/au/kilemon/messagequeue/authentication/authenticator/cache/redis/RedisSentinelAuthenticatorTest.kt new file mode 100644 index 0000000..fe5953d --- /dev/null +++ b/src/test/kotlin/au/kilemon/messagequeue/authentication/authenticator/cache/redis/RedisSentinelAuthenticatorTest.kt @@ -0,0 +1,98 @@ +package au.kilemon.messagequeue.authentication.authenticator.cache.redis + +import au.kilemon.messagequeue.authentication.authenticator.MultiQueueAuthenticatorTest +import au.kilemon.messagequeue.configuration.QueueConfiguration +import au.kilemon.messagequeue.configuration.cache.redis.RedisConfiguration +import au.kilemon.messagequeue.logging.LoggingConfiguration +import au.kilemon.messagequeue.queue.MultiQueueTest +import au.kilemon.messagequeue.settings.MessageQueueSettings +import org.junit.jupiter.api.AfterAll +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.extension.ExtendWith +import org.springframework.boot.test.util.TestPropertyValues +import org.springframework.context.ApplicationContextInitializer +import org.springframework.context.ConfigurableApplicationContext +import org.springframework.context.annotation.Import +import org.springframework.test.context.ContextConfiguration +import org.springframework.test.context.TestPropertySource +import org.springframework.test.context.junit.jupiter.SpringExtension +import org.testcontainers.containers.GenericContainer +import org.testcontainers.junit.jupiter.Testcontainers +import org.testcontainers.utility.DockerImageName + +/** + * A test class for [RedisAuthenticator] running in a sentinel configuration. + * + * @author github.com/Kilemonn + */ +@ExtendWith(SpringExtension::class) +@TestPropertySource(properties = ["${MessageQueueSettings.STORAGE_MEDIUM}=REDIS"]) +@Testcontainers +@ContextConfiguration(initializers = [RedisSentinelAuthenticatorTest.Initializer::class]) +@Import(*[LoggingConfiguration::class, RedisConfiguration::class, QueueConfiguration::class, MultiQueueTest.MultiQueueTestConfiguration::class]) +class RedisSentinelAuthenticatorTest: MultiQueueAuthenticatorTest() +{ + companion object + { + private const val REDIS_CONTAINER: String = "redis:7.2.3-alpine" + private const val REDIS_SENTINEL_CONTAINER: String = "s7anley/redis-sentinel-docker:3.2.12" + + lateinit var redis: GenericContainer<*> + lateinit var sentinel: GenericContainer<*> + + /** + * Stop the container at the end of all the tests. + */ + @AfterAll + @JvmStatic + fun afterClass() + { + sentinel.stop() + redis.stop() + } + } + + /** + * The test initialiser for [RedisSentinelAuthenticatorTest] to initialise the container and test properties. + * + * @author github.com/Kilemonn + */ + internal class Initializer : ApplicationContextInitializer + { + /** + * Force start the container, so we can place its host and dynamic ports into the system properties. + * + * Set the environment variables before any of the beans are initialised. + */ + override fun initialize(configurableApplicationContext: ConfigurableApplicationContext) + { + redis = GenericContainer(DockerImageName.parse(REDIS_CONTAINER)) + .withExposedPorts(RedisConfiguration.REDIS_DEFAULT_PORT.toInt()).withReuse(false) + redis.start() + + val envMap = HashMap() + // For the sentinel container to determine where the master node is accessible from + envMap["MASTER"] = redis.host + envMap["REDIS_PORT"] = redis.getMappedPort(RedisConfiguration.REDIS_DEFAULT_PORT.toInt()).toString() + envMap["MASTER_NAME"] = MessageQueueSettings.REDIS_MASTER_NAME_DEFAULT + sentinel = GenericContainer(DockerImageName.parse(REDIS_SENTINEL_CONTAINER)) + .withExposedPorts(RedisConfiguration.REDIS_SENTINEL_DEFAULT_PORT.toInt()).withReuse(false) + .withEnv(envMap) + sentinel.start() + + TestPropertyValues.of( + "${MessageQueueSettings.REDIS_ENDPOINT}=${sentinel.host}:${sentinel.getMappedPort(RedisConfiguration.REDIS_SENTINEL_DEFAULT_PORT.toInt())}", + "${MessageQueueSettings.REDIS_USE_SENTINELS}=true" + ).applyTo(configurableApplicationContext.environment) + } + } + + @BeforeEach + fun beforeEach() + { + Assertions.assertTrue(redis.isRunning) + Assertions.assertTrue(sentinel.isRunning) + multiQueueAuthenticator.clearRestrictedSubQueues() + } +} diff --git a/src/test/kotlin/au/kilemon/messagequeue/authentication/authenticator/cache/redis/RedisStandAloneAuthenticatorTest.kt b/src/test/kotlin/au/kilemon/messagequeue/authentication/authenticator/cache/redis/RedisStandAloneAuthenticatorTest.kt new file mode 100644 index 0000000..3446d16 --- /dev/null +++ b/src/test/kotlin/au/kilemon/messagequeue/authentication/authenticator/cache/redis/RedisStandAloneAuthenticatorTest.kt @@ -0,0 +1,84 @@ +package au.kilemon.messagequeue.authentication.authenticator.cache.redis + +import au.kilemon.messagequeue.authentication.authenticator.MultiQueueAuthenticatorTest +import au.kilemon.messagequeue.configuration.QueueConfiguration +import au.kilemon.messagequeue.configuration.cache.redis.RedisConfiguration +import au.kilemon.messagequeue.logging.LoggingConfiguration +import au.kilemon.messagequeue.queue.MultiQueueTest +import au.kilemon.messagequeue.settings.MessageQueueSettings +import org.junit.jupiter.api.AfterAll +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.extension.ExtendWith +import org.springframework.boot.test.util.TestPropertyValues +import org.springframework.context.ApplicationContextInitializer +import org.springframework.context.ConfigurableApplicationContext +import org.springframework.context.annotation.Import +import org.springframework.test.context.ContextConfiguration +import org.springframework.test.context.TestPropertySource +import org.springframework.test.context.junit.jupiter.SpringExtension +import org.testcontainers.containers.GenericContainer +import org.testcontainers.junit.jupiter.Testcontainers +import org.testcontainers.utility.DockerImageName + +/** + * A test class for [RedisAuthenticator] running in a standalone configuration. + * + * @author github.com/Kilemonn + */ +@ExtendWith(SpringExtension::class) +@TestPropertySource(properties = ["${MessageQueueSettings.STORAGE_MEDIUM}=REDIS", "${MessageQueueSettings.REDIS_PREFIX}=test"]) +@Testcontainers +@ContextConfiguration(initializers = [RedisStandAloneAuthenticatorTest.Initializer::class]) +@Import(*[QueueConfiguration::class, LoggingConfiguration::class, RedisConfiguration::class, MultiQueueTest.MultiQueueTestConfiguration::class]) +class RedisStandAloneAuthenticatorTest: MultiQueueAuthenticatorTest() +{ + companion object + { + private const val REDIS_PORT: Int = 6379 + private const val REDIS_CONTAINER: String = "redis:7.2.3-alpine" + + lateinit var redis: GenericContainer<*> + + /** + * Stop the container at the end of all the tests. + */ + @AfterAll + @JvmStatic + fun afterClass() + { + redis.stop() + } + } + + /** + * The test initialiser for [RedisStandAloneAuthenticatorTest] to initialise the container and test properties. + * + * @author github.com/Kilemonn + */ + internal class Initializer : ApplicationContextInitializer + { + /** + * Force start the container, so we can place its host and dynamic ports into the system properties. + * + * Set the environment variables before any of the beans are initialised. + */ + override fun initialize(configurableApplicationContext: ConfigurableApplicationContext) + { + redis = GenericContainer(DockerImageName.parse(REDIS_CONTAINER)) + .withExposedPorts(REDIS_PORT).withReuse(false) + redis.start() + + TestPropertyValues.of( + "${MessageQueueSettings.REDIS_ENDPOINT}=${redis.host}:${redis.getMappedPort(REDIS_PORT)}" + ).applyTo(configurableApplicationContext.environment) + } + } + + @BeforeEach + fun beforeEach() + { + Assertions.assertTrue(redis.isRunning) + multiQueueAuthenticator.clearRestrictedSubQueues() + } +} diff --git a/src/test/kotlin/au/kilemon/messagequeue/authentication/authenticator/inmemory/InMemoryAuthenticatorTest.kt b/src/test/kotlin/au/kilemon/messagequeue/authentication/authenticator/inmemory/InMemoryAuthenticatorTest.kt new file mode 100644 index 0000000..218534c --- /dev/null +++ b/src/test/kotlin/au/kilemon/messagequeue/authentication/authenticator/inmemory/InMemoryAuthenticatorTest.kt @@ -0,0 +1,88 @@ +package au.kilemon.messagequeue.authentication.authenticator.inmemory + +import au.kilemon.messagequeue.authentication.RestrictionMode +import au.kilemon.messagequeue.authentication.authenticator.MultiQueueAuthenticator +import au.kilemon.messagequeue.authentication.authenticator.MultiQueueAuthenticatorTest +import au.kilemon.messagequeue.configuration.QueueConfiguration +import au.kilemon.messagequeue.logging.LoggingConfiguration +import au.kilemon.messagequeue.queue.MultiQueueTest +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.mockito.Mockito +import org.springframework.context.annotation.Import +import org.springframework.test.context.junit.jupiter.SpringExtension + +/** + * A test class for the [InMemoryAuthenticator] class. + * + * This class also does mock testing for the different types of [RestrictionMode] since they don't + * rely on the backing mechanism. + * + * @author github.com/Kilemonn + */ +@ExtendWith(SpringExtension::class) +@Import( *[QueueConfiguration::class, LoggingConfiguration::class, MultiQueueTest.MultiQueueTestConfiguration::class] ) +class InMemoryAuthenticatorTest: MultiQueueAuthenticatorTest() +{ + @BeforeEach + fun setUp() + { + multiQueueAuthenticator.clearRestrictedSubQueues() + } + + /** + * Ensure that [MultiQueueAuthenticator.isInNoneMode] returns the correct value based on the stored + * [RestrictionMode]. + */ + @Test + fun testIsInNoneMode() + { + val authenticator = Mockito.spy(MultiQueueAuthenticator::class.java) + Mockito.doReturn(RestrictionMode.NONE).`when`(authenticator).getRestrictionMode() + Assertions.assertTrue(authenticator.isInNoneMode()) + + Mockito.doReturn(RestrictionMode.HYBRID).`when`(authenticator).getRestrictionMode() + Assertions.assertFalse(authenticator.isInNoneMode()) + + Mockito.doReturn(RestrictionMode.RESTRICTED).`when`(authenticator).getRestrictionMode() + Assertions.assertFalse(authenticator.isInNoneMode()) + } + + /** + * Ensure that [MultiQueueAuthenticator.isInHybridMode] returns the correct value based on the stored + * [RestrictionMode]. + */ + @Test + fun testIsInHybridMode() + { + val authenticator = Mockito.spy(MultiQueueAuthenticator::class.java) + Mockito.doReturn(RestrictionMode.HYBRID).`when`(authenticator).getRestrictionMode() + Assertions.assertTrue(authenticator.isInHybridMode()) + + Mockito.doReturn(RestrictionMode.NONE).`when`(authenticator).getRestrictionMode() + Assertions.assertFalse(authenticator.isInHybridMode()) + + Mockito.doReturn(RestrictionMode.RESTRICTED).`when`(authenticator).getRestrictionMode() + Assertions.assertFalse(authenticator.isInHybridMode()) + } + + /** + * Ensure that [MultiQueueAuthenticator.isInRestrictedMode] returns the correct value based on the stored + * [RestrictionMode]. + */ + @Test + fun testIsInRestrictedMode() + { + val authenticator = Mockito.spy(MultiQueueAuthenticator::class.java) + Mockito.doReturn(RestrictionMode.RESTRICTED).`when`(authenticator).getRestrictionMode() + Assertions.assertTrue(authenticator.isInRestrictedMode()) + + Mockito.doReturn(RestrictionMode.NONE).`when`(authenticator).getRestrictionMode() + Assertions.assertFalse(authenticator.isInRestrictedMode()) + + Mockito.doReturn(RestrictionMode.HYBRID).`when`(authenticator).getRestrictionMode() + Assertions.assertFalse(authenticator.isInRestrictedMode()) + } +} diff --git a/src/test/kotlin/au/kilemon/messagequeue/authentication/authenticator/nosql/mongo/MongoMultiAuthenticatorTest.kt b/src/test/kotlin/au/kilemon/messagequeue/authentication/authenticator/nosql/mongo/MongoMultiAuthenticatorTest.kt new file mode 100644 index 0000000..0373c50 --- /dev/null +++ b/src/test/kotlin/au/kilemon/messagequeue/authentication/authenticator/nosql/mongo/MongoMultiAuthenticatorTest.kt @@ -0,0 +1,104 @@ +package au.kilemon.messagequeue.authentication.authenticator.nosql.mongo + +import au.kilemon.messagequeue.authentication.authenticator.MultiQueueAuthenticatorTest +import au.kilemon.messagequeue.authentication.authenticator.nosql.mongo.MongoMultiAuthenticatorTest.Companion.MONGO_CONTAINER +import au.kilemon.messagequeue.configuration.QueueConfiguration +import au.kilemon.messagequeue.logging.LoggingConfiguration +import au.kilemon.messagequeue.queue.MultiQueueTest +import au.kilemon.messagequeue.settings.MessageQueueSettings +import org.junit.jupiter.api.AfterAll +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.extension.ExtendWith +import org.springframework.boot.test.autoconfigure.data.mongo.DataMongoTest +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase +import org.springframework.boot.test.util.TestPropertyValues +import org.springframework.context.ApplicationContextInitializer +import org.springframework.context.ConfigurableApplicationContext +import org.springframework.context.annotation.Import +import org.springframework.test.context.ContextConfiguration +import org.springframework.test.context.junit.jupiter.SpringExtension +import org.testcontainers.containers.GenericContainer +import org.testcontainers.junit.jupiter.Testcontainers +import org.testcontainers.utility.DockerImageName + +/** + * A test class for the [MONGO_CONTAINER] to ensure the [MongoAuthenticator] works as expected with this underlying data + * storage DB. + * + * @author github.com/Kilemonn + */ +@ExtendWith(SpringExtension::class) +@Testcontainers +@DataMongoTest(properties = ["${MessageQueueSettings.STORAGE_MEDIUM}=MONGO"]) +@ContextConfiguration(initializers = [MongoMultiAuthenticatorTest.Initializer::class]) +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) +@Import( *[QueueConfiguration::class, LoggingConfiguration::class, MultiQueueTest.MultiQueueTestConfiguration::class] ) +class MongoMultiAuthenticatorTest: MultiQueueAuthenticatorTest() +{ + companion object + { + lateinit var mongoDb: GenericContainer<*> + + private const val MONGO_CONTAINER = "mongo:7.0.0" + private const val MONGO_PORT = 27017 + + /** + * Stop the container at the end of all the tests. + */ + @AfterAll + @JvmStatic + fun afterClass() + { + mongoDb.stop() + } + } + + /** + * The test initialiser for [MongoMultiAuthenticatorTest] to initialise the container and test properties. + * + * @author github.com/Kilemonn + */ + internal class Initializer : ApplicationContextInitializer + { + /** + * Force start the container, so we can place its host and dynamic ports into the system properties. + * + * Set the environment variables before any of the beans are initialised. + * + * The following properties can also be used: + * - "spring.data.mongodb.host=${mongoDb.host}" + * - "spring.data.mongodb.database=$databaseName" + * - "spring.data.mongodb.username=$username" + * - "spring.data.mongodb.password=$password" + * - "spring.data.mongodb.port=${mongoDb.getMappedPort(MONGO_PORT)}" + */ + override fun initialize(configurableApplicationContext: ConfigurableApplicationContext) + { + val password = "password" + val username = "root" + val envMap = HashMap() + envMap["MONGO_INITDB_ROOT_PASSWORD"] = password + envMap["MONGO_INITDB_ROOT_USERNAME"] = username + + mongoDb = GenericContainer(DockerImageName.parse(MONGO_CONTAINER)) + .withExposedPorts(MONGO_PORT).withReuse(false).withEnv(envMap) + mongoDb.start() + + val databaseName = "MultiQueue" + // mongodb://:@:/ + val endpoint = "mongodb://$username:$password@${mongoDb.host}:${mongoDb.getMappedPort(MONGO_PORT)}/$databaseName?authSource=admin" + + TestPropertyValues.of( + "spring.data.mongodb.uri=$endpoint" + ).applyTo(configurableApplicationContext.environment) + } + } + + @BeforeEach + fun beforeEach() + { + Assertions.assertTrue(mongoDb.isRunning) + multiQueueAuthenticator.clearRestrictedSubQueues() + } +} diff --git a/src/test/kotlin/au/kilemon/messagequeue/authentication/authenticator/sql/MySqlAuthenticatorTest.kt b/src/test/kotlin/au/kilemon/messagequeue/authentication/authenticator/sql/MySqlAuthenticatorTest.kt new file mode 100644 index 0000000..f34e523 --- /dev/null +++ b/src/test/kotlin/au/kilemon/messagequeue/authentication/authenticator/sql/MySqlAuthenticatorTest.kt @@ -0,0 +1,94 @@ +package au.kilemon.messagequeue.authentication.authenticator.sql + +import au.kilemon.messagequeue.authentication.authenticator.MultiQueueAuthenticatorTest +import au.kilemon.messagequeue.configuration.QueueConfiguration +import au.kilemon.messagequeue.logging.LoggingConfiguration +import au.kilemon.messagequeue.queue.MultiQueueTest +import au.kilemon.messagequeue.settings.MessageQueueSettings +import org.junit.jupiter.api.AfterAll +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.extension.ExtendWith +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest +import org.springframework.boot.test.util.TestPropertyValues +import org.springframework.context.ApplicationContextInitializer +import org.springframework.context.ConfigurableApplicationContext +import org.springframework.context.annotation.Import +import org.springframework.test.context.ContextConfiguration +import org.springframework.test.context.junit.jupiter.SpringExtension +import org.testcontainers.containers.GenericContainer +import org.testcontainers.junit.jupiter.Testcontainers +import org.testcontainers.utility.DockerImageName + +/** + * A test class for [SqlAuthenticator] using MySQL as the backing database. + * + * @author github.com/Kilemonn + */ +@ExtendWith(SpringExtension::class) +@Testcontainers +@DataJpaTest(properties = ["${MessageQueueSettings.STORAGE_MEDIUM}=SQL", "spring.jpa.hibernate.ddl-auto=create", "spring.autoconfigure.exclude="]) +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) +@ContextConfiguration(initializers = [MySqlAuthenticatorTest.Initializer::class]) +@Import( *[QueueConfiguration::class, LoggingConfiguration::class, MultiQueueTest.MultiQueueTestConfiguration::class] ) +class MySqlAuthenticatorTest: MultiQueueAuthenticatorTest() +{ + companion object + { + lateinit var database: GenericContainer<*> + + private const val MYSQL_CONTAINER = "mysql:8.0.35" + private const val MYSQL_PORT = 3306 + + /** + * Stop the container at the end of all the tests. + */ + @AfterAll + @JvmStatic + fun afterClass() + { + database.stop() + } + } + + /** + * The test initialiser for [MySqlAuthenticatorTest] to initialise the container and test properties. + * + * @author github.com/Kilemonn + */ + internal class Initializer : ApplicationContextInitializer + { + /** + * Force start the container, so we can place its host and dynamic ports into the system properties. + * + * Set the environment variables before any of the beans are initialised. + */ + override fun initialize(configurableApplicationContext: ConfigurableApplicationContext) + { + val password = "password" + val envMap = HashMap() + envMap["MYSQL_ROOT_PASSWORD"] = password + + database = GenericContainer(DockerImageName.parse(MYSQL_CONTAINER)) + .withExposedPorts(MYSQL_PORT).withReuse(false).withEnv(envMap) + database.start() + + val endpoint = "jdbc:mysql://${database.host}:${database.getMappedPort(MYSQL_PORT)}/mysql" + val username = "root" + + TestPropertyValues.of( + "spring.datasource.url=$endpoint", + "spring.datasource.username=$username", + "spring.datasource.password=$password", + ).applyTo(configurableApplicationContext.environment) + } + } + + @BeforeEach + fun beforeEach() + { + Assertions.assertTrue(database.isRunning) + multiQueueAuthenticator.clearRestrictedSubQueues() + } +} diff --git a/src/test/kotlin/au/kilemon/messagequeue/authentication/authenticator/sql/PostgreSqlAuthenticatorTest.kt b/src/test/kotlin/au/kilemon/messagequeue/authentication/authenticator/sql/PostgreSqlAuthenticatorTest.kt new file mode 100644 index 0000000..a92fdc5 --- /dev/null +++ b/src/test/kotlin/au/kilemon/messagequeue/authentication/authenticator/sql/PostgreSqlAuthenticatorTest.kt @@ -0,0 +1,94 @@ +package au.kilemon.messagequeue.authentication.authenticator.sql + +import au.kilemon.messagequeue.authentication.authenticator.MultiQueueAuthenticatorTest +import au.kilemon.messagequeue.configuration.QueueConfiguration +import au.kilemon.messagequeue.logging.LoggingConfiguration +import au.kilemon.messagequeue.queue.MultiQueueTest +import au.kilemon.messagequeue.settings.MessageQueueSettings +import org.junit.jupiter.api.AfterAll +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.extension.ExtendWith +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest +import org.springframework.boot.test.util.TestPropertyValues +import org.springframework.context.ApplicationContextInitializer +import org.springframework.context.ConfigurableApplicationContext +import org.springframework.context.annotation.Import +import org.springframework.test.context.ContextConfiguration +import org.springframework.test.context.junit.jupiter.SpringExtension +import org.testcontainers.containers.GenericContainer +import org.testcontainers.junit.jupiter.Testcontainers +import org.testcontainers.utility.DockerImageName + +/** + * A test class for [SqlAuthenticator] using PostgreSQL as the backing database. + * + * @author github.com/Kilemonn + */ +@ExtendWith(SpringExtension::class) +@Testcontainers +@DataJpaTest(properties = ["${MessageQueueSettings.STORAGE_MEDIUM}=SQL", "spring.jpa.hibernate.ddl-auto=create", "spring.autoconfigure.exclude="]) +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) +@ContextConfiguration(initializers = [PostgreSqlAuthenticatorTest.Initializer::class]) +@Import( *[QueueConfiguration::class, LoggingConfiguration::class, MultiQueueTest.MultiQueueTestConfiguration::class] ) +class PostgreSqlAuthenticatorTest: MultiQueueAuthenticatorTest() +{ + companion object + { + lateinit var database: GenericContainer<*> + + private const val POSTGRES_CONTAINER = "postgres:14.9-alpine" + private const val POSTGRES_PORT = 5432 + + /** + * Stop the container at the end of all the tests. + */ + @AfterAll + @JvmStatic + fun afterClass() + { + database.stop() + } + } + + /** + * The test initialiser for [PostgreSqlAuthenticatorTest] to initialise the container and test properties. + * + * @author github.com/Kilemonn + */ + internal class Initializer : ApplicationContextInitializer + { + /** + * Force start the container, so we can place its host and dynamic ports into the system properties. + * + * Set the environment variables before any of the beans are initialised. + */ + override fun initialize(configurableApplicationContext: ConfigurableApplicationContext) + { + val password = "password" + val envMap = HashMap() + envMap["POSTGRES_PASSWORD"] = password + + database = GenericContainer(DockerImageName.parse(POSTGRES_CONTAINER)) + .withExposedPorts(POSTGRES_PORT).withReuse(false).withEnv(envMap) + database.start() + + val endpoint = "jdbc:postgresql://${database.host}:${database.getMappedPort(POSTGRES_PORT)}/postgres" + val username = "postgres" + + TestPropertyValues.of( + "spring.datasource.url=$endpoint", + "spring.datasource.username=$username", + "spring.datasource.password=$password", + ).applyTo(configurableApplicationContext.environment) + } + } + + @BeforeEach + fun beforeEach() + { + Assertions.assertTrue(database.isRunning) + multiQueueAuthenticator.clearRestrictedSubQueues() + } +} diff --git a/src/test/kotlin/au/kilemon/messagequeue/authentication/exception/MultiQueueAuthenticationExceptionTest.kt b/src/test/kotlin/au/kilemon/messagequeue/authentication/exception/MultiQueueAuthenticationExceptionTest.kt new file mode 100644 index 0000000..432fb26 --- /dev/null +++ b/src/test/kotlin/au/kilemon/messagequeue/authentication/exception/MultiQueueAuthenticationExceptionTest.kt @@ -0,0 +1,25 @@ +package au.kilemon.messagequeue.authentication.exception + +import au.kilemon.messagequeue.queue.exception.DuplicateMessageException +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test + +/** + * A unit test class for the [MultiQueueAuthenticationException] for any specific tests related to this exception. + * + * @author github.com/Kilemonn + */ +class MultiQueueAuthenticationExceptionTest +{ + /** + * Ensure that [MultiQueueAuthenticationException] is a type of [Exception] and not [RuntimeException]. + * Incase this is changed in future. + */ + @Test + fun testTypeOfException() + { + val e = MultiQueueAuthenticationException() + Assertions.assertTrue(Exception::class.isInstance(e)) + Assertions.assertFalse(RuntimeException::class.isInstance(e)) + } +} diff --git a/src/test/kotlin/au/kilemon/messagequeue/authentication/exception/MultiQueueAuthorisationExceptionTest.kt b/src/test/kotlin/au/kilemon/messagequeue/authentication/exception/MultiQueueAuthorisationExceptionTest.kt new file mode 100644 index 0000000..2fb693d --- /dev/null +++ b/src/test/kotlin/au/kilemon/messagequeue/authentication/exception/MultiQueueAuthorisationExceptionTest.kt @@ -0,0 +1,25 @@ +package au.kilemon.messagequeue.authentication.exception + +import au.kilemon.messagequeue.authentication.RestrictionMode +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test + +/** + * A unit test class for the [MultiQueueAuthorisationException] for any specific tests related to this exception. + * + * @author github.com/Kilemonn + */ +class MultiQueueAuthorisationExceptionTest +{ + /** + * Ensure that [MultiQueueAuthorisationException] is a type of [Exception] and not [RuntimeException]. + * Incase this is changed in future. + */ + @Test + fun testTypeOfException() + { + val e = MultiQueueAuthorisationException("sub-queue", RestrictionMode.NONE) + Assertions.assertTrue(Exception::class.isInstance(e)) + Assertions.assertFalse(RuntimeException::class.isInstance(e)) + } +} diff --git a/src/test/kotlin/au/kilemon/messagequeue/authentication/token/JwtTokenProviderTest.kt b/src/test/kotlin/au/kilemon/messagequeue/authentication/token/JwtTokenProviderTest.kt new file mode 100644 index 0000000..f19a058 --- /dev/null +++ b/src/test/kotlin/au/kilemon/messagequeue/authentication/token/JwtTokenProviderTest.kt @@ -0,0 +1,160 @@ +package au.kilemon.messagequeue.authentication.token + +import com.auth0.jwt.exceptions.JWTCreationException +import org.junit.Assert +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.mockito.Mockito +import org.springframework.test.context.junit.jupiter.SpringExtension +import java.util.* + +/** + * A test class for [JwtTokenProvider] and different token issuing and verification scenarios. + */ +@ExtendWith(SpringExtension::class) +class JwtTokenProviderTest +{ + private val jwtTokenProvider = JwtTokenProvider() + + /** + * Ensure [JwtTokenProvider.createTokenForSubQueue] can successfully provision a token for a sub-queue. + */ + @Test + fun testCreateTokenForSubQueue() + { + val subQueue = "testCreateTokenForSubQueue" + val token = jwtTokenProvider.createTokenForSubQueue(subQueue) + + Assertions.assertNotNull(token) + Assertions.assertTrue(token.isPresent) + } + + /** + * Ensure [JwtTokenProvider.createTokenForSubQueue] returns an [Optional.empty] when the underlying method + * fails to generate the token and throws a [JWTCreationException]. + */ + @Test + fun testCreateTokenForSubQueue_failsToCreateToken() + { + val mockJwtTokenProvider = Mockito.spy(JwtTokenProvider::class.java) + val subQueue = "testCreateTokenForSubQueue_failsToCreateToken" + + Mockito.doThrow(JWTCreationException("message", Exception())).`when`(mockJwtTokenProvider).createTokenInternal(subQueue) + val token = mockJwtTokenProvider.createTokenForSubQueue(subQueue) + Assertions.assertTrue(token.isEmpty) + } + + /** + * Ensure [JwtTokenProvider.verifyTokenForSubQueue] correctly parses out the token's properties + * and that the issuer and claim are set correctly. And expiry is not set. + */ + @Test + fun testVerifyTokenForSubQueue_withoutExpiry() + { + val subQueue = "testVerifyTokenForSubQueue" + val token = jwtTokenProvider.createTokenForSubQueue(subQueue) + Assertions.assertNotNull(token) + Assertions.assertTrue(token.isPresent) + + val decodedJwt = jwtTokenProvider.verifyTokenForSubQueue(token.get()) + Assertions.assertNotNull(decodedJwt) + Assertions.assertTrue(decodedJwt.isPresent) + + val jwt = decodedJwt.get() + Assertions.assertEquals(subQueue, jwt.getClaim(JwtTokenProvider.SUB_QUEUE_CLAIM).asString()) + Assertions.assertEquals(JwtTokenProvider.ISSUER, jwt.issuer) + Assertions.assertNull(jwt.expiresAt) + } + + /** + * Ensure [JwtTokenProvider.verifyTokenForSubQueue] correctly parses out the token's properties + * and that the issuer, claim and expiry are set correctly. + */ + @Test + fun testVerifyTokenForSubQueue_withExpiringToken() + { + val subQueue = "testVerifyTokenForSubQueue_withExpiringToken" + val expiryInMinutes = 60L + val date = Date() + Thread.sleep(1000) // Forcing a sleep here to make sure the token is generated AFTER the current time + val token = jwtTokenProvider.createTokenForSubQueue(subQueue, expiryInMinutes) + Assertions.assertNotNull(token) + Assertions.assertTrue(token.isPresent) + + val decodedJwt = jwtTokenProvider.verifyTokenForSubQueue(token.get()) + Assertions.assertNotNull(decodedJwt) + Assertions.assertTrue(decodedJwt.isPresent) + + val jwt = decodedJwt.get() + Assertions.assertEquals(subQueue, jwt.getClaim(JwtTokenProvider.SUB_QUEUE_CLAIM).asString()) + Assertions.assertEquals(JwtTokenProvider.ISSUER, jwt.issuer) + Assertions.assertNotNull(jwt.expiresAt) + Assertions.assertTrue(jwt.expiresAt >= Date(date.time + (expiryInMinutes * 60 * 1000))) + Assertions.assertTrue(jwt.expiresAt < Date(date.time + ((expiryInMinutes + 1) * 60 * 1000))) + } + + /** + * Ensure [JwtTokenProvider.verifyTokenForSubQueue] fails to decode a token that has an expiry date that is in the + * past (expired token). + */ + @Test + fun testVerifyTokenForSubQueue_withExpiredToken() + { + val subQueue = "testVerifyTokenForSubQueue_withExpiringToken" + val expiryInMinutes = -10L + val token = jwtTokenProvider.createTokenForSubQueue(subQueue, expiryInMinutes) + + val decodedJwt = jwtTokenProvider.verifyTokenForSubQueue(token.get()) + Assertions.assertNotNull(decodedJwt) + Assertions.assertTrue(decodedJwt.isEmpty) + } + + /** + * Ensure [JwtTokenProvider.verifyTokenForSubQueue] fails to parse the provided token if it's not a valid JWT token. + */ + @Test + fun testVerifyTokenForSubQueue_invalidToken() + { + val token = "testVerifyTokenForSubQueue_invalidToken" + val decodedJwt = jwtTokenProvider.verifyTokenForSubQueue(token) + Assertions.assertNotNull(decodedJwt) + Assertions.assertTrue(decodedJwt.isEmpty) + } + + /** + * Ensure the [JwtTokenProvider.tokenKey] is set to `""` by default. + */ + @Test + fun testCheckTokenDefaultValue() + { + Assertions.assertEquals("", jwtTokenProvider.tokenKey) + } + + /** + * Ensure when [JwtTokenProvider.getOrGenerateKey] is called with an empty or blank string that a randomly generated byte array is returned. + */ + @Test + fun testGetOrGenerateKey_emptyOrBlankKey() + { + val emptyKey = "" + val generatedEmpty = jwtTokenProvider.getOrGenerateKey(emptyKey) + Assertions.assertFalse(emptyKey.toByteArray().contentEquals(generatedEmpty)) + + val blankKey = " " + val generatedBlank = jwtTokenProvider.getOrGenerateKey(blankKey) + Assertions.assertFalse(blankKey.toByteArray().contentEquals(generatedBlank)) + + Assertions.assertFalse(generatedEmpty.contentEquals(generatedBlank)) + } + + /** + * Ensure when [JwtTokenProvider.getOrGenerateKey] with a non-blank argument that it will be returned as a [ByteArray]. + */ + @Test + fun testGetOrGenerateKey_withProvidedKey() + { + val newKey = "testGetOrGenerateKey_withProvidedKey" + Assertions.assertTrue(newKey.toByteArray().contentEquals(jwtTokenProvider.getOrGenerateKey(newKey))) + } +} diff --git a/src/test/kotlin/au/kilemon/messagequeue/configuration/cache/redis/RedisConfigurationTest.kt b/src/test/kotlin/au/kilemon/messagequeue/configuration/cache/redis/RedisConfigurationTest.kt index ce9e259..82f419e 100644 --- a/src/test/kotlin/au/kilemon/messagequeue/configuration/cache/redis/RedisConfigurationTest.kt +++ b/src/test/kotlin/au/kilemon/messagequeue/configuration/cache/redis/RedisConfigurationTest.kt @@ -8,7 +8,7 @@ import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest import org.springframework.boot.test.context.TestConfiguration import org.springframework.context.annotation.Bean -import org.springframework.context.annotation.Lazy +import org.springframework.context.annotation.Import import org.springframework.test.context.junit.jupiter.SpringExtension import java.util.stream.IntStream @@ -18,6 +18,7 @@ import java.util.stream.IntStream * @author github.com/Kilemonn */ @ExtendWith(SpringExtension::class) +@Import(*[RedisConfiguration::class]) class RedisConfigurationTest { /** @@ -40,7 +41,10 @@ class RedisConfigurationTest } @Autowired - lateinit var messageQueueSettings: MessageQueueSettings + private lateinit var messageQueueSettings: MessageQueueSettings + + @Autowired + private lateinit var redisConfiguration: RedisConfiguration /** * The default port to be used when no port is supplied with the hostname endpoint string. @@ -133,9 +137,7 @@ class RedisConfigurationTest Assertions.assertTrue(messageQueueSettings.redisUseSentinels.toBoolean()) Assertions.assertEquals("", messageQueueSettings.redisEndpoint) Assertions.assertThrows(RedisInitialisationException::class.java) { - val config = RedisConfiguration() - config.messageQueueSettings = messageQueueSettings - config.getSentinelConfiguration() + redisConfiguration.getSentinelConfiguration() } } @@ -150,9 +152,7 @@ class RedisConfigurationTest Assertions.assertFalse(messageQueueSettings.redisUseSentinels.toBoolean()) Assertions.assertEquals("", messageQueueSettings.redisEndpoint) Assertions.assertThrows(RedisInitialisationException::class.java) { - val config = RedisConfiguration() - config.messageQueueSettings = messageQueueSettings - config.getStandAloneConfiguration() + redisConfiguration.getStandAloneConfiguration() } } @@ -171,9 +171,7 @@ class RedisConfigurationTest messageQueueSettings.redisEndpoint = endpoints Assertions.assertFalse(messageQueueSettings.redisUseSentinels.toBoolean()) Assertions.assertEquals(endpoints, messageQueueSettings.redisEndpoint) - val config = RedisConfiguration() - config.messageQueueSettings = messageQueueSettings - val standAloneConfiguration = config.getStandAloneConfiguration() + val standAloneConfiguration = redisConfiguration.getStandAloneConfiguration() Assertions.assertEquals(endpoint1Host, standAloneConfiguration.hostName) Assertions.assertEquals(endpoint1Port.toInt(), standAloneConfiguration.port) } diff --git a/src/test/kotlin/au/kilemon/messagequeue/filter/JwtAuthenticationFilterTest.kt b/src/test/kotlin/au/kilemon/messagequeue/filter/JwtAuthenticationFilterTest.kt new file mode 100644 index 0000000..b9598b2 --- /dev/null +++ b/src/test/kotlin/au/kilemon/messagequeue/filter/JwtAuthenticationFilterTest.kt @@ -0,0 +1,285 @@ +package au.kilemon.messagequeue.filter + +import au.kilemon.messagequeue.authentication.authenticator.MultiQueueAuthenticator +import au.kilemon.messagequeue.authentication.exception.MultiQueueAuthenticationException +import au.kilemon.messagequeue.authentication.token.JwtTokenProvider +import au.kilemon.messagequeue.configuration.QueueConfiguration +import au.kilemon.messagequeue.logging.LoggingConfiguration +import au.kilemon.messagequeue.queue.MultiQueueTest +import au.kilemon.messagequeue.rest.controller.MessageQueueController +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.mockito.Mockito +import org.slf4j.MDC +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.context.TestConfiguration +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Import +import org.springframework.http.HttpMethod +import org.springframework.test.context.ContextConfiguration +import org.springframework.test.context.junit.jupiter.SpringExtension +import org.springframework.web.servlet.HandlerExceptionResolver +import org.springframework.web.servlet.handler.HandlerExceptionResolverComposite +import java.util.* +import javax.servlet.http.HttpServletRequest + +/** + * A test class for the [JwtAuthenticationFilter]. + * + * @author github.com/Kilemonn + */ +@ExtendWith(SpringExtension::class) +@ContextConfiguration(classes = [JwtAuthenticationFilter::class]) +@Import( *[QueueConfiguration::class, LoggingConfiguration::class, MultiQueueTest.MultiQueueTestConfiguration::class] ) +class JwtAuthenticationFilterTest +{ + /** + * A [TestConfiguration] for the outer class. + * + * @author github.com/Kilemonn + */ + @TestConfiguration + open class TestConfig + { + @Bean + open fun getHandlerExceptionResolver(): HandlerExceptionResolver + { + return HandlerExceptionResolverComposite() + } + } + + @Autowired + private lateinit var jwtAuthenticationFilter: JwtAuthenticationFilter + + @Autowired + private lateinit var jwtTokenProvider: JwtTokenProvider + + + + @BeforeEach + fun setUp() + { + MDC.clear() + } + + /** + * This should be afterclass but works better here. + */ + @AfterEach + fun tearDown() + { + MDC.clear() + } + + /** + * Ensure that [JwtAuthenticationFilter.setSubQueue] does set the [MDC] [JwtAuthenticationFilter.SUB_QUEUE] property + * if the provided [Optional] is [Optional.isPresent]. + */ + @Test + fun testSetSubQueue_valuePresent() + { + val subQueue = Optional.of("testSetSubQueue_valuePresent") + Assertions.assertTrue(subQueue.isPresent) + Assertions.assertNull(MDC.get(JwtAuthenticationFilter.SUB_QUEUE)) + + jwtAuthenticationFilter.setSubQueue(subQueue) + + Assertions.assertEquals(subQueue.get(), MDC.get(JwtAuthenticationFilter.SUB_QUEUE)) + } + + /** + * Ensure that [JwtAuthenticationFilter.setSubQueue] does not set the [MDC] [JwtAuthenticationFilter.SUB_QUEUE] + * property if the provided [Optional] is [Optional.isEmpty]. + */ + @Test + fun testSetSubQueue_valueEmpty() + { + val subQueue = Optional.empty() + Assertions.assertTrue(subQueue.isEmpty) + Assertions.assertNull(MDC.get(JwtAuthenticationFilter.SUB_QUEUE)) + + jwtAuthenticationFilter.setSubQueue(subQueue) + + Assertions.assertNull(MDC.get(JwtAuthenticationFilter.SUB_QUEUE)) + } + + /** + * Ensure that [JwtAuthenticationFilter.getSubQueueInTokenFromHeaders] will retrieve the value correctly from the + * [JwtAuthenticationFilter.AUTHORIZATION_HEADER] and return it. + */ + @Test + fun testGetSubQueueInTokenFromHeaders_headerExists() + { + val request = Mockito.mock(HttpServletRequest::class.java) + val initialSubQueue = "testGetSubQueueInTokenFromHeaders_headerExists" + val token = jwtTokenProvider.createTokenForSubQueue(initialSubQueue) + Assertions.assertTrue(token.isPresent) + val authHeaderValue = "${JwtAuthenticationFilter.BEARER_HEADER_VALUE}${token.get()}" + Mockito.`when`(request.getHeader(JwtAuthenticationFilter.AUTHORIZATION_HEADER)).thenReturn(authHeaderValue) + + val subQueue = jwtAuthenticationFilter.getSubQueueInTokenFromHeaders(request) + Assertions.assertTrue(subQueue.isPresent) + Assertions.assertEquals(initialSubQueue, subQueue.get()) + } + + /** + * Ensure that [JwtAuthenticationFilter.getSubQueueInTokenFromHeaders] will fail to retrieve and verify the token + * when it does not have the [JwtAuthenticationFilter.BEARER_HEADER_VALUE] prefix. + */ + @Test + fun testGetSubQueueInTokenFromHeaders_withoutBearerPrefix() + { + val request = Mockito.mock(HttpServletRequest::class.java) + val authHeaderValue = "testGetSubQueueInTokenFromHeaders_withoutBearerPrefix" + Mockito.`when`(request.getHeader(JwtAuthenticationFilter.AUTHORIZATION_HEADER)).thenReturn(authHeaderValue) + + val subQueue = jwtAuthenticationFilter.getSubQueueInTokenFromHeaders(request) + Assertions.assertTrue(subQueue.isEmpty) + } + + /** + * Ensure that [JwtAuthenticationFilter.getSubQueueInTokenFromHeaders] will return an [Optional.empty] when the + * [JwtAuthenticationFilter.AUTHORIZATION_HEADER] header value is `null`. + */ + @Test + fun testGetSubQueueInTokenFromHeaders_headerDoesNotExists() + { + val request = Mockito.mock(HttpServletRequest::class.java) + Mockito.`when`(request.getHeader(JwtAuthenticationFilter.AUTHORIZATION_HEADER)).thenReturn(null) + + val subQueue = jwtAuthenticationFilter.getSubQueueInTokenFromHeaders(request) + Assertions.assertTrue(subQueue.isEmpty) + } + + /** + * Ensure the [JwtAuthenticationFilter.getSubQueue] retrieves the stored [JwtAuthenticationFilter.SUB_QUEUE] + * property from the [MDC]. + */ + @Test + fun testGetSubQueue() + { + val subQueue = "testGetSubQueue" + Assertions.assertNull(JwtAuthenticationFilter.getSubQueue()) + + jwtAuthenticationFilter.setSubQueue(Optional.of(subQueue)) + Assertions.assertEquals(subQueue, JwtAuthenticationFilter.getSubQueue()) + } + + /** + * Ensure that [JwtAuthenticationFilter.tokenIsPresentAndQueueIsRestricted] returns `true` when the provided + * token is present and the queue is restricted. + */ + @Test + fun testTokenIsPresentAndQueueIsRestricted_TokenPresentAndQueueRestricted() + { + val subQueue = Optional.of("testTokenIsPresentAndQueueIsRestricted_TokenPresentAndQueueRestricted") + val authenticator = Mockito.mock(MultiQueueAuthenticator::class.java) + Mockito.`when`(authenticator.isRestricted(subQueue.get())).thenReturn(true) + + Assertions.assertTrue(jwtAuthenticationFilter.tokenIsPresentAndQueueIsRestricted(subQueue, authenticator)) + } + + /** + * Ensure that [JwtAuthenticationFilter.tokenIsPresentAndQueueIsRestricted] returns `false` when the provided + * token is present and the queue is NOT restricted. + */ + @Test + fun testTokenIsPresentAndQueueIsRestricted_TokenPresentAndQueueNotRestricted() + { + val subQueue = Optional.of("testTokenIsPresentAndQueueIsRestricted_TokenPresentAndQueueNotRestricted") + val authenticator = Mockito.mock(MultiQueueAuthenticator::class.java) + Mockito.`when`(authenticator.isRestricted(subQueue.get())).thenReturn(false) + + Assertions.assertFalse(jwtAuthenticationFilter.tokenIsPresentAndQueueIsRestricted(subQueue, authenticator)) + } + + /** + * Ensure that [JwtAuthenticationFilter.tokenIsPresentAndQueueIsRestricted] returns `false` when the provided + * token is empty. + */ + @Test + fun testTokenIsPresentAndQueueIsRestricted_TokenNotPresent() + { + val subQueue = Optional.empty() + val authenticator = Mockito.mock(MultiQueueAuthenticator::class.java) + + Assertions.assertFalse(jwtAuthenticationFilter.tokenIsPresentAndQueueIsRestricted(subQueue, authenticator)) + } + + /** + * Ensure [JwtAuthenticationFilter.canSkipTokenVerification] returns `true` when the provided URI does not + * match the "no auth required" list. + */ + @Test + fun testUrlRequiresAuthentication_notInWhitelist() + { + val request = Mockito.mock(HttpServletRequest::class.java) + val uriPath = "/another/test/endpoint${MessageQueueController.MESSAGE_QUEUE_BASE_PATH}" + Mockito.`when`(request.requestURI).thenReturn(uriPath) + Mockito.`when`(request.method).thenReturn(HttpMethod.POST.toString()) + + Assertions.assertFalse(jwtAuthenticationFilter.canSkipTokenVerification(request)) + } + + /** + * Ensure [JwtAuthenticationFilter.canSkipTokenVerification] returns `false` when the provided URI does + * start with an un-authorised path prefix. + */ + @Test + fun testUrlRequiresAuthentication_authRequiredURL() + { + val request = Mockito.mock(HttpServletRequest::class.java) + val uriPath = "${MessageQueueController.MESSAGE_QUEUE_BASE_PATH}${MessageQueueController.ENDPOINT_HEALTH_CHECK}" + Mockito.`when`(request.requestURI).thenReturn(uriPath) + Mockito.`when`(request.method).thenReturn(HttpMethod.GET.toString()) + + Assertions.assertTrue(jwtAuthenticationFilter.canSkipTokenVerification(request)) + } + + /** + * Ensure [JwtAuthenticationFilter.canSkipTokenVerification] returns `true` when the provided HTTP method is + * not in the whitelist. + */ + @Test + fun testUrlRequiresAuthentication_nonMatchingMethod() + { + val request = Mockito.mock(HttpServletRequest::class.java) + val uriPath = "/a/path" + Mockito.`when`(request.requestURI).thenReturn(uriPath) + Mockito.`when`(request.method).thenReturn(HttpMethod.OPTIONS.toString()) + + Assertions.assertFalse(jwtAuthenticationFilter.canSkipTokenVerification(request)) + } + + /** + * Ensure that [JwtAuthenticationFilter.isValidJwtToken] returns a valid [Optional] with the correct sub-queue + * value when a valid token is provided. + */ + @Test + fun testIsValidJwtToken_validToken() + { + val subQueue = "testIsValidJwtToken_validToken" + val token = jwtTokenProvider.createTokenForSubQueue(subQueue) + Assertions.assertTrue(token.isPresent) + + val embeddedSubQueue = jwtAuthenticationFilter.isValidJwtToken(token.get()) + Assertions.assertTrue(embeddedSubQueue.isPresent) + Assertions.assertEquals(subQueue, embeddedSubQueue.get()) + } + + /** + * Ensure that [JwtAuthenticationFilter.isValidJwtToken] throws a [MultiQueueAuthenticationException] when the + * provided token is invalid. + */ + @Test + fun testIsValidJwtToken_throws() + { + val token = "testIsValidJwtToken_throws" + Assertions.assertThrows(MultiQueueAuthenticationException::class.java) { + jwtAuthenticationFilter.isValidJwtToken(token) + } + } +} diff --git a/src/test/kotlin/au/kilemon/messagequeue/message/QueueMessageTest.kt b/src/test/kotlin/au/kilemon/messagequeue/message/QueueMessageTest.kt index 58396a9..1b41d37 100644 --- a/src/test/kotlin/au/kilemon/messagequeue/message/QueueMessageTest.kt +++ b/src/test/kotlin/au/kilemon/messagequeue/message/QueueMessageTest.kt @@ -33,18 +33,18 @@ class QueueMessageTest /** * Ensure that two [QueueMessage]s are not equal if one has `null` [QueueMessage.payload] and [QueueMessage.payloadBytes], but the same - * [QueueMessage.uuid] and [QueueMessage.type]. + * [QueueMessage.uuid] and [QueueMessage.subQueue]. */ @Test fun testEquals_withOneMessageHavingNullPayloadAndBytes() { val uuid = UUID.randomUUID().toString() - val type = "type" + val subQueue = "testEquals_withOneMessageHavingNullPayloadAndBytes" val message1 = QueueMessage() message1.payload = null message1.uuid = uuid - message1.type = type - val message2 = QueueMessage(payload = "stuff", type = type) + message1.subQueue = subQueue + val message2 = QueueMessage(payload = "stuff", subQueue = subQueue) message2.uuid = uuid Assertions.assertNull(message1.payload) @@ -56,17 +56,17 @@ class QueueMessageTest /** * Ensure that two [QueueMessage] are equal if they both have `null` [QueueMessage.payload] but `equal` [QueueMessage.payloadBytes], and the same - * [QueueMessage.uuid] and [QueueMessage.type]. + * [QueueMessage.uuid] and [QueueMessage.subQueue]. */ @Test fun testEquals_withEqualPayloadBytes() { val uuid = UUID.randomUUID().toString() - val type = "type" - val message1 = QueueMessage(payload = "stuff", type = type) + val subQueue = "testEquals_withEqualPayloadBytes" + val message1 = QueueMessage(payload = "stuff", subQueue = subQueue) message1.payload = null message1.uuid = uuid - val message2 = QueueMessage(payload = "stuff", type = type) + val message2 = QueueMessage(payload = "stuff", subQueue = subQueue) message2.payload = null message2.uuid = uuid @@ -76,19 +76,19 @@ class QueueMessageTest /** * Ensure that two [QueueMessage] are equal if they both have `null` [QueueMessage.payloadBytes] but `equal` [QueueMessage.payload], and the same - * [QueueMessage.uuid] and [QueueMessage.type]. + * [QueueMessage.uuid] and [QueueMessage.subQueue]. */ @Test fun testEquals_withEqualPayloads() { val uuid = UUID.randomUUID().toString() - val type = "type" + val subQueue = "testEquals_withEqualPayloads" val message1 = QueueMessage() message1.uuid = uuid - message1.type = type + message1.subQueue = subQueue val message2 = QueueMessage() message2.uuid = uuid - message2.type = type + message2.subQueue = subQueue // Set the payload into both objects to ensure this is considered in the equals check too val obj = 1287354 @@ -103,16 +103,16 @@ class QueueMessageTest /** * Ensure that two [QueueMessage] are equal if they both have `equal` [QueueMessage.payloadBytes] and [QueueMessage.payload], and the same - * [QueueMessage.uuid] and [QueueMessage.type]. + * [QueueMessage.uuid] and [QueueMessage.subQueue]. */ @Test fun testEquals_withEqualPayloadsAndBytes() { val uuid = UUID.randomUUID().toString() - val type = "type" - val message1 = QueueMessage(payload = "stuff", type = type) + val subQueue = "testEquals_withEqualPayloadsAndBytes" + val message1 = QueueMessage(payload = "stuff", subQueue = subQueue) message1.uuid = uuid - val message2 = QueueMessage(payload = "stuff", type = type) + val message2 = QueueMessage(payload = "stuff", subQueue = subQueue) message2.uuid = uuid Assertions.assertEquals(message1.payload, message2.payload) @@ -121,15 +121,15 @@ class QueueMessageTest } /** - * Ensure that [QueueMessage.equals] returns `false` when all properties are equal except [QueueMessage.type]. + * Ensure that [QueueMessage.equals] returns `false` when all properties are equal except [QueueMessage.subQueue]. */ @Test - fun testEquals_nonEqualType() + fun testEquals_nonEqualSubQueue() { val uuid = UUID.randomUUID().toString() - val message1 = QueueMessage(payload = "stuff", type = "type1") + val message1 = QueueMessage(payload = "stuff", subQueue = "type1") message1.uuid = uuid - val message2 = QueueMessage(payload = "stuff", type = "type2") + val message2 = QueueMessage(payload = "stuff", subQueue = "type2") message2.uuid = uuid Assertions.assertEquals(message1.payload, message2.payload) @@ -143,7 +143,7 @@ class QueueMessageTest @Test fun testEquals_withNull() { - val message = QueueMessage(payload = "data", type = "testEquals_withNull") + val message = QueueMessage(payload = "data", subQueue = "testEquals_withNull") Assertions.assertNotEquals(message, null) } @@ -153,7 +153,7 @@ class QueueMessageTest @Test fun testEquals_withNonQueueMessageObject() { - val message = QueueMessage(payload = "data", type = "testEquals_withNonQueueMessageObject") + val message = QueueMessage(payload = "data", subQueue = "testEquals_withNonQueueMessageObject") val obj = Any() Assertions.assertTrue(obj !is QueueMessage) Assertions.assertNotEquals(message, obj) @@ -167,7 +167,7 @@ class QueueMessageTest fun testResolvePayload_payloadNotNullBytesNull() { val payload = "testResolvePayload_payloadNotNullBytesNull" - val message = QueueMessage(null, type = "test") + val message = QueueMessage(null, subQueue = "test") Assertions.assertNull(message.payloadBytes) message.payload = payload message.resolvePayloadObject() @@ -183,7 +183,7 @@ class QueueMessageTest { val payload1 = "testResolvePayload_payloadNotNullBytesNotNull" val payload2 = "payload-bytes" - val message = QueueMessage(payload1, type = "test") + val message = QueueMessage(payload1, subQueue = "test") message.payloadBytes = SerializationUtils.serialize(payload2) message.resolvePayloadObject() Assertions.assertEquals(payload1, message.payload) @@ -197,7 +197,7 @@ class QueueMessageTest @Test fun testResolvePayload_payloadNullBytesNull() { - val message = QueueMessage(null, type = "test") + val message = QueueMessage(null, subQueue = "test") Assertions.assertNull(message.payload) Assertions.assertNull(message.payloadBytes) @@ -215,7 +215,7 @@ class QueueMessageTest fun testResolvePayload_payloadNullBytesNotNull() { val payload = "testResolvePayload_payloadNullBytesNotNull" - val message = QueueMessage(null, type = "test") + val message = QueueMessage(null, subQueue = "test") message.payloadBytes = SerializationUtils.serialize(payload) // At this point the payload property will quest payloadBytes, we need to overwrite diff --git a/src/test/kotlin/au/kilemon/messagequeue/queue/AbstractMultiQueueTest.kt b/src/test/kotlin/au/kilemon/messagequeue/queue/MultiQueueTest.kt similarity index 62% rename from src/test/kotlin/au/kilemon/messagequeue/queue/AbstractMultiQueueTest.kt rename to src/test/kotlin/au/kilemon/messagequeue/queue/MultiQueueTest.kt index c536f12..9812ba0 100644 --- a/src/test/kotlin/au/kilemon/messagequeue/queue/AbstractMultiQueueTest.kt +++ b/src/test/kotlin/au/kilemon/messagequeue/queue/MultiQueueTest.kt @@ -1,7 +1,10 @@ package au.kilemon.messagequeue.queue +import au.kilemon.messagequeue.authentication.RestrictionMode +import au.kilemon.messagequeue.authentication.authenticator.MultiQueueAuthenticator import au.kilemon.messagequeue.message.QueueMessage import au.kilemon.messagequeue.queue.exception.DuplicateMessageException +import au.kilemon.messagequeue.queue.exception.IllegalSubQueueIdentifierException import au.kilemon.messagequeue.queue.exception.MessageUpdateException import au.kilemon.messagequeue.queue.inmemory.InMemoryMultiQueue import au.kilemon.messagequeue.queue.nosql.mongo.MongoMultiQueue @@ -9,7 +12,10 @@ import au.kilemon.messagequeue.queue.sql.SqlMultiQueue import au.kilemon.messagequeue.rest.model.Payload import au.kilemon.messagequeue.rest.model.PayloadEnum import au.kilemon.messagequeue.settings.MessageQueueSettings -import org.junit.jupiter.api.* +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.api.assertAll import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.Arguments import org.junit.jupiter.params.provider.MethodSource @@ -20,6 +26,7 @@ import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Lazy import java.io.Serializable import java.util.* +import java.util.function.Supplier import java.util.stream.Stream /** @@ -30,7 +37,7 @@ import java.util.stream.Stream * @author github.com/Kilemonn */ @TestInstance(TestInstance.Lifecycle.PER_CLASS) -abstract class AbstractMultiQueueTest +abstract class MultiQueueTest { /** * A Spring configuration that is used for this test class. @@ -38,7 +45,7 @@ abstract class AbstractMultiQueueTest * @author github.com/Kilemonn */ @TestConfiguration - class AbstractMultiQueueTestConfiguration + class MultiQueueTestConfiguration { /** * The bean initialise here will have all its properties overridden by environment variables. @@ -55,27 +62,30 @@ abstract class AbstractMultiQueueTest @Autowired protected lateinit var multiQueue: MultiQueue + @Autowired + private lateinit var authenticator: MultiQueueAuthenticator + /** * Ensure that when a new entry is added, that the [MultiQueue] is no longer empty and reports the correct size. * - * @param data the incoming [Serializable] data to store in the [MultiQueue] to test that we can cater for multiple types + * @param data the incoming [Serializable] data to store in the [MultiQueue] to test that we can cater for multiple [QueueMessage.subQueue] */ @ParameterizedTest @MethodSource("parameters_testAdd") fun testAdd(data: Serializable) { Assertions.assertTrue(multiQueue.isEmpty()) - val message = QueueMessage(data, "type") + val message = QueueMessage(data, "testAdd") Assertions.assertTrue(multiQueue.add(message)) Assertions.assertFalse(multiQueue.isEmpty()) Assertions.assertEquals(1, multiQueue.size) // Getting the element two ways, via the queue for type and via poll to ensure both ways resolve the object payload properly - val queue = multiQueue.getQueueForType(message.type) + val queue = multiQueue.getSubQueue(message.subQueue) Assertions.assertEquals(1, queue.size) val storedElement = queue.elementAt(0) - val retrievedMessage = multiQueue.pollForType(message.type) + val retrievedMessage = multiQueue.pollSubQueue(message.subQueue) Assertions.assertTrue(multiQueue.isEmpty()) Assertions.assertEquals(0, multiQueue.size) @@ -89,7 +99,7 @@ abstract class AbstractMultiQueueTest } /** - * An argument provider for the [AbstractMultiQueueTest.testAdd] method. + * An argument provider for the [MultiQueueTest.testAdd] method. */ private fun parameters_testAdd(): Stream { @@ -102,37 +112,37 @@ abstract class AbstractMultiQueueTest } /*** - * Test [MultiQueue.add] to ensure that [DuplicateMessageException] is thrown if a [QueueMessage] already exists with the same `UUID` even if it is assigned to a different `queue type`. + * Test [MultiQueue.add] to ensure that [DuplicateMessageException] is thrown if a [QueueMessage] already exists with the same `UUID` even if it is assigned to a different `sub-queue`. */ @Test - fun testAdd_entryAlreadyExistsInDifferentQueueType() + fun testAdd_entryAlreadyExistsInDifferentSubQueue() { Assertions.assertTrue(multiQueue.isEmpty()) - val message = QueueMessage("test", "type") + val message = QueueMessage("test", "testAdd_entryAlreadyExistsInDifferentSubQueue") Assertions.assertTrue(multiQueue.add(message)) - val differentType = "different-type" - val differentTypeMessage = QueueMessage(message.payload, differentType) - differentTypeMessage.uuid = message.uuid + val differentSubQueue = "testAdd_entryAlreadyExistsInDifferentSubQueue2" + val differentMessage = QueueMessage(message.payload, differentSubQueue) + differentMessage.uuid = message.uuid - Assertions.assertEquals(message.payload, differentTypeMessage.payload) - Assertions.assertEquals(message.uuid, differentTypeMessage.uuid) - Assertions.assertNotEquals(message.type, differentTypeMessage.type) + Assertions.assertEquals(message.payload, differentMessage.payload) + Assertions.assertEquals(message.uuid, differentMessage.uuid) + Assertions.assertNotEquals(message.subQueue, differentMessage.subQueue) Assertions.assertThrows(DuplicateMessageException::class.java) { - multiQueue.add(differentTypeMessage) + multiQueue.add(differentMessage) } } /*** - * Test [MultiQueue.add] to ensure that [DuplicateMessageException] is thrown if a [QueueMessage] already exists with the same `UUID` even if it is assigned to the same `queue type`. + * Test [MultiQueue.add] to ensure that [DuplicateMessageException] is thrown if a [QueueMessage] already exists with the same `UUID` even if it is assigned to the same `sub-queue`. */ @Test fun testAdd_sameEntryAlreadyExists() { Assertions.assertTrue(multiQueue.isEmpty()) - val message = QueueMessage("test", "type") + val message = QueueMessage("test", "testAdd_sameEntryAlreadyExists") Assertions.assertTrue(multiQueue.add(message)) Assertions.assertThrows(DuplicateMessageException::class.java) @@ -149,7 +159,7 @@ abstract class AbstractMultiQueueTest { Assertions.assertTrue(multiQueue.isEmpty()) - val message = QueueMessage("A test value", "type") + val message = QueueMessage("A test value", "testRemove") Assertions.assertTrue(multiQueue.add(message)) Assertions.assertFalse(multiQueue.isEmpty()) @@ -167,7 +177,7 @@ abstract class AbstractMultiQueueTest fun testRemove_whenEntryDoesntExist() { Assertions.assertTrue(multiQueue.isEmpty()) - val messageThatDoesntExist = QueueMessage(Payload("some Other data", 23, false, PayloadEnum.A), "type") + val messageThatDoesntExist = QueueMessage(Payload("some Other data", 23, false, PayloadEnum.A), "testRemove_whenEntryDoesntExist") Assertions.assertFalse(multiQueue.remove(messageThatDoesntExist)) Assertions.assertTrue(multiQueue.isEmpty()) @@ -180,9 +190,9 @@ abstract class AbstractMultiQueueTest fun testContains_whenEntryDoesntExist() { Assertions.assertTrue(multiQueue.isEmpty()) - val type = "type" + val subQueue = "testContains_whenEntryDoesntExist" val otherData = Payload("some Other data", 65, true, PayloadEnum.B) - val messageThatDoesntExist = QueueMessage(otherData, type) + val messageThatDoesntExist = QueueMessage(otherData, subQueue) Assertions.assertFalse(multiQueue.contains(messageThatDoesntExist)) } @@ -193,7 +203,7 @@ abstract class AbstractMultiQueueTest fun testContains_whenEntryExists() { Assertions.assertTrue(multiQueue.isEmpty()) - val message = QueueMessage(0x52347, "type") + val message = QueueMessage(0x52347, "testContains_whenEntryExists") Assertions.assertTrue(multiQueue.add(message)) Assertions.assertFalse(multiQueue.isEmpty()) @@ -211,7 +221,7 @@ abstract class AbstractMultiQueueTest fun testContains_whenMetadataPropertiesAreSet() { Assertions.assertTrue(multiQueue.isEmpty()) - val message = QueueMessage(0x5234, "type") + val message = QueueMessage(0x5234, "testContains_whenMetadataPropertiesAreSet") Assertions.assertTrue(multiQueue.add(message)) Assertions.assertFalse(multiQueue.isEmpty()) @@ -238,94 +248,94 @@ abstract class AbstractMultiQueueTest @Test fun testGetNextQueueIndex_doesNotIncrement() { - val queueType = "testGetNextQueueIndex_doesNotIncrement" + val subQueue = "testGetNextQueueIndex_doesNotIncrement" if (multiQueue is SqlMultiQueue) { - Assertions.assertTrue(multiQueue.getNextQueueIndex(queueType).isEmpty) + Assertions.assertTrue(multiQueue.getNextSubQueueIndex(subQueue).isEmpty) } else if (multiQueue is InMemoryMultiQueue) { - Assertions.assertEquals(1, multiQueue.getNextQueueIndex(queueType).get()) - Assertions.assertEquals(2, multiQueue.getNextQueueIndex(queueType).get()) - Assertions.assertEquals(3, multiQueue.getNextQueueIndex(queueType).get()) - Assertions.assertEquals(4, multiQueue.getNextQueueIndex(queueType).get()) - Assertions.assertEquals(5, multiQueue.getNextQueueIndex(queueType).get()) - - multiQueue.clearForType(queueType) - Assertions.assertEquals(1, multiQueue.getNextQueueIndex(queueType).get()) - Assertions.assertEquals(2, multiQueue.getNextQueueIndex(queueType).get()) - Assertions.assertEquals(3, multiQueue.getNextQueueIndex(queueType).get()) - Assertions.assertEquals(4, multiQueue.getNextQueueIndex(queueType).get()) - Assertions.assertEquals(5, multiQueue.getNextQueueIndex(queueType).get()) + Assertions.assertEquals(1, multiQueue.getNextSubQueueIndex(subQueue).get()) + Assertions.assertEquals(2, multiQueue.getNextSubQueueIndex(subQueue).get()) + Assertions.assertEquals(3, multiQueue.getNextSubQueueIndex(subQueue).get()) + Assertions.assertEquals(4, multiQueue.getNextSubQueueIndex(subQueue).get()) + Assertions.assertEquals(5, multiQueue.getNextSubQueueIndex(subQueue).get()) + + multiQueue.clearSubQueue(subQueue) + Assertions.assertEquals(1, multiQueue.getNextSubQueueIndex(subQueue).get()) + Assertions.assertEquals(2, multiQueue.getNextSubQueueIndex(subQueue).get()) + Assertions.assertEquals(3, multiQueue.getNextSubQueueIndex(subQueue).get()) + Assertions.assertEquals(4, multiQueue.getNextSubQueueIndex(subQueue).get()) + Assertions.assertEquals(5, multiQueue.getNextSubQueueIndex(subQueue).get()) multiQueue.clear() - Assertions.assertEquals(1, multiQueue.getNextQueueIndex(queueType).get()) - Assertions.assertEquals(2, multiQueue.getNextQueueIndex(queueType).get()) - Assertions.assertEquals(3, multiQueue.getNextQueueIndex(queueType).get()) - Assertions.assertEquals(4, multiQueue.getNextQueueIndex(queueType).get()) - Assertions.assertEquals(5, multiQueue.getNextQueueIndex(queueType).get()) + Assertions.assertEquals(1, multiQueue.getNextSubQueueIndex(subQueue).get()) + Assertions.assertEquals(2, multiQueue.getNextSubQueueIndex(subQueue).get()) + Assertions.assertEquals(3, multiQueue.getNextSubQueueIndex(subQueue).get()) + Assertions.assertEquals(4, multiQueue.getNextSubQueueIndex(subQueue).get()) + Assertions.assertEquals(5, multiQueue.getNextSubQueueIndex(subQueue).get()) } else { - Assertions.assertTrue(multiQueue.getNextQueueIndex(queueType).isPresent) - Assertions.assertEquals(1, multiQueue.getNextQueueIndex(queueType).get()) - Assertions.assertEquals(1, multiQueue.getNextQueueIndex(queueType).get()) - Assertions.assertEquals(1, multiQueue.getNextQueueIndex(queueType).get()) + Assertions.assertTrue(multiQueue.getNextSubQueueIndex(subQueue).isPresent) + Assertions.assertEquals(1, multiQueue.getNextSubQueueIndex(subQueue).get()) + Assertions.assertEquals(1, multiQueue.getNextSubQueueIndex(subQueue).get()) + Assertions.assertEquals(1, multiQueue.getNextSubQueueIndex(subQueue).get()) } } /** - * Ensure that [MultiQueue.getNextQueueIndex] starts at `1` and increments properly as called once entries are added. + * Ensure that [MultiQueue.getNextSubQueueIndex] starts at `1` and increments properly as called once entries are added. */ @Test fun testGetNextQueueIndex_withMessages() { Assertions.assertTrue(multiQueue.isEmpty()) - val queueType1 = "testGetNextQueueIndex_reInitialise1" - val queueType2 = "testGetNextQueueIndex_reInitialise2" + val subQueue1 = "testGetNextQueueIndex_reInitialise1" + val subQueue2 = "testGetNextQueueIndex_reInitialise2" - val list1 = listOf(QueueMessage(81273648, queueType1), QueueMessage("test test test", queueType1), QueueMessage(false, queueType1)) - val list2 = listOf(QueueMessage("test", queueType2), QueueMessage(123, queueType2)) + val list1 = listOf(QueueMessage(81273648, subQueue1), QueueMessage("test test test", subQueue1), QueueMessage(false, subQueue1)) + val list2 = listOf(QueueMessage("test", subQueue2), QueueMessage(123, subQueue2)) Assertions.assertTrue(multiQueue.addAll(list1)) Assertions.assertTrue(multiQueue.addAll(list2)) if (multiQueue is SqlMultiQueue) { - Assertions.assertTrue(multiQueue.getNextQueueIndex(queueType1).isEmpty) - Assertions.assertTrue(multiQueue.getNextQueueIndex(queueType2).isEmpty) + Assertions.assertTrue(multiQueue.getNextSubQueueIndex(subQueue1).isEmpty) + Assertions.assertTrue(multiQueue.getNextSubQueueIndex(subQueue2).isEmpty) } else if (multiQueue is InMemoryMultiQueue) { - Assertions.assertEquals((list1.size + 1).toLong(), multiQueue.getNextQueueIndex(queueType1).get()) - Assertions.assertEquals((list2.size + 1).toLong(), multiQueue.getNextQueueIndex(queueType2).get()) + Assertions.assertEquals((list1.size + 1).toLong(), multiQueue.getNextSubQueueIndex(subQueue1).get()) + Assertions.assertEquals((list2.size + 1).toLong(), multiQueue.getNextSubQueueIndex(subQueue2).get()) } else if (multiQueue is MongoMultiQueue) { - Assertions.assertEquals((list1.size + list2.size + 1).toLong(), multiQueue.getNextQueueIndex(queueType1).get()) - Assertions.assertEquals((list1.size + list2.size + 1).toLong(), multiQueue.getNextQueueIndex(queueType2).get()) + Assertions.assertEquals((list1.size + list2.size + 1).toLong(), multiQueue.getNextSubQueueIndex(subQueue1).get()) + Assertions.assertEquals((list1.size + list2.size + 1).toLong(), multiQueue.getNextSubQueueIndex(subQueue2).get()) } else { - Assertions.assertEquals((list1.size + 1).toLong(), multiQueue.getNextQueueIndex(queueType1).get()) - Assertions.assertEquals((list2.size + 1).toLong(), multiQueue.getNextQueueIndex(queueType2).get()) + Assertions.assertEquals((list1.size + 1).toLong(), multiQueue.getNextSubQueueIndex(subQueue1).get()) + Assertions.assertEquals((list2.size + 1).toLong(), multiQueue.getNextSubQueueIndex(subQueue2).get()) } } /** - * Ensure [MultiQueue.getQueueForType] returns the list of [QueueMessage]s always ordered by their [QueueMessage.id]. + * Ensure [MultiQueue.getSubQueue] returns the list of [QueueMessage]s always ordered by their [QueueMessage.id]. * * This also ensures they are assigned the `id` in the order they are enqueued. */ @Test - fun testGetQueueForType_ordered() + fun testGetqueue_ordered() { Assertions.assertTrue(multiQueue.isEmpty()) - val queueType = "testGetQueueForType_ordered" + val subQueue = "testGetqueue_ordered" - val list = listOf(QueueMessage(81248, queueType), QueueMessage("test data", queueType), QueueMessage(false, queueType)) + val list = listOf(QueueMessage(81248, subQueue), QueueMessage("test data", subQueue), QueueMessage(false, subQueue)) Assertions.assertTrue(multiQueue.addAll(list)) - val queue = multiQueue.getQueueForType(queueType) + val queue = multiQueue.getSubQueue(subQueue) Assertions.assertEquals(list.size, queue.size) var previousIndex: Long? = null list.zip(queue).forEach { pair -> @@ -343,22 +353,22 @@ abstract class AbstractMultiQueueTest } /** - * Ensure [MultiQueue.getQueueForType] returns the list of [QueueMessage]s always ordered by their [QueueMessage.id]. + * Ensure [MultiQueue.getSubQueue] returns the list of [QueueMessage]s always ordered by their [QueueMessage.id]. * Even when messages are changed and re-enqueued we need to make sure the returned message order is retained. */ @Test - fun testGetQueueForType_reordered() + fun testGetqueue_reordered() { Assertions.assertTrue(multiQueue.isEmpty()) - val queueType = "testGetQueueForType_reordered" + val subQueue = "testGetqueue_reordered" - val list = listOf(QueueMessage(81248, queueType), QueueMessage("test data", queueType), QueueMessage(false, queueType)) + val list = listOf(QueueMessage(81248, subQueue), QueueMessage("test data", subQueue), QueueMessage(false, subQueue)) Assertions.assertTrue(multiQueue.addAll(list)) // Force an object change, which for some mechanisms would re-enqueue it at the end // We will re-retrieve the queue and ensure they are in order to test that the ordering is correct // even after the object is changed - var queue = multiQueue.getQueueForType(queueType) + var queue = multiQueue.getSubQueue(subQueue) Assertions.assertEquals(list.size, queue.size) val firstMessage = queue.first() Assertions.assertEquals(list[0].uuid, firstMessage.uuid) @@ -366,7 +376,7 @@ abstract class AbstractMultiQueueTest firstMessage.payload = newData multiQueue.persistMessage(firstMessage) - queue = multiQueue.getQueueForType(queueType) + queue = multiQueue.getSubQueue(subQueue) var previousIndex: Long? = null list.zip(queue).forEach { pair -> Assertions.assertEquals(pair.first.uuid, pair.second.uuid) @@ -383,6 +393,22 @@ abstract class AbstractMultiQueueTest } } + /** + * Ensure that calls to [MultiQueue.getSubQueue] with a [MultiQueueAuthenticator.getReservedSubQueues] as an + * argument will throw [IllegalSubQueueIdentifierException]. + */ + @Test + fun testGetqueue_reservedSubQueue() + { + doWithRestrictedMode(RestrictionMode.HYBRID) { + authenticator.getReservedSubQueues().forEach { reservedSubQueueIdentifier -> + Assertions.assertThrows(IllegalSubQueueIdentifierException::class.java) { + multiQueue.getSubQueue(reservedSubQueueIdentifier) + } + } + } + } + /** * Ensure that all elements are added, and contained and removed via the provided [Collection]. */ @@ -390,7 +416,7 @@ abstract class AbstractMultiQueueTest fun testAddAll_containsAll_removeAll() { Assertions.assertTrue(multiQueue.isEmpty()) - val list = listOf(QueueMessage(81273648, "type"), QueueMessage("test test test", "type")) + val list = listOf(QueueMessage(81273648, "testAddAll_containsAll_removeAll"), QueueMessage("test test test", "testAddAll_containsAll_removeAll")) Assertions.assertTrue(multiQueue.addAll(list)) Assertions.assertFalse(multiQueue.isEmpty()) Assertions.assertEquals(2, multiQueue.size) @@ -410,7 +436,7 @@ abstract class AbstractMultiQueueTest @Test fun testAddAll_throwsDuplicateException() { - val list = listOf(QueueMessage(81273648, "type"), QueueMessage("test test test", "type")) + val list = listOf(QueueMessage(81273648, "testAddAll_throwsDuplicateException"), QueueMessage("test test test", "testAddAll_throwsDuplicateException")) Assertions.assertTrue(multiQueue.add(list[1])) Assertions.assertFalse(multiQueue.addAll(list)) Assertions.assertEquals(list.size, multiQueue.size) @@ -421,15 +447,15 @@ abstract class AbstractMultiQueueTest * Otherwise, if it does exist make sure that the correct entry is returned and that it is removed. */ @Test - fun testPollForType() + fun testPollForSubQueue() { Assertions.assertTrue(multiQueue.isEmpty()) - val message = QueueMessage(Payload("poll for type", 89, true, PayloadEnum.B), "poll-type") + val message = QueueMessage(Payload("poll for type", 89, true, PayloadEnum.B), "testPollForSubQueue") - Assertions.assertFalse(multiQueue.pollForType(message.type).isPresent) + Assertions.assertFalse(multiQueue.pollSubQueue(message.subQueue).isPresent) Assertions.assertTrue(multiQueue.add(message)) Assertions.assertFalse(multiQueue.isEmpty()) - val polledMessage = multiQueue.pollForType(message.type).get() + val polledMessage = multiQueue.pollSubQueue(message.subQueue).get() Assertions.assertEquals(message, polledMessage) Assertions.assertTrue(multiQueue.isEmpty()) } @@ -439,68 +465,68 @@ abstract class AbstractMultiQueueTest * Otherwise, if it does exist make sure that the correct entry is returned. */ @Test - fun testPeekForType() + fun testPeekForSubQueue() { Assertions.assertTrue(multiQueue.isEmpty()) - val message = QueueMessage(Payload("peek for type", 1121, false, PayloadEnum.C), "peek-type") + val message = QueueMessage(Payload("peek for type", 1121, false, PayloadEnum.C), "testPeekForSubQueue") - Assertions.assertFalse(multiQueue.peekForType(message.type).isPresent) + Assertions.assertFalse(multiQueue.peekSubQueue(message.subQueue).isPresent) Assertions.assertTrue(multiQueue.add(message)) Assertions.assertFalse(multiQueue.isEmpty()) - val peekedMessage = multiQueue.peekForType(message.type).get() + val peekedMessage = multiQueue.peekSubQueue(message.subQueue).get() Assertions.assertEquals(message, peekedMessage) Assertions.assertFalse(multiQueue.isEmpty()) } /** - * Ensure that [MultiQueue.isEmptyForType] operates as expected when entries exist and don't exist for a specific type. + * Ensure that [MultiQueue.isEmptySubQueue] operates as expected when entries exist and don't exist for a specific sub-queue. */ @Test - fun testIsEmptyForType() + fun testIsEmptyForSubQueue() { Assertions.assertTrue(multiQueue.isEmpty()) - val type = "type" + val subQueue = "testIsEmptyForSubQueue" val data = "test data" - val message = QueueMessage(data, type) + val message = QueueMessage(data, subQueue) Assertions.assertTrue(multiQueue.add(message)) Assertions.assertFalse(multiQueue.isEmpty()) - Assertions.assertFalse(multiQueue.isEmptyForType(type)) - Assertions.assertTrue(multiQueue.isEmptyForType("another-type")) + Assertions.assertFalse(multiQueue.isEmptySubQueue(subQueue)) + Assertions.assertTrue(multiQueue.isEmptySubQueue("testIsEmptyForSubQueue-another")) } /** - * Ensure that only the specific entries are removed when [MultiQueue.clearForTypeInternal] is called. + * Ensure that only the specific entries are removed when [MultiQueue.clearSubQueueInternal] is called. */ @Test - fun testClearForType() + fun testClearForSubQueue() { Assertions.assertTrue(multiQueue.isEmpty()) - val type = "clear-for-type" - val list = listOf(QueueMessage(81273648, type), QueueMessage("test test test", type)) + val subQueue = "testClearForSubQueue" + val list = listOf(QueueMessage(81273648, subQueue), QueueMessage("test test test", subQueue)) Assertions.assertTrue(multiQueue.addAll(list)) - val singleEntryType = "single-entry-type" - val message = QueueMessage("test message", singleEntryType) + val singleEntrySubQueue = "testClearForSubQueue2" + val message = QueueMessage("test message", singleEntrySubQueue) Assertions.assertTrue(multiQueue.add(message)) Assertions.assertEquals(3, multiQueue.size) - multiQueue.clearForTypeInternal(type) + multiQueue.clearSubQueueInternal(subQueue) Assertions.assertEquals(1, multiQueue.size) - multiQueue.clearForTypeInternal(singleEntryType) + multiQueue.clearSubQueueInternal(singleEntrySubQueue) Assertions.assertTrue(multiQueue.isEmpty()) } /** - * Ensure that no change is made when the specific type has no entries. + * Ensure that no change is made when the specific sub-queue has no entries. */ @Test - fun testClearForType_DoesNotExist() + fun testClearSubQueue_DoesNotExist() { Assertions.assertTrue(multiQueue.isEmpty()) - val type = "clear-for-type-does-not-exist" - multiQueue.clearForTypeInternal(type) + val subQueue = "testClearSubQueue_DoesNotExist" + multiQueue.clearSubQueueInternal(subQueue) Assertions.assertTrue(multiQueue.isEmpty()) } @@ -511,11 +537,11 @@ abstract class AbstractMultiQueueTest fun testRetainAll() { Assertions.assertTrue(multiQueue.isEmpty()) - val type = "type1" - val type2 = "type2" + val subQueue = "testRetainAll1" + val subQueue2 = "testRetainAll2" val data = Payload("some payload", 1, true, PayloadEnum.A) val data2 = Payload("some more data", 2, false, PayloadEnum.B) - val list = listOf(QueueMessage(data, type), QueueMessage(data, type2), QueueMessage(data2, type), QueueMessage(data2, type2)) + val list = listOf(QueueMessage(data, subQueue), QueueMessage(data, subQueue2), QueueMessage(data2, subQueue), QueueMessage(data2, subQueue2)) Assertions.assertTrue(multiQueue.addAll(list)) Assertions.assertEquals(4, multiQueue.size) @@ -524,9 +550,9 @@ abstract class AbstractMultiQueueTest toRetain.addAll(list.subList(0, 2)) Assertions.assertEquals(2, toRetain.size) // No elements of this type to cover all branches of code - val type3 = "type3" - val type3Message = QueueMessage(Payload("type3 data", 3, false, PayloadEnum.C), type3) - toRetain.add(type3Message) + val subQueue3 = "testRetainAll3" + val message3 = QueueMessage(Payload("data 3", 3, false, PayloadEnum.C), subQueue3) + toRetain.add(message3) Assertions.assertEquals(3, toRetain.size) Assertions.assertTrue(multiQueue.retainAll(toRetain)) @@ -548,14 +574,14 @@ abstract class AbstractMultiQueueTest fun testPersistMessage() { Assertions.assertTrue(multiQueue.isEmpty()) - val type = "test-persist" + val subQueue = "testPersistMessage" val data = Payload("some payload", 1, true, PayloadEnum.A) val data2 = Payload("some more data", 2, false, PayloadEnum.B) - val message = QueueMessage(data, type) + val message = QueueMessage(data, subQueue) Assertions.assertTrue(multiQueue.add(message)) - val persistedMessage = multiQueue.peekForType(message.type) + val persistedMessage = multiQueue.peekSubQueue(message.subQueue) Assertions.assertTrue(persistedMessage.isPresent) val messageToUpdate = persistedMessage.get() Assertions.assertEquals(message, messageToUpdate) @@ -575,7 +601,7 @@ abstract class AbstractMultiQueueTest multiQueue.persistMessage(messageToUpdate) - val reRetrievedMessage = multiQueue.peekForType(message.type) + val reRetrievedMessage = multiQueue.peekSubQueue(message.subQueue) Assertions.assertTrue(reRetrievedMessage.isPresent) Assertions.assertEquals(messageToUpdate, reRetrievedMessage.get()) } @@ -587,7 +613,7 @@ abstract class AbstractMultiQueueTest @Test fun testPersistMessage_messageHasNullID() { - val message = QueueMessage("payload", "type") + val message = QueueMessage("payload", "testPersistMessage_messageHasNullID") Assertions.assertNull(message.id) if (multiQueue !is InMemoryMultiQueue) @@ -600,16 +626,16 @@ abstract class AbstractMultiQueueTest } /** - * Test [MultiQueue.getAssignedMessagesForType] returns only messages with a non-null [QueueMessage.assignedTo] property. + * Test [MultiQueue.getAssignedMessagesInSubQueue] returns only messages with a non-null [QueueMessage.assignedTo] property. */ @Test - fun testGetAssignedMessagesForType_noAssignedTo() + fun testGetAssignedMessagesForSubQueue_noAssignedTo() { Assertions.assertTrue(multiQueue.isEmpty()) - val type = "test-assigned-messages-for-type" - val message = QueueMessage(Payload("some payload", 1, true, PayloadEnum.A), type) - val message2 = QueueMessage(Payload("some more data", 2, false, PayloadEnum.B), type) - val message3 = QueueMessage(Payload("some more data data", 3, false, PayloadEnum.C), type) + val subQueue = "testGetAssignedMessagesForSubQueue_noAssignedTo" + val message = QueueMessage(Payload("some payload", 1, true, PayloadEnum.A), subQueue) + val message2 = QueueMessage(Payload("some more data", 2, false, PayloadEnum.B), subQueue) + val message3 = QueueMessage(Payload("some more data data", 3, false, PayloadEnum.C), subQueue) // Assign message 1 val assignedTo = "me" @@ -625,11 +651,11 @@ abstract class AbstractMultiQueueTest multiQueue.persistMessage(message2) // Ensure all messages are in the queue - val messagesInSubQueue = multiQueue.getQueueForType(type) + val messagesInSubQueue = multiQueue.getSubQueue(subQueue) Assertions.assertEquals(3, messagesInSubQueue.size) // Check only messages 1 and 2 are returned in the assigned queue - val assignedMessages = multiQueue.getAssignedMessagesForType(type, null) + val assignedMessages = multiQueue.getAssignedMessagesInSubQueue(subQueue, null) Assertions.assertEquals(2, assignedMessages.size) val list = ArrayList() @@ -640,17 +666,18 @@ abstract class AbstractMultiQueueTest } /** - * Test [MultiQueue.getAssignedMessagesForType] returns only messages with the matching [QueueMessage.assignedTo] property. + * Test [MultiQueue.getAssignedMessagesInSubQueue] returns only messages with the matching [QueueMessage.assignedTo] property. */ @Test - fun testGetAssignedMessagesForType_withAssignedTo() + fun testGetAssignedMessagesForSubQueue_withAssignedTo() { Assertions.assertTrue(multiQueue.isEmpty()) - val type = "test-assigned-messages-for-type" - val message = QueueMessage(Payload("some payload", 1, true, PayloadEnum.A), type) - val message2 = QueueMessage(Payload("some more data", 2, false, PayloadEnum.B), type) - val message3 = QueueMessage(Payload("some more data data", 3, false, PayloadEnum.C), type) - val message4 = QueueMessage(Payload("some more data data data", 4, false, PayloadEnum.A), type) + Assertions.assertTrue(multiQueue.isEmpty()) + val subQueue = "testGetAssignedMessagesForSubQueue_withAssignedTo" + val message = QueueMessage(Payload("some payload", 1, true, PayloadEnum.A), subQueue) + val message2 = QueueMessage(Payload("some more data", 2, false, PayloadEnum.B), subQueue) + val message3 = QueueMessage(Payload("some more data data", 3, false, PayloadEnum.C), subQueue) + val message4 = QueueMessage(Payload("some more data data data", 4, false, PayloadEnum.A), subQueue) // Assign message 1, 2 and 3 val assignedTo = "me" @@ -669,11 +696,11 @@ abstract class AbstractMultiQueueTest multiQueue.persistMessage(message2) // Ensure all messages are in the queue - val messagesInSubQueue = multiQueue.getQueueForType(type) + val messagesInSubQueue = multiQueue.getSubQueue(subQueue) Assertions.assertEquals(4, messagesInSubQueue.size) // Check only messages 1 and 2 are assigned to 'assignedTo' - val assignedMessages = multiQueue.getAssignedMessagesForType(type, assignedTo) + val assignedMessages = multiQueue.getAssignedMessagesInSubQueue(subQueue, assignedTo) Assertions.assertEquals(2, assignedMessages.size) val list = ArrayList() @@ -685,17 +712,17 @@ abstract class AbstractMultiQueueTest } /** - * Test [MultiQueue.getUnassignedMessagesForType] returns only messages with a `null` [QueueMessage.assignedTo] property. + * Test [MultiQueue.getUnassignedMessagesInSubQueue] returns only messages with a `null` [QueueMessage.assignedTo] property. */ @Test - fun testGetUnassignedMessagesForType() + fun testGetUnassignedMessagesForSubQueue() { Assertions.assertTrue(multiQueue.isEmpty()) - val type = "test-unassigned-messages-for-type" - val message = QueueMessage(Payload("some payload", 1, true, PayloadEnum.A), type) - val message2 = QueueMessage(Payload("some more data", 2, false, PayloadEnum.B), type) - val message3 = QueueMessage(Payload("some more data data", 3, false, PayloadEnum.C), type) - val message4 = QueueMessage(Payload("some more data data data", 4, true, PayloadEnum.A), type) + val subQueue = "testGetUnassignedMessagesForSubQueue" + val message = QueueMessage(Payload("some payload", 1, true, PayloadEnum.A), subQueue) + val message2 = QueueMessage(Payload("some more data", 2, false, PayloadEnum.B), subQueue) + val message3 = QueueMessage(Payload("some more data data", 3, false, PayloadEnum.C), subQueue) + val message4 = QueueMessage(Payload("some more data data data", 4, true, PayloadEnum.A), subQueue) val assignedTo = "you" message.assignedTo = assignedTo @@ -713,11 +740,11 @@ abstract class AbstractMultiQueueTest multiQueue.persistMessage(message3) // Ensure all messages are in the queue - val messagesInSubQueue = multiQueue.getQueueForType(type) + val messagesInSubQueue = multiQueue.getSubQueue(subQueue) Assertions.assertEquals(4, messagesInSubQueue.size) // Check only messages 3 and 4 are returned in the unassigned queue - val assignedMessages = multiQueue.getUnassignedMessagesForType(type) + val assignedMessages = multiQueue.getUnassignedMessagesInSubQueue(subQueue) Assertions.assertEquals(2, assignedMessages.size) val list = ArrayList() @@ -729,21 +756,21 @@ abstract class AbstractMultiQueueTest } /** - * Test [MultiQueue.getOwnersAndKeysMapForType] to ensure that the provided map is populated properly with the correct entries + * Test [MultiQueue.getOwnersAndKeysMapForSubQueue] to ensure that the provided map is populated properly with the correct entries * for the current [MultiQueue] state. */ @Test - fun testGetOwnersAndKeysMapForType() + fun testGetOwnersAndKeysMapForSubQueue() { val responseMap = HashMap>() Assertions.assertTrue(multiQueue.isEmpty()) - val type = "test-get-owners-and-keys-map-for-type" - val type2 = "test-get-owners-and-keys-map-for-type2" - val message = QueueMessage(Payload("some payload", 1, true, PayloadEnum.A), type) - val message2 = QueueMessage(Payload("some more data", 2, false, PayloadEnum.B), type) - val message3 = QueueMessage(Payload("some more data data", 3, false, PayloadEnum.C), type2) - val message4 = QueueMessage(Payload("some more data data data", 4, true, PayloadEnum.A), type) + val subQueue = "test-get-owners-and-keys-map-for-subqueue" + val subQueue2 = "test-get-owners-and-keys-map-for-subqueue2" + val message = QueueMessage(Payload("some payload", 1, true, PayloadEnum.A), subQueue) + val message2 = QueueMessage(Payload("some more data", 2, false, PayloadEnum.B), subQueue) + val message3 = QueueMessage(Payload("some more data data", 3, false, PayloadEnum.C), subQueue2) + val message4 = QueueMessage(Payload("some more data data data", 4, true, PayloadEnum.A), subQueue) val assignedTo = "assigned1" val assignedTo2 = "assigned2" @@ -757,7 +784,7 @@ abstract class AbstractMultiQueueTest Assertions.assertTrue(multiQueue.add(message3)) Assertions.assertTrue(multiQueue.add(message4)) - multiQueue.getOwnersAndKeysMapForType(type, responseMap) + multiQueue.getOwnersAndKeysMapForSubQueue(subQueue, responseMap) Assertions.assertEquals(2, responseMap.keys.size) val listOfKeys = ArrayList() @@ -766,13 +793,13 @@ abstract class AbstractMultiQueueTest Assertions.assertTrue(listOfKeys.contains(assignedTo)) Assertions.assertTrue(listOfKeys.contains(assignedTo2)) - val typesForAssignedTo = responseMap[assignedTo] - Assertions.assertEquals(1, typesForAssignedTo!!.size) - Assertions.assertEquals(type, typesForAssignedTo.iterator().next()) + val subQueuesForAssignedTo = responseMap[assignedTo] + Assertions.assertEquals(1, subQueuesForAssignedTo!!.size) + Assertions.assertEquals(subQueue, subQueuesForAssignedTo.iterator().next()) - val typesForAssignedTo2 = responseMap[assignedTo2] - Assertions.assertEquals(1, typesForAssignedTo2!!.size) - Assertions.assertEquals(type, typesForAssignedTo2.iterator().next()) + val subQueuesForAssignedTo2 = responseMap[assignedTo2] + Assertions.assertEquals(1, subQueuesForAssignedTo2!!.size) + Assertions.assertEquals(subQueue, subQueuesForAssignedTo2.iterator().next()) } /** @@ -780,15 +807,15 @@ abstract class AbstractMultiQueueTest * for the current [MultiQueue] state. */ @Test - fun testGetOwnersAndKeysMap_withQueueType() + fun testGetOwnersAndKeysMap_inSubQueue() { Assertions.assertTrue(multiQueue.isEmpty()) - val type = "test-get-owners-and-keys-map" - val type2 = "test-get-owners-and-keys-map2" - val message = QueueMessage(Payload("some payload", 1, true, PayloadEnum.A), type) - val message2 = QueueMessage(Payload("some more data", 2, false, PayloadEnum.B), type) - val message3 = QueueMessage(Payload("some more data data", 3, false, PayloadEnum.C), type2) - val message4 = QueueMessage(Payload("some more data data data", 4, true, PayloadEnum.A), type) + val subQueue = "testGetOwnersAndKeysMap_inSubQueue" + val subQueue2 = "testGetOwnersAndKeysMap_inSubQueue2" + val message = QueueMessage(Payload("some payload", 1, true, PayloadEnum.A), subQueue) + val message2 = QueueMessage(Payload("some more data", 2, false, PayloadEnum.B), subQueue) + val message3 = QueueMessage(Payload("some more data data", 3, false, PayloadEnum.C), subQueue2) + val message4 = QueueMessage(Payload("some more data data data", 4, true, PayloadEnum.A), subQueue) val assignedTo = "assigned1" val assignedTo2 = "assigned2" @@ -802,7 +829,7 @@ abstract class AbstractMultiQueueTest Assertions.assertTrue(multiQueue.add(message3)) Assertions.assertTrue(multiQueue.add(message4)) - val responseMap = multiQueue.getOwnersAndKeysMap(type) + val responseMap = multiQueue.getOwnersAndKeysMap(subQueue) Assertions.assertEquals(2, responseMap.keys.size) val listOfKeys = responseMap.keys.toList() @@ -810,13 +837,13 @@ abstract class AbstractMultiQueueTest Assertions.assertTrue(listOfKeys.contains(assignedTo)) Assertions.assertTrue(listOfKeys.contains(assignedTo2)) - val typesForAssignedTo = responseMap[assignedTo] - Assertions.assertEquals(1, typesForAssignedTo!!.size) - Assertions.assertEquals(type, typesForAssignedTo.iterator().next()) + val subQueuesForAssignedTo = responseMap[assignedTo] + Assertions.assertEquals(1, subQueuesForAssignedTo!!.size) + Assertions.assertEquals(subQueue, subQueuesForAssignedTo.iterator().next()) - val typesForAssignedTo2 = responseMap[assignedTo2] - Assertions.assertEquals(1, typesForAssignedTo2!!.size) - Assertions.assertEquals(type, typesForAssignedTo2.iterator().next()) + val subQueuesForAssignedTo2 = responseMap[assignedTo2] + Assertions.assertEquals(1, subQueuesForAssignedTo2!!.size) + Assertions.assertEquals(subQueue, subQueuesForAssignedTo2.iterator().next()) } /** @@ -824,19 +851,19 @@ abstract class AbstractMultiQueueTest * for the current [MultiQueue] state. */ @Test - fun testGetOwnersAndKeysMap_withoutQueueType() + fun testGetOwnersAndKeysMap_notInSubQueue() { Assertions.assertTrue(multiQueue.isEmpty()) - val type = "test-get-owners-and-keys-map" - val type2 = "test-get-owners-and-keys-map2" - val type3 = "test-get-owners-and-keys-map3" - val message = QueueMessage(Payload("some payload", 1, true, PayloadEnum.A), type) - val message2 = QueueMessage(Payload("some more data", 2, false, PayloadEnum.B), type) - val message3 = QueueMessage(Payload("some more data data", 3, false, PayloadEnum.C), type2) - val message4 = QueueMessage(Payload("some more data data data", 4, true, PayloadEnum.A), type) - val message5 = QueueMessage(Payload("just data", 5, true, PayloadEnum.C), type3) - val message6 = QueueMessage(Payload("just more data", 6, false, PayloadEnum.B), type2) - val message7 = QueueMessage(Payload("just more and more data", 7, false, PayloadEnum.A), type) + val subQueue = "test-get-owners-and-keys-map" + val subQueue2 = "test-get-owners-and-keys-map2" + val subQueue3 = "test-get-owners-and-keys-map3" + val message = QueueMessage(Payload("some payload", 1, true, PayloadEnum.A), subQueue) + val message2 = QueueMessage(Payload("some more data", 2, false, PayloadEnum.B), subQueue) + val message3 = QueueMessage(Payload("some more data data", 3, false, PayloadEnum.C), subQueue2) + val message4 = QueueMessage(Payload("some more data data data", 4, true, PayloadEnum.A), subQueue) + val message5 = QueueMessage(Payload("just data", 5, true, PayloadEnum.C), subQueue3) + val message6 = QueueMessage(Payload("just more data", 6, false, PayloadEnum.B), subQueue2) + val message7 = QueueMessage(Payload("just more and more data", 7, false, PayloadEnum.A), subQueue) val assignedTo = "assigned1" val assignedTo2 = "assigned2" @@ -866,19 +893,19 @@ abstract class AbstractMultiQueueTest Assertions.assertTrue(listOfKeys.contains(assignedTo2)) Assertions.assertTrue(listOfKeys.contains(assignedTo3)) - val typesForAssignedTo = responseMap[assignedTo]!!.toList() - Assertions.assertEquals(1, typesForAssignedTo.size) - Assertions.assertTrue(typesForAssignedTo.contains(type)) + val subQueuesForAssignedTo = responseMap[assignedTo]!!.toList() + Assertions.assertEquals(1, subQueuesForAssignedTo.size) + Assertions.assertTrue(subQueuesForAssignedTo.contains(subQueue)) - val typesForAssignedTo2 = responseMap[assignedTo2]!!.toList() - Assertions.assertEquals(2, typesForAssignedTo2.size) - Assertions.assertTrue(typesForAssignedTo2.contains(type)) - Assertions.assertTrue(typesForAssignedTo2.contains(type2)) + val subQueuesForAssignedTo2 = responseMap[assignedTo2]!!.toList() + Assertions.assertEquals(2, subQueuesForAssignedTo2.size) + Assertions.assertTrue(subQueuesForAssignedTo2.contains(subQueue)) + Assertions.assertTrue(subQueuesForAssignedTo2.contains(subQueue2)) - val typesForAssignedTo3 = responseMap[assignedTo3]!!.toList() - Assertions.assertEquals(2, typesForAssignedTo3.size) - Assertions.assertTrue(typesForAssignedTo3.contains(type2)) - Assertions.assertTrue(typesForAssignedTo3.contains(type3)) + val subQueuesForAssignedTo3 = responseMap[assignedTo3]!!.toList() + Assertions.assertEquals(2, subQueuesForAssignedTo3.size) + Assertions.assertTrue(subQueuesForAssignedTo3.contains(subQueue2)) + Assertions.assertTrue(subQueuesForAssignedTo3.contains(subQueue3)) } /** @@ -887,11 +914,14 @@ abstract class AbstractMultiQueueTest @Test fun testGetMessageByUUID_matchingMessage() { - val type = "testGetMessageByUUID_matchingMessage" - val message = QueueMessage(Payload("some payload", 1, true, PayloadEnum.A), type) + val subQueue = "testGetMessageByUUID_matchingMessage" + val message = QueueMessage(Payload("some payload", 1, true, PayloadEnum.A), subQueue) Assertions.assertTrue(multiQueue.add(message)) - Assertions.assertEquals(message, multiQueue.getMessageByUUID(message.uuid).get()) + val retrievedMessage = multiQueue.getMessageByUUID(message.uuid) + Assertions.assertTrue(retrievedMessage.isPresent) + Assertions.assertEquals(message, retrievedMessage.get()) + Assertions.assertEquals(message.payload, retrievedMessage.get().payload) } /** @@ -914,6 +944,71 @@ abstract class AbstractMultiQueueTest multiQueue.performHealthCheck() } + /** + * Ensure that we cannot add a new [QueueMessage] with [QueueMessage.subQueue] set to any of the [MultiQueueAuthenticator.getReservedSubQueues] entries. + */ + @Test + fun testAddReservedSubQueue() + { + doWithRestrictedMode(RestrictionMode.RESTRICTED) { + authenticator.getReservedSubQueues().forEach { reservedSubQueueIdentifier -> + val message = QueueMessage("Data", reservedSubQueueIdentifier) + Assertions.assertThrows(IllegalSubQueueIdentifierException::class.java) { + multiQueue.add(message) + } + } + } + } + + /** + * Ensure that even we have a restricted queue entry registered, when [MultiQueue.keys] is called, the entry + * is removed and not returned. + */ + @Test + fun testKeysWithReservedSubQueueUsage() + { + doWithRestrictedMode(RestrictionMode.HYBRID) { + var keys = multiQueue.keys() + authenticator.getReservedSubQueues().forEach { reservedSubQueueIdentifier -> + Assertions.assertFalse(keys.contains(reservedSubQueueIdentifier)) + } + + val restrictedSubQueue = "testKeysWithReservedSubQueueUsage" + authenticator.addRestrictedEntry(restrictedSubQueue) + + keys = multiQueue.keys() + authenticator.getReservedSubQueues().forEach { reservedSubQueueIdentifier -> + Assertions.assertFalse(keys.contains(reservedSubQueueIdentifier)) + } + + // Need to clear the restricted queue otherwise it affects other redis tests if they are not in a "non-None" auth state + authenticator.clearRestrictedSubQueues() + } + } + + /** + * Perform the provided [function] with the [RestrictionMode] set to [restrictionMode]. + * Once completed the [RestrictionMode] will be set back to its initial value. + * + * @param restrictionMode the [RestrictionMode] to be set while the [function] is being called + * @param function the function to call with the provided [RestrictionMode] being active + * @return `T` the result of the [function] + */ + private fun doWithRestrictedMode(restrictionMode: RestrictionMode, function: Supplier): T + { + val previousRestrictedMode = authenticator.getRestrictionMode() + authenticator.setRestrictionMode(restrictionMode) + + try + { + return function.get() + } + finally + { + authenticator.setRestrictionMode(previousRestrictedMode) + } + } + /** * Ensure that all applicable methods throw an [UnsupportedOperationException]. */ @@ -930,7 +1025,7 @@ abstract class AbstractMultiQueueTest { Assertions.assertThrows(UnsupportedOperationException::class.java) { - multiQueue.offer(QueueMessage(Payload("test data", 13, false, PayloadEnum.C), "test type")) + multiQueue.offer(QueueMessage(Payload("test data", 13, false, PayloadEnum.C), "test sub-queue")) } }, { diff --git a/src/test/kotlin/au/kilemon/messagequeue/queue/cache/redis/RedisSentinelMultiQueueTest.kt b/src/test/kotlin/au/kilemon/messagequeue/queue/cache/redis/RedisSentinelMultiQueueTest.kt index a3101f0..b7a4002 100644 --- a/src/test/kotlin/au/kilemon/messagequeue/queue/cache/redis/RedisSentinelMultiQueueTest.kt +++ b/src/test/kotlin/au/kilemon/messagequeue/queue/cache/redis/RedisSentinelMultiQueueTest.kt @@ -3,23 +3,16 @@ package au.kilemon.messagequeue.queue.cache.redis import au.kilemon.messagequeue.configuration.QueueConfiguration import au.kilemon.messagequeue.configuration.cache.redis.RedisConfiguration import au.kilemon.messagequeue.logging.LoggingConfiguration -import au.kilemon.messagequeue.queue.AbstractMultiQueueTest +import au.kilemon.messagequeue.queue.MultiQueueTest import au.kilemon.messagequeue.settings.MessageQueueSettings import org.junit.jupiter.api.AfterAll import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.extension.ExtendWith -import org.springframework.beans.factory.annotation.Autowired -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest -import org.springframework.boot.test.context.TestConfiguration import org.springframework.boot.test.util.TestPropertyValues import org.springframework.context.ApplicationContextInitializer import org.springframework.context.ConfigurableApplicationContext -import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Import -import org.springframework.context.annotation.Lazy -import org.springframework.data.redis.connection.RedisConnectionFactory -import org.springframework.data.redis.connection.RedisSentinelConfiguration import org.springframework.test.context.ContextConfiguration import org.springframework.test.context.TestPropertySource import org.springframework.test.context.junit.jupiter.SpringExtension @@ -36,20 +29,18 @@ import java.util.* * @author github.com/Kilemonn */ @ExtendWith(SpringExtension::class) -@TestPropertySource(properties = ["${MessageQueueSettings.MULTI_QUEUE_TYPE}=REDIS"]) +@TestPropertySource(properties = ["${MessageQueueSettings.STORAGE_MEDIUM}=REDIS"]) @Testcontainers @ContextConfiguration(initializers = [RedisSentinelMultiQueueTest.Initializer::class]) -@Import(*[LoggingConfiguration::class, RedisConfiguration::class, QueueConfiguration::class, AbstractMultiQueueTest.AbstractMultiQueueTestConfiguration::class]) -class RedisSentinelMultiQueueTest: AbstractMultiQueueTest() +@Import(*[LoggingConfiguration::class, RedisConfiguration::class, QueueConfiguration::class, MultiQueueTest.MultiQueueTestConfiguration::class]) +class RedisSentinelMultiQueueTest: MultiQueueTest() { companion object { - private const val REDIS_CONTAINER: String = "redis:7.0.5-alpine" - + private const val REDIS_CONTAINER: String = "redis:7.2.3-alpine" private const val REDIS_SENTINEL_CONTAINER: String = "s7anley/redis-sentinel-docker:3.2.12" lateinit var redis: GenericContainer<*> - lateinit var sentinel: GenericContainer<*> /** diff --git a/src/test/kotlin/au/kilemon/messagequeue/queue/cache/redis/RedisStandAloneMultiQueueTest.kt b/src/test/kotlin/au/kilemon/messagequeue/queue/cache/redis/RedisStandAloneMultiQueueTest.kt index d736ab8..c00c9f6 100644 --- a/src/test/kotlin/au/kilemon/messagequeue/queue/cache/redis/RedisStandAloneMultiQueueTest.kt +++ b/src/test/kotlin/au/kilemon/messagequeue/queue/cache/redis/RedisStandAloneMultiQueueTest.kt @@ -3,31 +3,27 @@ package au.kilemon.messagequeue.queue.cache.redis import au.kilemon.messagequeue.configuration.QueueConfiguration import au.kilemon.messagequeue.configuration.cache.redis.RedisConfiguration import au.kilemon.messagequeue.logging.LoggingConfiguration -import au.kilemon.messagequeue.queue.AbstractMultiQueueTest +import au.kilemon.messagequeue.message.QueueMessage +import au.kilemon.messagequeue.queue.MultiQueueTest import au.kilemon.messagequeue.settings.MessageQueueSettings import org.junit.jupiter.api.AfterAll import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.BeforeAll import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith -import org.springframework.beans.factory.annotation.Autowired -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest import org.springframework.boot.test.context.TestConfiguration import org.springframework.boot.test.util.TestPropertyValues import org.springframework.context.ApplicationContextInitializer import org.springframework.context.ConfigurableApplicationContext -import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Import import org.springframework.context.annotation.Lazy -import org.springframework.data.redis.connection.RedisConnectionFactory -import org.springframework.data.redis.connection.RedisStandaloneConfiguration import org.springframework.test.context.ContextConfiguration import org.springframework.test.context.TestPropertySource import org.springframework.test.context.junit.jupiter.SpringExtension import org.testcontainers.containers.GenericContainer import org.testcontainers.junit.jupiter.Testcontainers import org.testcontainers.utility.DockerImageName -import java.util.* /** @@ -45,16 +41,16 @@ import java.util.* * @author github.com/Kilemonn */ @ExtendWith(SpringExtension::class) -@TestPropertySource(properties = ["${MessageQueueSettings.MULTI_QUEUE_TYPE}=REDIS", "${MessageQueueSettings.REDIS_PREFIX}=test"]) +@TestPropertySource(properties = ["${MessageQueueSettings.STORAGE_MEDIUM}=REDIS", "${MessageQueueSettings.REDIS_PREFIX}=test"]) @Testcontainers @ContextConfiguration(initializers = [RedisStandAloneMultiQueueTest.Initializer::class]) -@Import(*[QueueConfiguration::class, LoggingConfiguration::class, RedisConfiguration::class, AbstractMultiQueueTest.AbstractMultiQueueTestConfiguration::class]) -class RedisStandAloneMultiQueueTest: AbstractMultiQueueTest() +@Import(*[QueueConfiguration::class, LoggingConfiguration::class, RedisConfiguration::class, MultiQueueTest.MultiQueueTestConfiguration::class]) +class RedisStandAloneMultiQueueTest: MultiQueueTest() { companion object { private const val REDIS_PORT: Int = 6379 - private const val REDIS_CONTAINER: String = "redis:7.0.5-alpine" + private const val REDIS_CONTAINER: String = "redis:7.2.3-alpine" lateinit var redis: GenericContainer<*> @@ -102,4 +98,35 @@ class RedisStandAloneMultiQueueTest: AbstractMultiQueueTest() Assertions.assertTrue(redis.isRunning) multiQueue.clear() } + + /** + * Test [RedisMultiQueue.removePrefix] to make sure the prefix is removed correctly. + */ + @Test + fun testRemovePrefix() + { + Assertions.assertTrue(multiQueue is RedisMultiQueue) + val redisMultiQueue: RedisMultiQueue = (multiQueue as RedisMultiQueue) + Assertions.assertTrue(redisMultiQueue.hasPrefix()) + + val prefix = redisMultiQueue.getPrefix() + + val subQueue = "removePrefix" + val subQueue2 = "removePrefix2" + Assertions.assertTrue(redisMultiQueue.add(QueueMessage("data", subQueue))) + Assertions.assertTrue(redisMultiQueue.add(QueueMessage("data2", subQueue2))) + + val keys = redisMultiQueue.keys() + Assertions.assertTrue(keys.contains("$prefix$subQueue")) + Assertions.assertTrue(keys.contains("$prefix$subQueue2")) + keys.forEach { key -> Assertions.assertTrue(key.startsWith(prefix)) } + + val removedPrefix = redisMultiQueue.removePrefix(keys) + Assertions.assertFalse(removedPrefix.contains("$prefix$subQueue")) + Assertions.assertFalse(removedPrefix.contains("$prefix$subQueue2")) + removedPrefix.forEach { key -> Assertions.assertFalse(key.startsWith(prefix)) } + + Assertions.assertTrue(removedPrefix.contains(subQueue)) + Assertions.assertTrue(removedPrefix.contains(subQueue2)) + } } diff --git a/src/test/kotlin/au/kilemon/messagequeue/queue/inmemory/InMemoryMockMultiQueueTest.kt b/src/test/kotlin/au/kilemon/messagequeue/queue/inmemory/InMemoryMockMultiQueueTest.kt index 7cb8f27..da7f0cc 100644 --- a/src/test/kotlin/au/kilemon/messagequeue/queue/inmemory/InMemoryMockMultiQueueTest.kt +++ b/src/test/kotlin/au/kilemon/messagequeue/queue/inmemory/InMemoryMockMultiQueueTest.kt @@ -1,12 +1,19 @@ package au.kilemon.messagequeue.queue.inmemory +import au.kilemon.messagequeue.authentication.authenticator.MultiQueueAuthenticator +import au.kilemon.messagequeue.configuration.QueueConfiguration +import au.kilemon.messagequeue.logging.LoggingConfiguration import au.kilemon.messagequeue.message.QueueMessage +import au.kilemon.messagequeue.queue.MultiQueue +import au.kilemon.messagequeue.queue.MultiQueueTest import au.kilemon.messagequeue.queue.exception.HealthCheckFailureException import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith import org.mockito.Mockito +import org.springframework.boot.test.mock.mockito.SpyBean +import org.springframework.context.annotation.Import import org.springframework.test.context.junit.jupiter.SpringExtension import java.util.* @@ -14,9 +21,17 @@ import java.util.* * A [Mockito] test that is used to simulate hard to cover error cases in calling code for all `MultiQueue` related methods that are hard to test. */ @ExtendWith(SpringExtension::class) +@Import( *[QueueConfiguration::class, LoggingConfiguration::class, MultiQueueTest.MultiQueueTestConfiguration::class] ) class InMemoryMockMultiQueueTest { - private val multiQueue: InMemoryMultiQueue = Mockito.spy(InMemoryMultiQueue::class.java) + @SpyBean + private lateinit var multiQueue: MultiQueue + + @BeforeEach + fun setup() + { + multiQueue.clear() + } /** * Test [InMemoryMultiQueue.add] to ensure that `false` is returned when [InMemoryMultiQueue.addInternal] returns `false`. @@ -24,7 +39,7 @@ class InMemoryMockMultiQueueTest @Test fun testPerformAdd_returnsFalse() { - val message = QueueMessage(null, "type") + val message = QueueMessage(null, "testPerformAdd_returnsFalse") Mockito.`when`(multiQueue.addInternal(message)).thenReturn(false) Mockito.`when`(multiQueue.containsUUID(message.uuid)).thenReturn(Optional.empty()) Assertions.assertFalse(multiQueue.add(message)) @@ -52,7 +67,7 @@ class InMemoryMockMultiQueueTest @Test fun testRetainAll_removeFails() { - val message = QueueMessage("payload", "type-string") + val message = QueueMessage("payload", "testRetainAll_removeFails") Mockito.`when`(multiQueue.remove(message)).thenReturn(false) Assertions.assertTrue(multiQueue.add(message)) diff --git a/src/test/kotlin/au/kilemon/messagequeue/queue/inmemory/InMemoryMultiQueueTest.kt b/src/test/kotlin/au/kilemon/messagequeue/queue/inmemory/InMemoryMultiQueueTest.kt index ce5ae0a..852955b 100644 --- a/src/test/kotlin/au/kilemon/messagequeue/queue/inmemory/InMemoryMultiQueueTest.kt +++ b/src/test/kotlin/au/kilemon/messagequeue/queue/inmemory/InMemoryMultiQueueTest.kt @@ -2,16 +2,11 @@ package au.kilemon.messagequeue.queue.inmemory import au.kilemon.messagequeue.configuration.QueueConfiguration import au.kilemon.messagequeue.logging.LoggingConfiguration -import au.kilemon.messagequeue.queue.AbstractMultiQueueTest +import au.kilemon.messagequeue.queue.MultiQueueTest import au.kilemon.messagequeue.queue.MultiQueue -import au.kilemon.messagequeue.settings.MessageQueueSettings import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.extension.ExtendWith -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest -import org.springframework.boot.test.context.TestConfiguration -import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Import -import org.springframework.context.annotation.Lazy import org.springframework.test.context.junit.jupiter.SpringExtension @@ -21,8 +16,8 @@ import org.springframework.test.context.junit.jupiter.SpringExtension * @author github.com/Kilemonn */ @ExtendWith(SpringExtension::class) -@Import( *[QueueConfiguration::class, LoggingConfiguration::class, AbstractMultiQueueTest.AbstractMultiQueueTestConfiguration::class] ) -class InMemoryMultiQueueTest: AbstractMultiQueueTest() +@Import( *[QueueConfiguration::class, LoggingConfiguration::class, MultiQueueTest.MultiQueueTestConfiguration::class] ) +class InMemoryMultiQueueTest: MultiQueueTest() { /** * Ensure the [MultiQueue] is cleared before each test. diff --git a/src/test/kotlin/au/kilemon/messagequeue/queue/nosql/mongo/MongoMultiQueueTest.kt b/src/test/kotlin/au/kilemon/messagequeue/queue/nosql/mongo/MongoMultiQueueTest.kt index e9ddd6f..775645b 100644 --- a/src/test/kotlin/au/kilemon/messagequeue/queue/nosql/mongo/MongoMultiQueueTest.kt +++ b/src/test/kotlin/au/kilemon/messagequeue/queue/nosql/mongo/MongoMultiQueueTest.kt @@ -2,7 +2,7 @@ package au.kilemon.messagequeue.queue.nosql.mongo import au.kilemon.messagequeue.configuration.QueueConfiguration import au.kilemon.messagequeue.logging.LoggingConfiguration -import au.kilemon.messagequeue.queue.AbstractMultiQueueTest +import au.kilemon.messagequeue.queue.MultiQueueTest import au.kilemon.messagequeue.queue.nosql.mongo.MongoMultiQueueTest.Companion.MONGO_CONTAINER import au.kilemon.messagequeue.settings.MessageQueueSettings import org.junit.jupiter.api.AfterAll @@ -28,11 +28,11 @@ import org.testcontainers.utility.DockerImageName */ @ExtendWith(SpringExtension::class) @Testcontainers -@DataMongoTest(properties = ["${MessageQueueSettings.MULTI_QUEUE_TYPE}=MONGO"]) +@DataMongoTest(properties = ["${MessageQueueSettings.STORAGE_MEDIUM}=MONGO"]) @ContextConfiguration(initializers = [MongoMultiQueueTest.Initializer::class]) @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) -@Import( *[QueueConfiguration::class, LoggingConfiguration::class, AbstractMultiQueueTest.AbstractMultiQueueTestConfiguration::class] ) -class MongoMultiQueueTest: AbstractMultiQueueTest() +@Import( *[QueueConfiguration::class, LoggingConfiguration::class, MultiQueueTest.MultiQueueTestConfiguration::class] ) +class MongoMultiQueueTest: MultiQueueTest() { companion object { @@ -95,8 +95,6 @@ class MongoMultiQueueTest: AbstractMultiQueueTest() /** * Check the container is running before each test as it's required for the methods to access the [MongoMultiQueue]. - * - * We will call [MongoMultiQueue.initialiseQueueIndex] here because we pass in the [MessageQueueSettings.MULTI_QUEUE_LAZY_INITIALISE] in the [DataMongoTest] annotation. */ @BeforeEach fun beforeEach() diff --git a/src/test/kotlin/au/kilemon/messagequeue/queue/sql/MySqlMultiQueueTest.kt b/src/test/kotlin/au/kilemon/messagequeue/queue/sql/MySqlMultiQueueTest.kt index b90a587..d697817 100644 --- a/src/test/kotlin/au/kilemon/messagequeue/queue/sql/MySqlMultiQueueTest.kt +++ b/src/test/kotlin/au/kilemon/messagequeue/queue/sql/MySqlMultiQueueTest.kt @@ -2,7 +2,7 @@ package au.kilemon.messagequeue.queue.sql import au.kilemon.messagequeue.configuration.QueueConfiguration import au.kilemon.messagequeue.logging.LoggingConfiguration -import au.kilemon.messagequeue.queue.AbstractMultiQueueTest +import au.kilemon.messagequeue.queue.MultiQueueTest import org.junit.jupiter.api.AfterAll import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.BeforeEach @@ -21,14 +21,14 @@ import java.util.HashMap * @author github.com/Kilemonn */ @ContextConfiguration(initializers = [MySqlMultiQueueTest.Initializer::class]) -@Import( *[QueueConfiguration::class, LoggingConfiguration::class, AbstractMultiQueueTest.AbstractMultiQueueTestConfiguration::class] ) -class MySqlMultiQueueTest : AbstractSqlMultiQueueTest() +@Import( *[QueueConfiguration::class, LoggingConfiguration::class, MultiQueueTest.MultiQueueTestConfiguration::class] ) +class MySqlMultiQueueTest : SqlMultiQueueTest() { companion object { lateinit var database: GenericContainer<*> - private const val MYSQL_CONTAINER = "mysql:8.0.31" + private const val MYSQL_CONTAINER = "mysql:8.0.35" private const val MYSQL_PORT = 3306 /** diff --git a/src/test/kotlin/au/kilemon/messagequeue/queue/sql/PostgreSqlMultiQueueTest.kt b/src/test/kotlin/au/kilemon/messagequeue/queue/sql/PostgreSqlMultiQueueTest.kt index 8e5dcc1..4c9849f 100644 --- a/src/test/kotlin/au/kilemon/messagequeue/queue/sql/PostgreSqlMultiQueueTest.kt +++ b/src/test/kotlin/au/kilemon/messagequeue/queue/sql/PostgreSqlMultiQueueTest.kt @@ -2,7 +2,7 @@ package au.kilemon.messagequeue.queue.sql import au.kilemon.messagequeue.configuration.QueueConfiguration import au.kilemon.messagequeue.logging.LoggingConfiguration -import au.kilemon.messagequeue.queue.AbstractMultiQueueTest +import au.kilemon.messagequeue.queue.MultiQueueTest import org.junit.jupiter.api.AfterAll import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.BeforeEach @@ -22,14 +22,14 @@ import java.util.* * @author github.com/Kilemonn */ @ContextConfiguration(initializers = [PostgreSqlMultiQueueTest.Initializer::class]) -@Import( *[QueueConfiguration::class, LoggingConfiguration::class, AbstractMultiQueueTest.AbstractMultiQueueTestConfiguration::class] ) -class PostgreSqlMultiQueueTest: AbstractSqlMultiQueueTest() +@Import( *[QueueConfiguration::class, LoggingConfiguration::class, MultiQueueTest.MultiQueueTestConfiguration::class] ) +class PostgreSqlMultiQueueTest: SqlMultiQueueTest() { companion object { lateinit var database: GenericContainer<*> - private const val POSTGRES_CONTAINER = "postgres:14.5" + private const val POSTGRES_CONTAINER = "postgres:14.9-alpine" private const val POSTGRES_PORT = 5432 /** diff --git a/src/test/kotlin/au/kilemon/messagequeue/queue/sql/AbstractSqlMultiQueueTest.kt b/src/test/kotlin/au/kilemon/messagequeue/queue/sql/SqlMultiQueueTest.kt similarity index 60% rename from src/test/kotlin/au/kilemon/messagequeue/queue/sql/AbstractSqlMultiQueueTest.kt rename to src/test/kotlin/au/kilemon/messagequeue/queue/sql/SqlMultiQueueTest.kt index a0b5889..accda91 100644 --- a/src/test/kotlin/au/kilemon/messagequeue/queue/sql/AbstractSqlMultiQueueTest.kt +++ b/src/test/kotlin/au/kilemon/messagequeue/queue/sql/SqlMultiQueueTest.kt @@ -1,19 +1,11 @@ package au.kilemon.messagequeue.queue.sql -import au.kilemon.messagequeue.configuration.QueueConfiguration -import au.kilemon.messagequeue.configuration.cache.redis.RedisConfiguration -import au.kilemon.messagequeue.logging.LoggingConfiguration -import au.kilemon.messagequeue.queue.AbstractMultiQueueTest +import au.kilemon.messagequeue.queue.MultiQueueTest import au.kilemon.messagequeue.settings.MessageQueueSettings import org.junit.jupiter.api.extension.ExtendWith -import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest import org.springframework.boot.test.context.TestConfiguration -import org.springframework.context.annotation.Bean -import org.springframework.context.annotation.Import -import org.springframework.context.annotation.Lazy import org.springframework.test.context.junit.jupiter.SpringExtension import org.testcontainers.junit.jupiter.Testcontainers @@ -33,6 +25,6 @@ import org.testcontainers.junit.jupiter.Testcontainers */ @ExtendWith(SpringExtension::class) @Testcontainers -@DataJpaTest(properties = ["${MessageQueueSettings.MULTI_QUEUE_TYPE}=SQL", "spring.jpa.hibernate.ddl-auto=create", "spring.autoconfigure.exclude="]) +@DataJpaTest(properties = ["${MessageQueueSettings.STORAGE_MEDIUM}=SQL", "spring.jpa.hibernate.ddl-auto=create", "spring.autoconfigure.exclude="]) @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) -abstract class AbstractSqlMultiQueueTest: AbstractMultiQueueTest() +abstract class SqlMultiQueueTest: MultiQueueTest() diff --git a/src/test/kotlin/au/kilemon/messagequeue/rest/controller/AuthControllerTest.kt b/src/test/kotlin/au/kilemon/messagequeue/rest/controller/AuthControllerTest.kt new file mode 100644 index 0000000..e4b8505 --- /dev/null +++ b/src/test/kotlin/au/kilemon/messagequeue/rest/controller/AuthControllerTest.kt @@ -0,0 +1,433 @@ +package au.kilemon.messagequeue.rest.controller + +import au.kilemon.messagequeue.authentication.RestrictionMode +import au.kilemon.messagequeue.authentication.authenticator.MultiQueueAuthenticator +import au.kilemon.messagequeue.authentication.token.JwtTokenProvider +import au.kilemon.messagequeue.configuration.QueueConfiguration +import au.kilemon.messagequeue.filter.JwtAuthenticationFilter +import au.kilemon.messagequeue.logging.LoggingConfiguration +import au.kilemon.messagequeue.message.QueueMessage +import au.kilemon.messagequeue.queue.MultiQueue +import au.kilemon.messagequeue.rest.response.AuthResponse +import au.kilemon.messagequeue.settings.MessageQueueSettings +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.mockito.Mockito +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest +import org.springframework.boot.test.context.TestConfiguration +import org.springframework.boot.test.mock.mockito.SpyBean +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Import +import org.springframework.http.MediaType +import org.springframework.test.context.junit.jupiter.SpringExtension +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.MvcResult +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders +import org.springframework.test.web.servlet.result.MockMvcResultMatchers +import java.util.* + +/** + * A test class for the [AuthController]. + * Providing tests of the context and endpoint validation/handling itself. + * + * @author github.com/Kilemonn + */ +@ExtendWith(SpringExtension::class) +@WebMvcTest(controllers = [AuthController::class], properties = ["${MessageQueueSettings.STORAGE_MEDIUM}=IN_MEMORY"]) +@Import(*[QueueConfiguration::class, LoggingConfiguration::class]) +class AuthControllerTest +{ + /** + * A [TestConfiguration] for the outer class. + * + * @author github.com/Kilemonn + */ + @TestConfiguration + open class TestConfig + { + @Bean + open fun getSettings(): MessageQueueSettings + { + return MessageQueueSettings() + } + } + + // Setting as a Spy to override it to replicate different scenarios + @SpyBean + private lateinit var multiQueueAuthenticator: MultiQueueAuthenticator + + // Setting as a Spy to override it to replicate different scenarios + @SpyBean + private lateinit var jwtTokenProvider: JwtTokenProvider + + @Autowired + private lateinit var multiQueue: MultiQueue + + @Autowired + private lateinit var mockMvc: MockMvc + + private val gson: Gson = Gson() + + @BeforeEach + fun setUp() + { + multiQueueAuthenticator.clearRestrictedSubQueues() + multiQueue.clear() + } + + /** + * Ensure [AuthController.restrictSubQueue] returns [org.springframework.http.HttpStatus.NO_CONTENT] when the + * [RestrictionMode] is set to [RestrictionMode.NONE]. + */ + @Test + fun testRestrictSubQueue_inNoneMode() + { + val subQueue = "testRestrictSubQueue_inNoneMode" + Assertions.assertEquals(RestrictionMode.NONE, multiQueueAuthenticator.getRestrictionMode()) + mockMvc.perform( + MockMvcRequestBuilders.post("${AuthController.AUTH_PATH}/${subQueue}") + .contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(MockMvcResultMatchers.status().isNoContent) + } + + /** + * Ensure [AuthController.restrictSubQueue] returns [org.springframework.http.HttpStatus.CONFLICT] when the + * requested sub-queue requested is already restricted. + */ + @Test + fun testRestrictSubQueue_alreadyRestricted() + { + Mockito.doReturn(RestrictionMode.HYBRID).`when`(multiQueueAuthenticator).getRestrictionMode() + Assertions.assertEquals(RestrictionMode.HYBRID, multiQueueAuthenticator.getRestrictionMode()) + + val subQueue = "testRestrictSubQueue_alreadyRestricted" + Assertions.assertTrue(multiQueueAuthenticator.addRestrictedEntry(subQueue)) + Assertions.assertTrue(multiQueueAuthenticator.isRestricted(subQueue)) + + mockMvc.perform( + MockMvcRequestBuilders.post("${AuthController.AUTH_PATH}/${subQueue}") + .contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(MockMvcResultMatchers.status().isConflict) + } + + /** + * Ensure [AuthController.restrictSubQueue] returns [org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR] + * when the requested sub-queue fails to be restricted. + */ + @Test + fun testRestrictSubQueue_wasNotAdded() + { + Mockito.doReturn(RestrictionMode.HYBRID).`when`(multiQueueAuthenticator).getRestrictionMode() + Assertions.assertEquals(RestrictionMode.HYBRID, multiQueueAuthenticator.getRestrictionMode()) + + val subQueue = "testRestrictSubQueue_wasNotAdded" + Mockito.doReturn(false).`when`(multiQueueAuthenticator).addRestrictedEntry(subQueue) + + mockMvc.perform( + MockMvcRequestBuilders.post("${AuthController.AUTH_PATH}/${subQueue}") + .contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(MockMvcResultMatchers.status().isInternalServerError) + } + + /** + * Ensure [AuthController.restrictSubQueue] returns [org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR] + * when the token fails to generate. + */ + @Test + fun testRestrictSubQueue_tokenGenerationFailure() + { + Mockito.doReturn(RestrictionMode.HYBRID).`when`(multiQueueAuthenticator).getRestrictionMode() + Assertions.assertEquals(RestrictionMode.HYBRID, multiQueueAuthenticator.getRestrictionMode()) + + val subQueue = "testRestrictSubQueue_tokenGenerationFailure" + Mockito.doReturn(Optional.empty()).`when`(jwtTokenProvider).createTokenForSubQueue(subQueue) + + mockMvc.perform( + MockMvcRequestBuilders.post("${AuthController.AUTH_PATH}/${subQueue}") + .contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(MockMvcResultMatchers.status().isInternalServerError) + } + + /** + * Ensure [AuthController.restrictSubQueue] returns [org.springframework.http.HttpStatus.OK] and a valid + * [AuthResponse] when the requested sub-queue is restricted successfully. + */ + @Test + fun testRestrictSubQueue_tokenGenerated() + { + Mockito.doReturn(RestrictionMode.HYBRID).`when`(multiQueueAuthenticator).getRestrictionMode() + Assertions.assertEquals(RestrictionMode.HYBRID, multiQueueAuthenticator.getRestrictionMode()) + + val subQueue = "testRestrictSubQueue_tokenGenerated" + + val mvcResult: MvcResult = mockMvc.perform( + MockMvcRequestBuilders.post("${AuthController.AUTH_PATH}/${subQueue}") + .contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(MockMvcResultMatchers.status().isCreated) + .andReturn() + + val authResponse = gson.fromJson(mvcResult.response.contentAsString, AuthResponse::class.java) + Assertions.assertNotNull(authResponse.token) + Assertions.assertNotNull(authResponse.correlationId) + Assertions.assertEquals(subQueue, authResponse.subQueue) + } + + /** + * Ensure that even in [RestrictionMode.RESTRICTED] mode we can call the + * [AuthController.restrictSubQueue] this is important, without this being accessible new sub-queues can never + * be restricted meaning the queue would be completely inaccessible. + */ + @Test + fun testRestrictSubQueue_inRestrictedMode() + { + Mockito.doReturn(RestrictionMode.RESTRICTED).`when`(multiQueueAuthenticator).getRestrictionMode() + Assertions.assertEquals(RestrictionMode.RESTRICTED, multiQueueAuthenticator.getRestrictionMode()) + + val subQueue = "testRestrictSubQueue_inRestrictedMode" + + val mvcResult: MvcResult = mockMvc.perform( + MockMvcRequestBuilders.post("${AuthController.AUTH_PATH}/${subQueue}") + .contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(MockMvcResultMatchers.status().isCreated) + .andReturn() + + val authResponse = gson.fromJson(mvcResult.response.contentAsString, AuthResponse::class.java) + Assertions.assertNotNull(authResponse.token) + Assertions.assertNotNull(authResponse.correlationId) + Assertions.assertEquals(subQueue, authResponse.subQueue) + } + + /** + * Ensure [AuthController.removeRestrictionFromSubQueue] returns [org.springframework.http.HttpStatus.NO_CONTENT] + * when the [RestrictionMode] is set to [RestrictionMode.NONE]. + */ + @Test + fun testRemoveRestrictionFromSubQueue_inNoneMode() + { + val subQueue = "testRemoveRestrictionFromSubQueue_inNoneMode" + + Assertions.assertEquals(RestrictionMode.NONE, multiQueueAuthenticator.getRestrictionMode()) + mockMvc.perform( + MockMvcRequestBuilders.delete("${AuthController.AUTH_PATH}/${subQueue}") + .contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(MockMvcResultMatchers.status().isAccepted) + } + + /** + * Ensure [AuthController.removeRestrictionFromSubQueue] returns [org.springframework.http.HttpStatus.FORBIDDEN] + * when the provided token does not + */ + @Test + fun testRemoveRestrictionFromSubQueue_invalidToken() + { + Mockito.doReturn(RestrictionMode.HYBRID).`when`(multiQueueAuthenticator).getRestrictionMode() + Assertions.assertEquals(RestrictionMode.HYBRID, multiQueueAuthenticator.getRestrictionMode()) + + val subQueue = "testRemoveRestrictionFromSubQueue_invalidToken" + val invalidsubQueue = "invalidsubQueue" + val token = jwtTokenProvider.createTokenForSubQueue(subQueue) + Assertions.assertTrue(token.isPresent) + + mockMvc.perform( + MockMvcRequestBuilders.delete("${AuthController.AUTH_PATH}/${invalidsubQueue}") + .header(JwtAuthenticationFilter.AUTHORIZATION_HEADER, "${JwtAuthenticationFilter.BEARER_HEADER_VALUE}${token.get()}") + .contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(MockMvcResultMatchers.status().isForbidden) + } + + /** + * Ensure [AuthController.removeRestrictionFromSubQueue] returns [org.springframework.http.HttpStatus.UNAUTHORIZED] + * when there is no auth token provided and the [RestrictionMode] is + * [RestrictionMode.RESTRICTED]. + */ + @Test + fun testRemoveRestrictionFromSubQueue_withoutAuthToken() + { + Mockito.doReturn(RestrictionMode.RESTRICTED).`when`(multiQueueAuthenticator).getRestrictionMode() + Assertions.assertEquals(RestrictionMode.RESTRICTED, multiQueueAuthenticator.getRestrictionMode()) + + val subQueue = "testRemoveRestrictionFromSubQueue_withoutAuthToken" + + mockMvc.perform( + MockMvcRequestBuilders.delete("${AuthController.AUTH_PATH}/${subQueue}") + .contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(MockMvcResultMatchers.status().isUnauthorized) + } + + /** + * Ensure [AuthController.removeRestrictionFromSubQueue] returns [org.springframework.http.HttpStatus.NO_CONTENT] + * when the token is valid but there is no restriction placed on the requested sub-queue. + */ + @Test + fun testRemoveRestrictionFromSubQueue_validTokenButNotRestricted() + { + Mockito.doReturn(RestrictionMode.HYBRID).`when`(multiQueueAuthenticator).getRestrictionMode() + Assertions.assertEquals(RestrictionMode.HYBRID, multiQueueAuthenticator.getRestrictionMode()) + + val subQueue = "testRemoveRestrictionFromSubQueue_validToken" + val token = jwtTokenProvider.createTokenForSubQueue(subQueue) + Assertions.assertTrue(token.isPresent) + + Assertions.assertFalse(multiQueueAuthenticator.isRestricted(subQueue)) + + mockMvc.perform( + MockMvcRequestBuilders.delete("${AuthController.AUTH_PATH}/${subQueue}") + .header(JwtAuthenticationFilter.AUTHORIZATION_HEADER, "${JwtAuthenticationFilter.BEARER_HEADER_VALUE}${token.get()}") + .contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(MockMvcResultMatchers.status().isNoContent) + } + + /** + * Ensure [AuthController.removeRestrictionFromSubQueue] returns + * [org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR] when there is an error when attempting to remove + * the restriction on the sub-queue. + */ + @Test + fun testRemoveRestrictionFromSubQueue_failedToRemoveRestriction() + { + Mockito.doReturn(RestrictionMode.HYBRID).`when`(multiQueueAuthenticator).getRestrictionMode() + Assertions.assertEquals(RestrictionMode.HYBRID, multiQueueAuthenticator.getRestrictionMode()) + + val subQueue = "testRemoveRestrictionFromSubQueue_failedToRemoveRestriction" + val token = jwtTokenProvider.createTokenForSubQueue(subQueue) + Assertions.assertTrue(token.isPresent) + + Assertions.assertTrue(multiQueueAuthenticator.addRestrictedEntry(subQueue)) + Assertions.assertTrue(multiQueueAuthenticator.isRestricted(subQueue)) + + Mockito.doReturn(false).`when`(multiQueueAuthenticator).removeRestriction(subQueue) + + mockMvc.perform( + MockMvcRequestBuilders.delete("${AuthController.AUTH_PATH}/${subQueue}") + .header(JwtAuthenticationFilter.AUTHORIZATION_HEADER, "${JwtAuthenticationFilter.BEARER_HEADER_VALUE}${token.get()}") + .contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(MockMvcResultMatchers.status().isInternalServerError) + } + + /** + * Ensure [AuthController.removeRestrictionFromSubQueue] returns [org.springframework.http.HttpStatus.OK] when + * the sub-queue restriction is removed BUT the sub-queue is not cleared when the query parameter + * [RestParameters.CLEAR_QUEUE] is not provided. + */ + @Test + fun testRemoveRestrictionFromSubQueue_removeButDontClearQueue() + { + Mockito.doReturn(RestrictionMode.HYBRID).`when`(multiQueueAuthenticator).getRestrictionMode() + Assertions.assertEquals(RestrictionMode.HYBRID, multiQueueAuthenticator.getRestrictionMode()) + + val subQueue = "testRemoveRestrictionFromSubQueue_removeButDontClearQueue" + val token = jwtTokenProvider.createTokenForSubQueue(subQueue) + Assertions.assertTrue(token.isPresent) + + Assertions.assertTrue(multiQueueAuthenticator.addRestrictedEntry(subQueue)) + Assertions.assertTrue(multiQueueAuthenticator.isRestricted(subQueue)) + + multiQueue.clear() + try + { + Assertions.assertTrue(multiQueue.add(QueueMessage("a payload", subQueue))) + Assertions.assertEquals(1, multiQueue.size) + + mockMvc.perform( + MockMvcRequestBuilders.delete("${AuthController.AUTH_PATH}/${subQueue}") + .header(JwtAuthenticationFilter.AUTHORIZATION_HEADER, "${JwtAuthenticationFilter.BEARER_HEADER_VALUE}${token.get()}") + .contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(MockMvcResultMatchers.status().isOk) + + Assertions.assertEquals(1, multiQueue.size) + Assertions.assertFalse(multiQueueAuthenticator.isRestricted(subQueue)) + } + finally + { + multiQueue.clear() + } + } + + /** + * Ensure [AuthController.removeRestrictionFromSubQueue] returns [org.springframework.http.HttpStatus.OK] when + * the sub-queue restriction is removed AND make sure the sub-queue is cleared when the query parameter + * [RestParameters.CLEAR_QUEUE] is provided and set to `true`. + */ + @Test + fun testRemoveRestrictionFromSubQueue_removeAndClearQueue() + { + Mockito.doReturn(RestrictionMode.HYBRID).`when`(multiQueueAuthenticator).getRestrictionMode() + Assertions.assertEquals(RestrictionMode.HYBRID, multiQueueAuthenticator.getRestrictionMode()) + + val subQueue = "testRemoveRestrictionFromSubQueue_removeAndClearQueue" + val token = jwtTokenProvider.createTokenForSubQueue(subQueue) + Assertions.assertTrue(token.isPresent) + + Assertions.assertTrue(multiQueueAuthenticator.addRestrictedEntry(subQueue)) + Assertions.assertTrue(multiQueueAuthenticator.isRestricted(subQueue)) + + multiQueue.clear() + try + { + Assertions.assertTrue(multiQueue.add(QueueMessage("a payload", subQueue))) + Assertions.assertEquals(1, multiQueue.size) + + mockMvc.perform( + MockMvcRequestBuilders.delete("${AuthController.AUTH_PATH}/${subQueue}?${RestParameters.CLEAR_QUEUE}=true") + .header(JwtAuthenticationFilter.AUTHORIZATION_HEADER, "${JwtAuthenticationFilter.BEARER_HEADER_VALUE}${token.get()}") + .contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(MockMvcResultMatchers.status().isOk) + + Assertions.assertEquals(0, multiQueue.size) + Assertions.assertFalse(multiQueueAuthenticator.isRestricted(subQueue)) + } + finally + { + multiQueue.clear() + } + } + + /** + * Ensure [AuthController.getRestrictedSubQueueIdentifiers] returns [org.springframework.http.HttpStatus.NO_CONTENT] + * when the [RestrictionMode] is [RestrictionMode.NONE]. + */ + @Test + fun testGetRestrictedSubQueueIdentifiers_inNoneMode() + { + Mockito.doReturn(RestrictionMode.NONE).`when`(multiQueueAuthenticator).getRestrictionMode() + Assertions.assertEquals(RestrictionMode.NONE, multiQueueAuthenticator.getRestrictionMode()) + + mockMvc.perform(MockMvcRequestBuilders.get(AuthController.AUTH_PATH) + .contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(MockMvcResultMatchers.status().isNoContent) + } + + /** + * Ensure [AuthController.getRestrictedSubQueueIdentifiers] returns [org.springframework.http.HttpStatus.OK] + * when the [RestrictionMode] is not in [RestrictionMode.NONE]. + * And the response set matches the entries that are restricted. + */ + @Test + fun testGetRestrictedSubQueueIdentifiers_notInNoneMode() + { + Mockito.doReturn(RestrictionMode.HYBRID).`when`(multiQueueAuthenticator).getRestrictionMode() + Assertions.assertEquals(RestrictionMode.HYBRID, multiQueueAuthenticator.getRestrictionMode()) + + val restrictedIdentifiers = setOf("testGetRestrictedSubQueueIdentifiers_inNoneMode1", "testGetRestrictedSubQueueIdentifiers_inNoneMode2", + "testGetRestrictedSubQueueIdentifiers_inNoneMode3", "testGetRestrictedSubQueueIdentifiers_inNoneMode4", "testGetRestrictedSubQueueIdentifiers_inNoneMode5") + + restrictedIdentifiers.forEach { identifier -> Assertions.assertTrue(multiQueueAuthenticator.addRestrictedEntry(identifier)) } + restrictedIdentifiers.forEach { identifier -> Assertions.assertTrue(multiQueueAuthenticator.isRestricted(identifier)) } + + val mvcResult: MvcResult = mockMvc.perform(MockMvcRequestBuilders.get(AuthController.AUTH_PATH) + .contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(MockMvcResultMatchers.status().isOk) + .andReturn() + + val stringSetType = object : TypeToken>() {}.type + val identifiers = gson.fromJson>(mvcResult.response.contentAsString, stringSetType) + + Assertions.assertEquals(restrictedIdentifiers.size, identifiers.size) + identifiers.forEach { identifier -> Assertions.assertTrue(restrictedIdentifiers.contains(identifier)) } + } +} diff --git a/src/test/kotlin/au/kilemon/messagequeue/rest/controller/MessageQueueControllerMockTest.kt b/src/test/kotlin/au/kilemon/messagequeue/rest/controller/MessageQueueControllerMockTest.kt deleted file mode 100644 index 7e654da..0000000 --- a/src/test/kotlin/au/kilemon/messagequeue/rest/controller/MessageQueueControllerMockTest.kt +++ /dev/null @@ -1,89 +0,0 @@ -package au.kilemon.messagequeue.rest.controller - -import au.kilemon.messagequeue.logging.LoggingConfiguration -import au.kilemon.messagequeue.message.QueueMessage -import au.kilemon.messagequeue.queue.MultiQueue -import au.kilemon.messagequeue.queue.inmemory.InMemoryMultiQueue -import au.kilemon.messagequeue.settings.MessageQueueSettings -import com.google.gson.Gson -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.mockito.Mockito -import org.springframework.beans.factory.annotation.Autowired -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest -import org.springframework.boot.test.context.TestConfiguration -import org.springframework.context.annotation.Bean -import org.springframework.context.annotation.Import -import org.springframework.http.HttpStatus -import org.springframework.http.MediaType -import org.springframework.test.context.junit.jupiter.SpringExtension -import org.springframework.test.web.servlet.MockMvc -import org.springframework.test.web.servlet.request.MockMvcRequestBuilders -import org.springframework.test.web.servlet.result.MockMvcResultMatchers - -/** - * A test class for the [MessageQueueController]. - * Specifically mocking scenarios that are hard to replicate. - * - * @author github.com/Kilemonn - */ -@ExtendWith(SpringExtension::class) -@WebMvcTest(controllers = [MessageQueueController::class], properties = ["${MessageQueueSettings.MULTI_QUEUE_TYPE}=IN_MEMORY"]) -@Import(*[LoggingConfiguration::class]) -class MessageQueueControllerMockTest -{ - /** - * A [TestConfiguration] for the outer [MessageQueueControllerTest] class. - * - * @author github.com/Kilemonn - */ - @TestConfiguration - open class TestConfig - { - @Bean - open fun getMultiQueue(): MultiQueue - { - return Mockito.spy(InMemoryMultiQueue::class.java) - } - } - - @Autowired - private lateinit var mockMvc: MockMvc - - @Autowired - lateinit var multiQueue: MultiQueue - - private val gson: Gson = Gson() - - /** - * Perform a health check call on the [MessageQueueController] to ensure a [HttpStatus.INTERNAL_SERVER_ERROR] is returned when the health check fails. - */ - @Test - fun testGetPerformHealthCheck_failureResponse() - { - Mockito.`when`(multiQueue.performHealthCheckInternal()).thenThrow(RuntimeException("Failed to perform health check.")) - - mockMvc.perform( - MockMvcRequestBuilders.get(MessageQueueController.MESSAGE_QUEUE_BASE_PATH + "/" + MessageQueueController.ENDPOINT_HEALTH_CHECK) - .contentType(MediaType.APPLICATION_JSON_VALUE)) - .andExpect(MockMvcResultMatchers.status().isInternalServerError) - .andReturn() - } - - /** - * Test [MessageQueueController.createMessage] to ensure that an internal server error is returned when [MultiQueue.add] returns `false`. - */ - @Test - fun testCreateMessage_addFails() - { - val message = QueueMessage("payload", "type") - - Mockito.`when`(multiQueue.add(message)).thenReturn(false) - - mockMvc.perform( - MockMvcRequestBuilders.post(MessageQueueController.MESSAGE_QUEUE_BASE_PATH + "/" + MessageQueueController.ENDPOINT_ENTRY) - .contentType(MediaType.APPLICATION_JSON_VALUE) - .content(gson.toJson(message))) - .andExpect(MockMvcResultMatchers.status().isInternalServerError) - } -} diff --git a/src/test/kotlin/au/kilemon/messagequeue/rest/controller/MessageQueueControllerTest.kt b/src/test/kotlin/au/kilemon/messagequeue/rest/controller/MessageQueueControllerTest.kt index 6f65aee..b6f2aed 100644 --- a/src/test/kotlin/au/kilemon/messagequeue/rest/controller/MessageQueueControllerTest.kt +++ b/src/test/kotlin/au/kilemon/messagequeue/rest/controller/MessageQueueControllerTest.kt @@ -1,7 +1,11 @@ package au.kilemon.messagequeue.rest.controller +import au.kilemon.messagequeue.authentication.RestrictionMode +import au.kilemon.messagequeue.authentication.authenticator.MultiQueueAuthenticator +import au.kilemon.messagequeue.authentication.token.JwtTokenProvider import au.kilemon.messagequeue.configuration.QueueConfiguration import au.kilemon.messagequeue.filter.CorrelationIdFilter +import au.kilemon.messagequeue.filter.JwtAuthenticationFilter import au.kilemon.messagequeue.logging.LoggingConfiguration import au.kilemon.messagequeue.message.QueueMessage import au.kilemon.messagequeue.queue.MultiQueue @@ -15,9 +19,11 @@ import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith +import org.mockito.Mockito import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest import org.springframework.boot.test.context.TestConfiguration +import org.springframework.boot.test.mock.mockito.SpyBean import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Import import org.springframework.http.HttpStatus @@ -36,7 +42,7 @@ import java.util.* * @author github.com/Kilemonn */ @ExtendWith(SpringExtension::class) -@WebMvcTest(controllers = [MessageQueueController::class], properties = ["${MessageQueueSettings.MULTI_QUEUE_TYPE}=IN_MEMORY"]) +@WebMvcTest(controllers = [MessageQueueController::class], properties = ["${MessageQueueSettings.STORAGE_MEDIUM}=IN_MEMORY"]) @Import(*[QueueConfiguration::class, LoggingConfiguration::class]) class MessageQueueControllerTest { @@ -59,63 +65,78 @@ class MessageQueueControllerTest private lateinit var mockMvc: MockMvc @Autowired + private lateinit var jwtTokenProvider: JwtTokenProvider + + @SpyBean + private lateinit var authenticator: MultiQueueAuthenticator + + @SpyBean private lateinit var multiQueue: MultiQueue private val gson: Gson = Gson() /** - * [BeforeEach] method to run [MultiQueue.clear] and ensure that [MultiQueue.isEmpty] returns `true` at the beginning of each test. + * [BeforeEach] method to run [MultiQueue.clear] and ensure that [MultiQueue.isEmpty] returns `true` at the + * beginning of each test. */ @BeforeEach fun setUp() { multiQueue.clear() Assertions.assertTrue(multiQueue.isEmpty()) + + authenticator.clearRestrictedSubQueues() + Assertions.assertTrue(authenticator.getRestrictedSubQueueIdentifiers().isEmpty()) } /** - * Test [MessageQueueController.getQueueTypeInfo] to ensure the correct information is returned for the specified `queueType`. + * Test [MessageQueueController.getSubQueueInfo] to ensure the correct information is returned for the specified + * `sub-queue`. */ @Test - fun testGetQueueTypeInfo() + fun testGetSubQueueInfo() { - val queueType = "testGetQueueTypeInfo" - Assertions.assertEquals(0, multiQueue.getQueueForType(queueType).size) - mockMvc.perform(get(MessageQueueController.MESSAGE_QUEUE_BASE_PATH + "/" + MessageQueueController.ENDPOINT_TYPE + "/" + queueType) + Assertions.assertEquals(RestrictionMode.NONE, authenticator.getRestrictionMode()) + + val subQueue = "testGetSubQueueInfo" + Assertions.assertEquals(0, multiQueue.getSubQueue(subQueue).size) + mockMvc.perform(get(MessageQueueController.MESSAGE_QUEUE_BASE_PATH + "/" + MessageQueueController.ENDPOINT_TYPE + "/" + subQueue) .contentType(MediaType.APPLICATION_JSON_VALUE)) .andExpect(MockMvcResultMatchers.status().isOk) .andExpect(MockMvcResultMatchers.content().json("0")) - val message = createQueueMessage(type = queueType) + val message = createQueueMessage(subQueue = subQueue) Assertions.assertTrue(multiQueue.add(message)) - Assertions.assertEquals(1, multiQueue.getQueueForType(queueType).size) + Assertions.assertEquals(1, multiQueue.getSubQueue(subQueue).size) - mockMvc.perform(get(MessageQueueController.MESSAGE_QUEUE_BASE_PATH + "/" + MessageQueueController.ENDPOINT_TYPE + "/" + queueType) + mockMvc.perform(get(MessageQueueController.MESSAGE_QUEUE_BASE_PATH + "/" + MessageQueueController.ENDPOINT_TYPE + "/" + subQueue) .contentType(MediaType.APPLICATION_JSON_VALUE)) .andExpect(MockMvcResultMatchers.status().isOk) .andExpect(MockMvcResultMatchers.content().json("1")) } /** - * Test [MessageQueueController.getAllQueueTypeInfo] to ensure that information for all `queue type`s is returned when no `queue type` is specified. + * Test [MessageQueueController.getAllQueueInfo] to ensure that information for all `sub-queue`s is returned + * when no `sub-queue` is specified. */ @Test - fun testGetAllQueueTypeInfo() + fun testGetAllSubQueueInfo() { - Assertions.assertTrue(multiQueue.isEmpty()) + Assertions.assertEquals(RestrictionMode.NONE, authenticator.getRestrictionMode()) + mockMvc.perform(get(MessageQueueController.MESSAGE_QUEUE_BASE_PATH + "/" + MessageQueueController.ENDPOINT_TYPE) .contentType(MediaType.APPLICATION_JSON_VALUE)) .andExpect(MockMvcResultMatchers.status().isOk) .andExpect(MockMvcResultMatchers.content().json("0")) - val message = createQueueMessage(type = "testGetAllQueueTypeInfo_type1") - val message2 = createQueueMessage(type = "testGetAllQueueTypeInfo_type2") + val message = createQueueMessage(subQueue = "testGetAllSubQueueInfo1") + val message2 = createQueueMessage(subQueue = "testGetAllSubQueueInfo2") Assertions.assertTrue(multiQueue.add(message)) Assertions.assertTrue(multiQueue.add(message2)) - Assertions.assertEquals(1, multiQueue.getQueueForType(message.type).size) - Assertions.assertEquals(1, multiQueue.getQueueForType(message2.type).size) + Assertions.assertEquals(1, multiQueue.getSubQueue(message.subQueue).size) + Assertions.assertEquals(1, multiQueue.getSubQueue(message2.subQueue).size) Assertions.assertEquals(2, multiQueue.size) mockMvc.perform(get(MessageQueueController.MESSAGE_QUEUE_BASE_PATH + "/" + MessageQueueController.ENDPOINT_TYPE) @@ -125,12 +146,15 @@ class MessageQueueControllerTest } /** - * Test [MessageQueueController.getEntry] to ensure that [HttpStatus.OK] and the correct [QueueMessage] is returned as the response. + * Test [MessageQueueController.getEntry] to ensure that [HttpStatus.OK] and the correct [QueueMessage] is returned + * as the response. */ @Test fun testGetEntry() { - val message = createQueueMessage(type = "testGetEntry") + Assertions.assertEquals(RestrictionMode.NONE, authenticator.getRestrictionMode()) + + val message = createQueueMessage(subQueue = "testGetEntry") Assertions.assertTrue(multiQueue.add(message)) @@ -144,23 +168,87 @@ class MessageQueueControllerTest val deserialisedPayload = gson.fromJson(gson.toJson(messageResponse.message.payload), Payload::class.java) Assertions.assertEquals(message.payload, deserialisedPayload) Assertions.assertNull(messageResponse.message.assignedTo) - Assertions.assertEquals(message.type, messageResponse.message.type) - Assertions.assertEquals(message.type, messageResponse.queueType) + Assertions.assertEquals(message.subQueue, messageResponse.message.subQueue) + Assertions.assertEquals(message.subQueue, messageResponse.subQueue) Assertions.assertNotNull(messageResponse.message.uuid) } /** - * Test [MessageQueueController.getEntry] to ensure that [HttpStatus.NO_CONTENT] is returned when a [UUID] that does not exist is provided. + * Test [MessageQueueController.getEntry] to ensure that [HttpStatus.NO_CONTENT] is returned when a [UUID] that + * does not exist is provided. */ @Test fun testGetEntry_ResponseBody_NotExists() { + Assertions.assertEquals(RestrictionMode.NONE, authenticator.getRestrictionMode()) + val uuid = "invalid-not-found-uuid" mockMvc.perform(get(MessageQueueController.MESSAGE_QUEUE_BASE_PATH + "/" + MessageQueueController.ENDPOINT_ENTRY + "/" + uuid) .contentType(MediaType.APPLICATION_JSON_VALUE)) .andExpect(MockMvcResultMatchers.status().isNoContent) } + /** + * Ensure that in [RestrictionMode.HYBRID] mode, when we restrict the sub-queue that we can no longer + * get entries from that specific sub-queue, other sub-queues are still accessible without a token. + */ + @Test + fun testGetEntry_usingHybridMode_withRestrictedSubQueue() + { + Mockito.doReturn(RestrictionMode.HYBRID).`when`(authenticator).getRestrictionMode() + Assertions.assertEquals(RestrictionMode.HYBRID, authenticator.getRestrictionMode()) + + val entries = initialiseMapWithEntries() + + val subQueue1 = entries.second[0] + val message1 = entries.first[0] + val subQueue1Token = jwtTokenProvider.createTokenForSubQueue(subQueue1) + Assertions.assertTrue(subQueue1Token.isPresent) + Assertions.assertTrue(authenticator.addRestrictedEntry(subQueue1)) + Assertions.assertTrue(authenticator.isRestricted(subQueue1)) + + // Make sure we cannot access without a token + mockMvc.perform(get(MessageQueueController.MESSAGE_QUEUE_BASE_PATH + "/" + MessageQueueController.ENDPOINT_ENTRY + "/" + message1.uuid) + .contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(MockMvcResultMatchers.status().isForbidden) + + // Checking entry is retrieve with provided token + val mvcResult: MvcResult = mockMvc.perform(get(MessageQueueController.MESSAGE_QUEUE_BASE_PATH + "/" + MessageQueueController.ENDPOINT_ENTRY + "/" + message1.uuid) + .header(JwtAuthenticationFilter.AUTHORIZATION_HEADER, "${JwtAuthenticationFilter.BEARER_HEADER_VALUE}${subQueue1Token.get()}") + .contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(MockMvcResultMatchers.status().isOk) + .andReturn() + + val messageResponse = gson.fromJson(mvcResult.response.contentAsString, MessageResponse::class.java) + + val deserialisedPayload = gson.fromJson(gson.toJson(messageResponse.message.payload), Payload::class.java) + Assertions.assertEquals(message1.payload, deserialisedPayload) + Assertions.assertNull(messageResponse.message.assignedTo) + Assertions.assertEquals(message1.subQueue, messageResponse.message.subQueue) + Assertions.assertEquals(message1.subQueue, messageResponse.subQueue) + Assertions.assertNotNull(messageResponse.message.uuid) + + val subQueue2 = entries.second[1] + val message2 = entries.first[1] + Assertions.assertFalse(authenticator.isRestricted(subQueue2)) + + // Check un-restricted entry is still accessible without a token + val mvcResult2: MvcResult = mockMvc.perform(get(MessageQueueController.MESSAGE_QUEUE_BASE_PATH + "/" + MessageQueueController.ENDPOINT_ENTRY + "/" + message2.uuid) + .header(JwtAuthenticationFilter.AUTHORIZATION_HEADER, "${JwtAuthenticationFilter.BEARER_HEADER_VALUE}${subQueue1Token.get()}") + .contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(MockMvcResultMatchers.status().isOk) + .andReturn() + + val messageResponse2 = gson.fromJson(mvcResult2.response.contentAsString, MessageResponse::class.java) + + val deserialisedPayload2 = gson.fromJson(gson.toJson(messageResponse2.message.payload), Payload::class.java) + Assertions.assertEquals(message2.payload, deserialisedPayload2) + Assertions.assertNull(messageResponse2.message.assignedTo) + Assertions.assertEquals(message2.subQueue, messageResponse2.message.subQueue) + Assertions.assertEquals(message2.subQueue, messageResponse2.subQueue) + Assertions.assertNotNull(messageResponse2.message.uuid) + } + /** * Calling create with provided [QueueMessage.assignedTo] and [QueueMessage.uuid] to * ensure they are set correctly in the returned [MessageResponse]. @@ -168,7 +256,9 @@ class MessageQueueControllerTest @Test fun testCreateQueueEntry_withProvidedDefaults() { - val message = createQueueMessage(type = "testCreateQueueEntry_withProvidedDefaults", assignedTo = "user-1") + Assertions.assertEquals(RestrictionMode.NONE, authenticator.getRestrictionMode()) + + val message = createQueueMessage(subQueue = "testCreateQueueEntry_withProvidedDefaults", assignedTo = "user-1") val mvcResult: MvcResult = mockMvc.perform(post(MessageQueueController.MESSAGE_QUEUE_BASE_PATH + "/" + MessageQueueController.ENDPOINT_ENTRY) .contentType(MediaType.APPLICATION_JSON_VALUE) @@ -181,13 +271,13 @@ class MessageQueueControllerTest val deserialisedPayload = gson.fromJson(gson.toJson(messageResponse.message.payload), Payload::class.java) Assertions.assertEquals(message.payload, deserialisedPayload) Assertions.assertEquals(message.assignedTo, messageResponse.message.assignedTo) - Assertions.assertEquals(message.type, messageResponse.message.type) - Assertions.assertEquals(message.type, messageResponse.queueType) + Assertions.assertEquals(message.subQueue, messageResponse.message.subQueue) + Assertions.assertEquals(message.subQueue, messageResponse.subQueue) Assertions.assertEquals(message.uuid, messageResponse.message.uuid) - val createdMessage = multiQueue.peekForType(message.type).get() + val createdMessage = multiQueue.peekSubQueue(message.subQueue).get() Assertions.assertEquals(message.assignedTo, createdMessage.assignedTo) - Assertions.assertEquals(message.type, createdMessage.type) + Assertions.assertEquals(message.subQueue, createdMessage.subQueue) Assertions.assertEquals(message.uuid, createdMessage.uuid) } @@ -198,7 +288,9 @@ class MessageQueueControllerTest @Test fun testCreateQueueEntry_withOutDefaults() { - val message = createQueueMessage(type = "testCreateQueueEntry_withOutDefaults") + Assertions.assertEquals(RestrictionMode.NONE, authenticator.getRestrictionMode()) + + val message = createQueueMessage(subQueue = "testCreateQueueEntry_withOutDefaults") val mvcResult: MvcResult = mockMvc.perform(post(MessageQueueController.MESSAGE_QUEUE_BASE_PATH + "/" + MessageQueueController.ENDPOINT_ENTRY) .contentType(MediaType.APPLICATION_JSON_VALUE) @@ -211,23 +303,26 @@ class MessageQueueControllerTest val deserialisedPayload = gson.fromJson(gson.toJson(messageResponse.message.payload), Payload::class.java) Assertions.assertEquals(message.payload, deserialisedPayload) Assertions.assertNull(messageResponse.message.assignedTo) - Assertions.assertEquals(message.type, messageResponse.message.type) - Assertions.assertEquals(message.type, messageResponse.queueType) + Assertions.assertEquals(message.subQueue, messageResponse.message.subQueue) + Assertions.assertEquals(message.subQueue, messageResponse.subQueue) Assertions.assertNotNull(messageResponse.message.uuid) - val createdMessage = multiQueue.peekForType(message.type).get() + val createdMessage = multiQueue.peekSubQueue(message.subQueue).get() Assertions.assertNull(createdMessage.assignedTo) - Assertions.assertEquals(message.type, createdMessage.type) + Assertions.assertEquals(message.subQueue, createdMessage.subQueue) Assertions.assertEquals(message.uuid, createdMessage.uuid) } /** - * Test [MessageQueueController.createMessage] to ensure that [HttpStatus.CONFLICT] is returned if a message with the same [UUID] already exists in the queue. + * Test [MessageQueueController.createMessage] to ensure that [HttpStatus.CONFLICT] is returned if a message with + * the same [UUID] already exists in the queue. */ @Test fun testCreateEntry_Conflict() { - val message = createQueueMessage(type = "testCreateEntry_Conflict") + Assertions.assertEquals(RestrictionMode.NONE, authenticator.getRestrictionMode()) + + val message = createQueueMessage(subQueue = "testCreateEntry_Conflict") Assertions.assertTrue(multiQueue.add(message)) @@ -238,12 +333,15 @@ class MessageQueueControllerTest } /** - * Calling create with a blank [QueueMessage.assignedTo] to make sure that [QueueMessage.assignedTo] is provided as `null` in the response. + * Calling create with a blank [QueueMessage.assignedTo] to make sure that [QueueMessage.assignedTo] is provided as + * `null` in the response. */ @Test fun testCreateQueueEntry_withBlankAssignedTo() { - val message = createQueueMessage(type = "testCreateQueueEntry_withAssignedButNoAssignedTo") + Assertions.assertEquals(RestrictionMode.NONE, authenticator.getRestrictionMode()) + + val message = createQueueMessage(subQueue = "testCreateQueueEntry_withAssignedButNoAssignedTo") message.assignedTo = " " val mvcResult: MvcResult = mockMvc.perform(post(MessageQueueController.MESSAGE_QUEUE_BASE_PATH + "/" + MessageQueueController.ENDPOINT_ENTRY) @@ -257,11 +355,52 @@ class MessageQueueControllerTest } /** - * Test [MessageQueueController.getKeys] to ensure that all keys for existing entries are provided and exist within the [MultiQueue]. + * Ensure that in [RestrictionMode.HYBRID] mode, when we restrict the sub-queue that we can no longer + * create entries for that specific sub-queue, other sub-queues can still have messages created without a token. + */ + @Test + fun testCreateEntry_inHybridMode() + { + Mockito.doReturn(RestrictionMode.HYBRID).`when`(authenticator).getRestrictionMode() + Assertions.assertEquals(RestrictionMode.HYBRID, authenticator.getRestrictionMode()) + + val message = createQueueMessage(subQueue = "testCreateEntry_inHybridMode") + + Assertions.assertTrue(authenticator.addRestrictedEntry(message.subQueue)) + Assertions.assertTrue(authenticator.isRestricted(message.subQueue)) + + val token = jwtTokenProvider.createTokenForSubQueue(message.subQueue) + Assertions.assertTrue(token.isPresent) + + mockMvc.perform(post(MessageQueueController.MESSAGE_QUEUE_BASE_PATH + "/" + MessageQueueController.ENDPOINT_ENTRY) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .content(gson.toJson(message))) + .andExpect(MockMvcResultMatchers.status().isForbidden) + + mockMvc.perform(post(MessageQueueController.MESSAGE_QUEUE_BASE_PATH + "/" + MessageQueueController.ENDPOINT_ENTRY) + .header(JwtAuthenticationFilter.AUTHORIZATION_HEADER, "${JwtAuthenticationFilter.BEARER_HEADER_VALUE}${token.get()}") + .contentType(MediaType.APPLICATION_JSON_VALUE) + .content(gson.toJson(message))) + .andExpect(MockMvcResultMatchers.status().isCreated) + + val message2 = createQueueMessage(subQueue = "testCreateEntry_inHybridMode2") + Assertions.assertFalse(authenticator.isRestricted(message2.subQueue)) + + mockMvc.perform(post(MessageQueueController.MESSAGE_QUEUE_BASE_PATH + "/" + MessageQueueController.ENDPOINT_ENTRY) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .content(gson.toJson(message2))) + .andExpect(MockMvcResultMatchers.status().isCreated) + } + + /** + * Test [MessageQueueController.getKeys] to ensure that all keys for existing entries are provided and exist within + * the [MultiQueue]. */ @Test fun testGetKeys() { + Assertions.assertEquals(RestrictionMode.NONE, authenticator.getRestrictionMode()) + val entries = initialiseMapWithEntries() val mvcResult: MvcResult = mockMvc.perform(get(MessageQueueController.MESSAGE_QUEUE_BASE_PATH + "/" + MessageQueueController.ENDPOINT_KEYS) @@ -272,20 +411,23 @@ class MessageQueueControllerTest val keys = gson.fromJson(mvcResult.response.contentAsString, List::class.java) Assertions.assertFalse(keys.isNullOrEmpty()) Assertions.assertEquals(entries.second.size, keys.size) - entries.second.forEach { type -> Assertions.assertTrue(keys.contains(type)) } + entries.second.forEach { subQueue -> Assertions.assertTrue(keys.contains(subQueue)) } val mapKeys = multiQueue.keys(true) Assertions.assertFalse(mapKeys.isEmpty()) Assertions.assertEquals(entries.second.size, mapKeys.size) - entries.second.forEach { type -> Assertions.assertTrue(mapKeys.contains(type)) } + entries.second.forEach { subQueue -> Assertions.assertTrue(mapKeys.contains(subQueue)) } } /** - * Test [MessageQueueController.getKeys] to ensure that all keys are returned. Specifically when entries are added and [RestParameters.INCLUDE_EMPTY] is set to `false`. + * Test [MessageQueueController.getKeys] to ensure that all keys are returned. Specifically when entries are added + * and [RestParameters.INCLUDE_EMPTY] is set to `false`. */ @Test fun testGetKeys_excludeEmpty() { + Assertions.assertEquals(RestrictionMode.NONE, authenticator.getRestrictionMode()) + val entries = initialiseMapWithEntries() Assertions.assertTrue(multiQueue.remove(entries.first[0])) Assertions.assertTrue(multiQueue.remove(entries.first[1])) @@ -299,23 +441,27 @@ class MessageQueueControllerTest val keys = gson.fromJson(mvcResult.response.contentAsString, List::class.java) Assertions.assertFalse(keys.isNullOrEmpty()) Assertions.assertEquals(2, keys.size) - entries.second.subList(2, 3).forEach { type -> Assertions.assertTrue(keys.contains(type)) } + entries.second.subList(2, 3).forEach { subQueue -> Assertions.assertTrue(keys.contains(subQueue)) } val mapKeys = multiQueue.keys(false) Assertions.assertFalse(mapKeys.isEmpty()) Assertions.assertEquals(2, mapKeys.size) - entries.second.subList(2, 3).forEach { type -> Assertions.assertTrue(mapKeys.contains(type)) } + entries.second.subList(2, 3).forEach { subQueue -> Assertions.assertTrue(mapKeys.contains(subQueue)) } } /** - * Test [MessageQueueController.getAll] to ensure that all entries are returned from all `queueTypes` when no explicit `queueType` is provided. - * This also checks the returned object has a `non-null` value in the payload since the `detailed` flag is set to `true`. + * Test [MessageQueueController.getAll] to ensure that all entries are returned from all `sub-queues` when no + * explicit `sub-queue` is provided. + * This also checks the returned object has a `non-null` value in the payload since the `detailed` flag is set to + * `true`. */ @Test fun testGetAll() { + Assertions.assertEquals(RestrictionMode.NONE, authenticator.getRestrictionMode()) + val entries = initialiseMapWithEntries() - val type = entries.first[0].type + val subQueue = entries.first[0].subQueue val detailed = true val mvcResult: MvcResult = mockMvc.perform(get("${MessageQueueController.MESSAGE_QUEUE_BASE_PATH}/${MessageQueueController.ENDPOINT_ALL}?${RestParameters.DETAILED}=$detailed") @@ -327,29 +473,32 @@ class MessageQueueControllerTest val keys = gson.fromJson>>(mvcResult.response.contentAsString, mapType) Assertions.assertNotNull(keys) Assertions.assertEquals(entries.second.size, keys.keys.size) - entries.second.forEach { typeString -> Assertions.assertTrue(keys.keys.contains(typeString)) } + entries.second.forEach { subQueueId -> Assertions.assertTrue(keys.keys.contains(subQueueId)) } keys.values.forEach { detailList -> Assertions.assertEquals(1, detailList.size) } - Assertions.assertEquals(entries.first[0].removePayload(detailed).uuid, keys[type]!![0].uuid) + Assertions.assertEquals(entries.first[0].removePayload(detailed).uuid, keys[subQueue]!![0].uuid) // Since we passed in true for the detailed flag, ensure the payload is equal - val payloadObject = gson.fromJson(keys[type]!![0].payload.toString(), Payload::class.java) + val payloadObject = gson.fromJson(keys[subQueue]!![0].payload.toString(), Payload::class.java) Assertions.assertEquals(entries.first[0].payload, payloadObject) - Assertions.assertEquals(entries.first[0].removePayload(detailed).assignedTo, keys[type]!![0].assignedTo) - Assertions.assertEquals(entries.first[0].removePayload(detailed).type, keys[type]!![0].type) + Assertions.assertEquals(entries.first[0].removePayload(detailed).assignedTo, keys[subQueue]!![0].assignedTo) + Assertions.assertEquals(entries.first[0].removePayload(detailed).subQueue, keys[subQueue]!![0].subQueue) } /** - * Test [MessageQueueController.getAll] to ensure that all entries are returned from the `queueType` when an explicit `queueType` is provided. + * Test [MessageQueueController.getAll] to ensure that all entries are returned from the `sub-queue` when an + * explicit `sub-queue` is provided. * This also checks the returned object has `null` in the payload since the `detailed` flag is not provided. */ @Test - fun testGetAll_SpecificQueueType() + fun testGetAll_SpecificSubQueue() { + Assertions.assertEquals(RestrictionMode.NONE, authenticator.getRestrictionMode()) + val entries = initialiseMapWithEntries() - val type = entries.first[0].type + val subQueue = entries.first[0].subQueue val mvcResult: MvcResult = mockMvc.perform(get(MessageQueueController.MESSAGE_QUEUE_BASE_PATH + "/" + MessageQueueController.ENDPOINT_ALL) .contentType(MediaType.APPLICATION_JSON_VALUE) - .param(RestParameters.QUEUE_TYPE, type)) + .param(RestParameters.SUB_QUEUE, subQueue)) .andExpect(MockMvcResultMatchers.status().isOk) .andReturn() @@ -357,28 +506,126 @@ class MessageQueueControllerTest val keys = gson.fromJson>>(mvcResult.response.contentAsString, mapType) Assertions.assertNotNull(keys) Assertions.assertEquals(1, keys.keys.size) - Assertions.assertTrue(keys.keys.contains(type)) + Assertions.assertTrue(keys.keys.contains(subQueue)) keys.values.forEach { detailList -> Assertions.assertEquals(1, detailList.size) } - Assertions.assertEquals(entries.first[0].removePayload(false).uuid, keys[type]!![0].uuid) - // Since we did not pass a detailed flag value, ensure the payload is null + Assertions.assertEquals(entries.first[0].removePayload(false).uuid, keys[subQueue]!![0].uuid) + // Since we did not pass a detailed flag value, ensure the payload is a placeholder value Assertions.assertEquals("***", entries.first[0].removePayload(false).payload) - Assertions.assertEquals(entries.first[0].removePayload(false).assignedTo, keys[type]!![0].assignedTo) - Assertions.assertEquals(entries.first[0].removePayload(false).type, keys[type]!![0].type) + Assertions.assertEquals(entries.first[0].removePayload(false).assignedTo, keys[subQueue]!![0].assignedTo) + Assertions.assertEquals(entries.first[0].removePayload(false).subQueue, keys[subQueue]!![0].subQueue) + } + + /** + * Ensure then when in [RestrictionMode.HYBRID] and [MessageQueueController.getAll] is called + * that restricted queues are not included until a valid token is provided. + */ + @Test + fun testGetAll_inHybridMode() + { + Mockito.doReturn(RestrictionMode.HYBRID).`when`(authenticator).getRestrictionMode() + Assertions.assertEquals(RestrictionMode.HYBRID, authenticator.getRestrictionMode()) + + val subQueue = "testGetAll_inHybridMode" + val messages = listOf(createQueueMessage(subQueue), createQueueMessage(subQueue)) + messages.forEach { message -> Assertions.assertTrue(multiQueue.add(message)) } + + Assertions.assertTrue(authenticator.addRestrictedEntry(subQueue)) + Assertions.assertTrue(authenticator.isRestricted(subQueue)) + + val token = jwtTokenProvider.createTokenForSubQueue(subQueue) + Assertions.assertTrue(token.isPresent) + + val entries = initialiseMapWithEntries() + entries.second.forEach { subQueueId -> Assertions.assertFalse(authenticator.isRestricted(subQueueId)) } + val detailed = true + + // Ensure the message in the restricted sub-queue are not returned + var mvcResult: MvcResult = mockMvc.perform(get("${MessageQueueController.MESSAGE_QUEUE_BASE_PATH}/${MessageQueueController.ENDPOINT_ALL}?${RestParameters.DETAILED}=$detailed") + .contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(MockMvcResultMatchers.status().isOk) + .andReturn() + + val mapType = object : TypeToken>>() {}.type + var keys = gson.fromJson>>(mvcResult.response.contentAsString, mapType) + Assertions.assertNotNull(keys) + Assertions.assertEquals(entries.second.size, keys.keys.size) + + Assertions.assertFalse(keys.keys.contains(subQueue)) + Assertions.assertNull(keys[subQueue]) + + // After providing the token we should see the messages for the restricted queue + mvcResult = mockMvc.perform(get("${MessageQueueController.MESSAGE_QUEUE_BASE_PATH}/${MessageQueueController.ENDPOINT_ALL}?${RestParameters.DETAILED}=$detailed") + .header(JwtAuthenticationFilter.AUTHORIZATION_HEADER, "${JwtAuthenticationFilter.BEARER_HEADER_VALUE}${token.get()}") + .contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(MockMvcResultMatchers.status().isOk) + .andReturn() + + keys = gson.fromJson(mvcResult.response.contentAsString, mapType) + Assertions.assertNotNull(keys) + Assertions.assertEquals(entries.second.size + 1, keys.keys.size) + + Assertions.assertTrue(keys.keys.contains(subQueue)) + Assertions.assertNotNull(keys[subQueue]) + } + + /** + * Ensure then when in [RestrictionMode.HYBRID] and [MessageQueueController.getAll] is called + * and all messages are requested for a restricted queue are not accessible unless a token is provided. + */ + @Test + fun testGetAll_SpecificSubQueue_inHybridMode() + { + Mockito.doReturn(RestrictionMode.HYBRID).`when`(authenticator).getRestrictionMode() + Assertions.assertEquals(RestrictionMode.HYBRID, authenticator.getRestrictionMode()) + + val subQueue = "testGetAll_SpecificSubQueue_inHybridMode" + val messages = listOf(createQueueMessage(subQueue), createQueueMessage(subQueue)) + messages.forEach { message -> Assertions.assertTrue(multiQueue.add(message)) } + + Assertions.assertTrue(authenticator.addRestrictedEntry(subQueue)) + Assertions.assertTrue(authenticator.isRestricted(subQueue)) + + mockMvc.perform(get(MessageQueueController.MESSAGE_QUEUE_BASE_PATH + "/" + MessageQueueController.ENDPOINT_ALL) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .param(RestParameters.SUB_QUEUE, subQueue)) + .andExpect(MockMvcResultMatchers.status().isForbidden) + + val token = jwtTokenProvider.createTokenForSubQueue(subQueue) + Assertions.assertTrue(token.isPresent) + + val mvcResult: MvcResult = mockMvc.perform(get(MessageQueueController.MESSAGE_QUEUE_BASE_PATH + "/" + MessageQueueController.ENDPOINT_ALL) + .header(JwtAuthenticationFilter.AUTHORIZATION_HEADER, "${JwtAuthenticationFilter.BEARER_HEADER_VALUE}${token.get()}") + .contentType(MediaType.APPLICATION_JSON_VALUE) + .param(RestParameters.SUB_QUEUE, subQueue)) + .andExpect(MockMvcResultMatchers.status().isOk) + .andReturn() + + val mapType = object : TypeToken>>() {}.type + val keys = gson.fromJson>>(mvcResult.response.contentAsString, mapType) + Assertions.assertNotNull(keys) + Assertions.assertEquals(1, keys.keys.size) + + Assertions.assertTrue(keys.keys.contains(subQueue)) + Assertions.assertNotNull(keys[subQueue]) + Assertions.assertEquals(2, keys[subQueue]!!.size) } /** - * Test [MessageQueueController.getOwned] to ensure that no entries are returned when no [QueueMessage] are assigned by the provided `assignedTo` parameter. + * Test [MessageQueueController.getOwned] to ensure that no entries are returned when no [QueueMessage] are + * assigned by the provided `assignedTo` parameter. */ @Test fun testGetOwned_NoneOwned() { + Assertions.assertEquals(RestrictionMode.NONE, authenticator.getRestrictionMode()) + val entries = initialiseMapWithEntries() val assignedTo = "my-assigned-to-identifier" val mvcResult: MvcResult = mockMvc.perform(get(MessageQueueController.MESSAGE_QUEUE_BASE_PATH + "/" + MessageQueueController.ENDPOINT_OWNED) .contentType(MediaType.APPLICATION_JSON_VALUE) .param(RestParameters.ASSIGNED_TO, assignedTo) - .param(RestParameters.QUEUE_TYPE, entries.first[0].type)) + .param(RestParameters.SUB_QUEUE, entries.first[0].subQueue)) .andExpect(MockMvcResultMatchers.status().isOk) .andReturn() @@ -388,23 +635,75 @@ class MessageQueueControllerTest } /** - * Test [MessageQueueController.getOwned] to ensure that all appropriate [QueueMessage] entries are returned when the provided `assignedTo` parameter owns the existing [QueueMessage]. + * Test [MessageQueueController.getOwned] to ensure that all appropriate [QueueMessage] entries are returned when + * the provided `assignedTo` parameter owns the existing [QueueMessage]. */ @Test fun testGetOwned_SomeOwned() { + Assertions.assertEquals(RestrictionMode.NONE, authenticator.getRestrictionMode()) + + val assignedTo = "my-assigned-to-identifier" + val subQueue = "testGetOwned_SomeOwned" + val message1 = createQueueMessage(assignedTo = assignedTo, subQueue = subQueue) + val message2 = createQueueMessage(assignedTo = assignedTo, subQueue = subQueue) + + Assertions.assertTrue(multiQueue.add(message1)) + Assertions.assertTrue(multiQueue.add(message2)) + + val mvcResult: MvcResult = mockMvc.perform(get(MessageQueueController.MESSAGE_QUEUE_BASE_PATH + "/" + MessageQueueController.ENDPOINT_OWNED) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .param(RestParameters.ASSIGNED_TO, assignedTo) + .param(RestParameters.SUB_QUEUE, subQueue)) + .andExpect(MockMvcResultMatchers.status().isOk) + .andReturn() + + val listType = object : TypeToken>() {}.type + val owned = gson.fromJson>(mvcResult.response.contentAsString, listType) + Assertions.assertTrue(owned.isNotEmpty()) + owned.forEach { message -> + Assertions.assertTrue(message.message.uuid == message1.uuid || message.message.uuid == message2.uuid) + Assertions.assertEquals(subQueue, message.subQueue) + Assertions.assertEquals(subQueue, message.message.subQueue) + Assertions.assertEquals(assignedTo, message.message.assignedTo) + } + } + + /** + * Ensure when [MessageQueueController.getOwned] is called on a restricted sub-queue that no entries are returned + * unless a valid token is provided. + */ + @Test + fun testGetOwned_SomeOwned_inHybridMode() + { + Mockito.doReturn(RestrictionMode.HYBRID).`when`(authenticator).getRestrictionMode() + Assertions.assertEquals(RestrictionMode.HYBRID, authenticator.getRestrictionMode()) + val assignedTo = "my-assigned-to-identifier" - val type = "testGetOwned_SomeOwned" - val message1 = createQueueMessage(assignedTo = assignedTo, type = type) - val message2 = createQueueMessage(assignedTo = assignedTo, type = type) + val subQueue = "testGetOwned_SomeOwned_inHybridMode" + val message1 = createQueueMessage(assignedTo = assignedTo, subQueue = subQueue) + val message2 = createQueueMessage(assignedTo = assignedTo, subQueue = subQueue) Assertions.assertTrue(multiQueue.add(message1)) Assertions.assertTrue(multiQueue.add(message2)) + Assertions.assertTrue(authenticator.addRestrictedEntry(subQueue)) + Assertions.assertTrue(authenticator.isRestricted(subQueue)) + + mockMvc.perform(get(MessageQueueController.MESSAGE_QUEUE_BASE_PATH + "/" + MessageQueueController.ENDPOINT_OWNED) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .param(RestParameters.ASSIGNED_TO, assignedTo) + .param(RestParameters.SUB_QUEUE, subQueue)) + .andExpect(MockMvcResultMatchers.status().isForbidden) + + val token = jwtTokenProvider.createTokenForSubQueue(subQueue) + Assertions.assertTrue(token.isPresent) + val mvcResult: MvcResult = mockMvc.perform(get(MessageQueueController.MESSAGE_QUEUE_BASE_PATH + "/" + MessageQueueController.ENDPOINT_OWNED) + .header(JwtAuthenticationFilter.AUTHORIZATION_HEADER, "${JwtAuthenticationFilter.BEARER_HEADER_VALUE}${token.get()}") .contentType(MediaType.APPLICATION_JSON_VALUE) .param(RestParameters.ASSIGNED_TO, assignedTo) - .param(RestParameters.QUEUE_TYPE, type)) + .param(RestParameters.SUB_QUEUE, subQueue)) .andExpect(MockMvcResultMatchers.status().isOk) .andReturn() @@ -413,38 +712,86 @@ class MessageQueueControllerTest Assertions.assertTrue(owned.isNotEmpty()) owned.forEach { message -> Assertions.assertTrue(message.message.uuid == message1.uuid || message.message.uuid == message2.uuid) - Assertions.assertEquals(type, message.queueType) - Assertions.assertEquals(type, message.message.type) + Assertions.assertEquals(subQueue, message.subQueue) + Assertions.assertEquals(subQueue, message.message.subQueue) Assertions.assertEquals(assignedTo, message.message.assignedTo) } } /** - * Test [MessageQueueController.assignMessage] to ensure that [HttpStatus.NO_CONTENT] is returned when a [QueueMessage] with the provided [UUID] does not exist. + * Test [MessageQueueController.assignMessage] to ensure that [HttpStatus.NO_CONTENT] is returned when a + * [QueueMessage] with the provided [UUID] does not exist. */ @Test fun testAssignMessage_doesNotExist() { + Assertions.assertEquals(RestrictionMode.NONE, authenticator.getRestrictionMode()) + val uuid = UUID.randomUUID().toString() val assignedTo = "assigned" - mockMvc.perform(put(MessageQueueController.MESSAGE_QUEUE_BASE_PATH + "/" + MessageQueueController.ENDPOINT_ASSIGN + "/" + uuid) + mockMvc.perform(put(MessageQueueController.MESSAGE_QUEUE_BASE_PATH + MessageQueueController.ENDPOINT_ENTRY + "/" + uuid + MessageQueueController.ENDPOINT_ASSIGN) .contentType(MediaType.APPLICATION_JSON_VALUE) .param(RestParameters.ASSIGNED_TO, assignedTo)) .andExpect(MockMvcResultMatchers.status().isNoContent) } /** - * Test [MessageQueueController.assignMessage] to ensure that the message is assigned correctly and [HttpStatus.OK] is returned when the [QueueMessage] was initially not assigned. + * Test [MessageQueueController.assignMessage] to ensure that the message is assigned correctly and [HttpStatus.OK] + * is returned when the [QueueMessage] was initially not assigned. */ @Test fun testAssignMessage_messageIsAssigned() { + Assertions.assertEquals(RestrictionMode.NONE, authenticator.getRestrictionMode()) + + val assignedTo = "assigned" + val message = createQueueMessage(subQueue = "testAssignMessage_messageIsAssigned") + Assertions.assertNull(message.assignedTo) + Assertions.assertTrue(multiQueue.add(message)) + + val mvcResult: MvcResult = mockMvc.perform(put(MessageQueueController.MESSAGE_QUEUE_BASE_PATH + MessageQueueController.ENDPOINT_ENTRY + "/" + message.uuid + MessageQueueController.ENDPOINT_ASSIGN) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .param(RestParameters.ASSIGNED_TO, assignedTo)) + .andExpect(MockMvcResultMatchers.status().isOk) + .andReturn() + + val messageResponse = gson.fromJson(mvcResult.response.contentAsString, MessageResponse::class.java) + Assertions.assertEquals(assignedTo, messageResponse.message.assignedTo) + Assertions.assertEquals(message.uuid, messageResponse.message.uuid) + + val assignedMessage = multiQueue.peekSubQueue(message.subQueue).get() + Assertions.assertEquals(assignedTo, assignedMessage.assignedTo) + Assertions.assertEquals(message.uuid, assignedMessage.uuid) + } + + /** + * Ensure when [MessageQueueController.assignMessage] is called in [RestrictionMode.HYBRID] mode + * that the message is not assigned or changed if the sub-queue is restricted and a valid token is not provided. + */ + @Test + fun testAssignMessage_messageIsAssigned_inHybridMode() + { + Mockito.doReturn(RestrictionMode.HYBRID).`when`(authenticator).getRestrictionMode() + Assertions.assertEquals(RestrictionMode.HYBRID, authenticator.getRestrictionMode()) + val assignedTo = "assigned" - val message = createQueueMessage(type = "testAssignMessage_messageIsAssigned") + val message = createQueueMessage(subQueue = "testAssignMessage_messageIsAssigned_inHybridMode") Assertions.assertNull(message.assignedTo) Assertions.assertTrue(multiQueue.add(message)) - val mvcResult: MvcResult = mockMvc.perform(put(MessageQueueController.MESSAGE_QUEUE_BASE_PATH + "/" + MessageQueueController.ENDPOINT_ASSIGN + "/" + message.uuid) + Assertions.assertTrue(authenticator.addRestrictedEntry(message.subQueue)) + Assertions.assertTrue(authenticator.isRestricted(message.subQueue)) + + mockMvc.perform(put(MessageQueueController.MESSAGE_QUEUE_BASE_PATH + MessageQueueController.ENDPOINT_ENTRY + "/" + message.uuid + MessageQueueController.ENDPOINT_ASSIGN) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .param(RestParameters.ASSIGNED_TO, assignedTo)) + .andExpect(MockMvcResultMatchers.status().isForbidden) + + val token = jwtTokenProvider.createTokenForSubQueue(message.subQueue) + Assertions.assertTrue(token.isPresent) + + val mvcResult: MvcResult = mockMvc.perform(put(MessageQueueController.MESSAGE_QUEUE_BASE_PATH + MessageQueueController.ENDPOINT_ENTRY + "/" + message.uuid + MessageQueueController.ENDPOINT_ASSIGN) + .header(JwtAuthenticationFilter.AUTHORIZATION_HEADER, "${JwtAuthenticationFilter.BEARER_HEADER_VALUE}${token.get()}") .contentType(MediaType.APPLICATION_JSON_VALUE) .param(RestParameters.ASSIGNED_TO, assignedTo)) .andExpect(MockMvcResultMatchers.status().isOk) @@ -454,24 +801,28 @@ class MessageQueueControllerTest Assertions.assertEquals(assignedTo, messageResponse.message.assignedTo) Assertions.assertEquals(message.uuid, messageResponse.message.uuid) - val assignedMessage = multiQueue.peekForType(message.type).get() + val assignedMessage = multiQueue.peekSubQueue(message.subQueue).get() Assertions.assertEquals(assignedTo, assignedMessage.assignedTo) Assertions.assertEquals(message.uuid, assignedMessage.uuid) } /** - * Test [MessageQueueController.assignMessage] to ensure that the message is assigned correctly and [HttpStatus.ACCEPTED] is returned when the [QueueMessage] is already assigned by the provided `assignTo` identifier. + * Test [MessageQueueController.assignMessage] to ensure that the message is assigned correctly and + * [HttpStatus.ACCEPTED] is returned when the [QueueMessage] is already assigned by the provided `assignTo` + * identifier. */ @Test fun testAssignMessage_alreadyAssignedToSameID() { + Assertions.assertEquals(RestrictionMode.NONE, authenticator.getRestrictionMode()) + val assignedTo = "assigned" - val message = createQueueMessage(type = "testAssignMessage_alreadyAssignedToSameID", assignedTo = assignedTo) + val message = createQueueMessage(subQueue = "testAssignMessage_alreadyAssignedToSameID", assignedTo = assignedTo) Assertions.assertEquals(assignedTo, message.assignedTo) Assertions.assertTrue(multiQueue.add(message)) - val mvcResult: MvcResult = mockMvc.perform(put(MessageQueueController.MESSAGE_QUEUE_BASE_PATH + "/" + MessageQueueController.ENDPOINT_ASSIGN + "/" + message.uuid) + val mvcResult: MvcResult = mockMvc.perform(put(MessageQueueController.MESSAGE_QUEUE_BASE_PATH + MessageQueueController.ENDPOINT_ENTRY + "/" + message.uuid + MessageQueueController.ENDPOINT_ASSIGN) .contentType(MediaType.APPLICATION_JSON_VALUE) .param(RestParameters.ASSIGNED_TO, assignedTo)) .andExpect(MockMvcResultMatchers.status().isAccepted) @@ -481,101 +832,164 @@ class MessageQueueControllerTest Assertions.assertEquals(message.assignedTo, messageResponse.message.assignedTo) Assertions.assertEquals(message.uuid, messageResponse.message.uuid) - val assignedMessage = multiQueue.peekForType(message.type).get() + val assignedMessage = multiQueue.peekSubQueue(message.subQueue).get() Assertions.assertEquals(assignedTo, assignedMessage.assignedTo) Assertions.assertEquals(message.uuid, assignedMessage.uuid) } /** - * Test [MessageQueueController.assignMessage] to ensure that [HttpStatus.CONFLICT] is returned when the [QueueMessage] is already assigned to another identifier. + * Test [MessageQueueController.assignMessage] to ensure that [HttpStatus.CONFLICT] is returned when the + * [QueueMessage] is already assigned to another identifier. */ @Test fun testAssignMessage_alreadyAssignedToOtherID() { + Assertions.assertEquals(RestrictionMode.NONE, authenticator.getRestrictionMode()) + val assignedTo = "assignee" - val message = createQueueMessage(type = "testAssignMessage_alreadyAssignedToOtherID", assignedTo = assignedTo) + val message = createQueueMessage(subQueue = "testAssignMessage_alreadyAssignedToOtherID", assignedTo = assignedTo) Assertions.assertEquals(assignedTo, message.assignedTo) Assertions.assertTrue(multiQueue.add(message)) // Check the message is set correctly - var assignedMessage = multiQueue.peekForType(message.type).get() + var assignedMessage = multiQueue.peekSubQueue(message.subQueue).get() Assertions.assertEquals(assignedTo, assignedMessage.assignedTo) Assertions.assertEquals(message.uuid, assignedMessage.uuid) val wrongAssignee = "wrong-assignee" - mockMvc.perform(put(MessageQueueController.MESSAGE_QUEUE_BASE_PATH + "/" + MessageQueueController.ENDPOINT_ASSIGN + "/" + message.uuid) + mockMvc.perform(put(MessageQueueController.MESSAGE_QUEUE_BASE_PATH + MessageQueueController.ENDPOINT_ENTRY + "/" + message.uuid + MessageQueueController.ENDPOINT_ASSIGN) .contentType(MediaType.APPLICATION_JSON_VALUE) .param(RestParameters.ASSIGNED_TO, wrongAssignee)) .andExpect(MockMvcResultMatchers.status().isConflict) // Check the message is still assigned to the correct ID - assignedMessage = multiQueue.peekForType(message.type).get() + assignedMessage = multiQueue.peekSubQueue(message.subQueue).get() Assertions.assertEquals(assignedTo, assignedMessage.assignedTo) Assertions.assertEquals(message.uuid, assignedMessage.uuid) } /** - * Test [MessageQueueController.getNext] to ensure [HttpStatus.NO_CONTENT] is returned when there are no [QueueMessage]s available for the provided `queueType`. + * Test [MessageQueueController.getNext] to ensure [HttpStatus.NO_CONTENT] is returned when there are no + * [QueueMessage]s available for the provided `sub-queue`. */ @Test fun testGetNext_noNewMessages() { + Assertions.assertEquals(RestrictionMode.NONE, authenticator.getRestrictionMode()) + val assignedTo = "assignee" - val type = "testGetNext_noNewMessages" + val subQueue = "testGetNext_noNewMessages" mockMvc.perform(put(MessageQueueController.MESSAGE_QUEUE_BASE_PATH + "/" + MessageQueueController.ENDPOINT_NEXT) .contentType(MediaType.APPLICATION_JSON_VALUE) - .param(RestParameters.QUEUE_TYPE, type) + .param(RestParameters.SUB_QUEUE, subQueue) .param(RestParameters.ASSIGNED_TO, assignedTo)) .andExpect(MockMvcResultMatchers.status().isNoContent) - Assertions.assertTrue(multiQueue.getQueueForType(type).isEmpty()) + Assertions.assertTrue(multiQueue.getSubQueue(subQueue).isEmpty()) } /** - * Test [MessageQueueController.getNext] to ensure that [HttpStatus.NO_CONTENT] is returned if there are no `assigned` [QueueMessage]s available. + * Test [MessageQueueController.getNext] to ensure that [HttpStatus.NO_CONTENT] is returned if there are no + * `assigned` [QueueMessage]s available. */ @Test fun testGetNext_noNewUnAssignedMessages() { + Assertions.assertEquals(RestrictionMode.NONE, authenticator.getRestrictionMode()) + val assignedTo = "assignee" - val type = "testGetNext_noNewUnAssignedMessages" - val message = createQueueMessage(type = type, assignedTo = assignedTo) - val message2 = createQueueMessage(type = type, assignedTo = assignedTo) + val subQueue = "testGetNext_noNewUnAssignedMessages" + val message = createQueueMessage(subQueue = subQueue, assignedTo = assignedTo) + val message2 = createQueueMessage(subQueue = subQueue, assignedTo = assignedTo) Assertions.assertTrue(multiQueue.add(message)) Assertions.assertTrue(multiQueue.add(message2)) - Assertions.assertFalse(multiQueue.getQueueForType(type).isEmpty()) + Assertions.assertFalse(multiQueue.getSubQueue(subQueue).isEmpty()) mockMvc.perform(put(MessageQueueController.MESSAGE_QUEUE_BASE_PATH + "/" + MessageQueueController.ENDPOINT_NEXT) .contentType(MediaType.APPLICATION_JSON_VALUE) - .param(RestParameters.QUEUE_TYPE, type) + .param(RestParameters.SUB_QUEUE, subQueue) .param(RestParameters.ASSIGNED_TO, assignedTo)) .andExpect(MockMvcResultMatchers.status().isNoContent) } /** - * Test [MessageQueueController.getNext] to ensure that the correct next message is returned if it exists in the [MultiQueue]. + * Test [MessageQueueController.getNext] to ensure that the correct next message is returned if it exists in + * the [MultiQueue]. */ @Test fun testGetNext() { + Assertions.assertEquals(RestrictionMode.NONE, authenticator.getRestrictionMode()) + + val assignedTo = "assignee" + val subQueue = "testGetNext" + val message = createQueueMessage(subQueue = subQueue, assignedTo = assignedTo) + val message2 = createQueueMessage(subQueue = subQueue) + + Assertions.assertTrue(multiQueue.add(message)) + Assertions.assertTrue(multiQueue.add(message2)) + + val storedMessage2 = multiQueue.getSubQueue(subQueue).stream().filter{ m -> m.uuid == message2.uuid }.findFirst().get() + Assertions.assertNull(storedMessage2.assignedTo) + Assertions.assertEquals(message2.uuid, storedMessage2.uuid) + + val mvcResult: MvcResult = mockMvc.perform(put(MessageQueueController.MESSAGE_QUEUE_BASE_PATH + "/" + MessageQueueController.ENDPOINT_NEXT) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .param(RestParameters.SUB_QUEUE, subQueue) + .param(RestParameters.ASSIGNED_TO, assignedTo)) + .andExpect(MockMvcResultMatchers.status().isOk) + .andReturn() + + val messageResponse = gson.fromJson(mvcResult.response.contentAsString, MessageResponse::class.java) + Assertions.assertEquals(assignedTo, messageResponse.message.assignedTo) + Assertions.assertEquals(message2.uuid, messageResponse.message.uuid) + + val assignedMessage2 = multiQueue.getSubQueue(subQueue).stream().filter{ m -> m.uuid == message2.uuid }.findFirst().get() + Assertions.assertEquals(assignedTo, assignedMessage2.assignedTo) + Assertions.assertEquals(message2.uuid, assignedMessage2.uuid) + } + + /** + * Ensure that when in [RestrictionMode.HYBRID] mode that the next message for a restricted sub-queue + * cannot be retrieved without a valid token being provided. + */ + @Test + fun testGetNext_inHybridMode() + { + Mockito.doReturn(RestrictionMode.HYBRID).`when`(authenticator).getRestrictionMode() + Assertions.assertEquals(RestrictionMode.HYBRID, authenticator.getRestrictionMode()) + val assignedTo = "assignee" - val type = "testGetNext" - val message = createQueueMessage(type = type, assignedTo = assignedTo) - val message2 = createQueueMessage(type = type) + val subQueue = "testGetNext_inHybridMode" + val message = createQueueMessage(subQueue = subQueue, assignedTo = assignedTo) + val message2 = createQueueMessage(subQueue = subQueue) Assertions.assertTrue(multiQueue.add(message)) Assertions.assertTrue(multiQueue.add(message2)) - val storedMessage2 = multiQueue.getQueueForType(type).stream().filter{ m -> m.uuid == message2.uuid }.findFirst().get() + Assertions.assertTrue(authenticator.addRestrictedEntry(subQueue)) + Assertions.assertTrue(authenticator.isRestricted(subQueue)) + + val storedMessage2 = multiQueue.getSubQueue(subQueue).stream().filter{ m -> m.uuid == message2.uuid }.findFirst().get() Assertions.assertNull(storedMessage2.assignedTo) Assertions.assertEquals(message2.uuid, storedMessage2.uuid) + mockMvc.perform(put(MessageQueueController.MESSAGE_QUEUE_BASE_PATH + "/" + MessageQueueController.ENDPOINT_NEXT) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .param(RestParameters.SUB_QUEUE, subQueue) + .param(RestParameters.ASSIGNED_TO, assignedTo)) + .andExpect(MockMvcResultMatchers.status().isForbidden) + + val token = jwtTokenProvider.createTokenForSubQueue(subQueue) + Assertions.assertTrue(token.isPresent) + val mvcResult: MvcResult = mockMvc.perform(put(MessageQueueController.MESSAGE_QUEUE_BASE_PATH + "/" + MessageQueueController.ENDPOINT_NEXT) + .header(JwtAuthenticationFilter.AUTHORIZATION_HEADER, "${JwtAuthenticationFilter.BEARER_HEADER_VALUE}${token.get()}") .contentType(MediaType.APPLICATION_JSON_VALUE) - .param(RestParameters.QUEUE_TYPE, type) + .param(RestParameters.SUB_QUEUE, subQueue) .param(RestParameters.ASSIGNED_TO, assignedTo)) .andExpect(MockMvcResultMatchers.status().isOk) .andReturn() @@ -584,36 +998,86 @@ class MessageQueueControllerTest Assertions.assertEquals(assignedTo, messageResponse.message.assignedTo) Assertions.assertEquals(message2.uuid, messageResponse.message.uuid) - val assignedMessage2 = multiQueue.getQueueForType(type).stream().filter{ m -> m.uuid == message2.uuid }.findFirst().get() + val assignedMessage2 = multiQueue.getSubQueue(subQueue).stream().filter{ m -> m.uuid == message2.uuid }.findFirst().get() Assertions.assertEquals(assignedTo, assignedMessage2.assignedTo) Assertions.assertEquals(message2.uuid, assignedMessage2.uuid) } /** - * Test [MessageQueueController.releaseMessage] to ensure that [HttpStatus.NO_CONTENT] is returned when a [QueueMessage] with the provided [UUID] does not exist. + * Test [MessageQueueController.releaseMessage] to ensure that [HttpStatus.NO_CONTENT] is returned when a + * [QueueMessage] with the provided [UUID] does not exist. */ @Test fun testReleaseMessage_doesNotExist() { + Assertions.assertEquals(RestrictionMode.NONE, authenticator.getRestrictionMode()) + val uuid = UUID.randomUUID().toString() - mockMvc.perform(put(MessageQueueController.MESSAGE_QUEUE_BASE_PATH + "/" + MessageQueueController.ENDPOINT_RELEASE + "/" + uuid) + mockMvc.perform(put(MessageQueueController.MESSAGE_QUEUE_BASE_PATH + MessageQueueController.ENDPOINT_ENTRY + "/" + uuid + MessageQueueController.ENDPOINT_RELEASE) .contentType(MediaType.APPLICATION_JSON_VALUE)) .andExpect(MockMvcResultMatchers.status().isNoContent) } /** - * Test [MessageQueueController.releaseMessage] to ensure that the [QueueMessage] is released if it is currently assigned. + * Test [MessageQueueController.releaseMessage] to ensure that the [QueueMessage] is released if it is currently + * assigned. */ @Test fun testReleaseMessage_messageIsReleased() { + Assertions.assertEquals(RestrictionMode.NONE, authenticator.getRestrictionMode()) + + val assignedTo = "assignee" + val message = createQueueMessage(subQueue = "testReleaseMessage_messageIsReleased", assignedTo = assignedTo) + + Assertions.assertEquals(assignedTo, message.assignedTo) + Assertions.assertTrue(multiQueue.add(message)) + + val mvcResult: MvcResult = mockMvc.perform(put(MessageQueueController.MESSAGE_QUEUE_BASE_PATH + MessageQueueController.ENDPOINT_ENTRY + "/" + message.uuid + MessageQueueController.ENDPOINT_RELEASE) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .param(RestParameters.ASSIGNED_TO, assignedTo)) + .andExpect(MockMvcResultMatchers.status().isOk) + .andReturn() + + val messageResponse = gson.fromJson(mvcResult.response.contentAsString, MessageResponse::class.java) + Assertions.assertNull(messageResponse.message.assignedTo) + Assertions.assertEquals(message.uuid, messageResponse.message.uuid) + + val updatedMessage = multiQueue.peekSubQueue(message.subQueue).get() + Assertions.assertNull(updatedMessage.assignedTo) + Assertions.assertEquals(message.uuid, updatedMessage.uuid) + } + + /** + * Ensure that when in [RestrictionMode.HYBRID] mode that a restricted sub-queue cannot have its + * message released without a valid token being provided. + */ + @Test + fun testReleaseMessage_messageIsReleased_inHybridMode() + { + Mockito.doReturn(RestrictionMode.HYBRID).`when`(authenticator).getRestrictionMode() + Assertions.assertEquals(RestrictionMode.HYBRID, authenticator.getRestrictionMode()) + val assignedTo = "assignee" - val message = createQueueMessage(type = "testReleaseMessage_messageIsReleased", assignedTo = assignedTo) + val message = createQueueMessage(subQueue = "testReleaseMessage_messageIsReleased_inHybridMode", assignedTo = assignedTo) Assertions.assertEquals(assignedTo, message.assignedTo) Assertions.assertTrue(multiQueue.add(message)) - val mvcResult: MvcResult = mockMvc.perform(put(MessageQueueController.MESSAGE_QUEUE_BASE_PATH + "/" + MessageQueueController.ENDPOINT_RELEASE + "/" + message.uuid) + Assertions.assertTrue(authenticator.addRestrictedEntry(message.subQueue)) + Assertions.assertTrue(authenticator.isRestricted(message.subQueue)) + + mockMvc.perform(put(MessageQueueController.MESSAGE_QUEUE_BASE_PATH + MessageQueueController.ENDPOINT_ENTRY + "/" + message.uuid + MessageQueueController.ENDPOINT_RELEASE) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .param(RestParameters.ASSIGNED_TO, assignedTo)) + .andExpect(MockMvcResultMatchers.status().isForbidden) + + + val token = jwtTokenProvider.createTokenForSubQueue(message.subQueue) + Assertions.assertTrue(token.isPresent) + + val mvcResult: MvcResult = mockMvc.perform(put(MessageQueueController.MESSAGE_QUEUE_BASE_PATH + MessageQueueController.ENDPOINT_ENTRY + "/" + message.uuid + MessageQueueController.ENDPOINT_RELEASE) + .header(JwtAuthenticationFilter.AUTHORIZATION_HEADER, "${JwtAuthenticationFilter.BEARER_HEADER_VALUE}${token.get()}") .contentType(MediaType.APPLICATION_JSON_VALUE) .param(RestParameters.ASSIGNED_TO, assignedTo)) .andExpect(MockMvcResultMatchers.status().isOk) @@ -623,24 +1087,27 @@ class MessageQueueControllerTest Assertions.assertNull(messageResponse.message.assignedTo) Assertions.assertEquals(message.uuid, messageResponse.message.uuid) - val updatedMessage = multiQueue.peekForType(message.type).get() + val updatedMessage = multiQueue.peekSubQueue(message.subQueue).get() Assertions.assertNull(updatedMessage.assignedTo) Assertions.assertEquals(message.uuid, updatedMessage.uuid) } /** - * Test [MessageQueueController.releaseMessage] to ensure that the [QueueMessage] is released if it is currently assigned. Even when the `assignedTo` is not provided. + * Test [MessageQueueController.releaseMessage] to ensure that the [QueueMessage] is released if it is currently + * assigned. Even when the `assignedTo` is not provided. */ @Test fun testReleaseMessage_messageIsReleased_withoutAssignedToInQuery() { + Assertions.assertEquals(RestrictionMode.NONE, authenticator.getRestrictionMode()) + val assignedTo = "assigned" - val message = createQueueMessage(type = "testReleaseMessage_messageIsReleased_withoutAssignedToInQuery", assignedTo = assignedTo) + val message = createQueueMessage(subQueue = "testReleaseMessage_messageIsReleased_withoutAssignedToInQuery", assignedTo = assignedTo) Assertions.assertEquals(assignedTo, message.assignedTo) Assertions.assertTrue(multiQueue.add(message)) - val mvcResult: MvcResult = mockMvc.perform(put(MessageQueueController.MESSAGE_QUEUE_BASE_PATH + "/" + MessageQueueController.ENDPOINT_RELEASE + "/" + message.uuid) + val mvcResult: MvcResult = mockMvc.perform(put(MessageQueueController.MESSAGE_QUEUE_BASE_PATH + MessageQueueController.ENDPOINT_ENTRY + "/" + message.uuid + MessageQueueController.ENDPOINT_RELEASE) .contentType(MediaType.APPLICATION_JSON_VALUE)) .andExpect(MockMvcResultMatchers.status().isOk) .andReturn() @@ -649,22 +1116,25 @@ class MessageQueueControllerTest Assertions.assertNull(messageResponse.message.assignedTo) Assertions.assertEquals(message.uuid, messageResponse.message.uuid) - val updatedMessage = multiQueue.peekForType(message.type).get() + val updatedMessage = multiQueue.peekSubQueue(message.subQueue).get() Assertions.assertNull(updatedMessage.assignedTo) Assertions.assertEquals(message.uuid, updatedMessage.uuid) } /** - * Test [MessageQueueController.releaseMessage] to ensure that [HttpStatus.ACCEPTED] is returned if the message is already released and not owned by anyone. + * Test [MessageQueueController.releaseMessage] to ensure that [HttpStatus.ACCEPTED] is returned if the message + * is already released and not owned by anyone. */ @Test fun testReleaseMessage_alreadyReleased() { - val message = createQueueMessage(type = "testReleaseMessage_alreadyReleased") + Assertions.assertEquals(RestrictionMode.NONE, authenticator.getRestrictionMode()) + + val message = createQueueMessage(subQueue = "testReleaseMessage_alreadyReleased") Assertions.assertNull(message.assignedTo) Assertions.assertTrue(multiQueue.add(message)) - val mvcResult: MvcResult = mockMvc.perform(put(MessageQueueController.MESSAGE_QUEUE_BASE_PATH + "/" + MessageQueueController.ENDPOINT_RELEASE + "/" + message.uuid) + val mvcResult: MvcResult = mockMvc.perform(put(MessageQueueController.MESSAGE_QUEUE_BASE_PATH + MessageQueueController.ENDPOINT_ENTRY + "/" + message.uuid + MessageQueueController.ENDPOINT_RELEASE) .contentType(MediaType.APPLICATION_JSON_VALUE)) .andExpect(MockMvcResultMatchers.status().isAccepted) .andReturn() @@ -674,39 +1144,46 @@ class MessageQueueControllerTest Assertions.assertEquals(message.uuid, messageResponse.message.uuid) // Ensure the message is updated in the queue too - val updatedMessage = multiQueue.peekForType(message.type).get() + val updatedMessage = multiQueue.peekSubQueue(message.subQueue).get() Assertions.assertNull(updatedMessage.assignedTo) Assertions.assertEquals(message.uuid, updatedMessage.uuid) } /** - * Test [MessageQueueController.releaseMessage] to ensure that [HttpStatus.CONFLICT] is returned if `assignedTo` is provided and does not match the [QueueMessage.assignedTo], meaning the user cannot `release` the [QueueMessage] if it's not the current [QueueMessage.assignedTo] of the [QueueMessage]. + * Test [MessageQueueController.releaseMessage] to ensure that [HttpStatus.CONFLICT] is returned if `assignedTo` + * is provided and does not match the [QueueMessage.assignedTo], meaning the user cannot `release` the + * [QueueMessage] if it's not the current [QueueMessage.assignedTo] of the [QueueMessage]. */ @Test fun testReleaseMessage_cannotBeReleasedWithMisMatchingID() { + Assertions.assertEquals(RestrictionMode.NONE, authenticator.getRestrictionMode()) + val assignedTo = "assigned" - val message = createQueueMessage(type = "testReleaseMessage_cannotBeReleasedWithMisMatchingID", assignedTo = assignedTo) + val message = createQueueMessage(subQueue = "testReleaseMessage_cannotBeReleasedWithMisMatchingID", assignedTo = assignedTo) Assertions.assertEquals(assignedTo, message.assignedTo) Assertions.assertTrue(multiQueue.add(message)) val wrongAssignedTo = "wrong-assigned" - mockMvc.perform(put(MessageQueueController.MESSAGE_QUEUE_BASE_PATH + "/" + MessageQueueController.ENDPOINT_RELEASE + "/" + message.uuid) + mockMvc.perform(put(MessageQueueController.MESSAGE_QUEUE_BASE_PATH + MessageQueueController.ENDPOINT_ENTRY + "/" + message.uuid + MessageQueueController.ENDPOINT_RELEASE) .contentType(MediaType.APPLICATION_JSON_VALUE) .param(RestParameters.ASSIGNED_TO, wrongAssignedTo)) .andExpect(MockMvcResultMatchers.status().isConflict) - val assignedEntry = multiQueue.peekForType(message.type).get() + val assignedEntry = multiQueue.peekSubQueue(message.subQueue).get() Assertions.assertEquals(assignedTo, assignedEntry.assignedTo) } /** - * Test [MessageQueueController.removeMessage] to ensure that [HttpStatus.NO_CONTENT] is returned when a [QueueMessage] with the provided [UUID] does not exist. + * Test [MessageQueueController.removeMessage] to ensure that [HttpStatus.NO_CONTENT] is returned when a + * [QueueMessage] with the provided [UUID] does not exist. */ @Test fun testRemoveMessage_notFound() { + Assertions.assertEquals(RestrictionMode.NONE, authenticator.getRestrictionMode()) + val uuid = UUID.randomUUID().toString() mockMvc.perform(delete(MessageQueueController.MESSAGE_QUEUE_BASE_PATH + "/" + MessageQueueController.ENDPOINT_ENTRY + "/" + uuid) @@ -715,29 +1192,68 @@ class MessageQueueControllerTest } /** - * Test [MessageQueueController.removeMessage] to ensure that a [HttpStatus.OK] is returned when the message is correctly removed. + * Test [MessageQueueController.removeMessage] to ensure that a [HttpStatus.OK] is returned when the message is + * correctly removed. */ @Test fun testRemoveMessage_removeExistingEntry() { - val message = createQueueMessage(type = "testRemoveMessage_removed") + Assertions.assertEquals(RestrictionMode.NONE, authenticator.getRestrictionMode()) + + val message = createQueueMessage(subQueue = "testRemoveMessage_removed") + Assertions.assertTrue(multiQueue.add(message)) + Assertions.assertTrue(multiQueue.containsUUID(message.uuid).isPresent) + + mockMvc.perform(delete(MessageQueueController.MESSAGE_QUEUE_BASE_PATH + "/" + MessageQueueController.ENDPOINT_ENTRY + "/" + message.uuid) + .contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(MockMvcResultMatchers.status().isOk) + + Assertions.assertFalse(multiQueue.containsUUID(message.uuid).isPresent) + Assertions.assertTrue(multiQueue.getSubQueue(message.subQueue).isEmpty()) + } + + /** + * Ensure when in [RestrictionMode.HYBRID] mode that messages cannot be removed unless a valid + * token is provided on a restricted sub-queue. + */ + @Test + fun testRemoveMessage_removeExistingEntry_inHybridMode() + { + Mockito.doReturn(RestrictionMode.HYBRID).`when`(authenticator).getRestrictionMode() + Assertions.assertEquals(RestrictionMode.HYBRID, authenticator.getRestrictionMode()) + + val message = createQueueMessage(subQueue = "testRemoveMessage_removeExistingEntry_inHybridMode") Assertions.assertTrue(multiQueue.add(message)) Assertions.assertTrue(multiQueue.containsUUID(message.uuid).isPresent) + Assertions.assertTrue(authenticator.addRestrictedEntry(message.subQueue)) + Assertions.assertTrue(authenticator.isRestricted(message.subQueue)) + + mockMvc.perform(delete(MessageQueueController.MESSAGE_QUEUE_BASE_PATH + "/" + MessageQueueController.ENDPOINT_ENTRY + "/" + message.uuid) + .contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(MockMvcResultMatchers.status().isForbidden) + + val token = jwtTokenProvider.createTokenForSubQueue(message.subQueue) + Assertions.assertTrue(token.isPresent) + mockMvc.perform(delete(MessageQueueController.MESSAGE_QUEUE_BASE_PATH + "/" + MessageQueueController.ENDPOINT_ENTRY + "/" + message.uuid) + .header(JwtAuthenticationFilter.AUTHORIZATION_HEADER, "${JwtAuthenticationFilter.BEARER_HEADER_VALUE}${token.get()}") .contentType(MediaType.APPLICATION_JSON_VALUE)) .andExpect(MockMvcResultMatchers.status().isOk) Assertions.assertFalse(multiQueue.containsUUID(message.uuid).isPresent) - Assertions.assertTrue(multiQueue.getQueueForType(message.type).isEmpty()) + Assertions.assertTrue(multiQueue.getSubQueue(message.subQueue).isEmpty()) } /** - * Test [MessageQueueController.removeMessage] to ensure that a [HttpStatus.NO_CONTENT] is returned when the matching message does not exist. + * Test [MessageQueueController.removeMessage] to ensure that a [HttpStatus.NO_CONTENT] is returned when the + * matching message does not exist. */ @Test fun testRemoveMessage_doesNotExist() { + Assertions.assertEquals(RestrictionMode.NONE, authenticator.getRestrictionMode()) + val uuid = UUID.randomUUID().toString() Assertions.assertFalse(multiQueue.containsUUID(uuid).isPresent) @@ -749,13 +1265,16 @@ class MessageQueueControllerTest } /** - * Test [MessageQueueController.removeMessage] to ensure that [HttpStatus.FORBIDDEN] is returned if the message is attempting to be removed while another user is consuming it. + * Test [MessageQueueController.removeMessage] to ensure that [HttpStatus.FORBIDDEN] is returned if the message is + * attempting to be removed while another user is consuming it. */ @Test fun testRemoveMessage_assignedToAnotherID() { + Assertions.assertEquals(RestrictionMode.NONE, authenticator.getRestrictionMode()) + val assignedTo = "assignee" - val message = createQueueMessage(type = "testRemoveMessage_assignedToAnotherID", assignedTo = assignedTo) + val message = createQueueMessage(subQueue = "testRemoveMessage_assignedToAnotherID", assignedTo = assignedTo) Assertions.assertTrue(multiQueue.add(message)) val wrongAssignedTo = "wrong-assignee" @@ -764,24 +1283,27 @@ class MessageQueueControllerTest .param(RestParameters.ASSIGNED_TO, wrongAssignedTo)) .andExpect(MockMvcResultMatchers.status().isForbidden) - val assignedEntry = multiQueue.peekForType(message.type).get() + val assignedEntry = multiQueue.peekSubQueue(message.subQueue).get() Assertions.assertEquals(assignedTo, assignedEntry.assignedTo) } /** - * Test [MessageQueueController.getOwners] with a provided `queueType` parameter to ensure the appropriate map is provided in the response and [HttpStatus.OK] is returned. + * Test [MessageQueueController.getOwners] with a provided `sub-queue` parameter to ensure the appropriate map is + * provided in the response and [HttpStatus.OK] is returned. */ @Test - fun testGetOwners_withQueueType() + fun testGetOwners_inSubQueue() { + Assertions.assertEquals(RestrictionMode.NONE, authenticator.getRestrictionMode()) + val assignedTo = "assignedTo" val assignedTo2 = "assignedTo2" - val type = "testGetOwners" + val subQueue = "testGetOwners" - val message = createQueueMessage(type = type, assignedTo = assignedTo) - val message2 = createQueueMessage(type = type, assignedTo = assignedTo2) - val message3 = createQueueMessage(type = type, assignedTo = assignedTo2) + val message = createQueueMessage(subQueue = subQueue, assignedTo = assignedTo) + val message2 = createQueueMessage(subQueue = subQueue, assignedTo = assignedTo2) + val message3 = createQueueMessage(subQueue = subQueue, assignedTo = assignedTo2) Assertions.assertTrue(multiQueue.add(message)) Assertions.assertTrue(multiQueue.add(message2)) @@ -789,7 +1311,7 @@ class MessageQueueControllerTest val mvcResult = mockMvc.perform(get(MessageQueueController.MESSAGE_QUEUE_BASE_PATH + "/" + MessageQueueController.ENDPOINT_OWNERS) .contentType(MediaType.APPLICATION_JSON_VALUE) - .param(RestParameters.QUEUE_TYPE, type)) + .param(RestParameters.SUB_QUEUE, subQueue)) .andExpect(MockMvcResultMatchers.status().isOk) .andReturn() @@ -801,28 +1323,31 @@ class MessageQueueControllerTest val valuesInAssignedTo = owners[assignedTo] Assertions.assertTrue(valuesInAssignedTo is ArrayList<*>) - Assertions.assertTrue((valuesInAssignedTo as ArrayList<*>).contains(type)) + Assertions.assertTrue((valuesInAssignedTo as ArrayList<*>).contains(subQueue)) val valuesInAssignedTo2 = owners[assignedTo2] Assertions.assertTrue(valuesInAssignedTo2 is ArrayList<*>) - Assertions.assertTrue((valuesInAssignedTo2 as ArrayList<*>).contains(type)) + Assertions.assertTrue((valuesInAssignedTo2 as ArrayList<*>).contains(subQueue)) } /** - * Test [MessageQueueController.getOwners] without a provided `queueType` parameter to ensure the appropriate map is provided in the response and [HttpStatus.OK] is returned. + * Test [MessageQueueController.getOwners] without a provided `sub-queue` parameter to ensure the appropriate map + * is provided in the response and [HttpStatus.OK] is returned. */ @Test - fun testGetOwners_withoutQueueType() + fun testGetOwners_notInSubQueue() { + Assertions.assertEquals(RestrictionMode.NONE, authenticator.getRestrictionMode()) + val assignedTo = "assignedTo" val assignedTo2 = "assignedTo2" - val type = "testGetOwners" - val type2 = "testGetOwners2" + val subQueue = "testGetOwners" + val subQueue2 = "testGetOwners2" - val message = createQueueMessage(type = type, assignedTo = assignedTo) - val message2 = createQueueMessage(type = type2, assignedTo = assignedTo) - val message3 = createQueueMessage(type = type2, assignedTo = assignedTo2) + val message = createQueueMessage(subQueue = subQueue, assignedTo = assignedTo) + val message2 = createQueueMessage(subQueue = subQueue2, assignedTo = assignedTo) + val message3 = createQueueMessage(subQueue = subQueue2, assignedTo = assignedTo2) Assertions.assertTrue(multiQueue.add(message)) Assertions.assertTrue(multiQueue.add(message2)) @@ -841,20 +1366,23 @@ class MessageQueueControllerTest val valuesInAssignedTo = owners[assignedTo] Assertions.assertTrue(valuesInAssignedTo is ArrayList<*>) - Assertions.assertTrue((valuesInAssignedTo as ArrayList<*>).contains(type)) - Assertions.assertTrue(valuesInAssignedTo.contains(type2)) + Assertions.assertTrue((valuesInAssignedTo as ArrayList<*>).contains(subQueue)) + Assertions.assertTrue(valuesInAssignedTo.contains(subQueue2)) val valuesInAssignedTo2 = owners[assignedTo2] Assertions.assertTrue(valuesInAssignedTo2 is ArrayList<*>) - Assertions.assertTrue((valuesInAssignedTo2 as ArrayList<*>).contains(type2)) + Assertions.assertTrue((valuesInAssignedTo2 as ArrayList<*>).contains(subQueue2)) } /** - * Perform a health check call on the [MessageQueueController] to ensure a [HttpStatus.OK] is returned when the application is running ok. + * Ensure a call to the [MessageQueueController.getHealthCheck] to ensure a [HttpStatus.OK] is returned when the + * application is running ok. */ @Test fun testGetPerformHealthCheck() { + Assertions.assertEquals(RestrictionMode.NONE, authenticator.getRestrictionMode()) + mockMvc.perform(get(MessageQueueController.MESSAGE_QUEUE_BASE_PATH + "/" + MessageQueueController.ENDPOINT_HEALTH_CHECK) .contentType(MediaType.APPLICATION_JSON_VALUE)) .andExpect(MockMvcResultMatchers.status().isOk) @@ -862,12 +1390,15 @@ class MessageQueueControllerTest } /** - * Ensure that the [CorrelationIdFilter] will generate a random Correlation ID when one is not provided and that it is returned in the [MessageResponse]. + * Ensure that the [CorrelationIdFilter] will generate a random Correlation ID when one is not provided and that + * it is returned in the [MessageResponse]. */ @Test fun testCorrelationId_randomIdOnSuccess() { - val message = createQueueMessage(type = "testCorrelationId_providedId") + Assertions.assertEquals(RestrictionMode.NONE, authenticator.getRestrictionMode()) + + val message = createQueueMessage(subQueue = "testCorrelationId_providedId") val mvcResult: MvcResult = mockMvc.perform(post(MessageQueueController.MESSAGE_QUEUE_BASE_PATH + "/" + MessageQueueController.ENDPOINT_ENTRY) .contentType(MediaType.APPLICATION_JSON_VALUE) @@ -881,12 +1412,15 @@ class MessageQueueControllerTest } /** - * Ensure that the [CorrelationIdFilter] will use the same correlationID that is provided is used and returned in the [MessageResponse]. + * Ensure that the [CorrelationIdFilter] will use the same correlationID that is provided is used and returned in + * the [MessageResponse]. */ @Test fun testCorrelationId_providedId() { - val message = createQueueMessage(type = "testCorrelationId_providedId") + Assertions.assertEquals(RestrictionMode.NONE, authenticator.getRestrictionMode()) + + val message = createQueueMessage(subQueue = "testCorrelationId_providedId") val correlationId = "my-correlation-id-123456" val mvcResult: MvcResult = mockMvc.perform(post(MessageQueueController.MESSAGE_QUEUE_BASE_PATH + "/" + MessageQueueController.ENDPOINT_ENTRY) @@ -901,13 +1435,16 @@ class MessageQueueControllerTest } /** - * Ensure that the [CorrelationIdFilter] will generate a random Correlation ID is generated on error and returned in the response. + * Ensure that the [CorrelationIdFilter] will generate a random Correlation ID is generated on error and returned + * in the response. */ @Test fun testCorrelationId_randomIdOnError() { + Assertions.assertEquals(RestrictionMode.NONE, authenticator.getRestrictionMode()) + val assignedTo = "assignee" - val message = createQueueMessage(type = "testCorrelationId_randomIdOnError", assignedTo = assignedTo) + val message = createQueueMessage(subQueue = "testCorrelationId_randomIdOnError", assignedTo = assignedTo) Assertions.assertTrue(multiQueue.add(message)) val wrongAssignedTo = "wrong-assignee" @@ -926,93 +1463,306 @@ class MessageQueueControllerTest } /** - * Ensure that [MessageQueueController.deleteKeys] will only delete keys by the specified [RestParameters.QUEUE_TYPE] when it is provided and that other sub queues are not cleared. + * Ensure that [MessageQueueController.deleteKeys] will only delete keys by the specified + * [RestParameters.SUB_QUEUE] when it is provided and that other sub-queues are not cleared. */ @Test fun testDeleteKeys_singleKey() { + Assertions.assertEquals(RestrictionMode.NONE, authenticator.getRestrictionMode()) + val subQueue1 = "testDeleteKeys_singleKey1" - var message = createQueueMessage(subQueue1) - Assertions.assertTrue(multiQueue.add(message)) - message = createQueueMessage(subQueue1) - Assertions.assertTrue(multiQueue.add(message)) - message = createQueueMessage(subQueue1) - Assertions.assertTrue(multiQueue.add(message)) + var messages = listOf(createQueueMessage(subQueue1), createQueueMessage(subQueue1), createQueueMessage(subQueue1)) + messages.forEach { message -> Assertions.assertTrue(multiQueue.add(message)) } Assertions.assertEquals(3, multiQueue.size) val subQueue2 = "testDeleteKeys_singleKey2" - message = createQueueMessage(subQueue2) - Assertions.assertTrue(multiQueue.add(message)) - message = createQueueMessage(subQueue2) - Assertions.assertTrue(multiQueue.add(message)) + messages = listOf(createQueueMessage(subQueue2), createQueueMessage(subQueue2)) + messages.forEach { message -> Assertions.assertTrue(multiQueue.add(message)) } + + Assertions.assertEquals(5, multiQueue.size) + + Assertions.assertTrue(multiQueue.keys().contains(subQueue1)) + Assertions.assertTrue(multiQueue.keys().contains(subQueue2)) + + mockMvc.perform(delete(MessageQueueController.MESSAGE_QUEUE_BASE_PATH + "/" + MessageQueueController.ENDPOINT_KEYS) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .param(RestParameters.SUB_QUEUE, subQueue1)) + .andExpect(MockMvcResultMatchers.status().isNoContent) + + Assertions.assertEquals(2, multiQueue.size) + Assertions.assertFalse(multiQueue.keys().contains(subQueue1)) + Assertions.assertTrue(multiQueue.keys().contains(subQueue2)) + } + + /** + * Ensure that when in [RestrictionMode.HYBRID] mode any restricted sub-queues cannot be deleted + * unless a valid token is provided. + */ + @Test + fun testDeleteKeys_singleKey_inHybridMode() + { + Mockito.doReturn(RestrictionMode.HYBRID).`when`(authenticator).getRestrictionMode() + Assertions.assertEquals(RestrictionMode.HYBRID, authenticator.getRestrictionMode()) + + val subQueue1 = "testDeleteKeys_singleKey_inHybridMode1" + var messages = listOf(createQueueMessage(subQueue1), createQueueMessage(subQueue1), createQueueMessage(subQueue1)) + messages.forEach { message -> Assertions.assertTrue(multiQueue.add(message)) } + + Assertions.assertEquals(3, multiQueue.size) + + val subQueue2 = "testDeleteKeys_singleKey_inHybridMode2" + messages = listOf(createQueueMessage(subQueue2), createQueueMessage(subQueue2)) + messages.forEach { message -> Assertions.assertTrue(multiQueue.add(message)) } Assertions.assertEquals(5, multiQueue.size) Assertions.assertTrue(multiQueue.keys().contains(subQueue1)) Assertions.assertTrue(multiQueue.keys().contains(subQueue2)) + Assertions.assertTrue(authenticator.addRestrictedEntry(subQueue2)) + Assertions.assertTrue(authenticator.isRestricted(subQueue2)) + mockMvc.perform(delete(MessageQueueController.MESSAGE_QUEUE_BASE_PATH + "/" + MessageQueueController.ENDPOINT_KEYS) .contentType(MediaType.APPLICATION_JSON_VALUE) - .param(RestParameters.QUEUE_TYPE, subQueue1)) + .param(RestParameters.SUB_QUEUE, subQueue1)) .andExpect(MockMvcResultMatchers.status().isNoContent) Assertions.assertEquals(2, multiQueue.size) Assertions.assertFalse(multiQueue.keys().contains(subQueue1)) Assertions.assertTrue(multiQueue.keys().contains(subQueue2)) + + mockMvc.perform(delete(MessageQueueController.MESSAGE_QUEUE_BASE_PATH + "/" + MessageQueueController.ENDPOINT_KEYS) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .param(RestParameters.SUB_QUEUE, subQueue2)) + .andExpect(MockMvcResultMatchers.status().isForbidden) + + val token = jwtTokenProvider.createTokenForSubQueue(subQueue2) + Assertions.assertTrue(token.isPresent) + + mockMvc.perform(delete(MessageQueueController.MESSAGE_QUEUE_BASE_PATH + "/" + MessageQueueController.ENDPOINT_KEYS) + .header(JwtAuthenticationFilter.AUTHORIZATION_HEADER, "${JwtAuthenticationFilter.BEARER_HEADER_VALUE}${token.get()}") + .contentType(MediaType.APPLICATION_JSON_VALUE) + .param(RestParameters.SUB_QUEUE, subQueue2)) + .andExpect(MockMvcResultMatchers.status().isNoContent) + + Assertions.assertEquals(0, multiQueue.size) + Assertions.assertFalse(multiQueue.keys().contains(subQueue1)) + Assertions.assertFalse(multiQueue.keys().contains(subQueue2)) } /** - * Ensure that [MessageQueueController.deleteKeys] will only delete all keys/queues when the provided [RestParameters.QUEUE_TYPE] is `null`. + * Ensure that [MessageQueueController.deleteKeys] will only delete all keys/queues when the provided + * [RestParameters.SUB_QUEUE] is `null`. */ @Test fun testDeleteKeys_allKeys() { - val (messages, types) = initialiseMapWithEntries() + Assertions.assertEquals(RestrictionMode.NONE, authenticator.getRestrictionMode()) + + val (messages, subQueues) = initialiseMapWithEntries() + Assertions.assertEquals(messages.size, multiQueue.size) + subQueues.forEach { subQueue -> Assertions.assertTrue(multiQueue.keys().contains(subQueue)) } + + mockMvc.perform(delete(MessageQueueController.MESSAGE_QUEUE_BASE_PATH + "/" + MessageQueueController.ENDPOINT_KEYS) + .contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(MockMvcResultMatchers.status().isNoContent) + + Assertions.assertTrue(multiQueue.isEmpty()) + subQueues.forEach { subQueue -> Assertions.assertFalse(multiQueue.keys().contains(subQueue)) } + } + + /** + * Ensure that when in [RestrictionMode.HYBRID] mode any restricted sub-queues cannot be deleted + * unless a valid token is provided. + */ + @Test + fun testDeleteKeys_allKeys_inHybridMode() + { + Mockito.doReturn(RestrictionMode.HYBRID).`when`(authenticator).getRestrictionMode() + Assertions.assertEquals(RestrictionMode.HYBRID, authenticator.getRestrictionMode()) + + val (messages, subQueues) = initialiseMapWithEntries() Assertions.assertEquals(messages.size, multiQueue.size) - types.forEach { type -> Assertions.assertTrue(multiQueue.keys().contains(type)) } + subQueues.forEach { subQueue -> Assertions.assertTrue(multiQueue.keys().contains(subQueue)) } + + Assertions.assertTrue(authenticator.addRestrictedEntry(subQueues[0])) + Assertions.assertTrue(authenticator.isRestricted(subQueues[0])) mockMvc.perform(delete(MessageQueueController.MESSAGE_QUEUE_BASE_PATH + "/" + MessageQueueController.ENDPOINT_KEYS) + .contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(MockMvcResultMatchers.status().isPartialContent) + + Assertions.assertEquals(1, multiQueue.size) + Assertions.assertTrue(multiQueue.keys().contains(subQueues[0])) + subQueues.subList(1, subQueues.size - 1).forEach { subQueue -> Assertions.assertFalse(multiQueue.keys().contains(subQueue)) } + + val token = jwtTokenProvider.createTokenForSubQueue(subQueues[0]) + Assertions.assertTrue(token.isPresent) + + mockMvc.perform(delete(MessageQueueController.MESSAGE_QUEUE_BASE_PATH + "/" + MessageQueueController.ENDPOINT_KEYS) + .header(JwtAuthenticationFilter.AUTHORIZATION_HEADER, "${JwtAuthenticationFilter.BEARER_HEADER_VALUE}${token.get()}") .contentType(MediaType.APPLICATION_JSON_VALUE)) .andExpect(MockMvcResultMatchers.status().isNoContent) Assertions.assertTrue(multiQueue.isEmpty()) - types.forEach { type -> Assertions.assertFalse(multiQueue.keys().contains(type)) } + subQueues.forEach { subQueue -> Assertions.assertFalse(multiQueue.keys().contains(subQueue)) } + } + + /** + * `Mock Test`. + * + * Perform a health check call to the [MessageQueueController.getHealthCheck] to ensure a + * [HttpStatus.INTERNAL_SERVER_ERROR] is returned when the health check fails. + */ + @Test + fun testGetPerformHealthCheck_failureResponse() + { + Assertions.assertEquals(RestrictionMode.NONE, authenticator.getRestrictionMode()) + + Mockito.doThrow(RuntimeException("Failed to perform health check.")).`when`(multiQueue).performHealthCheckInternal() + + mockMvc.perform(get(MessageQueueController.MESSAGE_QUEUE_BASE_PATH + "/" + MessageQueueController.ENDPOINT_HEALTH_CHECK) + .contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(MockMvcResultMatchers.status().isInternalServerError) + .andReturn() + } + + /** + * `Mock Test`. + * + * Test [MessageQueueController.createMessage] to ensure that an internal server error is returned when [MultiQueue.add] returns `false`. + */ + @Test + fun testCreateMessage_addFails() + { + Assertions.assertEquals(RestrictionMode.NONE, authenticator.getRestrictionMode()) + + val message = QueueMessage("payload", "testCreateMessage_addFails") + + Mockito.doReturn(false).`when`(multiQueue).add(message) + + mockMvc.perform(post(MessageQueueController.MESSAGE_QUEUE_BASE_PATH + "/" + MessageQueueController.ENDPOINT_ENTRY) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .content(gson.toJson(message))) + .andExpect(MockMvcResultMatchers.status().isInternalServerError) + } + + /** + * Ensure that when [RestrictionMode] is set to [RestrictionMode.RESTRICTED] that any + * of the endpoints failing the [JwtAuthenticationFilter.canSkipTokenVerification] will be inaccessible. + */ + @Test + fun testRestrictedModeMakesAllEndpointsInaccessibleWithoutAToken() + { + Mockito.doReturn(RestrictionMode.RESTRICTED).`when`(authenticator).getRestrictionMode() + Assertions.assertEquals(RestrictionMode.RESTRICTED, authenticator.getRestrictionMode()) + + mockMvc.perform(get(MessageQueueController.MESSAGE_QUEUE_BASE_PATH + "/" + MessageQueueController.ENDPOINT_ENTRY + "/" + UUID.randomUUID().toString()) + .contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(MockMvcResultMatchers.status().isUnauthorized) + + mockMvc.perform(delete(MessageQueueController.MESSAGE_QUEUE_BASE_PATH + "/" + MessageQueueController.ENDPOINT_ENTRY + "/" + UUID.randomUUID().toString()) + .contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(MockMvcResultMatchers.status().isUnauthorized) + + val message = QueueMessage("", "testRestrictedModeMakesAllEndpointsInaccessible") + mockMvc.perform(post(MessageQueueController.MESSAGE_QUEUE_BASE_PATH + "/" + MessageQueueController.ENDPOINT_ENTRY) + .content(gson.toJson(message)) + .contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(MockMvcResultMatchers.status().isUnauthorized) + + mockMvc.perform(put(MessageQueueController.MESSAGE_QUEUE_BASE_PATH + MessageQueueController.ENDPOINT_ENTRY + "/" + UUID.randomUUID().toString() + MessageQueueController.ENDPOINT_RELEASE) + .contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(MockMvcResultMatchers.status().isUnauthorized) + + mockMvc.perform(put(MessageQueueController.MESSAGE_QUEUE_BASE_PATH + "/" + MessageQueueController.ENDPOINT_ENTRY + "/" + UUID.randomUUID().toString() + MessageQueueController.ENDPOINT_ASSIGN) + .contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(MockMvcResultMatchers.status().isUnauthorized) + + mockMvc.perform(put(MessageQueueController.MESSAGE_QUEUE_BASE_PATH + "/" + MessageQueueController.ENDPOINT_NEXT + "?" + RestParameters.SUB_QUEUE +"=someType&" + RestParameters.ASSIGNED_TO + "=me") + .contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(MockMvcResultMatchers.status().isUnauthorized) + + mockMvc.perform(get(MessageQueueController.MESSAGE_QUEUE_BASE_PATH + "/" + MessageQueueController.ENDPOINT_ALL) + .contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(MockMvcResultMatchers.status().isUnauthorized) + + mockMvc.perform(get(MessageQueueController.MESSAGE_QUEUE_BASE_PATH + "/" + MessageQueueController.ENDPOINT_OWNED + "?" + RestParameters.SUB_QUEUE +"=someType&" + RestParameters.ASSIGNED_TO + "=me") + .contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(MockMvcResultMatchers.status().isUnauthorized) + } + + /** + * Ensure that the [JwtAuthenticationFilter] will respond with [HttpStatus.UNAUTHORIZED] when no token is provided, + * and it is in [RestrictionMode.RESTRICTED] mode. + */ + @Test + fun testGetEntry_inRestrictedMode() + { + Mockito.doReturn(RestrictionMode.RESTRICTED).`when`(authenticator).getRestrictionMode() + Assertions.assertEquals(RestrictionMode.RESTRICTED, authenticator.getRestrictionMode()) + + val subQueue = "testGetEntry_inRestrictedMode" + val message = createQueueMessage(subQueue) + + Assertions.assertTrue(multiQueue.add(message)) + + mockMvc.perform(get(MessageQueueController.MESSAGE_QUEUE_BASE_PATH + "/" + MessageQueueController.ENDPOINT_ENTRY + "/" + message.uuid) + .contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(MockMvcResultMatchers.status().isUnauthorized) + + val token = jwtTokenProvider.createTokenForSubQueue(subQueue) + Assertions.assertTrue(token.isPresent) + + mockMvc.perform(get(MessageQueueController.MESSAGE_QUEUE_BASE_PATH + "/" + MessageQueueController.ENDPOINT_ENTRY + "/" + message.uuid) + .header(JwtAuthenticationFilter.AUTHORIZATION_HEADER, "${JwtAuthenticationFilter.BEARER_HEADER_VALUE}${token.get()}") + .contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(MockMvcResultMatchers.status().isUnauthorized) + + Assertions.assertTrue(authenticator.addRestrictedEntry(subQueue)) + Assertions.assertTrue(authenticator.isRestricted(subQueue)) + + mockMvc.perform(get(MessageQueueController.MESSAGE_QUEUE_BASE_PATH + "/" + MessageQueueController.ENDPOINT_ENTRY + "/" + message.uuid) + .header(JwtAuthenticationFilter.AUTHORIZATION_HEADER, "${JwtAuthenticationFilter.BEARER_HEADER_VALUE}${token.get()}") + .contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(MockMvcResultMatchers.status().isOk) } /** * A helper method which creates `4` [QueueMessage] objects and inserts them into the [MultiQueue]. * - * @return a [Pair] containing the [List] of [QueueMessage] and their related matching [List] of [String] `queueTypes` in order. + * @return a [Pair] containing the [List] of [QueueMessage] and their related matching [List] of [String] `sub-queue` IDs in order. */ private fun initialiseMapWithEntries(): Pair, List> { - val types = listOf("type1", "type2", "type3", "type4") - val message = createQueueMessage(type = types[0]) - val message2 = createQueueMessage(type = types[1]) - val message3 = createQueueMessage(type = types[2], assignedTo = "assignee") - val message4 = createQueueMessage(type = types[3]) + val subQueues = listOf("type1", "type2", "type3", "type4") + val message = createQueueMessage(subQueue = subQueues[0]) + val message2 = createQueueMessage(subQueue = subQueues[1]) + val message3 = createQueueMessage(subQueue = subQueues[2], assignedTo = "assignee") + val message4 = createQueueMessage(subQueue = subQueues[3]) - multiQueue.add(message) - multiQueue.add(message2) - multiQueue.add(message3) - multiQueue.add(message4) + Assertions.assertTrue(multiQueue.add(message)) + Assertions.assertTrue(multiQueue.add(message2)) + Assertions.assertTrue(multiQueue.add(message3)) + Assertions.assertTrue(multiQueue.add(message4)) - return Pair(listOf(message, message2, message3, message4), types) + return Pair(listOf(message, message2, message3, message4), subQueues) } /** * A helper method to create a [QueueMessage] that can be easily re-used between each test. * - * @param type the `queueType` to assign to the created [QueueMessage] + * @param subQueue the `subQueue` to set in to the created [QueueMessage] * @param assignedTo the [QueueMessage.assignedTo] value to set * @return a [QueueMessage] initialised with multiple parameters */ - private fun createQueueMessage(type: String, assignedTo: String? = null): QueueMessage + private fun createQueueMessage(subQueue: String, assignedTo: String? = null): QueueMessage { val uuid = UUID.randomUUID().toString() val payload = Payload("test", 12, true, PayloadEnum.C) - val message = QueueMessage(payload = payload, type = type) + val message = QueueMessage(payload = payload, subQueue = subQueue) message.uuid = UUID.fromString(uuid).toString() message.assignedTo = assignedTo diff --git a/src/test/kotlin/au/kilemon/messagequeue/rest/controller/SettingsControllerTest.kt b/src/test/kotlin/au/kilemon/messagequeue/rest/controller/SettingsControllerTest.kt index 4a2fd76..8421e58 100644 --- a/src/test/kotlin/au/kilemon/messagequeue/rest/controller/SettingsControllerTest.kt +++ b/src/test/kotlin/au/kilemon/messagequeue/rest/controller/SettingsControllerTest.kt @@ -1,20 +1,23 @@ package au.kilemon.messagequeue.rest.controller +import au.kilemon.messagequeue.authentication.RestrictionMode +import au.kilemon.messagequeue.authentication.authenticator.MultiQueueAuthenticator +import au.kilemon.messagequeue.configuration.QueueConfiguration import au.kilemon.messagequeue.logging.LoggingConfiguration import au.kilemon.messagequeue.settings.MessageQueueSettings -import au.kilemon.messagequeue.settings.MultiQueueType +import au.kilemon.messagequeue.settings.StorageMedium import com.google.gson.Gson import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith +import org.mockito.Mockito import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest -import org.springframework.boot.test.context.SpringBootTest import org.springframework.boot.test.context.TestConfiguration +import org.springframework.boot.test.mock.mockito.SpyBean import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Import import org.springframework.http.MediaType -import org.springframework.test.context.ContextConfiguration import org.springframework.test.context.junit.jupiter.SpringExtension import org.springframework.test.web.servlet.MockMvc import org.springframework.test.web.servlet.MvcResult @@ -27,8 +30,8 @@ import org.springframework.test.web.servlet.result.MockMvcResultMatchers * @author github.com/Kilemonn */ @ExtendWith(SpringExtension::class) -@WebMvcTest(controllers = [SettingsController::class], properties = ["${MessageQueueSettings.MULTI_QUEUE_TYPE}=IN_MEMORY"]) -@Import(LoggingConfiguration::class) +@WebMvcTest(controllers = [SettingsController::class], properties = ["${MessageQueueSettings.STORAGE_MEDIUM}=IN_MEMORY"]) +@Import(*[QueueConfiguration::class, LoggingConfiguration::class]) class SettingsControllerTest { /** @@ -53,21 +56,26 @@ class SettingsControllerTest @Autowired private lateinit var mockMvc: MockMvc + @SpyBean + private lateinit var multiQueueAuthenticator: MultiQueueAuthenticator + private val gson: Gson = Gson() /** - * Test [SettingsController.getSettings] and verify the response payload and default values. + * A helper method to call [SettingsController.getSettings] and verify the response default values. */ - @Test - fun testGetSettings_defaultValues() + private fun testGetSettings_defaultValues(authenticationType: RestrictionMode) { + Assertions.assertEquals(authenticationType, multiQueueAuthenticator.getRestrictionMode()) + val mvcResult: MvcResult = mockMvc.perform(MockMvcRequestBuilders.get(SettingsController.SETTINGS_PATH) .contentType(MediaType.APPLICATION_JSON_VALUE)) .andExpect(MockMvcResultMatchers.status().isOk) .andReturn() val settings = gson.fromJson(mvcResult.response.contentAsString, MessageQueueSettings::class.java) - Assertions.assertEquals(MultiQueueType.IN_MEMORY.toString(), settings.multiQueueType) + Assertions.assertEquals(StorageMedium.IN_MEMORY.toString(), settings.storageMedium) + Assertions.assertEquals(RestrictionMode.NONE.toString(), settings.restrictionMode) Assertions.assertTrue(settings.redisPrefix.isEmpty()) Assertions.assertEquals(MessageQueueSettings.REDIS_ENDPOINT_DEFAULT, settings.redisEndpoint) @@ -83,4 +91,37 @@ class SettingsControllerTest Assertions.assertTrue(settings.mongoUsername.isEmpty()) Assertions.assertTrue(settings.mongoUri.isEmpty()) } + + /** + * Ensure calls to [SettingsController.getSettings] is still available even then the [RestrictionMode] + * is set to [RestrictionMode.NONE]. + */ + @Test + fun testGetSettings_noneMode() + { + Mockito.doReturn(RestrictionMode.NONE).`when`(multiQueueAuthenticator).getRestrictionMode() + testGetSettings_defaultValues(RestrictionMode.NONE) + } + + /** + * Ensure calls to [SettingsController.getSettings] is still available even then the [RestrictionMode] + * is set to [RestrictionMode.HYBRID]. + */ + @Test + fun testGetSettings_hybridMode() + { + Mockito.doReturn(RestrictionMode.HYBRID).`when`(multiQueueAuthenticator).getRestrictionMode() + testGetSettings_defaultValues(RestrictionMode.HYBRID) + } + + /** + * Ensure calls to [SettingsController.getSettings] is still available even then the [RestrictionMode] + * is set to [RestrictionMode.RESTRICTED]. + */ + @Test + fun testGetSettings_restrictedMode() + { + Mockito.doReturn(RestrictionMode.RESTRICTED).`when`(multiQueueAuthenticator).getRestrictionMode() + testGetSettings_defaultValues(RestrictionMode.RESTRICTED) + } } diff --git a/src/test/kotlin/au/kilemon/messagequeue/rest/response/RestResponseExceptionHandlerTest.kt b/src/test/kotlin/au/kilemon/messagequeue/rest/response/RestResponseExceptionHandlerTest.kt index beda368..77f392d 100644 --- a/src/test/kotlin/au/kilemon/messagequeue/rest/response/RestResponseExceptionHandlerTest.kt +++ b/src/test/kotlin/au/kilemon/messagequeue/rest/response/RestResponseExceptionHandlerTest.kt @@ -1,6 +1,10 @@ package au.kilemon.messagequeue.rest.response +import au.kilemon.messagequeue.authentication.RestrictionMode +import au.kilemon.messagequeue.authentication.exception.MultiQueueAuthenticationException +import au.kilemon.messagequeue.authentication.exception.MultiQueueAuthorisationException import au.kilemon.messagequeue.filter.CorrelationIdFilter +import au.kilemon.messagequeue.queue.exception.IllegalSubQueueIdentifierException import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.BeforeEach @@ -17,6 +21,8 @@ import java.util.* */ class RestResponseExceptionHandlerTest { + private val responseHandler = RestResponseExceptionHandler() + @BeforeEach fun setUp() { @@ -33,16 +39,16 @@ class RestResponseExceptionHandlerTest } /** - * Ensure all the properties required to create an [ErrorResponse] are correctly extracted from the [ResponseStatusException]. + * Ensure [RestResponseExceptionHandler.handleResponseStatusException] sets all the properties required to create an + * [ErrorResponse] are correctly extracted from the [ResponseStatusException]. */ @Test fun testHandleResponseStatusException() { val correlationId = UUID.randomUUID().toString() MDC.put(CorrelationIdFilter.CORRELATION_ID, correlationId) - val responseHandler = RestResponseExceptionHandler() val message = "Bad error message" - val statusCode = HttpStatus.FORBIDDEN + val statusCode = HttpStatus.I_AM_A_TEAPOT val exception = ResponseStatusException(statusCode, message) val response = responseHandler.handleResponseStatusException(exception) @@ -51,4 +57,60 @@ class RestResponseExceptionHandlerTest Assertions.assertEquals(message, response.body!!.message) Assertions.assertEquals(correlationId, response.body!!.correlationId) } + + /** + * Ensure the [RestResponseExceptionHandler.handleMultiQueueAuthorisationException] returns the appropriate + * response code and message on error. + */ + @Test + fun testHandleMultiQueueAuthorisationException() + { + val correlationId = UUID.randomUUID().toString() + MDC.put(CorrelationIdFilter.CORRELATION_ID, correlationId) + val message = "testHandleMultiQueueAuthorisationException" + val exception = MultiQueueAuthorisationException(message, RestrictionMode.NONE) + val response = responseHandler.handleMultiQueueAuthorisationException(exception) + + Assertions.assertEquals(HttpStatus.FORBIDDEN, response.statusCode) + Assertions.assertNotNull(response.body) + Assertions.assertEquals(String.format(MultiQueueAuthorisationException.MESSAGE_FORMAT, message, RestrictionMode.NONE), response.body!!.message) + Assertions.assertEquals(correlationId, response.body!!.correlationId) + } + + /** + * Ensure the [RestResponseExceptionHandler.handleMultiQueueAuthenticationException] returns the appropriate + * response code and message on error. + */ + @Test + fun testHandleMultiQueueAuthenticationException() + { + val correlationId = UUID.randomUUID().toString() + MDC.put(CorrelationIdFilter.CORRELATION_ID, correlationId) + val exception = MultiQueueAuthenticationException() + val response = responseHandler.handleMultiQueueAuthenticationException(exception) + + Assertions.assertEquals(HttpStatus.UNAUTHORIZED, response.statusCode) + Assertions.assertNotNull(response.body) + Assertions.assertEquals(MultiQueueAuthenticationException.ERROR_MESSAGE, response.body!!.message) + Assertions.assertEquals(correlationId, response.body!!.correlationId) + } + + /** + * Ensure the [RestResponseExceptionHandler.handleIllegalSubQueueIdentifierException] returns the appropriate reponse + * and error message. + */ + @Test + fun testHandleIllegalSubQueueIdentifierException() + { + val correlationId = UUID.randomUUID().toString() + MDC.put(CorrelationIdFilter.CORRELATION_ID, correlationId) + val subQueue = "testHandleIllegalSubQueueIdentifierException" + val exception = IllegalSubQueueIdentifierException(subQueue) + val response = responseHandler.handleIllegalSubQueueIdentifierException(exception) + + Assertions.assertEquals(HttpStatus.BAD_REQUEST, response.statusCode) + Assertions.assertNotNull(response.body) + Assertions.assertTrue(response.body!!.message!!.contains(subQueue)) + Assertions.assertEquals(correlationId, response.body!!.correlationId) + } } diff --git a/src/test/kotlin/au/kilemon/messagequeue/settings/MessageQueueSettingsDefaultTest.kt b/src/test/kotlin/au/kilemon/messagequeue/settings/MessageQueueSettingsDefaultTest.kt index c3b9088..9f1cc79 100644 --- a/src/test/kotlin/au/kilemon/messagequeue/settings/MessageQueueSettingsDefaultTest.kt +++ b/src/test/kotlin/au/kilemon/messagequeue/settings/MessageQueueSettingsDefaultTest.kt @@ -42,10 +42,21 @@ class MessageQueueSettingsDefaultTest fun testDefaults() { Assertions.assertNotNull(messageQueueSettings) - Assertions.assertEquals(MessageQueueSettings.MULTI_QUEUE_TYPE_DEFAULT, messageQueueSettings.multiQueueType) + Assertions.assertEquals(MessageQueueSettings.STORAGE_MEDIUM_DEFAULT, messageQueueSettings.storageMedium) + Assertions.assertEquals(MessageQueueSettings.RESTRICTION_MODE_DEFAULT, messageQueueSettings.restrictionMode) + Assertions.assertEquals(MessageQueueSettings.REDIS_ENDPOINT_DEFAULT, messageQueueSettings.redisEndpoint) Assertions.assertEquals("", messageQueueSettings.redisPrefix) Assertions.assertEquals(MessageQueueSettings.REDIS_MASTER_NAME_DEFAULT, messageQueueSettings.redisMasterName) Assertions.assertEquals(false.toString(), messageQueueSettings.redisUseSentinels) + + Assertions.assertEquals("", messageQueueSettings.sqlEndpoint) + Assertions.assertEquals("", messageQueueSettings.sqlUsername) + + Assertions.assertEquals("", messageQueueSettings.mongoHost) + Assertions.assertEquals("", messageQueueSettings.mongoUri) + Assertions.assertEquals("", messageQueueSettings.mongoPort) + Assertions.assertEquals("", messageQueueSettings.mongoDatabase) + Assertions.assertEquals("", messageQueueSettings.mongoUsername) } } diff --git a/src/test/kotlin/au/kilemon/messagequeue/settings/MessageQueueSettingsTest.kt b/src/test/kotlin/au/kilemon/messagequeue/settings/MessageQueueSettingsTest.kt index 2c072c3..e95c07a 100644 --- a/src/test/kotlin/au/kilemon/messagequeue/settings/MessageQueueSettingsTest.kt +++ b/src/test/kotlin/au/kilemon/messagequeue/settings/MessageQueueSettingsTest.kt @@ -16,7 +16,7 @@ import org.springframework.test.context.junit.jupiter.SpringExtension */ @ExtendWith(SpringExtension::class) @TestPropertySource(properties = [ - "${MessageQueueSettings.MULTI_QUEUE_TYPE}=REDIS", + "${MessageQueueSettings.STORAGE_MEDIUM}=REDIS", "${MessageQueueSettings.REDIS_ENDPOINT}=123.123.123.123", "${MessageQueueSettings.REDIS_PREFIX}=redis", "${MessageQueueSettings.REDIS_USE_SENTINELS}=true", @@ -50,7 +50,7 @@ class MessageQueueSettingsTest fun testValues() { Assertions.assertNotNull(messageQueueSettings) - Assertions.assertEquals("REDIS", messageQueueSettings.multiQueueType) + Assertions.assertEquals("REDIS", messageQueueSettings.storageMedium) Assertions.assertEquals("123.123.123.123", messageQueueSettings.redisEndpoint) Assertions.assertEquals("redis", messageQueueSettings.redisPrefix) Assertions.assertEquals("master", messageQueueSettings.redisMasterName)