Skip to content

Commit d226c0d

Browse files
authored
feat: adding missing tests for device types, api response body (#137)
1 parent 715bad0 commit d226c0d

7 files changed

Lines changed: 385 additions & 20 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,6 @@
2222
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
2323
hs_err_pid*
2424
replay_pid*
25+
26+
target/
27+
.idea/

CLAUDE.md

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
# SwitchBotAPI-java Project
2+
3+
## Project Overview
4+
5+
A Java wrapper library for the SwitchBot API v1.1, providing easy integration with SwitchBot devices.
6+
7+
## Key Information
8+
9+
- **Language**: Java 17
10+
- **Build Tool**: Maven
11+
- **Version**: 1.2.5
12+
- **Main Package**: `com.bigboxer23.switch_bot`
13+
14+
## Project Structure
15+
16+
```
17+
src/
18+
├── main/java/com/bigboxer23/switch_bot/
19+
│ ├── SwitchBotApi.java # Main API client
20+
│ ├── SwitchBotDeviceApi.java # Device-specific API operations
21+
│ ├── IDeviceTypes.java # Device type constants
22+
│ ├── IDeviceCommands.java # Device command constants
23+
│ └── data/ # Data models and response classes
24+
└── test/java/com/bigboxer23/switch_bot/ # Unit and integration tests
25+
```
26+
27+
## Build Commands
28+
29+
- **Test**: `mvn test`
30+
- **Integration Tests**: `mvn failsafe:integration-test -Dintegration=true`
31+
- **Build**: `mvn compile`
32+
- **Package**: `mvn package`
33+
- **Format Code**: `mvn spotless:apply`
34+
- **Coverage Report**: `mvn jacoco:report`
35+
36+
## Code Style
37+
38+
- Uses Spotless for code formatting with Google Java Format (AOSP style)
39+
- Tab indentation (4 spaces per tab)
40+
- Lombok for reducing boilerplate code
41+
42+
## Testing
43+
44+
- JUnit 5 for unit tests
45+
- Mockito for mocking
46+
- Integration tests with SwitchBot API
47+
- Code coverage with JaCoCo
48+
49+
## Dependencies
50+
51+
- **utils**: Custom utility library (bigboxer23)
52+
- **moshi**: JSON serialization
53+
- **lombok**: Code generation
54+
- **logback**: Logging (test scope)
55+
56+
## GitHub Actions
57+
58+
- Unit tests on push/PR
59+
- CodeQL security analysis
60+
- Code coverage reporting
61+
- Automatic package publishing
62+
- Auto-merge for dependency updates
63+
64+
## API Features
65+
66+
- Device listing and status retrieval
67+
- Device control commands
68+
- Battery status monitoring
69+
- Support for curtains, plugs, and other SwitchBot devices
70+
- HMAC-SHA256 authentication with SwitchBot API
71+
72+
## Example Usage
73+
74+
```java
75+
SwitchBotApi instance = SwitchBotApi.getInstance(token, secret);
76+
List<Device> devices = instance.getDeviceApi().getDevices();
77+
Device status = instance.getDeviceApi().getDeviceStatus(devices.getFirst().getDeviceId());
78+
```
79+

pom.xml

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
<properties>
2020
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
2121
<java.version>17</java.version>
22+
<lombok.version>1.18.40</lombok.version>
2223
</properties>
2324

2425
<dependencies>
@@ -48,7 +49,7 @@
4849
<dependency>
4950
<groupId>org.projectlombok</groupId>
5051
<artifactId>lombok</artifactId>
51-
<version>1.18.40</version>
52+
<version>${lombok.version}</version>
5253
</dependency>
5354
<dependency>
5455
<groupId>com.squareup.moshi</groupId>
@@ -82,6 +83,13 @@
8283
<version>3.14.0</version>
8384
<configuration>
8485
<release>17</release>
86+
<annotationProcessorPaths>
87+
<path>
88+
<groupId>org.projectlombok</groupId>
89+
<artifactId>lombok</artifactId>
90+
<version>${lombok.version}</version>
91+
</path>
92+
</annotationProcessorPaths>
8593
</configuration>
8694
</plugin>
8795
<plugin>
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package com.bigboxer23.switch_bot;
2+
3+
import static org.junit.jupiter.api.Assertions.*;
4+
5+
import org.junit.jupiter.api.Test;
6+
7+
public class IDeviceTypesTest {
8+
9+
@Test
10+
public void testDeviceTypeConstants() {
11+
assertEquals("Curtain", IDeviceTypes.CURTAIN);
12+
assertEquals("Hub 2", IDeviceTypes.HUB2);
13+
assertEquals("Meter", IDeviceTypes.METER);
14+
assertEquals("WoIOSensor", IDeviceTypes.WOIOSENSOR);
15+
assertEquals("Plug Mini (US)", IDeviceTypes.PLUG_MINI);
16+
assertEquals("Water Detector", IDeviceTypes.WATER_DETECTOR);
17+
assertEquals("MeterPro(CO2)", IDeviceTypes.METER_PRO_CO2);
18+
assertEquals("Roller Shade", IDeviceTypes.ROLLER_SHADE);
19+
}
20+
21+
@Test
22+
public void testDeviceTypeConstantsAreNotNull() {
23+
assertNotNull(IDeviceTypes.CURTAIN);
24+
assertNotNull(IDeviceTypes.HUB2);
25+
assertNotNull(IDeviceTypes.METER);
26+
assertNotNull(IDeviceTypes.WOIOSENSOR);
27+
assertNotNull(IDeviceTypes.PLUG_MINI);
28+
assertNotNull(IDeviceTypes.WATER_DETECTOR);
29+
assertNotNull(IDeviceTypes.METER_PRO_CO2);
30+
assertNotNull(IDeviceTypes.ROLLER_SHADE);
31+
}
32+
33+
@Test
34+
public void testDeviceTypeUniqueness() {
35+
String[] deviceTypes = {
36+
IDeviceTypes.CURTAIN,
37+
IDeviceTypes.HUB2,
38+
IDeviceTypes.METER,
39+
IDeviceTypes.WOIOSENSOR,
40+
IDeviceTypes.PLUG_MINI,
41+
IDeviceTypes.WATER_DETECTOR,
42+
IDeviceTypes.METER_PRO_CO2,
43+
IDeviceTypes.ROLLER_SHADE
44+
};
45+
46+
for (int i = 0; i < deviceTypes.length; i++) {
47+
for (int j = i + 1; j < deviceTypes.length; j++) {
48+
assertNotEquals(
49+
deviceTypes[i],
50+
deviceTypes[j],
51+
"Device types should be unique: " + deviceTypes[i] + " vs " + deviceTypes[j]);
52+
}
53+
}
54+
}
55+
}

src/test/java/com/bigboxer23/switch_bot/SwitchBotApiUnitTest.java

Lines changed: 49 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,17 +30,15 @@ public void testSingletonBehavior() {
3030

3131
@Test
3232
public void testGetInstanceWithNullToken() {
33-
RuntimeException exception = assertThrows(RuntimeException.class, () -> {
34-
SwitchBotApi.getInstance(null, "secret");
35-
});
33+
RuntimeException exception =
34+
assertThrows(RuntimeException.class, () -> SwitchBotApi.getInstance(null, "secret"));
3635
assertEquals("need to define token and secret values.", exception.getMessage());
3736
}
3837

3938
@Test
4039
public void testGetInstanceWithNullSecret() {
41-
RuntimeException exception = assertThrows(RuntimeException.class, () -> {
42-
SwitchBotApi.getInstance("token", null);
43-
});
40+
RuntimeException exception =
41+
assertThrows(RuntimeException.class, () -> SwitchBotApi.getInstance("token", null));
4442
assertEquals("need to define token and secret values.", exception.getMessage());
4543
}
4644

@@ -130,4 +128,49 @@ public void testDeviceNameCacheRefreshBehavior() throws IOException {
130128
deviceApi.getDeviceNameFromId("12345");
131129
verify(deviceApi, times(0)).getDevices();
132130
}
131+
132+
@Test
133+
public void testAddAuthCreatesValidHeaders() {
134+
SwitchBotApi api = SwitchBotApi.getInstance("testToken", "testSecret");
135+
com.bigboxer23.utils.http.RequestBuilderCallback callback = api.addAuth();
136+
137+
assertNotNull(callback);
138+
}
139+
140+
@Test
141+
public void testAddAuthWithRequestBuilder() {
142+
SwitchBotApi api = SwitchBotApi.getInstance("testToken", "testSecret");
143+
com.bigboxer23.utils.http.RequestBuilderCallback callback = api.addAuth();
144+
145+
okhttp3.Request.Builder mockBuilder = mock(okhttp3.Request.Builder.class);
146+
when(mockBuilder.addHeader(anyString(), anyString())).thenReturn(mockBuilder);
147+
148+
okhttp3.Request.Builder result = callback.modifyBuilder(mockBuilder);
149+
150+
assertNotNull(result);
151+
verify(mockBuilder, times(5)).addHeader(anyString(), anyString());
152+
}
153+
154+
@Test
155+
public void testGetMoshiReturnsInstance() {
156+
SwitchBotApi api = SwitchBotApi.getInstance("testToken", "testSecret");
157+
com.squareup.moshi.Moshi moshi = api.getMoshi();
158+
159+
assertNotNull(moshi);
160+
assertSame(moshi, api.getMoshi(), "Should return the same Moshi instance");
161+
}
162+
163+
@Test
164+
public void testGetDeviceApiReturnsInstance() {
165+
SwitchBotApi api = SwitchBotApi.getInstance("testToken", "testSecret");
166+
SwitchBotDeviceApi deviceApi = api.getDeviceApi();
167+
168+
assertNotNull(deviceApi);
169+
assertSame(deviceApi, api.getDeviceApi(), "Should return the same DeviceApi instance");
170+
}
171+
172+
@Test
173+
public void testBaseUrlConstant() {
174+
assertEquals("https://api.switch-bot.com/", SwitchBotApi.baseUrl);
175+
}
133176
}

src/test/java/com/bigboxer23/switch_bot/SwitchBotDeviceApiTest.java

Lines changed: 87 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import java.io.IOException;
99
import java.util.Arrays;
1010
import java.util.Collections;
11+
import java.util.List;
1112
import org.junit.jupiter.api.BeforeEach;
1213
import org.junit.jupiter.api.Test;
1314
import org.mockito.Mock;
@@ -53,7 +54,7 @@ public void testGetDeviceNameFromIdWithUnknownDevice() throws IOException {
5354
device.setDeviceName("Known Device");
5455

5556
SwitchBotDeviceApi spyDeviceApi = spy(deviceApi);
56-
doReturn(Arrays.asList(device)).when(spyDeviceApi).getDevices();
57+
doReturn(List.of(device)).when(spyDeviceApi).getDevices();
5758

5859
String result = spyDeviceApi.getDeviceNameFromId("unknown-device");
5960
assertEquals("unknown-device", result);
@@ -68,7 +69,7 @@ public void testGetDeviceNameFromIdCacheExpiry() throws IOException {
6869
SwitchBotDeviceApi spyDeviceApi = spy(deviceApi);
6970
spyDeviceApi.deviceIdToNamesCacheTime = System.currentTimeMillis() - (ITimeConstants.HOUR * 2);
7071

71-
doReturn(Arrays.asList(device)).when(spyDeviceApi).getDevices();
72+
doReturn(List.of(device)).when(spyDeviceApi).getDevices();
7273

7374
String result = spyDeviceApi.getDeviceNameFromId("device1");
7475
assertEquals("Test Device", result);
@@ -85,7 +86,7 @@ public void testGetDeviceNameFromIdWithValidCache() throws IOException {
8586
SwitchBotDeviceApi spyDeviceApi = spy(deviceApi);
8687
spyDeviceApi.deviceIdToNamesCacheTime = System.currentTimeMillis();
8788

88-
doReturn(Arrays.asList(device)).when(spyDeviceApi).getDevices();
89+
doReturn(List.of(device)).when(spyDeviceApi).getDevices();
8990
spyDeviceApi.getDeviceNameFromId("device1");
9091

9192
reset(spyDeviceApi);
@@ -112,9 +113,7 @@ public void testGetDeviceStatusWithNullDeviceId() throws IOException {
112113

113114
@Test
114115
public void testGetDeviceStatusInputValidation() {
115-
assertDoesNotThrow(() -> {
116-
assertNotNull(deviceApi);
117-
});
116+
assertDoesNotThrow(() -> assertNotNull(deviceApi));
118117
}
119118

120119
@Test
@@ -145,18 +144,14 @@ public void testConcurrentCacheRefresh() throws InterruptedException {
145144
device.setDeviceId("concurrent-device");
146145
device.setDeviceName("Concurrent Test");
147146
try {
148-
doReturn(Arrays.asList(device)).when(spyDeviceApi).getDevices();
147+
doReturn(List.of(device)).when(spyDeviceApi).getDevices();
149148
} catch (IOException e) {
150149
fail("Setup failed: " + e.getMessage());
151150
}
152151

153-
Thread thread1 = new Thread(() -> {
154-
spyDeviceApi.getDeviceNameFromId("concurrent-device");
155-
});
152+
Thread thread1 = new Thread(() -> spyDeviceApi.getDeviceNameFromId("concurrent-device"));
156153

157-
Thread thread2 = new Thread(() -> {
158-
spyDeviceApi.getDeviceNameFromId("concurrent-device");
159-
});
154+
Thread thread2 = new Thread(() -> spyDeviceApi.getDeviceNameFromId("concurrent-device"));
160155

161156
thread1.start();
162157
thread2.start();
@@ -192,4 +187,83 @@ public void testGetDeviceStatusReturnsNullForNullInput() throws IOException {
192187
public void testDeviceApiNotNull() {
193188
assertNotNull(deviceApi);
194189
}
190+
191+
@Test
192+
public void testParseResponseWithIOException() {
193+
SwitchBotDeviceApi spyDeviceApi = spy(deviceApi);
194+
when(mockSwitchBotApi.getMoshi()).thenReturn(new com.squareup.moshi.Moshi.Builder().build());
195+
196+
DeviceCommand command = new DeviceCommand("turnOn", "default");
197+
assertDoesNotThrow(() -> {
198+
String json =
199+
mockSwitchBotApi.getMoshi().adapter(DeviceCommand.class).toJson(command);
200+
assertNotNull(json);
201+
});
202+
}
203+
204+
@Test
205+
public void testSendDeviceControlCommandsWithValidInput() {
206+
DeviceCommand command = new DeviceCommand("turnOn", "default");
207+
String deviceId = "valid-device-id";
208+
209+
when(mockSwitchBotApi.getMoshi()).thenReturn(new com.squareup.moshi.Moshi.Builder().build());
210+
211+
assertNotNull(command.getCommand());
212+
assertNotNull(command.getParameter());
213+
assertNotNull(deviceId);
214+
assertEquals("turnOn", command.getCommand());
215+
assertEquals("default", command.getParameter());
216+
}
217+
218+
@Test
219+
public void testDeviceStatusValidation() throws IOException {
220+
assertNull(deviceApi.getDeviceStatus(null));
221+
}
222+
223+
@Test
224+
public void testGetDeviceNameFromIdWithNullDeviceNameMap() throws IOException {
225+
SwitchBotDeviceApi spyDeviceApi = spy(deviceApi);
226+
spyDeviceApi.deviceIdToNamesCacheTime = -1;
227+
228+
doThrow(new IOException("Network error")).when(spyDeviceApi).getDevices();
229+
230+
String result = spyDeviceApi.getDeviceNameFromId("test-device");
231+
assertEquals("test-device", result);
232+
}
233+
234+
@Test
235+
public void testCacheTimeValidation() {
236+
SwitchBotDeviceApi spyDeviceApi = spy(deviceApi);
237+
238+
spyDeviceApi.deviceIdToNamesCacheTime = System.currentTimeMillis() - (ITimeConstants.HOUR * 2);
239+
assertTrue(spyDeviceApi.deviceIdToNamesCacheTime < System.currentTimeMillis() - ITimeConstants.HOUR);
240+
241+
spyDeviceApi.deviceIdToNamesCacheTime = System.currentTimeMillis();
242+
assertTrue(spyDeviceApi.deviceIdToNamesCacheTime > System.currentTimeMillis() - ITimeConstants.HOUR);
243+
}
244+
245+
@Test
246+
public void testDeviceApiConstructor() {
247+
SwitchBotDeviceApi newDeviceApi = new SwitchBotDeviceApi(mockSwitchBotApi);
248+
assertNotNull(newDeviceApi);
249+
assertEquals(-1, newDeviceApi.deviceIdToNamesCacheTime);
250+
}
251+
252+
@Test
253+
public void testGetDeviceStatusWithEmptyDeviceId() {
254+
assertDoesNotThrow(() -> assertNotNull(deviceApi));
255+
}
256+
257+
@Test
258+
public void testGetDeviceNameFromIdWithCacheRefreshFailure() throws IOException {
259+
SwitchBotDeviceApi spyDeviceApi = spy(deviceApi);
260+
261+
spyDeviceApi.deviceIdToNamesCacheTime = System.currentTimeMillis() - (ITimeConstants.HOUR * 2);
262+
263+
doThrow(new IOException("Network failure")).when(spyDeviceApi).getDevices();
264+
265+
String result = spyDeviceApi.getDeviceNameFromId("test-device");
266+
assertEquals("test-device", result);
267+
assertEquals(-1, spyDeviceApi.deviceIdToNamesCacheTime);
268+
}
195269
}

0 commit comments

Comments
 (0)