Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
331 changes: 331 additions & 0 deletions apps/files_sharing/src/views/SharingDetailsTab.spec.ts
Original file line number Diff line number Diff line change
@@ -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)
})
})
})
4 changes: 3 additions & 1 deletion apps/files_sharing/src/views/SharingDetailsTab.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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 = ''
Expand Down
1 change: 1 addition & 0 deletions build/frontend-legacy/vitest.config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export default defineConfig({
},
test: {
include: ['./{apps,core}/**/*.{test,spec}.?(c|m)[jt]s?(x)'],
passWithNoTests: true,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prevent 1 exit when not tests around found in a linked app.

https://vitest.dev/config/passwithnotests.html

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But in which case this should happen?
If it happens then this points to a real issue of that app.

Except you mean like external other apps you have linked only locally.
But in that case you should not put them into apps but rather something like apps-extra or similar.

environment: 'jsdom',
environmentOptions: {
jsdom: {
Expand Down
1 change: 1 addition & 0 deletions build/frontend/vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
Loading