Compare commits

...

107 Commits

Author SHA1 Message Date
dependabot[bot]
113b18a93d Bump ua-parser-js from 0.7.32 to 0.7.33 in /quartz-manager-frontend
Bumps [ua-parser-js](https://github.com/faisalman/ua-parser-js) from 0.7.32 to 0.7.33.
- [Release notes](https://github.com/faisalman/ua-parser-js/releases)
- [Changelog](https://github.com/faisalman/ua-parser-js/blob/master/changelog.md)
- [Commits](https://github.com/faisalman/ua-parser-js/compare/0.7.32...0.7.33)

---
updated-dependencies:
- dependency-name: ua-parser-js
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-01-27 18:18:56 +00:00
Fabio Formosa
82a60eb651 Update README.md 2022-12-10 16:59:31 +01:00
Fabio Formosa
7c910196e1 upgraded README and CHANGELOG 2022-12-10 16:54:50 +01:00
Fabio Formosa
554e7e5e25 #80 bump version to 4.0.8 2022-12-10 16:39:26 +01:00
Fabio Formosa
8b70319436 #80 upgraded node and npm into the quartz-manager-starter-ui 2022-12-10 16:38:34 +01:00
Fabio Formosa
053f196b6b Merge pull request #100 from fabioformosa/develop
v4.0.7
2022-12-10 16:29:16 +01:00
Fabio Formosa
86f8cc8347 bump version to 4.0.7 2022-12-10 16:25:17 +01:00
Fabio Formosa
e91d02ba9f #80 added moment to the allowed commonJS module 2022-12-10 16:19:39 +01:00
Fabio Formosa
109d2868d9 #80 upgraded to the new linter and added prettier 2022-12-10 16:15:00 +01:00
Fabio Formosa
4673e41fc5 #80 removed unnecessary asterisks 2022-12-10 14:53:35 +01:00
Fabio Formosa
a2122351d6 #80 removed extractCss 2022-12-10 13:31:45 +01:00
Fabio Formosa
375aaf71d3 #80 fixed the build command as angular v14 2022-12-10 13:25:14 +01:00
Fabio Formosa
b752af948d #80 downgraded jest to v28 2022-12-10 13:21:18 +01:00
Fabio Formosa
52ed016073 #80 fixed tests 2022-12-10 12:20:57 +01:00
Fabio Formosa
632288726b #80 upgraded some deps 2022-12-10 11:55:00 +01:00
Fabio Formosa
1dc9bee987 #80 upgraded to angular v14 2022-12-10 11:27:14 +01:00
Fabio Formosa
387c440e06 #80 step into angular v14 2022-12-10 11:20:01 +01:00
Fabio Formosa
136afa0fca #80 step into angular v14 2022-12-10 11:17:39 +01:00
Fabio Formosa
8ba3daa2f0 #80 upgraded angular to v13 2022-12-08 00:15:48 +01:00
Fabio Formosa
b3fe337203 #80 upgraded angular to v13 2022-12-08 00:15:39 +01:00
Fabio Formosa
da7375ea0d #80 first step to angular v13 2022-12-07 00:38:30 +01:00
Fabio Formosa
d9487d1106 #80 completed the migration to angular12 2022-12-07 00:37:25 +01:00
Fabio Formosa
3110496630 #80 completed the migration to angular12 2022-12-07 00:36:59 +01:00
Fabio Formosa
22e90dc7d1 #80 step into angular v12 2022-12-07 00:29:20 +01:00
Fabio Formosa
5943243e62 #80 step into angular v12 2022-12-07 00:27:20 +01:00
Fabio Formosa
1244a64d34 #80 fixes for angular v11 2022-12-07 00:25:50 +01:00
Fabio Formosa
40a6ecd159 #80 fixes for angular v11 2022-12-07 00:24:56 +01:00
Fabio Formosa
9df68dccac #80 first step to angular v11 2022-12-06 23:35:35 +01:00
Fabio Formosa
72bba6f80c #80 reverted deps to angular 10 2022-12-06 23:29:29 +01:00
Fabio Formosa
cf803bb1dd #80 reverted deps to angular 10 2022-12-06 23:29:13 +01:00
Fabio Formosa
13c78ed5d3 #80 removed the package-lock.json 2022-12-06 23:19:08 +01:00
Fabio Formosa
3bd878a978 #80 reverted deps to angular 10 2022-12-06 23:14:45 +01:00
Fabio Formosa
c9d3528f4b #80 reverted deps to angular 10 2022-12-06 23:14:32 +01:00
Fabio Formosa
3b2f8fc780 #80 reverted deps to angular 10 2022-12-06 23:08:17 +01:00
Fabio Formosa
bc3da09d60 #80 step into angular upgrade to v11 2022-12-06 19:33:42 +01:00
Fabio Formosa
284f56302c #80 fixed tests 2022-12-06 01:08:17 +01:00
Fabio Formosa
cdd5047bbc #80 jest setup fix for angular 10 2022-12-06 00:39:45 +01:00
Fabio Formosa
5d3ba95bd9 #80 upgraded angular to v10 2022-12-05 23:42:36 +01:00
Fabio Formosa
ec384113eb angular material upgrade to v4 2022-12-05 23:36:36 +01:00
Fabio Formosa
8b2651876c new package-lock.json 2022-12-05 23:31:55 +01:00
Fabio Formosa
a3852c421e bump version to 4.0.7-SNAPSHOT 2022-12-05 23:09:51 +01:00
Fabio Formosa
e1e1bdbd01 bump version to 4.0.6 2022-12-04 12:15:51 +01:00
Fabio Formosa
6757511de3 #62 added a missing test 2022-12-04 12:07:20 +01:00
Fabio Formosa
f5b717ec36 Update README.md 2022-12-04 11:54:06 +01:00
Fabio Formosa
6b491d9949 Update README.md 2022-12-04 11:52:54 +01:00
Fabio Formosa
cf382db49f Update README.md 2022-12-04 11:52:24 +01:00
Fabio Formosa
ee2f80e582 Update README.md
added a table of contents
2022-12-04 11:49:26 +01:00
Fabio Formosa
4453d62515 Update README.md
Added sonar quality badges
2022-12-04 11:33:47 +01:00
Fabio Formosa
72068818d7 #62 fixed sonar smells 2022-12-04 11:25:21 +01:00
Fabio Formosa
92bb94b9fa #62 added coverage exclusion 2022-12-04 11:15:53 +01:00
Fabio Formosa
7a2098e9ce #62 added coverage exclusion 2022-12-04 11:08:48 +01:00
Fabio Formosa
56b81f4f48 #62 added some exclusion from coverage 2022-12-03 19:54:07 +01:00
Fabio Formosa
82c060c2a7 #62 set jacoco properties 2022-12-03 19:46:14 +01:00
Fabio Formosa
fba35b796d #62 set jacoco properties 2022-12-03 19:42:12 +01:00
Fabio Formosa
5f6fc1fa6f #62 set jacoco properties 2022-12-03 19:16:28 +01:00
Fabio Formosa
a9c227cd05 Merge branch 'develop' 2022-12-03 19:11:49 +01:00
Fabio Formosa
a8330e062f #62 set jacoco properties 2022-12-03 19:07:29 +01:00
Fabio Formosa
30d0cbf6de Merge pull request #97 from fabioformosa/develop
v4.0.6
2022-12-03 17:17:00 +01:00
Fabio Formosa
3063e08eb3 #62 solved a couple of sonar smells 2022-12-03 17:10:34 +01:00
Fabio Formosa
f7054b160f #62 added missing tests 2022-12-03 16:46:27 +01:00
Fabio Formosa
98b5d0e37a #62 fixed the job class name in the TriggerFiredBundleDTO 2022-12-03 16:21:13 +01:00
Fabio Formosa
6348bac11a #62 added missing tests 2022-12-03 12:19:41 +01:00
Fabio Formosa
c2a26c97a8 #62 added some coverage exclusion 2022-12-02 00:37:56 +01:00
Fabio Formosa
cd6e01109b #62 added some coverage exclusion 2022-12-02 00:32:52 +01:00
Fabio Formosa
7553efdc3b #62 added missing tests 2022-12-02 00:21:23 +01:00
Fabio Formosa
6578dc312a #62 added missing tests 2022-12-01 23:52:48 +01:00
Fabio Formosa
c490a7ab28 #62 added missing tests 2022-12-01 23:48:16 +01:00
Fabio Formosa
0969a406c6 #62 fixed a failing test 2022-12-01 00:55:38 +01:00
Fabio Formosa
eed3021373 #62 fixed a failing test 2022-12-01 00:35:40 +01:00
Fabio Formosa
5b33bd4dca #62 added missing tests 2022-12-01 00:30:47 +01:00
Fabio Formosa
6d22207e27 #62 added missing tests 2022-11-29 23:39:14 +01:00
Fabio Formosa
249cf49873 #62 added missing tests 2022-11-29 23:12:54 +01:00
Fabio Formosa
b17d487c8b #62 added missing tests 2022-11-29 20:29:55 +01:00
Fabio Formosa
fedb2b50b6 #62 clean up 2022-11-29 18:44:46 +01:00
Fabio Formosa
4b18313e2d #62 removed unused security filter 2022-11-23 20:54:37 +01:00
Fabio Formosa
8387f587b3 bump version to 4.0.6-SNAPSHOT 2022-11-20 14:02:45 +01:00
Fabio Formosa
c7b64dbdf3 Update README.md 2022-11-20 14:01:24 +01:00
Fabio Formosa
085d61cf29 Merge pull request #95 from fabioformosa/develop
v4.0.5
2022-11-20 14:00:34 +01:00
Fabio Formosa
e6cf2e9390 bump version to 4.0.5 2022-11-20 13:43:07 +01:00
Fabio Formosa
4bcea96789 Merge remote-tracking branch 'origin/master' into develop 2022-11-20 13:42:52 +01:00
Fabio Formosa
866062bdcb #62 added a missing test for the MisfireTestJob 2022-11-20 13:36:25 +01:00
Fabio Formosa
53a54ddbda #62 added a missing test to the UserController 2022-11-20 13:16:42 +01:00
Fabio Formosa
8bdb85b878 Update README.md 2022-11-20 12:50:26 +01:00
Fabio Formosa
9a26be41a8 #78 excluded lombok annotations from the coverage 2022-11-19 18:59:24 +01:00
Fabio Formosa
5cf39b3861 #78 fixed sonar errors 2022-11-19 18:53:13 +01:00
Fabio Formosa
99d87636d2 #78 removed an unused CORS config 2022-11-19 18:45:47 +01:00
Fabio Formosa
bccd50ac4a #78 put a secureRandom value as default value for the JWT token 2022-11-19 18:28:39 +01:00
Fabio Formosa
3bb30accfd #78 fixed some sonar warnings 2022-11-19 16:34:36 +01:00
Fabio Formosa
5d2b71652c #78 fixed 2 sonar warnings 2022-11-19 16:25:53 +01:00
Fabio Formosa
b70af4dafe Merge remote-tracking branch 'origin/master' into develop 2022-11-19 16:21:07 +01:00
Fabio Formosa
a95cf20c6b bump version to 4.0.5-SNAPSHOT 2022-11-19 13:54:25 +01:00
Fabio Formosa
b58a5dbe9f Update README.md 2022-11-19 13:31:52 +01:00
Fabio Formosa
f7222d65ae moved the nexus plugin under the maven profile 2022-11-19 13:29:14 +01:00
Fabio Formosa
ff43103f37 Merge pull request #89 from fabioformosa/develop
v4.0.4
2022-11-19 13:01:44 +01:00
Fabio Formosa
93ab9c55bc bump version to 4.0.4 2022-11-19 13:00:39 +01:00
Fabio Formosa
28715cdf62 enhanced the info message within the UnsupportedMultipleJobsDialog 2022-11-19 12:40:23 +01:00
Fabio Formosa
bd5276116b fixed the redirect to the login in case of security layer enabled 2022-11-19 12:18:36 +01:00
Fabio Formosa
b20fb9e9c3 bump version to 4.0.4-SNAPSHOT 2022-11-19 12:09:41 +01:00
Fabio Formosa
d3a406a382 Update maven-release.yml 2022-11-19 11:38:35 +01:00
Fabio Formosa
c70fe687e1 Update README.md 2022-11-19 11:37:55 +01:00
Fabio Formosa
fc81685044 Update README.md 2022-11-19 11:22:00 +01:00
Fabio Formosa
6e24d7caf5 bump version to 4.0.3 2022-11-19 11:05:57 +01:00
Fabio Formosa
6786cffb4d fixed the maven-release.yml 2022-11-19 11:05:23 +01:00
Fabio Formosa
1805705ff2 bump version to 4.0.2 2022-11-19 10:57:48 +01:00
Fabio Formosa
64fbabba4d fixed the maven-release.yml 2022-11-19 10:55:38 +01:00
Fabio Formosa
0db13872a1 Update maven-release.yml 2022-11-19 10:46:40 +01:00
Fabio Formosa
694e199709 Merge pull request #84 from fabioformosa/develop
v4.0.0
2022-11-19 10:36:20 +01:00
87 changed files with 28915 additions and 12840 deletions

View File

@@ -26,10 +26,10 @@ jobs:
gpg-passphrase: MAVEN_GPG_PASSPHRASE
- name: Build with Maven
run: mvn -B package
run: mvn -B package --file quartz-manager-parent/pom.xml
- name: Publish to maven central
run: mvn deploy --batch-mode -P release-maven-central, build-webjar
run: mvn deploy --file quartz-manager-parent/pom.xml --batch-mode -P "release-maven-central,build-webjar"
env:
MAVEN_USERNAME: ${{ secrets.OSSRH_USERNAME }}
MAVEN_PASSWORD: ${{ secrets.OSSRH_TOKEN }}
@@ -41,6 +41,6 @@ jobs:
java-version: '11'
distribution: 'temurin'
- name: Publish to GitHub Packages Apache Maven
run: mvn deploy -P deploy-github, build-webjar
run: mvn deploy --file quartz-manager-parent/pom.xml -P "deploy-github,build-webjar"
env:
GITHUB_TOKEN: ${{ github.token }}

View File

@@ -1,3 +1,20 @@
## **v4.0.8**
Upgraded the frontend to angular v14
## **v4.0.6**
Minor bug fixes
## **v4.0.5**
Fixed potential security issues
## **v4.0.4**
* Conformed the trigger configuration to the Simple Trigger of Quartz
* **BREAKING CHANGE** Changed accordingly the API and the UI
* Made Quartz Manager embeddable in projects with existing quartz instance, security layer, swagger ui.
## **v3.1.0**
* Added a new persistence module to persist the quartz triggers in a postgresql database
## **v3.0.1**
Quartz-Manager is now publicly available into the maven central repo into 3 different packages.

View File

@@ -1,6 +1,23 @@
[![Java CI with Maven](https://github.com/fabioformosa/quartz-manager/actions/workflows/maven.yml/badge.svg)](https://github.com/fabioformosa/quartz-manager/actions/workflows/maven.yml)
[![npm CI](https://github.com/fabioformosa/quartz-manager/actions/workflows/npm.yml/badge.svg)](https://github.com/fabioformosa/quartz-manager/actions/workflows/npm.yml)
[![Maven Central](https://maven-badges.herokuapp.com/maven-central/it.fabioformosa.quartz-manager/quartz-manager-starter-api/badge.svg)](https://maven-badges.herokuapp.com/maven-central/it.fabioformosa.quartz-manager/quartz-manager-starter-api)
[![Maven Central](https://maven-badges.herokuapp.com/maven-central/it.fabioformosa.quartz-manager/quartz-manager-starter-api/badge.svg)](https://maven-badges.herokuapp.com/maven-central/it.fabioformosa.quartz-manager/quartz-manager-starter-api)
[![Coverage](https://sonarcloud.io/api/project_badges/measure?project=fabioformosa_quartz-manager&metric=coverage)](https://sonarcloud.io/summary/new_code?id=fabioformosa_quartz-manager) [![Bugs](https://sonarcloud.io/api/project_badges/measure?project=fabioformosa_quartz-manager&metric=bugs)](https://sonarcloud.io/summary/new_code?id=fabioformosa_quartz-manager) [![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=fabioformosa_quartz-manager&metric=security_rating)](https://sonarcloud.io/summary/new_code?id=fabioformosa_quartz-manager) [![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=fabioformosa_quartz-manager&metric=sqale_rating)](https://sonarcloud.io/summary/new_code?id=fabioformosa_quartz-manager)
[QUARTZ MANAGER](https://github.com/fabioformosa/quartz-manager#quartz-manager)
&nbsp;&nbsp;&nbsp;&nbsp;[Quartz Manager UI](https://github.com/fabioformosa/quartz-manager#quartz-manager-ui)
&nbsp;&nbsp;&nbsp;&nbsp;[Quartz Manager API](https://github.com/fabioformosa/quartz-manager#quartz-manager-api)
[HOW IT WORKS](https://github.com/fabioformosa/quartz-managerhttps://github.com/fabioformosa/quartz-manager#get-started)
&nbsp;&nbsp;&nbsp;&nbsp;[Quartz Manager Starter API Lib](https://github.com/fabioformosa/quartz-manager#quartz-manager-starter-api-lib)
&nbsp;&nbsp;&nbsp;&nbsp;[Quartz Manager Starter UI Lib](https://github.com/fabioformosa/quartz-manager#quartz-manager-starter-ui-lib)
&nbsp;&nbsp;&nbsp;&nbsp;[Quartz Manager Starter Security Lib](https://github.com/fabioformosa/quartz-manager#quartz-manager-starter-security-lib)
&nbsp;&nbsp;&nbsp;&nbsp;[Quartz Manager Persistence Lib](https://github.com/fabioformosa/quartz-manager#quartz-manager-starter-persistence-lib)
[EXAMPLES](https://github.com/fabioformosa/quartz-manager#examples)
[LIMITATIONS](https://github.com/fabioformosa/quartz-manager#limitations)
[ROADMAP](https://github.com/fabioformosa/quartz-manager#roadmap)
[REPOSITORY](https://github.com/fabioformosa/quartz-manager#repository)
[HOW TO CONTRIBUTE](https://github.com/fabioformosa/quartz-manager#how-to-contribute)
[SUPPORT THE PROJECT](https://github.com/fabioformosa/quartz-manager#%EF%B8%8F-support-the-project-%EF%B8%8F)
# QUARTZ MANAGER
In the last decade, the [Quartz Scheduler](http://www.quartz-scheduler.org/) has become the most adopted opensource job scheduling library for Java applications.
@@ -14,13 +31,13 @@ Quartz Manager is a Java library you can import in your Spring-Based Web Applica
The **Quartz Manager UI** is a dashboard in the form of a single-page-application provided by the Quartz Manager Java library itself. You can have it embedded in your project, as well as you get embedded the Swagger UI.
It leverages the websockets to receive in real-time the trigger updates and the outcomes of the job executions.
![](https://github.com/fabioformosa/quartz-manager/blob/develop/quartz-manager-parent/quartz-manager-web-showcase/src/main/resources/quartz-manager-4-screenshot.png)
![](https://github.com/fabioformosa/quartz-manager/blob/master/quartz-manager-parent/quartz-manager-web-showcase/src/main/resources/quartz-manager-4-screenshot.png)
## QUARTZ MANAGER API
Quart-Manager exposes REST endpoints to interact with the Quartz Scheduler. This endpoints are invoked by Quartz Manager UI also.
The REST API are documented by an OpenAPI Specification interface.
![](https://github.com/fabioformosa/quartz-manager/blob/develop/quartz-manager-parent/quartz-manager-web-showcase/src/main/resources/quartz-manager-4-swagger.png)
![](https://github.com/fabioformosa/quartz-manager/blob/master/quartz-manager-parent/quartz-manager-web-showcase/src/main/resources/quartz-manager-4-swagger.png)
# HOW IT WORKS
@@ -37,7 +54,7 @@ In the latter case, in which there isn't an existing quartz instance, you can re
* You can enable a secure layer, if your project doesn't have any, to give access at the API and the UI only to authenticated users.
* You can enable a persistent layer, to persist the config and the progress of your trigger, in a postgresql database.
# QUICK START
# GET STARTED
**Requirements**
Java 9+, Spring Framework 5+ (Spring Boot 2.x)
@@ -52,7 +69,7 @@ In order to decrease the overall configuration time for the project, all modules
Below the list of the quartz-manager modules you can import.
## Quartz Manager API Lib
## Quartz Manager Starter API Lib
This is the only mandatory module of the library.
Add the dependency, make eligible for Quart Manager the job classes and set the props in your `application.properties` file.
@@ -63,12 +80,12 @@ Add the dependency, make eligible for Quart Manager the job classes and set the
<dependency>
<groupId>it.fabioformosa.quartz-manager</groupId>
<artifactId>quartz-manager-starter-api</artifactId>
<version>4.0.0</version>
<version>4.0.8</version>
</dependency>
```
#### Gradle
```
implementation group: 'it.fabioformosa.quartz-manager', name: 'quartz-manager-starter-api', version: '4.0.0'
implementation group: 'it.fabioformosa.quartz-manager', name: 'quartz-manager-starter-api', version: '4.0.8'
```
### Step 2. Quartz Manager Job Classes
@@ -96,11 +113,17 @@ In this way, Quartz Manager is able to collect and display the outcomes at the U
### REST API & OpenAPI Specification
Set the app prop `quartz-manager.oas.enabled=true` you want to expose the OpenApi Specification of the Quartz Manager APIs.
Set the app prop `quartz-manager.oas.enabled=true` if you want to expose the OpenApi Specification of the Quartz Manager APIs.
Reach out the swagger-ui at the URL:
[http://localhost:8080/swagger-ui.html](http://localhost:8080/swagger-ui.html)
If your project has already an OpenAPI instance and you've set `quartz-manager.oas.enabled=true`, then make sure to add an OpenApiGroup to group the API of your application. Quart Manager exposes its API in group called "Quartz Manager".
If your project has already an OpenAPI instance and you've set `quartz-manager.oas.enabled=true`, then make sure to add an OpenApiGroup to group the API of your application. Quart Manager exposes its API in group called "Quartz Manager". If you use OAS Annotations:
```
@Bean
public GroupedOpenApi simplySpringDemoGroupedOpenApi() {
return GroupedOpenApi.builder().group("myapp").packagesToScan("com.example.myapp").build();
}
```
### QUARTZ SETTINGS
Quartz Manager creates its own instance of a [Quartz Scheduler](http://www.quartz-scheduler.org/).
@@ -122,7 +145,7 @@ The prerequesite is that you've imported a quartz scheduler ver 2.3.x.
You can configure the Quartz instance managed by Quartz Manager through the file `managed-quartz.properties` and your own Quartz instance through the file `quartz.properties`.
If you've created a `SchedulerFactoryBean`, tag it as @Primary to avoid conflicts in-type, since Quartz Manager creates another bean of the same type.
If you've created a `SchedulerFactoryBean`, tag it as `@Primary` to avoid conflicts in-type, since Quartz Manager creates another bean of the same type.
```
@Primary
@@ -135,7 +158,7 @@ If you've created a `SchedulerFactoryBean`, tag it as @Primary to avoid conflict
```
## Quartz Manager UI Lib
## Quartz Manager Starter UI Lib
You can optionally import the following dependency to have the UI Dashboard to interact with the Quartz Manager API.
### Dependency
@@ -145,12 +168,12 @@ You can optionally import the following dependency to have the UI Dashboard to i
<dependency>
<groupId>it.fabioformosa.quartz-manager</groupId>
<artifactId>quartz-manager-starter-ui</artifactId>
<version>4.0.0</version>
<version>4.0.8</version>
</dependency>
```
#### Gradle
```
implementation group: 'it.fabioformosa.quartz-manager', name: 'quartz-manager-starter-ui', version: '4.0.0'
implementation group: 'it.fabioformosa.quartz-manager', name: 'quartz-manager-starter-ui', version: '4.0.8'
```
### Reach out the UI Console at URL
@@ -158,7 +181,7 @@ if you run locally [http://localhost:8080/quartz-manager-ui/index.html](http://l
## Quartz Manager Security Lib
## Quartz Manager Starter Security Lib
Import this optional dependency, if you want to enable a security layer and allow the access to the REST API and UI only to authenticated users.
The authentication model of Quartz Manager is based on [JWT](https://jwt.io/).
@@ -180,14 +203,14 @@ Future development: the Quart Manager Security OAuth2 client.
<dependency>
<groupId>it.fabioformosa.quartz-manager</groupId>
<artifactId>quartz-manager-starter-security</artifactId>
<version>4.0.0</version>
<version>4.0.8</version>
</dependency>
```
#### Gradle
```
compile group: 'it.fabioformosa.quartz-manager', name: 'quartz-manager-starter-security', version: '4.0.0'
compile group: 'it.fabioformosa.quartz-manager', name: 'quartz-manager-starter-security', version: '4.0.8'
```
@@ -203,11 +226,11 @@ compile group: 'it.fabioformosa.quartz-manager', name: 'quartz-manager-starter-s
|quartz-manager.security.accounts.in-memory.users[0].roles[0] | string | yes | | set the value ADMIN |
## Quart Manager Persistence Lib
## Quart Manager Starter Persistence Lib
By default, Quartz Manager runs with a `org.quartz.simpl.RAMJobStore` that stores your managed scheduler in memory. The RAMJobStore is the simplest store and also the most performant. However it comes with the drawback that all scheduling data are lost if your applications ends or crashes. In case of a restarting of your app, you can't resume the scheduler from the point it stopped.
Import the Quartz Manager Persistence Module if you want to persist your scheduler data.
The pre-requesite is the availability of Postgresql database where Quartz Manager can store its information. You have to provide it a bare database and a postresql user granted for DDL and DML queries. About the DDL, consider that Quartz Manager Persistence will create all tables it needs to work at the bootstrap.
By default, Quartz Manager runs with a `org.quartz.simpl.RAMJobStore` that stores your managed scheduler in memory. The RAMJobStore is the simplest store and also the most performant. However it comes with the drawback that all scheduling data are lost if your applications ends or crashes. In case of a restarting of your app, you can't resume the scheduler from the point it stopped.
Import the Quartz Manager Persistence Module if you want to persist your scheduler data.
The pre-requesite is the availability of Postgresql database where Quartz Manager can store its information. You have to provide it a bare database and a postresql user granted for DDL and DML queries. About the DDL, consider that Quartz Manager Persistence will create all tables, it needs to work, at the bootstrap.
### Dependency
@@ -217,14 +240,14 @@ The pre-requesite is the availability of Postgresql database where Quartz Manage
<dependency>
<groupId>it.fabioformosa.quartz-manager</groupId>
<artifactId>quartz-manager-starter-persistence</artifactId>
<version>4.0.0</version>
<version>4.0.8</version>
</dependency>
```
#### Gradle
```
compile group: 'it.fabioformosa.quartz-manager', name: 'quartz-manager-starter-persistence', version: '4.0.0'
compile group: 'it.fabioformosa.quartz-manager', name: 'quartz-manager-starter-persistence', version: '4.0.8'
```
### Quartz Manager Persistence Lib - App Props
@@ -239,13 +262,13 @@ compile group: 'it.fabioformosa.quartz-manager', name: 'quartz-manager-starter-p
## Examples
You can find some examples of different scenarios of projects which import Quartz Manager at the repository [quartz-manager-use-cases] (https://github.com/fabioformosa/quartz-manager-use-cases)
* *simply-spring* - tipical scenario in which you create a minimal spring project from scratch dedicated to launch your scheduled jobs. Imported libraries: Quartz Manager API, Quartz Manager UI and Quartz Manager security.
* *simply-spring-no-security* - as simple-spring, without the security
* *existing-security-cookie-based* - It demonstrates how Quartz Manager stays under the security of your project, in case of an auth model based on cookies
* *existing-security-header-based* - It demonstrates how Quartz Manager Security can coexists with another Spring Security Config present in your project
* *existing-quartz* - It demonstrates how to Quartz Manager can coexist with a Quartz instance already present in your project
* *existing-quartz-and-storage* - It demonstrates how to Quartz Manager Persistence can coexist with a Quartz instance already present in your project
You can find some examples of different scenarios of projects which import Quartz Manager at the repository [quartz-manager-use-cases](https://github.com/fabioformosa/quartz-manager-use-cases)
* *simply-spring* - tipical scenario in which you create a minimal spring project from scratch dedicated to launch your scheduled jobs. Imported libraries: Quartz Manager API, Quartz Manager UI and Quartz Manager Security.
* *simply-spring-no-security* - as simple-spring, without the security. Imported libraries: Quartz Manager API, Quartz Manager UI.
* *existing-security-cookie-based* - It demonstrates how Quartz Manager stays under the security of your project, in case of an auth model based on cookies. Imported libraries: Quartz Manager API, Quartz Manager UI.
* *existing-security-header-based* - It demonstrates how Quartz Manager Security can coexists with another Spring Security Config present in your project. Imported libraries: Quartz Manager API, Quartz Manager UI and Quartz Manager Security.
* *existing-quartz* - It demonstrates how to Quartz Manager can coexist with a Quartz instance already present in your project Imported libraries: Quartz Manager API, Quartz Manager UI.
* *existing-quartz-and-storage* - It demonstrates how to Quartz Manager Persistence can coexist with a Quartz instance already present in your project. Imported libraries: Quartz Manager API, Quartz Manager UI and Quartz Manager Persistence.
## Limitations

View File

@@ -0,0 +1,214 @@
/*
👋 Hi! This file was autogenerated by tslint-to-eslint-config.
https://github.com/typescript-eslint/tslint-to-eslint-config
It represents the closest reasonable ESLint configuration to this
project's original TSLint configuration.
We recommend eventually switching this configuration to extend from
the recommended rulesets in typescript-eslint.
https://github.com/typescript-eslint/tslint-to-eslint-config/blob/master/docs/FAQs.md
Happy linting! 💖
*/
{
"env": {
"browser": true,
"es6": true,
"node": true
},
"extends": [
"prettier"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"project": "tsconfig.json",
"sourceType": "module"
},
"plugins": [
"eslint-plugin-import",
"@angular-eslint/eslint-plugin",
"@typescript-eslint",
"@typescript-eslint/tslint"
],
"root": true,
"rules": {
"@angular-eslint/component-class-suffix": "error",
"@angular-eslint/component-selector": [
"error",
{
"type": "element",
"prefix": "app",
"style": "kebab-case"
}
],
"@angular-eslint/directive-class-suffix": "error",
"@angular-eslint/directive-selector": [
"error",
{
"type": "attribute",
"prefix": "app",
"style": "camelCase"
}
],
"@angular-eslint/no-host-metadata-property": "error",
"@angular-eslint/no-input-rename": "error",
"@angular-eslint/no-inputs-metadata-property": "error",
"@angular-eslint/no-output-rename": "error",
"@angular-eslint/no-outputs-metadata-property": "error",
"@angular-eslint/use-lifecycle-interface": "error",
"@angular-eslint/use-pipe-transform-interface": "error",
"@typescript-eslint/consistent-type-definitions": "error",
"@typescript-eslint/dot-notation": "off",
"@typescript-eslint/explicit-member-accessibility": [
"off",
{
"accessibility": "explicit"
}
],
"@typescript-eslint/indent": "off",
"@typescript-eslint/member-delimiter-style": [
"off",
{
"multiline": {
"delimiter": "none",
"requireLast": true
},
"singleline": {
"delimiter": "semi",
"requireLast": false
}
}
],
"@typescript-eslint/member-ordering": "error",
"@typescript-eslint/naming-convention": [
"error",
{
"selector": "variable",
"format": [
"camelCase",
"UPPER_CASE"
],
"leadingUnderscore": "forbid",
"trailingUnderscore": "forbid"
}
],
"@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/no-empty-interface": "error",
"@typescript-eslint/no-inferrable-types": [
"error",
{
"ignoreParameters": true
}
],
"@typescript-eslint/no-shadow": [
"error",
{
"hoist": "all"
}
],
"@typescript-eslint/no-unused-expressions": "error",
"@typescript-eslint/prefer-function-type": "error",
"@typescript-eslint/quotes": [
"error",
"single"
],
"@typescript-eslint/semi": [
"off",
null
],
"@typescript-eslint/tslint/config": [
"error",
{
"rules": {
"import-spacing": true,
"invoke-injectable": true,
"no-access-missing-member": true,
"templates-use-public": true,
"whitespace": true
}
}
],
"@typescript-eslint/type-annotation-spacing": "off",
"@typescript-eslint/unified-signatures": "error",
"brace-style": [
"error",
"1tbs"
],
"curly": "error",
"dot-notation": "off",
"eol-last": "off",
"eqeqeq": [
"error",
"smart"
],
"guard-for-in": "error",
"id-denylist": "off",
"id-match": "off",
"import/no-deprecated": "warn",
"indent": "off",
"max-len": [
"error",
{
"code": 140
}
],
"no-bitwise": "error",
"no-caller": "error",
"no-console": [
"error",
{
"allow": [
"log",
"warn",
"dir",
"timeLog",
"assert",
"clear",
"count",
"countReset",
"group",
"groupEnd",
"table",
"dirxml",
"error",
"groupCollapsed",
"Console",
"profile",
"profileEnd",
"timeStamp",
"context"
]
}
],
"no-debugger": "error",
"no-empty": "off",
"no-empty-function": "off",
"no-eval": "error",
"no-fallthrough": "error",
"no-new-wrappers": "error",
"no-redeclare": "error",
"no-restricted-imports": "error",
"no-shadow": "off",
"no-throw-literal": "error",
"no-trailing-spaces": "off",
"no-underscore-dangle": "off",
"no-unused-expressions": "off",
"no-unused-labels": "error",
"no-var": "error",
"prefer-const": "error",
"quotes": "off",
"radix": "error",
"semi": "off",
"spaced-comment": [
"error",
"always",
{
"markers": [
"/"
]
}
],
"valid-typeof": "error"
}
}

View File

@@ -40,3 +40,4 @@ testem.log
# System Files
.DS_Store
Thumbs.db
/.angular/

View File

@@ -0,0 +1,47 @@
src/polyfills.ts
src/typings.d.ts
_test.ts
# See http://help.github.com/ignore-files/ for more about ignoring files.
# compiled output
/dist
/tmp
/out-tsc
# dependencies
/node_modules
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
# misc
/.sass-cache
/connect.lock
/coverage
/libpeerconnection.log
npm-debug.log
testem.log
/typings
# e2e
/e2e/*.js
/e2e/*.map
# System Files
.DS_Store
Thumbs.db
/.angular/

View File

@@ -0,0 +1,11 @@
{
"tabWidth": 2,
"useTabs": false,
"singleQuote": true,
"semi": true,
"bracketSpacing": true,
"arrowParens": "avoid",
"trailingComma": "es5",
"bracketSameLine": true,
"printWidth": 80
}

View File

@@ -18,6 +18,9 @@
"main": "src/main.ts",
"tsConfig": "src/tsconfig.app.json",
"polyfills": "src/polyfills.ts",
"allowedCommonJsDependencies": [
"stompjs", "sockjs-client", "moment"
],
"assets": [
"src/assets",
"src/favicon.ico"
@@ -38,7 +41,6 @@
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"extractCss": true,
"namedChunks": false,
"aot": true,
"extractLicenses": true,
@@ -71,13 +73,10 @@
}
},
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"builder": "@angular-eslint/builder:lint",
"options": {
"tsConfig": [
"src/tsconfig.app.json",
"src/tsconfig.spec.json"
],
"exclude": []
"eslintConfig": ".eslintrc.json",
"lintFilePatterns": ["**/*.spec.ts", "**/*.ts"]
}
}
}
@@ -106,7 +105,6 @@
}
}
},
"defaultProject": "angular-spring-starter",
"schematics": {
"@schematics/angular:component": {
"prefix": "qrzmng",
@@ -115,5 +113,8 @@
"@schematics/angular:directive": {
"prefix": "qrzmng"
}
},
"cli": {
"analytics": false
}
}

View File

@@ -1 +1 @@
import 'jest-preset-angular';
import 'jest-preset-angular/setup-jest';

File diff suppressed because it is too large Load Diff

View File

@@ -5,67 +5,81 @@
"scripts": {
"ng": "ng",
"start": "ng serve --proxy-config proxy.conf.json",
"build": "ng build --prod",
"build": "ng build --configuration production",
"test": "jest",
"lint": "ng lint",
"e2e": "ng e2e"
},
"private": true,
"dependencies": {
"@angular-material-components/datetime-picker": "2.0.4",
"@angular-material-components/moment-adapter": "2.0.2",
"@angular/animations": "9.1.4",
"@angular/cdk": "9.2.1",
"@angular/common": "9.1.4",
"@angular/compiler": "9.1.4",
"@angular/core": "9.1.4",
"@angular/flex-layout": "9.0.0-beta.29",
"@angular/forms": "9.1.4",
"@angular/material": "9.2.1",
"@angular/platform-browser": "9.1.4",
"@angular/platform-browser-dynamic": "9.1.4",
"@angular/platform-server": "9.1.4",
"@angular/router": "9.1.4",
"@auth0/angular-jwt": "^4.0.0",
"@angular-material-components/datetime-picker": "8.0.0",
"@angular-material-components/moment-adapter": "8.0.0",
"@angular/animations": "14.2.12",
"@angular/cdk": "^14.0.1",
"@angular/common": "14.2.12",
"@angular/compiler": "14.2.12",
"@angular/core": "14.2.12",
"@angular/flex-layout": "14.0.0-beta.41",
"@angular/forms": "14.2.12",
"@angular/material": "^14.0.1",
"@angular/platform-browser": "14.2.12",
"@angular/platform-browser-dynamic": "14.2.12",
"@angular/platform-server": "14.2.12",
"@angular/router": "14.2.12",
"@auth0/angular-jwt": "5.1.0",
"@fortawesome/fontawesome": "^1.1.4",
"@fortawesome/fontawesome-free-regular": "^5.0.8",
"@fortawesome/fontawesome-free-solid": "^5.0.8",
"@stomp/ng2-stompjs": "^0.6.3",
"@types/jest": "^27.0.2",
"core-js": "2.5.1",
"hammerjs": "2.0.8",
"moment": "^2.29.1",
"net": "^1.0.2",
"rxjs": "6.5.5",
"sockjs-client": "^1.1.1",
"stompjs": "^2.3.3",
"tslib": "^1.10.0",
"zone.js": "~0.10.2"
"tslib": "~2.4.1",
"zone.js": "~0.12.0"
},
"devDependencies": {
"@angular-devkit/build-angular": "~0.901.4",
"@angular-devkit/core": "^9.1.4",
"@angular/cli": "9.1.4",
"@angular/compiler-cli": "9.1.4",
"@angular/language-service": "9.1.4",
"@angular-devkit/build-angular": "14.2.10",
"@angular-devkit/core": "^14.2.10",
"@angular-eslint/builder": "14.4.0",
"@angular-eslint/eslint-plugin": "14.4.0",
"@angular-eslint/eslint-plugin-template": "14.4.0",
"@angular-eslint/schematics": "14.4.0",
"@angular-eslint/template-parser": "14.4.0",
"@angular/cli": "14.2.10",
"@angular/compiler-cli": "14.2.12",
"@angular/language-service": "14.2.12",
"@types/hammerjs": "2.0.34",
"@types/jasmine": "2.5.54",
"@types/jasminewd2": "2.0.3",
"@types/jest": "28.1.1",
"@types/node": "^12.11.1",
"codelyzer": "^5.1.2",
"jasmine-core": "2.6.4",
"jasmine-spec-reporter": "4.1.1",
"jest": "^26.0.1",
"jest-preset-angular": "^8.2.0",
"karma": "1.7.1",
"karma-chrome-launcher": "2.1.1",
"karma-cli": "1.0.1",
"karma-coverage-istanbul-reporter": "1.3.0",
"karma-jasmine": "1.1.0",
"karma-jasmine-html-reporter": "0.2.2",
"protractor": "5.1.2",
"ts-node": "3.0.6",
"tslint": "5.7.0",
"typescript": "3.8.3"
"@typescript-eslint/eslint-plugin": "5.43.0",
"@typescript-eslint/eslint-plugin-tslint": "^5.46.0",
"@typescript-eslint/parser": "5.43.0",
"codelyzer": "~6.0.2",
"eslint": "^8.28.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-prettier": "^4.2.1",
"jasmine-core": "~4.5.0",
"jasmine-spec-reporter": "~7.0.0",
"jest": "28.1.3",
"jest-preset-angular": "~12.2.3",
"karma": "~6.4.1",
"karma-chrome-launcher": "~3.1.1",
"karma-cli": "2.0.0",
"karma-coverage-istanbul-reporter": "~3.0.3",
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.0.0",
"prettier": "^2.8.1",
"prettier-eslint": "^15.0.1",
"protractor": "~7.0.0",
"ts-node": "10.9.1",
"typescript": "4.6.4"
},
"jest": {
"preset": "jest-preset-angular",

View File

@@ -46,7 +46,7 @@ export const routes: Routes = [
@NgModule({
imports: [RouterModule.forRoot(routes, {
initialNavigation: false
initialNavigation: 'disabled'
})],
exports: [RouterModule],
providers: []

View File

@@ -1,4 +1,4 @@
import { TestBed, async } from '@angular/core/testing';
import {TestBed, async, waitForAsync} from '@angular/core/testing';
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { RouterTestingModule } from '@angular/router/testing';
import { AppComponent } from './app.component';
@@ -18,7 +18,7 @@ import {
} from './services';
describe('AppComponent', () => {
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [
AppComponent,

View File

@@ -1,4 +1,4 @@
import { async, ComponentFixture, TestBed, inject } from '@angular/core/testing';
import { ComponentFixture, TestBed, inject, waitForAsync} from '@angular/core/testing';
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { RouterTestingModule } from '@angular/router/testing';
@@ -18,7 +18,7 @@ describe('AccountMenuComponent', () => {
let component: AccountMenuComponent;
let fixture: ComponentFixture<AccountMenuComponent>;
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [
RouterTestingModule

View File

@@ -1,4 +1,4 @@
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing';
import {SchedulerControlComponent} from './scheduler-control.component';
import {ApiService, ConfigService, SchedulerService, UserService} from '../../services';
import {HttpClient} from '@angular/common/http';
@@ -19,7 +19,7 @@ describe('SchedulerControlComponent', () => {
let httpClient: HttpClient;
let httpTestingController: HttpTestingController;
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [MatCardModule, MatDividerModule, MatIconModule, HttpClientTestingModule, RouterTestingModule],
declarations: [SchedulerControlComponent],

View File

@@ -20,7 +20,7 @@
<mat-form-field
[appearance]="enabledTriggerForm && !trigger ? 'standard': 'none'"
class="full-size-input">
<mat-label>Trigger Name *</mat-label>
<mat-label>Trigger Name</mat-label>
<input id="triggerName"
[readonly]="!enabledTriggerForm || trigger"
matInput placeholder="name of the trigger (unique)"
@@ -36,7 +36,7 @@
[appearance]="enabledTriggerForm ? 'standard': 'none'"
class="full-size-input"
>
<mat-label>Job Class *</mat-label>
<mat-label>Job Class</mat-label>
<mat-select id="jobClass" name="jobClass" formControlName="jobClass" [disabled]="!enabledTriggerForm">
<mat-option *ngFor="let job of jobs" [value]="job" style="font-size: 0.8em">
{{job}}
@@ -53,7 +53,7 @@
[appearance]="enabledTriggerForm ? 'standard': 'none'"
class="full-size-input"
>
<mat-label>Misfire Instruction *</mat-label>
<mat-label>Misfire Instruction</mat-label>
<mat-select id="misfireInstruction" name="misfireInstruction" formControlName="misfireInstruction"
[disabled]="!enabledTriggerForm" style="font-size: 0.8em">
<mat-option value="MISFIRE_INSTRUCTION_FIRE_NOW">FIRE NOW</mat-option>
@@ -93,9 +93,6 @@
formControlName="startDate" name="startDate">
<mat-datepicker-toggle matSuffix [for]="startDatePicker"></mat-datepicker-toggle>
<ngx-mat-datetime-picker #startDatePicker showSpinners="true" showSeconds="true">
<!-- [stepHour]="stepHour"-->
<!-- [stepMinute]="stepMinute" [stepSecond]="stepSecond" [touchUi]="touchUi" [color]="color"-->
<!-- [enableMeridian]="enableMeridian" [disableMinute]="disableMinute" [hideTime]="hideTime">-->
</ngx-mat-datetime-picker>
</mat-form-field>
</div>
@@ -114,9 +111,6 @@
>
<mat-datepicker-toggle matSuffix [for]="endDatePicker"></mat-datepicker-toggle>
<ngx-mat-datetime-picker #endDatePicker showSpinners="true" showSeconds="true">
<!-- [stepHour]="stepHour"-->
<!-- [stepMinute]="stepMinute" [stepSecond]="stepSecond" [touchUi]="touchUi" [color]="color"-->
<!-- [enableMeridian]="enableMeridian" [disableMinute]="disableMinute" [hideTime]="hideTime">-->
</ngx-mat-datetime-picker>
</mat-form-field>
<mat-error *ngIf="simpleTriggerReactiveForm.controls.triggerPeriod.errors?.invalidTriggerPeriod" style="font-size: small">
@@ -141,9 +135,6 @@
repeatCount and repeatInterval must be <strong>both</strong> set or unset
</mat-error>
</mat-form-field>
<!-- <mat-error *ngIf="simpleTriggerReactiveForm.controls.triggerRecurrence.errors?.invalidTriggerRecurrence" style="font-size: small">-->
<!-- repeatCount and repeatInterval must be <strong>both</strong> set or unset-->
<!-- </mat-error>-->
</div>
<div>
<mat-form-field

View File

@@ -1,4 +1,4 @@
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing';
import {MatCardModule} from '@angular/material/card';
import {SimpleTriggerConfigComponent} from './simple-trigger-config.component';
import {ApiService, ConfigService, CONTEXT_PATH, SchedulerService} from '../../services';
@@ -12,7 +12,7 @@ import {FormBuilder, FormsModule, ReactiveFormsModule} from '@angular/forms';
import {MatFormFieldModule} from '@angular/material/form-field';
import {MatNativeDateModule} from '@angular/material/core';
import {MatInputModule} from '@angular/material/input';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
import {TriggerKey} from '../../model/triggerKey.model';
import {Trigger} from '../../model/trigger.model';
import {JobDetail} from '../../model/jobDetail.model';
@@ -29,9 +29,9 @@ describe('SimpleTriggerConfig', () => {
let httpClient: HttpClient;
let httpTestingController: HttpTestingController;
beforeEach(async( () => {
beforeEach(waitForAsync( () => {
TestBed.configureTestingModule({
imports: [FormsModule, MatFormFieldModule, MatFormFieldModule, MatSelectModule, MatInputModule, BrowserAnimationsModule,
imports: [FormsModule, MatFormFieldModule, MatFormFieldModule, MatSelectModule, MatInputModule, NoopAnimationsModule,
MatNativeDateModule, ReactiveFormsModule,
MatCardModule, MatIconModule, HttpClientTestingModule, RouterTestingModule],
declarations: [SimpleTriggerConfigComponent],
@@ -198,6 +198,10 @@ describe('SimpleTriggerConfig', () => {
it('should fetch and display the trigger when the triggerKey is passed as input', () => {
const mockTriggerKey = new TriggerKey('my-simple-trigger', null);
component.triggerKey = mockTriggerKey;
component.trigger = new SimpleTrigger();
component.trigger.triggerKeyDTO = mockTriggerKey;
fixture.detectChanges();
const mockTrigger = new Trigger();
@@ -226,6 +230,9 @@ describe('SimpleTriggerConfig', () => {
getJobsReq.flush([]);
fixture.detectChanges();
component.openTriggerForm();
fixture.detectChanges();
const componentDe: DebugElement = fixture.debugElement;
const warningCard = componentDe.query(By.css('#noEligibleJobsAlert'));
expect(warningCard).toBeTruthy();
@@ -237,6 +244,9 @@ describe('SimpleTriggerConfig', () => {
getJobsReq.flush(['sampleJob']);
fixture.detectChanges();
component.openTriggerForm();
fixture.detectChanges();
const componentDe: DebugElement = fixture.debugElement;
const warningCard = componentDe.query(By.css('#noEligibleJobsAlert'));
expect(warningCard).toBeFalsy();

View File

@@ -3,12 +3,11 @@ import {SchedulerService} from '../../services';
import {Scheduler} from '../../model/scheduler.model';
import {SimpleTriggerCommand} from '../../model/simple-trigger.command';
import {SimpleTrigger} from '../../model/simple-trigger.model';
import {SimpleTriggerForm} from '../../model/simple-trigger.form';
import * as moment from 'moment';
import {TriggerKey} from '../../model/triggerKey.model';
import JobService from '../../services/job.service';
import {MisfireInstruction, MisfireInstructionCaption} from '../../model/misfire-instruction.model';
import {AbstractControl, FormBuilder, FormControl, FormGroup, ValidationErrors, ValidatorFn, Validators} from '@angular/forms';
import {AbstractControl, UntypedFormBuilder, UntypedFormGroup, ValidationErrors, Validators} from '@angular/forms';
@Component({
selector: 'qrzmng-simple-trigger-config',
@@ -17,9 +16,9 @@ import {AbstractControl, FormBuilder, FormControl, FormGroup, ValidationErrors,
})
export class SimpleTriggerConfigComponent implements OnInit {
trigger: SimpleTrigger;
trigger: SimpleTrigger = null;
simpleTriggerReactiveForm: FormGroup = this.formBuilder.group({
simpleTriggerReactiveForm: UntypedFormGroup = this.formBuilder.group({
triggerName: [this.trigger?.triggerKeyDTO.name, Validators.required],
jobClass: [this.trigger?.jobDetailDTO.jobClassName, Validators.required],
triggerPeriod: this.formBuilder.group({
@@ -50,7 +49,7 @@ export class SimpleTriggerConfigComponent implements OnInit {
onNewTrigger = new EventEmitter<SimpleTrigger>();
constructor(
private formBuilder: FormBuilder,
private formBuilder: UntypedFormBuilder,
private schedulerService: SchedulerService,
private jobService: JobService
) {

View File

@@ -1,4 +1,4 @@
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing';
import {ApiService, ConfigService, CONTEXT_PATH, TriggerService} from '../../services';
import {HttpClient} from '@angular/common/http';
import {HttpClientTestingModule, HttpTestingController} from '@angular/common/http/testing';
@@ -21,7 +21,7 @@ describe('TriggerListComponent', () => {
let httpClient: HttpClient;
let httpTestingController: HttpTestingController;
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [MatCardModule, MatDialogModule, MatDividerModule,
MatIconModule, MatListModule, HttpClientTestingModule, RouterTestingModule],

View File

@@ -2,13 +2,25 @@ import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core';
import {TriggerService} from '../../services/trigger.service';
import {TriggerKey} from '../../model/triggerKey.model';
import {SimpleTrigger} from '../../model/simple-trigger.model';
import {MatDialog} from '@angular/material/dialog';
import {MatDialog, MatDialogRef} from '@angular/material/dialog';
@Component({
template: 'Multiple jobs not supported yet - Coming Soon...',
template: `
<h3 mat-dialog-title>Coming Soon</h3>
<div mat-dialog-content>
<p>This feature is in roadmap and it will come with the next releases</p>
</div>
<div mat-dialog-actions>
<button mat-button (click)="closeDialog()" style="padding: 0.5em;width: 5em;">Ok</button>
</div>`,
})
// tslint:disable-next-line:component-class-suffix
export class UnsupportedMultipleJobsDialog {
constructor(public dialogRef: MatDialogRef<UnsupportedMultipleJobsDialog>) {
}
closeDialog(): void {
this.dialogRef.close();
}
}
@Component({
@@ -69,7 +81,7 @@ export class TriggerListComponent implements OnInit {
}
onNewTriggerBtnClicked() {
if (this.triggerKeys && this.triggerKeys.length > 0) {
if (this.getTriggerKeyList() && this.getTriggerKeyList().length > 0) {
this.dialog.open(UnsupportedMultipleJobsDialog)
} else {
this.onNewTriggerClicked.emit();

View File

@@ -42,8 +42,9 @@ export class UserService {
this.router.initialNavigation();
return;
}
if (httpErrorResponse.status < 200 || httpErrorResponse.status > 399)
if (httpErrorResponse.status !== 401 && (httpErrorResponse.status < 200 || httpErrorResponse.status > 399)) {
this.router.navigateByUrl('/error');
}
});
}

View File

@@ -1,4 +1,4 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing';
import {GenericErrorComponent} from './genericError.component';
@@ -6,7 +6,7 @@ describe('GenericComponent', () => {
let component: GenericErrorComponent;
let fixture: ComponentFixture<GenericErrorComponent>;
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ GenericErrorComponent ]
})

View File

@@ -1,4 +1,4 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing';
import { ForbiddenComponent } from './forbidden.component';
@@ -6,7 +6,7 @@ describe('ForbiddenComponent', () => {
let component: ForbiddenComponent;
let fixture: ComponentFixture<ForbiddenComponent>;
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ ForbiddenComponent ]
})

View File

@@ -1,4 +1,4 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing';
import { LoginComponent } from './login.component';
import { RouterTestingModule } from '@angular/router/testing';
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
@@ -16,7 +16,7 @@ describe('LoginComponent', () => {
let component: LoginComponent;
let fixture: ComponentFixture<LoginComponent>;
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [LoginComponent],
imports: [

View File

@@ -1,5 +1,5 @@
import {Component, OnDestroy, OnInit} from '@angular/core';
import {FormBuilder, FormGroup, Validators} from '@angular/forms';
import {UntypedFormBuilder, UntypedFormGroup, Validators} from '@angular/forms';
import {ActivatedRoute, Router} from '@angular/router';
import {DisplayMessage} from '../../shared/models/display-message';
import {Subject} from 'rxjs';
@@ -15,7 +15,7 @@ import {AuthService, UserService} from '../../services';
export class LoginComponent implements OnInit, OnDestroy {
title = 'Login';
githubLink = 'https://github.com/fabioformosa/quartz-manager';
form: FormGroup;
form: UntypedFormGroup;
submitted = false;
@@ -29,7 +29,7 @@ export class LoginComponent implements OnInit, OnDestroy {
private authService: AuthService,
private router: Router,
private route: ActivatedRoute,
private formBuilder: FormBuilder
private formBuilder: UntypedFormBuilder
) {
}

View File

@@ -1,4 +1,4 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing';
import { NotFoundComponent } from './not-found.component';
@@ -6,7 +6,7 @@ describe('NotFoundComponent', () => {
let component: NotFoundComponent;
let fixture: ComponentFixture<NotFoundComponent>;
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [NotFoundComponent]
})

View File

@@ -10,7 +10,7 @@
"moduleResolution": "node",
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"target": "es2015",
"target": "es2020",
"typeRoots": [
"node_modules/@types"
],
@@ -18,6 +18,6 @@
"es2016",
"dom"
],
"module": "esnext"
"module": "es2020"
}
}

View File

@@ -12,6 +12,9 @@
"curly": true,
"eofline": true,
"forin": true,
"deprecation": {
"severity": "warning"
},
"import-blacklist": [true],
"import-spacing": true,
"indent": [
@@ -53,7 +56,6 @@
"no-switch-case-fall-through": true,
"no-trailing-whitespace": true,
"no-unused-expression": true,
"no-use-before-declare": true,
"no-var-keyword": true,
"object-literal-sort-keys": false,
"one-line": [

View File

@@ -58,13 +58,13 @@ Replace the dummy job (class: `it.fabioformosa.quartzmanager.jobs.SampleJob`) wi
**Backend Stack** Java 9, Spring Boot 2.5.6 (Spring MVC 5.3.12, Spring Security 5.5.3), Quartz Scheduler 2.3.2
**Frontend** Angular 9.1.4, Web-Socket (stompjs 2.3.3)
**Frontend** Angular 14.2.12, Web-Socket (stompjs 2.3.3)
**Style** Angular Material 9, FontAwesome 5
**Style** Angular Material 14, FontAwesome 5
Starting from Quartz Manager v2.x.x, the new structure of project is:
* Multi-module maven project: REST API backend
* Angular 9: Single Page Application frontend
* Angular 14: Single Page Application frontend
(The first version of quartz manager was a monolithic backend that provided also frontend developed with angularjs 1.6.x. You can find it at the branch 1.x.x)

View File

@@ -0,0 +1,2 @@
config.stopBubbling = true
lombok.addLombokGeneratedAnnotation = true

View File

@@ -10,7 +10,7 @@
<groupId>it.fabioformosa.quartz-manager</groupId>
<artifactId>quartz-manager-parent</artifactId>
<version>4.0.0</version>
<version>4.0.8</version>
<packaging>pom</packaging>
@@ -41,7 +41,6 @@
</developers>
<properties>
<sonar.organization>fabioformosa</sonar.organization>
<java.version>9</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven-surefire-plugin.version>2.22.0</maven-surefire-plugin.version>
@@ -51,8 +50,18 @@
<nexus-staging-maven-plugin.version>1.6.7</nexus-staging-maven-plugin.version>
<maven-release-plugin.version>2.5.3</maven-release-plugin.version>
<maven-gpg-plugin.version>3.0.1</maven-gpg-plugin.version>
<sonar.organization>fabioformosa</sonar.organization>
<sonar.host.url>https://sonarcloud.io</sonar.host.url>
<sonar.exclusions>**/SpringApplicationTest.java, **/QuartManagerApplicationTests.java</sonar.exclusions>
<sonar.exclusions>
**/SpringApplicationTest.java, **/QuartManagerApplicationTests.java, **/MisfireTestJob.java
</sonar.exclusions>
<sonar.coverage.exclusions>
OpenApiConfig.java,
**/SecurityOpenApiConfig.java, **/QuartzModuleProperties.java, **/QuartzManagerDemoApplication.java,
**/ServletInitializer.java, **/SessionController.java, **/HealthCheckController.java,
**/WebShowcaseOpenApiConfig.java, **/MisfireTestJob.java, **/PersistenceConfig.java,
**/QuartzManagerSecurityConfig.java
</sonar.coverage.exclusions>
</properties>
<modules>
@@ -69,27 +78,27 @@
<dependency>
<groupId>it.fabioformosa.quartz-manager</groupId>
<artifactId>quartz-manager-common</artifactId>
<version>4.0.0</version>
<version>4.0.8</version>
</dependency>
<dependency>
<groupId>it.fabioformosa.quartz-manager</groupId>
<artifactId>quartz-manager-starter-api</artifactId>
<version>4.0.0</version>
<version>4.0.8</version>
</dependency>
<dependency>
<groupId>it.fabioformosa.quartz-manager</groupId>
<artifactId>quartz-manager-starter-security</artifactId>
<version>4.0.0</version>
<version>4.0.8</version>
</dependency>
<dependency>
<groupId>it.fabioformosa.quartz-manager</groupId>
<artifactId>quartz-manager-starter-persistence</artifactId>
<version>4.0.0</version>
<version>4.0.8</version>
</dependency>
<dependency>
<groupId>it.fabioformosa.quartz-manager</groupId>
<artifactId>quartz-manager-starter-ui</artifactId>
<version>4.0.0</version>
<version>4.0.8</version>
</dependency>
</dependencies>
</dependencyManagement>
@@ -128,6 +137,19 @@
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>${jacoco-maven-plugin.version}</version>
<configuration>
<excludes>
<exclude>**/OpenApiConfig.class</exclude>
<exclude>**/SecurityOpenApiConfig.class</exclude>
<exclude>**/QuartzModuleProperties.class</exclude>
<exclude>**/QuartzManagerDemoApplication.class</exclude>
<exclude>**/ServletInitializer.class</exclude>
<exclude>**/SessionController.class</exclude>
<exclude>**/HealthCheckController.class</exclude>
<exclude>**/WebShowcaseOpenApiConfig.class</exclude>
<exclude>**/MisfireTestJob.class</exclude>
</excludes>
</configuration>
<executions>
<execution>
<goals>
@@ -141,6 +163,13 @@
<goal>report</goal>
</goals>
</execution>
<execution>
<id>report-aggregate</id>
<goals>
<goal>report-aggregate</goal>
</goals>
<phase>verify</phase>
</execution>
</executions>
</plugin>
@@ -179,17 +208,6 @@
</dependency>
</dependencies>
</plugin>
<plugin>
<groupId>org.sonatype.plugins</groupId>
<artifactId>nexus-staging-maven-plugin</artifactId>
<version>${nexus-staging-maven-plugin.version}</version>
<extensions>true</extensions>
<configuration>
<serverId>ossrh</serverId>
<nexusUrl>https://oss.sonatype.org/</nexusUrl>
<autoReleaseAfterClose>true</autoReleaseAfterClose>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
@@ -233,6 +251,17 @@
</distributionManagement>
<build>
<plugins>
<plugin>
<groupId>org.sonatype.plugins</groupId>
<artifactId>nexus-staging-maven-plugin</artifactId>
<version>${nexus-staging-maven-plugin.version}</version>
<extensions>true</extensions>
<configuration>
<serverId>ossrh</serverId>
<nexusUrl>https://oss.sonatype.org/</nexusUrl>
<autoReleaseAfterClose>true</autoReleaseAfterClose>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-gpg-plugin</artifactId>

View File

@@ -3,7 +3,7 @@
<parent>
<groupId>it.fabioformosa.quartz-manager</groupId>
<artifactId>quartz-manager-parent</artifactId>
<version>4.0.0</version>
<version>4.0.8</version>
</parent>
<artifactId>quartz-manager-common</artifactId>

View File

@@ -1,10 +1,12 @@
package it.fabioformosa.quartzmanager.api.common.properties;
import lombok.Data;
import lombok.Generated;
import java.util.Properties;
@Data
@Generated
public class QuartzModuleProperties {
private Properties properties = new Properties();

View File

@@ -1,13 +1,20 @@
package it.fabioformosa.quartzmanager.api.common.utils;
import lombok.Getter;
import java.util.function.Function;
/**
*
* @param <R> success type
*/
@Getter
public class Try<R> {
private final Throwable failure;
private final R success;
public Try(Throwable failure, R success) {
private Try(Throwable failure, R success) {
this.failure = failure;
this.success = success;
}
@@ -16,11 +23,11 @@ public class Try<R> {
return success;
}
public static <R> Try<R> success(R r){
private static <R> Try<R> success(R r){
return new Try<>(null, r);
}
public static <R> Try<R> failure(Throwable e){
private static <R> Try<R> failure(Throwable e){
return new Try<>(e, null);
}
@@ -38,14 +45,6 @@ public class Try<R> {
return t -> Try.with(checkedFunction).apply(t).getSuccess();
}
public boolean isSuccess(){
return this.failure == null;
}
public boolean isFailure(){
return this.failure != null;
}
@FunctionalInterface
public static interface CheckedFunction<T, R> {
R apply(T t) throws java.lang.Exception;

View File

@@ -5,6 +5,7 @@ import org.junit.jupiter.api.Test;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.Calendar;
import java.util.Date;
class DateUtilsTest {
@@ -17,4 +18,20 @@ class DateUtilsTest {
Assertions.assertThat(convertedLocalDateTime).isEqualTo(originalLocalDateTime);
}
@Test
void givenALocalDatetime_whenTheAddHoursToNowIsCalled_shouldReturnAFutureDate(){
Calendar calendar = Calendar.getInstance();
Date futureDate = DateUtils.addHoursToNow(1);
calendar.add(Calendar.HOUR_OF_DAY, 1);
calendar.add(Calendar.MINUTE, -1);
Date hourStartingAround = calendar.getTime();
calendar.add(Calendar.HOUR_OF_DAY, 1);
calendar.add(Calendar.MINUTE, 2);
Date hourEndingAround = calendar.getTime();
Assertions.assertThat(futureDate).isBetween(hourStartingAround, hourEndingAround);
}
}

View File

@@ -0,0 +1,42 @@
package it.fabioformosa.quartzmanager.api.common.utils;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import java.util.Optional;
class TryTest {
String raiseExceptionIfHello(String greetings) throws Exception {
if("hello".equals(greetings))
throw new Exception("hello");
return greetings;
}
@Test
void givenAFunctionWhichRaisesAnException_whenSneakyThrowIsCalled_thenItReturnsNull(){
String hello = Optional.of("hello").map(Try.sneakyThrow(this::raiseExceptionIfHello)).orElse(null);
Assertions.assertThat(hello).isNull();
}
@Test
void givenAFunctionWhichDoesntRaiseAnException_whenSneakyThrowIsCalled_thenItReturnsTheValue(){
String hello = Optional.of("not hello").map(Try.sneakyThrow(this::raiseExceptionIfHello)).orElse(null);
Assertions.assertThat(hello).isEqualTo("not hello");
}
@Test
void givenAFunctionWhichRaisesAnException_whenTryWithIsCalled_thenItReturnsAFailureObj(){
Try<String> aTry = Optional.of("hello").map(greet -> Try.with(this::raiseExceptionIfHello).apply(greet)).get();
Assertions.assertThat(aTry.getFailure()).isNotNull();
Assertions.assertThat(aTry.getFailure().getMessage()).isEqualTo("hello");
}
@Test
void givenAFunctionWhichDoesntRaiseAnException_whenTryWithIsCalled_thenItReturnsTheValue(){
Try<String> aTry = Optional.of("not hello").map(greet -> Try.with(this::raiseExceptionIfHello).apply(greet)).get();
Assertions.assertThat(aTry.getFailure()).isNull();
Assertions.assertThat(aTry.getSuccess()).isEqualTo("not hello");
}
}

View File

@@ -5,7 +5,7 @@
<parent>
<groupId>it.fabioformosa.quartz-manager</groupId>
<artifactId>quartz-manager-parent</artifactId>
<version>4.0.0</version>
<version>4.0.8</version>
</parent>
<artifactId>quartz-manager-starter-api</artifactId>
@@ -20,7 +20,7 @@
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<springdoc-openapi.version>1.5.12</springdoc-openapi.version>
<java.version>9</java.version>
<sonar.exclusions>**/QuartManagerApplicationTests.java</sonar.exclusions>
<sonar.exclusions>**/QuartManagerApplicationTests.java, **/OpenApiConfig.java</sonar.exclusions>
</properties>
<dependencies>
@@ -106,6 +106,10 @@
<artifactId>reflections</artifactId>
<version>0.10.2</version>
</dependency>
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
</dependency>
<!-- QUARTZ -->
<dependency>
@@ -118,34 +122,6 @@
<version>1.3.2</version>
</dependency>
<!-- Reactor -->
<!-- <dependency>-->
<!-- <groupId>io.projectreactor</groupId>-->
<!-- <artifactId>reactor-core</artifactId>-->
<!-- </dependency>-->
<!-- <dependency>-->
<!-- <groupId>io.projectreactor</groupId>-->
<!-- <artifactId>reactor-net</artifactId>-->
<!-- <version>2.0.8.RELEASE</version>-->
<!-- </dependency>-->
<!-- <dependency>-->
<!-- <groupId>io.projectreactor.spring</groupId>-->
<!-- <artifactId>reactor-spring-context</artifactId>-->
<!-- <version>2.0.7.RELEASE</version>-->
<!-- </dependency>-->
<!-- <dependency>-->
<!-- <groupId>io.netty</groupId>-->
<!-- <artifactId>netty-all</artifactId>-->
<!-- </dependency>-->
<!-- <dependency>-->
<!-- <groupId>org.springframework.boot</groupId>-->
<!-- <artifactId>spring-boot-starter-aop</artifactId>-->
<!-- </dependency>-->
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
</dependency>
<!-- OAS -->
<dependency>
<groupId>org.springdoc</groupId>

View File

@@ -4,6 +4,7 @@ import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.info.License;
import it.fabioformosa.quartzmanager.api.common.config.QuartzManagerPaths;
import lombok.Generated;
import lombok.extern.slf4j.Slf4j;
import org.springdoc.core.GroupedOpenApi;
import org.springdoc.core.customizers.OpenApiCustomiser;
@@ -19,6 +20,7 @@ import java.util.Optional;
@Slf4j
@Configuration
@Generated
public class OpenApiConfig {
@ConditionalOnProperty(name = "quartz-manager.oas.enabled")

View File

@@ -16,10 +16,11 @@ import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import java.util.stream.Collectors;
@RequestMapping(QuartzManagerPaths.QUARTZ_MANAGER_BASE_CONTEXT_PATH + "/jobs")
@RequestMapping(JobController.JOB_CONTROLLER_BASE_URL)
@SecurityRequirement(name = OpenAPIConfigConsts.QUARTZ_MANAGER_SEC_OAS_SCHEMA)
@RestController
public class JobController {
public static final String JOB_CONTROLLER_BASE_URL = QuartzManagerPaths.QUARTZ_MANAGER_BASE_CONTEXT_PATH + "/jobs";
private final JobService jobService;
public JobController(JobService jobService) {

View File

@@ -23,6 +23,5 @@ public class JobKeyToJobDetailDTO extends AbstractBaseConverterToDTO<JobKey, Job
JobDetail jobDetail = scheduler.getJobDetail(jobKey);
jobDetailDTO.setJobClassName(jobDetail.getJobClass().getName());
jobDetailDTO.setDescription(jobDetail.getDescription());
//jobDetail.getJobDataMap();
}
}

View File

@@ -17,7 +17,8 @@ public class SchedulerToSchedulerDTO extends AbstractBaseConverterToDTO<Schedule
protected void convert(Scheduler source, SchedulerDTO target) {
target.setName(source.getSchedulerName());
target.setInstanceId(source.getSchedulerInstanceId());
target.setTriggerKeys(source.getTriggerKeys(GroupMatcher.anyTriggerGroup()));
if(!source.isShutdown())
target.setTriggerKeys(source.getTriggerKeys(GroupMatcher.anyTriggerGroup()));
target.setStatus(buildTheSchedulerStatus(source));
}

View File

@@ -2,6 +2,7 @@ package it.fabioformosa.quartzmanager.api.dto;
import it.fabioformosa.quartzmanager.api.enums.SchedulerStatus;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.quartz.TriggerKey;
@@ -11,6 +12,7 @@ import java.util.Set;
@NoArgsConstructor
@AllArgsConstructor
@Data
@Builder
public class SchedulerDTO {
private String name;
private String instanceId;

View File

@@ -1,7 +1,13 @@
package it.fabioformosa.quartzmanager.api.dto;
import lombok.*;
import java.util.Date;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class TriggerFiredBundleDTO {
private int timesTriggered;
@@ -18,22 +24,6 @@ public class TriggerFiredBundleDTO {
private String jobClass;
public Date getFinalFireTime() {
return finalFireTime;
}
public String getJobClass() {
return jobClass;
}
public String getJobKey() {
return jobKey;
}
public Date getNextFireTime() {
return nextFireTime;
}
public int getPercentage() {
if (this.repeatCount <= 0)
return -1;
@@ -41,44 +31,4 @@ public class TriggerFiredBundleDTO {
* 100);
}
public Date getPreviousFireTime() {
return previousFireTime;
}
public int getRepeatCount() {
return repeatCount;
}
public int getTimesTriggered() {
return timesTriggered;
}
public void setFinalFireTime(Date finalFireTime) {
this.finalFireTime = finalFireTime;
}
public void setJobClass(String jobClass) {
this.jobClass = jobClass;
}
public void setJobKey(String jobKey) {
this.jobKey = jobKey;
}
public void setNextFireTime(Date nextFireTime) {
this.nextFireTime = nextFireTime;
}
public void setPreviousFireTime(Date previousFireTime) {
this.previousFireTime = previousFireTime;
}
public void setRepeatCount(int repeatCount) {
this.repeatCount = repeatCount;
}
public void setTimesTriggered(int timesTriggered) {
this.timesTriggered = timesTriggered;
}
}

View File

@@ -8,11 +8,8 @@ public class ResourceConflictException extends RuntimeException {
private static final long serialVersionUID = 1791564636123821405L;
private final Long resourceId;
public ResourceConflictException(Long resourceId, String message) {
super(message);
this.resourceId = resourceId;
super("Conflict on resourceID " + resourceId + " " + message);
}
}

View File

@@ -1,16 +1,7 @@
package it.fabioformosa.quartzmanager.api.exceptions;
import lombok.Getter;
import lombok.ToString;
@ToString
@Getter
public class TriggerNotFoundException extends Exception {
private final String name;
public TriggerNotFoundException(String name) {
super("Trigger with name " + name + " not found!");
this.name = name;
}
}

View File

@@ -1,5 +1,8 @@
package it.fabioformosa.quartzmanager.api.jobs.entities;
import lombok.Data;
import lombok.ToString;
import java.util.Date;
/**
@@ -8,6 +11,8 @@ import java.util.Date;
* @author Fabio.Formosa
*
*/
@Data
@ToString
public class LogRecord {
public enum LogType {
@@ -27,41 +32,4 @@ public class LogRecord {
date = new Date();
}
public Date getDate() {
return date;
}
public String getMessage() {
return message;
}
public String getThreadName() {
return threadName;
}
public LogType getType() {
return type;
}
public void setDate(Date date) {
this.date = date;
}
public void setMessage(String msg) {
message = msg;
}
public void setThreadName(String threadName) {
this.threadName = threadName;
}
public void setType(LogType type) {
this.type = type;
}
@Override
public String toString() {
return "LogRecord [date=" + date + ", type=" + type + ", message=" + message + "]";
}
}

View File

@@ -1,7 +1,6 @@
package it.fabioformosa.quartzmanager.api.services;
import it.fabioformosa.quartzmanager.api.dto.SimpleTriggerCommandDTO;
import it.fabioformosa.quartzmanager.api.dto.TriggerDTO;
import it.fabioformosa.quartzmanager.api.dto.SimpleTriggerDTO;
import it.fabioformosa.quartzmanager.api.exceptions.TriggerNotFoundException;
import org.quartz.*;
@@ -34,7 +33,7 @@ public class SimpleTriggerService extends AbstractSchedulerService {
return conversionService.convert(newSimpleTrigger, SimpleTriggerDTO.class);
}
public TriggerDTO rescheduleSimpleTrigger(SimpleTriggerCommandDTO triggerCommandDTO) throws SchedulerException {
public SimpleTriggerDTO rescheduleSimpleTrigger(SimpleTriggerCommandDTO triggerCommandDTO) throws SchedulerException {
SimpleTrigger newSimpleTrigger = conversionService.convert(triggerCommandDTO, SimpleTrigger.class);
TriggerKey triggerKey = TriggerKey.triggerKey(triggerCommandDTO.getTriggerName());

View File

@@ -43,7 +43,7 @@ public class WebSocketProgressNotifier implements WebhookSender<TriggerFiredBund
JobDetail jobDetail = jobExecutionContext.getJobDetail();
triggerFiredBundleDTO.setJobKey(jobDetail.getKey().getName());
triggerFiredBundleDTO.setJobClass(trigger.getClass().getSimpleName());
triggerFiredBundleDTO.setJobClass(jobDetail.getJobClass().getName());
return triggerFiredBundleDTO;
}

View File

@@ -7,8 +7,10 @@ import org.quartz.Scheduler;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.scheduling.quartz.SchedulerFactoryBean;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
class SchedulerConfigTest {
@@ -16,11 +18,8 @@ class SchedulerConfigTest {
public static final String QUARTZ_SCHEDULER_DEFAULT_NAME = "QuartzScheduler";
@Test
void givenASchedulerName_whenTheSchedulerIsInstatiated_thenTheSchedulerHasThatName() throws Exception {
QuartzModuleProperties quartzModuleProperties = new QuartzModuleProperties();
quartzModuleProperties.getProperties().put("org.quartz.scheduler.instanceName", TEST_SCHEDULER_NAME);
List<QuartzModuleProperties> quartzModulePropertiesList = new ArrayList<>();
quartzModulePropertiesList.add(quartzModuleProperties);
void givenASchedulerName_whenTheSchedulerIsInstantiated_thenTheSchedulerHasThatName() throws Exception {
List<QuartzModuleProperties> quartzModulePropertiesList = getQuartzModulePropertiesWithASchedulerName(TEST_SCHEDULER_NAME);
SchedulerConfig schedulerConfig = new SchedulerConfig(quartzModulePropertiesList);
GenericApplicationContext applicationContext = new GenericApplicationContext();
applicationContext.refresh();
@@ -31,8 +30,16 @@ class SchedulerConfigTest {
Assertions.assertThat(scheduler.getSchedulerName()).isEqualTo(TEST_SCHEDULER_NAME);
}
private static List<QuartzModuleProperties> getQuartzModulePropertiesWithASchedulerName(String schedulerName) {
QuartzModuleProperties quartzModuleProperties = new QuartzModuleProperties();
quartzModuleProperties.getProperties().put("org.quartz.scheduler.instanceName", schedulerName);
List<QuartzModuleProperties> quartzModulePropertiesList = new ArrayList<>();
quartzModulePropertiesList.add(quartzModuleProperties);
return quartzModulePropertiesList;
}
@Test
void givenNoSchedulerName_whenTheSchedulerIsInstatiated_thenTheSchedulerHasTheDefaultName() throws Exception {
void givenNoSchedulerName_whenTheSchedulerIsInstantiated_thenTheSchedulerHasTheDefaultName() throws Exception {
QuartzModuleProperties quartzModuleProperties = new QuartzModuleProperties();
List<QuartzModuleProperties> quartzModulePropertiesList = new ArrayList<>();
quartzModulePropertiesList.add(quartzModuleProperties);
@@ -46,4 +53,32 @@ class SchedulerConfigTest {
Assertions.assertThat(scheduler.getSchedulerName()).isEqualTo(QUARTZ_SCHEDULER_DEFAULT_NAME);
}
@Test
void givenAManagedProperties_whenTheSchedulerIsInstantiated_thenTheManagedPropsHavePriority() throws Exception {
List<QuartzModuleProperties> quartzModulePropertiesList = getQuartzModulePropertiesWithASchedulerName(TEST_SCHEDULER_NAME);
SchedulerConfig schedulerConfig = new SchedulerConfig(quartzModulePropertiesList);
GenericApplicationContext applicationContext = new GenericApplicationContext();
applicationContext.refresh();
Properties managedProps = new Properties();
String overridden_scheduler_name = "OVERRIDDEN_SCHEDULER_NAME";
managedProps.put("org.quartz.scheduler.instanceName", overridden_scheduler_name);
SchedulerFactoryBean schedulerFactoryBean = schedulerConfig.schedulerFactoryBean(schedulerConfig.jobFactory(applicationContext), managedProps);
schedulerFactoryBean.afterPropertiesSet();
Scheduler scheduler = schedulerFactoryBean.getScheduler();
Assertions.assertThat(scheduler.getSchedulerName()).isEqualTo(overridden_scheduler_name);
}
@Test
void givenAnEmptyManagedPropFile_whenSchedulerConfigRuns_thenItReturnsAnEmptyPropList() throws IOException {
List<QuartzModuleProperties> quartzModulePropertiesList = getQuartzModulePropertiesWithASchedulerName(TEST_SCHEDULER_NAME);
SchedulerConfig schedulerConfig = new SchedulerConfig(quartzModulePropertiesList);
GenericApplicationContext applicationContext = new GenericApplicationContext();
applicationContext.refresh();
Properties properties = schedulerConfig.quartzProperties();
Assertions.assertThat(properties).isEmpty();
}
}

View File

@@ -0,0 +1,46 @@
package it.fabioformosa.quartzmanager.api.controllers;
import it.fabioformosa.quartzmanager.api.QuartManagerApplicationTests;
import it.fabioformosa.quartzmanager.api.controllers.utils.TestUtils;
import it.fabioformosa.quartzmanager.api.jobs.SampleJob;
import it.fabioformosa.quartzmanager.api.services.JobService;
import org.junit.jupiter.api.Test;
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.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
@ContextConfiguration(classes = {QuartManagerApplicationTests.class})
@WebMvcTest(controllers = SimpleTriggerController.class, properties = {
"quartz-manager.jobClassPackages=it.fabioformosa.quartzmanager.jobs"
})
class JobControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private JobService jobService;
@Test
void whenGetListIsCalled_thenTheSimpleJobIsReturned() throws Exception {
Mockito.when(jobService.getJobClasses()).thenReturn(List.of(SampleJob.class));
List<String> expectedJobs = List.of(SampleJob.class.getName());
mockMvc.perform(get(JobController.JOB_CONTROLLER_BASE_URL)
.contentType(MediaType.APPLICATION_JSON)).andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.content().json(TestUtils.toJson(expectedJobs)));
Mockito.verify(jobService, Mockito.times(1)).getJobClasses();
}
}

View File

@@ -0,0 +1,28 @@
package it.fabioformosa.quartzmanager.api.controllers;
import it.fabioformosa.quartzmanager.api.QuartManagerApplicationTests;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
@ContextConfiguration(classes = {QuartManagerApplicationTests.class})
@WebMvcTest(controllers = SimpleTriggerController.class, properties = {
"quartz-manager.jobClassPackages=it.fabioformosa.quartzmanager.jobs"
})
class ResourceConflictControllerTest {
@Autowired
private MockMvc mockMvc;
@Test
void whenAResourceConflictExceptionIsRaised_thenTheExceptionHandlerReturns409() throws Exception {
mockMvc.perform(
MockMvcRequestBuilders.post(TestController.TEST_CONTROLLER_BASE_URL + "/test-conflict")
.contentType(MediaType.APPLICATION_JSON)).andExpect(MockMvcResultMatchers.status().isConflict());
}
}

View File

@@ -0,0 +1,89 @@
package it.fabioformosa.quartzmanager.api.controllers;
import it.fabioformosa.quartzmanager.api.QuartManagerApplicationTests;
import it.fabioformosa.quartzmanager.api.controllers.utils.TestUtils;
import it.fabioformosa.quartzmanager.api.dto.SchedulerDTO;
import it.fabioformosa.quartzmanager.api.enums.SchedulerStatus;
import it.fabioformosa.quartzmanager.api.services.SchedulerService;
import org.junit.jupiter.api.Test;
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.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
@ContextConfiguration(classes = {QuartManagerApplicationTests.class})
@WebMvcTest(controllers = SimpleTriggerController.class, properties = {
"quartz-manager.jobClassPackages=it.fabioformosa.quartzmanager.jobs"
})
class SchedulerControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private SchedulerService schedulerService;
@Test
void whenTheGetIsCalled_thenTheSchedulerServiceIsReturned() throws Exception {
SchedulerDTO schedulerDTO = SchedulerDTO.builder()
.name("TEST_SCHEDULER")
.instanceId("testSchedulerId")
.status(SchedulerStatus.STOPPED)
.build();
Mockito.when(schedulerService.getScheduler()).thenReturn(schedulerDTO);
mockMvc.perform(get(SchedulerController.SCHEDULER_CONTROLLER_BASE_URL)
.contentType(MediaType.APPLICATION_JSON))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.content().json(TestUtils.toJson(schedulerDTO)));
Mockito.verify(schedulerService).getScheduler();
}
@Test
void givenAScheduler_whenTheGetPausedIsCalled_then2xxReturned() throws Exception {
mockMvc.perform(get(SchedulerController.SCHEDULER_CONTROLLER_BASE_URL + "/pause")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(MockMvcResultMatchers.status().isNoContent())
.andExpect(MockMvcResultMatchers.content().string(""));
Mockito.verify(schedulerService).standby();
}
@Test
void givenAScheduler_whenTheGetResumedIsCalled_then2xxReturned() throws Exception {
mockMvc.perform(get(SchedulerController.SCHEDULER_CONTROLLER_BASE_URL + "/resume")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(MockMvcResultMatchers.status().isNoContent())
.andExpect(MockMvcResultMatchers.content().string(""));
Mockito.verify(schedulerService).start();
}
@Test
void givenAScheduler_whenTheGetRunIsCalled_then2xxReturned() throws Exception {
mockMvc.perform(get(SchedulerController.SCHEDULER_CONTROLLER_BASE_URL + "/run")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(MockMvcResultMatchers.status().isNoContent())
.andExpect(MockMvcResultMatchers.content().string(""));
Mockito.verify(schedulerService).start();
}
@Test
void givenAScheduler_whenTheGetStoppedIsCalled_then2xxReturned() throws Exception {
mockMvc.perform(get(SchedulerController.SCHEDULER_CONTROLLER_BASE_URL + "/stop")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(MockMvcResultMatchers.status().isNoContent())
.andExpect(MockMvcResultMatchers.content().string(""));
Mockito.verify(schedulerService).shutdown();
}
}

View File

@@ -0,0 +1,19 @@
package it.fabioformosa.quartzmanager.api.controllers;
import it.fabioformosa.quartzmanager.api.exceptions.ResourceConflictException;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RequestMapping(TestController.TEST_CONTROLLER_BASE_URL)
@RestController
public class TestController {
public static final String TEST_CONTROLLER_BASE_URL = "/test-controller";
@PostMapping("/test-conflict")
public void raiseConflictException(){
throw new ResourceConflictException(1000L, "another entity has found with the same ID");
}
}

View File

@@ -0,0 +1,71 @@
package it.fabioformosa.quartzmanager.api.converters;
import it.fabioformosa.quartzmanager.api.dto.SchedulerDTO;
import it.fabioformosa.quartzmanager.api.enums.SchedulerStatus;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.core.convert.ConversionService;
import org.springframework.test.annotation.DirtiesContext;
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
@SpringBootTest
class SchedulerToSchedulerDTOTest {
@Autowired
private Scheduler scheduler;
@Autowired
private ConversionService conversionService;
@Order(1)
@Test
void givenAnActiveScheduler_whenItIsConverted_thenADtoIsReturned () throws SchedulerException {
Assertions.assertThat(scheduler.isShutdown()).isFalse();
SchedulerDTO schedulerDTO = conversionService.convert(scheduler, SchedulerDTO.class);
Assertions.assertThat(schedulerDTO).isNotNull();
Assertions.assertThat(schedulerDTO.getName()).isEqualTo(scheduler.getSchedulerName());
Assertions.assertThat(schedulerDTO.getInstanceId()).isEqualTo(scheduler.getSchedulerInstanceId());
}
@Order(2)
@Test
void givenAnActiveScheduler_whenItIsConverted_thenADtoHasAStatus () throws SchedulerException {
scheduler.start();
Assertions.assertThat(scheduler.isStarted()).isTrue();
SchedulerDTO schedulerDTO = conversionService.convert(scheduler, SchedulerDTO.class);
Assertions.assertThat(schedulerDTO.getStatus()).isEqualTo(SchedulerStatus.RUNNING);
scheduler.standby();
Assertions.assertThat(scheduler.isInStandbyMode()).isTrue();
schedulerDTO = conversionService.convert(scheduler, SchedulerDTO.class);
Assertions.assertThat(schedulerDTO.getStatus()).isEqualTo(SchedulerStatus.PAUSED);
}
@DirtiesContext
@Order(3)
@Test
void givenASchedulerInShutdown_whenItIsConverted_thenADtoIsReturnedWithNoTriggers () throws SchedulerException {
Assertions.assertThat(scheduler.isShutdown()).isFalse();
scheduler.shutdown(false);
Assertions.assertThat(scheduler.isShutdown()).isTrue();
SchedulerDTO schedulerDTO = conversionService.convert(scheduler, SchedulerDTO.class);
Assertions.assertThat(schedulerDTO).isNotNull();
Assertions.assertThat(schedulerDTO.getName()).isEqualTo(scheduler.getSchedulerName());
Assertions.assertThat(schedulerDTO.getInstanceId()).isEqualTo(scheduler.getSchedulerInstanceId());
Assertions.assertThat(schedulerDTO.getTriggerKeys()).isNull();
Assertions.assertThat(schedulerDTO.getStatus()).isEqualTo(SchedulerStatus.STOPPED);
}
}

View File

@@ -0,0 +1,33 @@
package it.fabioformosa.quartzmanager.api.dto;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import static org.junit.jupiter.api.Assertions.*;
class TriggerFiredBundleDTOTest {
@CsvSource({
"10, 100, 10",
"23, 1000, 2",
"26, 1000, 3"
})
@ParameterizedTest
void givenARepeatCount_whenTheTriggerHasFiredXTimes_thenThePercentageIsCalculatedAccordingly(int timesTriggered, int repeatCount, int expectedPercentage){
TriggerFiredBundleDTO triggerFiredBundleDTO = TriggerFiredBundleDTO.builder().build();
triggerFiredBundleDTO.setTimesTriggered(timesTriggered);
triggerFiredBundleDTO.setRepeatCount(repeatCount);
Assertions.assertThat(triggerFiredBundleDTO.getPercentage()).isEqualTo(expectedPercentage);
}
@Test
void givenAnInfiniteRecursion_whenTheTriggerHasFired10_thenThePercentageIsMinus1(){
TriggerFiredBundleDTO triggerFiredBundleDTO = TriggerFiredBundleDTO.builder().build();
triggerFiredBundleDTO.setTimesTriggered(10);
triggerFiredBundleDTO.setRepeatCount(-1);
Assertions.assertThat(triggerFiredBundleDTO.getPercentage()).isEqualTo(-1);
}
}

View File

@@ -0,0 +1,79 @@
package it.fabioformosa.quartzmanager.api.jobs;
import it.fabioformosa.quartzmanager.api.dto.TriggerFiredBundleDTO;
import it.fabioformosa.quartzmanager.api.jobs.entities.LogRecord;
import it.fabioformosa.quartzmanager.api.websockets.WebhookSender;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.quartz.*;
import static org.mockito.ArgumentMatchers.argThat;
class SampleJobTest {
@InjectMocks
private SampleJob sampleJob;
@Mock
private WebhookSender<TriggerFiredBundleDTO> webSocketProgressNotifier;
@Mock
private WebhookSender<LogRecord> webSocketLogsNotifier;
@BeforeEach
void setUp() {
MockitoAnnotations.openMocks(this);
}
@Test
void givenASampleJob_whenTheJobIsExecuted_thenTheWebhookSendersAreCalled() {
JobExecutionContext jobExecutionContext = Mockito.mock(JobExecutionContext.class);
ScheduleBuilder schedulerBuilder = SimpleScheduleBuilder.simpleSchedule()
.withRepeatCount(5)
.withIntervalInMilliseconds(1000L);
JobDetail jobDetail = JobBuilder
.newJob(SampleJob.class).withIdentity(JobKey.jobKey("test-job"))
.build();
Trigger trigger = TriggerBuilder.newTrigger()
.forJob(jobDetail)
.withSchedule(schedulerBuilder)
.build();
Mockito.when(jobExecutionContext.getTrigger()).thenReturn(trigger);
Mockito.when(jobExecutionContext.getJobDetail()).thenReturn(jobDetail);
sampleJob.execute(jobExecutionContext);
Mockito.verify(webSocketLogsNotifier).send(argThat(actualLogRecord -> {
Assertions.assertThat(actualLogRecord.getMessage()).isEqualTo("Hello!");
Assertions.assertThat(actualLogRecord.getType()).isEqualTo(LogRecord.LogType.INFO);
Assertions.assertThat(actualLogRecord.getDate()).isNotNull();
Assertions.assertThat(actualLogRecord.getThreadName()).isNotNull();
return true;
}));
Mockito.verify(webSocketProgressNotifier).send(argThat(triggerFiredBundleDTO -> {
Assertions.assertThat(triggerFiredBundleDTO.getJobKey()).isEqualTo("test-job");
Assertions.assertThat(triggerFiredBundleDTO.getRepeatCount()).isEqualTo(6);
Assertions.assertThat(triggerFiredBundleDTO.getJobClass()).isEqualTo(SampleJob.class.getName());
Assertions.assertThat(triggerFiredBundleDTO.getTimesTriggered()).isZero();
Assertions.assertThat(triggerFiredBundleDTO.getNextFireTime()).isNull();
Assertions.assertThat(triggerFiredBundleDTO.getPercentage()).isZero();
Assertions.assertThat(triggerFiredBundleDTO.getFinalFireTime()).isNotNull();
Assertions.assertThat(triggerFiredBundleDTO.getPreviousFireTime()).isNull();
return true;
}));
}
@Test
void givenASampleJob_whenTheDoItMethodIsCalled_thenALogRecordIsReturned() {
JobExecutionContext jobExecutionContext = Mockito.mock(JobExecutionContext.class);
LogRecord logRecord = sampleJob.doIt(jobExecutionContext);
Assertions.assertThat(logRecord.getMessage()).isEqualTo("Hello!");
Assertions.assertThat(logRecord.getType()).isEqualTo(LogRecord.LogType.INFO);
Assertions.assertThat(logRecord.getDate()).isNotNull();
}
}

View File

@@ -0,0 +1,57 @@
package it.fabioformosa.quartzmanager.api.services;
import it.fabioformosa.quartzmanager.api.dto.SchedulerDTO;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.DirtiesContext;
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
@SpringBootTest
class SchedulerServiceIntegrationTest {
@Autowired
private SchedulerService schedulerService;
@Autowired
private Scheduler scheduler;
@Test
void givenASchedulerService_whenGetSchedulerIsCalled_thenReturnIt() throws SchedulerException {
SchedulerDTO schedulerDTO = schedulerService.getScheduler();
Assertions.assertThat(schedulerDTO).isNotNull();
Assertions.assertThat(schedulerDTO.getName()).isEqualTo(scheduler.getSchedulerName());
}
@Order(1)
@Test
void givenASchedulerService_whenTheStatusIsChange_thenTheSchedulerReflectsTheSame() throws SchedulerException {
Assertions.assertThat(scheduler.isStarted()).isFalse();
schedulerService.start();
Assertions.assertThat(scheduler.isStarted()).isTrue();
}
@Order(2)
@Test
void givenASchedulerService_whenStandByIsCalled_thenTheStandByIsPropagated() throws SchedulerException {
Assertions.assertThat(scheduler.isInStandbyMode()).isFalse();
schedulerService.standby();
Assertions.assertThat(scheduler.isInStandbyMode()).isTrue();
}
@Order(3)
@DirtiesContext(methodMode = DirtiesContext.MethodMode.AFTER_METHOD)
@Test
void givenASchedulerService_whenShutdownIsCalled_thenTheShutdownIsPropagated() throws SchedulerException {
Assertions.assertThat(scheduler.isShutdown()).isFalse();
schedulerService.start();
schedulerService.shutdown();
Assertions.assertThat(scheduler.isShutdown()).isTrue();
}
}

View File

@@ -0,0 +1,63 @@
package it.fabioformosa.quartzmanager.api.services;
import it.fabioformosa.quartzmanager.api.dto.SchedulerDTO;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.springframework.core.convert.ConversionService;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.MockitoAnnotations.openMocks;
class SchedulerServiceTest {
@InjectMocks
private SchedulerService schedulerService;
@Mock
private Scheduler scheduler;
@Mock
private ConversionService conversionService;
@BeforeEach
void setUp() {
openMocks(this);
}
@Test
void givenASchedulerService_whenGetSchedulerIsCalled_thenReturnIt(){
Mockito.when(conversionService.convert(any(Scheduler.class), eq(SchedulerDTO.class))).thenReturn(SchedulerDTO.builder()
.name("testScheduler")
.build());
SchedulerDTO schedulerDTO = schedulerService.getScheduler();
Assertions.assertThat(schedulerDTO).isNotNull();
Assertions.assertThat(schedulerDTO.getName()).isEqualTo("testScheduler");
}
@Test
void givenASchedulerService_whenStandByIsCalled_thenTheStandByIsPropagated() throws SchedulerException {
schedulerService.standby();
Mockito.verify(scheduler).standby();
}
@Test
void givenASchedulerService_whenShutdownIsCalled_thenTheShutdownIsPropagated() throws SchedulerException {
schedulerService.shutdown();
Mockito.verify(scheduler).shutdown(true);
}
@Test
void givenASchedulerService_whenStarted_thenTheSchedulerIsStarted() throws SchedulerException {
schedulerService.start();
Mockito.verify(scheduler).start();
}
}

View File

@@ -9,9 +9,7 @@ import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.SimpleTrigger;
import org.quartz.*;
import org.springframework.core.convert.ConversionService;
import java.util.Date;
@@ -45,6 +43,21 @@ class SimpleTriggerServiceTest {
Assertions.assertThat(throwable).isInstanceOf(TriggerNotFoundException.class);
}
@Test
void givenAnExistingTrigger_whenGetSimplerTriggerByNameIsCalled_thenTheDtoIsReturned() throws SchedulerException, TriggerNotFoundException {
String existing_trigger = "existing_trigger";
Mockito.when(scheduler.getTrigger(any(TriggerKey.class)))
.thenReturn(TriggerBuilder.newTrigger().withIdentity(existing_trigger).build());
Mockito.when(conversionService.convert(any(SimpleTrigger.class), eq(SimpleTriggerDTO.class)))
.thenReturn(SimpleTriggerDTO.builder()
.triggerKeyDTO(TriggerKeyDTO.builder().name(existing_trigger).build())
.build());
SimpleTriggerDTO simpleTriggerByName = simpleSchedulerService.getSimpleTriggerByName(existing_trigger);
Assertions.assertThat(simpleTriggerByName.getTriggerKeyDTO().getName()).isEqualTo(existing_trigger);
}
@Test
void givenASimpleTriggerCommandDTO_whenASimpleTriggerIsScheduled_thenATriggerDTOIsReturned() throws SchedulerException, ClassNotFoundException {
SimpleTriggerInputDTO triggerInputDTO = SimpleTriggerInputDTO.builder()
@@ -79,4 +92,41 @@ class SimpleTriggerServiceTest {
Assertions.assertThat(simpleTrigger).isEqualTo(expectedTriggerDTO);
}
@Test
void givenASimpleTriggerCommandDTO_whenASimpleTriggerIsRecheduled_thenATriggerDTOIsReturned() throws SchedulerException, ClassNotFoundException {
SimpleTriggerInputDTO triggerInputDTO = SimpleTriggerInputDTO.builder()
.jobClass("it.fabioformosa.quartzmanager.api.jobs.SampleJob")
.startDate(new Date())
.repeatInterval(5000L).repeatCount(5)
.endDate(DateUtils.addHoursToNow(1))
.build();
String simpleTriggerName = "simpleTrigger";
SimpleTriggerDTO expectedTriggerDTO = SimpleTriggerDTO.builder()
.startTime(triggerInputDTO.getStartDate())
.repeatInterval(1000)
.repeatCount(10)
.mayFireAgain(true)
.finalFireTime(triggerInputDTO.getEndDate())
.jobKeyDTO(JobKeyDTO.builder().name("MyJob").build())
.misfireInstruction(SimpleTrigger.MISFIRE_INSTRUCTION_FIRE_NOW)
.triggerKeyDTO(TriggerKeyDTO.builder().name(simpleTriggerName).build())
.build();
Mockito.when(scheduler.rescheduleJob(any(), any())).thenReturn(new Date());
Mockito.when(conversionService.convert(any(), eq(SimpleTriggerDTO.class))).thenReturn(expectedTriggerDTO);
SimpleTriggerCommandDTO simpleTriggerCommandDTO = SimpleTriggerCommandDTO.builder()
.triggerName(simpleTriggerName)
.simpleTriggerInputDTO(triggerInputDTO)
.build();
SimpleTriggerDTO simpleTrigger = simpleSchedulerService.rescheduleSimpleTrigger(simpleTriggerCommandDTO);
Assertions.assertThat(simpleTrigger).isEqualTo(expectedTriggerDTO);
Mockito.verify(scheduler).rescheduleJob(any(), any());
Mockito.verify(conversionService).convert(any(), eq(SimpleTriggerDTO.class));
}
}

View File

@@ -0,0 +1,50 @@
package it.fabioformosa.quartzmanager.api.services;
import it.fabioformosa.quartzmanager.api.dto.TriggerKeyDTO;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.TriggerKey;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.TypeDescriptor;
import java.util.List;
import java.util.Set;
import static org.mockito.ArgumentMatchers.any;
class TriggerServiceTest {
@InjectMocks
private TriggerService triggerService;
@Mock
private Scheduler scheduler;
@Mock
private ConversionService conversionService;
@BeforeEach
void setUp(){
MockitoAnnotations.openMocks(this);
}
@Test
void givenATrigger_whenTheyAreFecthed_TheServiceReturnsTheDtos() throws SchedulerException {
String triggerTestName = "triggerTest";
Mockito.when(scheduler.getTriggerKeys(any())).thenReturn(Set.of(TriggerKey.triggerKey(triggerTestName)));
Mockito.when(conversionService.convert(any(Set.class), any(TypeDescriptor.class), any(TypeDescriptor.class)))
.thenReturn(List.of(TriggerKeyDTO.builder().name(triggerTestName).build()));
List<TriggerKeyDTO> triggerKeyDTOs = triggerService.fetchTriggers();
Assertions.assertThat(triggerKeyDTOs).hasSize(1);
Assertions.assertThat(triggerKeyDTOs.get(0).getName()).isEqualTo(triggerTestName);
}
}

View File

@@ -3,7 +3,7 @@
<parent>
<groupId>it.fabioformosa.quartz-manager</groupId>
<artifactId>quartz-manager-parent</artifactId>
<version>4.0.0</version>
<version>4.0.8</version>
</parent>
<artifactId>quartz-manager-starter-persistence</artifactId>

View File

@@ -4,7 +4,7 @@
<parent>
<groupId>it.fabioformosa.quartz-manager</groupId>
<artifactId>quartz-manager-parent</artifactId>
<version>4.0.0</version>
<version>4.0.8</version>
</parent>
<artifactId>quartz-manager-starter-security</artifactId>

View File

@@ -34,9 +34,6 @@ import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.HttpStatusEntryPoint;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import java.util.ArrayList;
import java.util.List;
@@ -101,7 +98,9 @@ public class QuartzManagerSecurityConfig {
@Order(Ordered.HIGHEST_PRECEDENCE)
@Bean(name = "quartzManagerFilterChain")
public SecurityFilterChain filterChain(HttpSecurity http, @Qualifier("quartzManagerInMemoryAuthentication") InMemoryUserDetailsManager userDetailsService, AuthenticationManager authenticationManager) throws Exception {
public SecurityFilterChain filterChain(HttpSecurity http,
@Qualifier("quartzManagerInMemoryAuthentication") InMemoryUserDetailsManager userDetailsService,
AuthenticationManager authenticationManager) throws Exception {
http.antMatcher(QUARTZ_MANAGER_API_ANT_MATCHER).csrf().disable() //
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and() //
.exceptionHandling().authenticationEntryPoint(restAuthEntryPoint()).and() //
@@ -128,14 +127,6 @@ public class QuartzManagerSecurityConfig {
};
}
@Bean(name = "quartzManagerCorsConfigurationSource")
public CorsConfigurationSource corsConfigurationSource() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration(QUARTZ_MANAGER_API_ANT_MATCHER, new CorsConfiguration().applyPermitDefaultValues());
source.registerCorsConfiguration(QUARTZ_MANAGER_UI_ANT_MATCHER, new CorsConfiguration().applyPermitDefaultValues());
return source;
}
public LoginConfigurer formLoginConfigurer() {
JwtAuthenticationSuccessHandler jwtAuthenticationSuccessHandler = jwtAuthenticationSuccessHandler();
AuthenticationSuccessHandler authenticationSuccessHandler = new AuthenticationSuccessHandler(jwtAuthenticationSuccessHandler);

View File

@@ -11,6 +11,7 @@ import io.swagger.v3.oas.models.security.SecurityScheme;
import it.fabioformosa.quartzmanager.api.common.config.OpenAPIConfigConsts;
import it.fabioformosa.quartzmanager.api.common.config.QuartzManagerPaths;
import it.fabioformosa.quartzmanager.api.security.properties.JwtSecurityProperties;
import lombok.Generated;
import lombok.extern.slf4j.Slf4j;
import org.springdoc.core.customizers.OpenApiCustomiser;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
@@ -24,6 +25,7 @@ import java.util.Arrays;
@Slf4j
@ConditionalOnProperty(name = "quartz-manager.oas.enabled")
@Configuration
@Generated
public class SecurityOpenApiConfig {
@Order(Ordered.HIGHEST_PRECEDENCE)

View File

@@ -20,13 +20,14 @@ import static it.fabioformosa.quartzmanager.api.common.config.QuartzManagerPaths
@RequestMapping(value = QUARTZ_MANAGER_AUTH_PATH, produces = MediaType.APPLICATION_JSON_VALUE)
public class UserController {
public static final String WHOAMI_URL = "/whoami";
@GetMapping("/whoami")
@GetMapping(WHOAMI_URL)
public ResponseEntity<Object> getLoggedUser() {
SecurityContext context = SecurityContextHolder.getContext();
if (context != null && context.getAuthentication() != null)
return new ResponseEntity<>(context.getAuthentication().getPrincipal(), HttpStatus.OK);
return new ResponseEntity<>(null, HttpStatus.NOT_FOUND);
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
}

View File

@@ -1,51 +0,0 @@
package it.fabioformosa.quartzmanager.api.security.helpers.impl;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.WebAttributes;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
public class AjaxAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
public class AjaxLoginAuthSuccessHandler extends SimpleUrlAuthenticationSuccessHandler
implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) {
response.setStatus(HttpServletResponse.SC_OK);
super.clearAuthenticationAttributes(request);
}
}
public AjaxAuthenticationFilter(AuthenticationManager authenticationManager) {
setAuthenticationManager(authenticationManager);
setUsernameParameter("ajaxUsername");
setPasswordParameter("ajaxPassword");
setPostOnly(true);
setFilterProcessesUrl("/ajaxLogin");
setAuthenticationSuccessHandler(new AjaxLoginAuthSuccessHandler());
}
/**
* Removes temporary authentication-related data which may have been stored
* in the session during the authentication process.
*/
protected final void clearAuthenticationAttributes(HttpServletRequest request) {
HttpSession session = request.getSession(false);
if (session == null)
return;
session.removeAttribute(WebAttributes.AUTHENTICATION_EXCEPTION);
}
}

View File

@@ -1,7 +1,9 @@
package it.fabioformosa.quartzmanager.api.security.helpers.impl;
import lombok.EqualsAndHashCode;
import org.springframework.security.authentication.AbstractAuthenticationToken;
@EqualsAndHashCode
public class AnonAuthentication extends AbstractAuthenticationToken {
private static final long serialVersionUID = 1L;
@@ -9,15 +11,6 @@ public class AnonAuthentication extends AbstractAuthenticationToken {
super( null );
}
@Override
public boolean equals( Object obj ) {
if ( this == obj )
return true;
if ( obj == null )
return false;
return getClass() == obj.getClass();
}
@Override
public Object getCredentials() {
return null;
@@ -28,11 +21,6 @@ public class AnonAuthentication extends AbstractAuthenticationToken {
return null;
}
@Override
public int hashCode() {
return 7;
}
@Override
public boolean isAuthenticated() {
return true;

View File

@@ -1,24 +1,38 @@
package it.fabioformosa.quartzmanager.api.security.properties;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.Getter;
import lombok.Setter;
import org.apache.commons.lang3.RandomStringUtils;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import java.security.SecureRandom;
import java.util.Base64;
@Configuration
@ConfigurationProperties(prefix = "quartz-manager.security.jwt")
@Data
@AllArgsConstructor
@Getter
@Setter
public class JwtSecurityProperties {
private String secret = RandomStringUtils.randomAlphabetic(10);
private String secret;
private long expirationInSec = 28800;
private CookieStrategy cookieStrategy = new CookieStrategy();
private HeaderStrategy headerStrategy = new HeaderStrategy();
public JwtSecurityProperties() {
SecureRandom random = new SecureRandom();
byte[] bytes = new byte[20];
random.nextBytes(bytes);
Base64.Encoder encoder = Base64.getUrlEncoder().withoutPadding();
secret = encoder.encodeToString(bytes);
}
@Data
public static class CookieStrategy {
private boolean enabled = false;

View File

@@ -2,6 +2,8 @@ package it.fabioformosa.quartzmanager.api.security;
import it.fabioformosa.quartzmanager.api.common.config.QuartzManagerPaths;
import it.fabioformosa.quartzmanager.api.security.controllers.TestController;
import it.fabioformosa.quartzmanager.api.security.properties.JwtSecurityProperties;
import org.assertj.core.api.Assertions;
import org.hamcrest.core.IsNot;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
@@ -19,9 +21,8 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
@SpringBootTest
@AutoConfigureMockMvc
@TestPropertySource(properties = {
"quartz-manager.security.jwt.enabled=true",
"quartz-manager.security.jwt.secret=bibidibobidiboo",
"quartz-manager.security.jwt.expiration-in-sec=28800",
"quartz-manager.security.jwt.expiration-in-sec=36000",
"quartz-manager.security.jwt.header-strategy.enabled=false",
"quartz-manager.security.jwt.header-strategy.header=Authorization",
"quartz-manager.security.jwt.cookie-strategy.enabled=true",
@@ -31,11 +32,14 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
"quartz-manager.security.accounts.in-memory.users[0].password=bar",
"quartz-manager.security.accounts.in-memory.users[0].roles[0]=admin",
})
public class SecurityControllerTest {
class SecurityControllerTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private JwtSecurityProperties jwtSecurityProperties;
@Test
void givenAnAnonymousUser_whenCalledADMZController_thenShouldRaiseForbidden() throws Exception {
mockMvc.perform(MockMvcRequestBuilders.get("/dmz"))
@@ -72,4 +76,10 @@ public class SecurityControllerTest {
.andExpect(status().isOk());
}
@Test
void givenSecurityProps_whenTheBootstrapHasCompleted_thenJWTPropertiesShouldBeSetAccordingly() throws Exception {
Assertions.assertThat(jwtSecurityProperties.getExpirationInSec()).isEqualTo(36000);
Assertions.assertThat(jwtSecurityProperties.getSecret()).isEqualTo("bibidibobidiboo");
}
}

View File

@@ -13,7 +13,6 @@ import org.springframework.test.context.TestPropertySource;
@TestPropertySource(properties = {
"quartz-manager.security.login-model.form-login-enabled = false",
"quartz-manager.security.login-model.userpwd-filter-enabled = true",
"quartz-manager.security.jwt.enabled=true",
"quartz-manager.security.jwt.secret=bibidibobidiboo",
"quartz-manager.security.jwt.expiration-in-sec=28800",
"quartz-manager.security.jwt.header-strategy.enabled=true",

View File

@@ -12,7 +12,6 @@ import org.springframework.test.context.TestPropertySource;
@TestPropertySource(properties = {
"quartz-manager.security.login-model.form-login-enabled = true",
"quartz-manager.security.login-model.userpwd-filter-enabled = false",
"quartz-manager.security.jwt.enabled=true",
"quartz-manager.security.jwt.secret=bibidibobidiboo",
"quartz-manager.security.jwt.expiration-in-sec=28800",
"quartz-manager.security.jwt.header-strategy.enabled=true",

View File

@@ -0,0 +1,41 @@
package it.fabioformosa.quartzmanager.api.security.controllers;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import static it.fabioformosa.quartzmanager.api.common.config.QuartzManagerPaths.QUARTZ_MANAGER_AUTH_PATH;
import static it.fabioformosa.quartzmanager.api.security.controllers.UserController.WHOAMI_URL;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@SpringBootTest
@AutoConfigureMockMvc
@TestPropertySource(properties = {
"quartz-manager.security.accounts.in-memory.enabled=true",
"quartz-manager.security.accounts.in-memory.users[0].username=admin",
"quartz-manager.security.accounts.in-memory.users[0].password=admin",
"quartz-manager.security.accounts.in-memory.users[0].roles[0]=admin",
})
class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@Test
@WithMockUser("admin")
void givenAnUser_whenCalledTheWhoamiEndpoint_thenShouldReturn2xx() throws Exception {
mockMvc.perform(MockMvcRequestBuilders.get(QUARTZ_MANAGER_AUTH_PATH + WHOAMI_URL))
.andExpect(status().isOk());
}
@Test
void givenAnAnonymousUser_whenCalledTheWhoamiEndpoint_thenShouldReturnNotFound() throws Exception {
mockMvc.perform(MockMvcRequestBuilders.get(QUARTZ_MANAGER_AUTH_PATH + WHOAMI_URL))
.andExpect(status().isUnauthorized());
}
}

View File

@@ -0,0 +1,39 @@
package it.fabioformosa.quartzmanager.api.security.properties;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.springframework.boot.context.properties.bind.BindResult;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.boot.context.properties.source.ConfigurationPropertySource;
import org.springframework.boot.context.properties.source.MapConfigurationPropertySource;
import javax.validation.Validation;
import javax.validation.Validator;
import java.lang.reflect.InvocationTargetException;
import java.util.Map;
public abstract class AbstractPropertyValidatorTest {
protected static Validator propertyValidator;
@BeforeAll
public static void setup() {
propertyValidator = Validation.buildDefaultValidatorFactory().getValidator();
}
protected static <T> T inflateConfigurationPropertyFromAMap(Map<String, String> properties, String configurationPropName, Class<T> propClass) {
ConfigurationPropertySource source = new MapConfigurationPropertySource(properties);
Binder binder = new Binder(source);
BindResult<T> result = binder.bind(configurationPropName, propClass);
if (properties != null && !properties.isEmpty()) {
Assertions.assertThat(result.isBound()).isTrue();
T configPropObject = result.get();
return configPropObject;
} else {
try {
return propClass.getConstructor().newInstance();
} catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
throw new RuntimeException(e);
}
}
}
}

View File

@@ -1,25 +1,16 @@
package it.fabioformosa.quartzmanager.api.security.properties;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.springframework.boot.context.properties.bind.BindResult;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.boot.context.properties.source.ConfigurationPropertySource;
import org.springframework.boot.context.properties.source.MapConfigurationPropertySource;
import javax.validation.Validation;
import javax.validation.Validator;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Stream;
public class InMemoryUsersValidationControllerTest {
private static Validator propertyValidator;
class InMemoryUsersValidationControllerTest extends AbstractPropertyValidatorTest {
static Stream<Arguments> notValidInMemoryProps = Stream.of(
Arguments.of(
@@ -34,26 +25,15 @@ public class InMemoryUsersValidationControllerTest {
);
@BeforeAll
public static void setup() {
propertyValidator = Validation.buildDefaultValidatorFactory().getValidator();
}
static Stream<Arguments> getNotValidInMemoryProps(){
static Stream<Arguments> getNotValidInMemoryProps() {
return notValidInMemoryProps;
}
@ParameterizedTest
@MethodSource("it.fabioformosa.quartzmanager.api.security.properties.InMemoryUsersValidationControllerTest#getNotValidInMemoryProps")
void givenAMissingUsername_whenThePropertyValidationIsApplied_thenShouldRaiseValidationError(Map<String, String> properties) {
ConfigurationPropertySource source = new MapConfigurationPropertySource(properties);
Binder binder = new Binder(source);
BindResult<InMemoryAccountProperties> result = binder.bind("quartz-manager.security.accounts.in-memory", InMemoryAccountProperties.class);
Assertions.assertThat(result.isBound()).isTrue();
InMemoryAccountProperties inMemoryAccountProperties = result.get();
InMemoryAccountProperties inMemoryAccountProperties = inflateConfigurationPropertyFromAMap(properties,
"quartz-manager.security.accounts.in-memory", InMemoryAccountProperties.class);
Assertions.assertThat(propertyValidator.validate(inMemoryAccountProperties)).isNotEmpty();
}
@@ -65,14 +45,9 @@ public class InMemoryUsersValidationControllerTest {
properties.put("quartz-manager.security.accounts.in-memory.users[0].password", "bar");
properties.put("quartz-manager.security.accounts.in-memory.users[0].roles[0]", "admin");
ConfigurationPropertySource source = new MapConfigurationPropertySource(properties);
InMemoryAccountProperties inMemoryAccountProperties = inflateConfigurationPropertyFromAMap(properties,
"quartz-manager.security.accounts.in-memory", InMemoryAccountProperties.class);
Binder binder = new Binder(source);
BindResult<InMemoryAccountProperties> result = binder.bind("quartz-manager.security.accounts.in-memory", InMemoryAccountProperties.class);
Assertions.assertThat(result.isBound()).isTrue();
InMemoryAccountProperties inMemoryAccountProperties = result.get();
Assertions.assertThat(propertyValidator.validate(inMemoryAccountProperties)).isEmpty();
}

View File

@@ -0,0 +1,39 @@
package it.fabioformosa.quartzmanager.api.security.properties;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import java.util.HashMap;
import java.util.Map;
class JwtSecurityPropertiesTest extends AbstractPropertyValidatorTest {
@Test
void givenAllJWTSecurityPropSet_whenThePropertyValidationIsApplied_thenShouldBeValid() {
Map<String, String> properties = new HashMap<>();
String secret = "helloworld";
properties.put("quartz-manager.security.jwt.secret", secret);
String expirationInSec = "36000";
properties.put("quartz-manager.security.jwt.expirationInSec", expirationInSec);
JwtSecurityProperties jwtSecurityProperties = inflateConfigurationPropertyFromAMap(properties,
"quartz-manager.security.jwt", JwtSecurityProperties.class);
Assertions.assertThat(propertyValidator.validate(jwtSecurityProperties)).isEmpty();
Assertions.assertThat(jwtSecurityProperties.getExpirationInSec()).isEqualTo(Long.valueOf(expirationInSec));
Assertions.assertThat(jwtSecurityProperties.getSecret()).isEqualTo(secret);
}
@Test
void givenTheMandatoryJWTSecurityPropUnset_whenThePropertyValidationIsApplied_thenShouldBeSetWithDefault() {
Map<String, String> properties = new HashMap<>();
JwtSecurityProperties jwtSecurityProperties = inflateConfigurationPropertyFromAMap(properties,
"quartz-manager.security.jwt", JwtSecurityProperties.class);
Assertions.assertThat(jwtSecurityProperties.getExpirationInSec()).isEqualTo(28800L);
Assertions.assertThat(jwtSecurityProperties.getSecret()).isNotBlank();
}
}

View File

@@ -4,7 +4,7 @@
<parent>
<groupId>it.fabioformosa.quartz-manager</groupId>
<artifactId>quartz-manager-parent</artifactId>
<version>4.0.0</version>
<version>4.0.8</version>
</parent>
<artifactId>quartz-manager-starter-ui</artifactId>
@@ -19,8 +19,8 @@
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>9</java.version>
<frontend.folderName>quartz-manager-frontend</frontend.folderName>
<node.version>v10.16.3</node.version>
<npm.version>6.9.0</npm.version>
<node.version>v16.14.1</node.version>
<npm.version>8.19.3</npm.version>
</properties>
<dependencies>

View File

@@ -5,7 +5,7 @@
<parent>
<groupId>it.fabioformosa.quartz-manager</groupId>
<artifactId>quartz-manager-parent</artifactId>
<version>4.0.0</version>
<version>4.0.8</version>
</parent>
<artifactId>quartz-manager-web-showcase</artifactId>
@@ -31,10 +31,10 @@
<groupId>it.fabioformosa.quartz-manager</groupId>
<artifactId>quartz-manager-starter-ui</artifactId>
</dependency>
<!-- <dependency>-->
<!-- <groupId>it.fabioformosa.quartz-manager</groupId>-->
<!-- <artifactId>quartz-manager-starter-security</artifactId>-->
<!-- </dependency>-->
<dependency>
<groupId>it.fabioformosa.quartz-manager</groupId>
<artifactId>quartz-manager-starter-security</artifactId>
</dependency>
<!-- <dependency>-->
<!-- <groupId>it.fabioformosa.quartz-manager</groupId>-->
<!-- <artifactId>quartz-manager-starter-persistence</artifactId>-->
@@ -129,12 +129,12 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>

View File

@@ -1,12 +1,14 @@
package it.fabioformosa;
import lombok.Generated;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@Generated
@SpringBootApplication
public class QuartManagerDemoApplication {
public class QuartzManagerDemoApplication {
public static void main(String[] args) {
SpringApplication.run(QuartManagerDemoApplication.class, args);
SpringApplication.run(QuartzManagerDemoApplication.class, args);
}
}

View File

@@ -1,5 +1,6 @@
package it.fabioformosa;
import lombok.Generated;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
@@ -9,11 +10,12 @@ import org.springframework.boot.web.servlet.support.SpringBootServletInitializer
* @author Fabio Formosa
*
*/
@Generated
public class ServletInitializer extends SpringBootServletInitializer {
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
return application.sources(QuartManagerDemoApplication.class);
return application.sources(QuartzManagerDemoApplication.class);
}
}

View File

@@ -3,11 +3,13 @@ package it.fabioformosa;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.info.License;
import lombok.Generated;
import org.springdoc.core.GroupedOpenApi;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@Generated
public class WebShowcaseOpenApiConfig {
@Bean

View File

@@ -2,6 +2,7 @@ package it.fabioformosa.quartzmanager.controllers;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.Operation;
import lombok.Generated;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.GetMapping;
@@ -13,6 +14,7 @@ import org.springframework.web.bind.annotation.RestController;
@Hidden
@RestController
@RequestMapping
@Generated
public class HealthCheckController {
@ResponseStatus(code = HttpStatus.OK)

View File

@@ -1,6 +1,7 @@
package it.fabioformosa.quartzmanager.controllers;
import io.swagger.v3.oas.annotations.Operation;
import lombok.Generated;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpEntity;
@@ -15,12 +16,12 @@ import javax.servlet.http.HttpSession;
@Controller
@RequestMapping("/session")
@Generated
public class SessionController {
private final Logger log = LoggerFactory.getLogger(SessionController.class);
@GetMapping("/invalidate")
//@PreAuthorize("hasAuthority('ADMIN')") TODO
@ResponseStatus(HttpStatus.NO_CONTENT)
@Operation(hidden = true)
public void invalidateSession(HttpSession session) {
@@ -29,7 +30,6 @@ public class SessionController {
}
@GetMapping("/refresh")
// @PreAuthorize("hasAuthority('ADMIN')") TODO
@Operation(hidden = true)
public HttpEntity<Void> refreshSession(HttpSession session) {
return new ResponseEntity<>(HttpStatus.OK);

View File

@@ -2,9 +2,10 @@ package it.fabioformosa.quartzmanager.jobs.tests;
import it.fabioformosa.quartzmanager.api.jobs.AbstractQuartzManagerJob;
import it.fabioformosa.quartzmanager.api.jobs.entities.LogRecord;
import lombok.Generated;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.quartz.JobExecutionContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This job can be used to test the misfire policy. It pretends to be a long
@@ -13,20 +14,27 @@ import org.slf4j.LoggerFactory;
* @author Fabio.Formosa
*
*/
@Slf4j
@NoArgsConstructor
@Generated
public class MisfireTestJob extends AbstractQuartzManagerJob {
private Logger log = LoggerFactory.getLogger(MisfireTestJob.class);
private long sleepPeriodInMs = 10 * 1000L;
public MisfireTestJob(long sleepPeriodInMs) {
this.sleepPeriodInMs = sleepPeriodInMs;
}
@Override
public LogRecord doIt(JobExecutionContext jobExecutionContext) {
try {
log.info("{} is going to sleep...", Thread.currentThread().getName());
Thread.sleep(10 * 1000);
Thread.sleep(sleepPeriodInMs);
log.info("{} woke up!", Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
throw new IllegalStateException(e);
}
return new LogRecord(LogRecord.LogType.INFO, "Hello!");

View File

@@ -4,12 +4,12 @@ import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.web.WebAppConfiguration;
@SpringBootTest(classes = QuartManagerDemoApplication.class)
@SpringBootTest(classes = QuartzManagerDemoApplication.class)
@WebAppConfiguration
public class QuartManagerApplicationTests {
class QuartManagerApplicationTests {
@Test
public void contextLoads() {
void contextLoads() {
}
}

View File

@@ -0,0 +1,18 @@
package it.fabioformosa.quartzmanager.jobs.tests;
import it.fabioformosa.quartzmanager.api.jobs.entities.LogRecord;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
class MisfireTestJobTest {
@Test
void givenAMisfireTestJob_whenIsExecuted_shoulReturnALogRecord() {
MisfireTestJob misfireTestJob = new MisfireTestJob(10L);
LogRecord logRecord = misfireTestJob.doIt(null);
Assertions.assertThat(logRecord.getMessage()).isEqualTo("Hello!");
}
}