Skip to content

Commit c65392d

Browse files
jpicklykclaude
andcommitted
refactor(test): create shared test infrastructure — TestBase, fixtures, and helpers
Add reusable test utilities under current/src/test/.../test/: - TestFixtures.kt: makeItem(), blocksDep(), makeNote(), JSON param helpers, response extractors - BaseRepositoryTest.kt: H2 in-memory DB base class with createPersistedItem/Note/Dependency - MockRepositoryProvider.kt: MockK-based RepositoryProvider factory with context() builder - TestNoteSchemaService.kt: In-memory NoteSchemaService with FEATURE_IMPLEMENTATION/BUG_FIX presets - TestInfrastructureTest.kt: 27 validation tests covering all shared infrastructure Migrated SQLiteWorkItemRepositoryTest to extend BaseRepositoryTest as proof of concept. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 46340f4 commit c65392d

File tree

6 files changed

+670
-13
lines changed

6 files changed

+670
-13
lines changed

current/src/test/kotlin/io/github/jpicklyk/mcptask/current/infrastructure/database/repository/SQLiteWorkItemRepositoryTest.kt

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,9 @@ import io.github.jpicklyk.mcptask.current.domain.model.Role
55
import io.github.jpicklyk.mcptask.current.domain.model.WorkItem
66
import io.github.jpicklyk.mcptask.current.domain.repository.RepositoryError
77
import io.github.jpicklyk.mcptask.current.domain.repository.Result
8-
import io.github.jpicklyk.mcptask.current.infrastructure.database.DatabaseManager
9-
import io.github.jpicklyk.mcptask.current.infrastructure.database.schema.management.DirectDatabaseSchemaManager
10-
import io.github.jpicklyk.mcptask.current.infrastructure.repository.SQLiteWorkItemRepository
8+
import io.github.jpicklyk.mcptask.current.domain.repository.WorkItemRepository
9+
import io.github.jpicklyk.mcptask.current.test.BaseRepositoryTest
1110
import kotlinx.coroutines.runBlocking
12-
import org.jetbrains.exposed.v1.jdbc.Database
1311
import org.junit.jupiter.api.BeforeEach
1412
import org.junit.jupiter.api.Test
1513
import java.util.UUID
@@ -18,19 +16,14 @@ import kotlin.test.assertIs
1816
import kotlin.test.assertNotNull
1917
import kotlin.test.assertTrue
2018

