AuthController.java

1
package com.edtech.controller;
2
3
import com.edtech.dto.AuthResponseDto;
4
import com.edtech.dto.LoginRequestDto;
5
import com.edtech.dto.RecoveryRequestDto;
6
import com.edtech.dto.RegisterRequestDto;
7
import com.edtech.dto.ResetPasswordDto;
8
import com.edtech.dto.UserResponseDto;
9
import com.edtech.dto.VerifyCodeDto;
10
import com.edtech.exception.RateLimitExceededException;
11
import com.edtech.model.User;
12
import com.edtech.security.RateLimitingService;
13
import com.edtech.service.JwtService;
14
import com.edtech.service.RecoveryService;
15
import com.edtech.service.TwoFactorAuthService;
16
import com.edtech.service.UserService;
17
import io.github.bucket4j.Bucket;
18
import jakarta.servlet.http.HttpServletRequest;
19
import jakarta.validation.Valid;
20
import java.util.Map;
21
import org.springframework.http.HttpStatus;
22
import org.springframework.http.ResponseEntity;
23
import org.springframework.security.core.Authentication;
24
import org.springframework.web.bind.annotation.GetMapping;
25
import org.springframework.web.bind.annotation.PostMapping;
26
import org.springframework.web.bind.annotation.RequestBody;
27
import org.springframework.web.bind.annotation.RequestMapping;
28
import org.springframework.web.bind.annotation.RestController;
29
30
/** Documentação para AuthController. */
31
@RestController
32
@RequestMapping("/api/auth")
33
public class AuthController {
34
35
  private final UserService userService;
36
  private final JwtService jwtService;
37
  private final RecoveryService recoveryService;
38
  private final RateLimitingService rateLimitingService;
39
  private final TwoFactorAuthService twoFactorAuthService;
40
41
  /** Documentação. */
42
  public AuthController(
43
      UserService userService,
44
      JwtService jwtService,
45
      RecoveryService recoveryService,
46
      RateLimitingService rateLimitingService,
47
      TwoFactorAuthService twoFactorAuthService) {
48
    this.userService = userService;
49
    this.jwtService = jwtService;
50
    this.recoveryService = recoveryService;
51
    this.rateLimitingService = rateLimitingService;
52
    this.twoFactorAuthService = twoFactorAuthService;
53
  }
54
55
  /** Documentação. */
56
  @PostMapping("/register")
57
  public ResponseEntity<Void> register(@Valid @RequestBody RegisterRequestDto request) {
58
    userService.register(request);
59 1 1. register : replaced return value with null for com/edtech/controller/AuthController::register → KILLED
    return ResponseEntity.status(HttpStatus.CREATED).build();
60
  }
61
62
  /** Documentação. */
63
  @PostMapping("/register/verify")
64
  public ResponseEntity<AuthResponseDto> verifyRegistration(@RequestBody VerifyCodeDto request) {
65
    User user = userService.verifyRegistration(request.email(), request.code());
66
    String token = jwtService.generateToken(user);
67 1 1. verifyRegistration : replaced return value with null for com/edtech/controller/AuthController::verifyRegistration → KILLED
    return ResponseEntity.ok(new AuthResponseDto(UserResponseDto.from(user), token));
68
  }
69
70
  /** Documentação. */
71
  @PostMapping("/login")
72
  public ResponseEntity<?> login(
73
      @Valid @RequestBody LoginRequestDto request, HttpServletRequest httpRequest) {
74
    Bucket bucket = rateLimitingService.resolveBucket(httpRequest.getRemoteAddr());
75 1 1. login : negated conditional → KILLED
    if (!bucket.tryConsume(1)) {
76
      throw new RateLimitExceededException(
77
          "Limite de tentativas excedido. Tente novamente mais tarde.");
78
    }
79
80
    User user = userService.authenticate(request.email(), request.password());
81
82 1 1. login : negated conditional → KILLED
    if (user.isMfaEnabled()) {
83 1 1. login : replaced return value with null for com/edtech/controller/AuthController::login → KILLED
      return ResponseEntity.status(HttpStatus.ACCEPTED)
84
          .body(Map.of("mfaRequired", true, "email", user.getEmail()));
85
    }
86
87
    String token = jwtService.generateToken(user);
88 1 1. login : replaced return value with null for com/edtech/controller/AuthController::login → KILLED
    return ResponseEntity.ok(new AuthResponseDto(UserResponseDto.from(user), token));
89
  }
90
91
  /** Javadoc. */
92
  @PostMapping("/login/verify-2fa")
93
  public ResponseEntity<?> verify2FaLogin(
94
      @Valid @RequestBody com.edtech.dto.Verify2FaLoginDto request,
95
      HttpServletRequest httpRequest) {
96
    Bucket bucket = rateLimitingService.resolveBucket(httpRequest.getRemoteAddr());
97 1 1. verify2FaLogin : negated conditional → KILLED
    if (!bucket.tryConsume(1)) {
98
      throw new RateLimitExceededException(
99
          "Limite de tentativas excedido. Tente novamente mais tarde.");
100
    }
101
102
    User user = userService.authenticate(request.email(), request.password());
103 1 1. verify2FaLogin : negated conditional → KILLED
    if (!user.isMfaEnabled()) {
104 1 1. verify2FaLogin : replaced return value with null for com/edtech/controller/AuthController::verify2FaLogin → KILLED
      return ResponseEntity.badRequest().body("2FA is not enabled for this user.");
105
    }
106
107
    boolean isValid = twoFactorAuthService.verifyCode(user.getMfaSecret(), request.code());
108 1 1. verify2FaLogin : negated conditional → KILLED
    if (!isValid) {
109 1 1. verify2FaLogin : replaced return value with null for com/edtech/controller/AuthController::verify2FaLogin → KILLED
      return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid 2FA code.");
110
    }
111
112
    String token = jwtService.generateToken(user);
113 1 1. verify2FaLogin : replaced return value with null for com/edtech/controller/AuthController::verify2FaLogin → KILLED
    return ResponseEntity.ok(new AuthResponseDto(UserResponseDto.from(user), token));
114
  }
115
116
  /** Javadoc. */
117
  @GetMapping("/2fa/setup")
118
  public ResponseEntity<?> setup2Fa(Authentication authentication) {
119
    User user = (User) authentication.getPrincipal();
120
121 1 1. setup2Fa : negated conditional → KILLED
    if (user.isMfaEnabled()) {
122 1 1. setup2Fa : replaced return value with null for com/edtech/controller/AuthController::setup2Fa → KILLED
      return ResponseEntity.badRequest().body("2FA is already enabled.");
123
    }
124
125
    String secret = user.getMfaSecret();
126 2 1. setup2Fa : negated conditional → NO_COVERAGE
2. setup2Fa : negated conditional → KILLED
    if (secret == null || secret.isEmpty()) {
127
      secret = twoFactorAuthService.generateSecret();
128 1 1. setup2Fa : removed call to com/edtech/model/User::setMfaSecret → KILLED
      user.setMfaSecret(secret);
129
      userService.saveUserWithoutHash(
130
          user); // Wait, we need a method to save the user without rehashing
131
    }
132
133
    try {
134
      String qrCodeUri = twoFactorAuthService.getQrCodeImageUri(secret, user.getEmail());
135 1 1. setup2Fa : replaced return value with null for com/edtech/controller/AuthController::setup2Fa → KILLED
      return ResponseEntity.ok(Map.of("secret", secret, "qrCodeUri", qrCodeUri));
136
    } catch (Exception e) {
137 1 1. setup2Fa : replaced return value with null for com/edtech/controller/AuthController::setup2Fa → KILLED
      return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
138
          .body("Failed to generate QR Code");
139
    }
140
  }
141
142
  /** Javadoc. */
143
  @PostMapping("/2fa/enable")
144
  public ResponseEntity<?> enable2Fa(
145
      @RequestBody VerifyCodeDto request, Authentication authentication) {
146
    User user = (User) authentication.getPrincipal();
147
148 1 1. enable2Fa : negated conditional → KILLED
    if (user.isMfaEnabled()) {
149 1 1. enable2Fa : replaced return value with null for com/edtech/controller/AuthController::enable2Fa → KILLED
      return ResponseEntity.badRequest().body("2FA is already enabled.");
150
    }
151
152
    boolean isValid = twoFactorAuthService.verifyCode(user.getMfaSecret(), request.code());
153 1 1. enable2Fa : negated conditional → KILLED
    if (!isValid) {
154 1 1. enable2Fa : replaced return value with null for com/edtech/controller/AuthController::enable2Fa → KILLED
      return ResponseEntity.badRequest().body("Invalid 2FA code.");
155
    }
156
157 1 1. enable2Fa : removed call to com/edtech/model/User::setMfaEnabled → KILLED
    user.setMfaEnabled(true);
158
    userService.saveUserWithoutHash(user);
159 1 1. enable2Fa : replaced return value with null for com/edtech/controller/AuthController::enable2Fa → SURVIVED
    return ResponseEntity.ok().build();
160
  }
161
162
  /** Documentação. */
163
  @PostMapping("/recovery/request")
164
  public ResponseEntity<?> requestRecovery(
165
      @RequestBody RecoveryRequestDto request, HttpServletRequest httpRequest) {
166
    Bucket bucket = rateLimitingService.resolveBucket(httpRequest.getRemoteAddr());
167 1 1. requestRecovery : negated conditional → KILLED
    if (!bucket.tryConsume(1)) {
168
      throw new RateLimitExceededException(
169
          "Limite de tentativas excedido. Tente novamente mais tarde.");
170
    }
171
172 1 1. requestRecovery : removed call to com/edtech/service/RecoveryService::requestRecovery → KILLED
    recoveryService.requestRecovery(request.email());
173 1 1. requestRecovery : replaced return value with null for com/edtech/controller/AuthController::requestRecovery → SURVIVED
    return ResponseEntity.ok().build();
174
  }
175
176
  /** Documentação. */
177
  @PostMapping("/recovery/verify")
178
  public ResponseEntity<?> verifyCode(@RequestBody VerifyCodeDto request) {
179
    boolean valid = recoveryService.verifyCode(request.email(), request.code());
180 1 1. verifyCode : negated conditional → KILLED
    if (valid) {
181 1 1. verifyCode : replaced return value with null for com/edtech/controller/AuthController::verifyCode → SURVIVED
      return ResponseEntity.ok().build();
182
    }
183 1 1. verifyCode : replaced return value with null for com/edtech/controller/AuthController::verifyCode → KILLED
    return ResponseEntity.badRequest().body("Código inválido ou expirado.");
184
  }
185
186
  /** Documentação. */
187
  @PostMapping("/recovery/reset")
188
  public ResponseEntity<?> resetPassword(@RequestBody ResetPasswordDto request) {
189
    boolean success =
190
        recoveryService.resetPassword(request.email(), request.code(), request.newPassword());
191 1 1. resetPassword : negated conditional → KILLED
    if (success) {
192 1 1. resetPassword : replaced return value with null for com/edtech/controller/AuthController::resetPassword → SURVIVED
      return ResponseEntity.ok().build();
193
    }
194 1 1. resetPassword : replaced return value with null for com/edtech/controller/AuthController::resetPassword → KILLED
    return ResponseEntity.badRequest().body("Erro ao redefinir a senha.");
195
  }
196
197
  /** Documentação. */
198
  @PostMapping("/logout")
199
  public ResponseEntity<Void> logout() {
200 1 1. logout : replaced return value with null for com/edtech/controller/AuthController::logout → SURVIVED
    return ResponseEntity.ok().build();
201
  }
202
203
  /** Documentação. */
204
  @GetMapping("/me")
205
  public ResponseEntity<UserResponseDto> me(Authentication authentication) {
206
    User user = (User) authentication.getPrincipal();
207 1 1. me : replaced return value with null for com/edtech/controller/AuthController::me → KILLED
    return ResponseEntity.ok(UserResponseDto.from(user));
208
  }
209
}

