Testing secured EJBs with Arquillian
Testing secured EJBs has been historically hard to get right. Up until now, I have been using proprietary techniques like JBossLoginContextFactory described in the article Testing secured EJBs on WildFly 8.1.x with Arquillian to test secured EJBs.
During this year Devoxx, David Blevins, founder of the Apache TomEE project – a lightweight Java EE Application Server, brought to my knowledge a little trick we can use to deal with Java EE security in a standard way that works across all Java EE compliant servers.
The example used in this post is available at javaee-testing/security on GitHub.
The code
The code to test includes an entity and an EJB service as follows.
Book Entity
@Entity public class Book { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Integer id; private String isbn; private String title; public Book() { } public Book(String isbn, String title) { this.isbn = isbn; this.title = title; } // getters and setters omitted for brevity }
Bookshelf EJB Service
@Stateless public class BookshelfService { @PersistenceContext(unitName = "bookshelfManager") private EntityManager entityManager; @RolesAllowed({ "User", "Manager" }) public void addBook(Book book) { entityManager.persist(book); } @RolesAllowed({ "Manager" }) public void deleteBook(Book book) { entityManager.remove(book); } @PermitAll @TransactionAttribute(TransactionAttributeType.SUPPORTS) public List<Book> getBooks() { TypedQuery<Book> query = entityManager.createQuery("SELECT b from Book as b", Book.class); return query.getResultList(); } }
The test class uses Arquillian for the integration tests and asserts that the security roles defined on our EJB are respected.
Bookshelf Service Tests
@RunWith(Arquillian.class) public class BookshelfServiceIT { @Inject private BookshelfService bookshelfService; @Inject private BookshelfManager manager; @Inject private BookshelfUser user; @Deployment public static JavaArchive createDeployment() throws IOException { return ShrinkWrap.create(JavaArchive.class, "javaee-testing-security.jar") .addClasses(Book.class, BookshelfService.class, BookshelfManager.class, BookshelfUser.class) .addAsManifestResource("META-INF/persistence.xml", "persistence.xml") .addAsManifestResource(EmptyAsset.INSTANCE, ArchivePaths.create("beans.xml")); } @Test public void testAsManager() throws Exception { manager.call(new Callable<Book>() { @Override public Book call() throws Exception { bookshelfService.addBook(new Book("978-1-4302-4626-8", "Beginning Java EE 7")); bookshelfService.addBook(new Book("978-1-4493-2829-0", "Continuous Enterprise Development in Java")); List<Book> books = bookshelfService.getBooks(); Assert.assertEquals("List.size()", 2, books.size()); for (Book book : books) { bookshelfService.deleteBook(book); } Assert.assertEquals("BookshelfService.getBooks()", 0, bookshelfService.getBooks().size()); return null; } }); } @Test public void testAsUser() throws Exception { user.call(new Callable<Book>() { @Override public Book call() throws Exception { bookshelfService.addBook(new Book("978-1-4302-4626-8", "Beginning Java EE 7")); bookshelfService.addBook(new Book("978-1-4493-2829-0", "Continuous Enterprise Development in Java")); List<Book> books = bookshelfService.getBooks(); Assert.assertEquals("List.size()", 2, books.size()); for (Book book : books) { try { bookshelfService.deleteBook(book); Assert.fail("Users should not be allowed to delete"); } catch (EJBAccessException e) { // Good, users cannot delete things } } // The list should not be empty Assert.assertEquals("BookshelfService.getBooks()", 2, bookshelfService.getBooks().size()); return null; } }); } @Test public void testUnauthenticated() throws Exception { try { bookshelfService.addBook(new Book("978-1-4302-4626-8", "Beginning Java EE 7")); Assert.fail("Unauthenticated users should not be able to add books"); } catch (EJBAccessException e) { // Good, unauthenticated users cannot add things } try { bookshelfService.deleteBook(null); Assert.fail("Unauthenticated users should not be allowed to delete"); } catch (EJBAccessException e) { // Good, unauthenticated users cannot delete things } try { // Read access should be allowed List<Book> books = bookshelfService.getBooks(); Assert.assertEquals("BookshelfService.getBooks()", 0, books.size()); } catch (EJBAccessException e) { Assert.fail("Read access should be allowed"); } } }
The trick is on two helper EJBs which allow our test code to execute in the desired security scope by using the @RunAs
standard annotation.
Bookshelf Manager role
@Stateless @RunAs("Manager") @PermitAll public class BookshelfManager { public <V> V call(Callable<V> callable) throws Exception { return callable.call(); } }
Bookshelf User role
@Stateless @RunAs("User") @PermitAll public class BookshelfUser { public <V> V call(Callable<V> callable) throws Exception { return callable.call(); } }
Running
------------------------------------------------------- T E S T S ------------------------------------------------------- Running com.samaxes.javaeetesting.security.BookshelfServiceIT nov 23, 2014 2:44:48 AM org.xnio.Xnio <clinit> INFO: XNIO version 3.2.0.Beta4 nov 23, 2014 2:44:48 AM org.xnio.nio.NioXnio <clinit> INFO: XNIO NIO Implementation Version 3.2.0.Beta4 nov 23, 2014 2:44:49 AM org.jboss.remoting3.EndpointImpl <clinit> INFO: JBoss Remoting version (unknown) Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 36.69 sec - in com.samaxes.javaeetesting.security.BookshelfServiceIT Results : Tests run: 3, Failures: 0, Errors: 0, Skipped: 0
Happy tests!
Reference: | Testing secured EJBs with Arquillian from our JCG partner Samuel Santos at the Samaxes blog. |