Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Fixed
- NetworkMock: replaced ~35 unconditional `println` calls in `MockConfigRepository` and
`NetworkMockPlugin` with gated [Kermit](https://github.com/touchlab/Kermit) logging
(tag `DevViewNetworkMock`), consolidating the plugin's multi-line per-request trace into one
line per intercepted request. No response body content is ever logged. A host app can adjust
or silence this via `Logger.setMinSeverity(...)`; `devview-consolelogger`, if installed,
captures it automatically. Detekt's `ForbiddenMethodCall` rule (`println`/`print`) is now
enforced repo-wide. (`devview-networkmock-core`, `devview-networkmock-ktor`, #86)

## [0.2.0-alpha03] - 2026-09-11

### Added
Expand Down
2 changes: 1 addition & 1 deletion config/quality/detekt/default-config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -636,7 +636,7 @@ style:
allowedImports: [ ]
forbiddenImports: [ ]
ForbiddenMethodCall:
active: false
active: true
methods:
- reason: 'print does not allow you to configure the output stream. Use a logger instead.'
value: 'kotlin.io.print'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.worldline.devview.networkmock.core.repository

import co.touchlab.kermit.Logger
import com.worldline.devview.networkmock.core.NetworkMockResourceLoader
import com.worldline.devview.networkmock.core.model.MockConfiguration
import com.worldline.devview.networkmock.core.model.MockMatch
Expand All @@ -8,6 +9,8 @@ import com.worldline.devview.networkmock.core.model.OperationKey
import com.worldline.devview.networkmock.core.openapi.OpenApiParser
import kotlinx.serialization.SerializationException

private val logger = Logger.withTag(tag = "DevViewNetworkMock")

/**
* Repository for loading OpenAPI-based mock configuration and response files from resources.
*
Expand Down Expand Up @@ -68,20 +71,16 @@ public class MockConfigRepository(
cachedConfig = config
responseIndex = parsed.associate { it.apiSpec.id to it.responseIndex }

println(
message = "[NetworkMock][Config] Loaded ${config.specs.size} spec(s): " +
logger.d {
"Loaded ${config.specs.size} spec(s): " +
config.specs.joinToString { "${it.id} (${it.operations.size} operations)" }
)
}
Result.success(value = config)
} catch (e: IllegalStateException) {
println(
message = "[NetworkMock][Config] ERROR: Failed to load configuration - ${e.message}"
)
logger.w(throwable = e) { "Failed to load configuration" }
Result.failure(exception = e)
} catch (e: SerializationException) {
println(
message = "[NetworkMock][Config] ERROR: Failed to load configuration - ${e.message}"
)
logger.w(throwable = e) { "Failed to load configuration" }
Result.failure(exception = e)
}
}
Expand Down Expand Up @@ -127,15 +126,12 @@ public class MockConfigRepository(
}

if (match == null) {
println(message = "[NetworkMock][Matching] No match for $method $host$path")
logger.v { "No match for $method $host$path" }
return null
}

val (spec, matchingOperation) = match
println(
message = "[NetworkMock][Matching] Matched $method $path -> " +
"${spec.id}/${matchingOperation.operationId}"
)
logger.v { "Matched $method $path -> ${spec.id}/${matchingOperation.operationId}" }
return MockMatch(
key = OperationKey(specId = spec.id, operationId = matchingOperation.operationId),
config = matchingOperation,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
@file:Suppress("StringLiteralDuplication")

package com.worldline.devview.networkmock.ktor.plugin

import co.touchlab.kermit.Logger
import com.worldline.devview.networkmock.core.model.NetworkMockState
import com.worldline.devview.networkmock.core.model.OperationMockState
import io.ktor.client.HttpClient
Expand Down Expand Up @@ -34,7 +33,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch

private const val LOG_PREFIX = "[NetworkMock][Plugin]"
private val logger = Logger.withTag(tag = "DevViewNetworkMock")

/**
* Plugin configuration wrapper.
Expand Down Expand Up @@ -146,7 +145,7 @@ public val NetworkMockPlugin: HttpClientPlugin<NetworkMockConfig, NetworkMockPlu
val mockRepository = plugin.config.resolvedMockRepository()
val stateRepository = plugin.config.resolvedStateRepository()

println(message = "$LOG_PREFIX NetworkMock plugin installed successfully")
logger.d { "NetworkMock plugin installed" }

val cachedState = MutableStateFlow<NetworkMockState?>(value = null)
scope.launch {
Expand All @@ -164,144 +163,77 @@ public val NetworkMockPlugin: HttpClientPlugin<NetworkMockConfig, NetworkMockPlu
.entries()
.associate { (key, values) -> key to values }

println(message = "$LOG_PREFIX ========================================")
println(message = "$LOG_PREFIX Intercepted request: $method $host$path")

val currentState = cachedState.value ?: stateRepository.getState()

if (!currentState.globalMockingEnabled) {
println(
message = "$LOG_PREFIX Global mocking is DISABLED - using actual network"
)
println(message = "$LOG_PREFIX ========================================")
logger.d { "$method $path -> NETWORK (global mocking disabled)" }
return@intercept execute(requestBuilder = requestBuilder)
}

println(message = "$LOG_PREFIX Global mocking is ENABLED - checking for mock")

val mockMatch = mockRepository.findMatchingMock(
host = host,
path = path,
method = method,
queryParameters = queryParameters
)

mockMatch?.let { match ->
println(
message = "$LOG_PREFIX Found matching operation: " +
"${match.specId}/${match.operationId}"
)
if (mockMatch == null) {
logger.d { "$method $path -> NETWORK (no operation match)" }
return@intercept execute(requestBuilder = requestBuilder)
}

val endpointState = currentState.getOperationState(key = match.key)
val endpointState = currentState.getOperationState(key = mockMatch.key)

if (endpointState == null) {
println(
message = "$LOG_PREFIX No state found for operation key: ${match.key.compositeKey}"
)
println(
message = "$LOG_PREFIX Available operation states: ${currentState.operationStates.keys}"
)
println(message = "$LOG_PREFIX Using actual network")
println(message = "$LOG_PREFIX ========================================")
return@intercept execute(requestBuilder = requestBuilder)
if (endpointState == null) {
logger.d {
"$method $path -> NETWORK (${mockMatch.key.compositeKey} has no configured state)"
}
return@intercept execute(requestBuilder = requestBuilder)
}

println(
message =
"$LOG_PREFIX Operation state: ${
when (endpointState) {
is OperationMockState.Network -> "network"
is OperationMockState.Mock ->
"mock, status=${endpointState.statusCode}, " +
"example=${endpointState.exampleName}"
}
}"
)

when (endpointState) {
is OperationMockState.Network -> {
println(
message = "$LOG_PREFIX Endpoint mock not enabled"
)
println(message = "$LOG_PREFIX Using actual network")
println(
message = "$LOG_PREFIX ========================================"
)
}
is OperationMockState.Mock -> {
println(
message = "$LOG_PREFIX Mock is enabled with example: " +
endpointState.exampleName
when (endpointState) {
is OperationMockState.Network -> {
logger.d { "$method $path -> NETWORK (operation set to pass-through)" }
execute(requestBuilder = requestBuilder)
}
is OperationMockState.Mock -> {
@Suppress("TooGenericExceptionCaught")
try {
val mockResponse = mockRepository.loadMockResponse(
key = mockMatch.key,
statusCode = endpointState.statusCode,
exampleName = endpointState.exampleName
)

@Suppress("TooGenericExceptionCaught")
try {
val mockResponse = mockRepository.loadMockResponse(
key = match.key,
statusCode = endpointState.statusCode,
exampleName = endpointState.exampleName
)

mockResponse?.let { response ->
println(
message = "$LOG_PREFIX Successfully loaded mock response " +
"(status ${response.statusCode})"
)
println(
message = "$LOG_PREFIX Returning MOCK response - " +
"NO network call will be made"
)
println(
message = "$LOG_PREFIX ========================================"
)

match.delayMs?.let { ms ->
println(message = "$LOG_PREFIX Simulating delay of ${ms}ms")
delay(timeMillis = ms)
}

return@intercept createMockHttpClientCall(
client = scope,
requestData = request,
statusCode = HttpStatusCode.fromValue(
value = response.statusCode
),
content = response.content
)
if (mockResponse == null) {
logger.w {
"$method $path -> NETWORK (declared mock " +
"${endpointState.statusCode}/${endpointState.exampleName} not found)"
}
return@intercept execute(requestBuilder = requestBuilder)
}

if (mockResponse == null) {
println(
message = "$LOG_PREFIX ERROR: Mock response loaded as null"
)
println(message = "$LOG_PREFIX Falling back to actual network")
println(
message = "$LOG_PREFIX ========================================"
)
}
} catch (e: Exception) {
println(
message = "$LOG_PREFIX ERROR: Exception loading mock response - ${e.message}"
)
println(message = "$LOG_PREFIX Falling back to actual network")
println(
message = "$LOG_PREFIX ========================================"
)
mockMatch.delayMs?.let { ms -> delay(timeMillis = ms) }

logger.d {
"$method $path -> MOCK ${mockResponse.statusCode}/${mockResponse.exampleName}"
}
createMockHttpClientCall(
client = scope,
requestData = request,
statusCode = HttpStatusCode.fromValue(
value = mockResponse.statusCode
),
content = mockResponse.content
)
} catch (e: Exception) {
logger.w(
throwable = e
) { "$method $path -> NETWORK (error loading mock response)" }
execute(requestBuilder = requestBuilder)
}
}
}

if (mockMatch == null) {
println(message = "$LOG_PREFIX No matching endpoint config found")
println(message = "$LOG_PREFIX Using actual network")
}

println(
message = "$LOG_PREFIX No mock enabled for $method $path, using actual network"
)
println(message = "$LOG_PREFIX ========================================")
execute(requestBuilder = requestBuilder)
}
}
}
Expand Down
16 changes: 16 additions & 0 deletions docs/modules/networkmock-core.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,22 @@ State is persisted via `MockStateRepository`:

**Upgrading from a pre-0.2.0 release**: the operation-state key shape changed (`{groupId}-{environmentId}-{endpointId}` → `{specId}-{operationId}`), and so did the `Mock` payload (a response file name → `(statusCode, exampleName)`). On first launch after upgrading, every `network_mock_endpoint_*` entry from the old shape is wiped once — this is disabled-by-default developer-tooling state, not user data, so previously-selected mocks are reset rather than translated. The global mocking toggle is unaffected. See the [migration guide](../guides/migrating-to-openapi.md) for converting an existing `mocks.json`.

## Logging

`MockConfigRepository` logs spec-load outcomes through [Kermit](https://github.com/touchlab/Kermit), tagged `DevViewNetworkMock` — the same tag used by `devview-networkmock` and `devview-networkmock-ktor`, so a host can filter every NetworkMock-related log line by tag regardless of which module emitted it. Spec load success logs at `debug`, load failures at `warn` with the causing throwable attached. Request-matching outcomes (`findMatchingMock`) log at `verbose`, since they fire on every intercepted request.

No response body content is ever logged. To silence NetworkMock's logs (or raise/lower their verbosity) in a host app, configure Kermit directly — this module adds no separate on/off flag of its own:

```kotlin
import co.touchlab.kermit.Logger
import co.touchlab.kermit.Severity

// Silence everything below warnings, repo-wide (affects every Kermit-backed DevView module)
Logger.setMinSeverity(Severity.Warn)
```

`devview-consolelogger`, if installed, captures these logs into DevView's own in-app console screen for free — no extra wiring needed.

## NetworkMockResourceLoader

_Added in v0.1.3._
Expand Down
2 changes: 2 additions & 0 deletions docs/modules/networkmock-ktor.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ For every outgoing request, the plugin:

Mock responses are returned with HTTP/1.1 status, an empty header set, and the response body as the content.

Each intercepted request logs exactly one line through [Kermit](https://github.com/touchlab/Kermit) (tag `DevViewNetworkMock`, `debug` level, `warn` for a failed mock load) — e.g. `GET /v1/users/42 -> MOCK 200/default` or `-> NETWORK (no operation match)`. No response body content is ever logged. See [Logging](networkmock-core.md#logging) for how to adjust verbosity or route these into `devview-consolelogger`.

## Platform Actuals

- **Android**: Use `HttpClient(OkHttp)` as the engine.
Expand Down
Loading