InMemoryLoginAttemptStore.java

1
package com.rbctcsworld.ecommerce.auth;
2
3
import java.time.Clock;
4
import java.time.Duration;
5
import java.time.Instant;
6
import java.util.Map;
7
import java.util.concurrent.ConcurrentHashMap;
8
9
/**
10
 * Failed logins counted in this JVM. Default for development and tests, and the fallback while Redis is
11
 * unreachable. Limits: every backend instance counts on its own, and a restart forgets all locks.
12
 */
13
public class InMemoryLoginAttemptStore implements LoginAttemptStore {
14
15
    private static final int MAX_TRACKED = 10_000;
16
17
    private final Clock clock;
18
    private final int maxFailures;
19
    private final Duration window;
20
    private final Duration lock;
21
    private final Map<String, Attempts> attempts = new ConcurrentHashMap<>();
22
23
    public InMemoryLoginAttemptStore(Clock clock, int maxFailures, Duration window, Duration lock) {
24
        this.clock = clock;
25
        this.maxFailures = maxFailures;
26
        this.window = window;
27
        this.lock = lock;
28
    }
29
30
    private record Attempts(int failures, Instant firstFailure, Instant lockedUntil) {
31
    }
32
33
    @Override
34
    public long lockRemainingMillis(String email) {
35
        Attempts a = attempts.get(email);
36
        Instant now = clock.instant();
37 3 1. lockRemainingMillis : removed conditional - replaced equality check with false → KILLED
2. lockRemainingMillis : removed conditional - replaced equality check with false → KILLED
3. lockRemainingMillis : removed conditional - replaced equality check with false → KILLED
        if (a == null || a.lockedUntil() == null || !now.isBefore(a.lockedUntil())) {
38
            return 0;
39
        }
40 1 1. lockRemainingMillis : replaced long return with 0 for com/rbctcsworld/ecommerce/auth/InMemoryLoginAttemptStore::lockRemainingMillis → KILLED
        return Duration.between(now, a.lockedUntil()).toMillis();
41
    }
42
43
    @Override
44
    public void recordFailure(String email) {
45
        Instant now = clock.instant();
46 3 1. recordFailure : changed conditional boundary → KILLED
2. recordFailure : removed call to com/rbctcsworld/ecommerce/auth/InMemoryLoginAttemptStore::evictStale → KILLED
3. recordFailure : removed conditional - replaced comparison check with false → KILLED
        if (attempts.size() > MAX_TRACKED) evictStale(now);
47
        attempts.compute(email, (k, a) -> {
48 2 1. lambda$recordFailure$0 : removed conditional - replaced equality check with false → KILLED
2. lambda$recordFailure$0 : removed conditional - replaced equality check with false → KILLED
            if (a == null || now.isAfter(a.firstFailure().plus(window))
49 2 1. lambda$recordFailure$0 : removed conditional - replaced equality check with false → KILLED
2. lambda$recordFailure$0 : removed conditional - replaced equality check with false → KILLED
                    || (a.lockedUntil() != null && !now.isBefore(a.lockedUntil()))) {
50
                a = new Attempts(0, now, null);                 // new window (or lock has expired)
51
            }
52 1 1. lambda$recordFailure$0 : Replaced integer addition with subtraction → KILLED
            int failures = a.failures() + 1;
53 2 1. lambda$recordFailure$0 : changed conditional boundary → KILLED
2. lambda$recordFailure$0 : removed conditional - replaced comparison check with false → KILLED
            Instant lockedUntil = failures >= maxFailures ? now.plus(lock) : null;
54 1 1. lambda$recordFailure$0 : replaced return value with null for com/rbctcsworld/ecommerce/auth/InMemoryLoginAttemptStore::lambda$recordFailure$0 → KILLED
            return new Attempts(failures, a.firstFailure(), lockedUntil);
55
        });
56
    }
57
58
    @Override
59
    public void reset(String email) {
60
        attempts.remove(email);
61
    }
62
63
    @Override
64
    public int failures(String email) {
65
        Attempts a = attempts.get(email);
66 2 1. failures : replaced int return with 0 for com/rbctcsworld/ecommerce/auth/InMemoryLoginAttemptStore::failures → KILLED
2. failures : removed conditional - replaced equality check with false → KILLED
        return a == null ? 0 : a.failures();
67
    }
68
69
    int tracked() {
70 1 1. tracked : replaced int return with 0 for com/rbctcsworld/ecommerce/auth/InMemoryLoginAttemptStore::tracked → KILLED
        return attempts.size();
71
    }
72
73
    private void evictStale(Instant now) {
74
        attempts.entrySet().removeIf(e -> {
75
            Attempts a = e.getValue();
76
            boolean windowOver = now.isAfter(a.firstFailure().plus(window));
77 2 1. lambda$evictStale$1 : removed conditional - replaced equality check with false → KILLED
2. lambda$evictStale$1 : removed conditional - replaced equality check with false → KILLED
            boolean notLocked = a.lockedUntil() == null || !now.isBefore(a.lockedUntil());
78 3 1. lambda$evictStale$1 : removed conditional - replaced equality check with false → KILLED
2. lambda$evictStale$1 : removed conditional - replaced equality check with false → KILLED
3. lambda$evictStale$1 : replaced boolean return with true for com/rbctcsworld/ecommerce/auth/InMemoryLoginAttemptStore::lambda$evictStale$1 → KILLED
            return windowOver && notLocked;
79
        });
80
    }
81
}

