This quick walkthrough effectively aims for the following objectives:
Follow the instructions posted here to obtain the CAS source code. Remember to indicate the relevant branch
in the commands indicated to obtain the right source code for the CAS version at hand. In this tutorial and just like before, the branch to use would be 6.3.x
(at the time of writing this post, the appropriate branch is master
).
To understand what branches are available, see this link. Your CAS version is closely tied to the branches listed in the codebase. For example, if you are deploying CAS 5.1.8
, then the relevant branch to check out would be 5.1.x
. Remember that branches always contain the most recent changeset and version of the release line. You might be deploying 5.1.8
while the 5.1.x
might be marching towards 5.1.10
. This requires that you first upgrade to the latest available patch release for the CAS version at hand and if the problem or use case continues to manifest, you can then check out the appropriate source branch and get fancy [1].
It is important that to let IntelliJ IDEA open and refresh the Gradle project (using the Refresh button on the Gradle window’s toolbar) once you do the initial import. Running ./gradlew idea
MAY work but it may also completely mess up the project structure especially if the plugin is not quite compatible with your IDE version. Note that similar tasks are available for eclipse.
For best results, try with IntelliJ IDEA 2020.2
(Ultimate Edition). Given the size of the CAS projects and the number of sub-modules, you need to make sure you have enough memory available for IDEA and that your custom JVM settings are correctly set per the instructions here for IntelliJ IDEA.
It’s best to get familiar with CAS system requirements. Most importantly, this means that your system must be prepped with JDK 11
. Just about any JDK variant from any JDK vendor would do the job.
For basic development and prototyping, try with:
java -version
java version "11.0.9" 2020-10-20 LTS
Java(TM) SE Runtime Environment 18.9 (build 11.0.9+7-LTS)
Java HotSpot(TM) 64-Bit Server VM 18.9 (build 11.0.9+7-LTS, mixed mode)
The CAS web application itself can be started from the command prompt using an embedded Apache Tomcat container. In fact, this process is no different from deploying CAS using the same embedded Apache Tomcat container which means you will need to follow the instructions posted here in the way that certificates and other configurations are needed in /etc/cas/config
, etc to ensure CAS can function as you need it. All feature modules and behavior that would be stuffed into the web application artifact continue to read settings from the same location, as they would be when activated from an overlay. The process is the same.
I use the following alias in my bash profile to spin up CAS using an embedded Apache Tomcat container. You might want to do the same thing:
alias bc='clear; cd ~/Workspace/cas/webapp/cas-server-webapp-tomcat; \
../../gradlew build bootRun --configure-on-demand --build-cache --parallel \
-x test -x javadoc -x check -DenableRemoteDebugging=true --stacktrace \
-DskipNestedConfigMetadataGen=true'
Then, I simply execute the following in the terminal:
> bc
Since you’re running bootRun
under the cas-server-webapp-tomcat
module, the servlet container used will be based on Apache Tomcat. You can navigate to a different module that bases itself on top of a different servlet container such as Jetty.
bc.bat
file and making sure it's available on the PATH
. The syntax of course needs to be adjusted to account for file paths and commands.To understand the meaning and function behind various command-line arguments, please see instructions posted here. You may optionally decide to tweak each setting if you are interested in a particular build variant, such as generating javadocs, running tests, etc. One particular flag of interest is the addition of enableRemoteDebugging
, which allows you, later on, to connect a remote debugger to CAS on a specific port (i.e. 5000
) and step into the code. More on that later.
Per instructions posted here, the inclusion of a particular build module in the Gradle build script of the CAS web application should allow the build process to automatically allow the module to be packaged and become available to the runtime. You may include the module reference in the webapp.gradle
file, which is the common parent to build descriptors that do stuff with CAS web applications. Making changes in this file will ensure that it will be included by default in the generic CAS web application, regardless of how it is configured to run using a servlet container, which means you need to be extra careful about the sort of changes you make here and what is kept and what is checked in for follow-up pull requests and reviews.
So for reference and our task at hand, the file would look like the following:
dependencies {
...
implementation project(":support:cas-server-support-some-module")
...
}
Note the reference locates the module using its full path. The next time you run bc
, the final CAS web application will have enabled some-module
functionality when it’s booting up inside Apache Tomcat allowing you to make changes to the said module and begin testing. The same command, bc
, can be used over and over again to run CAS locally and test the change until the desired functionality is there.
Once done, you may then commit the change to a relevant branch (of your fork, which is something you should have done earlier when you cloned the codebase) and push upstream (again, to your fork) in order to prepare a pull request and send in the change targeted at the right destination branch. More info on that workflow is available here.
If you’d rather not modify the webapp.gradle
file to specify modules, you can try to pass those to the build directly from the command line. To do so, modify the bc
alias to include the casModules
property as such:
alias bc='clear; cd ~/Workspace/cas/webapp/cas-server-webapp-tomcat; \
... \
... \
-PcasModules="$1"
Such that you can invoke the command as:
bc gauth,duo
One of the very useful things you can include in your build is the ability to allow for remote debugging via -DenableRemoteDebugging=true
. Both IntelliJ IDEA and eclipse allow you ways to connect to a port remotely and activate a debugger to step into the code and troubleshoot. This is hugely useful, especially in cases where you can make a change to a source file and rebuild the component live hot-reloading the .class
file to allow the changes to kick in the very next time execution passes through without restarting the servlet container. Depending on how significant the change is, this should save you quite a bit of time.
There are also many fancier tools such as JRebel that let you do the same with a lot more power and flexibility.
The remote debugging port by default is 5000
and should be auto-incremented in case the port is busy or occupied by some other process. You should get notices and prompts from the build, if and when that happens.
A very useful flag that you may consider adding to your shell alias is -DremoteDebuggingSuspend=true
, which allows you to suspend the JVM until a debugger tool is attached to the running process. This is handy in situations where you need to debug and troubleshoot a particular component or behavior that executes early during startup (i.e. fetching CAS configuration settings or servlet container bootstrapping) and you don’t want the runtime to proceed too quickly and forcing you to miss the troubleshooting window.
With the inclusion of this new flag, the build outcome sort of looks like this:
> Task :webapp:cas-server-webapp-tomcat:bootRun
Listening for transport dt_socket at address: 5000
Sometimes, it’s useful to test the new change from the perspective of the CAS Overlay. While the behavior should be identical, this step can be used in quick smoke tests and to ensure the proper set of dependencies and modules are published and installed correctly and picked up by the overlay build process without any conflicts or duplicates.
To publish and install CAS artifacts locally, you may try the following:
# Build CAS and install...
alias bci='clear; cd ~/Workspace/cas \
./gradlew clean build install --configure-on-demand \
--build-cache --parallel \
-x test -x javadoc -x check --stacktrace \
-DskipNestedConfigMetadataGen=true \
-DskipBootifulArtifact=true'
Be patient. This might take some time.
A rather important flag in the above build is -DskipBootifulArtifact=true
. This stops the Gradle build from applying the Spring Boot plugin to bootify application components, mainly the various CAS web application artifacts. This is required because the CAS Overlay needs to operate on a vanilla web application untouched by Spring Boot plugins (a.k.a non-bootiful) before it can explode and repackage it with Spring Boot. Note that the CAS build and release processes automatically take this flag into account when snapshots or releases are published and more conveniently, whether you are working on the CAS codebase or overlay, you get to work with the same bootiful web application without any extra hassle.
Once the artifacts are successfully installed, you can pick up the -SNAPSHOT
artifacts in overlay by changing the CAS version and resume testing.
Ideally, changes that are introduced need to be tested using either simple unit tests or integration tests.
5.x
to design and execute tests. All test cases need to be aligned with the specific requirements of the JUnit framework properly before they can pass. This means the correct use of package imports, assertions, method lifecycle annotations, etc.Writing unit tests is rather easy. If you have added a few changes to src/main/java/org/apereo/cas/SomeCasComponent.java
, you will need to create the corresponding test component under src/test/java/org/apereo/cas/SomeCasComponentTests.java
for the build to identify and execute it. The outline of SomeCasComponentTests
would look something like this:
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest(classes = {
SomeCasComponentConfiguration.class
})
@TestPropertySource(properties = {"cas.some.property=value"})
@EnableConfigurationProperties(CasConfigurationProperties.class)
public class SomeCasComponentTests {
/*
Injected via SomeCasComponentConfiguration...
*/
@Autowired
@Qualifier("someCasComponent")
private SomeCasComponent someCasComponent;
@BeforeEach
public void initialize() {
}
@Test
public void verifyStuffHappens() throws Exception {
// Invoke someCasComponent.someMethod() and examine/assert the output
}
}
In short, you need to make sure all the correct Configuration classes are included to bootstrap the build so that the required objects can be injected into the test class at runtime. You may also need to include additional modules and dependencies in the build.gradle
file of the project to make sure the test runner has access to all required classes:
dependencies {
testImplementation project(":support:cas-server-support-xyz")
testImplementation project(path: ":support:cas-server-support-xyz", configuration: "tests")
}
@Tag("Groovy")
).@EnabledIfContinuousIntegration
. The opposite is also possible using @DisabledIfContinuousIntegration
.For best examples, scan the codebase to find similar test classes and try to follow the same pattern and structure as others to keep things as consistent as possible.
If the change you are working on has a dependency on an external system such as a REST API or SQL database, you will need to make sure the test class is categorized appropriately. For example, let’s assume that SomeCasComponentTests
requires an external Redis NoSQL database which means that your test class should indicate this as such:
@Tag("Redis")
@EnabledIfContinuousIntegration
public class SomeCasComponentTests {
...
}
Note that the test execution would always fail if the Redis database isn’t installed, running and configured correctly for everyone else working on the same CAS codebase. To work around this, we have also added a condition for the test runner to only execute the test when the CAS CI environment is handling the test execution. The CI environment, given the appropriate category, will bootstrap and initialize the required dependencies and systems (typically via Docker) for the tests to execute which allows you to run the tests locally with a (Redis) database of your own while allowing the CI process to handle the test execution all the same, automatically and with the needed external dependencies.
Again, for better examples simply scan the codebase to find similar test classes.
Our Gradle test commands need to be slightly modified to only run the tests that need to run based on the category of interest. For example, to run all Redis-related tests our test command would look like this:
clear
cd ~/Workspace/cas
./gradlew clean testRedis -x test -x javadoc \
--build-cache --configure-on-demand
-x check --parallel -DskipNestedConfigMetadataGen=true \
-DskipNestedConfigMetadataGen=true'
Or, to run simple unit tests our test command would look like this:
clear
cd ~/Workspace/cas
./gradlew clean testSimple -x javadoc \
--build-cache --configure-on-demand
-x check --parallel -DskipNestedConfigMetadataGen=true \
-DskipNestedConfigMetadataGen=true'
To make things more comfortable, CAS ships with a ready-made script that handles all of this for you:
./testcas.sh --help
Usage: ./testcas.sh --category [category1,category2,...] [--help] \
[--ignore-failures] [--no-wrapper] [--no-retry] [--debug] [--coverage]
Available test categories are:
simple, memcached,cassandra,groovy,kafka,ldap,rest,mfa,jdbc,mssql, \
oracle,radius,couchdb,mariadb,files,postgres,dynamodb,couchbase,uma, \
saml,mail,aws,activemq,oauth,oidc,redis,webflow,mongo,\
ignite,influxdb,zookeeper,mysql
Please see the test script for details.
As an example, let CAS run all basic unit tests:
./testcas.sh --category simple
Run all integration tests that are tagged as Mail
:
./testcas.sh --category mail
Run all MySQL tests that belong to the SomeJpaTests
component and enable remote debugging over port 5005
without using the Gradle wrapper:
./testcas.sh --category mysql --debug --no-wrapper
I hope this review was of some help to you and I am sure that both this post as well as the functionality it attempts to explain can be improved in any number of ways. Please feel free to engage and contribute as best as you can.
Happy Coding,
[1] There are ways to get around this limitation, by specifically downloading the source code for the exact CAS version at hand. I am skipping over those since they only lead to complications, suffering and further evil in most cases.
Monday-Friday
9am-6pm, Central European Time
7am-1pm, U.S. Eastern Time
Monday-Friday
9am-6pm, Central European Time