From 314a2d53d22584cb06d566e050a134626abf9d20 Mon Sep 17 00:00:00 2001 From: smail Date: Wed, 30 Sep 2026 04:41:58 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=A1=E6=9F=A5=E6=A3=80=E6=9F=A5=E7=82=B9?= =?UTF-8?q?=EF=BC=9A=E8=A1=A5=E9=BD=90=E5=AE=89=E5=85=A8=E7=89=88=E6=9C=AC?= =?UTF-8?q?=E5=B9=B6=E6=9B=B4=E6=96=B0Telegram=E5=9B=9E=E5=BD=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 补齐控制器测试中的会话与安全版本上下文,更新Telegram用户配置协议及前端回归断言。 --- .../password_reset_permissions_test.go | 14 ++- backend/controllers/user_security_test.go | 18 ++-- .../TelegramSettings/TelegramSettings.js | 2 +- test/frontend/telegram-settings.test.mjs | 94 ++++++++++++------- 4 files changed, 84 insertions(+), 44 deletions(-) diff --git a/backend/controllers/password_reset_permissions_test.go b/backend/controllers/password_reset_permissions_test.go index 2c92bd0..7f28924 100644 --- a/backend/controllers/password_reset_permissions_test.go +++ b/backend/controllers/password_reset_permissions_test.go @@ -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) diff --git a/backend/controllers/user_security_test.go b/backend/controllers/user_security_test.go index 003b1e8..186892c 100644 --- a/backend/controllers/user_security_test.go +++ b/backend/controllers/user_security_test.go @@ -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( diff --git a/frontend/src/views/components/PluginSettings/TelegramSettings/TelegramSettings.js b/frontend/src/views/components/PluginSettings/TelegramSettings/TelegramSettings.js index 176b9c4..308ee99 100644 --- a/frontend/src/views/components/PluginSettings/TelegramSettings/TelegramSettings.js +++ b/frontend/src/views/components/PluginSettings/TelegramSettings/TelegramSettings.js @@ -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 } } diff --git a/test/frontend/telegram-settings.test.mjs b/test/frontend/telegram-settings.test.mjs index 1733eb4..01b903e 100644 --- a/test/frontend/telegram-settings.test.mjs +++ b/test/frontend/telegram-settings.test.mjs @@ -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) -}) +}) \ No newline at end of file