You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@isis.apache.org by da...@apache.org on 2021/04/11 07:18:21 UTC

[isis] branch master updated: ISIS-2606: adds avatarUrl and realName to UserMemento

This is an automated email from the ASF dual-hosted git repository.

danhaywood pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/isis.git


The following commit(s) were added to refs/heads/master by this push:
     new ef6961a  ISIS-2606: adds avatarUrl and realName to UserMemento
ef6961a is described below

commit ef6961a6779620333e5593047cfe159d9bcece64
Author: danhaywood <da...@haywood-associates.co.uk>
AuthorDate: Sun Apr 11 08:18:05 2021 +0100

    ISIS-2606: adds avatarUrl and realName to UserMemento
---
 api/applib/pom.xml                                 |  14 +-
 .../isis/applib/services/user/UserMemento.java     |  64 +++++-
 .../applib/services/user/UserMemento_Test.java     | 217 ++++++++++++++++++++
 core/internaltestsupport/pom.xml                   |  18 +-
 security/spring/pom.xml                            |   7 +
 ...thenticationConverterOfOAuth2UserPrincipal.java |  43 +++-
 ...icationConverterOfOAuth2UserPrincipal_Test.java | 225 +++++++++++++++++++++
 7 files changed, 556 insertions(+), 32 deletions(-)

diff --git a/api/applib/pom.xml b/api/applib/pom.xml
index ae98b79..c94f3dd 100644
--- a/api/applib/pom.xml
+++ b/api/applib/pom.xml
@@ -7,9 +7,9 @@
   to you under the Apache License, Version 2.0 (the
   "License"); you may not use this file except in compliance
   with the License.  You may obtain a copy of the License at
-  
+
          http://www.apache.org/licenses/LICENSE-2.0
-         
+
   Unless required by applicable law or agreed to in writing,
   software distributed under the License is distributed on an
   "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
@@ -69,7 +69,7 @@
     </build>
 
     <dependencies>
-    
+
         <dependency>
             <groupId>org.apache.isis.commons</groupId>
 			<artifactId>isis-commons</artifactId>
@@ -98,19 +98,13 @@
 			<artifactId>isis-core-internaltestsupport</artifactId>
 			<scope>test</scope>
 		</dependency>
-        
+
         <dependency>
             <groupId>org.jmock</groupId>
             <artifactId>jmock</artifactId>
             <scope>test</scope>
         </dependency>
 
-        <dependency>
-            <groupId>org.assertj</groupId>
-            <artifactId>assertj-core</artifactId>
-            <scope>test</scope>
-        </dependency>
-
     </dependencies>
 
 </project>
diff --git a/api/applib/src/main/java/org/apache/isis/applib/services/user/UserMemento.java b/api/applib/src/main/java/org/apache/isis/applib/services/user/UserMemento.java
index 63f41b4..748da46 100644
--- a/api/applib/src/main/java/org/apache/isis/applib/services/user/UserMemento.java
+++ b/api/applib/src/main/java/org/apache/isis/applib/services/user/UserMemento.java
@@ -19,6 +19,7 @@
 package org.apache.isis.applib.services.user;
 
 import java.io.Serializable;
+import java.net.URL;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Objects;
@@ -27,8 +28,12 @@ import java.util.stream.Stream;
 
 import javax.annotation.Nullable;
 
+import org.apache.isis.applib.annotation.Collection;
 import org.apache.isis.applib.annotation.CollectionLayout;
 import org.apache.isis.applib.annotation.DomainObject;
+import org.apache.isis.applib.annotation.Optionality;
+import org.apache.isis.applib.annotation.Programmatic;
+import org.apache.isis.applib.annotation.Property;
 import org.apache.isis.applib.annotation.PropertyLayout;
 import org.apache.isis.commons.internal.base._Strings;
 import org.apache.isis.commons.internal.collections._Lists;
@@ -106,12 +111,6 @@ public final class UserMemento implements Serializable {
         this.roles = roles.collect(_Lists.toUnmodifiable());
     }
 
