From 1b7bafd9f0eed67c8d4f3be18c556a38d95603d2 Mon Sep 17 00:00:00 2001 From: lobo Date: Fri, 2 Oct 2026 07:53:35 +0200 Subject: [PATCH] feat(mail): send broadcasts to every active user Remove the platform_mails opt-out. Broadcasts go to all active users. V28 drops the column; the PUT /api/users/settings/platform-mails endpoint and the footer line about turning mails off are gone. --- readme.md | 14 +++---- .../api/dto/request/BroadcastRequest.java | 2 +- .../mail/exception/NoRecipientsException.java | 2 +- .../modules/mail/model/MailTemplate.java | 8 ++-- .../mail/repo/BroadcastRepository.java | 2 +- .../mail/repo/MailOutboxRepository.java | 4 +- .../mail/service/BroadcastService.java | 16 +++----- .../mail/service/MailOutboxService.java | 2 +- .../modules/user/api/UserController.java | 8 ---- .../api/dto/request/PlatformMailsRequest.java | 9 ----- .../api/dto/response/SettingsResponse.java | 2 +- .../modules/user/api/mapper/ApiMapper.java | 1 - .../modules/user/model/UserSettings.java | 3 -- .../entities/UserSettingsEntity.java | 3 -- .../user/persistence/mapper/EntityMapper.java | 2 - .../modules/user/service/UserService.java | 7 ---- .../db/migration/V28__Drop_Platform_Mails.sql | 2 + .../modules/mail/api/BroadcastFlowTest.java | 39 +++++++++---------- .../modules/mail/model/MailTemplateTest.java | 6 +-- .../mail/repo/BroadcastRepositoryTest.java | 25 ++++++------ .../mail/repo/MailOutboxRepositoryTest.java | 13 +++---- .../mail/service/BroadcastServiceTest.java | 7 ++-- .../modules/user/api/UserControllerTest.java | 16 -------- .../user/persistence/UserRepositoryTest.java | 16 -------- .../modules/user/service/UserServiceTest.java | 7 ---- 25 files changed, 65 insertions(+), 151 deletions(-) delete mode 100644 src/main/java/auth_service/modules/user/api/dto/request/PlatformMailsRequest.java create mode 100644 src/main/resources/db/migration/V28__Drop_Platform_Mails.sql diff --git a/readme.md b/readme.md index 3142c0f..c3466bf 100644 --- a/readme.md +++ b/readme.md @@ -88,12 +88,11 @@ To Be Played is split into three repositories: registration was denied", later "your account is blocked" and "your account will be deleted from ". The registration sends the browser language (`de` for a German browser, else `en`), which also becomes the user's app language. Plain text only. See "Mail flow". - **Broadcast mail**: a PLATFORM_ADMIN writes one mail with subject and text in English and German (plain text), - and sends it to all active users or to picked ones, each in their app language. A user can turn it off - (`user_settings.platform_mails`, default on); the footer of the mail says where. "Send test to me" mails the draft - to the admin only, in the admin's language. One `mail_broadcast` row keeps the texts and the counts (recipients, - sent, failed); `BROADCAST_SEND` goes to `admin_audit`. The sender takes account mails first and stays under - `APP_MAIL_DAILY_LIMIT` per 24 h, 10 of them kept for account mails. A recipient who is blocked, deleted or opted - out before the send counts as failed. + and sends it to all active users or to picked ones, each in their app language. Every active user gets it. + "Send test to me" mails the draft to the admin only, in the admin's language. One `mail_broadcast` row keeps the + texts and the counts (recipients, sent, failed); `BROADCAST_SEND` goes to `admin_audit`. The sender takes account + mails first and stays under `APP_MAIL_DAILY_LIMIT` per 24 h, 10 of them kept for account mails. A recipient who is + blocked or deleted before the send counts as failed. - **Delete my account** with the password (5 wrong tries per 15 minutes, then `429`). It only sets `users.deleted_at` and revokes every refresh token: from then on no login, no refresh, no access token, no public profile, no Steam link, not in the landing stats. Email and display name stay taken; the Steam ID is freed, so an @@ -139,7 +138,6 @@ To Be Played is split into three repositories: | `PUT` | `/api/users/settings/profile-style` (`{accent, corners, bannerAppid, backgroundAppid}`; `204`) | access token | | `PUT` | `/api/users/settings/dashboard-tiles` (`{tiles: [{id, size}]}` in order, at most 20; `{tiles: null}` resets; `204`). Auth checks only the id shape `[A-Z_]{1,30}`: Core and the Frontend skip ids they do not know, and Core never shares the overview or the activity | access token | | `PUT` | `/api/users/settings/welcome-seen` (`204`; stamps `user_settings.welcome_seen_at`, sent as `settings.welcomeSeenAt` in `/me`, null until then) | access token | -| `PUT` | `/api/users/settings/platform-mails` (`{enabled}`; `204`) | access token | | `GET` | `/api/admin/broadcasts` (`{results}` with the counts, newest first) | PLATFORM_ADMIN | | `GET` | `/api/admin/broadcasts/recipient-count?userIds=` (`{count}`; no `userIds` = all) | PLATFORM_ADMIN | | `POST` | `/api/admin/broadcasts` (`{subjectEn, subjectDe, bodyEn, bodyDe, userIds}`; `201 {recipientCount}`; `409 NO_RECIPIENTS`) | PLATFORM_ADMIN | @@ -296,7 +294,7 @@ sequenceDiagram participant A as Auth participant M as SMTP (Hostinger) B->>A: admin sends a broadcast - A->>A: mail_broadcast row + one outbox row per active, opted-in user + A->>A: mail_broadcast row + one outbox row per active user B->>A: register / admin decides / admin blocks / delete account A->>A: event + mail_outbox row (PENDING), one transaction loop every minute (MailOutboxService) diff --git a/src/main/java/auth_service/modules/mail/api/dto/request/BroadcastRequest.java b/src/main/java/auth_service/modules/mail/api/dto/request/BroadcastRequest.java index fa9c6f8..c35d96d 100644 --- a/src/main/java/auth_service/modules/mail/api/dto/request/BroadcastRequest.java +++ b/src/main/java/auth_service/modules/mail/api/dto/request/BroadcastRequest.java @@ -8,7 +8,7 @@ import java.util.List; /** * Plain text, never HTML. * - * @param userIds null = every active user with platform mails on; the test mail ignores it + * @param userIds null = every active user; the test mail ignores it */ public record BroadcastRequest(@NotBlank @Size(max = 150) String subjectEn, @NotBlank @Size(max = 150) String subjectDe, @NotBlank @Size(max = 5000) String bodyEn, @NotBlank @Size(max = 5000) String bodyDe, diff --git a/src/main/java/auth_service/modules/mail/exception/NoRecipientsException.java b/src/main/java/auth_service/modules/mail/exception/NoRecipientsException.java index ff58883..9f295cf 100644 --- a/src/main/java/auth_service/modules/mail/exception/NoRecipientsException.java +++ b/src/main/java/auth_service/modules/mail/exception/NoRecipientsException.java @@ -3,6 +3,6 @@ package auth_service.modules.mail.exception; public class NoRecipientsException extends RuntimeException { public NoRecipientsException() { - super("No active user with platform mails on among the recipients"); + super("No active user among the recipients"); } } diff --git a/src/main/java/auth_service/modules/mail/model/MailTemplate.java b/src/main/java/auth_service/modules/mail/model/MailTemplate.java index bd79719..f8f4293 100644 --- a/src/main/java/auth_service/modules/mail/model/MailTemplate.java +++ b/src/main/java/auth_service/modules/mail/model/MailTemplate.java @@ -126,21 +126,19 @@ public enum MailTemplate { ToBePlayed """), - // Admin text only: %1$s subject, %2$s body, %3$s settings URL. + // Admin text only: %1$s subject, %2$s body. BROADCAST( "%1$s", """ %2$s -- - You get this mail because you have an account on ToBePlayed. Turn these mails off in your settings: - %3$s + You get this mail because you have an account on ToBePlayed. """, "%1$s", """ %2$s -- - Du bekommst diese Mail, weil du ein Konto bei ToBePlayed hast. In den Einstellungen kannst du sie abschalten: - %3$s + Du bekommst diese Mail, weil du ein Konto bei ToBePlayed hast. """); private static final DateTimeFormatter DATE_DE = DateTimeFormatter.ofPattern("dd.MM.yyyy"); diff --git a/src/main/java/auth_service/modules/mail/repo/BroadcastRepository.java b/src/main/java/auth_service/modules/mail/repo/BroadcastRepository.java index ec9d197..432239b 100644 --- a/src/main/java/auth_service/modules/mail/repo/BroadcastRepository.java +++ b/src/main/java/auth_service/modules/mail/repo/BroadcastRepository.java @@ -18,7 +18,7 @@ import java.util.Objects; public class BroadcastRepository { static final String RECIPIENTS = "FROM users u JOIN user_settings s ON s.user_id = u.id WHERE " - + ActiveUser.of("u") + " AND s.platform_mails"; + + ActiveUser.of("u"); private final JdbcTemplate jdbc; private final NamedParameterJdbcTemplate namedJdbc; diff --git a/src/main/java/auth_service/modules/mail/repo/MailOutboxRepository.java b/src/main/java/auth_service/modules/mail/repo/MailOutboxRepository.java index e3f7cea..82362ad 100644 --- a/src/main/java/auth_service/modules/mail/repo/MailOutboxRepository.java +++ b/src/main/java/auth_service/modules/mail/repo/MailOutboxRepository.java @@ -70,14 +70,14 @@ public class MailOutboxRepository { } /** - * Fails the pending broadcast rows whose user is no longer active or turned the mails off since the queue time. + * Fails the pending broadcast rows whose user is no longer active since the queue time. * * @return the number of rows failed */ public int failBroadcastRowsOfInactiveUsers() { return Objects.requireNonNull(jdbc.queryForObject(""" WITH skipped AS ( - UPDATE mail_outbox o SET status = 'FAILED', last_error = 'recipient inactive or opted out' + UPDATE mail_outbox o SET status = 'FAILED', last_error = 'recipient inactive' WHERE o.status = 'PENDING' AND o.broadcast_id IS NOT NULL AND NOT EXISTS (SELECT 1 %s AND u.id = o.user_id) RETURNING o.broadcast_id), diff --git a/src/main/java/auth_service/modules/mail/service/BroadcastService.java b/src/main/java/auth_service/modules/mail/service/BroadcastService.java index 12b271c..05fce98 100644 --- a/src/main/java/auth_service/modules/mail/service/BroadcastService.java +++ b/src/main/java/auth_service/modules/mail/service/BroadcastService.java @@ -12,7 +12,6 @@ import auth_service.shared.audit.AdminAuditLog; import auth_service.shared.security.RequiresRole; import auth_service.shared.security.Role; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -27,14 +26,11 @@ public class BroadcastService { private final BroadcastRepository broadcasts; private final MailOutboxRepository outbox; private final AdminAuditLog adminAuditLog; - private final String settingsUrl; - public BroadcastService(BroadcastRepository broadcasts, MailOutboxRepository outbox, AdminAuditLog adminAuditLog, - @Value("${app.frontend-url}") String frontendUrl) { + public BroadcastService(BroadcastRepository broadcasts, MailOutboxRepository outbox, AdminAuditLog adminAuditLog) { this.broadcasts = broadcasts; this.outbox = outbox; this.adminAuditLog = adminAuditLog; - this.settingsUrl = frontendUrl + "/settings"; } public List findAll() { @@ -42,21 +38,21 @@ public class BroadcastService { } /** - * @param userIds null = every active user with platform mails on + * @param userIds null = every active user */ public long countRecipients(List userIds) { return broadcasts.countRecipients(userIds); } /** - * @param userIds null = every active user with platform mails on + * @param userIds null = every active user * @return the number of mails queued * @throws NoRecipientsException when nobody is left; nothing is stored then */ @Transactional public int send(long actorUserId, BroadcastText text, List userIds) { - Mail en = MailTemplate.BROADCAST.render("en", text.subjectEn(), text.bodyEn(), settingsUrl); - Mail de = MailTemplate.BROADCAST.render("de", text.subjectDe(), text.bodyDe(), settingsUrl); + Mail en = MailTemplate.BROADCAST.render("en", text.subjectEn(), text.bodyEn()); + Mail de = MailTemplate.BROADCAST.render("de", text.subjectDe(), text.bodyDe()); long id = broadcasts.insert(text, actorUserId, Instant.now()); int queued = broadcasts.queue(id, userIds, en, de); if (queued == 0) { @@ -76,7 +72,7 @@ public class BroadcastService { Recipient admin = outbox.findRecipient(actorUserId).orElseThrow(); boolean german = MailTemplate.isGerman(admin.language()); Mail mail = MailTemplate.BROADCAST.render(admin.language(), german ? text.subjectDe() : text.subjectEn(), - german ? text.bodyDe() : text.bodyEn(), settingsUrl); + german ? text.bodyDe() : text.bodyEn()); outbox.insert(MailTemplate.BROADCAST, actorUserId, admin.email(), mail); } } diff --git a/src/main/java/auth_service/modules/mail/service/MailOutboxService.java b/src/main/java/auth_service/modules/mail/service/MailOutboxService.java index 766e21f..9fca3f6 100644 --- a/src/main/java/auth_service/modules/mail/service/MailOutboxService.java +++ b/src/main/java/auth_service/modules/mail/service/MailOutboxService.java @@ -68,7 +68,7 @@ public class MailOutboxService { Instant now = Instant.now(); int skipped = repository.failBroadcastRowsOfInactiveUsers(); if (skipped > 0) { - log.info("{} broadcast mails skipped: recipient inactive or opted out", skipped); + log.info("{} broadcast mails skipped: recipient inactive", skipped); } int budget = dailyLimit - repository.countSentSince(now.minus(Duration.ofHours(24))); for (OutboxRow row : repository.findDue(now, Math.min(BATCH, Math.max(budget, 0)))) { diff --git a/src/main/java/auth_service/modules/user/api/UserController.java b/src/main/java/auth_service/modules/user/api/UserController.java index 3e287de..ad5b852 100644 --- a/src/main/java/auth_service/modules/user/api/UserController.java +++ b/src/main/java/auth_service/modules/user/api/UserController.java @@ -4,7 +4,6 @@ import auth_service.shared.security.UserPrincipal; import auth_service.modules.user.api.dto.request.AppearanceRequest; import auth_service.modules.user.api.dto.request.DashboardTilesRequest; import auth_service.modules.user.api.dto.request.FavoritePlatformRequest; -import auth_service.modules.user.api.dto.request.PlatformMailsRequest; import auth_service.modules.user.api.dto.request.ProfileTilesRequest; import auth_service.modules.user.api.dto.request.ProfileStyleRequest; import auth_service.modules.user.api.dto.request.UserSettingsRequest; @@ -65,13 +64,6 @@ public class UserController { return ResponseEntity.noContent().build(); } - @PutMapping(value = "/settings/platform-mails") - public ResponseEntity updatePlatformMails(@AuthenticationPrincipal UserPrincipal principal, - @Valid @RequestBody PlatformMailsRequest request) { - this.userService.updatePlatformMails(principal.getIdAsLong(), request.enabled()); - return ResponseEntity.noContent().build(); - } - @PutMapping(value = "/settings/welcome-seen") public ResponseEntity markWelcomeSeen(@AuthenticationPrincipal UserPrincipal principal) { this.userService.markWelcomeSeen(principal.getIdAsLong()); diff --git a/src/main/java/auth_service/modules/user/api/dto/request/PlatformMailsRequest.java b/src/main/java/auth_service/modules/user/api/dto/request/PlatformMailsRequest.java deleted file mode 100644 index 9f46229..0000000 --- a/src/main/java/auth_service/modules/user/api/dto/request/PlatformMailsRequest.java +++ /dev/null @@ -1,9 +0,0 @@ -package auth_service.modules.user.api.dto.request; - -import jakarta.validation.constraints.NotNull; - -/** - * @param enabled boxed, so a missing field is a 400 and not an opt-out - */ -public record PlatformMailsRequest(@NotNull Boolean enabled) { -} diff --git a/src/main/java/auth_service/modules/user/api/dto/response/SettingsResponse.java b/src/main/java/auth_service/modules/user/api/dto/response/SettingsResponse.java index fb9aa4d..e990ac3 100644 --- a/src/main/java/auth_service/modules/user/api/dto/response/SettingsResponse.java +++ b/src/main/java/auth_service/modules/user/api/dto/response/SettingsResponse.java @@ -9,5 +9,5 @@ import java.util.List; public record SettingsResponse(long id, boolean useDarkTheme, String language, boolean publicProfile, AppearanceResponse appearance, ProfileStyleResponse profileStyle, List dashboardTiles, List profileTiles, - String favoritePlatform, boolean platformMails, Instant welcomeSeenAt) { + String favoritePlatform, Instant welcomeSeenAt) { } diff --git a/src/main/java/auth_service/modules/user/api/mapper/ApiMapper.java b/src/main/java/auth_service/modules/user/api/mapper/ApiMapper.java index 0c0742e..f78e2aa 100644 --- a/src/main/java/auth_service/modules/user/api/mapper/ApiMapper.java +++ b/src/main/java/auth_service/modules/user/api/mapper/ApiMapper.java @@ -48,7 +48,6 @@ public abstract class ApiMapper { : toApi(user.getSettings().getDashboardTiles())) .profileTiles(toApi(user.getSettings().getProfileTiles())) .favoritePlatform(user.getSettings().getFavoritePlatform()) - .platformMails(user.getSettings().isPlatformMails()) .welcomeSeenAt(user.getSettings().getWelcomeSeenAt()) .build()) .build(); diff --git a/src/main/java/auth_service/modules/user/model/UserSettings.java b/src/main/java/auth_service/modules/user/model/UserSettings.java index e6f4623..44a1c26 100644 --- a/src/main/java/auth_service/modules/user/model/UserSettings.java +++ b/src/main/java/auth_service/modules/user/model/UserSettings.java @@ -21,9 +21,6 @@ public class UserSettings { @Builder.Default private List profileTiles = DEFAULT_PROFILE_TILES; private String favoritePlatform; - /** false = no broadcast mails; account mails always go. */ - @Builder.Default - private boolean platformMails = true; /** Null: the welcome page was not seen yet. */ private Instant welcomeSeenAt; diff --git a/src/main/java/auth_service/modules/user/persistence/entities/UserSettingsEntity.java b/src/main/java/auth_service/modules/user/persistence/entities/UserSettingsEntity.java index be969c4..429233d 100644 --- a/src/main/java/auth_service/modules/user/persistence/entities/UserSettingsEntity.java +++ b/src/main/java/auth_service/modules/user/persistence/entities/UserSettingsEntity.java @@ -73,9 +73,6 @@ public class UserSettingsEntity { @Column(name = "favorite_platform") private String favoritePlatform; - @Column(name = "platform_mails", nullable = false) - private boolean platformMails; - @JdbcTypeCode(SqlTypes.ARRAY) @Column(name = "profile_tiles", nullable = false) private List profileTiles; diff --git a/src/main/java/auth_service/modules/user/persistence/mapper/EntityMapper.java b/src/main/java/auth_service/modules/user/persistence/mapper/EntityMapper.java index 90fa861..da986e6 100644 --- a/src/main/java/auth_service/modules/user/persistence/mapper/EntityMapper.java +++ b/src/main/java/auth_service/modules/user/persistence/mapper/EntityMapper.java @@ -36,7 +36,6 @@ public abstract class EntityMapper { : settings.getDashboardTiles().stream().map(Tile::parse).toList()) .profileTiles(settings.getProfileTiles().stream().map(Tile::parse).toList()) .favoritePlatform(settings.getFavoritePlatform()) - .platformMails(settings.isPlatformMails()) .welcomeSeenAt(settings.getWelcomeSeenAt()) .build(); @@ -77,7 +76,6 @@ public abstract class EntityMapper { : Tile.storedAll(settings.getDashboardTiles())); target.setProfileTiles(Tile.storedAll(settings.getProfileTiles())); target.setFavoritePlatform(settings.getFavoritePlatform()); - target.setPlatformMails(settings.isPlatformMails()); target.setWelcomeSeenAt(settings.getWelcomeSeenAt()); } } diff --git a/src/main/java/auth_service/modules/user/service/UserService.java b/src/main/java/auth_service/modules/user/service/UserService.java index 9e685f3..d6e8419 100644 --- a/src/main/java/auth_service/modules/user/service/UserService.java +++ b/src/main/java/auth_service/modules/user/service/UserService.java @@ -129,13 +129,6 @@ public class UserService { user -> user.getSettings().setFavoritePlatform(platform == null ? null : platform.strip())); } - @RequiresRole(Role.USER) - @Transactional - @CacheEvict(value = "users", key = "#userId") - public void updatePlatformMails(long userId, boolean enabled) { - userRepository.update(userId, user -> user.getSettings().setPlatformMails(enabled)); - } - @RequiresRole(Role.USER) @Transactional @CacheEvict(value = "users", key = "#userId") diff --git a/src/main/resources/db/migration/V28__Drop_Platform_Mails.sql b/src/main/resources/db/migration/V28__Drop_Platform_Mails.sql new file mode 100644 index 0000000..c764f69 --- /dev/null +++ b/src/main/resources/db/migration/V28__Drop_Platform_Mails.sql @@ -0,0 +1,2 @@ +-- Broadcasts go to every active user (user decision 2026-10-02), so there is no opt-out any more. +ALTER TABLE user_settings DROP COLUMN platform_mails; diff --git a/src/test/java/auth_service/modules/mail/api/BroadcastFlowTest.java b/src/test/java/auth_service/modules/mail/api/BroadcastFlowTest.java index 7bd9090..46d88ed 100644 --- a/src/test/java/auth_service/modules/mail/api/BroadcastFlowTest.java +++ b/src/test/java/auth_service/modules/mail/api/BroadcastFlowTest.java @@ -2,6 +2,7 @@ package auth_service.modules.mail.api; import auth_service.configuration.security.ClientHeaderFilter; import auth_service.modules.mail.service.MailOutboxService; +import auth_service.shared.account.ActiveUser; import auth_service.shared.api.AuthCookies; import auth_service.shared.test.AbstractIntegrationTest; import jakarta.servlet.http.Cookie; @@ -34,7 +35,6 @@ class BroadcastFlowTest extends AbstractIntegrationTest { private static final String PASSWORD = "correct horse battery"; private static final String ADMIN = "broadcast-flow-admin@example.com"; private static final String PLAYER = "broadcast-flow-player@example.com"; - private static final String OPTED_OUT = "broadcast-flow-optedout@example.com"; private static final String BLOCKED = "broadcast-flow-blocked@example.com"; private static final String BASE = "/api/admin/broadcasts"; @@ -50,16 +50,16 @@ class BroadcastFlowTest extends AbstractIntegrationTest { @Autowired private MailOutboxService mailOutboxService; - private long optedOutId; - private List foreignOptedIn; + private long adminId; + private long playerId; + private long blockedId; - private long insertUser(String email, String role, String language, boolean platformMails, boolean enabled) { + private long insertUser(String email, String role, String language, boolean enabled) { long id = jdbc.queryForObject("INSERT INTO users (email, password, enabled, role, display_name) " + "VALUES (?, ?, ?, ?, ?) RETURNING id", Long.class, email, passwordEncoder.encode(PASSWORD), enabled, role, email.substring(15, 20)); addSettingsRows(); - jdbc.update("UPDATE user_settings SET language = ?, platform_mails = ? WHERE user_id = ?", language, - platformMails, id); + jdbc.update("UPDATE user_settings SET language = ? WHERE user_id = ?", language, id); return id; } @@ -68,23 +68,15 @@ class BroadcastFlowTest extends AbstractIntegrationTest { jdbc.update("DELETE FROM mail_outbox"); jdbc.update("DELETE FROM mail_broadcast"); jdbc.update("DELETE FROM users WHERE email LIKE 'broadcast-flow-%'"); - // Users that other test classes left behind must not count as recipients; tearDown restores them. - foreignOptedIn = jdbc.queryForList("SELECT user_id FROM user_settings WHERE platform_mails", Long.class); - jdbc.update("UPDATE user_settings SET platform_mails = FALSE"); - insertUser(ADMIN, "PLATFORM_ADMIN", "de", true, true); - insertUser(PLAYER, "USER", "en", true, true); - optedOutId = insertUser(OPTED_OUT, "USER", "en", false, true); - insertUser(BLOCKED, "USER", "en", true, true); - jdbc.update("UPDATE user_settings SET platform_mails = TRUE WHERE user_id IN " - + "(SELECT id FROM users WHERE email LIKE 'broadcast-flow-%' AND email <> ?)", OPTED_OUT); + adminId = insertUser(ADMIN, "PLATFORM_ADMIN", "de", true); + playerId = insertUser(PLAYER, "USER", "en", true); + blockedId = insertUser(BLOCKED, "USER", "en", true); jdbc.update("UPDATE users SET enabled = FALSE WHERE email = ?", BLOCKED); } @AfterEach void tearDown() { jdbc.update("DELETE FROM users WHERE email LIKE 'broadcast-flow-%'"); - jdbc.batchUpdate("UPDATE user_settings SET platform_mails = TRUE WHERE user_id = ?", - foreignOptedIn.stream().map(id -> new Object[]{id}).toList()); } private Cookie login(String email) throws Exception { @@ -109,11 +101,16 @@ class BroadcastFlowTest extends AbstractIntegrationTest { } @Test - void sendToAll_MailsActiveOptedInUsersInTheirLanguageAndCountsTheSend() throws Exception { + void sendToPicked_MailsActiveUsersInTheirLanguageAndCountsTheSend() throws Exception { Cookie admin = login(ADMIN); - send(get(BASE + "/recipient-count"), admin).andExpect(status().isOk()).andExpect(jsonPath("$.count").value(2)); - send(post(BASE).content(broadcast("Hallo", "null")), admin).andExpect(status().isCreated()) + String picked = adminId + "," + playerId + "," + blockedId; + + send(get(BASE + "/recipient-count?userIds=" + picked), admin).andExpect(status().isOk()) + .andExpect(jsonPath("$.count").value(2)); + long active = jdbc.queryForObject("SELECT COUNT(*) FROM users u WHERE " + ActiveUser.of("u"), Long.class); + send(get(BASE + "/recipient-count"), admin).andExpect(jsonPath("$.count").value(active)); + send(post(BASE).content(broadcast("Hallo", "[" + picked + "]")), admin).andExpect(status().isCreated()) .andExpect(jsonPath("$.recipientCount").value(2)); assertThat(rows()).extracting(row -> row.get("to_address") + " " + row.get("subject") + " " + row.get("kind")) @@ -136,7 +133,7 @@ class BroadcastFlowTest extends AbstractIntegrationTest { void pickedAndTest_StayNarrow() throws Exception { Cookie admin = login(ADMIN); - send(post(BASE).content(broadcast("Hallo", "[" + optedOutId + "]")), admin).andExpect(status().isConflict()) + send(post(BASE).content(broadcast("Hallo", "[" + blockedId + "]")), admin).andExpect(status().isConflict()) .andExpect(jsonPath("$.code").value("NO_RECIPIENTS")); assertThat(jdbc.queryForObject("SELECT COUNT(*) FROM mail_broadcast", Long.class)).isZero(); diff --git a/src/test/java/auth_service/modules/mail/model/MailTemplateTest.java b/src/test/java/auth_service/modules/mail/model/MailTemplateTest.java index b5403ba..95aeaa7 100644 --- a/src/test/java/auth_service/modules/mail/model/MailTemplateTest.java +++ b/src/test/java/auth_service/modules/mail/model/MailTemplateTest.java @@ -27,12 +27,12 @@ class MailTemplateTest extends AbstractUnitTest { } @Test - void render_Broadcast_PutsSubjectBodyAndTheSettingsLink() { + void render_Broadcast_PutsSubjectAndBody() { for (String language : new String[]{"de", "en"}) { - Mail mail = MailTemplate.BROADCAST.render(language, "50% off", "Hello 100%s", "https://tbp.example/settings"); + Mail mail = MailTemplate.BROADCAST.render(language, "50% off", "Hello 100%s"); assertThat(mail.subject()).isEqualTo("50% off"); - assertThat(mail.body()).startsWith("Hello 100%s").endsWith("https://tbp.example/settings\n"); + assertThat(mail.body()).startsWith("Hello 100%s").contains("ToBePlayed").doesNotContain("%1$s", "%2$s", "%3$s"); } } diff --git a/src/test/java/auth_service/modules/mail/repo/BroadcastRepositoryTest.java b/src/test/java/auth_service/modules/mail/repo/BroadcastRepositoryTest.java index 883321c..fe27358 100644 --- a/src/test/java/auth_service/modules/mail/repo/BroadcastRepositoryTest.java +++ b/src/test/java/auth_service/modules/mail/repo/BroadcastRepositoryTest.java @@ -25,7 +25,6 @@ class BroadcastRepositoryTest extends AbstractPersistenceTest { private long german; private long noLanguage; - private long optedOut; private long blocked; private long pending; private long deleted; @@ -35,30 +34,28 @@ class BroadcastRepositoryTest extends AbstractPersistenceTest { jdbc.update("DELETE FROM mail_outbox"); jdbc.update("DELETE FROM mail_broadcast"); jdbc.update("DELETE FROM users WHERE email LIKE 'bc-repo-%'"); - german = user("german", "true, 'ACTIVE', NULL", "de", true); - noLanguage = user("nolang", "true, 'ACTIVE', NULL", "", true); - optedOut = user("optedout", "true, 'ACTIVE', NULL", "de", false); - blocked = user("blocked", "false, 'ACTIVE', NULL", "de", true); - pending = user("pending", "true, 'PENDING', NULL", "de", true); - deleted = user("deleted", "true, 'ACTIVE', NOW()", "de", true); + german = user("german", "true, 'ACTIVE', NULL", "de"); + noLanguage = user("nolang", "true, 'ACTIVE', NULL", ""); + blocked = user("blocked", "false, 'ACTIVE', NULL", "de"); + pending = user("pending", "true, 'PENDING', NULL", "de"); + deleted = user("deleted", "true, 'ACTIVE', NOW()", "de"); } - private long user(String name, String flags, String language, boolean platformMails) { + private long user(String name, String flags, String language) { long id = jdbc.queryForObject("INSERT INTO users (email, password, enabled, status, deleted_at) VALUES (?, 'x', " + flags + ") RETURNING id", Long.class, "bc-repo-" + name + "@example.com"); - jdbc.update("INSERT INTO user_settings (user_id, language, platform_mails) VALUES (?, ?, ?)", id, language, - platformMails); + jdbc.update("INSERT INTO user_settings (user_id, language) VALUES (?, ?)", id, language); return id; } @Test - void countRecipients_TakesOnlyActiveOptedInUsers() { + void countRecipients_TakesOnlyActiveUsers() { // Other tests of the suite may leave users behind, so compare with the same query by SQL. long all = jdbc.queryForObject("SELECT COUNT(*) FROM users u JOIN user_settings s ON s.user_id = u.id " - + "WHERE u.enabled AND u.deleted_at IS NULL AND u.status = 'ACTIVE' AND s.platform_mails", Long.class); + + "WHERE u.enabled AND u.deleted_at IS NULL AND u.status = 'ACTIVE'", Long.class); assertThat(repository.countRecipients(null)).isEqualTo(all).isGreaterThanOrEqualTo(2); - assertThat(repository.countRecipients(List.of(german, blocked, optedOut, pending, deleted))).isEqualTo(1); + assertThat(repository.countRecipients(List.of(german, blocked, pending, deleted))).isEqualTo(1); assertThat(repository.countRecipients(List.of())).isZero(); } @@ -66,7 +63,7 @@ class BroadcastRepositoryTest extends AbstractPersistenceTest { void queue_WritesOneRowPerRecipientInTheirLanguage() { long id = repository.insert(new BroadcastText("Hi", "Hallo", "Body", "Inhalt"), 1L, Instant.now()); - int queued = repository.queue(id, List.of(german, noLanguage, optedOut, blocked), + int queued = repository.queue(id, List.of(german, noLanguage, blocked), new Mail("Hi", "Body"), new Mail("Hallo", "Inhalt")); repository.setRecipientCount(id, queued); diff --git a/src/test/java/auth_service/modules/mail/repo/MailOutboxRepositoryTest.java b/src/test/java/auth_service/modules/mail/repo/MailOutboxRepositoryTest.java index f5308d2..a8223b7 100644 --- a/src/test/java/auth_service/modules/mail/repo/MailOutboxRepositoryTest.java +++ b/src/test/java/auth_service/modules/mail/repo/MailOutboxRepositoryTest.java @@ -162,28 +162,27 @@ class MailOutboxRepositoryTest extends AbstractPersistenceTest { } @Test - void failBroadcastRowsOfInactiveUsers_FailsBlockedDeletedPendingAndOptedOut() { + void failBroadcastRowsOfInactiveUsers_FailsBlockedDeletedAndPending() { long broadcast = broadcast(); long active = user("active", "true, 'ACTIVE', NULL"); long blocked = user("blocked", "false, 'ACTIVE', NULL"); long deleted = user("deleted", "true, 'ACTIVE', NOW()"); long pending = user("pending", "true, 'PENDING', NULL"); - long optedOut = user("optedout", "true, 'ACTIVE', NULL"); - for (long id : new long[]{active, blocked, deleted, pending, optedOut}) { - jdbc.update("INSERT INTO user_settings (user_id, platform_mails) VALUES (?, ?)", id, id != optedOut); + for (long id : new long[]{active, blocked, deleted, pending}) { + jdbc.update("INSERT INTO user_settings (user_id) VALUES (?)", id); } long keep = broadcastRow(broadcast, active); - for (long id : new long[]{blocked, deleted, pending, optedOut}) { + for (long id : new long[]{blocked, deleted, pending}) { broadcastRow(broadcast, id); } long accountOfBlocked = jdbc.queryForObject("INSERT INTO mail_outbox (kind, user_id, to_address, subject, body) " + "VALUES ('ACCOUNT_BLOCKED', ?, 'x@example.com', 's', 'b') RETURNING id", Long.class, blocked); - assertThat(repository.failBroadcastRowsOfInactiveUsers()).isEqualTo(4); + assertThat(repository.failBroadcastRowsOfInactiveUsers()).isEqualTo(3); assertThat(jdbc.queryForList("SELECT id FROM mail_outbox WHERE status = 'PENDING' ORDER BY id", Long.class)) .containsExactly(keep, accountOfBlocked); - assertThat(counter(broadcast, "failed_count")).isEqualTo(4); + assertThat(counter(broadcast, "failed_count")).isEqualTo(3); } @Test diff --git a/src/test/java/auth_service/modules/mail/service/BroadcastServiceTest.java b/src/test/java/auth_service/modules/mail/service/BroadcastServiceTest.java index e185e73..14a6062 100644 --- a/src/test/java/auth_service/modules/mail/service/BroadcastServiceTest.java +++ b/src/test/java/auth_service/modules/mail/service/BroadcastServiceTest.java @@ -32,7 +32,6 @@ import static org.mockito.Mockito.when; class BroadcastServiceTest extends AbstractUnitTest { private static final long ADMIN = 1L; - private static final String SETTINGS = "https://tbp.example/settings"; private static final BroadcastText TEXT = new BroadcastText("Hi", "Hallo", "Body", "Inhalt"); @Mock @@ -48,7 +47,7 @@ class BroadcastServiceTest extends AbstractUnitTest { @BeforeEach void setUp() { - service = new BroadcastService(broadcasts, outbox, adminAuditLog, "https://tbp.example"); + service = new BroadcastService(broadcasts, outbox, adminAuditLog); } @Test @@ -62,9 +61,9 @@ class BroadcastServiceTest extends AbstractUnitTest { ArgumentCaptor de = ArgumentCaptor.forClass(Mail.class); verify(broadcasts).queue(eq(5L), isNull(), en.capture(), de.capture()); assertThat(en.getValue().subject()).isEqualTo("Hi"); - assertThat(en.getValue().body()).startsWith("Body").contains("You get this mail").endsWith(SETTINGS + "\n"); + assertThat(en.getValue().body()).startsWith("Body").contains("You get this mail").doesNotContain("settings"); assertThat(de.getValue().subject()).isEqualTo("Hallo"); - assertThat(de.getValue().body()).startsWith("Inhalt").contains("Du bekommst diese Mail").endsWith(SETTINGS + "\n"); + assertThat(de.getValue().body()).startsWith("Inhalt").contains("Du bekommst diese Mail").doesNotContain("settings"); verify(broadcasts).setRecipientCount(5L, 3); verify(adminAuditLog).record(ADMIN, "BROADCAST_SEND", null, "5"); } diff --git a/src/test/java/auth_service/modules/user/api/UserControllerTest.java b/src/test/java/auth_service/modules/user/api/UserControllerTest.java index bbbcd2b..01f6669 100644 --- a/src/test/java/auth_service/modules/user/api/UserControllerTest.java +++ b/src/test/java/auth_service/modules/user/api/UserControllerTest.java @@ -491,22 +491,6 @@ class UserControllerTest extends AbstractWebMvcTest { verify(userService, never()).updateFavoritePlatform(anyLong(), any()); } - @Test - void updatePlatformMails_StoresTheFlagAndRefusesAMissingOne() throws Exception { - SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken( - new UserPrincipal("1", null), null, Collections.emptyList())); - - mockMvc.perform(put("/api/users/settings/platform-mails") - .contentType(MediaType.APPLICATION_JSON).content("{\"enabled\":false}")) - .andExpect(status().isNoContent()); - mockMvc.perform(put("/api/users/settings/platform-mails") - .contentType(MediaType.APPLICATION_JSON).content("{}")) - .andExpect(status().isBadRequest()); - - verify(userService).updatePlatformMails(1L, false); - verify(userService, never()).updatePlatformMails(anyLong(), eq(true)); - } - @Test void updateProfileTiles_StoresTheTiles() throws Exception { SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken( diff --git a/src/test/java/auth_service/modules/user/persistence/UserRepositoryTest.java b/src/test/java/auth_service/modules/user/persistence/UserRepositoryTest.java index 84b6fc7..3b40abd 100644 --- a/src/test/java/auth_service/modules/user/persistence/UserRepositoryTest.java +++ b/src/test/java/auth_service/modules/user/persistence/UserRepositoryTest.java @@ -251,22 +251,6 @@ class UserRepositoryTest extends AbstractPersistenceTest { assertTrue(findByEmail("public@example.com").orElseThrow().getSettings().isPublicProfile()); } - @Test - void updatePlatformMails_IsKeptByOtherSettingsWrites() { - var userEntity = persisted(UserEntity.builder().email("optout@example.com").password("password") - .enabled(true).registrationDate(LocalDateTime.now()).build()); - assertTrue(findByEmail("optout@example.com").orElseThrow().getSettings().isPlatformMails()); - - userRepository.update(userEntity.getId(), user -> user.getSettings().setPlatformMails(false)); - entityManager.flush(); - entityManager.clear(); - userRepository.update(userEntity.getId(), user -> user.getSettings().setFavoritePlatform("PS5")); - entityManager.flush(); - entityManager.clear(); - - assertFalse(findByEmail("optout@example.com").orElseThrow().getSettings().isPlatformMails()); - } - @Test void updateSettings_KeepsTheAppearance() { var userEntity = persisted(UserEntity.builder() diff --git a/src/test/java/auth_service/modules/user/service/UserServiceTest.java b/src/test/java/auth_service/modules/user/service/UserServiceTest.java index 1cbfe0c..b3a6eea 100644 --- a/src/test/java/auth_service/modules/user/service/UserServiceTest.java +++ b/src/test/java/auth_service/modules/user/service/UserServiceTest.java @@ -195,13 +195,6 @@ class UserServiceTest extends AbstractUnitTest { assertEquals("Switch", appliedChanges(1).getFirst().getSettings().getFavoritePlatform()); } - @Test - void updatePlatformMails_StoresTheFlag() { - userService.updatePlatformMails(1L, false); - - org.junit.jupiter.api.Assertions.assertFalse(appliedChanges(1).getFirst().getSettings().isPlatformMails()); - } - @Test void markWelcomeSeen_StampsNow() { userService.markWelcomeSeen(1L);