Skip to content

Commit ceab50f

Browse files
authored
feat(ATS): add ODP API clients (#440)
Add ODP API clients for segment fetch and event dispatch: - DefaultODPAPIManager - ODPSegmentClient - ODPEventClient
1 parent 971761e commit ceab50f

File tree

21 files changed

+837
-26
lines changed

21 files changed

+837
-26
lines changed

.editorconfig

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# indicate this is the root of the project
2+
root = true
3+
4+
[*.{kt,java,xml,gradle,md}]
5+
charset = utf-8
6+
indent_style = space
7+
indent_size = 4
8+
trim_trailing_whitespace = true
9+
insert_final_newline = true
10+
end_of_line = lf
11+
12+
[{*.gradle.kts,*.kt,*.kts,*.main.kts}]
13+
disabled_rules = import-ordering

build.gradle

+7-2
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818

1919
buildscript {
2020
ext.kotlin_version = '1.7.0'
21-
21+
2222
ext.version_name = System.getenv('TRAVIS_TAG')
2323
if (version_name == null || version_name.isEmpty()) {
2424
ext.version_name = 'debugVersion'
@@ -38,6 +38,10 @@ buildscript {
3838
}
3939
}
4040

41+
plugins {
42+
id "org.jlleitschuh.gradle.ktlint" version "11.0.0"
43+
}
44+
4145
allprojects {
4246
tasks.withType(Test).configureEach {
4347
maxParallelForks = Runtime.runtime.availableProcessors().intdiv(2) ?: 1
@@ -61,7 +65,8 @@ ext {
6165
build_tools_version = "30.0.3"
6266
min_sdk_version = 14
6367
target_sdk_version = 33
64-
java_core_ver = "3.10.2"
68+
//java_core_ver = "3.10.2"
69+
java_core_ver = "BB-SNAPSHOT"
6570
android_logger_ver = "1.3.6"
6671
jacksonversion= "2.11.2"
6772
annotations_ver = "1.2.0"

event-handler/build.gradle

-2
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,6 @@ dependencies {
4949
api project(':shared')
5050
implementation "androidx.annotation:annotation:$annotations_ver"
5151
implementation "androidx.work:work-runtime:$work_runtime"
52-
// Base64
53-
implementation "commons-codec:commons-codec:1.15"
5452

5553
compileOnly "com.noveogroup.android:android-logger:$android_logger_ver"
5654

event-handler/src/main/java/com/optimizely/ab/android/event_handler/EventWorker.java

+1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import androidx.work.WorkerParameters;
2626

2727
import com.optimizely.ab.android.shared.Client;
28+
import com.optimizely.ab.android.shared.EventHandlerUtils;
2829
import com.optimizely.ab.android.shared.OptlyStorage;
2930
import com.optimizely.ab.event.LogEvent;
3031

event-handler/src/test/java/com/optimizely/ab/android/event_handler/EventWorkerUnitTest.java

+1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import androidx.work.Data;
3030
import androidx.work.WorkerParameters;
3131

32+
import com.optimizely.ab.android.shared.EventHandlerUtils;
3233
import com.optimizely.ab.event.LogEvent;
3334

3435
import org.junit.Test;

odp/build.gradle

+5-2
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
apply plugin: 'com.android.library'
1818
apply plugin: 'kotlin-android'
19+
apply plugin: 'org.jlleitschuh.gradle.ktlint'
1920

2021
android {
2122
compileSdkVersion compile_sdk_version
@@ -55,13 +56,15 @@ dependencies {
5556
api project(':shared')
5657
implementation "androidx.annotation:annotation:$annotations_ver"
5758
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
58-
implementation 'androidx.test.ext:junit-ktx:1.1.3'
59+
implementation "androidx.work:work-runtime:$work_runtime"
5960

6061
testImplementation "junit:junit:$junit_ver"
6162
testImplementation "org.mockito:mockito-core:$mockito_ver"
6263

6364
compileOnly "com.noveogroup.android:android-logger:$android_logger_ver"
6465

66+
androidTestImplementation "androidx.work:work-testing:$work_runtime"
67+
androidTestImplementation "androidx.test.ext:junit-ktx:$androidx_test_junit"
6568
androidTestImplementation "androidx.test.ext:junit:$androidx_test_junit"
6669
androidTestImplementation "androidx.test.espresso:espresso-core:$espresso_ver"
6770
// Set this dependency to use JUnit 4 rules
@@ -73,4 +76,4 @@ dependencies {
7376
androidTestImplementation "com.crittercism.dexmaker:dexmaker:$dexmaker_ver"
7477
androidTestImplementation "com.crittercism.dexmaker:dexmaker-dx:$dexmaker_ver"
7578
androidTestImplementation "com.crittercism.dexmaker:dexmaker-mockito:$dexmaker_ver"
76-
}
79+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// Copyright 2022, Optimizely, Inc. and contributors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package com.optimizely.ab.android.odp
16+
17+
import android.content.Context
18+
import androidx.test.core.app.ApplicationProvider
19+
import androidx.test.ext.junit.runners.AndroidJUnit4
20+
import org.junit.Assert.assertEquals
21+
import org.junit.Test
22+
import org.junit.runner.RunWith
23+
import org.mockito.Mockito.mock
24+
import org.mockito.Mockito.verify
25+
26+
@RunWith(AndroidJUnit4::class)
27+
class DefaultODPApiManagerTest {
28+
private val apiKey = "valid-key"
29+
private val apiEndpoint = "http://valid-endpoint"
30+
31+
private var context: Context = ApplicationProvider.getApplicationContext()
32+
private val defaultODPApiManager = DefaultODPApiManager(context)
33+
34+
@Test
35+
fun fetchQualifiedSegments() {
36+
val userKey = "fs_user_id"
37+
val userValue = "user123"
38+
val segmentsToCheck = setOf("a")
39+
40+
val expRequestPayload = "{\"query\": \"query(\$userId: String, \$audiences: [String]) {customer(fs_user_id: \$userId) {audiences(subset: \$audiences) {edges {node {name state}}}}}\", \"variables\": {\"userId\": \"user123\", \"audiences\": [\"a\"]}}"
41+
42+
val segmentClient = mock(ODPSegmentClient::class.java)
43+
defaultODPApiManager.segmentClient = segmentClient
44+
45+
defaultODPApiManager.fetchQualifiedSegments(apiKey, apiEndpoint, userKey, userValue, segmentsToCheck)
46+
verify(segmentClient).fetchQualifiedSegments(apiKey, apiEndpoint, expRequestPayload)
47+
}
48+
49+
@Test
50+
fun sendEvents() {
51+
val payload = "event-payload"
52+
val status = defaultODPApiManager.sendEvents(apiKey, apiEndpoint, payload)
53+
assertEquals(status, 200) // always return success to java-sdk
54+
}
55+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
// Copyright 2022, Optimizely, Inc. and contributors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package com.optimizely.ab.android.odp
16+
17+
import androidx.test.ext.junit.runners.AndroidJUnit4
18+
import com.optimizely.ab.android.shared.Client
19+
import java.io.OutputStream
20+
import java.net.HttpURLConnection
21+
import org.junit.Assert.assertTrue
22+
import org.junit.Assert.assertFalse
23+
import org.junit.Before
24+
import org.junit.Test
25+
import org.junit.runner.RunWith
26+
import org.mockito.ArgumentCaptor
27+
import org.mockito.Matchers.any
28+
import org.mockito.Matchers.anyInt
29+
import org.mockito.Matchers.contains
30+
import org.mockito.Matchers.eq
31+
import org.mockito.Mockito.`when`
32+
import org.mockito.Mockito.mock
33+
import org.mockito.Mockito.verify
34+
import org.slf4j.Logger
35+
36+
@RunWith(AndroidJUnit4::class)
37+
class ODPEventClientTest {
38+
private val logger = mock(Logger::class.java)
39+
private val client = mock(Client::class.java)
40+
private val urlConnection = mock(HttpURLConnection::class.java)
41+
private val captor = ArgumentCaptor.forClass(Client.Request::class.java)
42+
private lateinit var eventClient: ODPEventClient
43+
44+
private val apiKey = "valid-key"
45+
private var apiEndpoint = "http://valid-endpoint"
46+
private val payload = "valid-payload"
47+
48+
@Before
49+
fun setUp() {
50+
eventClient = ODPEventClient(client, logger)
51+
`when`(client.openConnection(any())).thenReturn(urlConnection)
52+
`when`(urlConnection.outputStream).thenReturn(mock(OutputStream::class.java))
53+
}
54+
55+
@Test
56+
fun dispatch_200() {
57+
`when`(urlConnection.responseCode).thenReturn(200)
58+
`when`(urlConnection.responseMessage).thenReturn("message")
59+
60+
eventClient.dispatch(apiKey, apiEndpoint, payload)
61+
62+
verify(client).execute(captor.capture(), eq(2), eq(3))
63+
val received = captor.value.execute() as Boolean
64+
65+
assertTrue(received)
66+
verify(urlConnection).connectTimeout = 10 * 1000
67+
verify(urlConnection).readTimeout = 60 * 1000
68+
verify(urlConnection).setRequestProperty("x-api-key", apiKey)
69+
verify(urlConnection).disconnect()
70+
}
71+
72+
@Test
73+
fun dispatch_400() {
74+
`when`(urlConnection.responseCode).thenReturn(400)
75+
`when`(urlConnection.responseMessage).thenReturn("message")
76+
77+
eventClient.dispatch(apiKey, apiEndpoint, payload)
78+
79+
verify(client).execute(captor.capture(), anyInt(), anyInt())
80+
val received = captor.value.execute() as Boolean
81+
82+
assertFalse(received)
83+
verify(logger).error("ODP event send failed (Response code: 400, message)")
84+
verify(urlConnection).disconnect()
85+
}
86+
87+
@Test
88+
fun dispatch_500() {
89+
`when`(urlConnection.responseCode).thenReturn(500)
90+
`when`(urlConnection.responseMessage).thenReturn("message")
91+
92+
eventClient.dispatch(apiKey, apiEndpoint, payload)
93+
94+
verify(client).execute(captor.capture(), anyInt(), anyInt())
95+
val received = captor.value.execute() as Boolean
96+
97+
assertFalse(received)
98+
verify(logger).error("ODP event send failed (Response code: 500, message)")
99+
verify(urlConnection).disconnect()
100+
}
101+
102+
@Test
103+
fun dispatch_connectionFailed() {
104+
`when`(urlConnection.responseCode).thenReturn(200)
105+
`when`(urlConnection.responseMessage).thenReturn("message")
106+
107+
apiEndpoint = "invalid-url"
108+
eventClient.dispatch(apiKey, apiEndpoint, payload)
109+
110+
verify(client).execute(captor.capture(), anyInt(), anyInt())
111+
val received = captor.value.execute() as Boolean
112+
113+
assertFalse(received)
114+
verify(logger).error(contains("Error making request"), any())
115+
}
116+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
// Copyright 2022, Optimizely, Inc. and contributors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package com.optimizely.ab.android.odp
16+
17+
import android.content.Context
18+
import androidx.test.core.app.ApplicationProvider
19+
import androidx.test.ext.junit.runners.AndroidJUnit4
20+
import androidx.work.Data
21+
import androidx.work.ListenableWorker
22+
import androidx.work.testing.TestWorkerBuilder
23+
import org.hamcrest.Matchers.`is`
24+
import org.junit.Assert.assertEquals
25+
import org.junit.Assert.assertNotNull
26+
import org.junit.Assert.assertNull
27+
import org.junit.Assert.assertThat
28+
import org.junit.Assert.assertTrue
29+
import org.junit.Test
30+
import org.junit.runner.RunWith
31+
import org.mockito.Mockito.`when`
32+
import org.mockito.Mockito.mock
33+
import org.mockito.Mockito.verify
34+
import java.util.concurrent.Executor
35+
import java.util.concurrent.Executors
36+
37+
/**
38+
* Tests [ODPEventWorker]
39+
*/
40+
@RunWith(AndroidJUnit4::class)
41+
class ODPEventWorkerTest {
42+
private val apiKey = "valid-key"
43+
private var apiEndpoint = "http://valid-endpoint"
44+
private val smallBody = "valid-payload"
45+
private val largeBody = makeLongString(20000)
46+
47+
private var context: Context = ApplicationProvider.getApplicationContext()
48+
private var executor: Executor = Executors.newSingleThreadExecutor()
49+
private var worker: ODPEventWorker = makeODPEventWorker(apiEndpoint, apiKey, smallBody)
50+
51+
@Test
52+
fun dispatch_success() {
53+
val eventClient = mock(ODPEventClient::class.java)
54+
`when`(eventClient.dispatch(apiKey, apiEndpoint, smallBody)).thenReturn(true)
55+
worker.eventClient = eventClient
56+
57+
val result = worker.doWork()
58+
assertThat(result, `is`(ListenableWorker.Result.success()))
59+
verify(eventClient).dispatch(apiKey, apiEndpoint, smallBody)
60+
}
61+
62+
@Test
63+
fun dispatch_failure() {
64+
val eventClient = mock(ODPEventClient::class.java)
65+
`when`(eventClient.dispatch(apiKey, apiEndpoint, smallBody)).thenReturn(false)
66+
worker.eventClient = eventClient
67+
68+
val result = worker.doWork()
69+
assertThat(result, `is`(ListenableWorker.Result.failure()))
70+
verify(eventClient).dispatch(apiKey, apiEndpoint, smallBody)
71+
}
72+
73+
// Data
74+
75+
@Test
76+
fun getDataForSmallEvent() {
77+
val data: Data = ODPEventWorker.getData(apiKey, apiEndpoint, smallBody)
78+
79+
assertEquals(worker.getApiEndpointFromInputData(data), apiEndpoint)
80+
assertEquals(worker.getApiKeyFromInputData(data), apiKey)
81+
assertEquals(worker.getEventBodyFromInputData(data), smallBody)
82+
assertNotNull(data.getString("body"))
83+
assertNull(data.getString("bodyCompressed"))
84+
assertTrue(smallBody.length < 1000)
85+
}
86+
87+
@Test
88+
fun getDataForCompressedEvent() {
89+
val data: Data = ODPEventWorker.getData(apiKey, apiEndpoint, largeBody)
90+
91+
assertEquals(worker.getApiEndpointFromInputData(data), apiEndpoint)
92+
assertEquals(worker.getApiKeyFromInputData(data), apiKey)
93+
assertEquals(worker.getEventBodyFromInputData(data), largeBody)
94+
assertNull(data.getString("body"))
95+
assertNotNull(data.getString("bodyCompressed"))
96+
assertTrue(largeBody.length > 15000)
97+
}
98+
99+
// Helpers
100+
101+
private fun makeODPEventWorker(apiEndpoint: String, apiKey: String, payload: String): ODPEventWorker {
102+
val inputData: Data = ODPEventWorker.getData(apiKey, apiEndpoint, payload)
103+
return TestWorkerBuilder.from<ODPEventWorker>(context, ODPEventWorker::class.java, executor)
104+
.setInputData(inputData)
105+
.build()
106+
}
107+
108+
private fun makeLongString(maxSize: Int): String {
109+
val builder = StringBuilder()
110+
val str = "random-string"
111+
val repeat = (maxSize / str.length) + 1
112+
for (i in 1..repeat) builder.append(str)
113+
return builder.toString()
114+
}
115+
}

0 commit comments

Comments
 (0)