审查检查点:补齐安全版本并更新Telegram回归
Some checks are pending
Security Checks / security (push) Waiting to run

补齐控制器测试中的会话与安全版本上下文,更新Telegram用户配置协议及前端回归断言。
This commit is contained in:
smail 2026-09-30 04:41:58 +08:00
parent 11000e8953
commit 314a2d53d2
4 changed files with 84 additions and 44 deletions

View File

@ -54,15 +54,21 @@ func TestEditUserPasswordResetRoleMatrix(t *testing.T) {
t.Fatal(err)
}
superContext := &appcontext.Context{UserID: superAdmin.ID, UserAccount: superAdmin.Account, Role: access.RoleSuperAdmin, RecentAuth: true, RecoveryCodeConfigured: true, Lang: "zhCn"}
adminContext := &appcontext.Context{UserID: admin.ID, UserAccount: admin.Account, Role: access.RoleAdmin, RecentAuth: true, RecoveryCodeConfigured: true, Lang: "zhCn"}
userContext := &appcontext.Context{UserID: user.ID, UserAccount: user.Account, Role: access.RoleUser, RecentAuth: true, RecoveryCodeConfigured: true, Lang: "zhCn"}
superContext := &appcontext.Context{UserID: superAdmin.ID, SessionVersion: superAdmin.SessionVersion, SecurityVersion: superAdmin.SecurityVersion, UserAccount: superAdmin.Account, Role: access.RoleSuperAdmin, RecentAuth: true, RecoveryCodeConfigured: true, Lang: "zhCn"}
adminContext := &appcontext.Context{UserID: admin.ID, SessionVersion: admin.SessionVersion, SecurityVersion: admin.SecurityVersion, UserAccount: admin.Account, Role: access.RoleAdmin, RecentAuth: true, RecoveryCodeConfigured: true, Lang: "zhCn"}
userContext := &appcontext.Context{UserID: user.ID, SessionVersion: user.SessionVersion, SecurityVersion: user.SecurityVersion, UserAccount: user.Account, Role: access.RoleUser, RecentAuth: true, RecoveryCodeConfigured: true, Lang: "zhCn"}
if result := editUserRequest(t, superContext, `{"id":`+itoa(admin.ID)+`,"password":"ResetAdm12","disabled":0}`); result != 0 {
t.Fatalf("super administrator could not reset administrator: %d", result)
}
assertUserPassword(t, admin.ID, "ResetAdm12")
if _, err := db.Instance.ID(admin.ID).Get(&admin); err != nil {
t.Fatal(err)
}
adminContext.SessionVersion = admin.SessionVersion
adminContext.SecurityVersion = admin.SecurityVersion
if result := editUserRequest(t, adminContext, `{"id":`+itoa(user.ID)+`,"password":"ResetUser123","disabled":0}`); result != 0 {
t.Fatalf("administrator could not reset regular user: %d", result)
}
@ -159,7 +165,7 @@ func TestPasswordChangeRevokesOfflineCredentialsAtomically(t *testing.T) {
func newControllerTestUser(t *testing.T, account, name string, role access.Role, plainPassword string) models.User {
t.Helper()
user := models.User{Account: account, Name: name, Role: string(role), SessionVersion: 1}
user := models.User{Account: account, Name: name, Role: string(role), SessionVersion: 1, SecurityVersion: 1}
user.ApplyRole(role)
if err := auth.SetPassword(&user, plainPassword); err != nil {
t.Fatal(err)

View File

@ -37,7 +37,7 @@ func TestEditUserProtectsAdministrativeCredentialsAndRevokesUserSessions(t *test
if err := db.Init(""); err != nil {
t.Fatal(err)
}
admin := models.User{Account: "admin-user", Name: "Admin", IsAdmin: 1, SessionVersion: 1}
admin := models.User{Account: "admin-user", Name: "Admin", IsAdmin: 1, SessionVersion: 1, SecurityVersion: 1}
if err := auth.SetPassword(&admin, "AdminPass123"); err != nil {
t.Fatal(err)
}
@ -49,7 +49,8 @@ func TestEditUserProtectsAdministrativeCredentialsAndRevokesUserSessions(t *test
t.Fatal(err)
}
ctx := &appcontext.Context{
UserID: admin.ID, UserAccount: admin.Account, IsAdmin: true, Lang: "zhCn",
UserID: admin.ID, SessionVersion: admin.SessionVersion, SecurityVersion: admin.SecurityVersion,
UserAccount: admin.Account, IsAdmin: true, Lang: "zhCn",
RecentAuth: true, RecoveryCodeConfigured: true,
}
@ -268,7 +269,8 @@ func TestManagedRecoveryCodesAreScopedAndShownOnce(t *testing.T) {
}
ConfigureSecurityRuntime(nil, nil, nil, credentialService)
ctx := &appcontext.Context{
UserID: actor.ID, UserAccount: actor.Account, Role: access.RoleSuperAdmin, RecentAuth: true,
UserID: actor.ID, SessionVersion: actor.SessionVersion, SecurityVersion: actor.SecurityVersion,
UserAccount: actor.Account, Role: access.RoleSuperAdmin, RecentAuth: true,
}
generateRecorder := httptest.NewRecorder()
@ -405,10 +407,12 @@ func TestManagedExternalEmailChangeInvalidatesPreviousChallenge(t *testing.T) {
}
ctx := &appcontext.Context{
UserID: actor.ID,
UserAccount: actor.Account,
Role: access.RoleSuperAdmin,
RecentAuth: true,
UserID: actor.ID,
SessionVersion: actor.SessionVersion,
SecurityVersion: actor.SecurityVersion,
UserAccount: actor.Account,
Role: access.RoleSuperAdmin,
RecentAuth: true,
}
recorder := httptest.NewRecorder()
request := httptest.NewRequest(

View File

@ -275,7 +275,7 @@ export function useTelegramSettings() {
})
return {
settings, status, binding, ready, working, errorMessage, bindingError,
dialogVisible, polling, hasChanges, bindingLabel, refresh, submit,
dialogVisible, polling, hasChanges, bindingLabel, load, refresh, submit,
beginBinding, confirmBinding, closeBinding, poll, beforeLeave
}
}

View File

@ -8,48 +8,76 @@ import { computed, reactive, ref } from './vue.mjs'
const root = new URL('../../frontend/src/views/components/PluginSettings/', import.meta.url)
const source = readFileSync(new URL('TelegramSettings/TelegramSettings.js', root), 'utf8')
const create = compileFunction(source.replace(/^import .*$/gm, '').replace('export function ', 'function ') + '\nreturn useTelegramSettings()',
['computed', 'onMounted', 'onBeforeUnmount', 'reactive', 'ref', 'ElMessage', 'http', 'ElMessageBox'])
['computed', 'onMounted', 'onBeforeUnmount', 'reactive', 'ref', 'ElMessage', 'ElMessageBox', 'QRCode', 'http', 'isRecentAuthenticationCancelled', 'requestWithRecentAuthentication'])
const createPage = (http, confirm = async () => {}) => create(
computed,
() => {},
() => {},
reactive,
ref,
{ success() {} },
{ confirm },
{ toDataURL: async () => '' },
http,
() => false,
request => request()
)
const userSettings = (overrides = {}) => ({
version: 7,
bound: true,
displayName: 'smail_test_user',
needsRebind: false,
available: true,
message: '',
enabled: false,
showContent: false,
spoilerContent: false,
sendAttachments: false,
disableLinkPreview: true,
...overrides
})
test('Telegram 原生设置保留版本、限制用户字段,不依赖插件 iframe', async () => {
const requests = []
let conflict = false
const page = create(computed, () => {}, () => {}, reactive, ref, { success() {} }, {
get: async url => ({ errorNo: 0, data: url.endsWith('/bot') ? { username: 'smail_test_bot', botLink: 'javascript:alert(1)' }
: { version: 7, settings: { chatId: '123', sendAttachments: false, secret: '不得传回' } } }),
const page = createPage({
get: async () => ({ errorNo: 0, data: userSettings() }),
put: async (url, body) => {
requests.push({ url, body })
return conflict ? { errorNo: 400, errorMsg: '配置已被其他操作更新,请刷新后重试' }
: { errorNo: 0, data: { version: 8, settings: body.settings } }
return conflict
? { errorNo: 400, errorMsg: '配置已被其他操作更新,请刷新后重试' }
: { errorNo: 0, data: userSettings({ ...body, version: 8, synced: true, secret: '不应返回' }) }
},
post: async (url, body) => { requests.push({ url, body }); return { errorNo: 0 } }
post: async (url, body) => {
requests.push({ url, body })
return { errorNo: 0, data: {} }
}
})
await page.load()
assert.equal(page.ready.value, true)
assert.equal(page.botLink.value, 'https://t.me/smail_test_bot')
assert.equal(page.status.bound, true)
assert.equal(page.status.displayName, 'smail_test_user')
page.settings.sendAttachments = true
await page.submit('save')
assert.equal(requests[0].body.version, 7)
assert.deepEqual(Object.keys(requests[0].body.settings).sort(), ['chatId', 'disableLinkPreview', 'sendAttachments', 'showContent', 'spoilerContent'])
assert.equal(page.version.value, 8)
assert.deepEqual(Object.keys(requests[0].body).sort(), [
'disableLinkPreview', 'enabled', 'sendAttachments', 'showContent', 'spoilerContent', 'version'
])
assert.equal(page.settings.secret, undefined)
await page.submit('test')
assert.equal(requests[1].url, '/api/v1/plugin/settings/telegram_push/test')
assert.equal(page.version.value, 8)
assert.equal(requests[1].body, undefined)
conflict = true
await page.submit('save')
assert.match(page.errorMessage.value, /刷新/)
assert.equal(page.version.value, 8)
const count = requests.length
page.settings.chatId = 'invalid'
await page.submit('test')
assert.equal(requests.length, count)
assert.notEqual(page.errorMessage.value, '')
})
test('Telegram 首次读取失败禁止覆盖存量设置,机器人故障不影响停用', async () => {
let fail = true
const page = create(computed, () => {}, () => {}, reactive, ref, { success() {} }, {
get: async url => {
if (fail || url.endsWith('/bot')) throw new Error('模拟离线')
return { errorNo: 0, data: { version: 0, settings: {} } }
const page = createPage({
get: async () => {
if (fail) throw new Error('模拟离线')
return { errorNo: 0, data: userSettings({ version: 0, bound: false, available: false, displayName: '' }) }
},
put: async () => assert.fail('读取失败后不能保存')
})
@ -59,14 +87,14 @@ test('Telegram 首次读取失败禁止覆盖存量设置,机器人故障不
fail = false
await page.load()
assert.equal(page.ready.value, true)
assert.equal(page.botLink.value, '')
assert.equal(page.status.bound, false)
})
test('Telegram 模板独立编译,其他插件继续保持 sandbox 隔离', () => {
const require = createRequire(new URL('../../frontend/package.json', import.meta.url))
const { parse, compileScript, compileStyle } = require('@vue/compiler-sfc')
const parent = readFileSync(new URL('PluginSettings.vue', root), 'utf8')
assert.match(parent, /TelegramSettings v-else-if="activePage\?\.kind === 'account'"/)
assert.match(parent, /TelegramSettings\s+v-else-if="activePage\?\.value === 'telegram-user'"/)
assert.match(parent, /sandbox="allow-scripts"/)
assert.doesNotMatch(parent, /allow-same-origin/)
const { descriptor, errors } = parse(readFileSync(new URL('TelegramSettings/TelegramSettings.vue', root), 'utf8'))
@ -80,22 +108,24 @@ test('Telegram 通知编辑在切页和重新加载前确认,取消保留输
let cancel = true
let reads = 0
let prompts = 0
const page = create(computed, () => {}, () => {}, reactive, ref, { success() {} }, {
get: async url => {
const page = createPage({
get: async () => {
reads++
return { errorNo: 0, data: url.endsWith('/bot') ? {} : { version: 1, settings: { chatId: '123' } } }
return { errorNo: 0, data: userSettings({ version: 1, showContent: false }) }
},
put: async (url, body) => ({ errorNo: 0, data: { version: 2, settings: body.settings } })
put: async (url, body) => ({ errorNo: 0, data: userSettings({ ...body, version: 2, synced: true }) })
}, { confirm: async () => { prompts++; if (cancel) throw new Error('取消') } })
await page.load()
assert.equal(await page.beforeLeave(), true)
page.settings.chatId = '456'
page.settings.showContent = true
assert.equal(await page.beforeLeave(), false)
await page.refresh()
assert.equal(reads, 2)
assert.equal(page.settings.chatId, '456')
assert.equal(reads, 1)
assert.equal(page.settings.showContent, true)
cancel = false
assert.equal(await page.beforeLeave(), true)
await page.refresh()
assert.equal(reads, 2)
assert.equal(page.settings.showContent, false)
await page.submit('save')
assert.equal(page.hasChanges.value, false)
const count = prompts
@ -103,4 +133,4 @@ test('Telegram 通知编辑在切页和重新加载前确认,取消保留输
assert.equal(prompts, count)
page.working.value = 'save'
assert.equal(await page.beforeLeave(), false)
})
})