@@ -128,6 +128,79 @@ void loginThrowsWhenPasswordIsWrong() {
128128 .hasMessage ("用户名或密码错误" );
129129 }
130130
131+ /**
132+ * INV-003 lazy upgrade:legacy SHA-256 hash 用户登录成功后,应被就地升级为 bcrypt。
133+ */
134+ @ Test
135+ void loginUpgradesLegacyHashAfterSuccessfulMatch () {
136+ String legacyHash = "ad89b64d66caa8e30e5d5ce4a9763f4ecc205814c412175f3e2c50027471426d" ;
137+ String newBcryptHash = "$2b$10$newbcryptsaltsaltsaltsaltsaltsaltsaltsaltsaltsaltsalts" ;
138+ UserAccount account = enabledUser (1L , "alice" , legacyHash );
139+ when (userCenterService .findByUsername ("alice" )).thenReturn (Optional .of (account ));
140+ when (passwordService .matches ("Alice@123" , legacyHash )).thenReturn (true );
141+ when (passwordService .isLegacyHash (legacyHash )).thenReturn (true );
142+ when (passwordService .hash ("Alice@123" )).thenReturn (newBcryptHash );
143+
144+ try (MockedStatic <StpUtil > stpUtil = mockStatic (StpUtil .class )) {
145+ stpUtil .when (StpUtil ::getTokenName ).thenReturn ("satoken" );
146+ stpUtil .when (StpUtil ::getTokenValue ).thenReturn ("token-abc" );
147+
148+ authService .login (new LoginRequest ("alice" , "Alice@123" ));
149+
150+ // 关键:登录成功路径必须触发 lazy upgrade 写回
151+ verify (userAccountRepository ).updatePasswordHash (1L , newBcryptHash );
152+ }
153+ }
154+
155+ /**
156+ * INV-003 lazy upgrade 容错:repository 写失败时不能阻断登录。
157+ */
158+ @ Test
159+ void loginStillSucceedsWhenLazyUpgradeWriteFails () {
160+ String legacyHash = "ad89b64d66caa8e30e5d5ce4a9763f4ecc205814c412175f3e2c50027471426d" ;
161+ UserAccount account = enabledUser (1L , "alice" , legacyHash );
162+ when (userCenterService .findByUsername ("alice" )).thenReturn (Optional .of (account ));
163+ when (passwordService .matches ("Alice@123" , legacyHash )).thenReturn (true );
164+ when (passwordService .isLegacyHash (legacyHash )).thenReturn (true );
165+ when (passwordService .hash ("Alice@123" )).thenReturn ("$2b$10$newhash" );
166+ // 模拟 DB 抖动:updatePasswordHash 抛异常
167+ org .mockito .Mockito .doThrow (new RuntimeException ("simulated DB hiccup" ))
168+ .when (userAccountRepository ).updatePasswordHash (any (), any ());
169+
170+ try (MockedStatic <StpUtil > stpUtil = mockStatic (StpUtil .class )) {
171+ stpUtil .when (StpUtil ::getTokenName ).thenReturn ("satoken" );
172+ stpUtil .when (StpUtil ::getTokenValue ).thenReturn ("token-abc" );
173+
174+ // 不阻断登录:仍返回有效 LoginResponse
175+ LoginResponse response = authService .login (new LoginRequest ("alice" , "Alice@123" ));
176+ assertThat (response .tokenValue ()).isEqualTo ("token-abc" );
177+ }
178+ }
179+
180+ /**
181+ * INV-003 反向:bcrypt 用户登录路径不应该触发 lazy upgrade(已经是 bcrypt 了)。
182+ */
183+ @ Test
184+ void loginDoesNotUpgradeWhenHashIsAlreadyBcrypt () {
185+ String bcryptHash = "$2b$10$alreadybcrypt$alreadybcrypt$alreadybcrypt$alreadybcryptxx" ;
186+ UserAccount account = enabledUser (1L , "alice" , bcryptHash );
187+ when (userCenterService .findByUsername ("alice" )).thenReturn (Optional .of (account ));
188+ when (passwordService .matches ("Alice@123" , bcryptHash )).thenReturn (true );
189+ when (passwordService .isLegacyHash (bcryptHash )).thenReturn (false );
190+
191+ try (MockedStatic <StpUtil > stpUtil = mockStatic (StpUtil .class )) {
192+ stpUtil .when (StpUtil ::getTokenName ).thenReturn ("satoken" );
193+ stpUtil .when (StpUtil ::getTokenValue ).thenReturn ("token-abc" );
194+
195+ authService .login (new LoginRequest ("alice" , "Alice@123" ));
196+
197+ // 不应该调 hash() 也不应该调 updatePasswordHash()
198+ verify (passwordService , org .mockito .Mockito .never ()).hash (any ());
199+ verify (userAccountRepository , org .mockito .Mockito .never ())
200+ .updatePasswordHash (any (), any ());
201+ }
202+ }
203+
131204 // =============================================
132205 // loginByGithub() - GitHub OAuth 登录
133206 // =============================================
0 commit comments