Mutations

59

1.1
Location : register
Killed by : com.edtech.controller.AuthControllerTest.[engine:junit-jupiter]/[class:com.edtech.controller.AuthControllerTest]/[method:registerCreatesResearcherWithoutReturningPasswordHash()]
replaced return value with null for com/edtech/controller/AuthController::register → KILLED

67

1.1
Location : verifyRegistration
Killed by : com.edtech.controller.AuthControllerTest.[engine:junit-jupiter]/[class:com.edtech.controller.AuthControllerTest]/[method:verifyRegistrationReturnsToken()]
replaced return value with null for com/edtech/controller/AuthController::verifyRegistration → KILLED

75

1.1
Location : login
Killed by : com.edtech.controller.AuthControllerTest.[engine:junit-jupiter]/[class:com.edtech.controller.AuthControllerTest]/[method:loginRejectsRequestWhenRateLimitExceeded()]
negated conditional → KILLED

82

1.1
Location : login
Killed by : com.edtech.controller.AuthControllerTest.[engine:junit-jupiter]/[class:com.edtech.controller.AuthControllerTest]/[method:logoutReturnsOk()]
negated conditional → KILLED

83

1.1
Location : login
Killed by : com.edtech.controller.AuthControllerTest.[engine:junit-jupiter]/[class:com.edtech.controller.AuthControllerTest]/[method:loginReturnsAcceptedWhenMfaEnabled()]
replaced return value with null for com/edtech/controller/AuthController::login → KILLED

