solibo-sdk

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:

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.