diff options
| author | Murilo Ijanc <[email protected]> | 2026-07-26 12:19:13 -0300 |
|---|---|---|
| committer | Murilo Ijanc <[email protected]> | 2026-07-26 12:19:13 -0300 |
| commit | 0757164507a1f9be8b9f61292a9069759f1da034 (patch) | |
| tree | e958ba14c7f6a6ae3cdabe1ed0cd5f69b530edb7 /src/test | |
| download | kotlin-0.1.0.tar.gz | |
Initial importv0.1.0
Diffstat (limited to 'src/test')
| -rw-r--r-- | src/test/kotlin/org/cepis/BatchTest.kt | 110 | ||||
| -rw-r--r-- | src/test/kotlin/org/cepis/ConfigurationTest.kt | 42 | ||||
| -rw-r--r-- | src/test/kotlin/org/cepis/FakeServer.kt | 98 | ||||
| -rw-r--r-- | src/test/kotlin/org/cepis/HealthTest.kt | 36 | ||||
| -rw-r--r-- | src/test/kotlin/org/cepis/LookupTest.kt | 128 | ||||
| -rw-r--r-- | src/test/kotlin/org/cepis/NormalizationTest.kt | 26 | ||||
| -rw-r--r-- | src/test/kotlin/org/cepis/ParserTest.kt | 86 | ||||
| -rw-r--r-- | src/test/kotlin/org/cepis/RetryTest.kt | 99 | ||||
| -rw-r--r-- | src/test/kotlin/org/cepis/ServerTest.kt | 51 |
9 files changed, 676 insertions, 0 deletions
diff --git a/src/test/kotlin/org/cepis/BatchTest.kt b/src/test/kotlin/org/cepis/BatchTest.kt new file mode 100644 index 0000000..67aab2d --- /dev/null +++ b/src/test/kotlin/org/cepis/BatchTest.kt @@ -0,0 +1,110 @@ +package org.cepis + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull + +internal class BatchTest : ServerTest() { + @Test + fun `preserves order and marks missing`() { + val body = + Fixtures.csv( + "01001000,Praça da Sé,lado ímpar,,Sé,São Paulo,SP,São Paulo," + + "Sudeste,3550308,1004,11,7107", + "99999999,,,,,,,,,,,,", + "20040020,Avenida Rio Branco,,,Centro,Rio de Janeiro,RJ," + + "Rio de Janeiro,Sudeste,3304557,,21,6001", + ) + server.respond("/csv?ceps=01001000%2C99999999%2C20040020", Reply(200, body)) + val result = client.lookupMany(listOf("01001000", "99999999", "20040020")) + + assertEquals(3, result.size) + assertEquals("São Paulo", assertNotNull(result[0]).localidade) + assertNull(result[1]) + assertEquals("RJ", assertNotNull(result[2]).uf) + } + + @Test + fun `batches carry no coordinate`() { + val body = + Fixtures.csv("01001000,Praça da Sé,,,Sé,São Paulo,SP,São Paulo,Sudeste,3550308,,11,1") + server.respond("/csv?ceps=01001000", Reply(200, body)) + + assertNull(assertNotNull(client.lookupMany(listOf("01001000"))[0]).geo) + } + + @Test + fun `field with a comma is parsed`() { + val body = + Fixtures.csv( + """01001000,"Rua Um, Dois",,,Centro,São Paulo,SP,São Paulo,Sudeste,1,,11,1""", + ) + server.respond("/csv?ceps=01001000", Reply(200, body)) + + assertEquals( + "Rua Um, Dois", + assertNotNull(client.lookupMany(listOf("01001000"))[0]).logradouro, + ) + } + + @Test + fun `field with an escaped quote is parsed`() { + val body = + Fixtures.csv( + "01001000,\"Rua \"\"Dois\"\"\",,,Centro,São Paulo,SP,São Paulo,Sudeste,1,,11,1", + ) + server.respond("/csv?ceps=01001000", Reply(200, body)) + + assertEquals( + "Rua \"Dois\"", + assertNotNull(client.lookupMany(listOf("01001000"))[0]).logradouro, + ) + } + + @Test + fun `empty list makes no request`() { + assertEquals(emptyList<Address?>(), client.lookupMany(emptyList())) + assertEquals(0, server.calls.size) + } + + @Test + fun `chunks above the api limit`() { + val ceps = (0 until 501).map { "%08d".format(it) } + val first = ceps.take(500).map { "$it,,,,,X,SP,São Paulo,Sudeste,1,,11,1" } + val second = ceps.drop(500).map { "$it,,,,,X,SP,São Paulo,Sudeste,1,,11,1" } + var seen = 0 + + server.responder = { + seen += 1 + val rows = if (seen == 1) first else second + Reply(200, Fixtures.csv(*rows.toTypedArray())) + } + val result = client.lookupMany(ceps) + + assertEquals(2, seen) + assertEquals(501, result.size) + assertEquals("X", assertNotNull(result[500]).localidade) + } + + @Test + fun `truncated response does not shift`() { + val body = Fixtures.csv("01001000,,,,,São Paulo,SP,São Paulo,Sudeste,1,,11,1") + server.respond("/csv?ceps=01001000%2C20040020", Reply(200, body)) + val result = client.lookupMany(listOf("01001000", "20040020")) + + assertEquals(2, result.size) + assertEquals("São Paulo", assertNotNull(result[0]).localidade) + assertNull(result[1]) + } + + @Test + fun `malformed cep is null rather than an error`() { + val body = Fixtures.csv("01001000,,,,,São Paulo,SP,São Paulo,Sudeste,1,,11,1", "abc,,,,,,,,,,,,") + server.respond("/csv?ceps=01001000%2Cabc", Reply(200, body)) + val result = client.lookupMany(listOf("01001000", "abc")) + + assertEquals(2, result.size) + assertNull(result[1]) + } +} diff --git a/src/test/kotlin/org/cepis/ConfigurationTest.kt b/src/test/kotlin/org/cepis/ConfigurationTest.kt new file mode 100644 index 0000000..e166b6a --- /dev/null +++ b/src/test/kotlin/org/cepis/ConfigurationTest.kt @@ -0,0 +1,42 @@ +package org.cepis + +import java.time.Duration +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertTrue + +internal class ConfigurationTest { + @Test + fun `trailing slash is not duplicated`() { + assertEquals("http://x", Client(baseUrl = "http://x/").baseUrl) + } + + @Test + fun `invalid parameters are rejected`() { + assertFailsWith<IllegalArgumentException> { Client(maxAttempts = 0) } + assertFailsWith<IllegalArgumentException> { Client(timeout = Duration.ZERO) } + assertFailsWith<IllegalArgumentException> { Client(timeout = Duration.ofSeconds(-1)) } + } + + @Test + fun `user agent identifies the library`() { + assertTrue(Client().userAgent.startsWith("cepis-kotlin/$VERSION")) + } + + @Test + fun `user agent can be overridden`() { + assertEquals("mine/1.0", Client(userAgent = "mine/1.0").userAgent) + } + + @Test + fun `defaults match the documented ones`() { + val client = Client() + + assertEquals(DEFAULT_BASE_URL, client.baseUrl) + assertEquals(3, client.maxAttempts) + assertEquals(Duration.ofMillis(500), client.backoffBase) + assertEquals(Duration.ofSeconds(8), client.backoffMax) + assertEquals(null, client.totalTimeout) + } +} diff --git a/src/test/kotlin/org/cepis/FakeServer.kt b/src/test/kotlin/org/cepis/FakeServer.kt new file mode 100644 index 0000000..04ad4e2 --- /dev/null +++ b/src/test/kotlin/org/cepis/FakeServer.kt @@ -0,0 +1,98 @@ +package org.cepis + +import com.sun.net.httpserver.HttpServer +import java.net.InetSocketAddress +import java.util.Collections +import java.util.concurrent.Executors +import java.util.concurrent.ThreadFactory +import java.util.concurrent.atomic.AtomicInteger + +/** One canned HTTP response for the [FakeServer] to hand out. */ +internal class Reply( + val status: Int, + val body: String, + val headers: Map<String, String> = emptyMap(), +) + +/** + * A throwaway HTTP server on a random loopback port. + * + * Tests point a real [Client] at it through `baseUrl`, so the code path under + * test is the one that runs in production — request construction, retries and + * error-body reading included. Nothing is mocked and nothing touches the + * network. + */ +internal class FakeServer { + private val server: HttpServer = HttpServer.create(InetSocketAddress("127.0.0.1", 0), 0) + private val routes = HashMap<String, MutableList<Reply>>() + private val lock = Any() + + /** Every path requested so far, in order, query string included. */ + val calls: MutableList<String> = Collections.synchronizedList(ArrayList()) + + /** When set, answers every request and bypasses the registered routes. */ + var responder: ((String) -> Reply)? = null + + private val pool = + Executors.newCachedThreadPool( + object : ThreadFactory { + private val counter = AtomicInteger() + + override fun newThread(runnable: Runnable): Thread = + Thread(runnable, "fake-server-${counter.incrementAndGet()}").apply { + isDaemon = true + } + }, + ) + + init { + server.createContext("/") { exchange -> + val uri = exchange.requestURI + val path = if (uri.rawQuery == null) uri.rawPath else "${uri.rawPath}?${uri.rawQuery}" + val reply = next(path) + val bytes = reply.body.toByteArray(Charsets.UTF_8) + reply.headers.forEach { (name, value) -> exchange.responseHeaders.add(name, value) } + exchange.sendResponseHeaders( + reply.status, + if (bytes.isEmpty()) -1L else bytes.size.toLong(), + ) + exchange.responseBody.use { it.write(bytes) } + } + server.executor = pool + server.start() + } + + /** The base URL to hand to [Client]. */ + val url: String + get() = "http://127.0.0.1:${server.address.port}" + + /** + * Register the responses for [path]. + * + * Each request consumes the next reply, which is how retrying is tested. + * The last one repeats once the list runs out. + */ + fun respond( + path: String, + vararg replies: Reply, + ) { + synchronized(lock) { routes[path] = replies.toMutableList() } + } + + /** How many requests so far began with [prefix]. */ + fun count(prefix: String): Int = synchronized(calls) { calls.count { it.startsWith(prefix) } } + + fun stop() { + server.stop(0) + pool.shutdownNow() + } + + private fun next(path: String): Reply { + synchronized(lock) { + calls.add(path) + responder?.let { return it(path) } + val queue = routes[path] ?: return Reply(404, """{"erro":"cep_nao_encontrado"}""") + return if (queue.size > 1) queue.removeAt(0) else queue[0] + } + } +} diff --git a/src/test/kotlin/org/cepis/HealthTest.kt b/src/test/kotlin/org/cepis/HealthTest.kt new file mode 100644 index 0000000..ae57e48 --- /dev/null +++ b/src/test/kotlin/org/cepis/HealthTest.kt @@ -0,0 +1,36 @@ +package org.cepis + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +internal class HealthTest : ServerTest() { + @Test + fun `reads health`() { + val body = + """ + {"status":"ok","dne":"26071","ceps":1596168,"geo":894489, + "base_gerada_em":"2026-07-24T03:48:28Z"} + """ + server.respond("/health", Reply(200, body)) + val health = client.health() + + assertTrue(health.ok) + assertEquals("26071", health.dne) + assertEquals(1596168, health.ceps) + assertEquals(894489, health.geo) + assertEquals("2026-07-24T03:48:28Z", health.baseGeradaEm) + } + + @Test + fun `degraded does not raise and is not retried`() { + val body = """{"status":"degradado","dne":"","ceps":0,"geo":0}""" + server.respond("/health", Reply(503, body)) + val health = client.health() + + assertFalse(health.ok) + assertEquals("degradado", health.status) + assertEquals(1, server.count("/health")) + } +} diff --git a/src/test/kotlin/org/cepis/LookupTest.kt b/src/test/kotlin/org/cepis/LookupTest.kt new file mode 100644 index 0000000..c266025 --- /dev/null +++ b/src/test/kotlin/org/cepis/LookupTest.kt @@ -0,0 +1,128 @@ +package org.cepis + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertFalse +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import kotlin.test.assertTrue + +internal class LookupTest : ServerTest() { + @Test + fun `successful lookup`() { + server.respond("/01001000", Reply(200, Fixtures.SE_ADDRESS_JSON)) + val address = client.lookup("01001-000") + + assertEquals("São Paulo", address.localidade) + assertEquals("SP", address.uf) + assertEquals("Praça da Sé", address.logradouro) + assertEquals("01001-000", address.formattedCep) + assertNull(address.geo) + } + + @Test + fun `reads the coordinate when present`() { + val body = + Fixtures.seAddressWith( + """"geo":{"lat":-23.5,"lon":-46.6,"raio_m":120,"precisao":"logradouro"}""", + ) + server.respond("/01310100", Reply(200, body)) + val geo = assertNotNull(client.lookup("01310100").geo) + + assertEquals(-23.5, geo.lat, 1e-9) + assertEquals(-46.6, geo.lon, 1e-9) + assertEquals(120, geo.raioM) + assertTrue(geo.isPrecise) + } + + @Test + fun `municipality coordinate is not precise`() { + val body = + Fixtures.seAddressWith( + """"geo":{"lat":-23.5,"lon":-46.6,"raio_m":22000,"precisao":"localidade"}""", + ) + server.respond("/01001000", Reply(200, body)) + val geo = assertNotNull(client.lookup("01001000").geo) + + assertFalse(geo.isPrecise) + assertEquals(22000, geo.raioM) + } + + @Test + fun `malformed geo does not lose the address`() { + val body = Fixtures.seAddressWith(""""geo":{"lat":"not a number"}""") + server.respond("/01001000", Reply(200, body)) + val address = client.lookup("01001000") + + assertNull(address.geo) + assertEquals("São Paulo", address.localidade) + } + + @Test + fun `new api field does not break the client`() { + val body = Fixtures.seAddressWith(""""campo_do_futuro":"value"""") + server.respond("/01001000", Reply(200, body)) + + assertEquals("value", client.lookup("01001000").raw["campo_do_futuro"]) + } + + @Test + fun `not found`() { + server.respond( + "/99999999", + Reply(404, """{"erro":"cep_nao_encontrado","mensagem":"gone"}"""), + ) + val error = assertFailsWith<CepNotFoundException> { client.lookup("99999999") } + + assertTrue(error.message!!.contains("99999999")) + } + + @Test + fun `not found is not retried`() { + server.respond("/99999999", Reply(404, """{"erro":"x"}""")) + assertFailsWith<CepNotFoundException> { client.lookup("99999999") } + + assertEquals(1, server.count("/99999999")) + } + + @Test + fun `invalid cep never leaves the machine`() { + assertFailsWith<InvalidCepException> { client.lookup("123") } + + assertEquals(0, server.calls.size) + } + + @Test + fun `api 400 becomes an invalid cep carrying the api message`() { + server.respond( + "/00000000", + Reply(400, """{"erro":"cep_invalido","mensagem":"rejected by the API"}"""), + ) + val error = assertFailsWith<InvalidCepException> { client.lookup("00000000") } + + assertTrue(error.message!!.contains("rejected by the API")) + } + + @Test + fun `api 400 is not retried`() { + server.respond("/00000000", Reply(400, """{"erro":"cep_invalido"}""")) + assertFailsWith<InvalidCepException> { client.lookup("00000000") } + + assertEquals(1, server.count("/00000000")) + } + + @Test + fun `response that is not json`() { + server.respond("/01001000", Reply(200, "<html>captive portal</html>")) + + assertFailsWith<InvalidResponseException> { client.lookup("01001000") } + } + + @Test + fun `json that is not an object`() { + server.respond("/01001000", Reply(200, "[1, 2, 3]")) + + assertFailsWith<InvalidResponseException> { client.lookup("01001000") } + } +} diff --git a/src/test/kotlin/org/cepis/NormalizationTest.kt b/src/test/kotlin/org/cepis/NormalizationTest.kt new file mode 100644 index 0000000..b3069b6 --- /dev/null +++ b/src/test/kotlin/org/cepis/NormalizationTest.kt @@ -0,0 +1,26 @@ +package org.cepis + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith + +internal class NormalizationTest { + @Test + fun `accepts masked and unmasked`() { + for (value in listOf("01001000", "01001-000", " 01001-000 ", "01001.000")) { + assertEquals("01001000", normalizeCep(value)) + } + } + + @Test + fun `rejects bad format`() { + for (value in listOf("", "123", "0100100a", "010010000", "abcdefgh")) { + assertFailsWith<InvalidCepException> { normalizeCep(value) } + } + } + + @Test + fun `is a cepis error so one handler catches everything`() { + assertFailsWith<CepisException> { normalizeCep("123") } + } +} diff --git a/src/test/kotlin/org/cepis/ParserTest.kt b/src/test/kotlin/org/cepis/ParserTest.kt new file mode 100644 index 0000000..0ec0cf8 --- /dev/null +++ b/src/test/kotlin/org/cepis/ParserTest.kt @@ -0,0 +1,86 @@ +package org.cepis + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertNull +import kotlin.test.assertTrue + +internal class JsonTest { + @Test + fun `reads the shapes the api uses`() { + val parsed = Json.parse("""{"a":"x","b":1,"c":-2.5,"d":true,"e":null,"f":[1,"two"]}""") + val map = parsed as Map<*, *> + + assertEquals("x", map["a"]) + assertEquals(1L, map["b"]) + assertEquals(-2.5, map["c"]) + assertEquals(true, map["d"]) + assertNull(map["e"]) + assertEquals(listOf(1L, "two"), map["f"]) + } + + @Test + fun `reads escapes`() { + val map = Json.parse(""" {"s":"a\"b\\c\/d\ne\tfé"} """) as Map<*, *> + + assertEquals("a\"b\\c/d\ne\tfé", map["s"]) + } + + @Test + fun `preserves key order`() { + val map = Json.parse("""{"z":1,"a":2,"m":3}""") as Map<*, *> + + assertEquals(listOf("z", "a", "m"), map.keys.toList()) + } + + @Test + fun `rejects malformed documents`() { + for (bad in listOf("", "{", """{"a"}""", """{"a":}""", "[1,]", "tru", "{} junk")) { + assertFailsWith<JsonSyntaxException>(bad) { Json.parse(bad) } + } + } + + @Test + fun `accepts an empty object and an empty array`() { + assertEquals(emptyMap<String, Any?>(), Json.parse("{}")) + assertEquals(emptyList<Any?>(), Json.parse("[]")) + } +} + +internal class CsvTest { + @Test + fun `quoted fields keep commas newlines and quotes`() { + val rows = Csv.parse("a,b,c\n\"x,1\",\"y\nz\",\"q\"\"q\"\n") + + assertEquals(listOf("a", "b", "c"), rows[0]) + assertEquals(listOf("x,1", "y\nz", "q\"q"), rows[1]) + assertEquals(2, rows.size) + } + + @Test + fun `handles crlf and a missing final newline`() { + val rows = Csv.parse("a,b\r\n1,2\r\n3,4") + + assertEquals(3, rows.size) + assertEquals(listOf("3", "4"), rows[2]) + } + + @Test + fun `a fully empty row is all empty fields`() { + val rows = Csv.rows("cep,localidade\n99999999,\n") + + assertEquals(1, rows.size) + assertEquals("99999999", rows[0]["cep"]) + assertTrue(rows[0]["localidade"].isNullOrEmpty()) + } + + @Test + fun `short rows are padded and blank lines skipped`() { + val rows = Csv.rows("a,b,c\n1\n\n2,3\n") + + assertEquals(2, rows.size) + assertEquals(mapOf("a" to "1", "b" to "", "c" to ""), rows[0]) + assertEquals(mapOf("a" to "2", "b" to "3", "c" to ""), rows[1]) + } +} diff --git a/src/test/kotlin/org/cepis/RetryTest.kt b/src/test/kotlin/org/cepis/RetryTest.kt new file mode 100644 index 0000000..d70735a --- /dev/null +++ b/src/test/kotlin/org/cepis/RetryTest.kt @@ -0,0 +1,99 @@ +package org.cepis + +import java.time.Duration +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertTrue + +internal class RetryTest : ServerTest() { + @Test + fun `retries on 500 then succeeds`() { + server.respond( + "/01001000", + Reply(500, """{"erro":"interno"}"""), + Reply(200, Fixtures.SE_ADDRESS_JSON), + ) + + assertEquals("São Paulo", client.lookup("01001000").localidade) + assertEquals(2, server.count("/01001000")) + } + + @Test + fun `exhausts attempts then raises`() { + server.respond("/01001000", Reply(503, """{"erro":"interno"}""")) + val error = assertFailsWith<ServerErrorException> { client.lookup("01001000") } + + assertEquals(503, error.status) + assertEquals(3, server.count("/01001000")) + } + + @Test + fun `max attempts of one disables retrying`() { + val single = Client(baseUrl = server.url, maxAttempts = 1, timeout = Duration.ofSeconds(5)) + server.respond("/01001000", Reply(500, "{}")) + + assertFailsWith<ServerErrorException> { single.lookup("01001000") } + assertEquals(1, server.count("/01001000")) + } + + @Test + fun `429 becomes rate limited with retry after`() { + server.respond("/01001000", Reply(429, "{}", mapOf("Retry-After" to "7"))) + val error = assertFailsWith<RateLimitedException> { client.lookup("01001000") } + + assertEquals(7.0, error.retryAfter) + assertEquals(3, server.count("/01001000")) + } + + @Test + fun `retry after honours the cap`() { + server.respond("/01001000", Reply(429, "{}", mapOf("Retry-After" to "3600"))) + val started = System.nanoTime() + + assertFailsWith<RateLimitedException> { client.lookup("01001000") } + assertTrue((System.nanoTime() - started) / 1e9 < 5.0) + } + + @Test + fun `retry after accepts an http date`() { + val future = java.time.ZonedDateTime.now(java.time.ZoneOffset.UTC).plusSeconds(30) + val header = java.time.format.DateTimeFormatter.RFC_1123_DATE_TIME.format(future) + server.respond("/01001000", Reply(429, "{}", mapOf("Retry-After" to header))) + val error = assertFailsWith<RateLimitedException> { client.lookup("01001000") } + val seconds = error.retryAfter + + assertTrue(seconds != null && seconds > 20.0 && seconds <= 30.0, "got $seconds") + } + + @Test + fun `network error when nothing listens`() { + val unreachable = + Client( + baseUrl = "http://127.0.0.1:1", + maxAttempts = 2, + backoffBase = Duration.ofMillis(1), + timeout = Duration.ofSeconds(1), + ) + + assertFailsWith<NetworkException> { unreachable.lookup("01001000") } + } + + @Test + fun `total timeout bounds the whole call`() { + val bounded = + Client( + baseUrl = server.url, + maxAttempts = 50, + backoffBase = Duration.ofMillis(50), + backoffMax = Duration.ofMillis(50), + totalTimeout = Duration.ofMillis(300), + ) + server.respond("/01001000", Reply(500, "{}")) + val started = System.nanoTime() + + assertFailsWith<ServerErrorException> { bounded.lookup("01001000") } + assertTrue((System.nanoTime() - started) / 1e9 < 3.0) + assertTrue(server.count("/01001000") < 50) + } +} diff --git a/src/test/kotlin/org/cepis/ServerTest.kt b/src/test/kotlin/org/cepis/ServerTest.kt new file mode 100644 index 0000000..b22975a --- /dev/null +++ b/src/test/kotlin/org/cepis/ServerTest.kt @@ -0,0 +1,51 @@ +package org.cepis + +import java.time.Duration +import kotlin.test.AfterTest +import kotlin.test.BeforeTest + +/** + * Fixture with a fake server and a client whose backoff is near zero. + * + * The backoff is tiny because what is under test is the retry logic, not + * `Thread.sleep`'s ability to sleep. + */ +internal abstract class ServerTest { + protected lateinit var server: FakeServer + protected lateinit var client: Client + + @BeforeTest + fun startServer() { + server = FakeServer() + client = + Client( + baseUrl = server.url, + timeout = Duration.ofSeconds(5), + backoffBase = Duration.ofMillis(1), + backoffMax = Duration.ofMillis(10), + ) + } + + @AfterTest + fun stopServer() { + server.stop() + } +} + +internal object Fixtures { + const val SE_ADDRESS_JSON = """ + {"cep":"01001000","logradouro":"Praça da Sé","complemento":"lado ímpar", + "unidade":"","bairro":"Sé","localidade":"São Paulo","uf":"SP", + "estado":"São Paulo","regiao":"Sudeste","ibge":"3550308","gia":"1004", + "ddd":"11","siafi":"7107"} + """ + + const val CSV_HEADER = + "cep,logradouro,complemento,unidade,bairro,localidade,uf,estado," + + "regiao,ibge,gia,ddd,siafi" + + /** The Sé address with an extra top-level field spliced in. */ + fun seAddressWith(extra: String): String = SE_ADDRESS_JSON.trim().removeSuffix("}").trimEnd() + ",$extra}" + + fun csv(vararg rows: String): String = CSV_HEADER + "\n" + rows.joinToString("\n") + "\n" +} |