88

1.1
Location : login
Killed by : com.edtech.controller.AuthControllerTest.[engine:junit-jupiter]/[class:com.edtech.controller.AuthControllerTest]/[method:logoutReturnsOk()]
replaced return value with null for com/edtech/controller/AuthController::login → KILLED

97

1.1
Location : verify2FaLogin
Killed by : com.edtech.controller.AuthControllerTest.[engine:junit-jupiter]/[class:com.edtech.controller.AuthControllerTest]/[method:verify2FaLoginRejectsRequestWhenRateLimitExceeded()]
negated conditional → KILLED

103

1.1
Location : verify2FaLogin
Killed by : com.edtech.controller.AuthControllerTest.[engine:junit-jupiter]/[class:com.edtech.controller.AuthControllerTest]/[method:verify2FaLoginRejectsInvalidCode()]
negated conditional → KILLED

104

1.1
Location : verify2FaLogin
Killed by : com.edtech.controller.AuthControllerTest.[engine:junit-jupiter]/[class:com.edtech.controller.AuthControllerTest]/[method:verify2FaLoginRejectsWhenMfaNotEnabled()]
replaced return value with null for com/edtech/controller/AuthController::verify2FaLogin → KILLED

108

1.1
Location : verify2FaLogin
Killed by : com.edtech.controller.AuthControllerTest.[engine:junit-jupiter]/[class:com.edtech.controller.AuthControllerTest]/[method:verify2FaLoginRejectsInvalidCode()]
negated conditional → KILLED

