loans) {
this.loans = loans;
}
- @Override
- public int hashCode() {
- int hash = 3;
- hash = 97 * hash + Objects.hashCode(this.patronID);
- return hash;
- }
-
- @Override
- public boolean equals(Object obj) {
- if (this == obj) {
- return true;
- }
- if (obj == null) {
- return false;
- }
- if (getClass() != obj.getClass()) {
- return false;
- }
- final Patron other = (Patron) obj;
- return Objects.equals(this.patronID, other.patronID);
- }
-
- @Override
- public String toString() {
- return String.valueOf(name);
- }
-
}
diff --git a/ecosystem/testcontainers-example/src/main/java/fish/payara/examples/resource/BookBean.java b/ecosystem/testcontainers-example/src/main/java/fish/payara/examples/resource/BookBean.java
index 267ef9b..19228b6 100644
--- a/ecosystem/testcontainers-example/src/main/java/fish/payara/examples/resource/BookBean.java
+++ b/ecosystem/testcontainers-example/src/main/java/fish/payara/examples/resource/BookBean.java
@@ -1,3 +1,42 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright (c) 2026 Payara Foundation and/or its affiliates. All rights reserved.
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * General Public License Version 2 only ("GPL") or the Common Development
+ * and Distribution License("CDDL") (collectively, the "License"). You
+ * may not use this file except in compliance with the License. You can
+ * obtain a copy of the License at
+ * https://github.com/payara/Payara/blob/master/LICENSE.txt
+ * See the License for the specific
+ * language governing permissions and limitations under the License.
+ *
+ * When distributing the software, include this License Header Notice in each
+ * file and include the License file at glassfish/legal/LICENSE.txt.
+ *
+ * GPL Classpath Exception:
+ * The Payara Foundation designates this particular file as subject to the "Classpath"
+ * exception as provided by the Payara Foundation in the GPL Version 2 section of the License
+ * file that accompanied this code.
+ *
+ * Modifications:
+ * If applicable, add the following below the License Header, with the fields
+ * enclosed by brackets [] replaced by your own identifying information:
+ * "Portions Copyright [year] [name of copyright owner]"
+ *
+ * Contributor(s):
+ * If you wish your version of this file to be governed by only the CDDL or
+ * only the GPL Version 2, indicate your decision by adding "[Contributor]
+ * elects to include this software in this distribution under the [CDDL or GPL
+ * Version 2] license." If you don't indicate a single choice of license, a
+ * recipient has the option to distribute your version of this file under
+ * either the CDDL, the GPL Version 2 or to extend the choice of license to
+ * its licensees as provided above. However, if you add GPL Version 2 code
+ * and therefore, elected the GPL Version 2 license, then the option applies
+ * only if the new code is made subject to such option by the copyright
+ * holder.
+ */
package fish.payara.examples.resource;
import jakarta.annotation.PostConstruct;
@@ -32,7 +71,9 @@ public String create() {
return null;
}
public String save() {
- if (book.getIsbn() == null) {
+ // isBlank(), not just == null: see LibrarianBean#save for why a
+ // plain h:inputText bound to a String id needs this guard too.
+ if (book.getIsbn() == null || book.getIsbn().isBlank()) {
bookService.create(book);
} else {
bookService.edit(book);
diff --git a/ecosystem/testcontainers-example/src/main/java/fish/payara/examples/resource/LibrarianBean.java b/ecosystem/testcontainers-example/src/main/java/fish/payara/examples/resource/LibrarianBean.java
index b84990d..3004c22 100644
--- a/ecosystem/testcontainers-example/src/main/java/fish/payara/examples/resource/LibrarianBean.java
+++ b/ecosystem/testcontainers-example/src/main/java/fish/payara/examples/resource/LibrarianBean.java
@@ -1,3 +1,42 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright (c) 2026 Payara Foundation and/or its affiliates. All rights reserved.
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * General Public License Version 2 only ("GPL") or the Common Development
+ * and Distribution License("CDDL") (collectively, the "License"). You
+ * may not use this file except in compliance with the License. You can
+ * obtain a copy of the License at
+ * https://github.com/payara/Payara/blob/master/LICENSE.txt
+ * See the License for the specific
+ * language governing permissions and limitations under the License.
+ *
+ * When distributing the software, include this License Header Notice in each
+ * file and include the License file at glassfish/legal/LICENSE.txt.
+ *
+ * GPL Classpath Exception:
+ * The Payara Foundation designates this particular file as subject to the "Classpath"
+ * exception as provided by the Payara Foundation in the GPL Version 2 section of the License
+ * file that accompanied this code.
+ *
+ * Modifications:
+ * If applicable, add the following below the License Header, with the fields
+ * enclosed by brackets [] replaced by your own identifying information:
+ * "Portions Copyright [year] [name of copyright owner]"
+ *
+ * Contributor(s):
+ * If you wish your version of this file to be governed by only the CDDL or
+ * only the GPL Version 2, indicate your decision by adding "[Contributor]
+ * elects to include this software in this distribution under the [CDDL or GPL
+ * Version 2] license." If you don't indicate a single choice of license, a
+ * recipient has the option to distribute your version of this file under
+ * either the CDDL, the GPL Version 2 or to extend the choice of license to
+ * its licensees as provided above. However, if you add GPL Version 2 code
+ * and therefore, elected the GPL Version 2 license, then the option applies
+ * only if the new code is made subject to such option by the copyright
+ * holder.
+ */
package fish.payara.examples.resource;
import jakarta.annotation.PostConstruct;
@@ -32,7 +71,12 @@ public String create() {
return null;
}
public String save() {
- if (librarian.getLibrarianID() == null) {
+ // isBlank(), not just == null: librarian.xhtml's id field is a plain
+ // h:inputText, so saving it with that field left blank submits ""
+ // rather than null (see Librarian.assignId() for the full story).
+ // Without this check, "" reads as "already has an id" and this would
+ // call edit()/merge() on a brand new librarian instead of create().
+ if (librarian.getLibrarianID() == null || librarian.getLibrarianID().isBlank()) {
librarianService.create(librarian);
} else {
librarianService.edit(librarian);
diff --git a/ecosystem/testcontainers-example/src/main/java/fish/payara/examples/resource/LoanBean.java b/ecosystem/testcontainers-example/src/main/java/fish/payara/examples/resource/LoanBean.java
index 05bba0e..6520ba8 100644
--- a/ecosystem/testcontainers-example/src/main/java/fish/payara/examples/resource/LoanBean.java
+++ b/ecosystem/testcontainers-example/src/main/java/fish/payara/examples/resource/LoanBean.java
@@ -1,3 +1,42 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright (c) 2026 Payara Foundation and/or its affiliates. All rights reserved.
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * General Public License Version 2 only ("GPL") or the Common Development
+ * and Distribution License("CDDL") (collectively, the "License"). You
+ * may not use this file except in compliance with the License. You can
+ * obtain a copy of the License at
+ * https://github.com/payara/Payara/blob/master/LICENSE.txt
+ * See the License for the specific
+ * language governing permissions and limitations under the License.
+ *
+ * When distributing the software, include this License Header Notice in each
+ * file and include the License file at glassfish/legal/LICENSE.txt.
+ *
+ * GPL Classpath Exception:
+ * The Payara Foundation designates this particular file as subject to the "Classpath"
+ * exception as provided by the Payara Foundation in the GPL Version 2 section of the License
+ * file that accompanied this code.
+ *
+ * Modifications:
+ * If applicable, add the following below the License Header, with the fields
+ * enclosed by brackets [] replaced by your own identifying information:
+ * "Portions Copyright [year] [name of copyright owner]"
+ *
+ * Contributor(s):
+ * If you wish your version of this file to be governed by only the CDDL or
+ * only the GPL Version 2, indicate your decision by adding "[Contributor]
+ * elects to include this software in this distribution under the [CDDL or GPL
+ * Version 2] license." If you don't indicate a single choice of license, a
+ * recipient has the option to distribute your version of this file under
+ * either the CDDL, the GPL Version 2 or to extend the choice of license to
+ * its licensees as provided above. However, if you add GPL Version 2 code
+ * and therefore, elected the GPL Version 2 license, then the option applies
+ * only if the new code is made subject to such option by the copyright
+ * holder.
+ */
package fish.payara.examples.resource;
import jakarta.annotation.PostConstruct;
diff --git a/ecosystem/testcontainers-example/src/main/java/fish/payara/examples/resource/NavigationBean.java b/ecosystem/testcontainers-example/src/main/java/fish/payara/examples/resource/NavigationBean.java
index 37f9faa..994f2dc 100644
--- a/ecosystem/testcontainers-example/src/main/java/fish/payara/examples/resource/NavigationBean.java
+++ b/ecosystem/testcontainers-example/src/main/java/fish/payara/examples/resource/NavigationBean.java
@@ -1,3 +1,42 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright (c) 2026 Payara Foundation and/or its affiliates. All rights reserved.
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * General Public License Version 2 only ("GPL") or the Common Development
+ * and Distribution License("CDDL") (collectively, the "License"). You
+ * may not use this file except in compliance with the License. You can
+ * obtain a copy of the License at
+ * https://github.com/payara/Payara/blob/master/LICENSE.txt
+ * See the License for the specific
+ * language governing permissions and limitations under the License.
+ *
+ * When distributing the software, include this License Header Notice in each
+ * file and include the License file at glassfish/legal/LICENSE.txt.
+ *
+ * GPL Classpath Exception:
+ * The Payara Foundation designates this particular file as subject to the "Classpath"
+ * exception as provided by the Payara Foundation in the GPL Version 2 section of the License
+ * file that accompanied this code.
+ *
+ * Modifications:
+ * If applicable, add the following below the License Header, with the fields
+ * enclosed by brackets [] replaced by your own identifying information:
+ * "Portions Copyright [year] [name of copyright owner]"
+ *
+ * Contributor(s):
+ * If you wish your version of this file to be governed by only the CDDL or
+ * only the GPL Version 2, indicate your decision by adding "[Contributor]
+ * elects to include this software in this distribution under the [CDDL or GPL
+ * Version 2] license." If you don't indicate a single choice of license, a
+ * recipient has the option to distribute your version of this file under
+ * either the CDDL, the GPL Version 2 or to extend the choice of license to
+ * its licensees as provided above. However, if you add GPL Version 2 code
+ * and therefore, elected the GPL Version 2 license, then the option applies
+ * only if the new code is made subject to such option by the copyright
+ * holder.
+ */
package fish.payara.examples.resource;
import jakarta.enterprise.context.SessionScoped;
diff --git a/ecosystem/testcontainers-example/src/main/java/fish/payara/examples/resource/PatronBean.java b/ecosystem/testcontainers-example/src/main/java/fish/payara/examples/resource/PatronBean.java
index 25e520a..6e39322 100644
--- a/ecosystem/testcontainers-example/src/main/java/fish/payara/examples/resource/PatronBean.java
+++ b/ecosystem/testcontainers-example/src/main/java/fish/payara/examples/resource/PatronBean.java
@@ -1,3 +1,42 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright (c) 2026 Payara Foundation and/or its affiliates. All rights reserved.
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * General Public License Version 2 only ("GPL") or the Common Development
+ * and Distribution License("CDDL") (collectively, the "License"). You
+ * may not use this file except in compliance with the License. You can
+ * obtain a copy of the License at
+ * https://github.com/payara/Payara/blob/master/LICENSE.txt
+ * See the License for the specific
+ * language governing permissions and limitations under the License.
+ *
+ * When distributing the software, include this License Header Notice in each
+ * file and include the License file at glassfish/legal/LICENSE.txt.
+ *
+ * GPL Classpath Exception:
+ * The Payara Foundation designates this particular file as subject to the "Classpath"
+ * exception as provided by the Payara Foundation in the GPL Version 2 section of the License
+ * file that accompanied this code.
+ *
+ * Modifications:
+ * If applicable, add the following below the License Header, with the fields
+ * enclosed by brackets [] replaced by your own identifying information:
+ * "Portions Copyright [year] [name of copyright owner]"
+ *
+ * Contributor(s):
+ * If you wish your version of this file to be governed by only the CDDL or
+ * only the GPL Version 2, indicate your decision by adding "[Contributor]
+ * elects to include this software in this distribution under the [CDDL or GPL
+ * Version 2] license." If you don't indicate a single choice of license, a
+ * recipient has the option to distribute your version of this file under
+ * either the CDDL, the GPL Version 2 or to extend the choice of license to
+ * its licensees as provided above. However, if you add GPL Version 2 code
+ * and therefore, elected the GPL Version 2 license, then the option applies
+ * only if the new code is made subject to such option by the copyright
+ * holder.
+ */
package fish.payara.examples.resource;
import jakarta.annotation.PostConstruct;
@@ -32,7 +71,9 @@ public String create() {
return null;
}
public String save() {
- if (patron.getPatronID() == null) {
+ // isBlank(), not just == null: see LibrarianBean#save for why a
+ // plain h:inputText bound to a String id needs this guard too.
+ if (patron.getPatronID() == null || patron.getPatronID().isBlank()) {
patronService.create(patron);
} else {
patronService.edit(patron);
diff --git a/ecosystem/testcontainers-example/src/main/java/fish/payara/examples/rest/BookResource.java b/ecosystem/testcontainers-example/src/main/java/fish/payara/examples/rest/BookResource.java
index 44a063f..54abe3b 100644
--- a/ecosystem/testcontainers-example/src/main/java/fish/payara/examples/rest/BookResource.java
+++ b/ecosystem/testcontainers-example/src/main/java/fish/payara/examples/rest/BookResource.java
@@ -1,3 +1,42 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright (c) 2026 Payara Foundation and/or its affiliates. All rights reserved.
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * General Public License Version 2 only ("GPL") or the Common Development
+ * and Distribution License("CDDL") (collectively, the "License"). You
+ * may not use this file except in compliance with the License. You can
+ * obtain a copy of the License at
+ * https://github.com/payara/Payara/blob/master/LICENSE.txt
+ * See the License for the specific
+ * language governing permissions and limitations under the License.
+ *
+ * When distributing the software, include this License Header Notice in each
+ * file and include the License file at glassfish/legal/LICENSE.txt.
+ *
+ * GPL Classpath Exception:
+ * The Payara Foundation designates this particular file as subject to the "Classpath"
+ * exception as provided by the Payara Foundation in the GPL Version 2 section of the License
+ * file that accompanied this code.
+ *
+ * Modifications:
+ * If applicable, add the following below the License Header, with the fields
+ * enclosed by brackets [] replaced by your own identifying information:
+ * "Portions Copyright [year] [name of copyright owner]"
+ *
+ * Contributor(s):
+ * If you wish your version of this file to be governed by only the CDDL or
+ * only the GPL Version 2, indicate your decision by adding "[Contributor]
+ * elects to include this software in this distribution under the [CDDL or GPL
+ * Version 2] license." If you don't indicate a single choice of license, a
+ * recipient has the option to distribute your version of this file under
+ * either the CDDL, the GPL Version 2 or to extend the choice of license to
+ * its licensees as provided above. However, if you add GPL Version 2 code
+ * and therefore, elected the GPL Version 2 license, then the option applies
+ * only if the new code is made subject to such option by the copyright
+ * holder.
+ */
package fish.payara.examples.rest;
import fish.payara.examples.domain.Book;
diff --git a/ecosystem/testcontainers-example/src/main/java/fish/payara/examples/rest/JAXRSConfiguration.java b/ecosystem/testcontainers-example/src/main/java/fish/payara/examples/rest/JAXRSConfiguration.java
index 8aa8bcf..eb534dd 100644
--- a/ecosystem/testcontainers-example/src/main/java/fish/payara/examples/rest/JAXRSConfiguration.java
+++ b/ecosystem/testcontainers-example/src/main/java/fish/payara/examples/rest/JAXRSConfiguration.java
@@ -1,3 +1,42 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright (c) 2026 Payara Foundation and/or its affiliates. All rights reserved.
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * General Public License Version 2 only ("GPL") or the Common Development
+ * and Distribution License("CDDL") (collectively, the "License"). You
+ * may not use this file except in compliance with the License. You can
+ * obtain a copy of the License at
+ * https://github.com/payara/Payara/blob/master/LICENSE.txt
+ * See the License for the specific
+ * language governing permissions and limitations under the License.
+ *
+ * When distributing the software, include this License Header Notice in each
+ * file and include the License file at glassfish/legal/LICENSE.txt.
+ *
+ * GPL Classpath Exception:
+ * The Payara Foundation designates this particular file as subject to the "Classpath"
+ * exception as provided by the Payara Foundation in the GPL Version 2 section of the License
+ * file that accompanied this code.
+ *
+ * Modifications:
+ * If applicable, add the following below the License Header, with the fields
+ * enclosed by brackets [] replaced by your own identifying information:
+ * "Portions Copyright [year] [name of copyright owner]"
+ *
+ * Contributor(s):
+ * If you wish your version of this file to be governed by only the CDDL or
+ * only the GPL Version 2, indicate your decision by adding "[Contributor]
+ * elects to include this software in this distribution under the [CDDL or GPL
+ * Version 2] license." If you don't indicate a single choice of license, a
+ * recipient has the option to distribute your version of this file under
+ * either the CDDL, the GPL Version 2 or to extend the choice of license to
+ * its licensees as provided above. However, if you add GPL Version 2 code
+ * and therefore, elected the GPL Version 2 license, then the option applies
+ * only if the new code is made subject to such option by the copyright
+ * holder.
+ */
package fish.payara.examples.rest;
import jakarta.ws.rs.ApplicationPath;
diff --git a/ecosystem/testcontainers-example/src/main/java/fish/payara/examples/rest/LibrarianResource.java b/ecosystem/testcontainers-example/src/main/java/fish/payara/examples/rest/LibrarianResource.java
index a962666..6b6d569 100644
--- a/ecosystem/testcontainers-example/src/main/java/fish/payara/examples/rest/LibrarianResource.java
+++ b/ecosystem/testcontainers-example/src/main/java/fish/payara/examples/rest/LibrarianResource.java
@@ -1,3 +1,42 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright (c) 2026 Payara Foundation and/or its affiliates. All rights reserved.
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * General Public License Version 2 only ("GPL") or the Common Development
+ * and Distribution License("CDDL") (collectively, the "License"). You
+ * may not use this file except in compliance with the License. You can
+ * obtain a copy of the License at
+ * https://github.com/payara/Payara/blob/master/LICENSE.txt
+ * See the License for the specific
+ * language governing permissions and limitations under the License.
+ *
+ * When distributing the software, include this License Header Notice in each
+ * file and include the License file at glassfish/legal/LICENSE.txt.
+ *
+ * GPL Classpath Exception:
+ * The Payara Foundation designates this particular file as subject to the "Classpath"
+ * exception as provided by the Payara Foundation in the GPL Version 2 section of the License
+ * file that accompanied this code.
+ *
+ * Modifications:
+ * If applicable, add the following below the License Header, with the fields
+ * enclosed by brackets [] replaced by your own identifying information:
+ * "Portions Copyright [year] [name of copyright owner]"
+ *
+ * Contributor(s):
+ * If you wish your version of this file to be governed by only the CDDL or
+ * only the GPL Version 2, indicate your decision by adding "[Contributor]
+ * elects to include this software in this distribution under the [CDDL or GPL
+ * Version 2] license." If you don't indicate a single choice of license, a
+ * recipient has the option to distribute your version of this file under
+ * either the CDDL, the GPL Version 2 or to extend the choice of license to
+ * its licensees as provided above. However, if you add GPL Version 2 code
+ * and therefore, elected the GPL Version 2 license, then the option applies
+ * only if the new code is made subject to such option by the copyright
+ * holder.
+ */
package fish.payara.examples.rest;
import fish.payara.examples.domain.Librarian;
diff --git a/ecosystem/testcontainers-example/src/main/java/fish/payara/examples/rest/LoanResource.java b/ecosystem/testcontainers-example/src/main/java/fish/payara/examples/rest/LoanResource.java
index 2bc2fb8..c1e18fd 100644
--- a/ecosystem/testcontainers-example/src/main/java/fish/payara/examples/rest/LoanResource.java
+++ b/ecosystem/testcontainers-example/src/main/java/fish/payara/examples/rest/LoanResource.java
@@ -1,3 +1,42 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright (c) 2026 Payara Foundation and/or its affiliates. All rights reserved.
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * General Public License Version 2 only ("GPL") or the Common Development
+ * and Distribution License("CDDL") (collectively, the "License"). You
+ * may not use this file except in compliance with the License. You can
+ * obtain a copy of the License at
+ * https://github.com/payara/Payara/blob/master/LICENSE.txt
+ * See the License for the specific
+ * language governing permissions and limitations under the License.
+ *
+ * When distributing the software, include this License Header Notice in each
+ * file and include the License file at glassfish/legal/LICENSE.txt.
+ *
+ * GPL Classpath Exception:
+ * The Payara Foundation designates this particular file as subject to the "Classpath"
+ * exception as provided by the Payara Foundation in the GPL Version 2 section of the License
+ * file that accompanied this code.
+ *
+ * Modifications:
+ * If applicable, add the following below the License Header, with the fields
+ * enclosed by brackets [] replaced by your own identifying information:
+ * "Portions Copyright [year] [name of copyright owner]"
+ *
+ * Contributor(s):
+ * If you wish your version of this file to be governed by only the CDDL or
+ * only the GPL Version 2, indicate your decision by adding "[Contributor]
+ * elects to include this software in this distribution under the [CDDL or GPL
+ * Version 2] license." If you don't indicate a single choice of license, a
+ * recipient has the option to distribute your version of this file under
+ * either the CDDL, the GPL Version 2 or to extend the choice of license to
+ * its licensees as provided above. However, if you add GPL Version 2 code
+ * and therefore, elected the GPL Version 2 license, then the option applies
+ * only if the new code is made subject to such option by the copyright
+ * holder.
+ */
package fish.payara.examples.resource;
import fish.payara.examples.domain.Loan;
diff --git a/ecosystem/testcontainers-example/src/main/java/fish/payara/examples/rest/PatronResource.java b/ecosystem/testcontainers-example/src/main/java/fish/payara/examples/rest/PatronResource.java
index 8d35853..964005d 100644
--- a/ecosystem/testcontainers-example/src/main/java/fish/payara/examples/rest/PatronResource.java
+++ b/ecosystem/testcontainers-example/src/main/java/fish/payara/examples/rest/PatronResource.java
@@ -1,3 +1,42 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright (c) 2026 Payara Foundation and/or its affiliates. All rights reserved.
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * General Public License Version 2 only ("GPL") or the Common Development
+ * and Distribution License("CDDL") (collectively, the "License"). You
+ * may not use this file except in compliance with the License. You can
+ * obtain a copy of the License at
+ * https://github.com/payara/Payara/blob/master/LICENSE.txt
+ * See the License for the specific
+ * language governing permissions and limitations under the License.
+ *
+ * When distributing the software, include this License Header Notice in each
+ * file and include the License file at glassfish/legal/LICENSE.txt.
+ *
+ * GPL Classpath Exception:
+ * The Payara Foundation designates this particular file as subject to the "Classpath"
+ * exception as provided by the Payara Foundation in the GPL Version 2 section of the License
+ * file that accompanied this code.
+ *
+ * Modifications:
+ * If applicable, add the following below the License Header, with the fields
+ * enclosed by brackets [] replaced by your own identifying information:
+ * "Portions Copyright [year] [name of copyright owner]"
+ *
+ * Contributor(s):
+ * If you wish your version of this file to be governed by only the CDDL or
+ * only the GPL Version 2, indicate your decision by adding "[Contributor]
+ * elects to include this software in this distribution under the [CDDL or GPL
+ * Version 2] license." If you don't indicate a single choice of license, a
+ * recipient has the option to distribute your version of this file under
+ * either the CDDL, the GPL Version 2 or to extend the choice of license to
+ * its licensees as provided above. However, if you add GPL Version 2 code
+ * and therefore, elected the GPL Version 2 license, then the option applies
+ * only if the new code is made subject to such option by the copyright
+ * holder.
+ */
package fish.payara.examples.resource;
import fish.payara.examples.domain.Patron;
diff --git a/ecosystem/testcontainers-example/src/main/java/fish/payara/examples/service/AbstractService.java b/ecosystem/testcontainers-example/src/main/java/fish/payara/examples/service/AbstractService.java
index df79c16..bd81bf6 100644
--- a/ecosystem/testcontainers-example/src/main/java/fish/payara/examples/service/AbstractService.java
+++ b/ecosystem/testcontainers-example/src/main/java/fish/payara/examples/service/AbstractService.java
@@ -1,3 +1,42 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright (c) 2026 Payara Foundation and/or its affiliates. All rights reserved.
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * General Public License Version 2 only ("GPL") or the Common Development
+ * and Distribution License("CDDL") (collectively, the "License"). You
+ * may not use this file except in compliance with the License. You can
+ * obtain a copy of the License at
+ * https://github.com/payara/Payara/blob/master/LICENSE.txt
+ * See the License for the specific
+ * language governing permissions and limitations under the License.
+ *
+ * When distributing the software, include this License Header Notice in each
+ * file and include the License file at glassfish/legal/LICENSE.txt.
+ *
+ * GPL Classpath Exception:
+ * The Payara Foundation designates this particular file as subject to the "Classpath"
+ * exception as provided by the Payara Foundation in the GPL Version 2 section of the License
+ * file that accompanied this code.
+ *
+ * Modifications:
+ * If applicable, add the following below the License Header, with the fields
+ * enclosed by brackets [] replaced by your own identifying information:
+ * "Portions Copyright [year] [name of copyright owner]"
+ *
+ * Contributor(s):
+ * If you wish your version of this file to be governed by only the CDDL or
+ * only the GPL Version 2, indicate your decision by adding "[Contributor]
+ * elects to include this software in this distribution under the [CDDL or GPL
+ * Version 2] license." If you don't indicate a single choice of license, a
+ * recipient has the option to distribute your version of this file under
+ * either the CDDL, the GPL Version 2 or to extend the choice of license to
+ * its licensees as provided above. However, if you add GPL Version 2 code
+ * and therefore, elected the GPL Version 2 license, then the option applies
+ * only if the new code is made subject to such option by the copyright
+ * holder.
+ */
package fish.payara.examples.service;
import java.util.Collections;
diff --git a/ecosystem/testcontainers-example/src/main/java/fish/payara/examples/service/BookService.java b/ecosystem/testcontainers-example/src/main/java/fish/payara/examples/service/BookService.java
index 7f83c5e..7fd9bb3 100644
--- a/ecosystem/testcontainers-example/src/main/java/fish/payara/examples/service/BookService.java
+++ b/ecosystem/testcontainers-example/src/main/java/fish/payara/examples/service/BookService.java
@@ -1,3 +1,42 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright (c) 2026 Payara Foundation and/or its affiliates. All rights reserved.
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * General Public License Version 2 only ("GPL") or the Common Development
+ * and Distribution License("CDDL") (collectively, the "License"). You
+ * may not use this file except in compliance with the License. You can
+ * obtain a copy of the License at
+ * https://github.com/payara/Payara/blob/master/LICENSE.txt
+ * See the License for the specific
+ * language governing permissions and limitations under the License.
+ *
+ * When distributing the software, include this License Header Notice in each
+ * file and include the License file at glassfish/legal/LICENSE.txt.
+ *
+ * GPL Classpath Exception:
+ * The Payara Foundation designates this particular file as subject to the "Classpath"
+ * exception as provided by the Payara Foundation in the GPL Version 2 section of the License
+ * file that accompanied this code.
+ *
+ * Modifications:
+ * If applicable, add the following below the License Header, with the fields
+ * enclosed by brackets [] replaced by your own identifying information:
+ * "Portions Copyright [year] [name of copyright owner]"
+ *
+ * Contributor(s):
+ * If you wish your version of this file to be governed by only the CDDL or
+ * only the GPL Version 2, indicate your decision by adding "[Contributor]
+ * elects to include this software in this distribution under the [CDDL or GPL
+ * Version 2] license." If you don't indicate a single choice of license, a
+ * recipient has the option to distribute your version of this file under
+ * either the CDDL, the GPL Version 2 or to extend the choice of license to
+ * its licensees as provided above. However, if you add GPL Version 2 code
+ * and therefore, elected the GPL Version 2 license, then the option applies
+ * only if the new code is made subject to such option by the copyright
+ * holder.
+ */
package fish.payara.examples.service;
import jakarta.enterprise.context.Dependent;
diff --git a/ecosystem/testcontainers-example/src/main/java/fish/payara/examples/service/LibrarianService.java b/ecosystem/testcontainers-example/src/main/java/fish/payara/examples/service/LibrarianService.java
index 7f1dee4..1417eea 100644
--- a/ecosystem/testcontainers-example/src/main/java/fish/payara/examples/service/LibrarianService.java
+++ b/ecosystem/testcontainers-example/src/main/java/fish/payara/examples/service/LibrarianService.java
@@ -1,3 +1,42 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright (c) 2026 Payara Foundation and/or its affiliates. All rights reserved.
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * General Public License Version 2 only ("GPL") or the Common Development
+ * and Distribution License("CDDL") (collectively, the "License"). You
+ * may not use this file except in compliance with the License. You can
+ * obtain a copy of the License at
+ * https://github.com/payara/Payara/blob/master/LICENSE.txt
+ * See the License for the specific
+ * language governing permissions and limitations under the License.
+ *
+ * When distributing the software, include this License Header Notice in each
+ * file and include the License file at glassfish/legal/LICENSE.txt.
+ *
+ * GPL Classpath Exception:
+ * The Payara Foundation designates this particular file as subject to the "Classpath"
+ * exception as provided by the Payara Foundation in the GPL Version 2 section of the License
+ * file that accompanied this code.
+ *
+ * Modifications:
+ * If applicable, add the following below the License Header, with the fields
+ * enclosed by brackets [] replaced by your own identifying information:
+ * "Portions Copyright [year] [name of copyright owner]"
+ *
+ * Contributor(s):
+ * If you wish your version of this file to be governed by only the CDDL or
+ * only the GPL Version 2, indicate your decision by adding "[Contributor]
+ * elects to include this software in this distribution under the [CDDL or GPL
+ * Version 2] license." If you don't indicate a single choice of license, a
+ * recipient has the option to distribute your version of this file under
+ * either the CDDL, the GPL Version 2 or to extend the choice of license to
+ * its licensees as provided above. However, if you add GPL Version 2 code
+ * and therefore, elected the GPL Version 2 license, then the option applies
+ * only if the new code is made subject to such option by the copyright
+ * holder.
+ */
package fish.payara.examples.service;
import jakarta.enterprise.context.Dependent;
diff --git a/ecosystem/testcontainers-example/src/main/java/fish/payara/examples/service/LoanService.java b/ecosystem/testcontainers-example/src/main/java/fish/payara/examples/service/LoanService.java
index d1007df..b091f24 100644
--- a/ecosystem/testcontainers-example/src/main/java/fish/payara/examples/service/LoanService.java
+++ b/ecosystem/testcontainers-example/src/main/java/fish/payara/examples/service/LoanService.java
@@ -1,3 +1,42 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright (c) 2026 Payara Foundation and/or its affiliates. All rights reserved.
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * General Public License Version 2 only ("GPL") or the Common Development
+ * and Distribution License("CDDL") (collectively, the "License"). You
+ * may not use this file except in compliance with the License. You can
+ * obtain a copy of the License at
+ * https://github.com/payara/Payara/blob/master/LICENSE.txt
+ * See the License for the specific
+ * language governing permissions and limitations under the License.
+ *
+ * When distributing the software, include this License Header Notice in each
+ * file and include the License file at glassfish/legal/LICENSE.txt.
+ *
+ * GPL Classpath Exception:
+ * The Payara Foundation designates this particular file as subject to the "Classpath"
+ * exception as provided by the Payara Foundation in the GPL Version 2 section of the License
+ * file that accompanied this code.
+ *
+ * Modifications:
+ * If applicable, add the following below the License Header, with the fields
+ * enclosed by brackets [] replaced by your own identifying information:
+ * "Portions Copyright [year] [name of copyright owner]"
+ *
+ * Contributor(s):
+ * If you wish your version of this file to be governed by only the CDDL or
+ * only the GPL Version 2, indicate your decision by adding "[Contributor]
+ * elects to include this software in this distribution under the [CDDL or GPL
+ * Version 2] license." If you don't indicate a single choice of license, a
+ * recipient has the option to distribute your version of this file under
+ * either the CDDL, the GPL Version 2 or to extend the choice of license to
+ * its licensees as provided above. However, if you add GPL Version 2 code
+ * and therefore, elected the GPL Version 2 license, then the option applies
+ * only if the new code is made subject to such option by the copyright
+ * holder.
+ */
package fish.payara.examples.service;
import jakarta.enterprise.context.Dependent;
diff --git a/ecosystem/testcontainers-example/src/main/java/fish/payara/examples/service/PatronService.java b/ecosystem/testcontainers-example/src/main/java/fish/payara/examples/service/PatronService.java
index e80a488..ba9fcb6 100644
--- a/ecosystem/testcontainers-example/src/main/java/fish/payara/examples/service/PatronService.java
+++ b/ecosystem/testcontainers-example/src/main/java/fish/payara/examples/service/PatronService.java
@@ -1,3 +1,42 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright (c) 2026 Payara Foundation and/or its affiliates. All rights reserved.
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * General Public License Version 2 only ("GPL") or the Common Development
+ * and Distribution License("CDDL") (collectively, the "License"). You
+ * may not use this file except in compliance with the License. You can
+ * obtain a copy of the License at
+ * https://github.com/payara/Payara/blob/master/LICENSE.txt
+ * See the License for the specific
+ * language governing permissions and limitations under the License.
+ *
+ * When distributing the software, include this License Header Notice in each
+ * file and include the License file at glassfish/legal/LICENSE.txt.
+ *
+ * GPL Classpath Exception:
+ * The Payara Foundation designates this particular file as subject to the "Classpath"
+ * exception as provided by the Payara Foundation in the GPL Version 2 section of the License
+ * file that accompanied this code.
+ *
+ * Modifications:
+ * If applicable, add the following below the License Header, with the fields
+ * enclosed by brackets [] replaced by your own identifying information:
+ * "Portions Copyright [year] [name of copyright owner]"
+ *
+ * Contributor(s):
+ * If you wish your version of this file to be governed by only the CDDL or
+ * only the GPL Version 2, indicate your decision by adding "[Contributor]
+ * elects to include this software in this distribution under the [CDDL or GPL
+ * Version 2] license." If you don't indicate a single choice of license, a
+ * recipient has the option to distribute your version of this file under
+ * either the CDDL, the GPL Version 2 or to extend the choice of license to
+ * its licensees as provided above. However, if you add GPL Version 2 code
+ * and therefore, elected the GPL Version 2 license, then the option applies
+ * only if the new code is made subject to such option by the copyright
+ * holder.
+ */
package fish.payara.examples.service;
import jakarta.enterprise.context.Dependent;
diff --git a/ecosystem/testcontainers-example/src/main/webapp/book.xhtml b/ecosystem/testcontainers-example/src/main/webapp/book.xhtml
index 7d95818..36cb667 100644
--- a/ecosystem/testcontainers-example/src/main/webapp/book.xhtml
+++ b/ecosystem/testcontainers-example/src/main/webapp/book.xhtml
@@ -9,9 +9,6 @@
Represents books available in the library.
-
-
-
@@ -23,6 +20,9 @@
+
+
diff --git a/ecosystem/testcontainers-example/src/main/webapp/librarian.xhtml b/ecosystem/testcontainers-example/src/main/webapp/librarian.xhtml
index bda645b..76cd22f 100644
--- a/ecosystem/testcontainers-example/src/main/webapp/librarian.xhtml
+++ b/ecosystem/testcontainers-example/src/main/webapp/librarian.xhtml
@@ -9,15 +9,15 @@
Represents librarians managing the library.
-
-
-
+
+
diff --git a/ecosystem/testcontainers-example/src/main/webapp/patron.xhtml b/ecosystem/testcontainers-example/src/main/webapp/patron.xhtml
index e6bff23..646a8ff 100644
--- a/ecosystem/testcontainers-example/src/main/webapp/patron.xhtml
+++ b/ecosystem/testcontainers-example/src/main/webapp/patron.xhtml
@@ -9,9 +9,6 @@
Represents library patrons who borrow books.
-
-
-
@@ -21,6 +18,9 @@
+
+
diff --git a/ecosystem/testcontainers-example/src/test/java/fish/payara/examples/arquillian/BookServiceArquillianIT.java b/ecosystem/testcontainers-example/src/test/java/fish/payara/examples/arquillian/BookServiceArquillianIT.java
new file mode 100644
index 0000000..e1a6ed1
--- /dev/null
+++ b/ecosystem/testcontainers-example/src/test/java/fish/payara/examples/arquillian/BookServiceArquillianIT.java
@@ -0,0 +1,175 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright (c) 2026 Payara Foundation and/or its affiliates. All rights reserved.
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * General Public License Version 2 only ("GPL") or the Common Development
+ * and Distribution License("CDDL") (collectively, the "License"). You
+ * may not use this file except in compliance with the License. You can
+ * obtain a copy of the License at
+ * https://github.com/payara/Payara/blob/master/LICENSE.txt
+ * See the License for the specific
+ * language governing permissions and limitations under the License.
+ *
+ * When distributing the software, include this License Header Notice in each
+ * file and include the License file at glassfish/legal/LICENSE.txt.
+ *
+ * GPL Classpath Exception:
+ * The Payara Foundation designates this particular file as subject to the "Classpath"
+ * exception as provided by the Payara Foundation in the GPL Version 2 section of the License
+ * file that accompanied this code.
+ *
+ * Modifications:
+ * If applicable, add the following below the License Header, with the fields
+ * enclosed by brackets [] replaced by your own identifying information:
+ * "Portions Copyright [year] [name of copyright owner]"
+ *
+ * Contributor(s):
+ * If you wish your version of this file to be governed by only the CDDL or
+ * only the GPL Version 2, indicate your decision by adding "[Contributor]
+ * elects to include this software in this distribution under the [CDDL or GPL
+ * Version 2] license." If you don't indicate a single choice of license, a
+ * recipient has the option to distribute your version of this file under
+ * either the CDDL, the GPL Version 2 or to extend the choice of license to
+ * its licensees as provided above. However, if you add GPL Version 2 code
+ * and therefore, elected the GPL Version 2 license, then the option applies
+ * only if the new code is made subject to such option by the copyright
+ * holder.
+ */
+package fish.payara.examples.arquillian;
+
+import fish.payara.examples.domain.Book;
+import fish.payara.examples.service.AbstractService;
+import fish.payara.examples.service.BookService;
+import jakarta.inject.Inject;
+import org.jboss.arquillian.container.test.api.Deployment;
+import org.jboss.arquillian.junit5.ArquillianExtension;
+import org.jboss.shrinkwrap.api.ShrinkWrap;
+import org.jboss.shrinkwrap.api.asset.EmptyAsset;
+import org.jboss.shrinkwrap.api.spec.JavaArchive;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+import java.util.stream.Collectors;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Covers the one corner of {@link AbstractService} that neither
+ * {@code BookServiceTest} (WeldInitiator, mocked {@code EntityManager}) nor
+ * {@code BookServiceIT} (Testcontainers, real Payara Micro but only reachable
+ * through the REST resources' CRUD endpoints) can reach: {@link
+ * AbstractService#count()}, {@link AbstractService#findRange(int, int)}, and
+ * the named-query methods ({@link AbstractService#findByNamedQuery(String, Map)},
+ * {@link AbstractService#findSingleByNamedQuery(String, Map)}). Nothing in this
+ * project calls those four methods or any of the fourteen {@code @NamedQuery}
+ * declarations across the domain classes - not a REST resource, not a JSF
+ * bean, not another test - so, until now, none of it was verified to actually
+ * work against a real persistence provider at all.
+ *
+ * Deploys a minimal {@link JavaArchive} - just the domain classes, {@code
+ * AbstractService}/{@code BookService}, the real {@code persistence.xml}, and
+ * an empty {@code beans.xml} - rather than the full WAR Testcontainers
+ * deploys, since this test only needs {@code BookService} injectable and a
+ * working persistence unit, not the JAX-RS/JSF layers.
+ */
+@ExtendWith(ArquillianExtension.class)
+class BookServiceArquillianIT {
+
+ @Deployment
+ public static JavaArchive createDeployment() {
+ return ShrinkWrap.create(JavaArchive.class, "book-service-arquillian-it.jar")
+ .addPackage(Book.class.getPackage())
+ .addClasses(AbstractService.class, BookService.class)
+ .addAsManifestResource("META-INF/persistence.xml", "persistence.xml")
+ .addAsManifestResource(EmptyAsset.INSTANCE, "beans.xml");
+ }
+
+ @Inject
+ private BookService bookService;
+
+ @Test
+ void countReflectsRealPersistedRows() {
+ int before = bookService.count();
+
+ bookService.create(newBook("Count Test One " + UUID.randomUUID()));
+ bookService.create(newBook("Count Test Two " + UUID.randomUUID()));
+ bookService.create(newBook("Count Test Three " + UUID.randomUUID()));
+
+ assertEquals(before + 3, bookService.count());
+ }
+
+ @Test
+ void findRangePaginatesWithoutOverlapOrLoss() {
+ for (int i = 0; i < 5; i++) {
+ bookService.create(newBook("Page Test " + i + " " + UUID.randomUUID()));
+ }
+
+ // AbstractService#findRange builds its CriteriaQuery without an
+ // explicit ORDER BY, so which rows land on which page is not
+ // guaranteed by the JPA spec - only that paging doesn't duplicate or
+ // drop rows is. Assert on that instead of on page contents.
+ List firstPage = bookService.findRange(0, 3);
+ List secondPage = bookService.findRange(3, 3);
+
+ assertEquals(3, firstPage.size());
+ assertFalse(secondPage.isEmpty());
+
+ Set firstPageIsbns = firstPage.stream().map(Book::getIsbn).collect(Collectors.toSet());
+ Set secondPageIsbns = secondPage.stream().map(Book::getIsbn).collect(Collectors.toSet());
+
+ assertTrue(java.util.Collections.disjoint(firstPageIsbns, secondPageIsbns),
+ "the same book showed up on both pages - pagination is broken");
+ }
+
+ @Test
+ void findByNamedQueryLocatesBookByTitle() {
+ String title = "Named Query Target " + UUID.randomUUID();
+ bookService.create(newBook(title));
+
+ // Book.findByTitle: "SELECT e FROM Book e WHERE e.title = :title"
+ // (see Book.java's @NamedQuery). Never invoked anywhere else in the
+ // project before this test - this is the first thing to ever confirm
+ // its JPQL actually compiles and runs against a real provider.
+ List found = bookService.findByNamedQuery("Book.findByTitle", paramsOf("title", title));
+
+ assertEquals(1, found.size());
+ assertEquals(title, found.get(0).getTitle());
+ }
+
+ @Test
+ void findSingleByNamedQueryIsEmptyWhenNoRowMatches() {
+ // Mirrors AbstractServiceTest#findOrEmptyReturnsEmptyOnNoResult, but
+ // that unit test proves findOrEmpty() correctly converts a
+ // hand-thrown NoResultException into Optional.empty() - it never
+ // provokes a real NoResultException from a real missed query. This
+ // does.
+ Optional found = bookService.findSingleByNamedQuery(
+ "Book.findByIsbn", paramsOf("isbn", "does-not-exist-" + UUID.randomUUID()));
+
+ assertTrue(found.isEmpty());
+ }
+
+ private Book newBook(String title) {
+ Book book = new Book();
+ book.setTitle(title);
+ book.setAuthor("Arquillian IT Author");
+ book.setPages(100);
+ return book;
+ }
+
+ private Map paramsOf(String key, Object value) {
+ Map params = new HashMap<>();
+ params.put(key, value);
+ return params;
+ }
+}
diff --git a/ecosystem/testcontainers-example/src/test/java/fish/payara/examples/service/AbstractServiceIT.java b/ecosystem/testcontainers-example/src/test/java/fish/payara/examples/service/AbstractServiceIT.java
new file mode 100644
index 0000000..8995f95
--- /dev/null
+++ b/ecosystem/testcontainers-example/src/test/java/fish/payara/examples/service/AbstractServiceIT.java
@@ -0,0 +1,78 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright (c) 2026 Payara Foundation and/or its affiliates. All rights reserved.
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * General Public License Version 2 only ("GPL") or the Common Development
+ * and Distribution License("CDDL") (collectively, the "License"). You
+ * may not use this file except in compliance with the License. You can
+ * obtain a copy of the License at
+ * https://github.com/payara/Payara/blob/master/LICENSE.txt
+ * See the License for the specific
+ * language governing permissions and limitations under the License.
+ *
+ * When distributing the software, include this License Header Notice in each
+ * file and include the License file at glassfish/legal/LICENSE.txt.
+ *
+ * GPL Classpath Exception:
+ * The Payara Foundation designates this particular file as subject to the "Classpath"
+ * exception as provided by the Payara Foundation in the GPL Version 2 section of the License
+ * file that accompanied this code.
+ *
+ * Modifications:
+ * If applicable, add the following below the License Header, with the fields
+ * enclosed by brackets [] replaced by your own identifying information:
+ * "Portions Copyright [year] [name of copyright owner]"
+ *
+ * Contributor(s):
+ * If you wish your version of this file to be governed by only the CDDL or
+ * only the GPL Version 2, indicate your decision by adding "[Contributor]
+ * elects to include this software in this distribution under the [CDDL or GPL
+ * Version 2] license." If you don't indicate a single choice of license, a
+ * recipient has the option to distribute your version of this file under
+ * either the CDDL, the GPL Version 2 or to extend the choice of license to
+ * its licensees as provided above. However, if you add GPL Version 2 code
+ * and therefore, elected the GPL Version 2 license, then the option applies
+ * only if the new code is made subject to such option by the copyright
+ * holder.
+ */
+package fish.payara.examples.service;
+
+import fish.payara.examples.testcontainers.AbstractContainerIT;
+import jakarta.ws.rs.client.Client;
+import jakarta.ws.rs.client.ClientBuilder;
+import jakarta.ws.rs.client.WebTarget;
+import org.glassfish.jersey.jackson.JacksonFeature;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+
+/**
+ * Base class for the REST integration tests running against the shared
+ * Payara Micro Testcontainer (see {@link AbstractContainerIT}).
+ *
+ * Subclasses only need to implement {@link #resourcePath()} to say which
+ * REST resource they exercise; the JAX-RS {@link Client} and the
+ * {@link #baseTarget} pointing at that resource are set up and torn down
+ * automatically before/after each test.
+ */
+abstract class AbstractServiceIT extends AbstractContainerIT {
+
+ protected Client client;
+ protected WebTarget baseTarget;
+
+ @BeforeEach
+ void setUpClient() {
+ client = ClientBuilder.newClient().register(JacksonFeature.class);
+ baseTarget = client.target(applicationContextUrl() + resourcePath());
+ }
+
+ @AfterEach
+ void tearDownClient() {
+ if (client != null) {
+ client.close();
+ }
+ }
+
+ protected abstract String resourcePath();
+}
diff --git a/ecosystem/testcontainers-example/src/test/java/fish/payara/examples/service/AbstractServiceTest.java b/ecosystem/testcontainers-example/src/test/java/fish/payara/examples/service/AbstractServiceTest.java
index 0f3891b..b18e540 100644
--- a/ecosystem/testcontainers-example/src/test/java/fish/payara/examples/service/AbstractServiceTest.java
+++ b/ecosystem/testcontainers-example/src/test/java/fish/payara/examples/service/AbstractServiceTest.java
@@ -1,3 +1,42 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright (c) 2026 Payara Foundation and/or its affiliates. All rights reserved.
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * General Public License Version 2 only ("GPL") or the Common Development
+ * and Distribution License("CDDL") (collectively, the "License"). You
+ * may not use this file except in compliance with the License. You can
+ * obtain a copy of the License at
+ * https://github.com/payara/Payara/blob/master/LICENSE.txt
+ * See the License for the specific
+ * language governing permissions and limitations under the License.
+ *
+ * When distributing the software, include this License Header Notice in each
+ * file and include the License file at glassfish/legal/LICENSE.txt.
+ *
+ * GPL Classpath Exception:
+ * The Payara Foundation designates this particular file as subject to the "Classpath"
+ * exception as provided by the Payara Foundation in the GPL Version 2 section of the License
+ * file that accompanied this code.
+ *
+ * Modifications:
+ * If applicable, add the following below the License Header, with the fields
+ * enclosed by brackets [] replaced by your own identifying information:
+ * "Portions Copyright [year] [name of copyright owner]"
+ *
+ * Contributor(s):
+ * If you wish your version of this file to be governed by only the CDDL or
+ * only the GPL Version 2, indicate your decision by adding "[Contributor]
+ * elects to include this software in this distribution under the [CDDL or GPL
+ * Version 2] license." If you don't indicate a single choice of license, a
+ * recipient has the option to distribute your version of this file under
+ * either the CDDL, the GPL Version 2 or to extend the choice of license to
+ * its licensees as provided above. However, if you add GPL Version 2 code
+ * and therefore, elected the GPL Version 2 license, then the option applies
+ * only if the new code is made subject to such option by the copyright
+ * holder.
+ */
package fish.payara.examples.service;
import jakarta.persistence.NoResultException;
diff --git a/ecosystem/testcontainers-example/src/test/java/fish/payara/examples/service/BookServiceIT.java b/ecosystem/testcontainers-example/src/test/java/fish/payara/examples/service/BookServiceIT.java
index 8082369..f588bfc 100644
--- a/ecosystem/testcontainers-example/src/test/java/fish/payara/examples/service/BookServiceIT.java
+++ b/ecosystem/testcontainers-example/src/test/java/fish/payara/examples/service/BookServiceIT.java
@@ -1,53 +1,60 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright (c) 2026 Payara Foundation and/or its affiliates. All rights reserved.
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * General Public License Version 2 only ("GPL") or the Common Development
+ * and Distribution License("CDDL") (collectively, the "License"). You
+ * may not use this file except in compliance with the License. You can
+ * obtain a copy of the License at
+ * https://github.com/payara/Payara/blob/master/LICENSE.txt
+ * See the License for the specific
+ * language governing permissions and limitations under the License.
+ *
+ * When distributing the software, include this License Header Notice in each
+ * file and include the License file at glassfish/legal/LICENSE.txt.
+ *
+ * GPL Classpath Exception:
+ * The Payara Foundation designates this particular file as subject to the "Classpath"
+ * exception as provided by the Payara Foundation in the GPL Version 2 section of the License
+ * file that accompanied this code.
+ *
+ * Modifications:
+ * If applicable, add the following below the License Header, with the fields
+ * enclosed by brackets [] replaced by your own identifying information:
+ * "Portions Copyright [year] [name of copyright owner]"
+ *
+ * Contributor(s):
+ * If you wish your version of this file to be governed by only the CDDL or
+ * only the GPL Version 2, indicate your decision by adding "[Contributor]
+ * elects to include this software in this distribution under the [CDDL or GPL
+ * Version 2] license." If you don't indicate a single choice of license, a
+ * recipient has the option to distribute your version of this file under
+ * either the CDDL, the GPL Version 2 or to extend the choice of license to
+ * its licensees as provided above. However, if you add GPL Version 2 code
+ * and therefore, elected the GPL Version 2 license, then the option applies
+ * only if the new code is made subject to such option by the copyright
+ * holder.
+ */
package fish.payara.examples.service;
import fish.payara.examples.domain.Book;
-import org.junit.jupiter.api.Test;
-import fish.payara.examples.testcontainers.PayaraMicroContainer;
-import org.testcontainers.junit.jupiter.Container;
-import org.testcontainers.junit.jupiter.Testcontainers;
-import org.testcontainers.utility.DockerImageName;
-import jakarta.ws.rs.client.Client;
-import jakarta.ws.rs.core.GenericType;
-import jakarta.ws.rs.client.ClientBuilder;
import jakarta.ws.rs.client.Entity;
-import jakarta.ws.rs.client.WebTarget;
+import jakarta.ws.rs.core.GenericType;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
-@Testcontainers
-class BookServiceIT {
-
- private static final String PAYARA_MICRO_VERSION = "6.2025.10";
- private static final int EXPOSED_PORT = 8080;
-
- @Container
- private final PayaraMicroContainer payara = new PayaraMicroContainer(
- DockerImageName.parse("payara/micro:" + PAYARA_MICRO_VERSION))
- .withExposedPorts(EXPOSED_PORT)
- .withDeploymentPath("target/testcontainers-example-1.0.0.war");
-
- private WebTarget baseTarget;
- private Client client;
-
- @AfterEach
- void tearDown() {
- if (client != null) {
- client.close();
- }
- }
+class BookServiceIT extends AbstractServiceIT {
- @BeforeEach
- void setUp() {
- client = ClientBuilder.newClient();
- String appUrl = payara.getApplicationUrl();
- String separator = appUrl.endsWith("/") ? "" : "/";
- String baseUri = appUrl + separator + "application/resources/books";
- baseTarget = client.target(baseUri);
+ @Override
+ protected String resourcePath() {
+ return "resources/books";
}
@Test
@@ -74,8 +81,7 @@ void testCreateAndRetrieveBook() {
}
if (createResponse.getStatus() != Response.Status.CREATED.getStatusCode()) {
- String logs = "";
- try { logs = payara.getLogs(); } catch (Exception ignored) {}
+ String logs = containerLogs();
String msg = String.format("POST failed, status=%d, body=%s, container-logs-start:\n%s\n:container-logs-end", createResponse.getStatus(), respBody, logs);
assertEquals(Response.Status.CREATED.getStatusCode(), createResponse.getStatus(), msg);
}
@@ -83,15 +89,15 @@ void testCreateAndRetrieveBook() {
// Get the created book's ISBN from the Location header
String location = createResponse.getHeaderString("Location");
assertNotNull(location, "Location header missing; POST response body=" + respBody);
-
+
// GET the book and verify its contents
Response getResponse = client.target(location)
.request(MediaType.APPLICATION_JSON)
.get();
-
+
assertEquals(Response.Status.OK.getStatusCode(), getResponse.getStatus());
Book retrievedBook = getResponse.readEntity(Book.class);
-
+
assertNotNull(retrievedBook);
assertEquals("Integration Test Book", retrievedBook.getTitle());
assertEquals("Test Author", retrievedBook.getAuthor());
@@ -130,12 +136,11 @@ void testFindAllBooks() {
if (response.getStatus() != Response.Status.OK.getStatusCode()) {
try { respBody = response.readEntity(String.class); } catch (Exception e) { respBody = ""; }
- String logs = "";
- try { logs = payara.getLogs(); } catch (Exception ignored) {}
+ String logs = containerLogs();
String msg = String.format("GET all failed, status=%d, body=%s, container-logs-start:\n%s\n:container-logs-end", response.getStatus(), respBody, logs);
assertEquals(Response.Status.OK.getStatusCode(), response.getStatus(), msg);
}
-
+
List books = response.readEntity(new GenericType>() {});
assertNotNull(books);
assertTrue(books.size() >= 2);
@@ -153,7 +158,7 @@ void testUpdateBook() {
Response createResponse = baseTarget
.request(MediaType.APPLICATION_JSON)
.post(Entity.entity(book, MediaType.APPLICATION_JSON));
-
+
String location = createResponse.getHeaderString("Location");
assertNotNull(location, "Location header missing after create");
Book createdBook = client.target(location)
@@ -162,7 +167,7 @@ void testUpdateBook() {
// Update the book
createdBook.setTitle("Updated Title");
-
+
Response updateResponse = client.target(location)
.request(MediaType.APPLICATION_JSON)
.put(Entity.entity(createdBook, MediaType.APPLICATION_JSON));
@@ -190,7 +195,7 @@ void testDeleteBook() {
Response createResponse = baseTarget
.request(MediaType.APPLICATION_JSON)
.post(Entity.entity(book, MediaType.APPLICATION_JSON));
-
+
String location = createResponse.getHeaderString("Location");
assertNotNull(location, "Location header missing after create");
@@ -208,4 +213,4 @@ void testDeleteBook() {
assertEquals(Response.Status.NOT_FOUND.getStatusCode(), getResponse.getStatus());
}
-}
\ No newline at end of file
+}
diff --git a/ecosystem/testcontainers-example/src/test/java/fish/payara/examples/service/BookServiceTest.java b/ecosystem/testcontainers-example/src/test/java/fish/payara/examples/service/BookServiceTest.java
index 19f72a8..7688fc4 100644
--- a/ecosystem/testcontainers-example/src/test/java/fish/payara/examples/service/BookServiceTest.java
+++ b/ecosystem/testcontainers-example/src/test/java/fish/payara/examples/service/BookServiceTest.java
@@ -1,3 +1,42 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright (c) 2026 Payara Foundation and/or its affiliates. All rights reserved.
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * General Public License Version 2 only ("GPL") or the Common Development
+ * and Distribution License("CDDL") (collectively, the "License"). You
+ * may not use this file except in compliance with the License. You can
+ * obtain a copy of the License at
+ * https://github.com/payara/Payara/blob/master/LICENSE.txt
+ * See the License for the specific
+ * language governing permissions and limitations under the License.
+ *
+ * When distributing the software, include this License Header Notice in each
+ * file and include the License file at glassfish/legal/LICENSE.txt.
+ *
+ * GPL Classpath Exception:
+ * The Payara Foundation designates this particular file as subject to the "Classpath"
+ * exception as provided by the Payara Foundation in the GPL Version 2 section of the License
+ * file that accompanied this code.
+ *
+ * Modifications:
+ * If applicable, add the following below the License Header, with the fields
+ * enclosed by brackets [] replaced by your own identifying information:
+ * "Portions Copyright [year] [name of copyright owner]"
+ *
+ * Contributor(s):
+ * If you wish your version of this file to be governed by only the CDDL or
+ * only the GPL Version 2, indicate your decision by adding "[Contributor]
+ * elects to include this software in this distribution under the [CDDL or GPL
+ * Version 2] license." If you don't indicate a single choice of license, a
+ * recipient has the option to distribute your version of this file under
+ * either the CDDL, the GPL Version 2 or to extend the choice of license to
+ * its licensees as provided above. However, if you add GPL Version 2 code
+ * and therefore, elected the GPL Version 2 license, then the option applies
+ * only if the new code is made subject to such option by the copyright
+ * holder.
+ */
package fish.payara.examples.service;
import fish.payara.examples.domain.Book;
diff --git a/ecosystem/testcontainers-example/src/test/java/fish/payara/examples/service/LibrarianServiceIT.java b/ecosystem/testcontainers-example/src/test/java/fish/payara/examples/service/LibrarianServiceIT.java
index 90b918a..86bffc2 100644
--- a/ecosystem/testcontainers-example/src/test/java/fish/payara/examples/service/LibrarianServiceIT.java
+++ b/ecosystem/testcontainers-example/src/test/java/fish/payara/examples/service/LibrarianServiceIT.java
@@ -1,53 +1,64 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright (c) 2026 Payara Foundation and/or its affiliates. All rights reserved.
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * General Public License Version 2 only ("GPL") or the Common Development
+ * and Distribution License("CDDL") (collectively, the "License"). You
+ * may not use this file except in compliance with the License. You can
+ * obtain a copy of the License at
+ * https://github.com/payara/Payara/blob/master/LICENSE.txt
+ * See the License for the specific
+ * language governing permissions and limitations under the License.
+ *
+ * When distributing the software, include this License Header Notice in each
+ * file and include the License file at glassfish/legal/LICENSE.txt.
+ *
+ * GPL Classpath Exception:
+ * The Payara Foundation designates this particular file as subject to the "Classpath"
+ * exception as provided by the Payara Foundation in the GPL Version 2 section of the License
+ * file that accompanied this code.
+ *
+ * Modifications:
+ * If applicable, add the following below the License Header, with the fields
+ * enclosed by brackets [] replaced by your own identifying information:
+ * "Portions Copyright [year] [name of copyright owner]"
+ *
+ * Contributor(s):
+ * If you wish your version of this file to be governed by only the CDDL or
+ * only the GPL Version 2, indicate your decision by adding "[Contributor]
+ * elects to include this software in this distribution under the [CDDL or GPL
+ * Version 2] license." If you don't indicate a single choice of license, a
+ * recipient has the option to distribute your version of this file under
+ * either the CDDL, the GPL Version 2 or to extend the choice of license to
+ * its licensees as provided above. However, if you add GPL Version 2 code
+ * and therefore, elected the GPL Version 2 license, then the option applies
+ * only if the new code is made subject to such option by the copyright
+ * holder.
+ */
package fish.payara.examples.service;
import fish.payara.examples.domain.Librarian;
-import fish.payara.examples.testcontainers.PayaraMicroContainer;
-import jakarta.ws.rs.client.Client;
-import jakarta.ws.rs.client.ClientBuilder;
import jakarta.ws.rs.client.Entity;
-import jakarta.ws.rs.client.WebTarget;
import jakarta.ws.rs.core.GenericType;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
-import org.junit.jupiter.api.*;
-import org.testcontainers.junit.jupiter.Container;
-import org.testcontainers.junit.jupiter.Testcontainers;
-import org.testcontainers.utility.DockerImageName;
+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 java.util.List;
import static org.junit.jupiter.api.Assertions.*;
-@Testcontainers
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
-class LibrarianServiceIT {
-
- private static final String PAYARA_MICRO_VERSION = "6.2025.10";
- private static final int EXPOSED_PORT = 8080;
-
- @Container
- private static final PayaraMicroContainer payara = new PayaraMicroContainer(
- DockerImageName.parse("payara/micro:" + PAYARA_MICRO_VERSION))
- .withExposedPorts(EXPOSED_PORT)
- .withDeploymentPath("target/testcontainers-example-1.0.0.war");
-
- private Client client;
- private WebTarget baseTarget;
-
- @BeforeEach
- void setUp() {
- client = ClientBuilder.newClient();
- String appUrl = payara.getApplicationUrl();
- String separator = appUrl.endsWith("/") ? "" : "/";
- String baseUri = appUrl + separator + "application/resources/librarians";
- baseTarget = client.target(baseUri);
- }
+class LibrarianServiceIT extends AbstractServiceIT {
- @AfterEach
- void tearDown() {
- if (client != null) {
- client.close();
- }
+ @Override
+ protected String resourcePath() {
+ return "resources/librarians";
}
@Test
@@ -75,7 +86,7 @@ void testCreateAndRetrieveLibrarian() throws Exception {
} catch (Exception e) {
respBody = "";
}
- String logs = payara.getLogs();
+ String logs = containerLogs();
String msg = String.format("POST failed, status=%d, body=%s, container logs:\n%s", createResponse.getStatus(), respBody, logs);
assertEquals(Response.Status.CREATED.getStatusCode(), createResponse.getStatus(), msg);
}
@@ -113,7 +124,7 @@ void testFindAllLibrarians() throws Exception {
} catch (Exception e) {
respBody = "";
}
- String logs = payara.getLogs();
+ String logs = containerLogs();
String msg = String.format("GET all failed, status=%d, body=%s, logs:\n%s", response.getStatus(), respBody, logs);
assertEquals(Response.Status.OK.getStatusCode(), response.getStatus(), msg);
}
diff --git a/ecosystem/testcontainers-example/src/test/java/fish/payara/examples/service/LibrarianServiceTest.java b/ecosystem/testcontainers-example/src/test/java/fish/payara/examples/service/LibrarianServiceTest.java
index fadf3f3..1cddab6 100644
--- a/ecosystem/testcontainers-example/src/test/java/fish/payara/examples/service/LibrarianServiceTest.java
+++ b/ecosystem/testcontainers-example/src/test/java/fish/payara/examples/service/LibrarianServiceTest.java
@@ -1,6 +1,44 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright (c) 2026 Payara Foundation and/or its affiliates. All rights reserved.
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * General Public License Version 2 only ("GPL") or the Common Development
+ * and Distribution License("CDDL") (collectively, the "License"). You
+ * may not use this file except in compliance with the License. You can
+ * obtain a copy of the License at
+ * https://github.com/payara/Payara/blob/master/LICENSE.txt
+ * See the License for the specific
+ * language governing permissions and limitations under the License.
+ *
+ * When distributing the software, include this License Header Notice in each
+ * file and include the License file at glassfish/legal/LICENSE.txt.
+ *
+ * GPL Classpath Exception:
+ * The Payara Foundation designates this particular file as subject to the "Classpath"
+ * exception as provided by the Payara Foundation in the GPL Version 2 section of the License
+ * file that accompanied this code.
+ *
+ * Modifications:
+ * If applicable, add the following below the License Header, with the fields
+ * enclosed by brackets [] replaced by your own identifying information:
+ * "Portions Copyright [year] [name of copyright owner]"
+ *
+ * Contributor(s):
+ * If you wish your version of this file to be governed by only the CDDL or
+ * only the GPL Version 2, indicate your decision by adding "[Contributor]
+ * elects to include this software in this distribution under the [CDDL or GPL
+ * Version 2] license." If you don't indicate a single choice of license, a
+ * recipient has the option to distribute your version of this file under
+ * either the CDDL, the GPL Version 2 or to extend the choice of license to
+ * its licensees as provided above. However, if you add GPL Version 2 code
+ * and therefore, elected the GPL Version 2 license, then the option applies
+ * only if the new code is made subject to such option by the copyright
+ * holder.
+ */
package fish.payara.examples.service;
-import fish.payara.examples.domain.Book;
import fish.payara.examples.domain.Librarian;
import jakarta.persistence.EntityManager;
import jakarta.persistence.TypedQuery;
@@ -60,6 +98,7 @@ void createAndFind() {
when(entityManager.find(eq(Librarian.class), eq("lib-1"))).thenReturn(l);
Librarian found = librarianService.find("lib-1");
assertNotNull(found);
+ assertEquals("Libby", found.getName());
}
@Test
@@ -70,20 +109,20 @@ void findAll() {
CriteriaBuilder cb = mock(CriteriaBuilder.class);
CriteriaQuery cq = mock(CriteriaQuery.class);
- Root root = mock(Root.class);
+ Root root = mock(Root.class);
when(entityManager.getCriteriaBuilder()).thenReturn(cb);
when(cb.createQuery()).thenReturn(cq);
- when(cq.from(Book.class)).thenReturn(root);
+ when(cq.from(Librarian.class)).thenReturn(root);
when(cq.select(root)).thenReturn(cq);
- @SuppressWarnings("unchecked")
TypedQuery typedQuery = mock(TypedQuery.class);
when(entityManager.createQuery(cq)).thenReturn(typedQuery);
when(typedQuery.getResultList()).thenReturn(list);
- when(entityManager.createQuery(any(), eq(Librarian.class))).thenReturn(typedQuery);
List result = librarianService.findAll();
+
+ verify(cq).from(Librarian.class);
assertEquals(2, result.size());
}
diff --git a/ecosystem/testcontainers-example/src/test/java/fish/payara/examples/service/LoanServiceIT.java b/ecosystem/testcontainers-example/src/test/java/fish/payara/examples/service/LoanServiceIT.java
new file mode 100644
index 0000000..4ed5aff
--- /dev/null
+++ b/ecosystem/testcontainers-example/src/test/java/fish/payara/examples/service/LoanServiceIT.java
@@ -0,0 +1,220 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright (c) 2026 Payara Foundation and/or its affiliates. All rights reserved.
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * General Public License Version 2 only ("GPL") or the Common Development
+ * and Distribution License("CDDL") (collectively, the "License"). You
+ * may not use this file except in compliance with the License. You can
+ * obtain a copy of the License at
+ * https://github.com/payara/Payara/blob/master/LICENSE.txt
+ * See the License for the specific
+ * language governing permissions and limitations under the License.
+ *
+ * When distributing the software, include this License Header Notice in each
+ * file and include the License file at glassfish/legal/LICENSE.txt.
+ *
+ * GPL Classpath Exception:
+ * The Payara Foundation designates this particular file as subject to the "Classpath"
+ * exception as provided by the Payara Foundation in the GPL Version 2 section of the License
+ * file that accompanied this code.
+ *
+ * Modifications:
+ * If applicable, add the following below the License Header, with the fields
+ * enclosed by brackets [] replaced by your own identifying information:
+ * "Portions Copyright [year] [name of copyright owner]"
+ *
+ * Contributor(s):
+ * If you wish your version of this file to be governed by only the CDDL or
+ * only the GPL Version 2, indicate your decision by adding "[Contributor]
+ * elects to include this software in this distribution under the [CDDL or GPL
+ * Version 2] license." If you don't indicate a single choice of license, a
+ * recipient has the option to distribute your version of this file under
+ * either the CDDL, the GPL Version 2 or to extend the choice of license to
+ * its licensees as provided above. However, if you add GPL Version 2 code
+ * and therefore, elected the GPL Version 2 license, then the option applies
+ * only if the new code is made subject to such option by the copyright
+ * holder.
+ */
+package fish.payara.examples.service;
+
+import jakarta.ws.rs.client.Entity;
+import jakarta.ws.rs.core.GenericType;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Response;
+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 java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
+class LoanServiceIT extends AbstractServiceIT {
+
+ @Override
+ protected String resourcePath() {
+ return "resources/loans";
+ }
+
+ @Test
+ @Order(1)
+ void testCreateAndRetrieveLoan() {
+ String librarianId = createLibrarian("Loan IT Librarian");
+ String patronId = createPatron("Loan IT Patron");
+ String isbn = createBook("Loan IT Book");
+
+ Response createResponse = createLoan(loanBody(librarianId, patronId, isbn,
+ "2026-07-14T09:00:00", "2026-07-28T09:00:00"));
+
+ assertEquals(Response.Status.CREATED.getStatusCode(), createResponse.getStatus());
+ String location = createResponse.getHeaderString("Location");
+ assertNotNull(location, "Location header missing after create");
+
+ Map retrieved = client.target(location)
+ .request(MediaType.APPLICATION_JSON)
+ .get(new GenericType>() {});
+
+ assertEquals("2026-07-14T09:00:00", retrieved.get("loanDate"));
+ assertEquals("2026-07-28T09:00:00", retrieved.get("returnDate"));
+ assertEquals(librarianId, asMap(retrieved.get("librarian")).get("librarianID"));
+ assertEquals(patronId, asMap(retrieved.get("patron")).get("patronID"));
+ assertEquals(isbn, asMap(retrieved.get("book")).get("isbn"));
+ }
+
+ @Test
+ @Order(2)
+ void testFindAllLoans() {
+ Response response = baseTarget.request(MediaType.APPLICATION_JSON).get();
+ assertEquals(Response.Status.OK.getStatusCode(), response.getStatus());
+
+ List> loans = response.readEntity(new GenericType>>() {});
+ assertNotNull(loans);
+ assertFalse(loans.isEmpty(), "Expected at least the loan created by testCreateAndRetrieveLoan");
+ }
+
+ @Test
+ @Order(3)
+ void testUpdateLoan() {
+ String librarianId = createLibrarian("Loan IT Librarian Update");
+ String patronId = createPatron("Loan IT Patron Update");
+ String isbn = createBook("Loan IT Book Update");
+
+ Response createResponse = createLoan(loanBody(librarianId, patronId, isbn,
+ "2026-07-14T09:00:00", "2026-07-28T09:00:00"));
+ String location = createResponse.getHeaderString("Location");
+ assertNotNull(location, "Location header missing after create");
+
+ Map updateBody = loanBody(librarianId, patronId, isbn,
+ "2026-07-14T09:00:00", "2026-08-04T09:00:00");
+
+ Response updateResponse = client.target(location)
+ .request(MediaType.APPLICATION_JSON)
+ .put(Entity.entity(updateBody, MediaType.APPLICATION_JSON));
+
+ assertEquals(Response.Status.OK.getStatusCode(), updateResponse.getStatus());
+
+ Map retrieved = client.target(location)
+ .request(MediaType.APPLICATION_JSON)
+ .get(new GenericType>() {});
+
+ assertEquals("2026-08-04T09:00:00", retrieved.get("returnDate"));
+ }
+
+ @Test
+ @Order(4)
+ void testDeleteLoan() {
+ String librarianId = createLibrarian("Loan IT Librarian Delete");
+ String patronId = createPatron("Loan IT Patron Delete");
+ String isbn = createBook("Loan IT Book Delete");
+
+ Response createResponse = createLoan(loanBody(librarianId, patronId, isbn,
+ "2026-07-14T09:00:00", "2026-07-28T09:00:00"));
+ String location = createResponse.getHeaderString("Location");
+ assertNotNull(location, "Location header missing after create");
+
+ Response deleteResponse = client.target(location).request().delete();
+ assertEquals(Response.Status.NO_CONTENT.getStatusCode(), deleteResponse.getStatus());
+
+ Response getResponse = client.target(location)
+ .request(MediaType.APPLICATION_JSON)
+ .get();
+ assertEquals(Response.Status.NOT_FOUND.getStatusCode(), getResponse.getStatus());
+ }
+
+ private Response createLoan(Map loan) {
+ return baseTarget.request(MediaType.APPLICATION_JSON)
+ .post(Entity.entity(loan, MediaType.APPLICATION_JSON));
+ }
+
+ private Map loanBody(String librarianId, String patronId, String isbn,
+ String loanDate, String returnDate) {
+ Map loan = new LinkedHashMap<>();
+ loan.put("loanDate", loanDate);
+ loan.put("returnDate", returnDate);
+ loan.put("librarian", singleField("librarianID", librarianId));
+ loan.put("patron", singleField("patronID", patronId));
+ loan.put("book", singleField("isbn", isbn));
+ return loan;
+ }
+
+ private Map singleField(String key, String value) {
+ Map map = new LinkedHashMap<>();
+ map.put(key, value);
+ return map;
+ }
+
+ @SuppressWarnings("unchecked")
+ private Map asMap(Object value) {
+ return (Map) value;
+ }
+
+ private String createLibrarian(String name) {
+ Map librarian = new LinkedHashMap<>();
+ librarian.put("name", name);
+
+ Response response = client.target(applicationContextUrl() + "resources/librarians")
+ .request(MediaType.APPLICATION_JSON)
+ .post(Entity.entity(librarian, MediaType.APPLICATION_JSON));
+ assertEquals(Response.Status.CREATED.getStatusCode(), response.getStatus(),
+ "Failed to create prerequisite librarian");
+
+ Map created = response.readEntity(new GenericType>() {});
+ return (String) created.get("librarianID");
+ }
+
+ private String createPatron(String name) {
+ Map patron = new LinkedHashMap<>();
+ patron.put("name", name);
+
+ Response response = client.target(applicationContextUrl() + "resources/patrons")
+ .request(MediaType.APPLICATION_JSON)
+ .post(Entity.entity(patron, MediaType.APPLICATION_JSON));
+ assertEquals(Response.Status.CREATED.getStatusCode(), response.getStatus(),
+ "Failed to create prerequisite patron");
+
+ Map created = response.readEntity(new GenericType>() {});
+ return (String) created.get("patronID");
+ }
+
+ private String createBook(String title) {
+ Map book = new LinkedHashMap<>();
+ book.put("title", title);
+ book.put("author", "Loan IT Author");
+ book.put("pages", 100);
+
+ Response response = client.target(applicationContextUrl() + "resources/books")
+ .request(MediaType.APPLICATION_JSON)
+ .post(Entity.entity(book, MediaType.APPLICATION_JSON));
+ assertEquals(Response.Status.CREATED.getStatusCode(), response.getStatus(),
+ "Failed to create prerequisite book");
+
+ Map created = response.readEntity(new GenericType>() {});
+ return (String) created.get("isbn");
+ }
+}
diff --git a/ecosystem/testcontainers-example/src/test/java/fish/payara/examples/service/LoanServiceTest.java b/ecosystem/testcontainers-example/src/test/java/fish/payara/examples/service/LoanServiceTest.java
index dd91fe7..a94c224 100644
--- a/ecosystem/testcontainers-example/src/test/java/fish/payara/examples/service/LoanServiceTest.java
+++ b/ecosystem/testcontainers-example/src/test/java/fish/payara/examples/service/LoanServiceTest.java
@@ -1,6 +1,44 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright (c) 2026 Payara Foundation and/or its affiliates. All rights reserved.
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * General Public License Version 2 only ("GPL") or the Common Development
+ * and Distribution License("CDDL") (collectively, the "License"). You
+ * may not use this file except in compliance with the License. You can
+ * obtain a copy of the License at
+ * https://github.com/payara/Payara/blob/master/LICENSE.txt
+ * See the License for the specific
+ * language governing permissions and limitations under the License.
+ *
+ * When distributing the software, include this License Header Notice in each
+ * file and include the License file at glassfish/legal/LICENSE.txt.
+ *
+ * GPL Classpath Exception:
+ * The Payara Foundation designates this particular file as subject to the "Classpath"
+ * exception as provided by the Payara Foundation in the GPL Version 2 section of the License
+ * file that accompanied this code.
+ *
+ * Modifications:
+ * If applicable, add the following below the License Header, with the fields
+ * enclosed by brackets [] replaced by your own identifying information:
+ * "Portions Copyright [year] [name of copyright owner]"
+ *
+ * Contributor(s):
+ * If you wish your version of this file to be governed by only the CDDL or
+ * only the GPL Version 2, indicate your decision by adding "[Contributor]
+ * elects to include this software in this distribution under the [CDDL or GPL
+ * Version 2] license." If you don't indicate a single choice of license, a
+ * recipient has the option to distribute your version of this file under
+ * either the CDDL, the GPL Version 2 or to extend the choice of license to
+ * its licensees as provided above. However, if you add GPL Version 2 code
+ * and therefore, elected the GPL Version 2 license, then the option applies
+ * only if the new code is made subject to such option by the copyright
+ * holder.
+ */
package fish.payara.examples.service;
-import fish.payara.examples.domain.Book;
import fish.payara.examples.domain.Loan;
import jakarta.persistence.EntityManager;
import jakarta.persistence.TypedQuery;
@@ -52,7 +90,8 @@ void setUp() {
@Test
void createAndFind() {
Loan loan = new Loan();
- loan.setLoanDate(LocalDateTime.now());
+ LocalDateTime loanDate = LocalDateTime.now();
+ loan.setLoanDate(loanDate);
doNothing().when(entityManager).persist(any(Loan.class));
loanService.create(loan);
@@ -61,6 +100,7 @@ void createAndFind() {
when(entityManager.find(eq(Loan.class), eq(1))).thenReturn(loan);
Loan found = loanService.find(1);
assertNotNull(found);
+ assertEquals(loanDate, found.getLoanDate());
}
@Test
@@ -71,20 +111,20 @@ void findAll() {
CriteriaBuilder cb = mock(CriteriaBuilder.class);
CriteriaQuery cq = mock(CriteriaQuery.class);
- Root root = mock(Root.class);
+ Root root = mock(Root.class);
when(entityManager.getCriteriaBuilder()).thenReturn(cb);
when(cb.createQuery()).thenReturn(cq);
- when(cq.from(Book.class)).thenReturn(root);
+ when(cq.from(Loan.class)).thenReturn(root);
when(cq.select(root)).thenReturn(cq);
- @SuppressWarnings("unchecked")
TypedQuery typedQuery = mock(TypedQuery.class);
when(entityManager.createQuery(cq)).thenReturn(typedQuery);
when(typedQuery.getResultList()).thenReturn(list);
- when(entityManager.createQuery(any(), eq(Loan.class))).thenReturn(typedQuery);
List result = loanService.findAll();
+
+ verify(cq).from(Loan.class);
assertEquals(2, result.size());
}
diff --git a/ecosystem/testcontainers-example/src/test/java/fish/payara/examples/service/PatronServiceIT.java b/ecosystem/testcontainers-example/src/test/java/fish/payara/examples/service/PatronServiceIT.java
index 0b67b1d..b3a6b3f 100644
--- a/ecosystem/testcontainers-example/src/test/java/fish/payara/examples/service/PatronServiceIT.java
+++ b/ecosystem/testcontainers-example/src/test/java/fish/payara/examples/service/PatronServiceIT.java
@@ -1,46 +1,64 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright (c) 2026 Payara Foundation and/or its affiliates. All rights reserved.
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * General Public License Version 2 only ("GPL") or the Common Development
+ * and Distribution License("CDDL") (collectively, the "License"). You
+ * may not use this file except in compliance with the License. You can
+ * obtain a copy of the License at
+ * https://github.com/payara/Payara/blob/master/LICENSE.txt
+ * See the License for the specific
+ * language governing permissions and limitations under the License.
+ *
+ * When distributing the software, include this License Header Notice in each
+ * file and include the License file at glassfish/legal/LICENSE.txt.
+ *
+ * GPL Classpath Exception:
+ * The Payara Foundation designates this particular file as subject to the "Classpath"
+ * exception as provided by the Payara Foundation in the GPL Version 2 section of the License
+ * file that accompanied this code.
+ *
+ * Modifications:
+ * If applicable, add the following below the License Header, with the fields
+ * enclosed by brackets [] replaced by your own identifying information:
+ * "Portions Copyright [year] [name of copyright owner]"
+ *
+ * Contributor(s):
+ * If you wish your version of this file to be governed by only the CDDL or
+ * only the GPL Version 2, indicate your decision by adding "[Contributor]
+ * elects to include this software in this distribution under the [CDDL or GPL
+ * Version 2] license." If you don't indicate a single choice of license, a
+ * recipient has the option to distribute your version of this file under
+ * either the CDDL, the GPL Version 2 or to extend the choice of license to
+ * its licensees as provided above. However, if you add GPL Version 2 code
+ * and therefore, elected the GPL Version 2 license, then the option applies
+ * only if the new code is made subject to such option by the copyright
+ * holder.
+ */
package fish.payara.examples.service;
import fish.payara.examples.domain.Patron;
-import fish.payara.examples.testcontainers.PayaraMicroContainer;
-import jakarta.ws.rs.client.*;
-import jakarta.ws.rs.core.*;
-import org.junit.jupiter.api.*;
-import org.testcontainers.junit.jupiter.Container;
-import org.testcontainers.junit.jupiter.Testcontainers;
-import org.testcontainers.utility.DockerImageName;
+import jakarta.ws.rs.client.Entity;
+import jakarta.ws.rs.core.GenericType;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Response;
+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 java.util.List;
import static org.junit.jupiter.api.Assertions.*;
-@Testcontainers
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
-class PatronServiceIT {
-
- private static final String PAYARA_MICRO_VERSION = "6.2025.10";
- private static final int EXPOSED_PORT = 8080;
-
- @Container
- private static final PayaraMicroContainer payara = new PayaraMicroContainer(
- DockerImageName.parse("payara/micro:" + PAYARA_MICRO_VERSION))
- .withExposedPorts(EXPOSED_PORT)
- .withDeploymentPath("target/testcontainers-example-1.0.0.war");
-
- private Client client;
- private WebTarget baseTarget;
-
- @BeforeEach
- void setUp() {
- client = ClientBuilder.newClient();
- String appUrl = payara.getApplicationUrl();
- String separator = appUrl.endsWith("/") ? "" : "/";
- String baseUri = appUrl + separator + "application/resources/patrons";
- baseTarget = client.target(baseUri);
- }
+class PatronServiceIT extends AbstractServiceIT {
- @AfterEach
- void tearDown() {
- client.close();
+ @Override
+ protected String resourcePath() {
+ return "resources/patrons";
}
@Test
@@ -88,6 +106,7 @@ void testUpdatePatron() {
.post(Entity.entity(patron, MediaType.APPLICATION_JSON));
String location = createResponse.getHeaderString("Location");
+ assertNotNull(location, "Location header missing after create");
patron.setEmail("jane.updated@example.com");
Response updateResponse = client.target(location)
@@ -95,6 +114,13 @@ void testUpdatePatron() {
.put(Entity.entity(patron, MediaType.APPLICATION_JSON));
assertEquals(Response.Status.OK.getStatusCode(), updateResponse.getStatus());
+
+ Patron updated = client.target(location)
+ .request(MediaType.APPLICATION_JSON)
+ .get(Patron.class);
+
+ assertEquals("jane.updated@example.com", updated.getEmail());
+ assertEquals("Jane Doe", updated.getName());
}
@Test
@@ -109,7 +135,14 @@ void testDeletePatron() {
.post(Entity.entity(patron, MediaType.APPLICATION_JSON));
String location = createResponse.getHeaderString("Location");
+ assertNotNull(location, "Location header missing after create");
+
Response deleteResponse = client.target(location).request().delete();
assertEquals(Response.Status.NO_CONTENT.getStatusCode(), deleteResponse.getStatus());
+
+ Response getResponse = client.target(location)
+ .request(MediaType.APPLICATION_JSON)
+ .get();
+ assertEquals(Response.Status.NOT_FOUND.getStatusCode(), getResponse.getStatus());
}
}
diff --git a/ecosystem/testcontainers-example/src/test/java/fish/payara/examples/service/PatronServiceTest.java b/ecosystem/testcontainers-example/src/test/java/fish/payara/examples/service/PatronServiceTest.java
index 64d6b42..8036307 100644
--- a/ecosystem/testcontainers-example/src/test/java/fish/payara/examples/service/PatronServiceTest.java
+++ b/ecosystem/testcontainers-example/src/test/java/fish/payara/examples/service/PatronServiceTest.java
@@ -1,6 +1,44 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright (c) 2026 Payara Foundation and/or its affiliates. All rights reserved.
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * General Public License Version 2 only ("GPL") or the Common Development
+ * and Distribution License("CDDL") (collectively, the "License"). You
+ * may not use this file except in compliance with the License. You can
+ * obtain a copy of the License at
+ * https://github.com/payara/Payara/blob/master/LICENSE.txt
+ * See the License for the specific
+ * language governing permissions and limitations under the License.
+ *
+ * When distributing the software, include this License Header Notice in each
+ * file and include the License file at glassfish/legal/LICENSE.txt.
+ *
+ * GPL Classpath Exception:
+ * The Payara Foundation designates this particular file as subject to the "Classpath"
+ * exception as provided by the Payara Foundation in the GPL Version 2 section of the License
+ * file that accompanied this code.
+ *
+ * Modifications:
+ * If applicable, add the following below the License Header, with the fields
+ * enclosed by brackets [] replaced by your own identifying information:
+ * "Portions Copyright [year] [name of copyright owner]"
+ *
+ * Contributor(s):
+ * If you wish your version of this file to be governed by only the CDDL or
+ * only the GPL Version 2, indicate your decision by adding "[Contributor]
+ * elects to include this software in this distribution under the [CDDL or GPL
+ * Version 2] license." If you don't indicate a single choice of license, a
+ * recipient has the option to distribute your version of this file under
+ * either the CDDL, the GPL Version 2 or to extend the choice of license to
+ * its licensees as provided above. However, if you add GPL Version 2 code
+ * and therefore, elected the GPL Version 2 license, then the option applies
+ * only if the new code is made subject to such option by the copyright
+ * holder.
+ */
package fish.payara.examples.service;
-import fish.payara.examples.domain.Book;
import fish.payara.examples.domain.Patron;
import jakarta.persistence.EntityManager;
import jakarta.persistence.TypedQuery;
@@ -71,20 +109,20 @@ void testFindAll() {
CriteriaBuilder cb = mock(CriteriaBuilder.class);
CriteriaQuery cq = mock(CriteriaQuery.class);
- Root root = mock(Root.class);
+ Root root = mock(Root.class);
when(entityManager.getCriteriaBuilder()).thenReturn(cb);
when(cb.createQuery()).thenReturn(cq);
- when(cq.from(Book.class)).thenReturn(root);
+ when(cq.from(Patron.class)).thenReturn(root);
when(cq.select(root)).thenReturn(cq);
- @SuppressWarnings("unchecked")
TypedQuery typedQuery = mock(TypedQuery.class);
when(entityManager.createQuery(cq)).thenReturn(typedQuery);
when(typedQuery.getResultList()).thenReturn(list);
- when(entityManager.createQuery(any(), eq(Patron.class))).thenReturn(typedQuery);
List result = patronService.findAll();
+
+ verify(cq).from(Patron.class);
assertNotNull(result);
assertEquals(2, result.size());
}
diff --git a/ecosystem/testcontainers-example/src/test/java/fish/payara/examples/testcontainers/AbstractContainerIT.java b/ecosystem/testcontainers-example/src/test/java/fish/payara/examples/testcontainers/AbstractContainerIT.java
new file mode 100644
index 0000000..aa76cd3
--- /dev/null
+++ b/ecosystem/testcontainers-example/src/test/java/fish/payara/examples/testcontainers/AbstractContainerIT.java
@@ -0,0 +1,79 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright (c) 2026 Payara Foundation and/or its affiliates. All rights reserved.
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * General Public License Version 2 only ("GPL") or the Common Development
+ * and Distribution License("CDDL") (collectively, the "License"). You
+ * may not use this file except in compliance with the License. You can
+ * obtain a copy of the License at
+ * https://github.com/payara/Payara/blob/master/LICENSE.txt
+ * See the License for the specific
+ * language governing permissions and limitations under the License.
+ *
+ * When distributing the software, include this License Header Notice in each
+ * file and include the License file at glassfish/legal/LICENSE.txt.
+ *
+ * GPL Classpath Exception:
+ * The Payara Foundation designates this particular file as subject to the "Classpath"
+ * exception as provided by the Payara Foundation in the GPL Version 2 section of the License
+ * file that accompanied this code.
+ *
+ * Modifications:
+ * If applicable, add the following below the License Header, with the fields
+ * enclosed by brackets [] replaced by your own identifying information:
+ * "Portions Copyright [year] [name of copyright owner]"
+ *
+ * Contributor(s):
+ * If you wish your version of this file to be governed by only the CDDL or
+ * only the GPL Version 2, indicate your decision by adding "[Contributor]
+ * elects to include this software in this distribution under the [CDDL or GPL
+ * Version 2] license." If you don't indicate a single choice of license, a
+ * recipient has the option to distribute your version of this file under
+ * either the CDDL, the GPL Version 2 or to extend the choice of license to
+ * its licensees as provided above. However, if you add GPL Version 2 code
+ * and therefore, elected the GPL Version 2 license, then the option applies
+ * only if the new code is made subject to such option by the copyright
+ * holder.
+ */
+package fish.payara.examples.testcontainers;
+
+/**
+ * Base class for every integration test that needs a running Payara Micro
+ * instance with the application deployed, whether it is tested over REST
+ * (see the {@code *ServiceIT} classes) or through a browser with Playwright
+ * (see the {@code *UiIT} classes).
+ *
+ */
+public abstract class AbstractContainerIT {
+
+ protected static final String APPLICATION_CONTEXT = "application/";
+
+ protected static final PayaraMicroContainer payara;
+
+ static {
+ payara = new PayaraMicroContainer();
+ payara.start();
+ }
+
+ /** Base URL of the deployed application, always ending with a trailing slash. */
+ protected static String applicationUrl() {
+ String appUrl = payara.getApplicationUrl();
+ return appUrl.endsWith("/") ? appUrl : appUrl + "/";
+ }
+
+ /** Base URL of the deployed application's "application" context, ending with a trailing slash. */
+ protected static String applicationContextUrl() {
+ return applicationUrl() + APPLICATION_CONTEXT;
+ }
+
+ /** Container logs, safe to call even if the container failed to start. */
+ protected static String containerLogs() {
+ try {
+ return payara.getLogs();
+ } catch (Exception e) {
+ return "";
+ }
+ }
+}
diff --git a/ecosystem/testcontainers-example/src/test/java/fish/payara/examples/testcontainers/PayaraMicroContainer.java b/ecosystem/testcontainers-example/src/test/java/fish/payara/examples/testcontainers/PayaraMicroContainer.java
index b636f2b..d156912 100644
--- a/ecosystem/testcontainers-example/src/test/java/fish/payara/examples/testcontainers/PayaraMicroContainer.java
+++ b/ecosystem/testcontainers-example/src/test/java/fish/payara/examples/testcontainers/PayaraMicroContainer.java
@@ -1,3 +1,42 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright (c) 2026 Payara Foundation and/or its affiliates. All rights reserved.
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * General Public License Version 2 only ("GPL") or the Common Development
+ * and Distribution License("CDDL") (collectively, the "License"). You
+ * may not use this file except in compliance with the License. You can
+ * obtain a copy of the License at
+ * https://github.com/payara/Payara/blob/master/LICENSE.txt
+ * See the License for the specific
+ * language governing permissions and limitations under the License.
+ *
+ * When distributing the software, include this License Header Notice in each
+ * file and include the License file at glassfish/legal/LICENSE.txt.
+ *
+ * GPL Classpath Exception:
+ * The Payara Foundation designates this particular file as subject to the "Classpath"
+ * exception as provided by the Payara Foundation in the GPL Version 2 section of the License
+ * file that accompanied this code.
+ *
+ * Modifications:
+ * If applicable, add the following below the License Header, with the fields
+ * enclosed by brackets [] replaced by your own identifying information:
+ * "Portions Copyright [year] [name of copyright owner]"
+ *
+ * Contributor(s):
+ * If you wish your version of this file to be governed by only the CDDL or
+ * only the GPL Version 2, indicate your decision by adding "[Contributor]
+ * elects to include this software in this distribution under the [CDDL or GPL
+ * Version 2] license." If you don't indicate a single choice of license, a
+ * recipient has the option to distribute your version of this file under
+ * either the CDDL, the GPL Version 2 or to extend the choice of license to
+ * its licensees as provided above. However, if you add GPL Version 2 code
+ * and therefore, elected the GPL Version 2 license, then the option applies
+ * only if the new code is made subject to such option by the copyright
+ * holder.
+ */
package fish.payara.examples.testcontainers;
import org.testcontainers.containers.GenericContainer;
@@ -5,27 +44,35 @@
import org.testcontainers.utility.DockerImageName;
import org.testcontainers.utility.MountableFile;
+/**
+ * A Testcontainers {@link GenericContainer} pre-configured to run Payara Micro
+ * with the application WAR deployed.
+ */
public class PayaraMicroContainer extends GenericContainer {
-
+
private static final int DEFAULT_PORT = 8080;
private static final String DEFAULT_CONTEXT_PATH = "/";
protected static final String CONTEXT = "ObservabilityTool";
-
+
+ public PayaraMicroContainer() {
+ this(DockerImageName.parse("payara/micro:" + requiredProperty("payara.version")));
+ withDeploymentPath(requiredProperty("war.path"));
+ }
+
public PayaraMicroContainer(DockerImageName dockerImageName) {
super(dockerImageName);
withExposedPorts(DEFAULT_PORT);
waitingFor(Wait.forLogMessage(".*Payara Micro .* ready.*\\n", 1));
-
}
-
+
public PayaraMicroContainer withDeploymentPath(String warPath) {
withCopyFileToContainer(
- MountableFile.forHostPath(warPath),
+ MountableFile.forHostPath(warPath),
"/opt/payara/deployments/application.war"
);
return this;
}
-
+
public String getApplicationUrl() {
return String.format(
"http://%s:%d%s",
@@ -34,4 +81,14 @@ public String getApplicationUrl() {
DEFAULT_CONTEXT_PATH
);
}
+
+ private static String requiredProperty(String name) {
+ String value = System.getProperty(name);
+ if (value == null || value.isBlank()) {
+ throw new IllegalStateException(
+ "System property '" + name + "' is not set. It must be supplied by the Maven build "
+ + "(see the failsafe plugin's systemPropertyVariables in pom.xml).");
+ }
+ return value;
+ }
}
\ No newline at end of file
diff --git a/ecosystem/testcontainers-example/src/test/java/fish/payara/examples/ui/AbstractUiIT.java b/ecosystem/testcontainers-example/src/test/java/fish/payara/examples/ui/AbstractUiIT.java
new file mode 100644
index 0000000..624dcde
--- /dev/null
+++ b/ecosystem/testcontainers-example/src/test/java/fish/payara/examples/ui/AbstractUiIT.java
@@ -0,0 +1,231 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright (c) 2026 Payara Foundation and/or its affiliates. All rights reserved.
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * General Public License Version 2 only ("GPL") or the Common Development
+ * and Distribution License("CDDL") (collectively, the "License"). You
+ * may not use this file except in compliance with the License. You can
+ * obtain a copy of the License at
+ * https://github.com/payara/Payara/blob/master/LICENSE.txt
+ * See the License for the specific
+ * language governing permissions and limitations under the License.
+ *
+ * When distributing the software, include this License Header Notice in each
+ * file and include the License file at glassfish/legal/LICENSE.txt.
+ *
+ * GPL Classpath Exception:
+ * The Payara Foundation designates this particular file as subject to the "Classpath"
+ * exception as provided by the Payara Foundation in the GPL Version 2 section of the License
+ * file that accompanied this code.
+ *
+ * Modifications:
+ * If applicable, add the following below the License Header, with the fields
+ * enclosed by brackets [] replaced by your own identifying information:
+ * "Portions Copyright [year] [name of copyright owner]"
+ *
+ * Contributor(s):
+ * If you wish your version of this file to be governed by only the CDDL or
+ * only the GPL Version 2, indicate your decision by adding "[Contributor]
+ * elects to include this software in this distribution under the [CDDL or GPL
+ * Version 2] license." If you don't indicate a single choice of license, a
+ * recipient has the option to distribute your version of this file under
+ * either the CDDL, the GPL Version 2 or to extend the choice of license to
+ * its licensees as provided above. However, if you add GPL Version 2 code
+ * and therefore, elected the GPL Version 2 license, then the option applies
+ * only if the new code is made subject to such option by the copyright
+ * holder.
+ */
+package fish.payara.examples.ui;
+
+import com.microsoft.playwright.Browser;
+import com.microsoft.playwright.BrowserContext;
+import com.microsoft.playwright.BrowserType;
+import com.microsoft.playwright.Dialog;
+import com.microsoft.playwright.Locator;
+import com.microsoft.playwright.Page;
+import com.microsoft.playwright.Playwright;
+import com.microsoft.playwright.options.AriaRole;
+import fish.payara.examples.testcontainers.AbstractContainerIT;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.extension.ExtensionContext;
+import org.junit.jupiter.api.extension.TestWatcher;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Optional;
+import java.util.UUID;
+
+/**
+ * Base class for browser-driven UI tests exercising the JSF pages of the
+ * application through Playwright, running against the same Payara Micro
+ * Testcontainer shared with the REST IT tests (see {@link AbstractContainerIT}).
+ *
+ * One headless Chromium instance is launched per test class (JUnit 5 runs
+ * {@code @BeforeAll}/{@code @AfterAll} around each concrete subclass); each
+ * individual test then gets its own {@link BrowserContext}/{@link Page} so
+ * tests don't leak cookies or state into one another.
+ * Note: the Playwright {@code chromium} browser binary needs to be installed once, which the
+ * build's {@code generate-test-resources} phase does automatically (see the
+ * exec-maven-plugin execution in pom.xml).
+ *
+ * On failure, a screenshot, the page's HTML, its URL and the Payara
+ * container logs are dumped under {@code target/playwright-failures/} (see
+ * {@link FailureDiagnostics}) so a failing test is debuggable from the build
+ * output alone, without having to reproduce it interactively.
+ */
+@ExtendWith(AbstractUiIT.FailureDiagnostics.class)
+public abstract class AbstractUiIT extends AbstractContainerIT {
+
+ private static Playwright playwright;
+ private static Browser browser;
+
+ protected BrowserContext context;
+ protected Page page;
+
+ @BeforeAll
+ static void launchBrowser() {
+ playwright = Playwright.create();
+ browser = playwright.chromium().launch(new BrowserType.LaunchOptions().setHeadless(true));
+ }
+
+ @AfterAll
+ static void closeBrowser() {
+ if (browser != null) {
+ browser.close();
+ }
+ if (playwright != null) {
+ playwright.close();
+ }
+ }
+
+ @BeforeEach
+ void newPage() {
+ context = browser.newContext();
+ page = context.newPage();
+ // The "Delete" links use a JS confirm() dialog; always accept it so
+ // delete flows don't need to register their own handler.
+ page.onDialog(Dialog::accept);
+ }
+
+ protected Page navigateTo(String relativePath) {
+ page.navigate(applicationContextUrl() + relativePath);
+ return page;
+ }
+
+ /** Clicks the form's "Save" button (an {@code h:commandButton}, so it has no stable id). */
+ protected void clickSave() {
+ page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Save")).click();
+ }
+
+ /**
+ * Locates the {@code h:dataTable} row containing the given text (e.g. a
+ * title or name). Scoped to {@code table.table-striped} - every page's
+ * {@code h:dataTable} is rendered with that class (see e.g. book.xhtml),
+ * while the entry form above it is a separate {@code h:panelGrid}
+ * ("table-form" class) that also renders as an HTML {@code }. That
+ * distinction matters on loan.xhtml in particular: a {@code }'s
+ * full option list counts as "text" for Playwright's hasText matching, so
+ * an unscoped locator would match the librarian/patron/book dropdown's
+ * row in the form instead of the actual data row. Matches any
+ * {@code } rather than assuming a {@code } wrapper, since
+ * {@code h:dataTable} doesn't always render one.
+ */
+ protected Locator rowContaining(String text) {
+ return page.locator("table.table-striped tr", new Page.LocatorOptions().setHasText(text));
+ }
+
+ /**
+ * The container's Payara instance and its database live for the whole test
+ * run, so test data isn't reset between tests. Suffix human-readable values
+ * with this to keep rows unique and independent of what other tests wrote.
+ */
+ protected static String unique(String prefix) {
+ return prefix + "-" + UUID.randomUUID().toString().substring(0, 8);
+ }
+
+ /**
+ * On test failure, saves a screenshot, the current page's HTML/URL, and
+ * the Payara container logs to {@code target/playwright-failures/}, named
+ * after the failing test. Best-effort: any problem while writing the
+ * diagnostics is swallowed so it never masks the original failure.
+ *
+ * This also owns closing the {@link BrowserContext} for every outcome
+ * (pass, fail, abort, disabled) instead of an {@code @AfterEach} method,
+ * because JUnit 5 runs {@code @AfterEach} - and therefore would have
+ * closed the page - before invoking {@link TestWatcher} callbacks.
+ * With cleanup in {@code @AfterEach}, {@link #testFailed} would always
+ * see an already-closed page and silently skip the screenshot/HTML
+ * capture (which is exactly what happened before this class took over
+ * closing: only the container log, which doesn't need the page, was ever
+ * written).
+ */
+ static final class FailureDiagnostics implements TestWatcher {
+
+ @Override
+ public void testFailed(ExtensionContext context, Throwable cause) {
+ withTestInstance(context, test -> {
+ captureDiagnostics(context, test);
+ closeContext(test);
+ });
+ }
+
+ @Override
+ public void testSuccessful(ExtensionContext context) {
+ withTestInstance(context, this::closeContext);
+ }
+
+ @Override
+ public void testAborted(ExtensionContext context, Throwable cause) {
+ withTestInstance(context, this::closeContext);
+ }
+
+ @Override
+ public void testDisabled(ExtensionContext context, Optional reason) {
+ withTestInstance(context, this::closeContext);
+ }
+
+ private void withTestInstance(ExtensionContext context, java.util.function.Consumer action) {
+ Object testInstance = context.getRequiredTestInstance();
+ if (testInstance instanceof AbstractUiIT) {
+ action.accept((AbstractUiIT) testInstance);
+ }
+ }
+
+ private void captureDiagnostics(ExtensionContext context, AbstractUiIT test) {
+ String name = context.getRequiredTestClass().getSimpleName() + "-" + context.getRequiredTestMethod().getName();
+ try {
+ Path dir = Path.of("target", "playwright-failures");
+ Files.createDirectories(dir);
+ if (test.page != null && !test.page.isClosed()) {
+ test.page.screenshot(new Page.ScreenshotOptions().setPath(dir.resolve(name + ".png")).setFullPage(true));
+ writeString(dir.resolve(name + ".html"), test.page.content());
+ writeString(dir.resolve(name + ".url.txt"), test.page.url());
+ }
+ writeString(dir.resolve(name + ".container.log"), AbstractUiIT.containerLogs());
+ } catch (Exception diagnosticsFailure) {
+ // Best-effort: never let diagnostics collection hide the real test failure.
+ }
+ }
+
+ private void closeContext(AbstractUiIT test) {
+ if (test.context != null) {
+ try {
+ test.context.close();
+ } catch (Exception ignored) {
+ // Best-effort cleanup.
+ }
+ }
+ }
+
+ private static void writeString(Path path, String content) throws IOException {
+ Files.write(path, content.getBytes(StandardCharsets.UTF_8));
+ }
+ }
+}
diff --git a/ecosystem/testcontainers-example/src/test/java/fish/payara/examples/ui/BookUiIT.java b/ecosystem/testcontainers-example/src/test/java/fish/payara/examples/ui/BookUiIT.java
new file mode 100644
index 0000000..523ecc4
--- /dev/null
+++ b/ecosystem/testcontainers-example/src/test/java/fish/payara/examples/ui/BookUiIT.java
@@ -0,0 +1,107 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright (c) 2026 Payara Foundation and/or its affiliates. All rights reserved.
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * General Public License Version 2 only ("GPL") or the Common Development
+ * and Distribution License("CDDL") (collectively, the "License"). You
+ * may not use this file except in compliance with the License. You can
+ * obtain a copy of the License at
+ * https://github.com/payara/Payara/blob/master/LICENSE.txt
+ * See the License for the specific
+ * language governing permissions and limitations under the License.
+ *
+ * When distributing the software, include this License Header Notice in each
+ * file and include the License file at glassfish/legal/LICENSE.txt.
+ *
+ * GPL Classpath Exception:
+ * The Payara Foundation designates this particular file as subject to the "Classpath"
+ * exception as provided by the Payara Foundation in the GPL Version 2 section of the License
+ * file that accompanied this code.
+ *
+ * Modifications:
+ * If applicable, add the following below the License Header, with the fields
+ * enclosed by brackets [] replaced by your own identifying information:
+ * "Portions Copyright [year] [name of copyright owner]"
+ *
+ * Contributor(s):
+ * If you wish your version of this file to be governed by only the CDDL or
+ * only the GPL Version 2, indicate your decision by adding "[Contributor]
+ * elects to include this software in this distribution under the [CDDL or GPL
+ * Version 2] license." If you don't indicate a single choice of license, a
+ * recipient has the option to distribute your version of this file under
+ * either the CDDL, the GPL Version 2 or to extend the choice of license to
+ * its licensees as provided above. However, if you add GPL Version 2 code
+ * and therefore, elected the GPL Version 2 license, then the option applies
+ * only if the new code is made subject to such option by the copyright
+ * holder.
+ */
+package fish.payara.examples.ui;
+
+import com.microsoft.playwright.Locator;
+import com.microsoft.playwright.options.AriaRole;
+import org.junit.jupiter.api.Test;
+
+import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat;
+
+/**
+ * Tests the "Book" JSF page (book.xhtml): create, edit and delete a book
+ * through the browser, driven by Playwright against the deployed application.
+ */
+class BookUiIT extends AbstractUiIT {
+
+ @Test
+ void createsABookAndShowsItInTheList() {
+ String title = unique("Playwright Book");
+
+ navigateTo("book.xhtml");
+ // Leave the Isbn field blank: the id is auto-generated on create, same
+ // as the REST API path (see BookBean#save / AbstractService#create).
+ page.getByLabel("Title:").fill(title);
+ page.getByLabel("Author:").fill("Jane Author");
+ page.getByLabel("Pages:").fill("123");
+ clickSave();
+
+ Locator row = rowContaining(title);
+ assertThat(row).isVisible();
+ assertThat(row).containsText("Jane Author");
+ assertThat(row).containsText("123");
+ }
+
+ @Test
+ void editsABook() {
+ String title = unique("Book To Edit");
+ String updatedTitle = unique("Updated Book");
+
+ navigateTo("book.xhtml");
+ page.getByLabel("Title:").fill(title);
+ page.getByLabel("Author:").fill("Original Author");
+ page.getByLabel("Pages:").fill("50");
+ clickSave();
+
+ rowContaining(title).getByRole(AriaRole.LINK, new Locator.GetByRoleOptions().setName("Edit")).click();
+ page.getByLabel("Title:").fill(updatedTitle);
+ clickSave();
+
+ assertThat(rowContaining(updatedTitle)).containsText("Original Author");
+ assertThat(rowContaining(title)).hasCount(0);
+ }
+
+ @Test
+ void deletesABook() {
+ String title = unique("Book To Delete");
+
+ navigateTo("book.xhtml");
+ page.getByLabel("Title:").fill(title);
+ page.getByLabel("Author:").fill("Delete Author");
+ page.getByLabel("Pages:").fill("10");
+ clickSave();
+
+ assertThat(rowContaining(title)).isVisible();
+
+ rowContaining(title).getByRole(AriaRole.LINK, new Locator.GetByRoleOptions().setName("Delete")).click();
+
+ assertThat(rowContaining(title)).hasCount(0);
+ }
+}
diff --git a/ecosystem/testcontainers-example/src/test/java/fish/payara/examples/ui/LibrarianUiIT.java b/ecosystem/testcontainers-example/src/test/java/fish/payara/examples/ui/LibrarianUiIT.java
new file mode 100644
index 0000000..64127ac
--- /dev/null
+++ b/ecosystem/testcontainers-example/src/test/java/fish/payara/examples/ui/LibrarianUiIT.java
@@ -0,0 +1,102 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright (c) 2026 Payara Foundation and/or its affiliates. All rights reserved.
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * General Public License Version 2 only ("GPL") or the Common Development
+ * and Distribution License("CDDL") (collectively, the "License"). You
+ * may not use this file except in compliance with the License. You can
+ * obtain a copy of the License at
+ * https://github.com/payara/Payara/blob/master/LICENSE.txt
+ * See the License for the specific
+ * language governing permissions and limitations under the License.
+ *
+ * When distributing the software, include this License Header Notice in each
+ * file and include the License file at glassfish/legal/LICENSE.txt.
+ *
+ * GPL Classpath Exception:
+ * The Payara Foundation designates this particular file as subject to the "Classpath"
+ * exception as provided by the Payara Foundation in the GPL Version 2 section of the License
+ * file that accompanied this code.
+ *
+ * Modifications:
+ * If applicable, add the following below the License Header, with the fields
+ * enclosed by brackets [] replaced by your own identifying information:
+ * "Portions Copyright [year] [name of copyright owner]"
+ *
+ * Contributor(s):
+ * If you wish your version of this file to be governed by only the CDDL or
+ * only the GPL Version 2, indicate your decision by adding "[Contributor]
+ * elects to include this software in this distribution under the [CDDL or GPL
+ * Version 2] license." If you don't indicate a single choice of license, a
+ * recipient has the option to distribute your version of this file under
+ * either the CDDL, the GPL Version 2 or to extend the choice of license to
+ * its licensees as provided above. However, if you add GPL Version 2 code
+ * and therefore, elected the GPL Version 2 license, then the option applies
+ * only if the new code is made subject to such option by the copyright
+ * holder.
+ */
+package fish.payara.examples.ui;
+
+import com.microsoft.playwright.Locator;
+import com.microsoft.playwright.options.AriaRole;
+import org.junit.jupiter.api.Test;
+
+import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat;
+
+/**
+ * Tests the "Librarian" JSF page (librarian.xhtml): create, edit and
+ * delete a librarian through the browser, driven by Playwright against the
+ * deployed application.
+ */
+class LibrarianUiIT extends AbstractUiIT {
+
+ @Test
+ void createsALibrarianAndShowsItInTheList() {
+ String name = unique("Playwright Librarian");
+
+ navigateTo("librarian.xhtml");
+ // Leave the Librarian ID field blank: it's auto-generated on create,
+ // same as the REST API path (see LibrarianBean#save / AbstractService#create).
+ page.getByLabel("Name:").fill(name);
+ page.getByLabel("Department:").fill("Circulation");
+ clickSave();
+
+ Locator row = rowContaining(name);
+ assertThat(row).isVisible();
+ assertThat(row).containsText("Circulation");
+ }
+
+ @Test
+ void editsALibrarian() {
+ String name = unique("Librarian To Edit");
+
+ navigateTo("librarian.xhtml");
+ page.getByLabel("Name:").fill(name);
+ page.getByLabel("Department:").fill("Old Department");
+ clickSave();
+
+ rowContaining(name).getByRole(AriaRole.LINK, new Locator.GetByRoleOptions().setName("Edit")).click();
+ page.getByLabel("Department:").fill("New Department");
+ clickSave();
+
+ assertThat(rowContaining(name)).containsText("New Department");
+ }
+
+ @Test
+ void deletesALibrarian() {
+ String name = unique("Librarian To Delete");
+
+ navigateTo("librarian.xhtml");
+ page.getByLabel("Name:").fill(name);
+ page.getByLabel("Department:").fill("Doomed Department");
+ clickSave();
+
+ assertThat(rowContaining(name)).isVisible();
+
+ rowContaining(name).getByRole(AriaRole.LINK, new Locator.GetByRoleOptions().setName("Delete")).click();
+
+ assertThat(rowContaining(name)).hasCount(0);
+ }
+}
diff --git a/ecosystem/testcontainers-example/src/test/java/fish/payara/examples/ui/LoanUiIT.java b/ecosystem/testcontainers-example/src/test/java/fish/payara/examples/ui/LoanUiIT.java
new file mode 100644
index 0000000..76ecffe
--- /dev/null
+++ b/ecosystem/testcontainers-example/src/test/java/fish/payara/examples/ui/LoanUiIT.java
@@ -0,0 +1,142 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright (c) 2026 Payara Foundation and/or its affiliates. All rights reserved.
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * General Public License Version 2 only ("GPL") or the Common Development
+ * and Distribution License("CDDL") (collectively, the "License"). You
+ * may not use this file except in compliance with the License. You can
+ * obtain a copy of the License at
+ * https://github.com/payara/Payara/blob/master/LICENSE.txt
+ * See the License for the specific
+ * language governing permissions and limitations under the License.
+ *
+ * When distributing the software, include this License Header Notice in each
+ * file and include the License file at glassfish/legal/LICENSE.txt.
+ *
+ * GPL Classpath Exception:
+ * The Payara Foundation designates this particular file as subject to the "Classpath"
+ * exception as provided by the Payara Foundation in the GPL Version 2 section of the License
+ * file that accompanied this code.
+ *
+ * Modifications:
+ * If applicable, add the following below the License Header, with the fields
+ * enclosed by brackets [] replaced by your own identifying information:
+ * "Portions Copyright [year] [name of copyright owner]"
+ *
+ * Contributor(s):
+ * If you wish your version of this file to be governed by only the CDDL or
+ * only the GPL Version 2, indicate your decision by adding "[Contributor]
+ * elects to include this software in this distribution under the [CDDL or GPL
+ * Version 2] license." If you don't indicate a single choice of license, a
+ * recipient has the option to distribute your version of this file under
+ * either the CDDL, the GPL Version 2 or to extend the choice of license to
+ * its licensees as provided above. However, if you add GPL Version 2 code
+ * and therefore, elected the GPL Version 2 license, then the option applies
+ * only if the new code is made subject to such option by the copyright
+ * holder.
+ */
+package fish.payara.examples.ui;
+
+import com.microsoft.playwright.Locator;
+import com.microsoft.playwright.options.AriaRole;
+import com.microsoft.playwright.options.SelectOption;
+import org.junit.jupiter.api.Test;
+
+import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat;
+
+/**
+ * Tests the "Loan" JSF page (loan.xhtml). Unlike the other entities, a
+ * loan is created by picking an existing librarian, patron and book from
+ * dropdowns, so this test first creates one of each through their own pages
+ * (reusing the same browser session) before creating, editing and deleting
+ * the loan itself.
+ */
+class LoanUiIT extends AbstractUiIT {
+
+ @Test
+ void createsALoanFromAnExistingLibrarianPatronAndBook() {
+ String librarianName = unique("Loan Librarian");
+ String patronName = unique("Loan Patron");
+ String bookTitle = unique("Loan Book");
+
+ createLibrarian(librarianName);
+ createPatron(patronName);
+ createBook(bookTitle);
+ fillAndSaveLoan(librarianName, patronName, bookTitle, "2026-07-14T09:00", "2026-07-28T09:00");
+
+ Locator row = rowContaining(librarianName);
+ assertThat(row).isVisible();
+ assertThat(row).containsText(patronName);
+ assertThat(row).containsText(bookTitle);
+ assertThat(row).containsText("2026-07-14T09:00");
+ }
+
+ @Test
+ void editsALoan() {
+ String librarianName = unique("Loan Librarian Edit");
+ String patronName = unique("Loan Patron Edit");
+ String bookTitle = unique("Loan Book Edit");
+
+ createLibrarian(librarianName);
+ createPatron(patronName);
+ createBook(bookTitle);
+ fillAndSaveLoan(librarianName, patronName, bookTitle, "2026-07-14T09:00", "2026-07-28T09:00");
+
+ rowContaining(librarianName).getByRole(AriaRole.LINK, new Locator.GetByRoleOptions().setName("Edit")).click();
+ page.getByLabel("Return Date:").fill("2026-08-04T09:00");
+ clickSave();
+
+ assertThat(rowContaining(librarianName)).containsText("2026-08-04T09:00");
+ }
+
+ @Test
+ void deletesALoan() {
+ String librarianName = unique("Loan Librarian Delete");
+ String patronName = unique("Loan Patron Delete");
+ String bookTitle = unique("Loan Book Delete");
+
+ createLibrarian(librarianName);
+ createPatron(patronName);
+ createBook(bookTitle);
+ fillAndSaveLoan(librarianName, patronName, bookTitle, "2026-07-14T09:00", "2026-07-28T09:00");
+
+ assertThat(rowContaining(librarianName)).isVisible();
+
+ rowContaining(librarianName).getByRole(AriaRole.LINK, new Locator.GetByRoleOptions().setName("Delete")).click();
+
+ assertThat(rowContaining(librarianName)).hasCount(0);
+ }
+
+ private void createLibrarian(String name) {
+ navigateTo("librarian.xhtml");
+ page.getByLabel("Name:").fill(name);
+ clickSave();
+ }
+
+ private void createPatron(String name) {
+ navigateTo("patron.xhtml");
+ page.getByLabel("Name:").fill(name);
+ clickSave();
+ }
+
+ private void createBook(String title) {
+ navigateTo("book.xhtml");
+ page.getByLabel("Title:").fill(title);
+ page.getByLabel("Author:").fill("Loan Test Author");
+ page.getByLabel("Pages:").fill("42");
+ clickSave();
+ }
+
+ private void fillAndSaveLoan(String librarianName, String patronName, String bookTitle,
+ String loanDate, String returnDate) {
+ navigateTo("loan.xhtml");
+ page.getByLabel("Loan Date:").fill(loanDate);
+ page.getByLabel("Return Date:").fill(returnDate);
+ page.getByLabel("Librarian:").selectOption(new SelectOption().setLabel(librarianName));
+ page.getByLabel("Patron:").selectOption(new SelectOption().setLabel(patronName));
+ page.getByLabel("Book:").selectOption(new SelectOption().setLabel(bookTitle));
+ clickSave();
+ }
+}
diff --git a/ecosystem/testcontainers-example/src/test/java/fish/payara/examples/ui/PatronUiIT.java b/ecosystem/testcontainers-example/src/test/java/fish/payara/examples/ui/PatronUiIT.java
new file mode 100644
index 0000000..46e4eb1
--- /dev/null
+++ b/ecosystem/testcontainers-example/src/test/java/fish/payara/examples/ui/PatronUiIT.java
@@ -0,0 +1,104 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright (c) 2026 Payara Foundation and/or its affiliates. All rights reserved.
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * General Public License Version 2 only ("GPL") or the Common Development
+ * and Distribution License("CDDL") (collectively, the "License"). You
+ * may not use this file except in compliance with the License. You can
+ * obtain a copy of the License at
+ * https://github.com/payara/Payara/blob/master/LICENSE.txt
+ * See the License for the specific
+ * language governing permissions and limitations under the License.
+ *
+ * When distributing the software, include this License Header Notice in each
+ * file and include the License file at glassfish/legal/LICENSE.txt.
+ *
+ * GPL Classpath Exception:
+ * The Payara Foundation designates this particular file as subject to the "Classpath"
+ * exception as provided by the Payara Foundation in the GPL Version 2 section of the License
+ * file that accompanied this code.
+ *
+ * Modifications:
+ * If applicable, add the following below the License Header, with the fields
+ * enclosed by brackets [] replaced by your own identifying information:
+ * "Portions Copyright [year] [name of copyright owner]"
+ *
+ * Contributor(s):
+ * If you wish your version of this file to be governed by only the CDDL or
+ * only the GPL Version 2, indicate your decision by adding "[Contributor]
+ * elects to include this software in this distribution under the [CDDL or GPL
+ * Version 2] license." If you don't indicate a single choice of license, a
+ * recipient has the option to distribute your version of this file under
+ * either the CDDL, the GPL Version 2 or to extend the choice of license to
+ * its licensees as provided above. However, if you add GPL Version 2 code
+ * and therefore, elected the GPL Version 2 license, then the option applies
+ * only if the new code is made subject to such option by the copyright
+ * holder.
+ */
+package fish.payara.examples.ui;
+
+import com.microsoft.playwright.Locator;
+import com.microsoft.playwright.options.AriaRole;
+import org.junit.jupiter.api.Test;
+
+import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat;
+
+/**
+ * Exercises the "Patron" JSF page (patron.xhtml): create, edit and delete a
+ * patron through the browser, driven by Playwright against the deployed
+ * application.
+ */
+class PatronUiIT extends AbstractUiIT {
+
+ @Test
+ void createsAPatronAndShowsItInTheList() {
+ String name = unique("Playwright Patron");
+
+ navigateTo("patron.xhtml");
+ page.getByLabel("Name:").fill(name);
+ page.getByLabel("Address:").fill("1 Library Way");
+ page.getByLabel("Email:").fill("patron@example.com");
+ clickSave();
+
+ Locator row = rowContaining(name);
+ assertThat(row).isVisible();
+ assertThat(row).containsText("1 Library Way");
+ assertThat(row).containsText("patron@example.com");
+ }
+
+ @Test
+ void editsAPatron() {
+ String name = unique("Patron To Edit");
+
+ navigateTo("patron.xhtml");
+ page.getByLabel("Name:").fill(name);
+ page.getByLabel("Address:").fill("Old Address");
+ page.getByLabel("Email:").fill("old@example.com");
+ clickSave();
+
+ rowContaining(name).getByRole(AriaRole.LINK, new Locator.GetByRoleOptions().setName("Edit")).click();
+ page.getByLabel("Email:").fill("new@example.com");
+ clickSave();
+
+ assertThat(rowContaining(name)).containsText("new@example.com");
+ }
+
+ @Test
+ void deletesAPatron() {
+ String name = unique("Patron To Delete");
+
+ navigateTo("patron.xhtml");
+ page.getByLabel("Name:").fill(name);
+ page.getByLabel("Address:").fill("Somewhere");
+ page.getByLabel("Email:").fill("delete@example.com");
+ clickSave();
+
+ assertThat(rowContaining(name)).isVisible();
+
+ rowContaining(name).getByRole(AriaRole.LINK, new Locator.GetByRoleOptions().setName("Delete")).click();
+
+ assertThat(rowContaining(name)).hasCount(0);
+ }
+}
diff --git a/ecosystem/testcontainers-example/src/test/resources/arquillian.xml b/ecosystem/testcontainers-example/src/test/resources/arquillian.xml
new file mode 100644
index 0000000..b9f96a1
--- /dev/null
+++ b/ecosystem/testcontainers-example/src/test/resources/arquillian.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+ true
+ true
+ 180
+
+
+
+