Skip to content

Commit cc8bab1

Browse files
moodyjmzclaude
andcommitted
fix: 🐛 handle negative lock timeout (ETA_INFINITE sentinel) from files_lock
When files_lock is configured with the default infinite timeout (-1 min), the server sends nc:lock-timeout=-60 via PROPFIND. The previous guard only checked for == 0, causing getLockedUntil() to compute a timestamp 60s in the past, making the lock appear expired immediately. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
1 parent f8fb1f2 commit cc8bab1

2 files changed

Lines changed: 74 additions & 1 deletion

File tree

app/src/main/java/com/nextcloud/ui/fileactions/FileActionsViewModel.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ package com.nextcloud.ui.fileactions
99

1010
import android.os.Bundle
1111
import androidx.annotation.IdRes
12+
import androidx.annotation.VisibleForTesting
1213
import androidx.lifecycle.LiveData
1314
import androidx.lifecycle.MutableLiveData
1415
import androidx.lifecycle.ViewModel
@@ -125,7 +126,8 @@ class FileActionsViewModel @Inject constructor(
125126
}
126127
}
127128

128-
private fun getLockedUntil(file: OCFile): Long? = if (file.lockTimestamp == 0L || file.lockTimeout == 0L) {
129+
@VisibleForTesting
130+
internal fun getLockedUntil(file: OCFile): Long? = if (file.lockTimestamp == 0L || file.lockTimeout <= 0L) {
129131
null
130132
} else {
131133
(file.lockTimestamp + file.lockTimeout) * TimeConstants.MILLIS_PER_SECOND
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/*
2+
* Nextcloud - Android Client
3+
*
4+
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH
5+
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
6+
*/
7+
package com.nextcloud.ui.fileactions
8+
9+
import com.nextcloud.client.account.CurrentAccountProvider
10+
import com.nextcloud.client.logger.Logger
11+
import com.nextcloud.utils.TimeConstants
12+
import com.owncloud.android.datamodel.OCFile
13+
import com.owncloud.android.files.FileMenuFilter
14+
import org.junit.Assert.assertEquals
15+
import org.junit.Assert.assertNull
16+
import org.junit.Before
17+
import org.junit.Test
18+
import org.mockito.kotlin.mock
19+
20+
class FileActionsViewModelTest {
21+
22+
private lateinit var viewModel: FileActionsViewModel
23+
24+
@Before
25+
fun setUp() {
26+
viewModel = FileActionsViewModel(
27+
mock<CurrentAccountProvider>(),
28+
mock<FileMenuFilter.Factory>(),
29+
mock<Logger>()
30+
)
31+
}
32+
33+
@Test
34+
fun `getLockedUntil returns null when lockTimestamp is zero`() {
35+
val file = OCFile("/test.docx").apply {
36+
lockTimestamp = 0L
37+
lockTimeout = 300L
38+
}
39+
assertNull(viewModel.getLockedUntil(file))
40+
}
41+
42+
@Test
43+
fun `getLockedUntil returns null when lockTimeout is zero`() {
44+
val file = OCFile("/test.docx").apply {
45+
lockTimestamp = 1_700_000_000L
46+
lockTimeout = 0L
47+
}
48+
assertNull(viewModel.getLockedUntil(file))
49+
}
50+
51+
@Test
52+
fun `getLockedUntil returns null when lockTimeout is negative (ETA_INFINITE sentinel)`() {
53+
val file = OCFile("/test.docx").apply {
54+
lockTimestamp = 1_700_000_000L
55+
lockTimeout = -60L
56+
}
57+
assertNull(viewModel.getLockedUntil(file))
58+
}
59+
60+
@Test
61+
fun `getLockedUntil returns correct millis when both values are positive`() {
62+
val timestamp = 1_700_000_000L
63+
val timeout = 300L
64+
val file = OCFile("/test.docx").apply {
65+
lockTimestamp = timestamp
66+
lockTimeout = timeout
67+
}
68+
val expected = (timestamp + timeout) * TimeConstants.MILLIS_PER_SECOND
69+
assertEquals(expected, viewModel.getLockedUntil(file))
70+
}
71+
}

0 commit comments

Comments
 (0)