fix: scope boards API calls to resolved workspace

This commit is contained in:
2026-03-15 22:47:02 -04:00
parent 5016704627
commit c820b413aa
11 changed files with 333 additions and 28 deletions

View File

@@ -50,6 +50,7 @@ Kanbn4Droid is an unofficial app to connect to and manipulate data stored in sel
- The API key is stored in app preferences together with the base URL.
- No migration is performed from prior Credential Manager storage, so users must re-enter their API key one time after upgrading.
- On success, the view stores the URL and API key pair in preferences and moves over to the boards view.
- On successful manual sign-in, the stored workspace id is cleared so the boards flow can resolve a fresh default workspace for the account.
- If there is a URL and API Key pair stored, the view tries to authenticate the user through the API automatically and proceeds to the boards view instantly without showing the login screen if successful.
- If startup authentication fails due to invalid credentials then the stored API key is invalidated; transient connectivity/server failures keep the stored key and return to login.
- Current status: implemented in `MainActivity` with XML views and navigation into `BoardsActivity`.
@@ -71,7 +72,7 @@ Kanbn4Droid is an unofficial app to connect to and manipulate data stored in sel
- Long-pressing an existing board shows a modal dialog asking the user if they want to delete the board, with buttons for "Cancel" and "Delete".
- Pressing "Delete" in the modal dialog MUST show a second confirmation modal asking if the user is sure, with buttons for "Cancel" and "I'm sure".
- Only on pressing "I'm sure" in the second confirmation modal dialog should a board delete request be sent to the API.
- Current status: implemented in `BoardsActivity` using XML Views, `RecyclerView`, `SwipeRefreshLayout`, and a `BoardsViewModel`/`BoardsRepository` flow. The screen includes auto-refresh on entry, pull-to-refresh, create board dialog with optional template selector, and two-step board delete confirmation. Board taps navigate to `BoardDetailPlaceholderActivity` while full board detail is still pending.
- Current status: implemented in `BoardsActivity` using XML Views, `RecyclerView`, `SwipeRefreshLayout`, and a `BoardsViewModel`/`BoardsRepository` flow. The screen includes auto-refresh on entry, pull-to-refresh, create board dialog with optional template selector, and two-step board delete confirmation. Board taps navigate to `BoardDetailPlaceholderActivity` while full board detail is still pending. API calls are workspace-scoped; when no workspace is stored the app resolves workspaces through the API, stores the first workspace id as default, and uses it for board listing, template listing (`type=template`), and board creation.
**Board detail view**

View File