Mutations

37

1.1
Location : lockRemainingMillis
Killed by : com.rbctcsworld.ecommerce.auth.InMemoryLoginAttemptStoreTest.[engine:junit-jupiter]/[class:com.rbctcsworld.ecommerce.auth.InMemoryLoginAttemptStoreTest]/[method:longAfterTheLockNothingNegativeIsReported()]
removed conditional - replaced equality check with false → KILLED

2.2
Location : lockRemainingMillis
Killed by : com.rbctcsworld.ecommerce.auth.InMemoryLoginAttemptStoreTest.[engine:junit-jupiter]/[class:com.rbctcsworld.ecommerce.auth.InMemoryLoginAttemptStoreTest]/[method:lockRemainingCountsDownToZero()]
removed conditional - replaced equality check with false → KILLED

3.3
Location : lockRemainingMillis
Killed by : com.rbctcsworld.ecommerce.auth.InMemoryLoginAttemptStoreTest.[engine:junit-jupiter]/[class:com.rbctcsworld.ecommerce.auth.InMemoryLoginAttemptStoreTest]/[method:lockRemainingCountsDownToZero()]
removed conditional - replaced equality check with false → KILLED

40

1.1
Location : lockRemainingMillis
Killed by : com.rbctcsworld.ecommerce.auth.InMemoryLoginAttemptStoreTest.[engine:junit-jupiter]/[class:com.rbctcsworld.ecommerce.auth.InMemoryLoginAttemptStoreTest]/[method:lockRemainingCountsDownToZero()]
replaced long return with 0 for com/rbctcsworld/ecommerce/auth/InMemoryLoginAttemptStore::lockRemainingMillis → KILLED

46

1.1
Location : recordFailure
Killed by : com.rbctcsworld.ecommerce.auth.InMemoryLoginAttemptStoreTest.[engine:junit-jupiter]/[class:com.rbctcsworld.ecommerce.auth.InMemoryLoginAttemptStoreTest]/[method:exactlyTenThousandTrackedIsStillBelowTheCleanupLimit()]
changed conditional boundary → KILLED

2.2
Location : recordFailure
Killed by : com.rbctcsworld.ecommerce.auth.InMemoryLoginAttemptStoreTest.[engine:junit-jupiter]/[class:com.rbctcsworld.ecommerce.auth.InMemoryLoginAttemptStoreTest]/[method:activeLocksSurviveTheEviction()]
removed call to com/rbctcsworld/ecommerce/auth/InMemoryLoginAttemptStore::evictStale → KILLED

3.3
Location : recordFailure
Killed by : com.rbctcsworld.ecommerce.auth.InMemoryLoginAttemptStoreTest.[engine:junit-jupiter]/[class:com.rbctcsworld.ecommerce.auth.InMemoryLoginAttemptStoreTest]/[method:activeLocksSurviveTheEviction()]
removed conditional - replaced comparison check with false → KILLED

48

1.1
Location : lambda$recordFailure$0
Killed by : com.rbctcsworld.ecommerce.auth.InMemoryLoginAttemptStoreTest.[engine:junit-jupiter]/[class:com.rbctcsworld.ecommerce.auth.InMemoryLoginAttemptStoreTest]/[method:lockRemainingCountsDownToZero()]
removed conditional - replaced equality check with false → KILLED

2.2
Location : lambda$recordFailure$0
Killed by : com.rbctcsworld.ecommerce.auth.InMemoryLoginAttemptStoreTest.[engine:junit-jupiter]/[class:com.rbctcsworld.ecommerce.auth.InMemoryLoginAttemptStoreTest]/[method:lockRemainingCountsDownToZero()]
removed conditional - replaced equality check with false → KILLED

49

1.1
Location : lambda$recordFailure$0
Killed by : com.rbctcsworld.ecommerce.auth.LoginAttemptServiceTest.[engine:junit-jupiter]/[class:com.rbctcsworld.ecommerce.auth.LoginAttemptServiceTest]/[method:afterTheLockExpiresCountingStartsAgain()]
removed conditional - replaced equality check with false → KILLED

2.2
Location : lambda$recordFailure$0
Killed by : com.rbctcsworld.ecommerce.auth.LoginAttemptServiceTest.[engine:junit-jupiter]/[class:com.rbctcsworld.ecommerce.auth.LoginAttemptServiceTest]/[method:afterTheLockExpiresCountingStartsAgain()]
removed conditional - replaced equality check with false → KILLED

52

