Why a test, not a feature

When I decided to contribute to supabase-kt — the Kotlin Multiplatform client for Supabase — I didn’t start by hunting for a big feature. Big features in an unfamiliar codebase mean long reviews, design debates, and a high chance of “thanks, but we’re going a different direction.” I wanted something small, self-contained, and obviously useful: the kind of change a maintainer can read in one sitting and merge with confidence.

So I went looking for gaps the maintainer had already acknowledged. That’s the cheapest possible signal that a contribution is wanted.

Finding the task

I scanned the repository for the markers that maintainers leave when they know something is missing: TODO, FIXME, open issues tagged “good first issue,” and tests that existed for one half of an API but not the other.

One line stood out, in Storage/src/commonTest/kotlin/BucketApiFlowTest.kt:

//TODO: Add tests for downloading as flow

It was perfect. Right above it sat a complete set of upload-as-flow tests — byte-array uploads, channel uploads, upserts, signed-URL uploads. The download side of the same API (downloadAuthenticatedAsFlow, downloadPublicAsFlow) had zero coverage. The maintainer had literally written down the task and left a template right next to it.

The approach

The whole strategy was mirror, don’t invent. The upload tests already established the house style: a mocked Supabase client, Ktor’s MockEngine for HTTP, and Turbine to assert on the emitted Flow statuses. I followed the same shape so the new tests would read as if the original author had written them.

I added four tests covering the two download functions across their two input modes:

The ByteArray variant asserts the full status sequence — Progress → Success → ByteData — and checks the decoded bytes. The channel variant asserts Progress → Success (no ByteData is emitted when streaming) and then verifies the bytes actually written to the channel.

The interesting part: downloads aren’t uploads

This is where a “mechanical” task taught me something. My first instinct was to copy the upload assertion verbatim, including this check on the progress event:

assertEquals(expectedData.size.toLong(), progressStatus.contentLength)

It passed for uploads. It failed for downloads, reporting contentLength = 0. Why?

Both progress callbacks have the same shape — (bytesTransferred, contentLength) — but the contentLength comes from opposite ends of the wire:

A second wrinkle: for streaming (channel) downloads, the progress callback fires at engine-chosen points, so a streamed download can emit several Progress statuses, and the final byte count isn’t guaranteed to arrive before Success. I wrote a small awaitProgress() helper that drains progress emissions and hands back the terminal status, rather than assuming exactly one.

The fix for the contentLength assertion was the realistic one: add a Content-Length header to the mocked response, the way a real server does. That kept the assertion meaningful instead of just deleting it.

Verifying before claiming “done”

Kotlin Multiplatform has no single test task — the plugin registers one per target (jvmTest, iosX64Test, macosArm64Test, …) plus an allTests umbrella. I ran the JVM suite locally:

./gradlew -DLibrariesOnly=true :storage-kt:jvmTest --tests "BucketApiFlowTest"

I ran it several times to confirm the streaming tests weren’t flaky, and checked that detekt (the project’s linter) stayed clean.

The pull request

I opened the PR from my fork into the project’s master branch, kept the description short and factual, and was upfront about how the work was done. The maintainer merged it.

What I took away

Small scope, real learning, merged code. A good first step into an open-source project.