109

1.1
Location : verify2FaLogin
Killed by : com.edtech.controller.AuthControllerTest.[engine:junit-jupiter]/[class:com.edtech.controller.AuthControllerTest]/[method:verify2FaLoginRejectsInvalidCode()]
replaced return value with null for com/edtech/controller/AuthController::verify2FaLogin → KILLED

113

1.1
Location : verify2FaLogin
Killed by : com.edtech.controller.AuthControllerTest.[engine:junit-jupiter]/[class:com.edtech.controller.AuthControllerTest]/[method:verify2FaLoginReturnsToken()]
replaced return value with null for com/edtech/controller/AuthController::verify2FaLogin → KILLED

121

1.1
Location : setup2Fa
Killed by : com.edtech.controller.AuthControllerTest.[engine:junit-jupiter]/[class:com.edtech.controller.AuthControllerTest]/[method:setup2FaHandlesException()]
negated conditional → KILLED

122

1.1
Location : setup2Fa
Killed by : com.edtech.controller.AuthControllerTest.[engine:junit-jupiter]/[class:com.edtech.controller.AuthControllerTest]/[method:setup2FaRejectsWhenAlreadyEnabled()]
replaced return value with null for com/edtech/controller/AuthController::setup2Fa → KILLED

126

1.1
Location : setup2Fa
Killed by : none
negated conditional → NO_COVERAGE

2.2
Location : setup2Fa
Killed by : com.edtech.controller.AuthControllerTest.[engine:junit-jupiter]/[class:com.edtech.controller.AuthControllerTest]/[method:setup2FaHandlesException()]
negated conditional → KILLED

128

1.1
Location : setup2Fa
Killed by : com.edtech.controller.AuthControllerTest.[engine:junit-jupiter]/[class:com.edtech.controller.AuthControllerTest]/[method:setup2FaReturnsSecret()]
removed call to com/edtech/model/User::setMfaSecret → KILLED

135

1.1
Location : setup2Fa
Killed by : com.edtech.controller.AuthControllerTest.[engine:junit-jupiter]/[class:com.edtech.controller.AuthControllerTest]/[method:setup2FaReturnsSecret()]
replaced return value with null for com/edtech/controller/AuthController::setup2Fa → KILLED

137

1.1
Location : setup2Fa
Killed by : com.edtech.controller.AuthControllerTest.[engine:junit-jupiter]/[class:com.edtech.controller.AuthControllerTest]/[method:setup2FaHandlesException()]
replaced return value with null for com/edtech/controller/AuthController::setup2Fa → KILLED

