1+ package com.soberg.kotlin.aoc.api
2+
3+ import assertk.assertThat
4+ import assertk.assertions.containsExactly
5+ import assertk.assertions.exists
6+ import assertk.assertions.hasMessage
7+ import assertk.assertions.isEqualTo
8+ import assertk.assertions.isFalse
9+ import assertk.assertions.isNotNull
10+ import com.soberg.kotlin.aoc.api.AdventOfCodeInputApi.CachingStrategy
11+ import io.ktor.client.HttpClient
12+ import io.ktor.client.engine.mock.MockEngine
13+ import io.ktor.client.engine.mock.respond
14+ import io.ktor.http.HttpHeaders
15+ import io.ktor.http.HttpStatusCode
16+ import io.ktor.http.headersOf
17+ import io.ktor.utils.io.ByteReadChannel
18+ import io.mockk.coEvery
19+ import io.mockk.mockkObject
20+ import io.mockk.unmockkObject
21+ import kotlinx.coroutines.test.runTest
22+ import org.junit.jupiter.api.AfterEach
23+ import org.junit.jupiter.api.Test
24+ import org.junit.jupiter.api.io.TempDir
25+ import java.net.UnknownHostException
26+ import kotlin.io.path.Path
27+ import kotlin.io.path.absolutePathString
28+ import kotlin.io.path.createFile
29+ import kotlin.io.path.exists
30+ import java.nio.file.Path as JavaNioPath
31+
32+ class AdventOfCodeInputApiTest {
33+
34+ @AfterEach
35+ fun teardown () {
36+ unmockkObject(AdventOfCodeHttpInputQuery )
37+ }
38+
39+ @Test
40+ fun `store in cache directory for LocalTextFile cache strategy` (
41+ @TempDir tempDir : JavaNioPath ,
42+ ) = runTest {
43+ val api = AdventOfCodeInputApi (CachingStrategy .LocalTextFile (tempDir.absolutePathString()))
44+ setupMockHttpEngine()
45+
46+ assertThat(Path (tempDir.absolutePathString(), " 2024" , " 23.txt" ).exists())
47+ .isFalse()
48+ api.readInput(2024 , 23 , " token" )
49+ assertThat(Path (tempDir.absolutePathString(), " 2024" , " 23.txt" ))
50+ .exists()
51+ }
52+
53+ @Test
54+ fun `read from cache when stored instead of network for LocalTextFile cache strategy` (
55+ @TempDir tempDir : JavaNioPath ,
56+ ) = runTest {
57+ val api = AdventOfCodeInputApi (CachingStrategy .LocalTextFile (tempDir.absolutePathString()))
58+ setupMockHttpEngine(bodyContent = " 1\n 2\n 3 and me\n " )
59+
60+ val result = api.readInput(2024 , 1 , " token" )
61+ assertThat(result.getOrNull())
62+ .isNotNull()
63+ .containsExactly(" 1" , " 2" , " 3 and me" )
64+
65+ // Assert that even though the "network" returns something else, we should get the same cached result as before.
66+ setupMockHttpEngine(bodyContent = " a and c\n b\n d\n " )
67+ val cachedResult = api.readInput(2024 , 1 , " token" )
68+ assertThat(cachedResult.getOrNull())
69+ .isNotNull()
70+ .containsExactly(" 1" , " 2" , " 3 and me" )
71+ }
72+
73+ @Test
74+ fun `read from cache for Custom cache strategy` () = runTest {
75+ val cachingStrategy = CachingStrategy .Custom (
76+ tryReadBlock = { _, _ -> listOf (" 1" , " 2" ) },
77+ writeBlock = { _, _, _ -> },
78+ )
79+ val api = AdventOfCodeInputApi (cachingStrategy)
80+ setupMockHttpEngine()
81+
82+ val result = api.readInput(2024 , 1 , " token" )
83+ assertThat(result.getOrNull())
84+ .isNotNull()
85+ .containsExactly(" 1" , " 2" )
86+ }
87+
88+ @Test
89+ fun `store in cache for Custom cache strategy` (
90+ @TempDir tempDir : JavaNioPath ,
91+ ) = runTest {
92+ val cachingStrategy = CachingStrategy .Custom (
93+ tryReadBlock = { _, _ -> null },
94+ writeBlock = { _, _, _ -> Path (tempDir.absolutePathString(), " TEST.txt" ).createFile() },
95+ )
96+ val api = AdventOfCodeInputApi (cachingStrategy)
97+ setupMockHttpEngine()
98+
99+ api.readInput(2024 , 1 , " token" )
100+ assertThat(Path (tempDir.absolutePathString(), " TEST.txt" ))
101+ .exists()
102+ }
103+
104+ @Test
105+ fun `read expected line input from network for success` () = runTest {
106+ val api = AdventOfCodeInputApi (CachingStrategy .None )
107+ setupMockHttpEngine(
108+ bodyContent = " a\n b\n c\n d" ,
109+ statusCode = HttpStatusCode .OK ,
110+ )
111+
112+ val result = api.readInput(2024 , 1 , " token" )
113+ assertThat(result.getOrNull())
114+ .isNotNull()
115+ .containsExactly(" a" , " b" , " c" , " d" )
116+ }
117+
118+ @Test
119+ fun `return failure result for non-200 status` () = runTest {
120+ val api = AdventOfCodeInputApi (CachingStrategy .None )
121+ setupMockHttpEngine(
122+ statusCode = HttpStatusCode .BadRequest ,
123+ )
124+
125+ val result = api.readInput(2024 , 1 , " token" )
126+ assertThat(result.exceptionOrNull())
127+ .isNotNull()
128+ .hasMessage(" Unexpected response code 400" )
129+ }
130+
131+ @Test
132+ fun `return failure result when exception thrown` () = runTest {
133+ val api = AdventOfCodeInputApi (CachingStrategy .None )
134+ val exception = UnknownHostException (" Test" )
135+ mockkObject(AdventOfCodeHttpInputQuery )
136+ coEvery { AdventOfCodeHttpInputQuery .createClient(any()) } throws exception
137+
138+ val result = api.readInput(2024 , 1 , " token" )
139+ assertThat(result.exceptionOrNull())
140+ .isEqualTo(exception)
141+ }
142+
143+ private fun setupMockHttpEngine (
144+ bodyContent : String = "",
145+ statusCode : HttpStatusCode = HttpStatusCode .OK ,
146+ ) {
147+ val mockEngine = MockEngine {
148+ respond(
149+ content = ByteReadChannel (bodyContent),
150+ status = statusCode,
151+ headers = headersOf(HttpHeaders .ContentType , " text/plain" )
152+ )
153+ }
154+
155+ mockkObject(AdventOfCodeHttpInputQuery )
156+ coEvery { AdventOfCodeHttpInputQuery .createClient(any()) } returns HttpClient (mockEngine)
157+ }
158+ }
0 commit comments