forms = page.getForms();
+ assertEquals(forms.size(), 1, "Page should have one and only one form");
+
+ HtmlForm form = forms.get(0);
+ assertTrue(form.getActionAttribute().contains("postlogin"), "Form does not have the expected action attribute");
+ assertEquals(form.getMethodAttribute().toLowerCase(), "post", "Form does not use POST");
+
+ } else {
+
+ assertTrue(title.contains("error"), "'error' word not found in page title");
+
+ String text = page.getVisibleText().toLowerCase();
+ assertTrue(text.contains("authentication"), "'authentication' word not found in page text");
+ assertTrue(text.contains("failed"), "'failed' word not found in page text");
+ }
+
+ }
+
+ void assertOK(Page page) {
+ assertEquals(page.getWebResponse().getStatusCode(), WebResponse.OK);
+ }
+
+ void assertServerError(Page page) {
+ assertEquals(page.getWebResponse().getStatusCode(), WebResponse.INTERNAL_SERVER_ERROR);
+ }
+
+ P doClick(DomElement el) {
+
+ try {
+ return el.click();
+ } catch (IOException e) {
+ fail(e.getMessage(), e);
+ return null;
+ }
+
+ }
+
+ void typeInInputWithName(HtmlForm form, String name, String text) {
+
+ try {
+ //See f1/index.ftl
+ form.getInputByName("something").type(text);
+ } catch (IOException e) {
+ fail(e.getMessage(), e);
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/agama/engine/src/test/java/io/jans/agama/test/CustomConfigsFlowTest.java b/agama/engine/src/test/java/io/jans/agama/test/CustomConfigsFlowTest.java
new file mode 100644
index 00000000000..6e511e5df9b
--- /dev/null
+++ b/agama/engine/src/test/java/io/jans/agama/test/CustomConfigsFlowTest.java
@@ -0,0 +1,64 @@
+package io.jans.agama.test;
+
+import com.gargoylesoftware.htmlunit.html.HtmlPage;
+import com.gargoylesoftware.htmlunit.html.HtmlSubmitInput;
+import com.gargoylesoftware.htmlunit.WebResponse;
+
+import org.testng.annotations.Parameters;
+import org.testng.annotations.Test;
+
+import static org.testng.Assert.*;
+
+@org.testng.annotations.Ignore
+public class CustomConfigsFlowTest extends BaseTest {
+
+ private static final String QNAME = "io.jans.agama.test.showConfig";
+
+ @Test
+ public void withTimeout() {
+
+ HtmlPage page = launchAndWait(10);
+ //Flow should have timed out now - see flow impl
+ //The page currently shown may correspond to the Agama timeout template or to
+ //the mismatch (not found) page in case the cleaner job already wiped the flow execution
+
+ int status = page.getWebResponse().getStatusCode();
+ String text = page.getVisibleText().toLowerCase();
+
+ if (status == WebResponse.OK) {
+ //See timeout.ftlh
+ assertTrue(text.contains("took"));
+ assertTrue(text.contains("more"));
+ assertTrue(text.contains("expected"));
+ } else if (status == WebResponse.NOT_FOUND) {
+ //See mismatch.ftlh
+ assertTrue(text.contains("not"));
+ assertTrue(text.contains("found"));
+ } else {
+ fail("Unexpected status code " + status);
+ }
+
+ }
+
+ @Test
+ public void noTimeout() {
+ HtmlPage page = launchAndWait(2);
+ validateFinishPage(page, false);
+ }
+
+ private HtmlPage launchAndWait(int wait) {
+
+ HtmlPage page = launch(QNAME, null);
+ try {
+ Thread.sleep(1000L * wait);
+ } catch (InterruptedException e) {
+ fail(e.getMessage(), e);
+ }
+
+ //click on the "Continue" button
+ HtmlSubmitInput button = page.getForms().get(0).getInputByValue("Continue");
+ return doClick(button);
+
+ }
+
+}
\ No newline at end of file
diff --git a/agama/engine/src/test/java/io/jans/agama/test/MathFlowTest.java b/agama/engine/src/test/java/io/jans/agama/test/MathFlowTest.java
new file mode 100644
index 00000000000..1d5ebeead10
--- /dev/null
+++ b/agama/engine/src/test/java/io/jans/agama/test/MathFlowTest.java
@@ -0,0 +1,62 @@
+package io.jans.agama.test;
+
+import com.gargoylesoftware.htmlunit.html.HtmlPage;
+
+import java.util.Arrays;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.testng.annotations.Test;
+
+import static org.testng.Assert.*;
+
+@org.testng.annotations.Ignore
+public class MathFlowTest extends BaseTest {
+
+ private static final String QNAME = "io.jans.agama.test.math";
+
+ @Test
+ public void runEmpty() {
+ HtmlPage page = launch(QNAME, Collections.singletonMap("numbers",Collections.emptyList()));
+ logger.info("Landed at {}", page.getUrl());
+ assertServerError(page);
+ }
+
+ @Test
+ public void runRandom() {
+
+ int len = (int) (Math.random() * 10);
+ List list = new ArrayList<>();
+
+ for (int i = 0; i < len; i++) {
+ list.add(1 + (int) (Math.random() * 100));
+ }
+ run(list);
+
+ }
+
+ @Test
+ public void runFixed1() {
+ run(Arrays.asList(30, 42, 70, 105));
+ }
+
+ @Test
+ public void runFixed2() {
+ run(Arrays.asList(6, 12, 22, 27));
+ }
+
+ @Test
+ public void runFixed3() {
+ run(Arrays.asList(1, 1, 1));
+ }
+
+ private void run(List list) {
+
+ HtmlPage page = launch(QNAME, Collections.singletonMap("numbers", list));
+ logger.info("Landed at {}", page.getUrl());
+ validateFinishPage(page, true);
+
+ }
+
+}
diff --git a/agama/engine/src/test/java/io/jans/agama/test/SaySomething2FlowTest.java b/agama/engine/src/test/java/io/jans/agama/test/SaySomething2FlowTest.java
new file mode 100644
index 00000000000..4a8ee169cfa
--- /dev/null
+++ b/agama/engine/src/test/java/io/jans/agama/test/SaySomething2FlowTest.java
@@ -0,0 +1,57 @@
+package io.jans.agama.test;
+
+import com.gargoylesoftware.htmlunit.html.HtmlForm;
+import com.gargoylesoftware.htmlunit.html.HtmlPage;
+import com.gargoylesoftware.htmlunit.html.HtmlSubmitInput;
+
+import java.util.Arrays;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.testng.annotations.Parameters;
+import org.testng.annotations.Test;
+
+import static org.testng.Assert.*;
+
+@org.testng.annotations.Ignore
+public class SaySomething2FlowTest extends BaseTest {
+
+ private static final String QNAME = "org.gluu.flow1";
+
+ @Test
+ public void something() {
+ HtmlPage page = run("Rosamaria Montibeller");
+ validateFinishPage(page, true);
+ }
+
+ @Test
+ public void nothing() {
+ HtmlPage page = run(null);
+ validateFinishPage(page, false);
+ }
+
+ private HtmlPage run(String text) {
+
+ HtmlPage page = launch(QNAME, null);
+
+ //click on the "Continue" button
+ HtmlSubmitInput button = page.getForms().get(0).getInputByValue("Continue");
+ page = doClick(button);
+
+ assertOK(page);
+ assertTrue(page.getVisibleText().contains("Gluu")); //see f1/*.ftl
+
+ HtmlForm form = page.getForms().get(0);
+ if (text != null) {
+ //See f1/index.ftl
+ typeInInputWithName(form, "something", text);
+ }
+
+ //click on the "Continue" button
+ button = form.getInputByValue("Continue");
+ return doClick(button);
+
+ }
+
+}
diff --git a/agama/engine/src/test/java/io/jans/agama/test/SaySomething3FlowTest.java b/agama/engine/src/test/java/io/jans/agama/test/SaySomething3FlowTest.java
new file mode 100644
index 00000000000..1edc5ec8723
--- /dev/null
+++ b/agama/engine/src/test/java/io/jans/agama/test/SaySomething3FlowTest.java
@@ -0,0 +1,40 @@
+package io.jans.agama.test;
+
+import com.gargoylesoftware.htmlunit.html.HtmlForm;
+import com.gargoylesoftware.htmlunit.html.HtmlPage;
+import com.gargoylesoftware.htmlunit.html.HtmlSubmitInput;
+
+import java.util.Arrays;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.testng.annotations.Parameters;
+import org.testng.annotations.Test;
+
+import static org.testng.Assert.*;
+
+@org.testng.annotations.Ignore
+public class SaySomething3FlowTest extends BaseTest {
+
+ private static final String QNAME = "org.gluu.flow3";
+
+ @Test
+ public void test() {
+ HtmlPage page = launch(QNAME, null);
+
+ //click on the "Go" button
+ HtmlSubmitInput button = page.getForms().get(0).getInputByValue("Go");
+ page = doClick(button);
+
+ assertOK(page);
+ assertTrue(page.getVisibleText().contains("Agama")); //see me/myindex.ftl and f1/index2.ftl
+
+ button = page.getForms().get(0).getInputByValue("Continue");
+ page = doClick(button);
+
+ validateFinishPage(page, false);
+
+ }
+
+}
\ No newline at end of file
diff --git a/agama/engine/src/test/java/io/jans/agama/test/SaySomethingFlowTest.java b/agama/engine/src/test/java/io/jans/agama/test/SaySomethingFlowTest.java
new file mode 100644
index 00000000000..509a7f9f17a
--- /dev/null
+++ b/agama/engine/src/test/java/io/jans/agama/test/SaySomethingFlowTest.java
@@ -0,0 +1,59 @@
+package io.jans.agama.test;
+
+import com.gargoylesoftware.htmlunit.html.HtmlForm;
+import com.gargoylesoftware.htmlunit.html.HtmlPage;
+import com.gargoylesoftware.htmlunit.html.HtmlSubmitInput;
+
+import java.util.Arrays;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.testng.annotations.Parameters;
+import org.testng.annotations.Test;
+
+import static org.testng.Assert.*;
+
+@org.testng.annotations.Ignore
+public class SaySomethingFlowTest extends BaseTest {
+
+ private static final String QNAME = "org.gluu.flow2";
+
+ @Test
+ public void nothing() {
+ HtmlPage page = run(null, null);
+ validateFinishPage(page, false);
+ }
+
+ @Test
+ public void NoOnesomething() {
+ HtmlPage page = run(null, "Jans over Gluu");
+ validateFinishPage(page, true);
+ }
+
+ @Test
+ public void someOnesomething() {
+ HtmlPage page = run("jgomer2001", "I like CE");
+ validateFinishPage(page, true);
+ }
+
+ private HtmlPage run(String name, String text) {
+
+ boolean nameEmpty = name == null;
+ HtmlPage page = launch(QNAME, nameEmpty ? null : Collections.singletonMap("val", name));
+
+ if (!nameEmpty) assertTrue(page.getVisibleText().contains(name));
+
+ HtmlForm form = page.getForms().get(0);
+
+ if (text != null) {
+ //See f1/index.ftl
+ typeInInputWithName(form, "something", text);
+ }
+ //click on the "Continue" button
+ HtmlSubmitInput button = form.getInputByValue("Continue");
+ return doClick(button);
+
+ }
+
+}
\ No newline at end of file
diff --git a/agama/engine/src/test/java/io/jans/agama/test/UidOnlyAuthTest.java b/agama/engine/src/test/java/io/jans/agama/test/UidOnlyAuthTest.java
new file mode 100644
index 00000000000..0c7720f5afa
--- /dev/null
+++ b/agama/engine/src/test/java/io/jans/agama/test/UidOnlyAuthTest.java
@@ -0,0 +1,42 @@
+package io.jans.agama.test;
+
+import com.gargoylesoftware.htmlunit.html.HtmlPage;
+import com.gargoylesoftware.htmlunit.html.HtmlSubmitInput;
+
+import java.util.Arrays;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Parameters;
+import org.testng.annotations.Test;
+
+import static org.testng.Assert.*;
+
+@org.testng.annotations.Ignore
+public class UidOnlyAuthTest extends BaseTest {
+
+ private static final String QNAME = "io.jans.agama.test.auth.uidOnly";
+
+ @BeforeClass
+ public void enableJS() {
+ client.getOptions().setJavaScriptEnabled(true);
+ }
+
+ @Test
+ public void randUid() {
+ HtmlPage page = run("" + Math.random());
+ validateFinishPage(page, false);
+ }
+
+ private HtmlPage run(String uid) {
+
+ HtmlPage page = launch(QNAME, Collections.singletonMap("uid", uid));
+ //click on the "Continue" button
+ HtmlSubmitInput button = page.getForms().get(0).getInputByValue("Continue");
+ return doClick(button);
+
+ }
+
+}
\ No newline at end of file
diff --git a/agama/engine/src/test/resources/flows/io.jans.agama.test.auth.uidOnly b/agama/engine/src/test/resources/flows/io.jans.agama.test.auth.uidOnly
new file mode 100644
index 00000000000..6397e9d13f3
--- /dev/null
+++ b/agama/engine/src/test/resources/flows/io.jans.agama.test.auth.uidOnly
@@ -0,0 +1,10 @@
+//This flow is based on the "hello world" flow found in Agama docs (quick-start quide)
+Flow io.jans.agama.test.auth.uidOnly
+ Basepath "hello"
+ Inputs uid
+
+in = { name: uid }
+RRF "index.ftlh" in
+
+Log "Done!"
+Finish uid
diff --git a/agama/engine/src/test/resources/flows/io.jans.agama.test.math b/agama/engine/src/test/resources/flows/io.jans.agama.test.math
new file mode 100644
index 00000000000..22df7423929
--- /dev/null
+++ b/agama/engine/src/test/resources/flows/io.jans.agama.test.math
@@ -0,0 +1,54 @@
+//This flow is used to test some Java calls. It does not make use of idiomatic Agama. There is no UI either here
+Flow io.jans.agama.test.math
+ Basepath ""
+ Inputs numbers //A non-empty list of positive integers
+
+// 1. Find the smallest
+small = Call java.util.Collections#min numbers
+Log "min element is" small
+
+
+// 2. Concat them all in a string
+strings = [ ]
+Iterate over numbers using n
+ i = strings.length
+ strings[i] = Call java.lang.Integer#toString n
+
+cat = Call java.lang.String#join "" strings
+Log "concatenation is" cat
+
+
+// 3. Sumation (with Repeat)
+s = 0
+Repeat numbers.length times max
+ i = idx[0]
+ s = Call java.lang.Math#addExact s numbers[i]
+
+Log "sumation is" s
+
+
+// 4. Find if they are mutually relatively prime (no integer divides them all)
+When numbers.length is 1 or small is 1
+ Finish true
+
+divisor = 1
+small = Call java.lang.Math#decrementExact small
+
+Repeat small times max
+ divisor = Call java.lang.Math#incrementExact divisor
+ k = 0
+
+ //Try to divide the numbers by 2, 3, ... small+1
+ Iterate over numbers using n
+ modulus = Call java.lang.Math#floorMod n divisor
+ Quit When modulus is 0
+ k = Call java.lang.Math#incrementExact k
+
+ Quit When k is numbers.length
+
+When k is numbers.length
+ Log "% are relative primes" numbers
+Otherwise
+ Log "All numbers can be divided by" divisor
+
+Finish true
diff --git a/agama/engine/src/test/resources/flows/io.jans.agama.test.showConfig b/agama/engine/src/test/resources/flows/io.jans.agama.test.showConfig
new file mode 100644
index 00000000000..616d4dcf80c
--- /dev/null
+++ b/agama/engine/src/test/resources/flows/io.jans.agama.test.showConfig
@@ -0,0 +1,8 @@
+Flow io.jans.agama.test.showConfig
+ Basepath ""
+ Timeout 10 seconds
+ Configs conf
+
+RRF "printConfigs.ftlh" conf
+
+Finish false
\ No newline at end of file
diff --git a/agama/engine/src/test/resources/flows/org.gluu.flow1 b/agama/engine/src/test/resources/flows/org.gluu.flow1
new file mode 100644
index 00000000000..206628a0085
--- /dev/null
+++ b/agama/engine/src/test/resources/flows/org.gluu.flow1
@@ -0,0 +1,11 @@
+//This flow appeared originally in the demos of the authentication-trees project
+Flow org.gluu.flow1
+ Basepath "f1"
+
+data = RRF "index.ftl"
+
+data = Trigger org.gluu.flow2 data.secret[0]
+
+Log "@debug Subflow finished successfully?" data.success
+
+Finish data
diff --git a/agama/engine/src/test/resources/flows/org.gluu.flow2 b/agama/engine/src/test/resources/flows/org.gluu.flow2
new file mode 100644
index 00000000000..e3bac09e4dd
--- /dev/null
+++ b/agama/engine/src/test/resources/flows/org.gluu.flow2
@@ -0,0 +1,14 @@
+//This flow appeared originally in the demos of the authentication-trees project
+Flow org.gluu.flow2
+ Basepath "f1"
+ Inputs val
+
+x = {value: val}
+data = RRF "index2.ftl" x
+
+When data.something is ""
+ Log "There was a missing value"
+ ret = { success: false, error: "You forgot something!" }
+ Finish ret
+Otherwise
+ Finish true
diff --git a/agama/engine/src/test/resources/flows/org.gluu.flow3 b/agama/engine/src/test/resources/flows/org.gluu.flow3
new file mode 100644
index 00000000000..24d7e8e06c9
--- /dev/null
+++ b/agama/engine/src/test/resources/flows/org.gluu.flow3
@@ -0,0 +1,7 @@
+Flow org.gluu.flow3
+ Basepath "me"
+
+obj = Trigger org.gluu.flow1
+ Override templates "f1/index.ftl" "myindex.ftlh"
+
+Finish obj
diff --git a/agama/engine/src/test/resources/log4j2-test.xml b/agama/engine/src/test/resources/log4j2-test.xml
new file mode 100644
index 00000000000..018859bed4c
--- /dev/null
+++ b/agama/engine/src/test/resources/log4j2-test.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/agama/engine/src/test/resources/templates/custom/printConfigs.ftlh b/agama/engine/src/test/resources/templates/custom/printConfigs.ftlh
new file mode 100644
index 00000000000..5193bcbe4c9
--- /dev/null
+++ b/agama/engine/src/test/resources/templates/custom/printConfigs.ftlh
@@ -0,0 +1,14 @@
+
+
+
+
+<#!-- rendering this page crashes if neither joke nor phrase are defined -->
+${joke}
+
${phrase}
+
+
+
+
+
diff --git a/agama/engine/src/test/resources/templates/f1/index.ftl b/agama/engine/src/test/resources/templates/f1/index.ftl
new file mode 100644
index 00000000000..4ada4213bd3
--- /dev/null
+++ b/agama/engine/src/test/resources/templates/f1/index.ftl
@@ -0,0 +1,14 @@
+<#ftl output_format="HTML">
+
+
+
+Hi I'm Flow 1!
+
+
+
+
+
diff --git a/agama/engine/src/test/resources/templates/f1/index2.ftl b/agama/engine/src/test/resources/templates/f1/index2.ftl
new file mode 100644
index 00000000000..3befc0da296
--- /dev/null
+++ b/agama/engine/src/test/resources/templates/f1/index2.ftl
@@ -0,0 +1,15 @@
+<#ftl output_format="HTML">
+
+
+
+Hi I'm Flow 2!
+${value!""}
+
+
+
+
+
diff --git a/agama/engine/src/test/resources/templates/hello/index.ftlh b/agama/engine/src/test/resources/templates/hello/index.ftlh
new file mode 100644
index 00000000000..e700dcb637e
--- /dev/null
+++ b/agama/engine/src/test/resources/templates/hello/index.ftlh
@@ -0,0 +1,11 @@
+
+
+
+Hi ${name}
+
+
+
+
+
diff --git a/agama/engine/src/test/resources/templates/login.ftlh b/agama/engine/src/test/resources/templates/login.ftlh
new file mode 100644
index 00000000000..5f8559a5db7
--- /dev/null
+++ b/agama/engine/src/test/resources/templates/login.ftlh
@@ -0,0 +1,59 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Welcome
+
+ <#if !(success!true)>
+
${msgs["login.errorMessage"]}
+ #if>
+
+
+
+
+
+
+
+
+
+
diff --git a/agama/engine/src/test/resources/templates/me/myindex.ftlh b/agama/engine/src/test/resources/templates/me/myindex.ftlh
new file mode 100644
index 00000000000..b0c1d3c989a
--- /dev/null
+++ b/agama/engine/src/test/resources/templates/me/myindex.ftlh
@@ -0,0 +1,14 @@
+
+
+
+Hi I'm Flow 3!
+
+
+
+
+
diff --git a/agama/engine/src/test/resources/testng.properties b/agama/engine/src/test/resources/testng.properties
new file mode 100644
index 00000000000..125373fd648
--- /dev/null
+++ b/agama/engine/src/test/resources/testng.properties
@@ -0,0 +1,8 @@
+
+authzEndpoint=${server}/jans-auth/restv1/authorize
+
+redirectUri=${server}/admin-ui
+
+clientId=${clientId}
+
+custParamName=${custParamName}
diff --git a/agama/engine/src/test/resources/testng.xml b/agama/engine/src/test/resources/testng.xml
new file mode 100644
index 00000000000..ec431e7e897
--- /dev/null
+++ b/agama/engine/src/test/resources/testng.xml
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/admin/developer/agama/dsl-full.md b/docs/admin/developer/agama/dsl-full.md
index 2047ee82589..44ba393f95b 100644
--- a/docs/admin/developer/agama/dsl-full.md
+++ b/docs/admin/developer/agama/dsl-full.md
@@ -160,7 +160,7 @@ Flow com.acme.FoodSurvey
Basepath "mydir"
```
-Next, flow timeout may be specified. This is the maximum amount of time available the end-user can take to fully complete a flow. For instance:
+Next, flow timeout may be specified. This is the maximum amount of time the end-user can take to fully complete a flow. For instance:
```
Flow com.acme.FoodSurvey