148

1.1
Location : enable2Fa
Killed by : com.edtech.controller.AuthControllerTest.[engine:junit-jupiter]/[class:com.edtech.controller.AuthControllerTest]/[method:enable2FaRejectsWhenAlreadyEnabled()]
negated conditional → KILLED

149

1.1
Location : enable2Fa
Killed by : com.edtech.controller.AuthControllerTest.[engine:junit-jupiter]/[class:com.edtech.controller.AuthControllerTest]/[method:enable2FaRejectsWhenAlreadyEnabled()]
replaced return value with null for com/edtech/controller/AuthController::enable2Fa → KILLED

153

1.1
Location : enable2Fa
Killed by : com.edtech.controller.AuthControllerTest.[engine:junit-jupiter]/[class:com.edtech.controller.AuthControllerTest]/[method:enable2FaWorks()]
negated conditional → KILLED

154

1.1
Location : enable2Fa
Killed by : com.edtech.controller.AuthControllerTest.[engine:junit-jupiter]/[class:com.edtech.controller.AuthControllerTest]/[method:enable2FaRejectsInvalidCode()]
replaced return value with null for com/edtech/controller/AuthController::enable2Fa → KILLED

157

1.1
Location : enable2Fa
Killed by : com.edtech.controller.AuthControllerTest.[engine:junit-jupiter]/[class:com.edtech.controller.AuthControllerTest]/[method:enable2FaWorks()]
removed call to com/edtech/model/User::setMfaEnabled → KILLED

159

1.1
Location : enable2Fa
Killed by : none
replaced return value with null for com/edtech/controller/AuthController::enable2Fa → SURVIVED

167

1.1
Location : requestRecovery
Killed by : com.edtech.controller.AuthControllerTest.[engine:junit-jupiter]/[class:com.edtech.controller.AuthControllerTest]/[method:requestRecoveryReturnsOk()]
negated conditional → KILLED

172

1.1
Location : requestRecovery
Killed by : com.edtech.controller.AuthControllerTest.[engine:junit-jupiter]/[class:com.edtech.controller.AuthControllerTest]/[method:requestRecoveryReturnsOk()]
removed call to com/edtech/service/RecoveryService::requestRecovery → KILLED

173

1.1
Location : requestRecovery
Killed by : none
replaced return value with null for com/edtech/controller/AuthController::requestRecovery → SURVIVED

180

1.1
Location : verifyCode
Killed by : com.edtech.controller.AuthControllerTest.[engine:junit-jupiter]/[class:com.edtech.controller.AuthControllerTest]/[method:verifyCodeValidReturnsOk()]
negated conditional → KILLED

181

1.1
Location : verifyCode
Killed by : none
replaced return value with null for com/edtech/controller/AuthController::verifyCode → SURVIVED

183

1.1
Location : verifyCode
Killed by : com.edtech.controller.AuthControllerTest.[engine:junit-jupiter]/[class:com.edtech.controller.AuthControllerTest]/[method:verifyCodeInvalidReturnsBadRequest()]
replaced return value with null for com/edtech/controller/AuthController::verifyCode → KILLED

191

1.1
Location : resetPassword
Killed by : com.edtech.controller.AuthControllerTest.[engine:junit-jupiter]/[class:com.edtech.controller.AuthControllerTest]/[method:resetPasswordInvalidReturnsBadRequest()]
negated conditional → KILLED

192

1.1
Location : resetPassword
Killed by : none
replaced return value with null for com/edtech/controller/AuthController::resetPassword → SURVIVED

194

1.1
Location : resetPassword
Killed by : com.edtech.controller.AuthControllerTest.[engine:junit-jupiter]/[class:com.edtech.controller.AuthControllerTest]/[method:resetPasswordInvalidReturnsBadRequest()]
replaced return value with null for com/edtech/controller/AuthController::resetPassword → KILLED

200

1.1
Location : logout
Killed by : none
replaced return value with null for com/edtech/controller/AuthController::logout → SURVIVED

207

1.1
Location : me
Killed by : com.edtech.controller.AuthControllerTest.[engine:junit-jupiter]/[class:com.edtech.controller.AuthControllerTest]/[method:authenticatedUserEndpointReturnsCurrentUser()]
replaced return value with null for com/edtech/controller/AuthController::me → KILLED

Active mutators

Tests examined


Report generated by PIT 1.16.1