diff --git a/broker-auth-local/src/main/java/org/aktin/broker/auth/apikey/ApiKeyDTO.java b/broker-auth-local/src/main/java/org/aktin/broker/auth/apikey/ApiKeyDTO.java new file mode 100644 index 00000000..826c6d19 --- /dev/null +++ b/broker-auth-local/src/main/java/org/aktin/broker/auth/apikey/ApiKeyDTO.java @@ -0,0 +1,35 @@ +package org.aktin.broker.auth.apikey; + +import javax.xml.bind.annotation.XmlRootElement; + +/** + * Data Transfer Object (DTO) for API key information. This class is used to transfer API key and client DN information between client and server in a + * XML format. + * + * @author akombeiz@ukaachen.de + */ +@XmlRootElement(name = "ApiKeyCred") +public class ApiKeyDTO { + + private String apiKey; + private String clientDn; + + public ApiKeyDTO() { + } + + public String getApiKey() { + return apiKey; + } + + public void setApiKey(String apiKey) { + this.apiKey = apiKey; + } + + public String getClientDn() { + return clientDn; + } + + public void setClientDn(String clientDn) { + this.clientDn = clientDn; + } +} diff --git a/broker-auth-local/src/main/java/org/aktin/broker/auth/apikey/ApiKeyManagementEndpoint.java b/broker-auth-local/src/main/java/org/aktin/broker/auth/apikey/ApiKeyManagementEndpoint.java new file mode 100644 index 00000000..9168f495 --- /dev/null +++ b/broker-auth-local/src/main/java/org/aktin/broker/auth/apikey/ApiKeyManagementEndpoint.java @@ -0,0 +1,149 @@ +package org.aktin.broker.auth.apikey; + +import java.io.IOException; +import java.util.NoSuchElementException; +import java.util.Properties; +import java.util.logging.Logger; +import javax.inject.Inject; +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; +import org.aktin.broker.rest.Authenticated; +import org.aktin.broker.rest.RequireAdmin; + +/** + * RESTful endpoint for managing API keys. This class provides operations to retrieve, create, activate, and deactivate API keys. Access to these + * operations requires authentication and admin privileges. + * + * @author akombeiz@ukaachen.de + */ +@Authenticated +@RequireAdmin +@Path("api-keys") +public class ApiKeyManagementEndpoint { + + private static final Logger log = Logger.getLogger(ApiKeyManagementEndpoint.class.getName()); + + @Inject + ApiKeyPropertiesAuthProvider authProvider; + + /** + * Retrieves all API keys. + * + * @return A Response with {@code 200} containing all API keys as a string, or {@code 500} if retrieval fails. + */ + @GET + public Response getApiKeys() { + try { + PropertyFileAPIKeys apiKeys = authProvider.getInstance(); + Properties props = apiKeys.getProperties(); + return Response.ok(convertPropertiesToString(props)).build(); + } catch (IOException e) { + log.severe("Error retrieving API keys: " + e.getMessage()); + return Response.status(Status.INTERNAL_SERVER_ERROR).build(); + } + } + + /** + * Converts Properties to a string representation. + * + * @param props The Properties object to convert. + * @return A string representation of the properties. + */ + private String convertPropertiesToString(Properties props) { + StringBuilder response = new StringBuilder(); + for (String name : props.stringPropertyNames()) { + response.append(name).append("=").append(props.getProperty(name)).append("\n"); + } + return response.toString(); + } + + /** + * Creates a new API key. + * + * @param apiKeyDTO The DTO containing the API key and client DN. + * @return A Response indicating the result of the operation: + */ + @POST + @Consumes(MediaType.APPLICATION_XML) + public Response createApiKey(ApiKeyDTO apiKeyDTO) { + String apiKey = apiKeyDTO.getApiKey(); + String clientDn = apiKeyDTO.getClientDn(); + if (apiKey == null || clientDn == null) { + return Response.status(Status.BAD_REQUEST).build(); + } + try { + authProvider.addNewApiKeyAndUpdatePropertiesFile(apiKey, clientDn); + return Response.status(Status.CREATED).build(); + } catch (IllegalArgumentException e) { + return Response.status(Status.CONFLICT).build(); + } catch (IOException e) { + return Response.status(Status.INTERNAL_SERVER_ERROR).build(); + } + } + + /** + * Activates an API key. + * + * @param apiKey The API key to activate. + * @return A Response indicating the result of the operation. + */ + @POST + @Path("{apiKey}/activate") + public Response activateApiKey(@PathParam("apiKey") String apiKey) { + return setApiKeyStatus(apiKey, ApiKeyStatus.ACTIVE); + } + + /** + * Deactivates an API key. + * + * @param apiKey The API key to deactivate. + * @return A Response indicating the result of the operation. + */ + @POST + @Path("{apiKey}/deactivate") + public Response deactivateApiKey(@PathParam("apiKey") String apiKey) { + return setApiKeyStatus(apiKey, ApiKeyStatus.INACTIVE); + } + + /** + * Sets the status of an API key. + * + * @param apiKey The API key to update. + * @param status The new status for the API key. + * @return A Response indicating the result of the operation: + */ + private Response setApiKeyStatus(String apiKey, ApiKeyStatus status) { + if (apiKey == null || status == null) { + return Response.status(Status.BAD_REQUEST).build(); + } + try { + authProvider.setStateOfApiKeyAndUpdatePropertiesFile(apiKey, status); + return Response.ok().build(); + } catch (NoSuchElementException e) { + return Response.status(Status.NOT_FOUND).build(); + } catch (SecurityException e) { + return Response.status(Status.FORBIDDEN).build(); + } catch (IllegalArgumentException e) { + return Response.status(Status.BAD_REQUEST).build(); + } catch (IOException e) { + return Response.status(Status.INTERNAL_SERVER_ERROR).build(); + } + } +} diff --git a/broker-auth-local/src/main/java/org/aktin/broker/auth/apikey/ApiKeyPropertiesAuthProvider.java b/broker-auth-local/src/main/java/org/aktin/broker/auth/apikey/ApiKeyPropertiesAuthProvider.java index fe87ae30..7cfd58e9 100644 --- a/broker-auth-local/src/main/java/org/aktin/broker/auth/apikey/ApiKeyPropertiesAuthProvider.java +++ b/broker-auth-local/src/main/java/org/aktin/broker/auth/apikey/ApiKeyPropertiesAuthProvider.java @@ -2,36 +2,186 @@ import java.io.IOException; import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; - +import java.nio.file.Path; +import java.util.NoSuchElementException; +import java.util.Properties; +import java.util.function.BiConsumer; import org.aktin.broker.server.auth.AbstractAuthProvider; +/** + * Provides authentication services using API keys stored in a properties file. This class extends {@link AbstractAuthProvider} and manages the + * lifecycle of API keys, including loading, adding, updating, and persisting them. + * + * @author akombeiz@ukaachen.de + */ public class ApiKeyPropertiesAuthProvider extends AbstractAuthProvider { - private PropertyFileAPIKeys keys; - - public ApiKeyPropertiesAuthProvider() { - keys = null; // lazy init, load later in getInstance by using supplied path - } - public ApiKeyPropertiesAuthProvider(InputStream in) throws IOException { - this.keys = new PropertyFileAPIKeys(in); - } - - - @Override - public PropertyFileAPIKeys getInstance() throws IOException { - if( this.keys == null ) { - // not previously loaded - try( InputStream in = Files.newInputStream(path.resolve("api-keys.properties")) ){ - this.keys = new PropertyFileAPIKeys(in); - } - }else { - ;// already loaded, use existing one - } - return keys; -// if( System.getProperty("rewriteNodeDN") != null ){ -// int count = BrokerImpl.updatePrincipalDN(ds, keys.getMap()); -// // output/log what happened, use count returned from above method -// System.out.println("Rewritten "+count+" node DN strings."); -// } - } + + private PropertyFileAPIKeys keys; + + /** + * Constructs a new ApiKeyPropertiesAuthProvider with lazy initialization. The API keys will be loaded later when getInstance() is called. + */ + public ApiKeyPropertiesAuthProvider() { + keys = null; + } + + /** + * Constructs a new ApiKeyPropertiesAuthProvider and immediately loads API keys from the given input stream. + * + * @param in The input stream containing the API keys properties. + * @throws IOException If an I/O error occurs while reading from the input stream. + */ + public ApiKeyPropertiesAuthProvider(InputStream in) throws IOException { + this.keys = new PropertyFileAPIKeys(in); + } + + /** + * Retrieves or initializes the PropertyFileAPIKeys instance. + * + * @return The PropertyFileAPIKeys instance. + * @throws IOException If an I/O error occurs while loading the properties file. + */ + @Override + public PropertyFileAPIKeys getInstance() throws IOException { + if (this.keys == null) { + try (InputStream in = Files.newInputStream(getPropertiesPath())) { + this.keys = new PropertyFileAPIKeys(in); + } + } + return keys; + } + + /** + * @return The Path object representing the location of API keys properties file + */ + private Path getPropertiesPath() { + return path.resolve("api-keys.properties"); + } + + /** + * Adds a new API key and updates the properties file. + * + * @param apiKey The new API key to add. + * @param clientDn The distinguished name of the client associated with the API key. + * @throws IOException If an I/O error occurs while updating the properties file. + * @throws IllegalArgumentException If the API key already exists. + * @throws IllegalStateException If the PropertyFileAPIKeys instance is not initialized. + */ + public void addNewApiKeyAndUpdatePropertiesFile(String apiKey, String clientDn) + throws IOException, IllegalArgumentException, IllegalStateException { + checkKeysInitialized(); + Properties properties = keys.getProperties(); + if (properties.containsKey(apiKey)) { + throw new IllegalArgumentException("API key already exists: " + apiKey); + } + keys.putApiKey(apiKey, clientDn); + saveProperties(keys); + } + + /** + * Sets the state of an existing API key and updates the properties file. + * + * @param apiKey The API key to update. + * @param status The new status to set for the API key. + * @throws IOException If an I/O error occurs while updating the properties file. + * @throws NoSuchElementException If the API key does not exist. + * @throws SecurityException If an attempt is made to modify an admin API key. + * @throws IllegalStateException If the PropertyFileAPIKeys instance is not initialized. + */ + public void setStateOfApiKeyAndUpdatePropertiesFile(String apiKey, ApiKeyStatus status) + throws IOException, NoSuchElementException, SecurityException, IllegalStateException { + checkKeysInitialized(); + Properties properties = keys.getProperties(); + if (!properties.containsKey(apiKey)) { + throw new NoSuchElementException("API key does not exist"); + } + String clientDn = properties.getProperty(apiKey); + checkNotAdminKey(clientDn); + String updatedClientDn = setStatusInClientDn(clientDn, status); + if (!clientDn.equals(updatedClientDn)) { + keys.putApiKey(apiKey, updatedClientDn); + saveProperties(keys); + } + } + + /** + * Checks if the given client DN belongs to an admin key. + * + * @param clientDn The client distinguished name to check. + * @throws SecurityException If the client DN indicates an admin key. + */ + private void checkNotAdminKey(String clientDn) { + if (clientDn != null && clientDn.contains("OU=admin")) { + throw new SecurityException("Admin API key state cannot be modified"); + } + } + + /** + * Updates the status in the client DN string. + * + * @param clientDn The original client DN. + * @param status The new status to set. + * @return The updated client DN string with the new status. + * @throws IllegalArgumentException If an unknown status is provided. + */ + private String setStatusInClientDn(String clientDn, ApiKeyStatus status) { + switch (status) { + case ACTIVE: + return clientDn.replace("," + ApiKeyStatus.INACTIVE.name(), ""); + case INACTIVE: + if (clientDn.endsWith(ApiKeyStatus.INACTIVE.name())) { + return clientDn; + } else { + return clientDn + "," + ApiKeyStatus.INACTIVE.name(); + } + default: + throw new IllegalArgumentException("Unknown status: " + status.name()); + } + } + + /** + * Checks if the API keys instance is initialized. + * + * @throws IllegalStateException If the API keys instance is not initialized. + */ + private void checkKeysInitialized() { + if (this.keys == null) { + throw new IllegalStateException("API keys instance is not initialized"); + } + } + + /** + * Saves the current state of API keys to the properties file in a latin-1 encoding. + * + * @param instance The PropertyFileAPIKeys instance to save. + * @throws IOException If an I/O error occurs while writing to the properties file. + */ + private void saveProperties(PropertyFileAPIKeys instance) throws IOException { + try (OutputStream out = Files.newOutputStream(getPropertiesPath())) { + instance.storeProperties(out, StandardCharsets.ISO_8859_1); + } + } + + /** + * Returns the array of endpoint classes associated with this auth provider. + * + * @return An array containing the ApiKeyManagementEndpoint class. + */ + @Override + public Class[] getEndpoints() { + return new Class[]{ApiKeyManagementEndpoint.class}; + } + + /** + * Binds this instance to the ApiKeyPropertiesAuthProvider class so it can be injected into the associated endpoints of {@link #getEndpoints()}. + * + * @param binder The binder function to use for binding. + */ + @Override + public void bindSingletons(BiConsumer> binder) { + binder.accept(this, ApiKeyPropertiesAuthProvider.class); + } } diff --git a/broker-auth-local/src/main/java/org/aktin/broker/auth/apikey/ApiKeyStatus.java b/broker-auth-local/src/main/java/org/aktin/broker/auth/apikey/ApiKeyStatus.java new file mode 100644 index 00000000..1c0e6a9b --- /dev/null +++ b/broker-auth-local/src/main/java/org/aktin/broker/auth/apikey/ApiKeyStatus.java @@ -0,0 +1,20 @@ +package org.aktin.broker.auth.apikey; + +/** + * Enum representing the status of an API key in the AKTIN broker authentication system. + * + * @author akombeiz@ukaachen.de + */ +public enum ApiKeyStatus { + /** + * Represents an active API key. This status is considered valid and operational within the system. + */ + ACTIVE, + + /** + * Represents an inactive API key. An API key with this status cannot be used for authentication purposes. Typically used for keys that have been + * revoked, expired, or temporarily suspended. + */ + INACTIVE; + +} diff --git a/broker-auth-local/src/main/java/org/aktin/broker/auth/apikey/PropertyFileAPIKeys.java b/broker-auth-local/src/main/java/org/aktin/broker/auth/apikey/PropertyFileAPIKeys.java index 239c62e0..acfc517b 100644 --- a/broker-auth-local/src/main/java/org/aktin/broker/auth/apikey/PropertyFileAPIKeys.java +++ b/broker-auth-local/src/main/java/org/aktin/broker/auth/apikey/PropertyFileAPIKeys.java @@ -2,31 +2,97 @@ import java.io.IOException; import java.io.InputStream; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.nio.charset.Charset; import java.util.Properties; import java.util.logging.Logger; - import org.aktin.broker.server.auth.AuthInfo; import org.aktin.broker.server.auth.AuthInfoImpl; import org.aktin.broker.server.auth.HttpBearerAuthentication; +/** + * Implements API key authentication using a property file as the storage mechanism. This class extends {@link HttpBearerAuthentication} to provide + * bearer token authentication with API keys stored in a properties file. + * + * @author akombeiz@ukaachen.de + */ public class PropertyFileAPIKeys extends HttpBearerAuthentication { - private static final Logger log = Logger.getLogger(PropertyFileAPIKeys.class.getName()); - - private Properties properties; - - public PropertyFileAPIKeys(InputStream in) throws IOException { - properties = new Properties(); - properties.load(in); - log.info("Loaded "+properties.size()+" client API keys"); - } - - @Override - protected AuthInfo lookupAuthInfo(String token) throws IOException { - String clientDn = properties.getProperty(token); - if( clientDn == null ) { - // unauthorized - return null; - } - return new AuthInfoImpl(token, clientDn, HttpBearerAuthentication.defaultRolesForClientDN(clientDn)); - } -} + + private static final Logger log = Logger.getLogger(PropertyFileAPIKeys.class.getName()); + private final Properties properties; + + /** + * Constructs a new PropertyFileAPIKeys instance by loading API keys from an input stream. + * + * @param in The input stream containing the properties file with API keys. + * @throws IOException If an I/O error occurs while reading from the input stream. + */ + public PropertyFileAPIKeys(InputStream in) throws IOException { + this.properties = new Properties(); + properties.load(in); + log.info("Loaded " + properties.size() + " client API keys"); + } + + /** + * @return The Properties object with API keys. + */ + public Properties getProperties() { + return properties; + } + + /** + * Adds or updates an API key for a client. + * + * @param apiKey The API key to add or update. + * @param clientDn The distinguished name of the client associated with the API key. + */ + public void putApiKey(String apiKey, String clientDn) { + properties.setProperty(apiKey, clientDn); + log.info("Put API key for client: " + clientDn); + } + + /** + * Stores the current API keys to an output stream using the specified character set. + * + * @param out The output stream to write the properties to. + * @param charset The character set to use for writing. + * @throws IOException If an I/O error occurs while writing to the output stream. + */ + public void storeProperties(OutputStream out, Charset charset) throws IOException { + try (OutputStreamWriter writer = new OutputStreamWriter(out, charset)) { + properties.store(writer, "API Keys"); + } + log.info("Saved " + properties.size() + " client API keys"); + } + + /** + * Looks up authentication information for a given token. If the token is not found in the properties or is marked as INACTIVE, null is returned. + * INACTIVE will always appear at the end of the Client Distinguished Name. + * + * @param token The API key token to authenticate. + * @return An {@link AuthInfo} object if authentication succeeds, null otherwise. + */ + @Override + protected AuthInfo lookupAuthInfo(String token) { + String clientDn = properties.getProperty(token); + if (clientDn != null) { + String[] parts = clientDn.split(","); + if (!ApiKeyStatus.INACTIVE.name().equals(parts[parts.length - 1])) { + return createAuthInfo(token, clientDn); + } + } + return null; + } + + /** + * Creates an {@link AuthInfo} object for a given token and client DN. + * + * @param token The API key token. + * @param clientDn The distinguished name of the client. + * @return A new {@link AuthInfo} object. + */ + private AuthInfo createAuthInfo(String token, String clientDn) { + return new AuthInfoImpl(token, clientDn, HttpBearerAuthentication.defaultRolesForClientDN(clientDn)); + } +} \ No newline at end of file diff --git a/broker-client-auth-keycloak/pom.xml b/broker-client-auth-keycloak/pom.xml new file mode 100644 index 00000000..e69de29b diff --git a/broker-client/pom.xml b/broker-client/pom.xml index 726de284..c8a814f1 100644 --- a/broker-client/pom.xml +++ b/broker-client/pom.xml @@ -100,7 +100,7 @@ org.json json - 20230227 + 20231013 test diff --git a/broker-client/src/main/java/org/aktin/broker/client/BrokerAdmin.java b/broker-client/src/main/java/org/aktin/broker/client/BrokerAdmin.java index 06b5b954..e23a88d2 100644 --- a/broker-client/src/main/java/org/aktin/broker/client/BrokerAdmin.java +++ b/broker-client/src/main/java/org/aktin/broker/client/BrokerAdmin.java @@ -6,7 +6,10 @@ import java.util.List; import java.util.Properties; -import org.aktin.broker.xml.*; +import org.aktin.broker.xml.RequestInfo; +import org.aktin.broker.xml.RequestStatusInfo; +import org.aktin.broker.xml.RequestStatusList; +import org.aktin.broker.xml.ResultInfo; import org.w3c.dom.Node; public interface BrokerAdmin { @@ -147,11 +150,12 @@ public interface BrokerAdmin { List listResults(int requestId) throws IOException; // TODO ResultInfo getResultInfo(int requestId, String nodeId) - - + String getResultString(int requestId, int nodeId) throws IOException; ResponseWithMetadata getResult(int requestId, int nodeId) throws IOException; + ResponseWithMetadata getRequestBundleExport(int requestId) throws IOException; + /** * Retrieves an array of target nodes that a request is aimed at based on the specified request ID. * diff --git a/broker-client/src/main/java/org/aktin/broker/client/BrokerAdminImpl.java b/broker-client/src/main/java/org/aktin/broker/client/BrokerAdminImpl.java index 1b72dfb3..85c5373c 100644 --- a/broker-client/src/main/java/org/aktin/broker/client/BrokerAdminImpl.java +++ b/broker-client/src/main/java/org/aktin/broker/client/BrokerAdminImpl.java @@ -349,4 +349,9 @@ public void clearRequestTargetNodes(int requestId) throws IOException{ public ResponseWithMetadata getResult(int requestId, int nodeId) throws IOException { throw new UnsupportedOperationException(); } + + @Override + public ResponseWithMetadata getRequestBundleExport(int requestId) throws IOException { + throw new UnsupportedOperationException(); + } } diff --git a/broker-client/src/main/java/org/aktin/broker/client2/BrokerAdmin2.java b/broker-client/src/main/java/org/aktin/broker/client2/BrokerAdmin2.java index 4b0be114..56fb8574 100644 --- a/broker-client/src/main/java/org/aktin/broker/client2/BrokerAdmin2.java +++ b/broker-client/src/main/java/org/aktin/broker/client2/BrokerAdmin2.java @@ -9,6 +9,7 @@ import java.net.URI; import java.net.URISyntaxException; import java.net.URLEncoder; +import java.net.http.HttpHeaders; import java.net.http.HttpRequest; import java.net.http.HttpRequest.BodyPublisher; import java.net.http.HttpRequest.BodyPublishers; @@ -18,6 +19,7 @@ import java.nio.charset.StandardCharsets; import java.util.Collections; import java.util.List; +import java.util.Optional; import java.util.Properties; import javax.xml.bind.JAXB; @@ -310,6 +312,39 @@ public ResponseWithMetadata getResult(int requestId, int nodeId) throws IOExcept HttpResponse resp = getResult(requestId, nodeId, BodyHandlers.ofInputStream()); return wrapResource(resp, requestId+"_result_"+nodeId); } + + public HttpResponse getRequestBundleExport(int requestId, BodyHandler handler) throws IOException { + HttpRequest exportRequest = createBrokerRequest("export/request-bundle/" + requestId) + .POST(BodyPublishers.noBody()).build(); + HttpResponse exportResponse = sendRequest(exportRequest, BodyHandlers.ofString()); + if (exportResponse.statusCode() != 200) + throw new IOException("Unexpected response code " + exportResponse.statusCode() + " (instead of 200) during request bundling"); + String uuid = exportResponse.body(); + HttpRequest downloadRequest = createBrokerRequest("download/" + uuid).GET().build(); + return sendRequest(downloadRequest, handler); + } + + @Override + public ResponseWithMetadata getRequestBundleExport(int requestId) throws IOException { + HttpResponse downloadResponse = getRequestBundleExport(requestId, BodyHandlers.ofInputStream()); + String filename = getFileNameFromHeaders(downloadResponse.headers()); + return wrapResource(downloadResponse, filename); + } + + private String getFileNameFromHeaders(HttpHeaders headers) { + Optional dispositionHeader = headers.firstValue("Content-Disposition"); + if (dispositionHeader.isPresent()) { + String disposition = dispositionHeader.get(); + int startIdx = disposition.indexOf("filename=\""); + if (startIdx != -1) { + int endIdx = disposition.indexOf("\"", startIdx + 10); // 10 is the length of 'filename="' + if (endIdx != -1) { + return disposition.substring(startIdx + 10, endIdx); + } + } + } + return null; + } @Override public int[] getRequestTargetNodes(int requestId) throws IOException { diff --git a/broker-server/src/test/java/org/aktin/broker/TestBroker.java b/broker-server/src/test/java/org/aktin/broker/TestBroker.java index 55bb62c4..01cc9d50 100644 --- a/broker-server/src/test/java/org/aktin/broker/TestBroker.java +++ b/broker-server/src/test/java/org/aktin/broker/TestBroker.java @@ -582,4 +582,29 @@ public void getRequestInfoOfNonexistingRequest() throws IOException { BrokerAdmin a = initializeAdmin(); assertNull(a.getRequestInfo(999)); } + + @Test + public void testGetAggregatedResults() throws IOException, InterruptedException { + BrokerAdmin2 a = initializeAdmin(); + int qid = a.createRequest("text/x-test-1", "test1"); + a.publishRequest(qid); + BrokerClient c = initializeClient(CLIENT_01_SERIAL); + List l = c.listMyRequests(); + Assert.assertEquals(1, l.size()); + c.putRequestResult(l.get(0).getId(), "test/vnd.test.result", new ByteArrayInputStream("test-result-data".getBytes())); + + ResponseWithMetadata result = a.getRequestBundleExport(qid); + Assert.assertEquals("application/zip", result.getContentType()); + Assert.assertEquals("export_" + qid + ".zip", result.getName()); + byte[] actualBytes = result.getInputStream().readAllBytes(); + Assert.assertTrue(actualBytes.length >= 810 && actualBytes.length <= 815); + a.deleteRequest(qid); + Assert.assertTrue(c.listMyRequests().isEmpty()); + } + + @Test + public void testResultsOfUnknownRequest() { + BrokerAdmin2 a = initializeAdmin(); + Assert.assertThrows(IOException.class, () -> a.getRequestBundleExport(999)); + } }