收敛邀请码标签元数据校验
将邀请码 metadata.tags 写入时收敛为 userTags。 拒绝非数组或非字符串 tags 元数据。 补充链接邀请码自动兑换失败时的前端测试。
This commit is contained in:
@@ -920,8 +920,10 @@ fn normalize_invite_code_metadata_user_tags(
|
||||
// 中文注释:邀请码授予标签复用 metadata,保存时统一收敛成 camelCase 字段。
|
||||
let raw = object
|
||||
.remove("userTags")
|
||||
.or_else(|| object.remove("user_tags"));
|
||||
.or_else(|| object.remove("user_tags"))
|
||||
.or_else(|| object.remove("tags"));
|
||||
object.remove("user_tags");
|
||||
object.remove("tags");
|
||||
|
||||
let Some(raw) = raw else {
|
||||
return Ok(());
|
||||
@@ -1034,3 +1036,53 @@ fn normalize_wallet_metadata_json(value: String) -> Result<String, RuntimeProfil
|
||||
fn normalize_profile_task_id(value: String) -> Result<String, RuntimeProfileFieldError> {
|
||||
normalize_required_string(value).ok_or(RuntimeProfileFieldError::MissingTaskId)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn invite_code_metadata_tags_alias_is_saved_as_user_tags() {
|
||||
let normalized = normalize_invite_code_metadata_json(
|
||||
r#"{"tags":["系统"," beta ","系统"],"channel":"spring"}"#.to_string(),
|
||||
)
|
||||
.expect("metadata tags should normalize");
|
||||
|
||||
assert_eq!(
|
||||
serde_json::from_str::<Value>(&normalized).expect("normalized metadata should parse"),
|
||||
serde_json::json!({
|
||||
"channel": "spring",
|
||||
"userTags": ["系统", "beta"]
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invite_code_metadata_rejects_invalid_tags_alias_shape() {
|
||||
assert_eq!(
|
||||
normalize_invite_code_metadata_json(r#"{"tags":"北科"}"#.to_string())
|
||||
.expect_err("string tags should reject"),
|
||||
RuntimeProfileFieldError::InvalidUserTag
|
||||
);
|
||||
assert_eq!(
|
||||
normalize_invite_code_metadata_json(r#"{"tags":[123,"北科"]}"#.to_string())
|
||||
.expect_err("mixed tags should reject"),
|
||||
RuntimeProfileFieldError::InvalidUserTag
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invite_code_metadata_user_tags_takes_precedence_over_tags_alias() {
|
||||
let normalized = normalize_invite_code_metadata_json(
|
||||
r#"{"userTags":["正式"],"user_tags":["旧"],"tags":["别名"]}"#.to_string(),
|
||||
)
|
||||
.expect("metadata userTags should normalize");
|
||||
|
||||
assert_eq!(
|
||||
serde_json::from_str::<Value>(&normalized).expect("normalized metadata should parse"),
|
||||
serde_json::json!({
|
||||
"userTags": ["正式"]
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -689,6 +689,48 @@ test('auth gate auto redeems link invite code for new sms account', async () =>
|
||||
expect(authMocks.redeemRegistrationInviteCode).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('auth gate keeps invite modal open when link invite auto redeem fails', async () => {
|
||||
const user = userEvent.setup();
|
||||
window.history.replaceState(null, '', '/?inviteCode=spring-2026');
|
||||
authMocks.getAuthLoginOptions.mockResolvedValue({
|
||||
availableLoginMethods: ['phone'],
|
||||
});
|
||||
authMocks.loginWithPhoneCode.mockResolvedValueOnce({
|
||||
token: 'jwt-phone-new',
|
||||
user: mockUser,
|
||||
created: true,
|
||||
referral: {
|
||||
ok: false,
|
||||
message: '邀请码无效,已继续注册',
|
||||
inviteeRewardGranted: false,
|
||||
inviterRewardGranted: false,
|
||||
inviteeBalanceAfter: null,
|
||||
inviterBalanceAfter: null,
|
||||
},
|
||||
});
|
||||
|
||||
render(
|
||||
<AuthGate>
|
||||
<ProtectedActionButton onAuthenticated={vi.fn()} />
|
||||
</AuthGate>,
|
||||
);
|
||||
|
||||
await user.click(await screen.findByRole('button', { name: '进入作品' }));
|
||||
const dialog = await screen.findByRole('dialog', { name: '账号入口' });
|
||||
await user.type(within(dialog).getByLabelText('手机号'), '13800000000');
|
||||
await user.type(within(dialog).getByLabelText('验证码'), '123456');
|
||||
await acceptLegalConsent(user, dialog);
|
||||
await user.click(within(dialog).getByRole('button', { name: '登录' }));
|
||||
|
||||
const inviteDialog = await screen.findByRole('dialog', {
|
||||
name: '请填写邀请码',
|
||||
});
|
||||
expect(
|
||||
(within(inviteDialog).getByLabelText('邀请码') as HTMLInputElement).value,
|
||||
).toBe('SPRING2026');
|
||||
expect(within(inviteDialog).getByText('邀请码无效,已继续注册')).toBeTruthy();
|
||||
});
|
||||
|
||||
test('registration invite modal can skip when invite code is empty', async () => {
|
||||
const user = userEvent.setup();
|
||||
authMocks.getAuthLoginOptions.mockResolvedValue({
|
||||
|
||||
Reference in New Issue
Block a user