Skip to content

Commit 1bff739

Browse files
namedgraphclaude
andcommitted
Add unit tests for graph store, RDF import streaming, and proxied WebID auth
DocumentHierarchyGraphStoreImplTest covers the stateless logic (getChangedResources, getLastModified, writeFile/writeFiles guards and file I/O) via a CALLS_REAL_METHODS mock, since the class only exposes a heavyweight @Inject constructor. StreamRDFOutputWriterTest covers the RDF-import streaming path: null guard, getter round-trip, and the non-RDF media type -> BadRequestException branch. The CSV transformation itself is covered upstream in CSV2RDF. ProxiedWebIDFilterTest covers Client-Cert header PEM parsing into an X509Certificate, absent header, and malformed input. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent a6d71b5 commit 1bff739

3 files changed

Lines changed: 489 additions & 0 deletions

File tree

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
/**
2+
* Copyright 2025 Martynas Jusevičius <martynas@atomgraph.com>
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
*/
17+
package com.atomgraph.linkeddatahub.imports.stream;
18+
19+
import com.atomgraph.linkeddatahub.client.GraphStoreClient;
20+
import com.atomgraph.linkeddatahub.model.Service;
21+
import java.io.ByteArrayInputStream;
22+
import java.io.InputStream;
23+
import jakarta.ws.rs.BadRequestException;
24+
import jakarta.ws.rs.core.MediaType;
25+
import jakarta.ws.rs.core.Response;
26+
import org.apache.jena.query.Query;
27+
import org.apache.jena.query.QueryFactory;
28+
import org.junit.jupiter.api.BeforeEach;
29+
import org.junit.jupiter.api.Test;
30+
import org.junit.jupiter.api.extension.ExtendWith;
31+
import org.mockito.Mock;
32+
import org.mockito.junit.jupiter.MockitoExtension;
33+
import static org.junit.jupiter.api.Assertions.assertEquals;
34+
import static org.junit.jupiter.api.Assertions.assertSame;
35+
import static org.junit.jupiter.api.Assertions.assertThrows;
36+
import static org.mockito.Mockito.lenient;
37+
import static org.mockito.Mockito.mock;
38+
import static org.mockito.Mockito.when;
39+
40+
/**
41+
* Unit tests for {@link StreamRDFOutputWriter}.
42+
*
43+
* @author Martynas Jusevičius {@literal <martynas@atomgraph.com>}
44+
*/
45+
@ExtendWith(MockitoExtension.class)
46+
public class StreamRDFOutputWriterTest
47+
{
48+
49+
@Mock private Service service;
50+
@Mock private Service adminService;
51+
@Mock private com.atomgraph.linkeddatahub.Application system;
52+
@Mock private GraphStoreClient gsc;
53+
54+
private static final String BASE_URI = "http://localhost/";
55+
private static final String GRAPH_URI = "http://localhost/graphs/import";
56+
57+
private Query query;
58+
private StreamRDFOutputWriter writer;
59+
60+
@BeforeEach
61+
public void setUp()
62+
{
63+
query = QueryFactory.create("CONSTRUCT WHERE { ?s ?p ?o }");
64+
writer = new StreamRDFOutputWriter(service, adminService, system, gsc, BASE_URI, query, GRAPH_URI);
65+
}
66+
67+
@Test
68+
public void testGettersRoundTrip()
69+
{
70+
assertSame(service, writer.getService());
71+
assertSame(adminService, writer.getAdminService());
72+
assertSame(system, writer.getSystem());
73+
assertSame(gsc, writer.getGraphStoreClient());
74+
assertEquals(BASE_URI, writer.getBaseURI());
75+
assertSame(query, writer.getQuery());
76+
assertEquals(GRAPH_URI, writer.getGraphURI());
77+
}
78+
79+
@Test
80+
public void testApplyNullResponse()
81+
{
82+
assertThrows(IllegalArgumentException.class, () -> writer.apply(null));
83+
}
84+
85+
/**
86+
* A response whose {@code Content-Type} does not map to an RDF language must be rejected with
87+
* {@code 400 Bad Request} — after the body has been buffered to a temp file but before any
88+
* Graph Store write is attempted.
89+
*/
90+
@Test
91+
public void testApplyNonRdfMediaTypeThrowsBadRequest()
92+
{
93+
Response response = mock(Response.class);
94+
lenient().when(response.readEntity(InputStream.class)).thenReturn((InputStream)new ByteArrayInputStream("not rdf".getBytes()));
95+
when(response.getMediaType()).thenReturn(MediaType.valueOf("image/png"));
96+
97+
assertThrows(BadRequestException.class, () -> writer.apply(response));
98+
}
99+
100+
}
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
/**
2+
* Copyright 2025 Martynas Jusevičius <martynas@atomgraph.com>
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
*/
17+
package com.atomgraph.linkeddatahub.server.filter.request.auth;
18+
19+
import jakarta.ws.rs.container.ContainerRequestContext;
20+
import java.net.URLEncoder;
21+
import java.nio.charset.StandardCharsets;
22+
import java.security.cert.CertificateException;
23+
import java.security.cert.X509Certificate;
24+
import org.junit.jupiter.api.BeforeEach;
25+
import org.junit.jupiter.api.Test;
26+
import org.junit.jupiter.api.extension.ExtendWith;
27+
import org.mockito.Mock;
28+
import org.mockito.junit.jupiter.MockitoExtension;
29+
import static org.junit.jupiter.api.Assertions.assertEquals;
30+
import static org.junit.jupiter.api.Assertions.assertNotNull;
31+
import static org.junit.jupiter.api.Assertions.assertNull;
32+
import static org.junit.jupiter.api.Assertions.assertThrows;
33+
import static org.junit.jupiter.api.Assertions.assertTrue;
34+
import static org.mockito.Mockito.when;
35+
36+
/**
37+
* Unit tests for {@link ProxiedWebIDFilter}, which extracts the client certificate from the
38+
* {@code Client-Cert} request header (URL-encoded PEM) instead of the TLS connection.
39+
*
40+
* @author Martynas Jusevičius {@literal <martynas@atomgraph.com>}
41+
*/
42+
@ExtendWith(MockitoExtension.class)
43+
public class ProxiedWebIDFilterTest
44+
{
45+
46+
private static final String CLIENT_CERT_HEADER_NAME = "Client-Cert";
47+
48+
/** A self-signed X.509 certificate (CN=Test WebID, O=LinkedDataHub) in PEM form. */
49+
private static final String CERT_PEM =
50+
"-----BEGIN CERTIFICATE-----\n" +
51+
"MIIC1jCCAb4CCQD35LqgLKP0ATANBgkqhkiG9w0BAQsFADAtMRMwEQYDVQQDDApU\n" +
52+
"ZXN0IFdlYklEMRYwFAYDVQQKDA1MaW5rZWREYXRhSHViMB4XDTI2MDYxODA4Mzkz\n" +
53+
"OFoXDTI2MDYxOTA4MzkzOFowLTETMBEGA1UEAwwKVGVzdCBXZWJJRDEWMBQGA1UE\n" +
54+
"CgwNTGlua2VkRGF0YUh1YjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB\n" +
55+
"AMRsMfh0IikmxDbZ0kca2FRI+WXelczdi+dU9ZC42cAZJM6pEu9icvCdTKBKipYE\n" +
56+
"07PkfAgsmkS3qzly1iQzyXZFzopndg9FdFvWZyn8SdxNSKCcQt13NTp8sXflkdxK\n" +
57+
"SfOseUx1cZ0T4ylGNwkqxcqZo5b06CJqiZqjgO4x7kYWWrli44AgzMkT3AgJqq5X\n" +
58+
"iSo5j8gOjicR+ZywLAEvWH0ITja4sIgsQzZHxbquOuPEevnT+135M33wHxsY5MHJ\n" +
59+
"Ykxid7C4ifVm4jXf81CmnoCifR9UeBnMZ0QBPBP/Exv+CpTgb3TBAfF1o1QsEuM3\n" +
60+
"60fYmqLcwLiKgZqDJ7ZH80UCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAeYJGVnFq\n" +
61+
"CARK15JQQk1YBAUPspkFWAeXH9UzYyxpqt0bLlYO9g4KExJVJvE9Qub2lHXBs36j\n" +
62+
"/elRF+PR5Zt/6LD26OnSu+QWkFSqbO6Otul7g9ikMufuhNrZyyOOzidqFfcfkhWx\n" +
63+
"FZh+yZhGoo2f+ddMuYbK3lKI+/DMswfdNN6VN++EOYskjWBB85GKUxEJTLEF2yE+\n" +
64+
"yRtqnQfX3ucvO2Zd1XHsgknzoSfG8CXZF3GDcqzzEZ6Aa//xtwYRCmNmj9E9SdMY\n" +
65+
"xuCHnQP3cV/vBBhxt1BWdIRtcU6xpasNMfWGgAxqrCTz+GnT7FExbe5qt6CgX7yl\n" +
66+
"JLw8c9VNQzsM9g==\n" +
67+
"-----END CERTIFICATE-----\n";
68+
69+
@Mock private ContainerRequestContext requestContext;
70+
71+
private ProxiedWebIDFilter filter;
72+
73+
@BeforeEach
74+
public void setUp()
75+
{
76+
filter = new ProxiedWebIDFilter();
77+
}
78+
79+
@Test
80+
public void testCertificateFactoryIsX509()
81+
{
82+
assertNotNull(filter.getCertificateFactory());
83+
assertEquals("X.509", filter.getCertificateFactory().getType());
84+
}
85+
86+
/** No {@code Client-Cert} header — no certificate. */
87+
@Test
88+
public void testNoHeaderReturnsNull() throws Exception
89+
{
90+
when(requestContext.getHeaderString(CLIENT_CERT_HEADER_NAME)).thenReturn(null);
91+
assertNull(filter.getWebIDCertificate(requestContext));
92+
}
93+
94+
/** A URL-encoded PEM certificate in the header is decoded and parsed into an X509Certificate. */
95+
@Test
96+
public void testValidHeaderParsesCertificate() throws Exception
97+
{
98+
String encoded = URLEncoder.encode(CERT_PEM, StandardCharsets.UTF_8);
99+
when(requestContext.getHeaderString(CLIENT_CERT_HEADER_NAME)).thenReturn(encoded);
100+
101+
X509Certificate cert = filter.getWebIDCertificate(requestContext);
102+
103+
assertNotNull(cert);
104+
assertTrue(cert.getSubjectX500Principal().getName().contains("Test WebID"));
105+
}
106+
107+
/** A header that is not a valid certificate must surface as a CertificateException. */
108+
@Test
109+
public void testMalformedHeaderThrows()
110+
{
111+
when(requestContext.getHeaderString(CLIENT_CERT_HEADER_NAME)).thenReturn("garbage-not-a-cert");
112+
assertThrows(CertificateException.class, () -> filter.getWebIDCertificate(requestContext));
113+
}
114+
115+
}

0 commit comments

Comments
 (0)