fix: scope boards API calls to resolved workspace
This commit is contained in:
@@ -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> {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -159,6 +159,7 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
if (saveKeyResult.isSuccess) {
|
||||
sessionStore.saveBaseUrl(normalizedBaseUrl)
|
||||
sessionStore.clearWorkspaceId()
|
||||
openBoards()
|
||||
} else {
|
||||
loginProgress.visibility = View.GONE
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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> {
|
||||
|
||||
Reference in New Issue
Block a user