21-
class SQLiteWorkItemRepositoryTest {
19+
class SQLiteWorkItemRepositoryTest : BaseRepositoryTest() {
2220

23-
private lateinit var database: Database
24-
private lateinit var databaseManager: DatabaseManager
25-
private lateinit var repository: SQLiteWorkItemRepository
21+
private lateinit var repository: WorkItemRepository
2622

2723
@BeforeEach
2824
fun setUp() {
29-
val dbName = "test_${System.nanoTime()}"
30-
database = Database.connect("jdbc:h2:mem:$dbName;DB_CLOSE_DELAY=-1", driver = "org.h2.Driver")
31-
databaseManager = DatabaseManager(database)
32-
DirectDatabaseSchemaManager().updateSchema()
33-
repository = SQLiteWorkItemRepository(databaseManager)
25+
// Base class setUpDatabase() runs first via @BeforeEach ordering
26+
repository = repositoryProvider.workItemRepository()
3427
}
3528

3629
// --- CRUD ---
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package io.github.jpicklyk.mcptask.current.test
2+
3+
import io.github.jpicklyk.mcptask.current.domain.model.*
4+
import io.github.jpicklyk.mcptask.current.domain.repository.Result
5+
import io.github.jpicklyk.mcptask.current.infrastructure.database.DatabaseManager
6+
import io.github.jpicklyk.mcptask.current.infrastructure.database.schema.management.DirectDatabaseSchemaManager
7+
import io.github.jpicklyk.mcptask.current.infrastructure.repository.DefaultRepositoryProvider
8+
import org.jetbrains.exposed.v1.jdbc.Database
9+
import org.junit.jupiter.api.BeforeEach
10+
import java.util.UUID
11+
12+
/**
13+
* Base class for repository integration tests.
14+
*
15+
* Sets up a fresh H2 in-memory database before each test with the full schema applied,
16+
* and provides convenience methods for creating persisted entities.
17+
*/
18+
abstract class BaseRepositoryTest {
19+
protected lateinit var database: Database
20+
protected lateinit var databaseManager: DatabaseManager
21+
protected lateinit var repositoryProvider: DefaultRepositoryProvider
22+
23+
@BeforeEach
24+
fun setUpDatabase() {
25+
val dbName = "test_${System.nanoTime()}"
26+
database = Database.connect("jdbc:h2:mem:$dbName;DB_CLOSE_DELAY=-1", driver = "org.h2.Driver")
27+
databaseManager = DatabaseManager(database)
28+
DirectDatabaseSchemaManager().updateSchema()
29+
repositoryProvider = DefaultRepositoryProvider(databaseManager)
30+
}
31+
32+
/** Create and persist a WorkItem, returning the persisted copy. */
33+
protected suspend fun createPersistedItem(
34+
title: String = "Test Item",
35+
role: Role = Role.QUEUE,
36+
parentId: UUID? = null,
37+
depth: Int = if (parentId != null) 1 else 0,
38+
tags: String? = null,
39+
priority: Priority = Priority.MEDIUM
40+
): WorkItem {
41+
val item = makeItem(
42+
title = title,
43+
role = role,
44+
parentId = parentId,
45+
depth = depth,
46+
tags = tags,
47+
priority = priority
48+
)
49+
val result = repositoryProvider.workItemRepository().create(item)
50+
return (result as Result.Success).data
51+
}
52+
53+
/** Create and persist a Note. */
54+
protected suspend fun createPersistedNote(
55+
itemId: UUID,
56+
key: String = "test-note",
57+
role: String = "queue",
58+
body: String = "Test body"
59+
): Note {
60+
val note = makeNote(itemId = itemId, key = key, role = role, body = body)
61+
val result = repositoryProvider.noteRepository().upsert(note)
62+
return (result as Result.Success).data
63+
}
64+
65+
/** Create and persist a Dependency. */
66+
protected fun createPersistedDependency(
67+
fromItemId: UUID,
68+
toItemId: UUID,
69+
type: DependencyType = DependencyType.BLOCKS,
70+
unblockAt: String? = null
71+
): Dependency {
72+
val dep = Dependency(fromItemId = fromItemId, toItemId = toItemId, type = type, unblockAt = unblockAt)
73+
return repositoryProvider.dependencyRepository().create(dep)
74+
}
75+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package io.github.jpicklyk.mcptask.current.test
2+
3+
import io.github.jpicklyk.mcptask.current.application.service.NoteSchemaService
4+
import io.github.jpicklyk.mcptask.current.application.service.NoOpNoteSchemaService
5+
import io.github.jpicklyk.mcptask.current.application.service.WorkTreeExecutor
6+
import io.github.jpicklyk.mcptask.current.application.tools.ToolExecutionContext
7+
import io.github.jpicklyk.mcptask.current.domain.repository.DependencyRepository
8+
import io.github.jpicklyk.mcptask.current.domain.repository.NoteRepository
9+
import io.github.jpicklyk.mcptask.current.domain.repository.Result
10+
import io.github.jpicklyk.mcptask.current.domain.repository.RoleTransitionRepository
11+
import io.github.jpicklyk.mcptask.current.domain.repository.WorkItemRepository
12+
import io.github.jpicklyk.mcptask.current.infrastructure.repository.RepositoryProvider
13+
import io.mockk.coEvery
14+
import io.mockk.every
15+
import io.mockk.mockk
16+
17+
/**
18+
* Creates a fully-mocked RepositoryProvider with individual repository mocks accessible.
19+
* Eliminates the 10+ lines of MockK boilerplate repeated across tool tests.
20+
*/
21+
class MockRepositoryProvider {
22+
val workItemRepo: WorkItemRepository = mockk()
23+
val noteRepo: NoteRepository = mockk()
24+
val depRepo: DependencyRepository = mockk()
25+
val roleTransitionRepo: RoleTransitionRepository = mockk()
26+
val workTreeExecutor: WorkTreeExecutor = mockk()
27+
val provider: RepositoryProvider = mockk()
28+
29+
init {
30+
every { provider.workItemRepository() } returns workItemRepo
31+
every { provider.noteRepository() } returns noteRepo
32+
every { provider.dependencyRepository() } returns depRepo
33+
every { provider.roleTransitionRepository() } returns roleTransitionRepo
34+
every { provider.database() } returns null
35+
every { provider.workTreeExecutor() } returns workTreeExecutor
36+
// Default: noteRepo returns empty lists for any query
37+
coEvery { noteRepo.findByItemId(any()) } returns Result.Success(emptyList())
38+
coEvery { noteRepo.findByItemId(any(), any()) } returns Result.Success(emptyList())
39+
}
40+
41+
/** Build a ToolExecutionContext with optional schema service. */
42+
fun context(noteSchemaService: NoteSchemaService = NoOpNoteSchemaService): ToolExecutionContext =
43+
ToolExecutionContext(provider, noteSchemaService)
44+
}
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
package io.github.jpicklyk.mcptask.current.test
2+
3+
import io.github.jpicklyk.mcptask.current.domain.model.*
4+
import kotlinx.serialization.json.*
5+
import java.util.UUID
6+
import kotlin.test.assertEquals
7+
import kotlin.test.assertIs
8+
import kotlin.test.assertTrue
9+
10+
// ── WorkItem builder ──
11+
12+
fun makeItem(
13+
id: UUID = UUID.randomUUID(),
14+
title: String = "Test Item",
15+
role: Role = Role.QUEUE,
16+
previousRole: Role? = null,
17+
parentId: UUID? = null,
18+
depth: Int = if (parentId != null) 1 else 0,
19+
priority: Priority = Priority.MEDIUM,
20+
complexity: Int? = null,
21+
tags: String? = null,
22+
statusLabel: String? = null,
23+
description: String? = null,
24+
summary: String = "",
25+
metadata: String? = null,
26+
requiresVerification: Boolean = false
27+
): WorkItem = WorkItem(
28+
id = id,
29+
title = title,
30+
role = role,
31+
previousRole = previousRole,
32+
parentId = parentId,
33+
depth = depth,
34+
priority = priority,
35+
complexity = complexity,
36+
tags = tags,
37+
statusLabel = statusLabel,
38+
description = description,
39+
summary = summary,
40+
metadata = metadata,
41+
requiresVerification = requiresVerification
42+
)
43+
44+
// ── Dependency builders ──
45+
46+
fun blocksDep(fromItemId: UUID, toItemId: UUID, unblockAt: String? = null): Dependency =
47+
Dependency(
48+
fromItemId = fromItemId,
49+
toItemId = toItemId,
50+
type = DependencyType.BLOCKS,
51+
unblockAt = unblockAt
52+
)
53+
54+
fun relatesDep(fromItemId: UUID, toItemId: UUID): Dependency =
55+
Dependency(
56+
fromItemId = fromItemId,
57+
toItemId = toItemId,
58+
type = DependencyType.RELATES_TO
59+
)
60+
61+
// ── Note builder ──
62+
63+
fun makeNote(
64+
itemId: UUID,
65+
key: String = "test-note",
66+
role: String = "queue",
67+
body: String = "Test body"
68+
): Note = Note(
69+
itemId = itemId,
70+
key = key,
71+
role = role,
72+
body = body
73+
)
74+
75+
// ── JSON param helpers ──
76+
77+
fun params(vararg pairs: Pair<String, JsonElement>): JsonObject =
78+
buildJsonObject { pairs.forEach { (k, v) -> put(k, v) } }
79+
80+
fun transitionObj(itemId: UUID, trigger: String, summary: String? = null): JsonObject =
81+
buildJsonObject {
82+
put("itemId", itemId.toString())
83+
put("trigger", trigger)
84+
summary?.let { put("summary", it) }
85+
}
86+
87+
fun buildTransitionParams(vararg transitions: JsonObject): JsonObject =
88+
buildJsonObject {
89+
put("transitions", buildJsonArray { transitions.forEach { add(it) } })
90+
}
91+
92+
// ── Response extraction helpers ──
93+
94+
/**
95+
* Asserts that the JSON result has `success=true` and returns the `data` object.
96+
*/
97+
fun extractSuccessData(result: JsonElement): JsonObject {
98+
val obj = result.jsonObject
99+
assertTrue(obj["success"]?.jsonPrimitive?.boolean == true, "Expected success=true but got: $obj")
100+
return obj["data"]!!.jsonObject
101+
}
102+
103+
/**
104+
* Extracts `data.results` as a JsonArray from a successful response.
105+
*/
106+
fun extractResults(result: JsonElement): JsonArray {
107+
val data = extractSuccessData(result)
108+
return data["results"]!!.jsonArray
109+
}
110+
111+
/**
112+
* Extracts `data.summary` as a JsonObject from a successful response.
113+
*/
114+
fun extractSummary(result: JsonElement): JsonObject {
115+
val data = extractSuccessData(result)
116+
return data["summary"]!!.jsonObject
117+
}
118+
119+
/**
120+
* Asserts that the JSON result represents an error response (success=false).
121+
* Optionally checks that the error message contains [expectedMessage].
122+
* Returns the full response object for further assertions.
123+
*/
124+
fun assertErrorResponse(result: JsonElement, expectedMessage: String? = null): JsonObject {
125+
val obj = result.jsonObject
126+
assertTrue(obj["success"]?.jsonPrimitive?.boolean == false, "Expected success=false but got: $obj")
127+
expectedMessage?.let { expected ->
128+
val msg = obj["error"]?.jsonPrimitive?.content ?: obj["message"]?.jsonPrimitive?.content ?: ""
129+
assertTrue(msg.contains(expected, ignoreCase = true),
130+
"Expected error message to contain '$expected' but got: '$msg'")
131+
}
132+
return obj
133+
}

0 commit comments

Comments
 (0)