forked from Hithomelabs/CFTunnels
Hithomelabs/CFTunnels#87: address PR review - add @JsonProperty, fix status type, add unit tests
This commit is contained in:
parent
e515532d99
commit
6fc44a6969
@ -8,4 +8,6 @@ dependencies {
|
||||
annotationProcessor 'org.projectlombok:lombok'
|
||||
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter'
|
||||
testImplementation 'org.mockito:mockito-core'
|
||||
testImplementation 'org.springframework.boot:spring-boot-starter-test'
|
||||
}
|
||||
|
||||
@ -1,7 +1,13 @@
|
||||
package com.hithomelabs.common.portainer.model;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
public class PortainerAuthRequest {
|
||||
|
||||
@JsonProperty("Username")
|
||||
private String username;
|
||||
|
||||
@JsonProperty("Password")
|
||||
private String password;
|
||||
|
||||
public PortainerAuthRequest() {}
|
||||
|
||||
@ -1,6 +1,10 @@
|
||||
package com.hithomelabs.common.portainer.model;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
public class PortainerAuthResponse {
|
||||
|
||||
@JsonProperty("jwt")
|
||||
private String jwt;
|
||||
|
||||
public PortainerAuthResponse() {}
|
||||
|
||||
@ -1,7 +1,13 @@
|
||||
package com.hithomelabs.common.portainer.model;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
public class PortainerRedeployRequest {
|
||||
|
||||
@JsonProperty("PullImage")
|
||||
private boolean pullImage;
|
||||
|
||||
@JsonProperty("Prune")
|
||||
private boolean prune;
|
||||
|
||||
public PortainerRedeployRequest() {}
|
||||
|
||||
@ -1,10 +1,22 @@
|
||||
package com.hithomelabs.common.portainer.model;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
public class PortainerStack {
|
||||
|
||||
@JsonProperty("Id")
|
||||
private Long id;
|
||||
|
||||
@JsonProperty("Name")
|
||||
private String name;
|
||||
|
||||
@JsonProperty("EndpointId")
|
||||
private Long endpointId;
|
||||
private String status;
|
||||
|
||||
@JsonProperty("Status")
|
||||
private int status;
|
||||
|
||||
@JsonProperty("ResourceControl")
|
||||
private String resourceControl;
|
||||
|
||||
public PortainerStack() {}
|
||||
@ -33,11 +45,11 @@ public class PortainerStack {
|
||||
this.endpointId = endpointId;
|
||||
}
|
||||
|
||||
public String getStatus() {
|
||||
public int getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(String status) {
|
||||
public void setStatus(int status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,223 @@
|
||||
package com.hithomelabs.common.portainer.client;
|
||||
|
||||
import com.hithomelabs.common.portainer.client.exception.PortainerAuthenticationException;
|
||||
import com.hithomelabs.common.portainer.client.exception.PortainerConnectionException;
|
||||
import com.hithomelabs.common.portainer.model.PortainerAuthRequest;
|
||||
import com.hithomelabs.common.portainer.model.PortainerAuthResponse;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.http.*;
|
||||
import org.springframework.web.client.HttpClientErrorException;
|
||||
import org.springframework.web.client.ResourceAccessException;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class PortainerApiClientTest {
|
||||
|
||||
@Mock
|
||||
private RestTemplate restTemplate;
|
||||
|
||||
private PortainerApiClient client;
|
||||
|
||||
private static final String BASE_URL = "http://portainer.local:9000";
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
client = new PortainerApiClient(restTemplate, BASE_URL);
|
||||
}
|
||||
|
||||
// -- authenticateWithPassword: success path --
|
||||
|
||||
@Test
|
||||
void authenticateWithPassword_validCredentials_returnsAuthResponse() {
|
||||
PortainerAuthResponse authResponse = new PortainerAuthResponse();
|
||||
authResponse.setJwt("test-jwt-token");
|
||||
|
||||
ResponseEntity<PortainerAuthResponse> responseEntity = new ResponseEntity<>(
|
||||
authResponse, HttpStatus.OK);
|
||||
|
||||
when(restTemplate.exchange(
|
||||
eq(BASE_URL + "/api/auth"),
|
||||
eq(HttpMethod.POST),
|
||||
any(HttpEntity.class),
|
||||
eq(PortainerAuthResponse.class)))
|
||||
.thenReturn(responseEntity);
|
||||
|
||||
PortainerAuthResponse result = client.authenticateWithPassword("admin", "password123");
|
||||
|
||||
assertNotNull(result);
|
||||
assertEquals("test-jwt-token", result.getJwt());
|
||||
verify(restTemplate).exchange(
|
||||
eq(BASE_URL + "/api/auth"),
|
||||
eq(HttpMethod.POST),
|
||||
any(HttpEntity.class),
|
||||
eq(PortainerAuthResponse.class));
|
||||
}
|
||||
|
||||
// -- authenticateWithPassword: input validation --
|
||||
|
||||
@Test
|
||||
void authenticateWithPassword_nullUsername_throwsException() {
|
||||
assertThrows(PortainerAuthenticationException.class,
|
||||
() -> client.authenticateWithPassword(null, "password"));
|
||||
verifyNoInteractions(restTemplate);
|
||||
}
|
||||
|
||||
@Test
|
||||
void authenticateWithPassword_blankUsername_throwsException() {
|
||||
assertThrows(PortainerAuthenticationException.class,
|
||||
() -> client.authenticateWithPassword(" ", "password"));
|
||||
verifyNoInteractions(restTemplate);
|
||||
}
|
||||
|
||||
@Test
|
||||
void authenticateWithPassword_nullPassword_throwsException() {
|
||||
assertThrows(PortainerAuthenticationException.class,
|
||||
() -> client.authenticateWithPassword("admin", null));
|
||||
verifyNoInteractions(restTemplate);
|
||||
}
|
||||
|
||||
@Test
|
||||
void authenticateWithPassword_blankPassword_throwsException() {
|
||||
assertThrows(PortainerAuthenticationException.class,
|
||||
() -> client.authenticateWithPassword("admin", " "));
|
||||
verifyNoInteractions(restTemplate);
|
||||
}
|
||||
|
||||
// -- authenticateWithPassword: HTTP error handling --
|
||||
|
||||
@Test
|
||||
void authenticateWithPassword_http401_throwsAuthenticationException() {
|
||||
when(restTemplate.exchange(
|
||||
anyString(),
|
||||
eq(HttpMethod.POST),
|
||||
any(HttpEntity.class),
|
||||
eq(PortainerAuthResponse.class)))
|
||||
.thenThrow(HttpClientErrorException.create(
|
||||
HttpStatus.UNAUTHORIZED,
|
||||
"Unauthorized",
|
||||
HttpHeaders.EMPTY,
|
||||
"Bad credentials".getBytes(StandardCharsets.UTF_8),
|
||||
StandardCharsets.UTF_8));
|
||||
|
||||
PortainerAuthenticationException ex = assertThrows(PortainerAuthenticationException.class,
|
||||
() -> client.authenticateWithPassword("admin", "wrong"));
|
||||
assertTrue(ex.getMessage().contains("401"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void authenticateWithPassword_http403_throwsAuthenticationException() {
|
||||
when(restTemplate.exchange(
|
||||
anyString(),
|
||||
eq(HttpMethod.POST),
|
||||
any(HttpEntity.class),
|
||||
eq(PortainerAuthResponse.class)))
|
||||
.thenThrow(HttpClientErrorException.create(
|
||||
HttpStatus.FORBIDDEN,
|
||||
"Forbidden",
|
||||
HttpHeaders.EMPTY,
|
||||
"Forbidden".getBytes(StandardCharsets.UTF_8),
|
||||
StandardCharsets.UTF_8));
|
||||
|
||||
assertThrows(PortainerAuthenticationException.class,
|
||||
() -> client.authenticateWithPassword("admin", "wrong"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void authenticateWithPassword_connectionError_throwsConnectionException() {
|
||||
when(restTemplate.exchange(
|
||||
anyString(),
|
||||
eq(HttpMethod.POST),
|
||||
any(HttpEntity.class),
|
||||
eq(PortainerAuthResponse.class)))
|
||||
.thenThrow(new ResourceAccessException("Connection refused"));
|
||||
|
||||
PortainerConnectionException ex = assertThrows(PortainerConnectionException.class,
|
||||
() -> client.authenticateWithPassword("admin", "password"));
|
||||
assertTrue(ex.getMessage().contains(BASE_URL));
|
||||
assertTrue(ex.getMessage().contains("Connection refused"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void authenticateWithPassword_nonOkStatus_throwsAuthenticationException() {
|
||||
PortainerAuthResponse authResponse = new PortainerAuthResponse();
|
||||
authResponse.setJwt("token");
|
||||
// Return a non-OK status with null body to trigger the status check path
|
||||
ResponseEntity<PortainerAuthResponse> responseEntity = new ResponseEntity<>(
|
||||
null, HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
|
||||
when(restTemplate.exchange(
|
||||
anyString(),
|
||||
eq(HttpMethod.POST),
|
||||
any(HttpEntity.class),
|
||||
eq(PortainerAuthResponse.class)))
|
||||
.thenReturn(responseEntity);
|
||||
|
||||
PortainerAuthenticationException ex = assertThrows(PortainerAuthenticationException.class,
|
||||
() -> client.authenticateWithPassword("admin", "password"));
|
||||
assertTrue(ex.getMessage().contains("500"));
|
||||
}
|
||||
|
||||
// -- authenticateWithApiKey: input validation --
|
||||
|
||||
@Test
|
||||
void authenticateWithApiKey_nullKey_throwsException() {
|
||||
assertThrows(PortainerAuthenticationException.class,
|
||||
() -> client.authenticateWithApiKey(null));
|
||||
verifyNoInteractions(restTemplate);
|
||||
}
|
||||
|
||||
@Test
|
||||
void authenticateWithApiKey_blankKey_throwsException() {
|
||||
assertThrows(PortainerAuthenticationException.class,
|
||||
() -> client.authenticateWithApiKey(" "));
|
||||
verifyNoInteractions(restTemplate);
|
||||
}
|
||||
|
||||
@Test
|
||||
void authenticateWithApiKey_validKey_setsApiKey() {
|
||||
client.authenticateWithApiKey("ptr_abc123def456");
|
||||
// No exception expected; we verify it doesn't throw
|
||||
// We can test this by calling a subsequent method that uses the API key
|
||||
assertDoesNotThrow(() -> client.authenticateWithApiKey("ptr_abc123def456"));
|
||||
}
|
||||
|
||||
// -- Constructor: trailing slash handling --
|
||||
|
||||
@Test
|
||||
void constructor_withTrailingSlash_stripsIt() {
|
||||
PortainerApiClient clientWithSlash = new PortainerApiClient(restTemplate, "http://portainer.local:9000/");
|
||||
// authenticateWithPassword should use the stripped URL
|
||||
PortainerAuthResponse authResponse = new PortainerAuthResponse();
|
||||
authResponse.setJwt("token");
|
||||
ResponseEntity<PortainerAuthResponse> responseEntity = new ResponseEntity<>(
|
||||
authResponse, HttpStatus.OK);
|
||||
|
||||
when(restTemplate.exchange(
|
||||
eq("http://portainer.local:9000/api/auth"),
|
||||
eq(HttpMethod.POST),
|
||||
any(HttpEntity.class),
|
||||
eq(PortainerAuthResponse.class)))
|
||||
.thenReturn(responseEntity);
|
||||
|
||||
PortainerAuthResponse result = clientWithSlash.authenticateWithPassword("admin", "pass");
|
||||
assertNotNull(result);
|
||||
assertEquals("token", result.getJwt());
|
||||
}
|
||||
|
||||
@Test
|
||||
void constructor_withNullBaseUrl_usesEmptyString() {
|
||||
PortainerApiClient clientNullUrl = new PortainerApiClient(restTemplate, null);
|
||||
// Should not throw; the client is created with an empty base URL
|
||||
assertDoesNotThrow(() -> clientNullUrl.authenticateWithApiKey("key"));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,107 @@
|
||||
package com.hithomelabs.common.registry.model;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class ImageReferenceTest {
|
||||
|
||||
@Test
|
||||
void parse_withValidImage_returnsImageReference() {
|
||||
ImageReference ref = ImageReference.parse("192.168.0.100:8928/hithomelabs/cftunnels:1.2.3");
|
||||
|
||||
assertNotNull(ref);
|
||||
assertEquals("192.168.0.100:8928", ref.getRegistry());
|
||||
assertEquals("hithomelabs/cftunnels", ref.getRepository());
|
||||
assertEquals("1.2.3", ref.getTag());
|
||||
assertEquals("192.168.0.100:8928/hithomelabs/cftunnels:1.2.3", ref.getFullImage());
|
||||
}
|
||||
|
||||
@Test
|
||||
void parse_withRegistryPortAndMultiLevelRepository_returnsImageReference() {
|
||||
ImageReference ref = ImageReference.parse("registry.example.com:5000/namespace/sub/image:latest");
|
||||
|
||||
assertNotNull(ref);
|
||||
assertEquals("registry.example.com:5000", ref.getRegistry());
|
||||
assertEquals("namespace/sub/image", ref.getRepository());
|
||||
assertEquals("latest", ref.getTag());
|
||||
}
|
||||
|
||||
@Test
|
||||
void parse_withSimpleRegistryPath_returnsImageReference() {
|
||||
ImageReference ref = ImageReference.parse("docker.io/library/nginx:1.25");
|
||||
|
||||
assertNotNull(ref);
|
||||
assertEquals("docker.io", ref.getRegistry());
|
||||
assertEquals("library/nginx", ref.getRepository());
|
||||
assertEquals("1.25", ref.getTag());
|
||||
}
|
||||
|
||||
@Test
|
||||
void parse_withNumericTag_returnsImageReference() {
|
||||
ImageReference ref = ImageReference.parse("myregistry.com/myapp:1");
|
||||
|
||||
assertNotNull(ref);
|
||||
assertEquals("myregistry.com", ref.getRegistry());
|
||||
assertEquals("myapp", ref.getRepository());
|
||||
assertEquals("1", ref.getTag());
|
||||
}
|
||||
|
||||
@Test
|
||||
void parse_withTagContainingHyphenAndDot_returnsImageReference() {
|
||||
ImageReference ref = ImageReference.parse("private.reg.io/team-app:3.2.1-rc1");
|
||||
|
||||
assertNotNull(ref);
|
||||
assertEquals("private.reg.io", ref.getRegistry());
|
||||
assertEquals("team-app", ref.getRepository());
|
||||
assertEquals("3.2.1-rc1", ref.getTag());
|
||||
}
|
||||
|
||||
@Test
|
||||
void parse_withNullImage_throwsIllegalArgumentException() {
|
||||
IllegalArgumentException ex = assertThrows(IllegalArgumentException.class,
|
||||
() -> ImageReference.parse(null));
|
||||
assertTrue(ex.getMessage().contains("null") || ex.getMessage().contains("blank"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void parse_withBlankImage_throwsIllegalArgumentException() {
|
||||
IllegalArgumentException ex = assertThrows(IllegalArgumentException.class,
|
||||
() -> ImageReference.parse(" "));
|
||||
assertTrue(ex.getMessage().contains("null") || ex.getMessage().contains("blank"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void parse_withEmptyImage_throwsIllegalArgumentException() {
|
||||
IllegalArgumentException ex = assertThrows(IllegalArgumentException.class,
|
||||
() -> ImageReference.parse(""));
|
||||
assertTrue(ex.getMessage().contains("null") || ex.getMessage().contains("blank"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void parse_withMissingRegistry_throwsIllegalArgumentException() {
|
||||
IllegalArgumentException ex = assertThrows(IllegalArgumentException.class,
|
||||
() -> ImageReference.parse("nginx:latest"));
|
||||
assertTrue(ex.getMessage().contains("Invalid"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void parse_withMissingTag_throwsIllegalArgumentException() {
|
||||
IllegalArgumentException ex = assertThrows(IllegalArgumentException.class,
|
||||
() -> ImageReference.parse("registry.com/repo"));
|
||||
assertTrue(ex.getMessage().contains("Invalid"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void parse_withMissingColonSeparator_throwsIllegalArgumentException() {
|
||||
IllegalArgumentException ex = assertThrows(IllegalArgumentException.class,
|
||||
() -> ImageReference.parse("registry.com/repo:"));
|
||||
assertTrue(ex.getMessage().contains("Invalid"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void parse_withOnlyRegistry_throwsIllegalArgumentException() {
|
||||
IllegalArgumentException ex = assertThrows(IllegalArgumentException.class,
|
||||
() -> ImageReference.parse("registry.com"));
|
||||
assertTrue(ex.getMessage().contains("Invalid"));
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user