mirror of
https://github.com/nextcloud/server.git
synced 2026-04-08 18:46:28 -04:00
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 <fenn25.fn@gmail.com>
This commit is contained in:
parent
010e49cfd4
commit
56fdf0ed37
2 changed files with 334 additions and 1 deletions
331
apps/files_sharing/src/views/SharingDetailsTab.spec.ts
Normal file
331
apps/files_sharing/src/views/SharingDetailsTab.spec.ts
Normal file
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -900,6 +900,7 @@ export default {
|
|||
|
||||
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
|
||||
}
|
||||
|
|
@ -1005,8 +1006,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 = ''
|
||||
|
|
|
|||
Loading…
Reference in a new issue