-    public UserMemento withRole(String role) {
-        final List<RoleMemento> roles = new ArrayList<>(this.roles);
-        roles.add(new RoleMemento(role));
-        return new UserMemento(this.name, roles.stream());
-    }
-
     public String title() {
         return name;
     }
@@ -119,19 +118,68 @@ public final class UserMemento implements Serializable {
     /**
      * The user's login name.
      */
+    @Property
     @PropertyLayout(sequence = "1.1")
     @Getter
     private final String name;
 
+    @Property(optionality = Optionality.OPTIONAL)
+    @PropertyLayout(sequence = "1.2")
+    @Getter
+    private String realName;
+
+    @Programmatic
+    public UserMemento withRealName(final String realName) {
+        val userMemento = copy();
+        userMemento.realName = realName;
+        return userMemento;
+    }
+
+    @Property(optionality = Optionality.OPTIONAL)
+    @PropertyLayout(sequence = "1.3")
+    @Getter
+    private URL avatarUrl;
+
+    @Programmatic
+    public UserMemento withAvatarUrl(final URL avatarUrl) {
+        val userMemento = copy();
+        userMemento.avatarUrl = avatarUrl;
+        return userMemento;
+    }
+
+    private UserMemento copy() {
+        return copy(this.roles);
+    }
+
+    private UserMemento copy(final List<RoleMemento> roles) {
+        val userMemento = new UserMemento(this.name, roles.stream());
+        userMemento.realName = this.realName;
+        userMemento.avatarUrl = this.avatarUrl;
+        return userMemento;
+    }
+
+
+
     /**
      * The roles associated with this user.
      */
-    @CollectionLayout(sequence = "1.2")
+    @Collection
+    @CollectionLayout(sequence = "1.4")
     private final List<RoleMemento> roles;
     public List<RoleMemento> getRoles() {
         return roles;
     }
 
+    @Programmatic
+    public UserMemento withRole(String role) {
+        final List<RoleMemento> roles = new ArrayList<>(this.roles);
+        roles.add(new RoleMemento(role));
+        val userMemento = copy(roles);
+        return userMemento;
+    }
+
+
+
     /**
      * Determine if the specified name is this user.
      *
@@ -143,11 +191,13 @@ public final class UserMemento implements Serializable {
         return name.equals(userName);
     }
 
+    @Programmatic
     public Stream<String> streamRoleNames() {
         return roles.stream()
                 .map(RoleMemento::getName);
     }
 
+    @Programmatic
     public boolean hasRoleName(final @Nullable String roleName) {
         return streamRoleNames().anyMatch(myRoleName->myRoleName.equals(roleName));
     }
diff --git a/api/applib/src/test/java/org/apache/isis/applib/services/user/UserMemento_Test.java b/api/applib/src/test/java/org/apache/isis/applib/services/user/UserMemento_Test.java
new file mode 100644
index 0000000..bd411d0
--- /dev/null
+++ b/api/applib/src/test/java/org/apache/isis/applib/services/user/UserMemento_Test.java
@@ -0,0 +1,217 @@
+package org.apache.isis.applib.services.user;
+
+import java.net.MalformedURLException;
+
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+
+import lombok.val;
+
+class UserMemento_Test {
+
+    @Nested
+    class constructor {
+
+        @Test
+        void name_only() {
+
+            // when
+            val userMemento = UserMemento.ofName("fredflintstone");
+
+            // then original unchanged
+            Assertions.assertThat(userMemento.getName()).isEqualTo("fredflintstone");
+            Assertions.assertThat(userMemento.getRoles()).hasSize(0);
+            Assertions.assertThat(userMemento.getRealName()).isNull();
+            Assertions.assertThat(userMemento.getAvatarUrl()).isNull();
+        }
+
+        @Test
+        void name_and_roles() {
+
+            // when
+            val userMemento = UserMemento.ofNameAndRoleNames("fredflintstone", "CAVEMAN", "HUSBAND");
+
+            // then
+            Assertions.assertThat(userMemento.getName()).isEqualTo("fredflintstone");
+            Assertions.assertThat(userMemento.getRoles()).hasSize(2);
+            Assertions.assertThat(userMemento.streamRoleNames()).anyMatch(x -> x.equals("CAVEMAN"));
+            Assertions.assertThat(userMemento.streamRoleNames()).anyMatch(x -> x.equals("HUSBAND"));
+            Assertions.assertThat(userMemento.getRealName()).isNull();
+            Assertions.assertThat(userMemento.getAvatarUrl()).isNull();
+        }
+    }
+
+    @Nested
+    class withRealName {
+
+        @Test
+        void user_and_roles_preserved_and_real_name_set() {
+            // copy
+            val userMemento = UserMemento.ofNameAndRoleNames("fredflintstone", "CAVEMAN", "HUSBAND");
+
+            // when
+            val userMemento2 = userMemento.withRealName("Fred Flintstone");
+
+            // then copy created
+            Assertions.assertThat(userMemento2).isNotSameAs(userMemento);
+
+            // then copy correct
+            Assertions.assertThat(userMemento2.getName()).isEqualTo("fredflintstone");
+            Assertions.assertThat(userMemento2.getRoles()).hasSize(2);
+            Assertions.assertThat(userMemento2.streamRoleNames()).anyMatch(x -> x.equals("CAVEMAN"));
+            Assertions.assertThat(userMemento2.streamRoleNames()).anyMatch(x -> x.equals("HUSBAND"));
+            Assertions.assertThat(userMemento2.getRealName()).isEqualTo("Fred Flintstone");
+
+            // then original unchanged
+            Assertions.assertThat(userMemento.getName()).isEqualTo("fredflintstone");
+            Assertions.assertThat(userMemento.getRoles()).hasSize(2);
+            Assertions.assertThat(userMemento.streamRoleNames()).anyMatch(x -> x.equals("CAVEMAN"));
+            Assertions.assertThat(userMemento.streamRoleNames()).anyMatch(x -> x.equals("HUSBAND"));
+            Assertions.assertThat(userMemento.getRealName()).isNull();
+        }
+
+        @Test
+        void user_and_roles_and_avatarUrl_preserved_and_real_name_set() throws MalformedURLException {
+            // copy
+            val userMemento = UserMemento.ofNameAndRoleNames("fredflintstone", "CAVEMAN", "HUSBAND")
+                    .withAvatarUrl(new java.net.URL("https://upload.wikimedia.org/wikipedia/en/a/ad/Fred_Flintstone.png"));
+
+            // when
+            val userMemento2 = userMemento.withRealName("Fred Flintstone");
+
+            // then copy created
+            Assertions.assertThat(userMemento2).isNotSameAs(userMemento);
+
+            // then copy correct
+            Assertions.assertThat(userMemento2.getName()).isEqualTo("fredflintstone");
+            Assertions.assertThat(userMemento2.getRoles()).hasSize(2);
+            Assertions.assertThat(userMemento2.streamRoleNames()).anyMatch(x -> x.equals("CAVEMAN"));
+            Assertions.assertThat(userMemento2.streamRoleNames()).anyMatch(x -> x.equals("HUSBAND"));
+            Assertions.assertThat(userMemento2.getAvatarUrl()).isEqualTo(new java.net.URL("https://upload.wikimedia.org/wikipedia/en/a/ad/Fred_Flintstone.png"));
+            Assertions.assertThat(userMemento2.getRealName()).isEqualTo("Fred Flintstone");
+
+            // then original unchanged
+            Assertions.assertThat(userMemento.getName()).isEqualTo("fredflintstone");
+            Assertions.assertThat(userMemento.getRoles()).hasSize(2);
+            Assertions.assertThat(userMemento.streamRoleNames()).anyMatch(x -> x.equals("CAVEMAN"));
+            Assertions.assertThat(userMemento.streamRoleNames()).anyMatch(x -> x.equals("HUSBAND"));
+            Assertions.assertThat(userMemento.getAvatarUrl()).isEqualTo(new java.net.URL("https://upload.wikimedia.org/wikipedia/en/a/ad/Fred_Flintstone.png"));
+            Assertions.assertThat(userMemento.getRealName()).isNull();
+        }
+    }
+
+    @Nested
+    class withAvatarUrl {
+
+        @Test
+        void user_and_roles_preserved_and_avatarUrl_set() throws MalformedURLException {
+
+            // copy
+            val userMemento = UserMemento.ofNameAndRoleNames("fredflintstone", "CAVEMAN", "HUSBAND");
+
+            // when
+            val userMemento2 = userMemento.withAvatarUrl(new java.net.URL("https://upload.wikimedia.org/wikipedia/en/a/ad/Fred_Flintstone.png"));
+
+            // then copy created
+            Assertions.assertThat(userMemento2).isNotSameAs(userMemento);
+
+            // then copy correct
+            Assertions.assertThat(userMemento2.getName()).isEqualTo("fredflintstone");
+            Assertions.assertThat(userMemento2.getRoles()).hasSize(2);
+            Assertions.assertThat(userMemento2.streamRoleNames()).anyMatch(x -> x.equals("CAVEMAN"));
+            Assertions.assertThat(userMemento2.streamRoleNames()).anyMatch(x -> x.equals("HUSBAND"));
+            Assertions.assertThat(userMemento2.getAvatarUrl()).isEqualTo(new java.net.URL("https://upload.wikimedia.org/wikipedia/en/a/ad/Fred_Flintstone.png"));
+
+            // then original unchanged
+            Assertions.assertThat(userMemento.getName()).isEqualTo("fredflintstone");
+            Assertions.assertThat(userMemento.getRoles()).hasSize(2);
+            Assertions.assertThat(userMemento.streamRoleNames()).anyMatch(x -> x.equals("CAVEMAN"));
+            Assertions.assertThat(userMemento.streamRoleNames()).anyMatch(x -> x.equals("HUSBAND"));
+            Assertions.assertThat(userMemento.getAvatarUrl()).isNull();
+        }
+
+        @Test
+        void user_and_roles_and_real_name_preserved_and_avatarUrl_set() throws MalformedURLException {
+
+            // copy
+            val userMemento = UserMemento.ofNameAndRoleNames("fredflintstone", "CAVEMAN", "HUSBAND").withRealName("Fred Flintstone");
+
+            // when
+            val userMemento2 = userMemento.withAvatarUrl(new java.net.URL("https://upload.wikimedia.org/wikipedia/en/a/ad/Fred_Flintstone.png"));
+
+            // then copy created
+            Assertions.assertThat(userMemento2).isNotSameAs(userMemento);
+
+            // then copy correct
+            Assertions.assertThat(userMemento2.getName()).isEqualTo("fredflintstone");
+            Assertions.assertThat(userMemento2.getRoles()).hasSize(2);
+            Assertions.assertThat(userMemento2.streamRoleNames()).anyMatch(x -> x.equals("CAVEMAN"));
+            Assertions.assertThat(userMemento2.streamRoleNames()).anyMatch(x -> x.equals("HUSBAND"));
+            Assertions.assertThat(userMemento2.getAvatarUrl()).isEqualTo(new java.net.URL("https://upload.wikimedia.org/wikipedia/en/a/ad/Fred_Flintstone.png"));
+            Assertions.assertThat(userMemento2.getRealName()).isEqualTo("Fred Flintstone");
+
+            // then original unchanged
+            Assertions.assertThat(userMemento.getName()).isEqualTo("fredflintstone");
+            Assertions.assertThat(userMemento.getRoles()).hasSize(2);
+            Assertions.assertThat(userMemento.streamRoleNames()).anyMatch(x -> x.equals("CAVEMAN"));
+            Assertions.assertThat(userMemento.streamRoleNames()).anyMatch(x -> x.equals("HUSBAND"));
+            Assertions.assertThat(userMemento.getRealName()).isEqualTo("Fred Flintstone");
+            Assertions.assertThat(userMemento.getAvatarUrl()).isNull();
+        }
+    }
+
+    @Nested
+    class withRole {
+
+        @Test
+        void user_and_roles_preserved_and_role_added() throws MalformedURLException {
+
+            // given
+            val userMemento = UserMemento.ofNameAndRoleNames("fredflintstone", "CAVEMAN", "HUSBAND");
+
+            // when
+            val userMemento2 = userMemento.withRole("WICKET_ROLE");
+
+            // then copy created
+            Assertions.assertThat(userMemento2).isNotSameAs(userMemento);
+
+            // then copy correct
+            Assertions.assertThat(userMemento2.getName()).isEqualTo("fredflintstone");
+            Assertions.assertThat(userMemento2.getRoles()).hasSize(3);
+            Assertions.assertThat(userMemento2.streamRoleNames()).anyMatch(x -> x.equals("CAVEMAN"));
+            Assertions.assertThat(userMemento2.streamRoleNames()).anyMatch(x -> x.equals("HUSBAND"));
+            Assertions.assertThat(userMemento2.streamRoleNames()).anyMatch(x -> x.equals("WICKET_ROLE"));
+
+            // then original unchanged
+            Assertions.assertThat(userMemento.getName()).isEqualTo("fredflintstone");
+            Assertions.assertThat(userMemento.getRoles()).hasSize(2);
+            Assertions.assertThat(userMemento.streamRoleNames()).anyMatch(x -> x.equals("CAVEMAN"));
+            Assertions.assertThat(userMemento.streamRoleNames()).anyMatch(x -> x.equals("HUSBAND"));
+        }
+    }
+
+    @Nested
+    class all_the_withers {
+
+        @Test
+        void happy_case() throws MalformedURLException {
+
+            // when
+            val userMemento = UserMemento.ofName("fredflintstone")
+                    .withRole("CAVEMAN")
+                    .withRole("HUSBAND")
+                    .withAvatarUrl(new java.net.URL("https://upload.wikimedia.org/wikipedia/en/a/ad/Fred_Flintstone.png"))
+                    .withRealName("Fred Flintstone");
+
+            // then
+            Assertions.assertThat(userMemento.getName()).isEqualTo("fredflintstone");
+            Assertions.assertThat(userMemento.getRoles()).hasSize(2);
+            Assertions.assertThat(userMemento.streamRoleNames()).anyMatch(x -> x.equals("CAVEMAN"));
+            Assertions.assertThat(userMemento.streamRoleNames()).anyMatch(x -> x.equals("HUSBAND"));
+            Assertions.assertThat(userMemento.getAvatarUrl()).isEqualTo(new java.net.URL("https://upload.wikimedia.org/wikipedia/en/a/ad/Fred_Flintstone.png"));
+            Assertions.assertThat(userMemento.getRealName()).isEqualTo("Fred Flintstone");
+        }
+    }
+
+}
diff --git a/core/internaltestsupport/pom.xml b/core/internaltestsupport/pom.xml
index d9e0184..855b216 100644
--- a/core/internaltestsupport/pom.xml
+++ b/core/internaltestsupport/pom.xml
@@ -1,13 +1,13 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<!-- Licensed to the Apache Software Foundation (ASF) under one or more contributor 
-	license agreements. See the NOTICE file distributed with this work for additional 
-	information regarding copyright ownership. The ASF licenses this file to 
-	you under the Apache License, Version 2.0 (the "License"); you may not use 
-	this file except in compliance with the License. You may obtain a copy of 
-	the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required 
-	by applicable law or agreed to in writing, software distributed under the 
-	License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS 
-	OF ANY KIND, either express or implied. See the License for the specific 
+<!-- Licensed to the Apache Software Foundation (ASF) under one or more contributor
+	license agreements. See the NOTICE file distributed with this work for additional
+	information regarding copyright ownership. The ASF licenses this file to
+	you under the Apache License, Version 2.0 (the "License"); you may not use
+	this file except in compliance with the License. You may obtain a copy of
+	the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required
+	by applicable law or agreed to in writing, software distributed under the
+	License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS
+	OF ANY KIND, either express or implied. See the License for the specific
 	language governing permissions and limitations under the License. -->
 <project xmlns="http://maven.apache.org/POM/4.0.0"
 	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
diff --git a/security/spring/pom.xml b/security/spring/pom.xml
index d5d4867..9ee0d52 100644
--- a/security/spring/pom.xml
+++ b/security/spring/pom.xml
@@ -69,6 +69,13 @@
 			</exclusions>
 		</dependency>
 
+		<!-- test -->
+		<dependency>
+			<groupId>org.apache.isis.core</groupId>
+			<artifactId>isis-core-internaltestsupport</artifactId>
+			<scope>test</scope>
+		</dependency>
+
 	</dependencies>
 
 
diff --git a/security/spring/src/main/java/org/apache/isis/security/spring/authconverters/AuthenticationConverterOfOAuth2UserPrincipal.java b/security/spring/src/main/java/org/apache/isis/security/spring/authconverters/AuthenticationConverterOfOAuth2UserPrincipal.java
index 783d47d..63de251 100644
--- a/security/spring/src/main/java/org/apache/isis/security/spring/authconverters/AuthenticationConverterOfOAuth2UserPrincipal.java
+++ b/security/spring/src/main/java/org/apache/isis/security/spring/authconverters/AuthenticationConverterOfOAuth2UserPrincipal.java
@@ -1,5 +1,8 @@
 package org.apache.isis.security.spring.authconverters;
 
+import java.net.MalformedURLException;
+import java.net.URL;
+
 import org.springframework.core.annotation.Order;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.oauth2.core.user.OAuth2User;
@@ -9,6 +12,7 @@ import org.apache.isis.applib.annotation.OrderPrecedence;
 import org.apache.isis.applib.services.user.UserMemento;
 
 import lombok.val;
+import lombok.var;
 
 /**
  * Interpret {@link Authentication} as containing an OAuth2 principal.
@@ -39,13 +43,40 @@ public class AuthenticationConverterOfOAuth2UserPrincipal implements Authenticat
         val principal = authentication.getPrincipal();
         if (principal instanceof OAuth2User) {
             val oAuth2User = (OAuth2User) principal;
-            final Object loginAttr = oAuth2User.getAttributes().get("login");
-            val principalIdentity =
-                    loginAttr instanceof CharSequence
-                            ? ((CharSequence) loginAttr).toString()
-                            : oAuth2User.getName();
-            return UserMemento.ofNameAndRoleNames(principalIdentity);
+            val username = usernameFrom(oAuth2User);
+            var userMemento = UserMemento.ofNameAndRoleNames(username);
+            userMemento = userMemento.withAvatarUrl(avatarUrlFrom(oAuth2User));
+            userMemento = userMemento.withRealName(realNameFrom(oAuth2User));
+            return userMemento;
+        }
+        return null;
+    }
+
+    protected static String usernameFrom(OAuth2User oAuth2User) {
+        val loginAttr = oAuth2User.getAttributes().get("login");
+        return loginAttr instanceof CharSequence
+                ? ((CharSequence) loginAttr).toString()
+                : oAuth2User.getName();
+    }
+
+    protected static URL avatarUrlFrom(OAuth2User oAuth2User) {
+        final Object avatarUrlObj = oAuth2User.getAttributes().get("avatar_url");
+        if(avatarUrlObj instanceof String) {
+            try {
+                return new URL((String)avatarUrlObj);
+            } catch (MalformedURLException e) {
+                return null;
+            }
+        }
+        return null;
+    }
+
+    protected static String realNameFrom(OAuth2User oAuth2User) {
+        final Object nameAttr = oAuth2User.getAttributes().get("name");
+        if(nameAttr instanceof String) {
+            return (String)nameAttr;
         }
         return null;
     }
+
 }
diff --git a/security/spring/src/test/java/org/apache/isis/security/spring/authconverters/AuthenticationConverterOfOAuth2UserPrincipal_Test.java b/security/spring/src/test/java/org/apache/isis/security/spring/authconverters/AuthenticationConverterOfOAuth2UserPrincipal_Test.java
new file mode 100644
index 0000000..1ceab90
--- /dev/null
+++ b/security/spring/src/test/java/org/apache/isis/security/spring/authconverters/AuthenticationConverterOfOAuth2UserPrincipal_Test.java
@@ -0,0 +1,225 @@
+package org.apache.isis.security.spring.authconverters;
+
+import java.net.MalformedURLException;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Map;
+
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.oauth2.core.user.OAuth2User;
+
+import lombok.val;
+
+@ExtendWith(MockitoExtension.class)
+class AuthenticationConverterOfOAuth2UserPrincipal_Test {
+
+    @Mock Authentication mockAuthentication;
+
+    AuthenticationConverterOfOAuth2UserPrincipal converter;
+
+    @BeforeEach
+    void before() {
+        converter = new AuthenticationConverterOfOAuth2UserPrincipal();
+    }
+
+    @Nested
+    class userNameFrom {
+
+        @Test
+        void given_login_attr_exists_then_used() {
+
+            // given
+            final OAuth2User oAuth2User = new OAuth2User() {
+                @Override public String getName() { return "fred"; }
+                @Override public Map<String, Object> getAttributes() {
+                    return Collections.singletonMap("login", "freddy");
+                }
+                @Override public Collection<? extends GrantedAuthority> getAuthorities() {
+                    return Collections.emptyList();
+                }
+            };
+
+            // expect
+            Mockito.when(mockAuthentication.getPrincipal()).thenReturn(oAuth2User);
+
+            // when
+            val userMemento = converter.convert(mockAuthentication);
+
+            // then
+            Assertions.assertThat(userMemento).isNotNull();
+            Assertions.assertThat(userMemento.getName()).isEqualTo("freddy");
+
+        }
+
+        @Test
+        void given_no_login_attr_exists_then_name_instead_is_used() {
+
+            // given
+            final OAuth2User oAuth2User = new OAuth2User() {
+                @Override public String getName() { return "fred"; }
+                @Override public Map<String, Object> getAttributes() {
+                    return Collections.emptyMap();
+                }
+                @Override public Collection<? extends GrantedAuthority> getAuthorities() {
+                    return Collections.emptyList();
+                }
+            };
+
+            // expect
+            Mockito.when(mockAuthentication.getPrincipal()).thenReturn(oAuth2User);
+
+            // when
+            val userMemento = converter.convert(mockAuthentication);
+
+            // then
+            Assertions.assertThat(userMemento).isNotNull();
+            Assertions.assertThat(userMemento.getName()).isEqualTo("fred");
+
+        }
+    }
+
+    @Nested
+    class avatarUrl {
+
+        @Test
+        void given_avatarUrl_attr_exists_and_can_be_parsed_then_used() throws MalformedURLException {
+
+            // given
+            final OAuth2User oAuth2User = new OAuth2User() {
+                @Override public String getName() { return "fred"; }
+                @Override public Map<String, Object> getAttributes() {
+                    return Collections.singletonMap("avatar_url", "https://upload.wikimedia.org/wikipedia/en/a/ad/Fred_Flintstone.png");
+                }
+                @Override public Collection<? extends GrantedAuthority> getAuthorities() {
+                    return Collections.emptyList();
+                }
+            };
+
+            // expect
+            Mockito.when(mockAuthentication.getPrincipal()).thenReturn(oAuth2User);
+
+            // when
+            val userMemento = converter.convert(mockAuthentication);
+
+            // then
+            Assertions.assertThat(userMemento).isNotNull();
+            Assertions.assertThat(userMemento.getAvatarUrl()).isEqualTo(new java.net.URL("https://upload.wikimedia.org/wikipedia/en/a/ad/Fred_Flintstone.png"));
+
+        }
+
+        @Test
+        void given_avatarUrl_attr_exists_but_cannot_be_parsed_then_not_used() throws MalformedURLException {
+
+            // given
+            final OAuth2User oAuth2User = new OAuth2User() {
+                @Override public String getName() { return "fred"; }
+                @Override public Map<String, Object> getAttributes() {
+                    return Collections.singletonMap("avatar_url", "GARBAGE://upload.wikimedia.org/wikipedia/en/a/ad/Fred_Flintstone.png");
+                }
+                @Override public Collection<? extends GrantedAuthority> getAuthorities() {
+                    return Collections.emptyList();
+                }
+            };
+
+            // expect
+            Mockito.when(mockAuthentication.getPrincipal()).thenReturn(oAuth2User);
+
+            // when
+            val userMemento = converter.convert(mockAuthentication);
+
+            // then
+            Assertions.assertThat(userMemento).isNotNull();
+            Assertions.assertThat(userMemento.getAvatarUrl()).isNull();
+        }
+
+        @Test
+        void given_avatarUrl_attr_does_not_exists_then_none() throws MalformedURLException {
+
+            // given
+            final OAuth2User oAuth2User = new OAuth2User() {
+                @Override public String getName() { return "fred"; }
+                @Override public Map<String, Object> getAttributes() {
+                    return Collections.emptyMap();
+                }
+                @Override public Collection<? extends GrantedAuthority> getAuthorities() {
+                    return Collections.emptyList();
+                }
+            };
+
+            // expect
+            Mockito.when(mockAuthentication.getPrincipal()).thenReturn(oAuth2User);
+
+            // when
+            val userMemento = converter.convert(mockAuthentication);
+
+            // then
+            Assertions.assertThat(userMemento).isNotNull();
+            Assertions.assertThat(userMemento.getAvatarUrl()).isNull();
+        }
+
+    }
+
+    @Nested
+    class realName {
+
+        @Test
+        void given_name_attr_exists_then_used() throws MalformedURLException {
+
+            // given
+            final OAuth2User oAuth2User = new OAuth2User() {
+                @Override public String getName() { return "fred"; }
+                @Override public Map<String, Object> getAttributes() {
+                    return Collections.singletonMap("name", "Fred Flintstone");
+                }
+                @Override public Collection<? extends GrantedAuthority> getAuthorities() {
+                    return Collections.emptyList();
+                }
+            };
+
+            // expect
+            Mockito.when(mockAuthentication.getPrincipal()).thenReturn(oAuth2User);
+
+            // when
+            val userMemento = converter.convert(mockAuthentication);
+
+            // then
+            Assertions.assertThat(userMemento).isNotNull();
+            Assertions.assertThat(userMemento.getRealName()).isEqualTo("Fred Flintstone");
+        }
+
+        @Test
+        void given_name_attr_does_not_exists_then_none() throws MalformedURLException {
+
+            // given
+            final OAuth2User oAuth2User = new OAuth2User() {
+                @Override public String getName() { return "fred"; }
+                @Override public Map<String, Object> getAttributes() {
+                    return Collections.emptyMap();
+                }
+                @Override public Collection<? extends GrantedAuthority> getAuthorities() {
+                    return Collections.emptyList();
+                }
+            };
+
+            // expect
+            Mockito.when(mockAuthentication.getPrincipal()).thenReturn(oAuth2User);
+
+            // when
+            val userMemento = converter.convert(mockAuthentication);
+
+            // then
+            Assertions.assertThat(userMemento).isNotNull();
+            Assertions.assertThat(userMemento.getRealName()).isNull();
+        }
+
+    }
+}