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 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, including mock building/floor metadata (stored for consistency on subsequent calls).
  • showCompanyDetailed(companyId) returns a CompanyDetailed whose sections summary reflects the current backend.sections entries 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.


Solibo AS