MockHomeApi — In-Memory Testing Guide
MockHomeApi is a complete in-memory mock of the Solibo Home backend. It intercepts all HTTP traffic with a MockEngine and maintains stateful maps so tests can create data, mutate it, and verify the result across multiple API calls — without touching a real server.
Instantiating
val mockApi = MockHomeApi(fingerprinter, device, settings = MapSettings())
mockApi.setInitialBaseUrl("https://api.example.com")
A minimal Fingerprinter and DeviceManager are required. MapSettings (from com.russhwolf:multiplatform-settings) gives an in-memory settings store. The URL must be set before any API call.
Minimal test stubs
private val fingerprinter = object : Fingerprinter {
override fun print(username: String) = "fingerprint"
}
private val device = object : DeviceManager {
override fun store(result: SoliboAuthentication) {}
override fun clear(userId: String) {}
override fun key(username: String): String? = "device-key"
override fun group(username: String): String? = "device-group"
override suspend fun token(): String? = "token"
override suspend fun generateVerification(
userId: String, password: String, deviceKey: String?, deviceGroupKey: String?
): Pair<String, String> = "" to ""
}
The backend property
mockApi.backend is the MockBackend instance. It exposes all state maps as public vals so tests can seed data directly.
// Seed a section before the API call
mockApi.backend.sections[500L] = Section(
id = 500L,
companyId = 400L,
classification = SectionType.BOLIG,
isBusiness = false,
)
val response = mockApi.sections.indexSection(400L, null, null, null, null)
assertEquals(1, response.body().items.size)
This pattern — write to the map, then call the API — works for any resource.
State maps reference
All maps live on MockBackend. Keys and value types:
| Map | Key | Value |
|---|---|---|
users | userId: String | AuthUser |
companies | companyId: Long | Company |
tasks | taskId: Long | Task |
issues | issueId: Long | Issue |
issueComments | issueId: Long | MutableList<IssueComment> |
documents | documentId: Long | Document |
sections | sectionId: Long | Section |
conversations | conversationId: Long | Conversation |
meetings | meetingId: Long | MeetingDetails |
persons | personId: Long | Person |
organizations | orgId: Long | Organization |
invoicePlans | planId: Long | InvoicePlan |
accounts | companyId: Long | MutableList<Account> |
residentsPerCompany | companyId: Long | MutableList<Resident> |
boardMembersPerCompany | companyId: Long | MutableList<Person> |
boardMemberPersonnummers | personId: Long | BoardmemberPersonnummer |
suppliersPerCompany | companyId: Long | MutableList<SupplierForCompany> |
expensesPerCompany | companyId: Long | MutableList<Expense> |
smsBroadcastsPerCompany | companyId: Long | MutableList<SMSBroadcast> |
invoicesPerCompany | companyId: Long | MutableList<Invoice> |
loansPerCompany | companyId: Long | MutableList<Loan> |
settlementsPerCompany | companyId: Long | MutableList<Settlement> |
settlementConfigurationsPerCompany | companyId: Long | MutableList<SettlementProviderConfiguration> |
customCostsPerSettlement | settlementId: Long | MutableList<SettlementCustomCost> |
storageRoomsPerCompany | companyId: Long | MutableList<StorageRoom> |
parkingSpacesPerCompany | companyId: Long | MutableList<ParkingSpace> |
insurancesPerCompany | companyId: Long | MutableList<Insurance> |
routinesPerCompany | companyId: Long | MutableList<Routine> |
newslettersPerCompany | companyId: Long | MutableList<Newsletter> |
postsPerCompany | companyId: Long | MutableList<Post> |
practicalInfoPerCompany | companyId: Long | MutableList<Post> |
homepagesPerCompany | companyId: Long | Homepage |
hmsSettingsPerCompany | companyId: Long | HmsSettings |
invoicePlanLinesPerPlan | planId: Long | MutableList<InvoicePlanLine> |
invoicePlanDistributionsPerPlan | planId: Long | MutableList<InvoicePlanDistribution> |
messagesPerConversation | conversationId: Long | MutableList<MessageInConversation> |
conversationCategoriesPerCompany | companyId: Long | MutableList<SecureConversationCategory> |
organizationEmployeesPerOrg | orgId: Long | MutableList<OrganizationEmployee> |
contactsPerSupplier | supplierId: Long | MutableList<Person> |
supplierCommentsPerSupplier | supplierId: Long | MutableList<SupplierComment> |
logsPerLoan | loanId: Long | MutableList<LoanLog> |
companyDetailedPerCompany | companyId: Long | CompanyDetailed |
economicReportsPerCompany | companyId: Long | MutableList<EconomicReport> |
tagsPerSection | sectionId: Long | MutableList<SectionTag> |
attendancePerResident | residentId: Long | MutableList<Attendance> |
issueSections | issueId: Long | MutableList<Long> (sectionIds) |
issueConversations | issueId: Long | MutableList<Long> (conversationIds) |
countries | — | MutableList<Country> |
capturedRequests | — | MutableList<HttpRequestData> |
Prefilling
Default state (always present)
MockBackend always seeds two companies (id = 1 and id = 2) and a default user (username = "mockuser") on construction. indexCompany() therefore returns 2 items out of the box.
prefillResources()
Calling mockApi.backend.prefillResources() (or passing prefill = true to the constructor) seeds one of each resource type for companyId = 1: a task, issue, section, meeting, residents, settlement, etc. Use this when a test needs a complete pre-populated world without caring about specific IDs.
val mockApi = MockHomeApi(fingerprinter, device, settings = MapSettings(), prefill = true)
mockApi.setInitialBaseUrl("https://api.example.com")
// Everything for company 1 is ready:
val tasks = mockApi.task.indexTasks(1L).body()
assertEquals(1, tasks.items.size)
reset()
Between tests (or within a single test to clear state):
mockApi.backend.reset()
This clears all maps, resets all ID counters to 1000L, and re-seeds the default companies and user. It does not re-run prefillResources().
Patterns
Create via API, then verify
val created = mockApi.task.createTask(
1L,
CreateTaskCommand(title = "My Task", description = "Details"),
).body()
val pagedTasks = mockApi.task.indexTasks(1L).body()
assertEquals(1, pagedTasks.items.size)
assertEquals("My Task", pagedTasks.items[0].title)
assertEquals(created.id, pagedTasks.items[0].id)
Seed via map, then verify via API
mockApi.backend.tasks[999L] = Task(
id = 999L,
companyId = 1L,
title = "Pre-seeded",
log = emptyList(),
)
val task = mockApi.task.showTask(1L, 999L).body()
assertEquals("Pre-seeded", task.title)
Mutate then verify
// Approve an expense and confirm status changes
val expense = mockApi.expense.createExpense(
1L,
CreateExpenseCommand(amount = 100.0, expenseTypeId = 1L),
).body()
mockApi.expense.acceptExpense(1L, expense.id)
val updated = mockApi.expense.showExpense(1L, expense.id).body()
assertEquals(ExpenseStatus.APPROVED, updated.status)
Verify cross-company isolation
val companyId = 100L
val otherId = 200L
mockApi.backend.tasks[1L] = Task(id = 1L, companyId = companyId, title = "Company A", log = emptyList())
mockApi.backend.tasks[2L] = Task(id = 2L, companyId = otherId, title = "Company B", log = emptyList())
assertEquals(1, mockApi.task.indexTasks(companyId).body().items.size)
assertEquals(1, mockApi.task.indexTasks(otherId).body().items.size)
Test delete
val id = mockApi.settlement.createSettlement(1L, CreateSettlementCommand(...)).body().id
assertEquals(1, mockApi.settlement.indexSettlements(1L).body().items.size)
mockApi.settlement.deleteSettlement(1L, id)
assertEquals(0, mockApi.settlement.indexSettlements(1L).body().items.size)
Test config update
val configId = mockApi.settlement.createSettlementConfiguration(
1L,
CreateSettlementProviderConfigurationCommand(startDate = LocalDate(2025, 1, 1), resolverPrefix = "NBBL"),
).body().id
mockApi.settlement.updateSettlementConfiguration(
1L, configId,
UpdateSettlementProviderConfigurationCommand(
startDate = LocalDate(2025, 1, 1),
active = false,
),
)
val config = mockApi.settlement.showSettlementConfiguration(1L, configId).body()
assertEquals(false, config.active)
Fallback behaviour
Resources not seeded return lazily-constructed mock objects whose IDs match the path parameter. This means:
showCompany(42L)returns a mockCompanywithid = 42Landname = "Mock Company 42".indexSection(companyId)with no matching sections returns a list with one auto-generated section for that company (stored for consistency on subsequent calls).
This prevents tests from failing on unrelated API calls they don’t care about — they always get a valid response.
Inspecting captured requests
All HTTP calls are recorded:
mockApi.backend.capturedRequests.clear()
mockApi.task.createTask(1L, CreateTaskCommand(title = "T"))
assertEquals(1, mockApi.backend.capturedRequests.size)
assertEquals("POST", mockApi.backend.capturedRequests[0].method.value)
assertTrue(mockApi.backend.capturedRequests[0].url.fullPath.contains("/tasks"))
Running tests
The SDK test suite uses Kotlin/JS (Node target):
./gradlew :sdk:jsNodeTest
Tests are in sdk/src/commonTest/kotlin/no/solibo/oss/sdk/api/MockHomeApiTest.kt. Each @Test creates its own MockHomeApi instance so tests are fully isolated from each other.