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.
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.
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 ""
}
backend propertymockApi.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.
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> |
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().
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)
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)
// 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)
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)
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)
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)
Resources not seeded return lazily-constructed mock objects whose IDs match the path parameter. This means:
showCompany(42L) returns a mock Company with id = 42L and name = "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.
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"))
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.