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,
buildingId = 10L,
hNr = "H0201",
identifier = "A-201",
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.
When you use the section endpoints instead of seeding the map manually, MockHomeApi now also fills in richer section metadata such as building, floor, hNr, unitType, and fraction fields. showCompanyDetailed(companyId) derives its sections summary from the current contents of backend.sections, so create/update/delete section tests can verify live counts without extra setup.
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, including mock building/floor metadata (stored for consistency on subsequent calls).showCompanyDetailed(companyId)returns aCompanyDetailedwhosesectionssummary reflects the currentbackend.sectionsentries for that company.
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.