1.1
Location : lambda$recordFailure$0
Killed by : com.rbctcsworld.ecommerce.auth.InMemoryLoginAttemptStoreTest.[engine:junit-jupiter]/[class:com.rbctcsworld.ecommerce.auth.InMemoryLoginAttemptStoreTest]/[method:lockRemainingCountsDownToZero()]
Replaced integer addition with subtraction → KILLED

53

1.1
Location : lambda$recordFailure$0
Killed by : com.rbctcsworld.ecommerce.auth.InMemoryLoginAttemptStoreTest.[engine:junit-jupiter]/[class:com.rbctcsworld.ecommerce.auth.InMemoryLoginAttemptStoreTest]/[method:lockRemainingCountsDownToZero()]
changed conditional boundary → KILLED

2.2
Location : lambda$recordFailure$0
Killed by : com.rbctcsworld.ecommerce.auth.InMemoryLoginAttemptStoreTest.[engine:junit-jupiter]/[class:com.rbctcsworld.ecommerce.auth.InMemoryLoginAttemptStoreTest]/[method:lockRemainingCountsDownToZero()]
removed conditional - replaced comparison check with false → KILLED

54

1.1
Location : lambda$recordFailure$0
Killed by : com.rbctcsworld.ecommerce.auth.InMemoryLoginAttemptStoreTest.[engine:junit-jupiter]/[class:com.rbctcsworld.ecommerce.auth.InMemoryLoginAttemptStoreTest]/[method:lockRemainingCountsDownToZero()]
replaced return value with null for com/rbctcsworld/ecommerce/auth/InMemoryLoginAttemptStore::lambda$recordFailure$0 → KILLED

66

1.1
Location : failures
Killed by : com.rbctcsworld.ecommerce.auth.InMemoryLoginAttemptStoreTest.[engine:junit-jupiter]/[class:com.rbctcsworld.ecommerce.auth.InMemoryLoginAttemptStoreTest]/[method:fourFailuresDoNotLock()]
replaced int return with 0 for com/rbctcsworld/ecommerce/auth/InMemoryLoginAttemptStore::failures → KILLED

2.2
Location : failures
Killed by : com.rbctcsworld.ecommerce.auth.InMemoryLoginAttemptStoreTest.[engine:junit-jupiter]/[class:com.rbctcsworld.ecommerce.auth.InMemoryLoginAttemptStoreTest]/[method:unknownEmailIsNeitherLockedNorCounted()]
removed conditional - replaced equality check with false → KILLED

70

1.1
Location : tracked
Killed by : com.rbctcsworld.ecommerce.auth.InMemoryLoginAttemptStoreTest.[engine:junit-jupiter]/[class:com.rbctcsworld.ecommerce.auth.InMemoryLoginAttemptStoreTest]/[method:exactlyTenThousandTrackedIsStillBelowTheCleanupLimit()]
replaced int return with 0 for com/rbctcsworld/ecommerce/auth/InMemoryLoginAttemptStore::tracked → KILLED

77

1.1
Location : lambda$evictStale$1
Killed by : com.rbctcsworld.ecommerce.auth.InMemoryLoginAttemptStoreTest.[engine:junit-jupiter]/[class:com.rbctcsworld.ecommerce.auth.InMemoryLoginAttemptStoreTest]/[method:memoryStaysBoundedOldEntriesAreEvicted()]
removed conditional - replaced equality check with false → KILLED

2.2
Location : lambda$evictStale$1
Killed by : com.rbctcsworld.ecommerce.auth.InMemoryLoginAttemptStoreTest.[engine:junit-jupiter]/[class:com.rbctcsworld.ecommerce.auth.InMemoryLoginAttemptStoreTest]/[method:activeLocksSurviveTheEviction()]
removed conditional - replaced equality check with false → KILLED

78

1.1
Location : lambda$evictStale$1
Killed by : com.rbctcsworld.ecommerce.auth.InMemoryLoginAttemptStoreTest.[engine:junit-jupiter]/[class:com.rbctcsworld.ecommerce.auth.InMemoryLoginAttemptStoreTest]/[method:activeLocksSurviveTheEviction()]
removed conditional - replaced equality check with false → KILLED

2.2
Location : lambda$evictStale$1
Killed by : com.rbctcsworld.ecommerce.auth.InMemoryLoginAttemptStoreTest.[engine:junit-jupiter]/[class:com.rbctcsworld.ecommerce.auth.InMemoryLoginAttemptStoreTest]/[method:activeLocksSurviveTheEviction()]
removed conditional - replaced equality check with false → KILLED

3.3
Location : lambda$evictStale$1
Killed by : com.rbctcsworld.ecommerce.auth.InMemoryLoginAttemptStoreTest.[engine:junit-jupiter]/[class:com.rbctcsworld.ecommerce.auth.InMemoryLoginAttemptStoreTest]/[method:memoryStaysBoundedOldEntriesAreEvicted()]
replaced boolean return with true for com/rbctcsworld/ecommerce/auth/InMemoryLoginAttemptStore::lambda$evictStale$1 → KILLED

Active mutators

Tests examined


Report generated by PIT 1.30.0 support