Public Access
Compare commits
2
Commits
0af76da9c7
...
6e7fb1e066
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6e7fb1e066 | ||
|
|
1b7bafd9f0 |
No files matched your search
@@ -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 <date>". The registration sends the browser language (`de` for a German browser, else `en`), which also becomes
|
registration was denied", later "your account is blocked" and "your account will be deleted from <date>". 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".
|
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),
|
- **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
|
and sends it to all active users or to picked ones, each in their app language. Every active user gets it.
|
||||||
(`user_settings.platform_mails`, default on); the footer of the mail says where. "Send test to me" mails the draft
|
"Send test to me" mails the draft to the admin only, in the admin's language. One `mail_broadcast` row keeps the
|
||||||
to the admin only, in the admin's language. One `mail_broadcast` row keeps the texts and the counts (recipients,
|
texts and the counts (recipients, sent, failed); `BROADCAST_SEND` goes to `admin_audit`. The sender takes account
|
||||||
sent, failed); `BROADCAST_SEND` goes to `admin_audit`. The sender takes account mails first and stays under
|
mails first and stays under `APP_MAIL_DAILY_LIMIT` per 24 h, 10 of them kept for account mails. A recipient who is
|
||||||
`APP_MAIL_DAILY_LIMIT` per 24 h, 10 of them kept for account mails. A recipient who is blocked, deleted or opted
|
blocked or deleted before the send counts as failed.
|
||||||
out before the send counts as failed.
|
|
||||||
- **Delete my account** with the password (5 wrong tries per 15 minutes, then `429`). It only sets
|
- **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
|
`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
|
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/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/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/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` (`{results}` with the counts, newest first) | PLATFORM_ADMIN |
|
||||||
| `GET` | `/api/admin/broadcasts/recipient-count?userIds=` (`{count}`; no `userIds` = all) | 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 |
|
| `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 A as Auth
|
||||||
participant M as SMTP (Hostinger)
|
participant M as SMTP (Hostinger)
|
||||||
B->>A: admin sends a broadcast
|
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
|
B->>A: register / admin decides / admin blocks / delete account
|
||||||
A->>A: event + mail_outbox row (PENDING), one transaction
|
A->>A: event + mail_outbox row (PENDING), one transaction
|
||||||
loop every minute (MailOutboxService)
|
loop every minute (MailOutboxService)
|
||||||
@@ -352,9 +350,9 @@ nor a database connection waits for Core; a failed call only warns.
|
|||||||
sequenceDiagram
|
sequenceDiagram
|
||||||
participant A as Auth
|
participant A as Auth
|
||||||
participant C as Core
|
participant C as Core
|
||||||
A->>A: link / unlink / block / unblock (commit)
|
A->>A: link / unlink / block / unblock / name change (commit)
|
||||||
A->>C: POST /internal/accounts/states {userId, steamId, blocked}
|
A->>C: POST /internal/accounts/states {userId, steamId, displayName, blocked}
|
||||||
C->>C: blocked_user, Steam ID in user_steam_sync
|
C->>C: blocked_user, Steam ID in user_steam_sync, display name in account_name
|
||||||
Note over A,C: nightly 04:15
|
Note over A,C: nightly 04:15
|
||||||
A->>C: PUT /internal/accounts/states (every user)
|
A->>C: PUT /internal/accounts/states (every user)
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -3,5 +3,5 @@ package auth_service.modules.account.model;
|
|||||||
/**
|
/**
|
||||||
* One user as Core copies it.
|
* One user as Core copies it.
|
||||||
*/
|
*/
|
||||||
public record CoreAccountState(long userId, Long steamId, boolean blocked) {
|
public record CoreAccountState(long userId, Long steamId, String displayName, boolean blocked) {
|
||||||
}
|
}
|
||||||
@@ -60,17 +60,18 @@ public class AccountRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static final RowMapper<CoreAccountState> STATE_ROW = (rs, row) ->
|
private static final RowMapper<CoreAccountState> STATE_ROW = (rs, row) ->
|
||||||
new CoreAccountState(rs.getLong("id"), rs.getObject("steam_id", Long.class), !rs.getBoolean("enabled"));
|
new CoreAccountState(rs.getLong("id"), rs.getObject("steam_id", Long.class), rs.getString("display_name"),
|
||||||
|
!rs.getBoolean("enabled"));
|
||||||
|
|
||||||
public Optional<CoreAccountState> findState(long userId) {
|
public Optional<CoreAccountState> findState(long userId) {
|
||||||
return jdbc.query("SELECT id, steam_id, enabled FROM users WHERE id = ?", STATE_ROW, userId).stream().findFirst();
|
return jdbc.query("SELECT id, steam_id, display_name, enabled FROM users WHERE id = ?", STATE_ROW, userId).stream().findFirst();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Every user, also the flagged ones.
|
* Every user, also the flagged ones.
|
||||||
*/
|
*/
|
||||||
public List<CoreAccountState> findAllStates() {
|
public List<CoreAccountState> findAllStates() {
|
||||||
return jdbc.query("SELECT id, steam_id, enabled FROM users ORDER BY id", STATE_ROW);
|
return jdbc.query("SELECT id, steam_id, display_name, enabled FROM users ORDER BY id", STATE_ROW);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import java.util.List;
|
|||||||
/**
|
/**
|
||||||
* Plain text, never HTML.
|
* 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,
|
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,
|
@NotBlank @Size(max = 5000) String bodyEn, @NotBlank @Size(max = 5000) String bodyDe,
|
||||||
|
|||||||
@@ -3,6 +3,6 @@ package auth_service.modules.mail.exception;
|
|||||||
public class NoRecipientsException extends RuntimeException {
|
public class NoRecipientsException extends RuntimeException {
|
||||||
|
|
||||||
public NoRecipientsException() {
|
public NoRecipientsException() {
|
||||||
super("No active user with platform mails on among the recipients");
|
super("No active user among the recipients");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -126,21 +126,19 @@ public enum MailTemplate {
|
|||||||
ToBePlayed
|
ToBePlayed
|
||||||
"""),
|
"""),
|
||||||
|
|
||||||
// Admin text only: %1$s subject, %2$s body, %3$s settings URL.
|
// Admin text only: %1$s subject, %2$s body.
|
||||||
BROADCAST(
|
BROADCAST(
|
||||||
"%1$s", """
|
"%1$s", """
|
||||||
%2$s
|
%2$s
|
||||||
|
|
||||||
--
|
--
|
||||||
You get this mail because you have an account on ToBePlayed. Turn these mails off in your settings:
|
You get this mail because you have an account on ToBePlayed.
|
||||||
%3$s
|
|
||||||
""",
|
""",
|
||||||
"%1$s", """
|
"%1$s", """
|
||||||
%2$s
|
%2$s
|
||||||
|
|
||||||
--
|
--
|
||||||
Du bekommst diese Mail, weil du ein Konto bei ToBePlayed hast. In den Einstellungen kannst du sie abschalten:
|
Du bekommst diese Mail, weil du ein Konto bei ToBePlayed hast.
|
||||||
%3$s
|
|
||||||
""");
|
""");
|
||||||
|
|
||||||
private static final DateTimeFormatter DATE_DE = DateTimeFormatter.ofPattern("dd.MM.yyyy");
|
private static final DateTimeFormatter DATE_DE = DateTimeFormatter.ofPattern("dd.MM.yyyy");
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import java.util.Objects;
|
|||||||
public class BroadcastRepository {
|
public class BroadcastRepository {
|
||||||
|
|
||||||
static final String RECIPIENTS = "FROM users u JOIN user_settings s ON s.user_id = u.id WHERE "
|
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 JdbcTemplate jdbc;
|
||||||
private final NamedParameterJdbcTemplate namedJdbc;
|
private final NamedParameterJdbcTemplate namedJdbc;
|
||||||
|
|||||||
@@ -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
|
* @return the number of rows failed
|
||||||
*/
|
*/
|
||||||
public int failBroadcastRowsOfInactiveUsers() {
|
public int failBroadcastRowsOfInactiveUsers() {
|
||||||
return Objects.requireNonNull(jdbc.queryForObject("""
|
return Objects.requireNonNull(jdbc.queryForObject("""
|
||||||
WITH skipped AS (
|
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
|
WHERE o.status = 'PENDING' AND o.broadcast_id IS NOT NULL
|
||||||
AND NOT EXISTS (SELECT 1 %s AND u.id = o.user_id)
|
AND NOT EXISTS (SELECT 1 %s AND u.id = o.user_id)
|
||||||
RETURNING o.broadcast_id),
|
RETURNING o.broadcast_id),
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import auth_service.shared.audit.AdminAuditLog;
|
|||||||
import auth_service.shared.security.RequiresRole;
|
import auth_service.shared.security.RequiresRole;
|
||||||
import auth_service.shared.security.Role;
|
import auth_service.shared.security.Role;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
@@ -27,14 +26,11 @@ public class BroadcastService {
|
|||||||
private final BroadcastRepository broadcasts;
|
private final BroadcastRepository broadcasts;
|
||||||
private final MailOutboxRepository outbox;
|
private final MailOutboxRepository outbox;
|
||||||
private final AdminAuditLog adminAuditLog;
|
private final AdminAuditLog adminAuditLog;
|
||||||
private final String settingsUrl;
|
|
||||||
|
|
||||||
public BroadcastService(BroadcastRepository broadcasts, MailOutboxRepository outbox, AdminAuditLog adminAuditLog,
|
public BroadcastService(BroadcastRepository broadcasts, MailOutboxRepository outbox, AdminAuditLog adminAuditLog) {
|
||||||
@Value("${app.frontend-url}") String frontendUrl) {
|
|
||||||
this.broadcasts = broadcasts;
|
this.broadcasts = broadcasts;
|
||||||
this.outbox = outbox;
|
this.outbox = outbox;
|
||||||
this.adminAuditLog = adminAuditLog;
|
this.adminAuditLog = adminAuditLog;
|
||||||
this.settingsUrl = frontendUrl + "/settings";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<Broadcast> findAll() {
|
public List<Broadcast> 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<Long> userIds) {
|
public long countRecipients(List<Long> userIds) {
|
||||||
return broadcasts.countRecipients(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
|
* @return the number of mails queued
|
||||||
* @throws NoRecipientsException when nobody is left; nothing is stored then
|
* @throws NoRecipientsException when nobody is left; nothing is stored then
|
||||||
*/
|
*/
|
||||||
@Transactional
|
@Transactional
|
||||||
public int send(long actorUserId, BroadcastText text, List<Long> userIds) {
|
public int send(long actorUserId, BroadcastText text, List<Long> userIds) {
|
||||||
Mail en = MailTemplate.BROADCAST.render("en", text.subjectEn(), text.bodyEn(), settingsUrl);
|
Mail en = MailTemplate.BROADCAST.render("en", text.subjectEn(), text.bodyEn());
|
||||||
Mail de = MailTemplate.BROADCAST.render("de", text.subjectDe(), text.bodyDe(), settingsUrl);
|
Mail de = MailTemplate.BROADCAST.render("de", text.subjectDe(), text.bodyDe());
|
||||||
long id = broadcasts.insert(text, actorUserId, Instant.now());
|
long id = broadcasts.insert(text, actorUserId, Instant.now());
|
||||||
int queued = broadcasts.queue(id, userIds, en, de);
|
int queued = broadcasts.queue(id, userIds, en, de);
|
||||||
if (queued == 0) {
|
if (queued == 0) {
|
||||||
@@ -76,7 +72,7 @@ public class BroadcastService {
|
|||||||
Recipient admin = outbox.findRecipient(actorUserId).orElseThrow();
|
Recipient admin = outbox.findRecipient(actorUserId).orElseThrow();
|
||||||
boolean german = MailTemplate.isGerman(admin.language());
|
boolean german = MailTemplate.isGerman(admin.language());
|
||||||
Mail mail = MailTemplate.BROADCAST.render(admin.language(), german ? text.subjectDe() : text.subjectEn(),
|
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);
|
outbox.insert(MailTemplate.BROADCAST, actorUserId, admin.email(), mail);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -68,7 +68,7 @@ public class MailOutboxService {
|
|||||||
Instant now = Instant.now();
|
Instant now = Instant.now();
|
||||||
int skipped = repository.failBroadcastRowsOfInactiveUsers();
|
int skipped = repository.failBroadcastRowsOfInactiveUsers();
|
||||||
if (skipped > 0) {
|
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)));
|
int budget = dailyLimit - repository.countSentSince(now.minus(Duration.ofHours(24)));
|
||||||
for (OutboxRow row : repository.findDue(now, Math.min(BATCH, Math.max(budget, 0)))) {
|
for (OutboxRow row : repository.findDue(now, Math.min(BATCH, Math.max(budget, 0)))) {
|
||||||
|
|||||||
@@ -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.AppearanceRequest;
|
||||||
import auth_service.modules.user.api.dto.request.DashboardTilesRequest;
|
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.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.ProfileTilesRequest;
|
||||||
import auth_service.modules.user.api.dto.request.ProfileStyleRequest;
|
import auth_service.modules.user.api.dto.request.ProfileStyleRequest;
|
||||||
import auth_service.modules.user.api.dto.request.UserSettingsRequest;
|
import auth_service.modules.user.api.dto.request.UserSettingsRequest;
|
||||||
@@ -65,13 +64,6 @@ public class UserController {
|
|||||||
return ResponseEntity.noContent().build();
|
return ResponseEntity.noContent().build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@PutMapping(value = "/settings/platform-mails")
|
|
||||||
public ResponseEntity<Void> updatePlatformMails(@AuthenticationPrincipal UserPrincipal principal,
|
|
||||||
@Valid @RequestBody PlatformMailsRequest request) {
|
|
||||||
this.userService.updatePlatformMails(principal.getIdAsLong(), request.enabled());
|
|
||||||
return ResponseEntity.noContent().build();
|
|
||||||
}
|
|
||||||
|
|
||||||
@PutMapping(value = "/settings/welcome-seen")
|
@PutMapping(value = "/settings/welcome-seen")
|
||||||
public ResponseEntity<Void> markWelcomeSeen(@AuthenticationPrincipal UserPrincipal principal) {
|
public ResponseEntity<Void> markWelcomeSeen(@AuthenticationPrincipal UserPrincipal principal) {
|
||||||
this.userService.markWelcomeSeen(principal.getIdAsLong());
|
this.userService.markWelcomeSeen(principal.getIdAsLong());
|
||||||
|
|||||||
@@ -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) {
|
|
||||||
}
|
|
||||||
@@ -9,5 +9,5 @@ import java.util.List;
|
|||||||
public record SettingsResponse(long id, boolean useDarkTheme, String language, boolean publicProfile,
|
public record SettingsResponse(long id, boolean useDarkTheme, String language, boolean publicProfile,
|
||||||
AppearanceResponse appearance, ProfileStyleResponse profileStyle,
|
AppearanceResponse appearance, ProfileStyleResponse profileStyle,
|
||||||
List<TileResponse> dashboardTiles, List<TileResponse> profileTiles,
|
List<TileResponse> dashboardTiles, List<TileResponse> profileTiles,
|
||||||
String favoritePlatform, boolean platformMails, Instant welcomeSeenAt) {
|
String favoritePlatform, Instant welcomeSeenAt) {
|
||||||
}
|
}
|
||||||
@@ -48,7 +48,6 @@ public abstract class ApiMapper {
|
|||||||
: toApi(user.getSettings().getDashboardTiles()))
|
: toApi(user.getSettings().getDashboardTiles()))
|
||||||
.profileTiles(toApi(user.getSettings().getProfileTiles()))
|
.profileTiles(toApi(user.getSettings().getProfileTiles()))
|
||||||
.favoritePlatform(user.getSettings().getFavoritePlatform())
|
.favoritePlatform(user.getSettings().getFavoritePlatform())
|
||||||
.platformMails(user.getSettings().isPlatformMails())
|
|
||||||
.welcomeSeenAt(user.getSettings().getWelcomeSeenAt())
|
.welcomeSeenAt(user.getSettings().getWelcomeSeenAt())
|
||||||
.build())
|
.build())
|
||||||
.build();
|
.build();
|
||||||
|
|||||||
@@ -21,9 +21,6 @@ public class UserSettings {
|
|||||||
@Builder.Default
|
@Builder.Default
|
||||||
private List<Tile> profileTiles = DEFAULT_PROFILE_TILES;
|
private List<Tile> profileTiles = DEFAULT_PROFILE_TILES;
|
||||||
private String favoritePlatform;
|
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. */
|
/** Null: the welcome page was not seen yet. */
|
||||||
private Instant welcomeSeenAt;
|
private Instant welcomeSeenAt;
|
||||||
|
|
||||||
|
|||||||
@@ -73,9 +73,6 @@ public class UserSettingsEntity {
|
|||||||
@Column(name = "favorite_platform")
|
@Column(name = "favorite_platform")
|
||||||
private String favoritePlatform;
|
private String favoritePlatform;
|
||||||
|
|
||||||
@Column(name = "platform_mails", nullable = false)
|
|
||||||
private boolean platformMails;
|
|
||||||
|
|
||||||
@JdbcTypeCode(SqlTypes.ARRAY)
|
@JdbcTypeCode(SqlTypes.ARRAY)
|
||||||
@Column(name = "profile_tiles", nullable = false)
|
@Column(name = "profile_tiles", nullable = false)
|
||||||
private List<String> profileTiles;
|
private List<String> profileTiles;
|
||||||
|
|||||||
@@ -36,7 +36,6 @@ public abstract class EntityMapper {
|
|||||||
: settings.getDashboardTiles().stream().map(Tile::parse).toList())
|
: settings.getDashboardTiles().stream().map(Tile::parse).toList())
|
||||||
.profileTiles(settings.getProfileTiles().stream().map(Tile::parse).toList())
|
.profileTiles(settings.getProfileTiles().stream().map(Tile::parse).toList())
|
||||||
.favoritePlatform(settings.getFavoritePlatform())
|
.favoritePlatform(settings.getFavoritePlatform())
|
||||||
.platformMails(settings.isPlatformMails())
|
|
||||||
.welcomeSeenAt(settings.getWelcomeSeenAt())
|
.welcomeSeenAt(settings.getWelcomeSeenAt())
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
@@ -77,7 +76,6 @@ public abstract class EntityMapper {
|
|||||||
: Tile.storedAll(settings.getDashboardTiles()));
|
: Tile.storedAll(settings.getDashboardTiles()));
|
||||||
target.setProfileTiles(Tile.storedAll(settings.getProfileTiles()));
|
target.setProfileTiles(Tile.storedAll(settings.getProfileTiles()));
|
||||||
target.setFavoritePlatform(settings.getFavoritePlatform());
|
target.setFavoritePlatform(settings.getFavoritePlatform());
|
||||||
target.setPlatformMails(settings.isPlatformMails());
|
|
||||||
target.setWelcomeSeenAt(settings.getWelcomeSeenAt());
|
target.setWelcomeSeenAt(settings.getWelcomeSeenAt());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -51,12 +51,17 @@ public class UserService {
|
|||||||
if (displayName != null && userRepository.isDisplayNameUsedByOtherUser(userId, displayName)) {
|
if (displayName != null && userRepository.isDisplayNameUsedByOtherUser(userId, displayName)) {
|
||||||
throw new DisplayNameAlreadyInUseException("The display name is already in use.");
|
throw new DisplayNameAlreadyInUseException("The display name is already in use.");
|
||||||
}
|
}
|
||||||
|
boolean renamed = displayName != null && userRepository.findById(userId)
|
||||||
|
.map(user -> !displayName.equals(user.getDisplayName())).orElse(true);
|
||||||
userRepository.update(userId, user -> {
|
userRepository.update(userId, user -> {
|
||||||
user.setDisplayName(displayName);
|
user.setDisplayName(displayName);
|
||||||
user.getSettings().setLanguage(userSettings.getLanguage());
|
user.getSettings().setLanguage(userSettings.getLanguage());
|
||||||
user.getSettings().setTheme(userSettings.getTheme());
|
user.getSettings().setTheme(userSettings.getTheme());
|
||||||
user.getSettings().setPublicProfile(userSettings.isPublicProfile());
|
user.getSettings().setPublicProfile(userSettings.isPublicProfile());
|
||||||
});
|
});
|
||||||
|
if (renamed) {
|
||||||
|
accountStatePublisher.publish(userId);
|
||||||
|
}
|
||||||
log.info("User settings updated for user {}", userId);
|
log.info("User settings updated for user {}", userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,13 +134,6 @@ public class UserService {
|
|||||||
user -> user.getSettings().setFavoritePlatform(platform == null ? null : platform.strip()));
|
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)
|
@RequiresRole(Role.USER)
|
||||||
@Transactional
|
@Transactional
|
||||||
@CacheEvict(value = "users", key = "#userId")
|
@CacheEvict(value = "users", key = "#userId")
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -52,12 +52,12 @@ class AccountRepositoryTest extends AbstractPersistenceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void findStates_CarryTheSteamIdAndTheBlock() {
|
void findStates_CarryTheSteamIdTheDisplayNameAndTheBlock() {
|
||||||
jdbc.update("update users set enabled = false, steam_id = 76561198000000001 where id = ?", alice);
|
jdbc.update("update users set enabled = false, steam_id = 76561198000000001, display_name = 'Alice' where id = ?", alice);
|
||||||
|
|
||||||
assertThat(repository.findState(alice)).contains(new CoreAccountState(alice, 76561198000000001L, true));
|
assertThat(repository.findState(alice)).contains(new CoreAccountState(alice, 76561198000000001L, "Alice", true));
|
||||||
assertThat(repository.findAllStates()).containsExactly(new CoreAccountState(alice, 76561198000000001L, true),
|
assertThat(repository.findAllStates()).containsExactly(new CoreAccountState(alice, 76561198000000001L, "Alice", true),
|
||||||
new CoreAccountState(bob, null, false));
|
new CoreAccountState(bob, null, null, false));
|
||||||
assertThat(repository.findState(-1L)).isEmpty();
|
assertThat(repository.findState(-1L)).isEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ import static org.mockito.Mockito.when;
|
|||||||
|
|
||||||
class AccountCleanupServiceTest extends AbstractUnitTest {
|
class AccountCleanupServiceTest extends AbstractUnitTest {
|
||||||
|
|
||||||
private static final List<CoreAccountState> STATES = List.of(new CoreAccountState(1L, 76561198000000001L, true));
|
private static final List<CoreAccountState> STATES = List.of(new CoreAccountState(1L, 76561198000000001L, "lobo", true));
|
||||||
|
|
||||||
private static final Duration RETENTION = Duration.ofDays(30);
|
private static final Duration RETENTION = Duration.ofDays(30);
|
||||||
|
|
||||||
|
|||||||
@@ -71,8 +71,8 @@ class CoreAccountServiceTest extends AbstractUnitTest {
|
|||||||
expect("applyStates");
|
expect("applyStates");
|
||||||
expect("replaceStates");
|
expect("replaceStates");
|
||||||
|
|
||||||
service.applyStates(List.of(new CoreAccountState(42L, 76561198000000001L, true)));
|
service.applyStates(List.of(new CoreAccountState(42L, 76561198000000001L, "lobo", true)));
|
||||||
service.replaceStates(List.of(new CoreAccountState(42L, null, false)));
|
service.replaceStates(List.of(new CoreAccountState(42L, null, "lobo", false)));
|
||||||
|
|
||||||
core.verify();
|
core.verify();
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -22,7 +22,7 @@ import static org.mockito.Mockito.when;
|
|||||||
|
|
||||||
class CoreAccountStatePublisherTest extends AbstractUnitTest {
|
class CoreAccountStatePublisherTest extends AbstractUnitTest {
|
||||||
|
|
||||||
private static final CoreAccountState STATE = new CoreAccountState(7L, 76561198000000001L, true);
|
private static final CoreAccountState STATE = new CoreAccountState(7L, 76561198000000001L, "lobo", true);
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
private AccountRepository accountRepository;
|
private AccountRepository accountRepository;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package auth_service.modules.mail.api;
|
|||||||
|
|
||||||
import auth_service.configuration.security.ClientHeaderFilter;
|
import auth_service.configuration.security.ClientHeaderFilter;
|
||||||
import auth_service.modules.mail.service.MailOutboxService;
|
import auth_service.modules.mail.service.MailOutboxService;
|
||||||
|
import auth_service.shared.account.ActiveUser;
|
||||||
import auth_service.shared.api.AuthCookies;
|
import auth_service.shared.api.AuthCookies;
|
||||||
import auth_service.shared.test.AbstractIntegrationTest;
|
import auth_service.shared.test.AbstractIntegrationTest;
|
||||||
import jakarta.servlet.http.Cookie;
|
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 PASSWORD = "correct horse battery";
|
||||||
private static final String ADMIN = "broadcast-flow-admin@example.com";
|
private static final String ADMIN = "broadcast-flow-admin@example.com";
|
||||||
private static final String PLAYER = "broadcast-flow-player@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 BLOCKED = "broadcast-flow-blocked@example.com";
|
||||||
private static final String BASE = "/api/admin/broadcasts";
|
private static final String BASE = "/api/admin/broadcasts";
|
||||||
|
|
||||||
@@ -50,16 +50,16 @@ class BroadcastFlowTest extends AbstractIntegrationTest {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private MailOutboxService mailOutboxService;
|
private MailOutboxService mailOutboxService;
|
||||||
|
|
||||||
private long optedOutId;
|
private long adminId;
|
||||||
private List<Long> foreignOptedIn;
|
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) "
|
long id = jdbc.queryForObject("INSERT INTO users (email, password, enabled, role, display_name) "
|
||||||
+ "VALUES (?, ?, ?, ?, ?) RETURNING id", Long.class, email, passwordEncoder.encode(PASSWORD),
|
+ "VALUES (?, ?, ?, ?, ?) RETURNING id", Long.class, email, passwordEncoder.encode(PASSWORD),
|
||||||
enabled, role, email.substring(15, 20));
|
enabled, role, email.substring(15, 20));
|
||||||
addSettingsRows();
|
addSettingsRows();
|
||||||
jdbc.update("UPDATE user_settings SET language = ?, platform_mails = ? WHERE user_id = ?", language,
|
jdbc.update("UPDATE user_settings SET language = ? WHERE user_id = ?", language, id);
|
||||||
platformMails, id);
|
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,23 +68,15 @@ class BroadcastFlowTest extends AbstractIntegrationTest {
|
|||||||
jdbc.update("DELETE FROM mail_outbox");
|
jdbc.update("DELETE FROM mail_outbox");
|
||||||
jdbc.update("DELETE FROM mail_broadcast");
|
jdbc.update("DELETE FROM mail_broadcast");
|
||||||
jdbc.update("DELETE FROM users WHERE email LIKE 'broadcast-flow-%'");
|
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.
|
adminId = insertUser(ADMIN, "PLATFORM_ADMIN", "de", true);
|
||||||
foreignOptedIn = jdbc.queryForList("SELECT user_id FROM user_settings WHERE platform_mails", Long.class);
|
playerId = insertUser(PLAYER, "USER", "en", true);
|
||||||
jdbc.update("UPDATE user_settings SET platform_mails = FALSE");
|
blockedId = insertUser(BLOCKED, "USER", "en", true);
|
||||||
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);
|
|
||||||
jdbc.update("UPDATE users SET enabled = FALSE WHERE email = ?", BLOCKED);
|
jdbc.update("UPDATE users SET enabled = FALSE WHERE email = ?", BLOCKED);
|
||||||
}
|
}
|
||||||
|
|
||||||
@AfterEach
|
@AfterEach
|
||||||
void tearDown() {
|
void tearDown() {
|
||||||
jdbc.update("DELETE FROM users WHERE email LIKE 'broadcast-flow-%'");
|
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 {
|
private Cookie login(String email) throws Exception {
|
||||||
@@ -109,11 +101,16 @@ class BroadcastFlowTest extends AbstractIntegrationTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void sendToAll_MailsActiveOptedInUsersInTheirLanguageAndCountsTheSend() throws Exception {
|
void sendToPicked_MailsActiveUsersInTheirLanguageAndCountsTheSend() throws Exception {
|
||||||
Cookie admin = login(ADMIN);
|
Cookie admin = login(ADMIN);
|
||||||
|
|
||||||
send(get(BASE + "/recipient-count"), admin).andExpect(status().isOk()).andExpect(jsonPath("$.count").value(2));
|
String picked = adminId + "," + playerId + "," + blockedId;
|
||||||
send(post(BASE).content(broadcast("Hallo", "null")), admin).andExpect(status().isCreated())
|
|
||||||
|
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));
|
.andExpect(jsonPath("$.recipientCount").value(2));
|
||||||
|
|
||||||
assertThat(rows()).extracting(row -> row.get("to_address") + " " + row.get("subject") + " " + row.get("kind"))
|
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 {
|
void pickedAndTest_StayNarrow() throws Exception {
|
||||||
Cookie admin = login(ADMIN);
|
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"));
|
.andExpect(jsonPath("$.code").value("NO_RECIPIENTS"));
|
||||||
assertThat(jdbc.queryForObject("SELECT COUNT(*) FROM mail_broadcast", Long.class)).isZero();
|
assertThat(jdbc.queryForObject("SELECT COUNT(*) FROM mail_broadcast", Long.class)).isZero();
|
||||||
|
|
||||||
|
|||||||
@@ -27,12 +27,12 @@ class MailTemplateTest extends AbstractUnitTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void render_Broadcast_PutsSubjectBodyAndTheSettingsLink() {
|
void render_Broadcast_PutsSubjectAndBody() {
|
||||||
for (String language : new String[]{"de", "en"}) {
|
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.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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ class BroadcastRepositoryTest extends AbstractPersistenceTest {
|
|||||||
|
|
||||||
private long german;
|
private long german;
|
||||||
private long noLanguage;
|
private long noLanguage;
|
||||||
private long optedOut;
|
|
||||||
private long blocked;
|
private long blocked;
|
||||||
private long pending;
|
private long pending;
|
||||||
private long deleted;
|
private long deleted;
|
||||||
@@ -35,30 +34,28 @@ class BroadcastRepositoryTest extends AbstractPersistenceTest {
|
|||||||
jdbc.update("DELETE FROM mail_outbox");
|
jdbc.update("DELETE FROM mail_outbox");
|
||||||
jdbc.update("DELETE FROM mail_broadcast");
|
jdbc.update("DELETE FROM mail_broadcast");
|
||||||
jdbc.update("DELETE FROM users WHERE email LIKE 'bc-repo-%'");
|
jdbc.update("DELETE FROM users WHERE email LIKE 'bc-repo-%'");
|
||||||
german = user("german", "true, 'ACTIVE', NULL", "de", true);
|
german = user("german", "true, 'ACTIVE', NULL", "de");
|
||||||
noLanguage = user("nolang", "true, 'ACTIVE', NULL", "", true);
|
noLanguage = user("nolang", "true, 'ACTIVE', NULL", "");
|
||||||
optedOut = user("optedout", "true, 'ACTIVE', NULL", "de", false);
|
blocked = user("blocked", "false, 'ACTIVE', NULL", "de");
|
||||||
blocked = user("blocked", "false, 'ACTIVE', NULL", "de", true);
|
pending = user("pending", "true, 'PENDING', NULL", "de");
|
||||||
pending = user("pending", "true, 'PENDING', NULL", "de", true);
|
deleted = user("deleted", "true, 'ACTIVE', NOW()", "de");
|
||||||
deleted = user("deleted", "true, 'ACTIVE', NOW()", "de", true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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', "
|
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");
|
+ flags + ") RETURNING id", Long.class, "bc-repo-" + name + "@example.com");
|
||||||
jdbc.update("INSERT INTO user_settings (user_id, language, platform_mails) VALUES (?, ?, ?)", id, language,
|
jdbc.update("INSERT INTO user_settings (user_id, language) VALUES (?, ?)", id, language);
|
||||||
platformMails);
|
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void countRecipients_TakesOnlyActiveOptedInUsers() {
|
void countRecipients_TakesOnlyActiveUsers() {
|
||||||
// Other tests of the suite may leave users behind, so compare with the same query by SQL.
|
// 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 "
|
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(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();
|
assertThat(repository.countRecipients(List.of())).isZero();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,7 +63,7 @@ class BroadcastRepositoryTest extends AbstractPersistenceTest {
|
|||||||
void queue_WritesOneRowPerRecipientInTheirLanguage() {
|
void queue_WritesOneRowPerRecipientInTheirLanguage() {
|
||||||
long id = repository.insert(new BroadcastText("Hi", "Hallo", "Body", "Inhalt"), 1L, Instant.now());
|
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"));
|
new Mail("Hi", "Body"), new Mail("Hallo", "Inhalt"));
|
||||||
repository.setRecipientCount(id, queued);
|
repository.setRecipientCount(id, queued);
|
||||||
|
|
||||||
|
|||||||
@@ -162,28 +162,27 @@ class MailOutboxRepositoryTest extends AbstractPersistenceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void failBroadcastRowsOfInactiveUsers_FailsBlockedDeletedPendingAndOptedOut() {
|
void failBroadcastRowsOfInactiveUsers_FailsBlockedDeletedAndPending() {
|
||||||
long broadcast = broadcast();
|
long broadcast = broadcast();
|
||||||
long active = user("active", "true, 'ACTIVE', NULL");
|
long active = user("active", "true, 'ACTIVE', NULL");
|
||||||
long blocked = user("blocked", "false, 'ACTIVE', NULL");
|
long blocked = user("blocked", "false, 'ACTIVE', NULL");
|
||||||
long deleted = user("deleted", "true, 'ACTIVE', NOW()");
|
long deleted = user("deleted", "true, 'ACTIVE', NOW()");
|
||||||
long pending = user("pending", "true, 'PENDING', NULL");
|
long pending = user("pending", "true, 'PENDING', NULL");
|
||||||
long optedOut = user("optedout", "true, 'ACTIVE', NULL");
|
for (long id : new long[]{active, blocked, deleted, pending}) {
|
||||||
for (long id : new long[]{active, blocked, deleted, pending, optedOut}) {
|
jdbc.update("INSERT INTO user_settings (user_id) VALUES (?)", id);
|
||||||
jdbc.update("INSERT INTO user_settings (user_id, platform_mails) VALUES (?, ?)", id, id != optedOut);
|
|
||||||
}
|
}
|
||||||
long keep = broadcastRow(broadcast, active);
|
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);
|
broadcastRow(broadcast, id);
|
||||||
}
|
}
|
||||||
long accountOfBlocked = jdbc.queryForObject("INSERT INTO mail_outbox (kind, user_id, to_address, subject, body) "
|
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);
|
+ "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))
|
assertThat(jdbc.queryForList("SELECT id FROM mail_outbox WHERE status = 'PENDING' ORDER BY id", Long.class))
|
||||||
.containsExactly(keep, accountOfBlocked);
|
.containsExactly(keep, accountOfBlocked);
|
||||||
assertThat(counter(broadcast, "failed_count")).isEqualTo(4);
|
assertThat(counter(broadcast, "failed_count")).isEqualTo(3);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ import static org.mockito.Mockito.when;
|
|||||||
class BroadcastServiceTest extends AbstractUnitTest {
|
class BroadcastServiceTest extends AbstractUnitTest {
|
||||||
|
|
||||||
private static final long ADMIN = 1L;
|
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");
|
private static final BroadcastText TEXT = new BroadcastText("Hi", "Hallo", "Body", "Inhalt");
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
@@ -48,7 +47,7 @@ class BroadcastServiceTest extends AbstractUnitTest {
|
|||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void setUp() {
|
void setUp() {
|
||||||
service = new BroadcastService(broadcasts, outbox, adminAuditLog, "https://tbp.example");
|
service = new BroadcastService(broadcasts, outbox, adminAuditLog);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -62,9 +61,9 @@ class BroadcastServiceTest extends AbstractUnitTest {
|
|||||||
ArgumentCaptor<Mail> de = ArgumentCaptor.forClass(Mail.class);
|
ArgumentCaptor<Mail> de = ArgumentCaptor.forClass(Mail.class);
|
||||||
verify(broadcasts).queue(eq(5L), isNull(), en.capture(), de.capture());
|
verify(broadcasts).queue(eq(5L), isNull(), en.capture(), de.capture());
|
||||||
assertThat(en.getValue().subject()).isEqualTo("Hi");
|
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().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(broadcasts).setRecipientCount(5L, 3);
|
||||||
verify(adminAuditLog).record(ADMIN, "BROADCAST_SEND", null, "5");
|
verify(adminAuditLog).record(ADMIN, "BROADCAST_SEND", null, "5");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -491,22 +491,6 @@ class UserControllerTest extends AbstractWebMvcTest {
|
|||||||
verify(userService, never()).updateFavoritePlatform(anyLong(), any());
|
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
|
@Test
|
||||||
void updateProfileTiles_StoresTheTiles() throws Exception {
|
void updateProfileTiles_StoresTheTiles() throws Exception {
|
||||||
SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(
|
SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(
|
||||||
|
|||||||
@@ -251,22 +251,6 @@ class UserRepositoryTest extends AbstractPersistenceTest {
|
|||||||
assertTrue(findByEmail("public@example.com").orElseThrow().getSettings().isPublicProfile());
|
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
|
@Test
|
||||||
void updateSettings_KeepsTheAppearance() {
|
void updateSettings_KeepsTheAppearance() {
|
||||||
var userEntity = persisted(UserEntity.builder()
|
var userEntity = persisted(UserEntity.builder()
|
||||||
|
|||||||
@@ -93,6 +93,7 @@ class UserServiceTest extends AbstractUnitTest {
|
|||||||
verify(userRepository, never()).clearSteamId(anyLong());
|
verify(userRepository, never()).clearSteamId(anyLong());
|
||||||
verify(userRepository, never()).setVerifiedSteamId(anyLong(), anyLong());
|
verify(userRepository, never()).setVerifiedSteamId(anyLong(), anyLong());
|
||||||
verifyNoInteractions(tokenGenerator);
|
verifyNoInteractions(tokenGenerator);
|
||||||
|
verifyNoInteractions(accountStatePublisher);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -120,6 +121,18 @@ class UserServiceTest extends AbstractUnitTest {
|
|||||||
User changed = appliedChanges(1).getFirst();
|
User changed = appliedChanges(1).getFirst();
|
||||||
assertEquals("Lobo", changed.getDisplayName());
|
assertEquals("Lobo", changed.getDisplayName());
|
||||||
assertEquals("en", changed.getSettings().getLanguage());
|
assertEquals("en", changed.getSettings().getLanguage());
|
||||||
|
verify(accountStatePublisher).publish(1L);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void updateUserSettings_UnchangedDisplayName_PublishesNothing() {
|
||||||
|
var userSettings = UserSettings.builder().language("en").build();
|
||||||
|
when(userRepository.findById(1L)).thenReturn(Optional.of(User.builder().id(1L).displayName("Lobo").build()));
|
||||||
|
|
||||||
|
userService.updateUserSettings(1L, "Lobo", userSettings);
|
||||||
|
|
||||||
|
appliedChanges(1);
|
||||||
|
verifyNoInteractions(accountStatePublisher);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final long STEAM_ID = 76561198000000001L;
|
private static final long STEAM_ID = 76561198000000001L;
|
||||||
@@ -186,6 +199,7 @@ class UserServiceTest extends AbstractUnitTest {
|
|||||||
|
|
||||||
assertEquals(appearance, appliedChanges(1).getFirst().getSettings().getAppearance());
|
assertEquals(appearance, appliedChanges(1).getFirst().getSettings().getAppearance());
|
||||||
verifyNoInteractions(tokenGenerator);
|
verifyNoInteractions(tokenGenerator);
|
||||||
|
verifyNoInteractions(accountStatePublisher);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -195,13 +209,6 @@ class UserServiceTest extends AbstractUnitTest {
|
|||||||
assertEquals("Switch", appliedChanges(1).getFirst().getSettings().getFavoritePlatform());
|
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
|
@Test
|
||||||
void markWelcomeSeen_StampsNow() {
|
void markWelcomeSeen_StampsNow() {
|
||||||
userService.markWelcomeSeen(1L);
|
userService.markWelcomeSeen(1L);
|
||||||
|
|||||||
@@ -37,12 +37,12 @@
|
|||||||
"applyStates": {
|
"applyStates": {
|
||||||
"method": "POST",
|
"method": "POST",
|
||||||
"path": "/internal/accounts/states",
|
"path": "/internal/accounts/states",
|
||||||
"request": {"accounts": [{"userId": 42, "steamId": 76561198000000001, "blocked": true}]}
|
"request": {"accounts": [{"userId": 42, "steamId": 76561198000000001, "displayName": "lobo", "blocked": true}]}
|
||||||
},
|
},
|
||||||
"replaceStates": {
|
"replaceStates": {
|
||||||
"method": "PUT",
|
"method": "PUT",
|
||||||
"path": "/internal/accounts/states",
|
"path": "/internal/accounts/states",
|
||||||
"request": {"accounts": [{"userId": 42, "steamId": null, "blocked": false}]}
|
"request": {"accounts": [{"userId": 42, "steamId": null, "displayName": "lobo", "blocked": false}]}
|
||||||
},
|
},
|
||||||
"purge": {
|
"purge": {
|
||||||
"method": "POST",
|
"method": "POST",
|
||||||
|
|||||||
Reference in new issue
Block a user