@@ -27,6 +27,7 @@ import space.hackenslacker.kanbn4droid.app.auth.SessionStore
import space.hackenslacker.kanbn4droid.app.boards.BoardSummary
import space.hackenslacker.kanbn4droid.app.boards.BoardTemplate
import space.hackenslacker.kanbn4droid.app.boards.BoardsApiResult
import space.hackenslacker.kanbn4droid.app.boards.WorkspaceSummary
@RunWith(AndroidJUnit4::class)
class BoardsFlowTest {
@@ -127,6 +128,14 @@ class BoardsFlowTest {
override fun clearBaseUrl() {
baseUrl = null
}
override fun getWorkspaceId(): String? = "ws-1"
override fun saveWorkspaceId(workspaceId: String) {
}
override fun clearWorkspaceId() {
}
}
private class InMemoryApiKeyStore(
@@ -151,13 +160,25 @@ class BoardsFlowTest {
) : KanbnApiClient {
override suspend fun healthCheck(baseUrl: String, apiKey: String): AuthResult = AuthResult.Success
override suspend fun listBoards(baseUrl: String, apiKey: String): BoardsApiResult<List<BoardSummary>> {
override suspend fun listWorkspaces(
baseUrl: String,
apiKey: String,
): BoardsApiResult<List<WorkspaceSummary>> {
return BoardsApiResult.Success(listOf(WorkspaceSummary("ws-1", "Main")))
}
override suspend fun listBoards(
baseUrl: String,
apiKey: String,
workspaceId: String,
): BoardsApiResult<List<BoardSummary>> {
return BoardsApiResult.Success(boards.toList())
}
override suspend fun listBoardTemplates(
baseUrl: String,
apiKey: String,
workspaceId: String,
): BoardsApiResult<List<BoardTemplate>> {
return BoardsApiResult.Success(templates)
}
@@ -165,6 +186,7 @@ class BoardsFlowTest {
override suspend fun createBoard(
baseUrl: String,
apiKey: String,
workspaceId: String,
name: String,
templateId: String?,
): BoardsApiResult<BoardSummary> {

View File

@@ -163,6 +163,14 @@ class LoginFlowTest {
override fun clearBaseUrl() {
baseUrl = null
}
override fun getWorkspaceId(): String? = null
override fun saveWorkspaceId(workspaceId: String) {
}
override fun clearWorkspaceId() {
}
}
private class InMemoryApiKeyStore(

View File

@@ -159,6 +159,7 @@ class MainActivity : AppCompatActivity() {
}
if (saveKeyResult.isSuccess) {
sessionStore.saveBaseUrl(normalizedBaseUrl)
sessionStore.clearWorkspaceId()
openBoards()
} else {
loginProgress.visibility = View.GONE

View File

@@ -10,21 +10,35 @@ import kotlinx.coroutines.withContext
import space.hackenslacker.kanbn4droid.app.boards.BoardSummary
import space.hackenslacker.kanbn4droid.app.boards.BoardTemplate
import space.hackenslacker.kanbn4droid.app.boards.BoardsApiResult
import space.hackenslacker.kanbn4droid.app.boards.WorkspaceSummary
interface KanbnApiClient {
suspend fun healthCheck(baseUrl: String, apiKey: String): AuthResult
suspend fun listBoards(baseUrl: String, apiKey: String): BoardsApiResult<List<BoardSummary>> {
suspend fun listWorkspaces(baseUrl: String, apiKey: String): BoardsApiResult<List<WorkspaceSummary>> {
return BoardsApiResult.Failure("Workspace listing is not implemented.")
}
suspend fun listBoards(
baseUrl: String,
apiKey: String,
workspaceId: String,
): BoardsApiResult<List<BoardSummary>> {
return BoardsApiResult.Failure("Boards listing is not implemented.")
}
suspend fun listBoardTemplates(baseUrl: String, apiKey: String): BoardsApiResult<List<BoardTemplate>> {
suspend fun listBoardTemplates(
baseUrl: String,
apiKey: String,
workspaceId: String,
): BoardsApiResult<List<BoardTemplate>> {
return BoardsApiResult.Failure("Board templates listing is not implemented.")
}
suspend fun createBoard(
baseUrl: String,
apiKey: String,
workspaceId: String,
name: String,
templateId: String?,
): BoardsApiResult<BoardSummary> {
@@ -38,6 +52,23 @@ interface KanbnApiClient {
class HttpKanbnApiClient : KanbnApiClient {
override suspend fun listWorkspaces(baseUrl: String, apiKey: String): BoardsApiResult<List<WorkspaceSummary>> {
return withContext(Dispatchers.IO) {
request(
baseUrl = baseUrl,
path = "/api/v1/workspaces",
method = "GET",
apiKey = apiKey,
) { code, body ->
if (code in 200..299) {
BoardsApiResult.Success(parseWorkspaces(body))
} else {
BoardsApiResult.Failure(serverMessage(body, code))
}
}
}
}
override suspend fun healthCheck(baseUrl: String, apiKey: String): AuthResult {
return withContext(Dispatchers.IO) {
val endpoint = "${baseUrl.trimEnd('/')}/api/v1/health"
@@ -67,11 +98,15 @@ class HttpKanbnApiClient : KanbnApiClient {
}
}
override suspend fun listBoards(baseUrl: String, apiKey: String): BoardsApiResult<List<BoardSummary>> {
override suspend fun listBoards(
baseUrl: String,
apiKey: String,
workspaceId: String,
): BoardsApiResult<List<BoardSummary>> {
return withContext(Dispatchers.IO) {
request(
baseUrl = baseUrl,
path = "/api/v1/boards",
path = "/api/v1/workspaces/$workspaceId/boards",
method = "GET",
apiKey = apiKey,
) { code, body ->
@@ -87,11 +122,12 @@ class HttpKanbnApiClient : KanbnApiClient {
override suspend fun listBoardTemplates(
baseUrl: String,
apiKey: String,
workspaceId: String,
): BoardsApiResult<List<BoardTemplate>> {
return withContext(Dispatchers.IO) {
request(
baseUrl = baseUrl,
path = "/api/v1/board-templates",
path = "/api/v1/workspaces/$workspaceId/boards?type=template",
method = "GET",
apiKey = apiKey,
) { code, body ->
@@ -107,18 +143,22 @@ class HttpKanbnApiClient : KanbnApiClient {
override suspend fun createBoard(
baseUrl: String,
apiKey: String,
workspaceId: String,
name: String,
templateId: String?,
): BoardsApiResult<BoardSummary> {
return withContext(Dispatchers.IO) {
val payload = JSONObject().put("title", name)
val payload = JSONObject()
.put("name", name)
.put("lists", JSONArray())
.put("labels", JSONArray())
if (!templateId.isNullOrBlank()) {
payload.put("template_id", templateId)
payload.put("sourceBoardPublicId", templateId)
}
request(
baseUrl = baseUrl,
path = "/api/v1/boards",
path = "/api/v1/workspaces/$workspaceId/boards",
method = "POST",
apiKey = apiKey,
body = payload.toString(),
@@ -222,9 +262,14 @@ class HttpKanbnApiClient : KanbnApiClient {
val boards = mutableListOf<BoardSummary>()
for (index in 0 until array.length()) {
val item = array.optJSONObject(index) ?: continue
val id = item.opt("id")?.toString().orEmpty()
val id = item.opt("id")?.toString().orEmpty().ifBlank {
item.optString("publicId")
.ifBlank { item.optString("public_id") }
}
val title = item.optString("title").ifBlank {
item.optString("name").ifBlank { "Board" }
item.optString("name").ifBlank {
item.optString("slug").ifBlank { "Board" }
}
}
if (id.isNotBlank()) {
boards += BoardSummary(id = id, title = title)
@@ -239,7 +284,11 @@ class HttpKanbnApiClient : KanbnApiClient {
}
val root = JSONObject(body)
val id = root.opt("id")?.toString().orEmpty().ifBlank { root.opt("board_id")?.toString().orEmpty() }
val id = root.opt("id")?.toString().orEmpty().ifBlank {
root.optString("publicId")
.ifBlank { root.optString("public_id") }
.ifBlank { root.opt("board_id")?.toString().orEmpty() }
}
val title = root.optString("title").ifBlank { root.optString("name").ifBlank { fallbackName } }
return BoardSummary(id = if (id.isBlank()) "new" else id, title = title)
}
@@ -262,7 +311,10 @@ class HttpKanbnApiClient : KanbnApiClient {
val templates = mutableListOf<BoardTemplate>()
for (index in 0 until array.length()) {
val item = array.optJSONObject(index) ?: continue
val id = item.opt("id")?.toString().orEmpty()
val id = item.opt("id")?.toString().orEmpty().ifBlank {
item.optString("publicId")
.ifBlank { item.optString("public_id") }
}
val name = item.optString("name").ifBlank {
item.optString("title").ifBlank { "Template" }
}
@@ -273,6 +325,42 @@ class HttpKanbnApiClient : KanbnApiClient {
return templates
}
private fun parseWorkspaces(body: String): List<WorkspaceSummary> {
if (body.isBlank()) {
return emptyList()
}
val trimmed = body.trim()
val array = if (trimmed.startsWith("[")) {
JSONArray(trimmed)
} else {
val root = JSONObject(trimmed)
listOf("workspaces", "items", "data")
.firstNotNullOfOrNull { root.optJSONArray(it) }
?: JSONArray()
}
val workspaces = mutableListOf<WorkspaceSummary>()
for (index in 0 until array.length()) {
val item = array.optJSONObject(index) ?: continue
val workspace = item.optJSONObject("workspace") ?: item
val id = workspace.opt("id")?.toString().orEmpty().ifBlank {
workspace.optString("publicId")
.ifBlank { workspace.optString("public_id") }
}
if (id.isBlank()) {
continue
}
val name = workspace.optString("name")
.ifBlank { workspace.optString("title") }
.ifBlank { workspace.optString("slug") }
.ifBlank { "Workspace" }
workspaces += WorkspaceSummary(id = id, name = name)
}
return workspaces
}
private fun serverMessage(body: String, code: Int): String {
if (body.isBlank()) {
return "Server error: $code"

View File

@@ -13,12 +13,23 @@ class SessionPreferences(context: Context) : SessionStore {
preferences.edit().putString(KEY_BASE_URL, url).apply()
}
override fun getWorkspaceId(): String? = preferences.getString(KEY_WORKSPACE_ID, null)
override fun saveWorkspaceId(workspaceId: String) {
preferences.edit().putString(KEY_WORKSPACE_ID, workspaceId).apply()
}
override fun clearBaseUrl() {
preferences.edit().remove(KEY_BASE_URL).apply()
}
override fun clearWorkspaceId() {
preferences.edit().remove(KEY_WORKSPACE_ID).apply()
}
private companion object {
private const val PREFS_NAME = "kanbn_session"
private const val KEY_BASE_URL = "base_url"
private const val KEY_WORKSPACE_ID = "workspace_id"
}
}

View File

@@ -3,5 +3,8 @@ package space.hackenslacker.kanbn4droid.app.auth
interface SessionStore {
fun getBaseUrl(): String?
fun saveBaseUrl(url: String)
fun getWorkspaceId(): String?
fun saveWorkspaceId(workspaceId: String)
fun clearBaseUrl()
fun clearWorkspaceId()
}

View File

@@ -10,6 +10,11 @@ data class BoardTemplate(
val name: String,
)
data class WorkspaceSummary(
val id: String,
val name: String,
)
sealed interface BoardsApiResult<out T> {
data class Success<T>(val value: T) : BoardsApiResult<T>
data class Failure(val message: String) : BoardsApiResult<Nothing>

View File

@@ -14,47 +14,96 @@ class BoardsRepository(
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO,
) {
suspend fun listBoards(): BoardsApiResult<List<BoardSummary>> {
val session = session() ?: return BoardsApiResult.Failure("Missing session. Please sign in again.")
return apiClient.listBoards(session.baseUrl, session.apiKey)
val session = when (val sessionResult = session()) {
is BoardsApiResult.Success -> sessionResult.value
is BoardsApiResult.Failure -> return sessionResult
}
return apiClient.listBoards(
baseUrl = session.baseUrl,
apiKey = session.apiKey,
workspaceId = session.workspaceId,
)
}
suspend fun listTemplates(): BoardsApiResult<List<BoardTemplate>> {
val session = session() ?: return BoardsApiResult.Failure("Missing session. Please sign in again.")
return apiClient.listBoardTemplates(session.baseUrl, session.apiKey)
val session = when (val sessionResult = session()) {
is BoardsApiResult.Success -> sessionResult.value
is BoardsApiResult.Failure -> return sessionResult
}
return apiClient.listBoardTemplates(
baseUrl = session.baseUrl,
apiKey = session.apiKey,
workspaceId = session.workspaceId,
)
}
suspend fun createBoard(name: String, templateId: String?): BoardsApiResult<BoardSummary> {
val session = session() ?: return BoardsApiResult.Failure("Missing session. Please sign in again.")
val session = when (val sessionResult = session()) {
is BoardsApiResult.Success -> sessionResult.value
is BoardsApiResult.Failure -> return sessionResult
}
if (name.isBlank()) {
return BoardsApiResult.Failure("Board name is required")
}
return apiClient.createBoard(
baseUrl = session.baseUrl,
apiKey = session.apiKey,
workspaceId = session.workspaceId,
name = name.trim(),
templateId = templateId,
)
}
suspend fun deleteBoard(boardId: String): BoardsApiResult<Unit> {
val session = session() ?: return BoardsApiResult.Failure("Missing session. Please sign in again.")
val session = when (val sessionResult = session()) {
is BoardsApiResult.Success -> sessionResult.value
is BoardsApiResult.Failure -> return sessionResult
}
if (boardId.isBlank()) {
return BoardsApiResult.Failure("Board id is required")
}
return apiClient.deleteBoard(session.baseUrl, session.apiKey, boardId)
}
private suspend fun session(): SessionSnapshot? {
val baseUrl = sessionStore.getBaseUrl()?.takeIf { it.isNotBlank() } ?: return null
private suspend fun session(): BoardsApiResult<SessionSnapshot> {
val baseUrl = sessionStore.getBaseUrl()?.takeIf { it.isNotBlank() }
?: return BoardsApiResult.Failure("Missing session. Please sign in again.")
val apiKey = withContext(ioDispatcher) {
apiKeyStore.getApiKey(baseUrl)
}.getOrNull()?.takeIf { it.isNotBlank() } ?: return null
}.getOrNull()?.takeIf { it.isNotBlank() }
?: return BoardsApiResult.Failure("Missing session. Please sign in again.")
return SessionSnapshot(baseUrl = baseUrl, apiKey = apiKey)
val workspaceId = when (val workspaceResult = resolveWorkspaceId(baseUrl = baseUrl, apiKey = apiKey)) {
is BoardsApiResult.Success -> workspaceResult.value
is BoardsApiResult.Failure -> return workspaceResult
}
return BoardsApiResult.Success(
SessionSnapshot(baseUrl = baseUrl, apiKey = apiKey, workspaceId = workspaceId),
)
}
private suspend fun resolveWorkspaceId(baseUrl: String, apiKey: String): BoardsApiResult<String> {
val storedWorkspaceId = sessionStore.getWorkspaceId()?.takeIf { it.isNotBlank() }
if (storedWorkspaceId != null) {
return BoardsApiResult.Success(storedWorkspaceId)
}
return when (val workspacesResult = apiClient.listWorkspaces(baseUrl, apiKey)) {
is BoardsApiResult.Success -> {
val first = workspacesResult.value.firstOrNull()?.id
?: return BoardsApiResult.Failure("No workspaces available for this account.")
sessionStore.saveWorkspaceId(first)
BoardsApiResult.Success(first)
}
is BoardsApiResult.Failure -> workspacesResult
}
}
private data class SessionSnapshot(
val baseUrl: String,
val apiKey: String,
val workspaceId: String,
)
}

View File

@@ -30,6 +30,7 @@ class BoardsRepositoryTest {
listBoardsResult = BoardsApiResult.Success(
listOf(BoardSummary("1", "Alpha"), BoardSummary("2", "Beta")),
)
workspacesResult = BoardsApiResult.Success(listOf(WorkspaceSummary("ws-1", "Main")))
}
val repository = BoardsRepository(
@@ -44,12 +45,74 @@ class BoardsRepositoryTest {
val boards = (result as BoardsApiResult.Success).value
assertEquals(2, boards.size)
assertEquals("Alpha", boards[0].title)
assertEquals("ws-1", fakeApi.lastWorkspaceId)
}
@Test
fun listBoardsUsesStoredWorkspaceWhenAvailable() = runTest {
val fakeApi = FakeBoardsApiClient().apply {
listBoardsResult = BoardsApiResult.Success(listOf(BoardSummary("1", "Alpha")))
}
val repository = BoardsRepository(
sessionStore = InMemorySessionStore("https://kan.bn/", workspaceId = "ws-stored"),
apiKeyStore = InMemoryApiKeyStore("api"),
apiClient = fakeApi,
)
repository.listBoards()
assertEquals("ws-stored", fakeApi.lastWorkspaceId)
assertEquals(0, fakeApi.listWorkspacesCalls)
}
@Test
fun listBoardsResolvesFirstWorkspaceWhenMissingInSession() = runTest {
val fakeApi = FakeBoardsApiClient().apply {
workspacesResult = BoardsApiResult.Success(
listOf(WorkspaceSummary("ws-first", "Alpha"), WorkspaceSummary("ws-second", "Beta")),
)
listBoardsResult = BoardsApiResult.Success(listOf(BoardSummary("1", "Alpha")))
}
val sessionStore = InMemorySessionStore("https://kan.bn/")
val repository = BoardsRepository(
sessionStore = sessionStore,
apiKeyStore = InMemoryApiKeyStore("api"),
apiClient = fakeApi,
)
val result = repository.listBoards()
assertTrue(result is BoardsApiResult.Success)
assertEquals("ws-first", sessionStore.getWorkspaceId())
assertEquals("ws-first", fakeApi.lastWorkspaceId)
assertEquals(1, fakeApi.listWorkspacesCalls)
}
@Test
fun listBoardsFailsWhenNoWorkspacesReturned() = runTest {
val fakeApi = FakeBoardsApiClient().apply {
workspacesResult = BoardsApiResult.Success(emptyList())
}
val repository = BoardsRepository(
sessionStore = InMemorySessionStore("https://kan.bn/"),
apiKeyStore = InMemoryApiKeyStore("api"),
apiClient = fakeApi,
)
val result = repository.listBoards()
assertTrue(result is BoardsApiResult.Failure)
assertEquals("No workspaces available for this account.", (result as BoardsApiResult.Failure).message)
}
@Test
fun createBoardTrimsNameAndPassesTemplateId() = runTest {
val fakeApi = FakeBoardsApiClient().apply {
createBoardResult = BoardsApiResult.Success(BoardSummary("33", "Roadmap"))
workspacesResult = BoardsApiResult.Success(listOf(WorkspaceSummary("ws-1", "Main")))
}
val repository = BoardsRepository(
@@ -63,12 +126,14 @@ class BoardsRepositoryTest {
assertTrue(result is BoardsApiResult.Success)
assertEquals("Roadmap", fakeApi.lastCreateName)
assertEquals("tpl-1", fakeApi.lastCreateTemplateId)
assertEquals("ws-1", fakeApi.lastCreateWorkspaceId)
}
@Test
fun deleteBoardPassesBoardIdToApi() = runTest {
val fakeApi = FakeBoardsApiClient().apply {
deleteBoardResult = BoardsApiResult.Success(Unit)
workspacesResult = BoardsApiResult.Success(listOf(WorkspaceSummary("ws-1", "Main")))
}
val repository = BoardsRepository(
@@ -83,15 +148,28 @@ class BoardsRepositoryTest {
assertEquals("42", fakeApi.lastDeletedId)
}
private class InMemorySessionStore(private var baseUrl: String?) : SessionStore {
private class InMemorySessionStore(
private var baseUrl: String?,
private var workspaceId: String? = null,
) : SessionStore {
override fun getBaseUrl(): String? = baseUrl
override fun saveBaseUrl(url: String) {
baseUrl = url
}
override fun getWorkspaceId(): String? = workspaceId
override fun saveWorkspaceId(workspaceId: String) {
this.workspaceId = workspaceId
}
override fun clearBaseUrl() {
baseUrl = null
}
override fun clearWorkspaceId() {
workspaceId = null
}
}
private class InMemoryApiKeyStore(private var apiKey: String?) : ApiKeyStore {
@@ -111,32 +189,54 @@ class BoardsRepositoryTest {
private class FakeBoardsApiClient : KanbnApiClient {
var listBoardsResult: BoardsApiResult<List<BoardSummary>> = BoardsApiResult.Success(emptyList())
var listTemplatesResult: BoardsApiResult<List<BoardTemplate>> = BoardsApiResult.Success(emptyList())
var workspacesResult: BoardsApiResult<List<WorkspaceSummary>> =
BoardsApiResult.Success(listOf(WorkspaceSummary("ws-1", "Main")))
var createBoardResult: BoardsApiResult<BoardSummary> = BoardsApiResult.Success(BoardSummary("new", "New"))
var deleteBoardResult: BoardsApiResult<Unit> = BoardsApiResult.Success(Unit)
var listWorkspacesCalls: Int = 0
var lastWorkspaceId: String? = null
var lastCreateName: String? = null
var lastCreateTemplateId: String? = null
var lastCreateWorkspaceId: String? = null
var lastDeletedId: String? = null
override suspend fun healthCheck(baseUrl: String, apiKey: String): AuthResult = AuthResult.Success
override suspend fun listBoards(baseUrl: String, apiKey: String): BoardsApiResult<List<BoardSummary>> {
override suspend fun listWorkspaces(
baseUrl: String,
apiKey: String,
): BoardsApiResult<List<WorkspaceSummary>> {
listWorkspacesCalls += 1
return workspacesResult
}
override suspend fun listBoards(
baseUrl: String,
apiKey: String,
workspaceId: String,
): BoardsApiResult<List<BoardSummary>> {
lastWorkspaceId = workspaceId
return listBoardsResult
}
override suspend fun listBoardTemplates(
baseUrl: String,
apiKey: String,
workspaceId: String,
): BoardsApiResult<List<BoardTemplate>> {
lastWorkspaceId = workspaceId
return listTemplatesResult
}
override suspend fun createBoard(
baseUrl: String,
apiKey: String,
workspaceId: String,
name: String,
templateId: String?,
): BoardsApiResult<BoardSummary> {
lastCreateWorkspaceId = workspaceId
lastCreateName = name
lastCreateTemplateId = templateId
return createBoardResult

View File

@@ -127,9 +127,17 @@ class BoardsViewModelTest {
baseUrl = url
}
override fun getWorkspaceId(): String? = "ws-1"
override fun saveWorkspaceId(workspaceId: String) {
}
override fun clearBaseUrl() {
baseUrl = null
}
override fun clearWorkspaceId() {
}
}
private class InMemoryApiKeyStore(private var apiKey: String?) : ApiKeyStore {
@@ -156,17 +164,26 @@ class BoardsViewModelTest {
override suspend fun healthCheck(baseUrl: String, apiKey: String): AuthResult = AuthResult.Success
override suspend fun listBoards(baseUrl: String, apiKey: String): BoardsApiResult<List<BoardSummary>> {
override suspend fun listBoards(
baseUrl: String,
apiKey: String,
workspaceId: String,
): BoardsApiResult<List<BoardSummary>> {
return listBoardsResult
}
override suspend fun listBoardTemplates(baseUrl: String, apiKey: String): BoardsApiResult<List<BoardTemplate>> {
override suspend fun listBoardTemplates(
baseUrl: String,
apiKey: String,
workspaceId: String,
): BoardsApiResult<List<BoardTemplate>> {
return listTemplatesResult
}
override suspend fun createBoard(
baseUrl: String,
apiKey: String,
workspaceId: String,
name: String,
templateId: String?,
): BoardsApiResult<BoardSummary> {