package com.mailserver.controller; import com.mailserver.config.MailAdmins; import com.mailserver.config.MailDomains; import com.mailserver.domain.User; import com.mailserver.service.MailService; import com.mailserver.service.UserService; import org.junit.jupiter.api.Test; import org.springframework.http.HttpStatus; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.web.server.ResponseStatusException; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; /** Settings → Mailboxes: only administrators; mailboxes only in our domains; a generated password comes back once. */ class MailboxAdminControllerTest { private final UserService users = mock(UserService.class); private final MailService mail = mock(MailService.class); private final MailDomains domains = new MailDomains("jambocheck.com", ""); private final MailAdmins admins = new MailAdmins("", "", domains); private final com.mailserver.util.JwtUtils jwt = mock(com.mailserver.util.JwtUtils.class); private final com.mailserver.service.MailLists lists = mock(com.mailserver.service.MailLists.class); private final com.mailserver.service.MailDirectory directory = new com.mailserver.service.MailDirectory(users, lists, domains); private final MailboxAdminController controller = new MailboxAdminController(users, mail, domains, admins, jwt, directory, lists); private final User admin = user(1L, "admin@jambocheck.com"); private final User alice = user(2L, "alice@jambocheck.com"); MailboxAdminControllerTest() { when(mail.usedBytes(anyLong())).thenReturn(Mono.just(0L)); when(lists.exists(anyString())).thenReturn(Mono.just(false)); when(lists.exists("alerts@jambocheck.com")).thenReturn(Mono.just(true)); when(users.findOptional(anyString())).thenReturn(Mono.empty()); when(users.findOptional("alice@jambocheck.com")).thenReturn(Mono.just(alice)); when(lists.save(anyString(), any(), any(), any(Boolean.class), any())).thenAnswer(i -> Mono.just(new com.mailserver.service.MailLists.MailList(5L, i.getArgument(0), i.getArgument(1), i.getArgument(2), i.getArgument(3), List.of("alice@jambocheck.com")))); when(users.all()).thenReturn(Flux.just(alice, admin)); when(users.byId(1L)).thenReturn(Mono.just(admin)); when(users.byId(2L)).thenReturn(Mono.just(alice)); when(users.byId(99L)).thenReturn(Mono.empty()); when(users.register(anyString(), anyString(), any())).thenAnswer(i -> Mono.just(user(3L, i.getArgument(0)))); when(users.setPassword(any(), anyString())).thenAnswer(i -> Mono.just((User) i.getArgument(0))); when(users.setActive(any(), any(Boolean.class))).thenAnswer(i -> { User u = i.getArgument(0); u.setActive(i.getArgument(1)); return Mono.just(u); }); when(users.setTrap(any(), any(Boolean.class))).thenAnswer(i -> { User u = i.getArgument(0); u.setTrap(i.getArgument(1)); return Mono.just(u); }); } @Test void mailboxAndListShareOneAddressSpaceNoPlusInNames() { assertThat(status(() -> create("admin@jambocheck.com", "alerts@jambocheck.com", ""))).isEqualTo(HttpStatus.CONFLICT); assertThat(status(() -> create("admin@jambocheck.com", "bob+x@jambocheck.com", ""))).isEqualTo(HttpStatus.BAD_REQUEST); assertThat(create("admin@jambocheck.com", "jambotron_prod@jambocheck.com", "").mailbox().email()).isEqualTo("jambotron_prod@jambocheck.com"); var request = new MailboxAdminController.ListRequest("prod", "alerts", false, List.of("Alice@jambocheck.com")); assertThat(controller.saveList(as("admin@jambocheck.com"), "alerts-prod@jambocheck.com", Mono.just(request)).block().members()) .containsExactly("alice@jambocheck.com"); assertThat(status(() -> controller.saveList(as("admin@jambocheck.com"), "alice@jambocheck.com", Mono.just(request)).block())) .isEqualTo(HttpStatus.CONFLICT); var unknown = new MailboxAdminController.ListRequest("prod", null, false, List.of("ghost@jambocheck.com")); assertThat(status(() -> controller.saveList(as("admin@jambocheck.com"), "alerts-prod@jambocheck.com", Mono.just(unknown)).block())) .isEqualTo(HttpStatus.BAD_REQUEST); var badFolder = new MailboxAdminController.ListRequest("Trash", null, false, List.of()); assertThat(status(() -> controller.saveList(as("admin@jambocheck.com"), "alerts-prod@jambocheck.com", Mono.just(badFolder)).block())) .isEqualTo(HttpStatus.BAD_REQUEST); assertThat(status(() -> controller.saveList(as("alice@jambocheck.com"), "alerts-prod@jambocheck.com", Mono.just(request)).block())) .isEqualTo(HttpStatus.FORBIDDEN); } @Test void administratorSwitchesTrapOthersCanNot() { assertThat(controller.trap(as("admin@jambocheck.com"), 2L, Mono.just(new MailboxAdminController.TrapRequest(true))).block().trap()).isTrue(); assertThat(alice.isTrap()).isTrue(); assertThat(controller.trap(as("admin@jambocheck.com"), 2L, Mono.just(new MailboxAdminController.TrapRequest(false))).block().trap()).isFalse(); assertThat(status(() -> controller.trap(as("alice@jambocheck.com"), 2L, Mono.just(new MailboxAdminController.TrapRequest(true))).block())) .isEqualTo(HttpStatus.FORBIDDEN); } @Test void administratorIsTheFirstMailboxByDefault() { assertThat(admins.isAdmin("ADMIN@jambocheck.com")).isTrue(); assertThat(admins.isAdmin("alice@jambocheck.com")).isFalse(); assertThat(new MailAdmins("ops@jambocheck.com, root@jambocheck.com", "admin@jambocheck.com", domains).isAdmin("admin@jambocheck.com")).isFalse(); } @Test void listsMailboxesSortedForAnAdministratorOnly() { List list = controller.list(as("admin@jambocheck.com")).collectList().block(); assertThat(list).extracting(MailboxAdminController.MailboxView::email).containsExactly("admin@jambocheck.com", "alice@jambocheck.com"); assertThat(list.get(0).admin()).isTrue(); assertThat(status(() -> controller.list(as("alice@jambocheck.com")).collectList().block())).isEqualTo(HttpStatus.FORBIDDEN); } @Test void createsReservedNamesWithAGeneratedPasswordShownOnce() { var created = controller.create(as("admin@jambocheck.com"), Mono.just(new MailboxAdminController.CreateRequest(" No-Reply@JamboCheck.com ", "Jambotron", ""))).block(); assertThat(created.mailbox().email()).isEqualTo("no-reply@jambocheck.com"); assertThat(created.password()).hasSizeGreaterThanOrEqualTo(20); verify(users).register(eq("no-reply@jambocheck.com"), eq(created.password()), eq("Jambotron")); var chosen = controller.create(as("admin@jambocheck.com"), Mono.just(new MailboxAdminController.CreateRequest("monitor@jambocheck.com", null, "long-enough-1"))).block(); assertThat(chosen.password()).isNull(); } @Test void refusesOtherDomainsBadAddressesShortPasswordsAndNonAdmins() { assertThat(status(() -> create("admin@jambocheck.com", "x@gmail.com", ""))).isEqualTo(HttpStatus.BAD_REQUEST); assertThat(status(() -> create("admin@jambocheck.com", "bad address@jambocheck.com", ""))).isEqualTo(HttpStatus.BAD_REQUEST); assertThat(status(() -> create("admin@jambocheck.com", "x@jambocheck.com", "short"))).isEqualTo(HttpStatus.BAD_REQUEST); assertThat(status(() -> create("alice@jambocheck.com", "x@jambocheck.com", ""))).isEqualTo(HttpStatus.FORBIDDEN); verify(users, never()).register(anyString(), anyString(), any()); when(users.register(eq("alice@jambocheck.com"), anyString(), any())).thenReturn(Mono.error(new IllegalArgumentException("exists"))); assertThat(status(() -> create("admin@jambocheck.com", "alice@jambocheck.com", ""))).isEqualTo(HttpStatus.CONFLICT); } @Test void newPasswordAndSwitchOffButNotYourself() { var reset = controller.password(as("admin@jambocheck.com"), 2L, Mono.just(new MailboxAdminController.PasswordRequest(null))).block(); assertThat(reset.password()).isNotBlank(); verify(users).setPassword(alice, reset.password()); assertThat(controller.active(as("admin@jambocheck.com"), 2L, Mono.just(new MailboxAdminController.ActiveRequest(false))).block().active()).isFalse(); assertThat(status(() -> controller.active(as("admin@jambocheck.com"), 1L, Mono.just(new MailboxAdminController.ActiveRequest(false))).block())) .isEqualTo(HttpStatus.BAD_REQUEST); } @Test void administratorOpensAnyMailboxOthersCanNot() { when(jwt.generateToken("alice@jambocheck.com")).thenReturn("alice-token"); var session = controller.open(as("admin@jambocheck.com"), 2L).block(); assertThat(session.getEmail()).isEqualTo("alice@jambocheck.com"); assertThat(session.getToken()).isEqualTo("alice-token"); assertThat(status(() -> controller.open(as("alice@jambocheck.com"), 1L).block())).isEqualTo(HttpStatus.FORBIDDEN); assertThat(status(() -> controller.open(as("admin@jambocheck.com"), 99L).block())).isEqualTo(HttpStatus.NOT_FOUND); } private MailboxAdminController.Created create(String me, String email, String password) { return controller.create(as(me), Mono.just(new MailboxAdminController.CreateRequest(email, null, password))).block(); } private static Mono as(String email) { return Mono.just(new UsernamePasswordAuthenticationToken(email, null, List.of())); } private static HttpStatus status(Runnable call) { Throwable error = org.assertj.core.api.Assertions.catchThrowable(call::run); assertThat(error).isInstanceOf(ResponseStatusException.class); return HttpStatus.valueOf(((ResponseStatusException) error).getStatusCode().value()); } private static User user(Long id, String email) { User user = new User(); user.setId(id); user.setEmail(email); user.setPassword("hash"); return user; } }