From bcfe4d4ef6fb2960f90ab5d3d5f8345b036b85f5 Mon Sep 17 00:00:00 2001 From: nfebe Date: Tue, 10 Feb 2026 16:59:42 +0100 Subject: [PATCH 1/2] fix(sharing): Prevent empty password when checkbox is enabled Set passwordProtectedState explicitly when initializing shares with default passwords. This ensures the checkbox state is tracked independently of the password value, preventing it from unchecking when the password field is cleared. Also block saving new shares when password protection is enabled but no password is entered, regardless of enforcement settings. Added passWithNoTests to vitest configs to handle Vue 2/3 dual frontend test runs gracefully. Fixes: #57732, #57011 Signed-off-by: nfebe --- .../src/views/SharingDetailsTab.spec.ts | 331 ++++++++++++++++++ .../src/views/SharingDetailsTab.vue | 4 +- build/frontend-legacy/vitest.config.mts | 1 + build/frontend/vitest.config.ts | 1 + 4 files changed, 336 insertions(+), 1 deletion(-) create mode 100644 apps/files_sharing/src/views/SharingDetailsTab.spec.ts diff --git a/apps/files_sharing/src/views/SharingDetailsTab.spec.ts b/apps/files_sharing/src/views/SharingDetailsTab.spec.ts new file mode 100644 index 0000000000000..07aacb2e36510 --- /dev/null +++ b/apps/files_sharing/src/views/SharingDetailsTab.spec.ts @@ -0,0 +1,331 @@ +/** + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { beforeEach, describe, expect, it, vi } from 'vitest' + +vi.mock('../services/ConfigService.ts', () => ({ + default: vi.fn().mockImplementation(() => ({ + enableLinkPasswordByDefault: false, + enforcePasswordForPublicLink: false, + isPublicUploadEnabled: true, + isDefaultExpireDateEnabled: false, + isDefaultInternalExpireDateEnabled: false, + isDefaultRemoteExpireDateEnabled: false, + defaultExpirationDate: null, + defaultInternalExpirationDate: null, + defaultRemoteExpirationDateString: null, + isResharingAllowed: true, + excludeReshareFromEdit: false, + showFederatedSharesAsInternal: false, + defaultPermissions: 31, + })), +})) + +vi.mock('../utils/GeneratePassword.ts', () => ({ + default: vi.fn().mockResolvedValue('generated-password-123'), +})) + +describe('SharingDetailsTab - Password State Management Logic', () => { + describe('isPasswordProtected getter logic', () => { + it('returns true when passwordProtectedState is explicitly true', () => { + const passwordProtectedState: boolean | undefined = true + const enforcePasswordForPublicLink = false + const newPassword: string | undefined = undefined + const password: string | undefined = undefined + + const isPasswordProtected = (() => { + if (enforcePasswordForPublicLink) { + return true + } + if (passwordProtectedState !== undefined) { + return passwordProtectedState + } + return typeof newPassword === 'string' + || typeof password === 'string' + })() + + expect(isPasswordProtected).toBe(true) + }) + + it('returns false when passwordProtectedState is explicitly false', () => { + const passwordProtectedState: boolean | undefined = false + const enforcePasswordForPublicLink = false + const newPassword: string | undefined = 'some-password' + const password: string | undefined = undefined + + const isPasswordProtected = (() => { + if (enforcePasswordForPublicLink) { + return true + } + if (passwordProtectedState !== undefined) { + return passwordProtectedState + } + return typeof newPassword === 'string' + || typeof password === 'string' + })() + + expect(isPasswordProtected).toBe(false) + }) + + it('returns true when enforcePasswordForPublicLink is true regardless of other state', () => { + const passwordProtectedState: boolean | undefined = false + const enforcePasswordForPublicLink = true + const newPassword: string | undefined = undefined + const password: string | undefined = undefined + + const isPasswordProtected = (() => { + if (enforcePasswordForPublicLink) { + return true + } + if (passwordProtectedState !== undefined) { + return passwordProtectedState + } + return typeof newPassword === 'string' + || typeof password === 'string' + })() + + expect(isPasswordProtected).toBe(true) + }) + + it('falls back to inferring from password when passwordProtectedState is undefined', () => { + const passwordProtectedState: boolean | undefined = undefined + const enforcePasswordForPublicLink = false + const newPassword: string | undefined = 'some-password' + const password: string | undefined = undefined + + const isPasswordProtected = (() => { + if (enforcePasswordForPublicLink) { + return true + } + if (passwordProtectedState !== undefined) { + return passwordProtectedState + } + return typeof newPassword === 'string' + || typeof password === 'string' + })() + + expect(isPasswordProtected).toBe(true) + }) + + it('returns false when passwordProtectedState is undefined and no passwords exist', () => { + const passwordProtectedState: boolean | undefined = undefined + const enforcePasswordForPublicLink = false + const newPassword: string | undefined = undefined + const password: string | undefined = undefined + + const isPasswordProtected = (() => { + if (enforcePasswordForPublicLink) { + return true + } + if (passwordProtectedState !== undefined) { + return passwordProtectedState + } + return typeof newPassword === 'string' + || typeof password === 'string' + })() + + expect(isPasswordProtected).toBe(false) + }) + }) + + describe('initializeAttributes sets passwordProtectedState', () => { + it('should set passwordProtectedState to true when enableLinkPasswordByDefault is true', async () => { + const config = { + enableLinkPasswordByDefault: true, + enforcePasswordForPublicLink: false, + } + const isNewShare = true + const isPublicShare = true + let passwordProtectedState: boolean | undefined + + if (isNewShare) { + if ((config.enableLinkPasswordByDefault || config.enforcePasswordForPublicLink) && isPublicShare) { + passwordProtectedState = true + } + } + + expect(passwordProtectedState).toBe(true) + }) + + it('should set passwordProtectedState to true when isPasswordEnforced is true', async () => { + const config = { + enableLinkPasswordByDefault: false, + enforcePasswordForPublicLink: true, + } + const isNewShare = true + const isPublicShare = true + let passwordProtectedState: boolean | undefined + + if (isNewShare) { + if ((config.enableLinkPasswordByDefault || config.enforcePasswordForPublicLink) && isPublicShare) { + passwordProtectedState = true + } + } + + expect(passwordProtectedState).toBe(true) + }) + + it('should not set passwordProtectedState for non-public shares', async () => { + const config = { + enableLinkPasswordByDefault: true, + enforcePasswordForPublicLink: false, + } + const isNewShare = true + const isPublicShare = false + let passwordProtectedState: boolean | undefined + + if (isNewShare) { + if ((config.enableLinkPasswordByDefault || config.enforcePasswordForPublicLink) && isPublicShare) { + passwordProtectedState = true + } + } + + expect(passwordProtectedState).toBe(undefined) + }) + + it('should not set passwordProtectedState for existing shares', async () => { + const config = { + enableLinkPasswordByDefault: true, + enforcePasswordForPublicLink: false, + } + const isNewShare = false + const isPublicShare = true + let passwordProtectedState: boolean | undefined + + if (isNewShare) { + if ((config.enableLinkPasswordByDefault || config.enforcePasswordForPublicLink) && isPublicShare) { + passwordProtectedState = true + } + } + + expect(passwordProtectedState).toBe(undefined) + }) + }) + + describe('saveShare validation blocks empty password', () => { + const isValidShareAttribute = (attr: unknown) => { + return typeof attr === 'string' && attr.length > 0 + } + + it('should set passwordError when isPasswordProtected but newPassword is empty for new share', () => { + const isPasswordProtected = true + const isNewShare = true + const newPassword = '' + let passwordError = false + + if (isPasswordProtected) { + if (isNewShare && !isValidShareAttribute(newPassword)) { + passwordError = true + } + } + + expect(passwordError).toBe(true) + }) + + it('should set passwordError when isPasswordProtected but newPassword is undefined for new share', () => { + const isPasswordProtected = true + const isNewShare = true + const newPassword = undefined + let passwordError = false + + if (isPasswordProtected) { + if (isNewShare && !isValidShareAttribute(newPassword)) { + passwordError = true + } + } + + expect(passwordError).toBe(true) + }) + + it('should not set passwordError when password is valid for new share', () => { + const isPasswordProtected = true + const isNewShare = true + const newPassword = 'valid-password-123' + let passwordError = false + + if (isPasswordProtected) { + if (isNewShare && !isValidShareAttribute(newPassword)) { + passwordError = true + } + } + + expect(passwordError).toBe(false) + }) + + it('should not set passwordError when isPasswordProtected is false', () => { + const isPasswordProtected = false + const isNewShare = true + const newPassword = '' + let passwordError = false + + if (isPasswordProtected) { + if (isNewShare && !isValidShareAttribute(newPassword)) { + passwordError = true + } + } + + expect(passwordError).toBe(false) + }) + + it('should not validate password for existing shares', () => { + const isPasswordProtected = true + const isNewShare = false + const newPassword = '' + let passwordError = false + + if (isPasswordProtected) { + if (isNewShare && !isValidShareAttribute(newPassword)) { + passwordError = true + } + } + + expect(passwordError).toBe(false) + }) + }) + + describe('checkbox persistence after clearing password', () => { + it('checkbox remains checked when passwordProtectedState is explicitly true even if password is cleared', () => { + let passwordProtectedState: boolean | undefined = true + const enforcePasswordForPublicLink = false + let newPassword: string | undefined = 'initial-password' + + newPassword = '' + + const isPasswordProtected = (() => { + if (enforcePasswordForPublicLink) { + return true + } + if (passwordProtectedState !== undefined) { + return passwordProtectedState + } + return typeof newPassword === 'string' + || false + })() + + expect(isPasswordProtected).toBe(true) + }) + + it('checkbox unchecks incorrectly if passwordProtectedState was never set (bug scenario)', () => { + let passwordProtectedState: boolean | undefined = undefined + const enforcePasswordForPublicLink = false + let newPassword: string | undefined = 'initial-password' + + newPassword = undefined + + const isPasswordProtected = (() => { + if (enforcePasswordForPublicLink) { + return true + } + if (passwordProtectedState !== undefined) { + return passwordProtectedState + } + return typeof newPassword === 'string' + || false + })() + + expect(isPasswordProtected).toBe(false) + }) + }) +}) diff --git a/apps/files_sharing/src/views/SharingDetailsTab.vue b/apps/files_sharing/src/views/SharingDetailsTab.vue index 9830fe88a6aca..e9668d78a01f2 100644 --- a/apps/files_sharing/src/views/SharingDetailsTab.vue +++ b/apps/files_sharing/src/views/SharingDetailsTab.vue @@ -974,6 +974,7 @@ export default { async initializeAttributes() { if (this.isNewShare) { if ((this.config.enableLinkPasswordByDefault || this.isPasswordEnforced) && this.isPublicShare) { + this.passwordProtectedState = true this.$set(this.share, 'newPassword', await GeneratePassword(true)) this.advancedSectionAccordionExpanded = true } @@ -1087,8 +1088,9 @@ export default { this.share.note = '' } if (this.isPasswordProtected) { - if (this.isPasswordEnforced && this.isNewShare && !this.isValidShareAttribute(this.share.newPassword)) { + if (this.isNewShare && !this.isValidShareAttribute(this.share.newPassword)) { this.passwordError = true + return } } else { this.share.password = '' diff --git a/build/frontend-legacy/vitest.config.mts b/build/frontend-legacy/vitest.config.mts index 066a5594c7424..5a480c80fdc5c 100644 --- a/build/frontend-legacy/vitest.config.mts +++ b/build/frontend-legacy/vitest.config.mts @@ -48,6 +48,7 @@ export default defineConfig({ }, test: { include: ['./{apps,core}/**/*.{test,spec}.?(c|m)[jt]s?(x)'], + passWithNoTests: true, environment: 'jsdom', environmentOptions: { jsdom: { diff --git a/build/frontend/vitest.config.ts b/build/frontend/vitest.config.ts index a7078a2356f17..09b3fe709f8d8 100644 --- a/build/frontend/vitest.config.ts +++ b/build/frontend/vitest.config.ts @@ -38,6 +38,7 @@ export default defineConfig({ }, test: { include: ['apps/**/*.{test,spec}.?(c|m)[jt]s?(x)'], + passWithNoTests: true, env: { LANG: 'en_US', TZ: 'UTC', From a51ed15af4e6f4aa2003343069ebb93b09fd4ad8 Mon Sep 17 00:00:00 2001 From: nfebe Date: Thu, 19 Mar 2026 13:05:03 +0100 Subject: [PATCH 2/2] fix(sharing): Prevent generated password from overwriting user input fix(sharing): Prevent generated password from overwriting user input Signed-off-by: nfebe [skip ci] --- apps/files_sharing/src/mixins/SharesMixin.js | 5 +- .../src/views/SharingDetailsTab.spec.ts | 346 ++++++++---------- .../src/views/SharingDetailsTab.vue | 5 +- build/frontend-legacy/vitest.config.mts | 1 - build/frontend/vitest.config.ts | 1 - 5 files changed, 166 insertions(+), 192 deletions(-) diff --git a/apps/files_sharing/src/mixins/SharesMixin.js b/apps/files_sharing/src/mixins/SharesMixin.js index f5e3902814f86..fa5dc2d5d27b6 100644 --- a/apps/files_sharing/src/mixins/SharesMixin.js +++ b/apps/files_sharing/src/mixins/SharesMixin.js @@ -180,7 +180,10 @@ export default { async set(enabled) { if (enabled) { this.passwordProtectedState = true - this.$set(this.share, 'newPassword', await GeneratePassword(true)) + const generatedPassword = await GeneratePassword(true) + if (!this.share.newPassword) { + this.$set(this.share, 'newPassword', generatedPassword) + } } else { this.passwordProtectedState = false this.$set(this.share, 'newPassword', '') diff --git a/apps/files_sharing/src/views/SharingDetailsTab.spec.ts b/apps/files_sharing/src/views/SharingDetailsTab.spec.ts index 07aacb2e36510..cf7168febe260 100644 --- a/apps/files_sharing/src/views/SharingDetailsTab.spec.ts +++ b/apps/files_sharing/src/views/SharingDetailsTab.spec.ts @@ -5,6 +5,8 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' +const mockGeneratePassword = vi.fn().mockResolvedValue('generated-password-123') + vi.mock('../services/ConfigService.ts', () => ({ default: vi.fn().mockImplementation(() => ({ enableLinkPasswordByDefault: false, @@ -24,180 +26,202 @@ vi.mock('../services/ConfigService.ts', () => ({ })) vi.mock('../utils/GeneratePassword.ts', () => ({ - default: vi.fn().mockResolvedValue('generated-password-123'), + default: (...args: unknown[]) => mockGeneratePassword(...args), })) +/** + * Simulates the isPasswordProtected getter from SharesMixin.js + */ +function getIsPasswordProtected(state: { + enforcePasswordForPublicLink: boolean + passwordProtectedState: boolean | undefined + newPassword: string | undefined + password: string | undefined +}): boolean { + if (state.enforcePasswordForPublicLink) { + return true + } + if (state.passwordProtectedState !== undefined) { + return state.passwordProtectedState + } + return typeof state.newPassword === 'string' + || typeof state.password === 'string' +} + +/** + * Simulates the isPasswordProtected setter from SharesMixin.js + * Returns the resulting share state after the async operation completes. + */ +async function setIsPasswordProtected( + enabled: boolean, + share: { newPassword?: string }, +): Promise<{ passwordProtectedState: boolean, share: { newPassword?: string } }> { + if (enabled) { + const generatedPassword = await mockGeneratePassword(true) + if (!share.newPassword) { + share.newPassword = generatedPassword + } + return { passwordProtectedState: true, share } + } else { + share.newPassword = '' + return { passwordProtectedState: false, share } + } +} + describe('SharingDetailsTab - Password State Management Logic', () => { - describe('isPasswordProtected getter logic', () => { + beforeEach(() => { + mockGeneratePassword.mockClear() + mockGeneratePassword.mockResolvedValue('generated-password-123') + }) + + describe('isPasswordProtected getter', () => { + it('returns true when enforcePasswordForPublicLink is true regardless of other state', () => { + expect(getIsPasswordProtected({ + enforcePasswordForPublicLink: true, + passwordProtectedState: false, + newPassword: undefined, + password: undefined, + })).toBe(true) + }) + it('returns true when passwordProtectedState is explicitly true', () => { - const passwordProtectedState: boolean | undefined = true - const enforcePasswordForPublicLink = false - const newPassword: string | undefined = undefined - const password: string | undefined = undefined - - const isPasswordProtected = (() => { - if (enforcePasswordForPublicLink) { - return true - } - if (passwordProtectedState !== undefined) { - return passwordProtectedState - } - return typeof newPassword === 'string' - || typeof password === 'string' - })() - - expect(isPasswordProtected).toBe(true) + expect(getIsPasswordProtected({ + enforcePasswordForPublicLink: false, + passwordProtectedState: true, + newPassword: undefined, + password: undefined, + })).toBe(true) }) it('returns false when passwordProtectedState is explicitly false', () => { - const passwordProtectedState: boolean | undefined = false - const enforcePasswordForPublicLink = false - const newPassword: string | undefined = 'some-password' - const password: string | undefined = undefined - - const isPasswordProtected = (() => { - if (enforcePasswordForPublicLink) { - return true - } - if (passwordProtectedState !== undefined) { - return passwordProtectedState - } - return typeof newPassword === 'string' - || typeof password === 'string' - })() - - expect(isPasswordProtected).toBe(false) + expect(getIsPasswordProtected({ + enforcePasswordForPublicLink: false, + passwordProtectedState: false, + newPassword: 'some-password', + password: undefined, + })).toBe(false) }) - it('returns true when enforcePasswordForPublicLink is true regardless of other state', () => { - const passwordProtectedState: boolean | undefined = false - const enforcePasswordForPublicLink = true - const newPassword: string | undefined = undefined - const password: string | undefined = undefined - - const isPasswordProtected = (() => { - if (enforcePasswordForPublicLink) { - return true - } - if (passwordProtectedState !== undefined) { - return passwordProtectedState - } - return typeof newPassword === 'string' - || typeof password === 'string' - })() - - expect(isPasswordProtected).toBe(true) + it('falls back to inferring from newPassword when passwordProtectedState is undefined', () => { + expect(getIsPasswordProtected({ + enforcePasswordForPublicLink: false, + passwordProtectedState: undefined, + newPassword: 'some-password', + password: undefined, + })).toBe(true) }) it('falls back to inferring from password when passwordProtectedState is undefined', () => { - const passwordProtectedState: boolean | undefined = undefined - const enforcePasswordForPublicLink = false - const newPassword: string | undefined = 'some-password' - const password: string | undefined = undefined - - const isPasswordProtected = (() => { - if (enforcePasswordForPublicLink) { - return true - } - if (passwordProtectedState !== undefined) { - return passwordProtectedState - } - return typeof newPassword === 'string' - || typeof password === 'string' - })() - - expect(isPasswordProtected).toBe(true) + expect(getIsPasswordProtected({ + enforcePasswordForPublicLink: false, + passwordProtectedState: undefined, + newPassword: undefined, + password: 'existing-password', + })).toBe(true) }) it('returns false when passwordProtectedState is undefined and no passwords exist', () => { - const passwordProtectedState: boolean | undefined = undefined - const enforcePasswordForPublicLink = false - const newPassword: string | undefined = undefined - const password: string | undefined = undefined - - const isPasswordProtected = (() => { - if (enforcePasswordForPublicLink) { - return true - } - if (passwordProtectedState !== undefined) { - return passwordProtectedState - } - return typeof newPassword === 'string' - || typeof password === 'string' - })() - - expect(isPasswordProtected).toBe(false) + expect(getIsPasswordProtected({ + enforcePasswordForPublicLink: false, + passwordProtectedState: undefined, + newPassword: undefined, + password: undefined, + })).toBe(false) + }) + + it('checkbox remains checked when passwordProtectedState is true even if password is cleared', () => { + expect(getIsPasswordProtected({ + enforcePasswordForPublicLink: false, + passwordProtectedState: true, + newPassword: '', + password: undefined, + })).toBe(true) + }) + }) + + describe('isPasswordProtected setter (race condition fix)', () => { + it('generated password does NOT overwrite user-typed password', async () => { + const share = { newPassword: 'user-typed-password' } + const result = await setIsPasswordProtected(true, share) + + expect(mockGeneratePassword).toHaveBeenCalledWith(true) + expect(result.passwordProtectedState).toBe(true) + expect(result.share.newPassword).toBe('user-typed-password') + }) + + it('generated password IS applied when user has not typed anything', async () => { + const share: { newPassword?: string } = {} + const result = await setIsPasswordProtected(true, share) + + expect(mockGeneratePassword).toHaveBeenCalledWith(true) + expect(result.passwordProtectedState).toBe(true) + expect(result.share.newPassword).toBe('generated-password-123') + }) + + it('generated password IS applied when newPassword is empty string (user cleared input)', async () => { + const share = { newPassword: '' } + const result = await setIsPasswordProtected(true, share) + + expect(result.share.newPassword).toBe('generated-password-123') + }) + + it('disabling password clears newPassword and sets state to false', async () => { + const share = { newPassword: 'some-password' } + const result = await setIsPasswordProtected(false, share) + + expect(result.passwordProtectedState).toBe(false) + expect(result.share.newPassword).toBe('') }) }) describe('initializeAttributes sets passwordProtectedState', () => { - it('should set passwordProtectedState to true when enableLinkPasswordByDefault is true', async () => { - const config = { - enableLinkPasswordByDefault: true, - enforcePasswordForPublicLink: false, - } + it('should set passwordProtectedState when enableLinkPasswordByDefault is true for new public share', () => { + const config = { enableLinkPasswordByDefault: true, enforcePasswordForPublicLink: false } const isNewShare = true const isPublicShare = true let passwordProtectedState: boolean | undefined - if (isNewShare) { - if ((config.enableLinkPasswordByDefault || config.enforcePasswordForPublicLink) && isPublicShare) { - passwordProtectedState = true - } + if (isNewShare && (config.enableLinkPasswordByDefault || config.enforcePasswordForPublicLink) && isPublicShare) { + passwordProtectedState = true } expect(passwordProtectedState).toBe(true) }) - it('should set passwordProtectedState to true when isPasswordEnforced is true', async () => { - const config = { - enableLinkPasswordByDefault: false, - enforcePasswordForPublicLink: true, - } + it('should set passwordProtectedState when isPasswordEnforced is true for new public share', () => { + const config = { enableLinkPasswordByDefault: false, enforcePasswordForPublicLink: true } const isNewShare = true const isPublicShare = true let passwordProtectedState: boolean | undefined - if (isNewShare) { - if ((config.enableLinkPasswordByDefault || config.enforcePasswordForPublicLink) && isPublicShare) { - passwordProtectedState = true - } + if (isNewShare && (config.enableLinkPasswordByDefault || config.enforcePasswordForPublicLink) && isPublicShare) { + passwordProtectedState = true } expect(passwordProtectedState).toBe(true) }) - it('should not set passwordProtectedState for non-public shares', async () => { - const config = { - enableLinkPasswordByDefault: true, - enforcePasswordForPublicLink: false, - } + it('should not set passwordProtectedState for non-public shares', () => { + const config = { enableLinkPasswordByDefault: true, enforcePasswordForPublicLink: false } const isNewShare = true const isPublicShare = false let passwordProtectedState: boolean | undefined - if (isNewShare) { - if ((config.enableLinkPasswordByDefault || config.enforcePasswordForPublicLink) && isPublicShare) { - passwordProtectedState = true - } + if (isNewShare && (config.enableLinkPasswordByDefault || config.enforcePasswordForPublicLink) && isPublicShare) { + passwordProtectedState = true } expect(passwordProtectedState).toBe(undefined) }) - it('should not set passwordProtectedState for existing shares', async () => { - const config = { - enableLinkPasswordByDefault: true, - enforcePasswordForPublicLink: false, - } + it('should not set passwordProtectedState for existing shares', () => { + const config = { enableLinkPasswordByDefault: true, enforcePasswordForPublicLink: false } const isNewShare = false const isPublicShare = true let passwordProtectedState: boolean | undefined - if (isNewShare) { - if ((config.enableLinkPasswordByDefault || config.enforcePasswordForPublicLink) && isPublicShare) { - passwordProtectedState = true - } + if (isNewShare && (config.enableLinkPasswordByDefault || config.enforcePasswordForPublicLink) && isPublicShare) { + passwordProtectedState = true } expect(passwordProtectedState).toBe(undefined) @@ -215,10 +239,8 @@ describe('SharingDetailsTab - Password State Management Logic', () => { const newPassword = '' let passwordError = false - if (isPasswordProtected) { - if (isNewShare && !isValidShareAttribute(newPassword)) { - passwordError = true - } + if (isPasswordProtected && isNewShare && !isValidShareAttribute(newPassword)) { + passwordError = true } expect(passwordError).toBe(true) @@ -230,10 +252,8 @@ describe('SharingDetailsTab - Password State Management Logic', () => { const newPassword = undefined let passwordError = false - if (isPasswordProtected) { - if (isNewShare && !isValidShareAttribute(newPassword)) { - passwordError = true - } + if (isPasswordProtected && isNewShare && !isValidShareAttribute(newPassword)) { + passwordError = true } expect(passwordError).toBe(true) @@ -245,10 +265,8 @@ describe('SharingDetailsTab - Password State Management Logic', () => { const newPassword = 'valid-password-123' let passwordError = false - if (isPasswordProtected) { - if (isNewShare && !isValidShareAttribute(newPassword)) { - passwordError = true - } + if (isPasswordProtected && isNewShare && !isValidShareAttribute(newPassword)) { + passwordError = true } expect(passwordError).toBe(false) @@ -260,10 +278,8 @@ describe('SharingDetailsTab - Password State Management Logic', () => { const newPassword = '' let passwordError = false - if (isPasswordProtected) { - if (isNewShare && !isValidShareAttribute(newPassword)) { - passwordError = true - } + if (isPasswordProtected && isNewShare && !isValidShareAttribute(newPassword)) { + passwordError = true } expect(passwordError).toBe(false) @@ -275,57 +291,11 @@ describe('SharingDetailsTab - Password State Management Logic', () => { const newPassword = '' let passwordError = false - if (isPasswordProtected) { - if (isNewShare && !isValidShareAttribute(newPassword)) { - passwordError = true - } + if (isPasswordProtected && isNewShare && !isValidShareAttribute(newPassword)) { + passwordError = true } expect(passwordError).toBe(false) }) }) - - describe('checkbox persistence after clearing password', () => { - it('checkbox remains checked when passwordProtectedState is explicitly true even if password is cleared', () => { - let passwordProtectedState: boolean | undefined = true - const enforcePasswordForPublicLink = false - let newPassword: string | undefined = 'initial-password' - - newPassword = '' - - const isPasswordProtected = (() => { - if (enforcePasswordForPublicLink) { - return true - } - if (passwordProtectedState !== undefined) { - return passwordProtectedState - } - return typeof newPassword === 'string' - || false - })() - - expect(isPasswordProtected).toBe(true) - }) - - it('checkbox unchecks incorrectly if passwordProtectedState was never set (bug scenario)', () => { - let passwordProtectedState: boolean | undefined = undefined - const enforcePasswordForPublicLink = false - let newPassword: string | undefined = 'initial-password' - - newPassword = undefined - - const isPasswordProtected = (() => { - if (enforcePasswordForPublicLink) { - return true - } - if (passwordProtectedState !== undefined) { - return passwordProtectedState - } - return typeof newPassword === 'string' - || false - })() - - expect(isPasswordProtected).toBe(false) - }) - }) }) diff --git a/apps/files_sharing/src/views/SharingDetailsTab.vue b/apps/files_sharing/src/views/SharingDetailsTab.vue index e9668d78a01f2..84c039de43b9a 100644 --- a/apps/files_sharing/src/views/SharingDetailsTab.vue +++ b/apps/files_sharing/src/views/SharingDetailsTab.vue @@ -975,7 +975,10 @@ export default { if (this.isNewShare) { if ((this.config.enableLinkPasswordByDefault || this.isPasswordEnforced) && this.isPublicShare) { this.passwordProtectedState = true - this.$set(this.share, 'newPassword', await GeneratePassword(true)) + const generatedPassword = await GeneratePassword(true) + if (!this.share.newPassword) { + this.$set(this.share, 'newPassword', generatedPassword) + } this.advancedSectionAccordionExpanded = true } /* Set default expiration dates if configured */ diff --git a/build/frontend-legacy/vitest.config.mts b/build/frontend-legacy/vitest.config.mts index 5a480c80fdc5c..066a5594c7424 100644 --- a/build/frontend-legacy/vitest.config.mts +++ b/build/frontend-legacy/vitest.config.mts @@ -48,7 +48,6 @@ export default defineConfig({ }, test: { include: ['./{apps,core}/**/*.{test,spec}.?(c|m)[jt]s?(x)'], - passWithNoTests: true, environment: 'jsdom', environmentOptions: { jsdom: { diff --git a/build/frontend/vitest.config.ts b/build/frontend/vitest.config.ts index 09b3fe709f8d8..a7078a2356f17 100644 --- a/build/frontend/vitest.config.ts +++ b/build/frontend/vitest.config.ts @@ -38,7 +38,6 @@ export default defineConfig({ }, test: { include: ['apps/**/*.{test,spec}.?(c|m)[jt]s?(x)'], - passWithNoTests: true, env: { LANG: 'en_US', TZ: 'UTC',