diff --git a/docs/API_TOOLS.md b/docs/API_TOOLS.md new file mode 100644 index 00000000..b990c268 --- /dev/null +++ b/docs/API_TOOLS.md @@ -0,0 +1,432 @@ +# REST API Tools Documentation + +## Overview + +This document describes the reusable REST API tools added to the Swagger Petstore project. These tools provide authentication, validation, and a client library for interacting with the Petstore API. + +## Architecture + +``` +io.swagger.petstore +├── client/ # Reusable REST API client +│ ├── PetStoreClient.java +│ ├── ApiClientConfig.java +│ └── ApiResponse.java +├── security/ # Authentication & authorization +│ ├── ApiKeyAuthFilter.java +│ ├── ApiKeyStore.java +│ └── SecurityContext.java +└── validation/ # Request/response validation + ├── RequestValidator.java + └── ValidationResult.java +``` + +--- + +## 1. REST API Client (`io.swagger.petstore.client`) + +### Quick Start + +```java +// Simple creation +PetStoreClient client = new PetStoreClient("http://localhost:8080/api/v3", "special-key"); + +// Builder pattern +ApiClientConfig config = ApiClientConfig.builder() + .baseUrl("http://localhost:8080/api/v3") + .apiKey("special-key") + .connectTimeout(5000) + .readTimeout(10000) + .build(); +PetStoreClient client = new PetStoreClient(config); +``` + +### Pet Operations + +```java +// Get pet by ID +ApiResponse response = client.getPetById(1L, Pet.class); +if (response.isSuccess()) { + Pet pet = response.getBody(); +} + +// Find pets by status +ApiResponse pets = client.findPetsByStatus("available"); + +// Find pets by tags +ApiResponse pets = client.findPetsByTags(Arrays.asList("tag1", "tag2")); + +// Add a new pet +Pet newPet = new Pet(); +newPet.setId(100L); +newPet.setName("Buddy"); +newPet.setStatus("available"); +ApiResponse response = client.addPet(newPet); + +// Update existing pet +pet.setName("New Name"); +ApiResponse response = client.updatePet(pet); + +// Delete pet +ApiResponse response = client.deletePet(1L); +``` + +### Store Operations + +```java +// Get inventory +ApiResponse inventory = client.getInventory(); + +// Place order +Order order = new Order(); +order.setId(10L); +order.setPetId(1L); +order.setQuantity(2); +order.setStatus("placed"); +ApiResponse response = client.placeOrder(order); + +// Get order by ID +ApiResponse order = client.getOrderById(10L, Order.class); + +// Delete order +ApiResponse response = client.deleteOrder(10L); +``` + +### User Operations + +```java +// Create user +User user = new User(); +user.setUsername("john_doe"); +user.setEmail("john@example.com"); +ApiResponse response = client.createUser(user); + +// Login +ApiResponse session = client.loginUser("john_doe", "password123"); + +// Get user +ApiResponse user = client.getUserByName("john_doe", User.class); + +// Update user +user.setEmail("newemail@example.com"); +ApiResponse response = client.updateUser("john_doe", user); + +// Delete user +ApiResponse response = client.deleteUser("john_doe"); + +// Logout +ApiResponse response = client.logoutUser(); +``` + +### Response Handling + +```java +ApiResponse response = client.getPetById(1L, Pet.class); + +// Check status +response.isSuccess(); // 2xx +response.isClientError(); // 4xx +response.isServerError(); // 5xx + +// Get data +response.getStatusCode(); // HTTP status code +response.getBody(); // Parsed response object +response.getRawBody(); // Raw JSON string +``` + +--- + +## 2. Authentication & Authorization (`io.swagger.petstore.security`) + +### API Key Authentication Filter + +The `ApiKeyAuthFilter` is a servlet filter that validates API keys on protected endpoints. + +**Supported authentication methods:** +- `api_key` header: `api_key: special-key` +- Authorization Bearer: `Authorization: Bearer special-key` +- Query parameter: `?api_key=special-key` + +**Public endpoints (no auth required):** +- `GET /api/v3/openapi.json` +- `GET /api/v3/openapi.yaml` +- `GET /api/v3/user/login` +- `GET /api/v3/user/logout` +- `OPTIONS` requests (CORS preflight) + +### Registering the Filter + +Add to `web.xml`: +```xml + + ApiKeyAuthFilter + io.swagger.petstore.security.ApiKeyAuthFilter + + + ApiKeyAuthFilter + /api/v3/* + +``` + +### Managing API Keys + +```java +ApiKeyStore store = ApiKeyStore.getInstance(); + +// Add a new valid key +store.addKey("new-api-key"); + +// Remove a key +store.removeKey("revoked-key"); + +// Validate a key +boolean valid = store.isValidKey("some-key"); +``` + +### Security Context + +```java +SecurityContext ctx = new SecurityContext("special-key", "read:pets", "write:pets"); + +ctx.hasScope("read:pets"); // true +ctx.hasReadAccess(); // true +ctx.hasWriteAccess(); // true +``` + +--- + +## 3. Request Validation (`io.swagger.petstore.validation`) + +### Validating Pets + +```java +Pet pet = new Pet(); +pet.setName("Buddy"); +pet.setPhotoUrls(Arrays.asList("http://example.com/photo.jpg")); +pet.setStatus("available"); + +ValidationResult result = RequestValidator.validatePet(pet); +if (!result.isValid()) { + String errors = result.getErrorMessage(); // semicolon-separated errors + List errorList = result.getErrors(); +} +``` + +**Pet validation rules:** +- `name` is required (max 100 chars) +- `photoUrls` must have at least one entry +- `status` must be: `available`, `pending`, or `sold` + +### Validating Orders + +```java +Order order = new Order(); +order.setPetId(1L); +order.setQuantity(5); +order.setStatus("placed"); + +ValidationResult result = RequestValidator.validateOrder(order); +``` + +**Order validation rules:** +- `petId` must be a positive number +- `quantity` must be greater than 0 +- `status` must be: `placed`, `approved`, or `delivered` + +### Validating Users + +```java +User user = new User(); +user.setUsername("john_doe"); +user.setEmail("john@example.com"); +user.setPhone("123-456-7890"); + +ValidationResult result = RequestValidator.validateUser(user); +``` + +**User validation rules:** +- `username` is required (max 50 chars) +- `email` must match basic email format (if provided) +- `phone` must contain only digits, hyphens, plus, parens, spaces (if provided) + +### Utility Validators + +```java +// Validate pet status parameter +ValidationResult result = RequestValidator.validatePetStatus("available,pending"); + +// Validate ID fields +ValidationResult result = RequestValidator.validateId(1L, "petId"); +``` + +--- + +## 4. Example curl Requests + +### Pet Endpoints + +```bash +# Get pet by ID +curl -X GET "http://localhost:8080/api/v3/pet/1" \ + -H "api_key: special-key" \ + -H "Accept: application/json" + +# Response: +# {"id":1,"category":{"id":2,"name":"Cats"},"name":"Cat 1","photoUrls":["url1","url2"],"tags":[{"id":1,"name":"tag1"},{"id":2,"name":"tag2"}],"status":"available"} + +# Find pets by status +curl -X GET "http://localhost:8080/api/v3/pet/findByStatus?status=available" \ + -H "api_key: special-key" \ + -H "Accept: application/json" + +# Add a new pet +curl -X POST "http://localhost:8080/api/v3/pet" \ + -H "api_key: special-key" \ + -H "Content-Type: application/json" \ + -d '{ + "id": 100, + "name": "Buddy", + "category": {"id": 1, "name": "Dogs"}, + "photoUrls": ["http://example.com/buddy.jpg"], + "tags": [{"id": 1, "name": "friendly"}], + "status": "available" + }' + +# Update a pet +curl -X PUT "http://localhost:8080/api/v3/pet" \ + -H "api_key: special-key" \ + -H "Content-Type: application/json" \ + -d '{ + "id": 1, + "name": "Updated Cat", + "status": "sold" + }' + +# Delete a pet +curl -X DELETE "http://localhost:8080/api/v3/pet/1" \ + -H "api_key: special-key" +``` + +### Store Endpoints + +```bash +# Get inventory +curl -X GET "http://localhost:8080/api/v3/store/inventory" \ + -H "api_key: special-key" + +# Response: +# {"placed":100,"approved":50,"delivered":50} + +# Place an order +curl -X POST "http://localhost:8080/api/v3/store/order" \ + -H "Content-Type: application/json" \ + -d '{ + "id": 10, + "petId": 1, + "quantity": 2, + "shipDate": "2024-01-15T00:00:00.000Z", + "status": "placed", + "complete": false + }' + +# Get order by ID +curl -X GET "http://localhost:8080/api/v3/store/order/1" + +# Delete order +curl -X DELETE "http://localhost:8080/api/v3/store/order/1" +``` + +### User Endpoints + +```bash +# Login +curl -X GET "http://localhost:8080/api/v3/user/login?username=user1&password=test" + +# Response: +# "Logged in user session: 1234567890" + +# Create user +curl -X POST "http://localhost:8080/api/v3/user" \ + -H "Content-Type: application/json" \ + -d '{ + "id": 100, + "username": "new_user", + "firstName": "John", + "lastName": "Doe", + "email": "john@example.com", + "password": "secret123", + "phone": "555-0100", + "userStatus": 1 + }' + +# Get user by name +curl -X GET "http://localhost:8080/api/v3/user/user1" \ + -H "api_key: special-key" + +# Update user +curl -X PUT "http://localhost:8080/api/v3/user/user1" \ + -H "api_key: special-key" \ + -H "Content-Type: application/json" \ + -d '{ + "id": 1, + "username": "user1", + "firstName": "Updated", + "lastName": "Name", + "email": "updated@example.com" + }' + +# Delete user +curl -X DELETE "http://localhost:8080/api/v3/user/user1" \ + -H "api_key: special-key" + +# Logout +curl -X GET "http://localhost:8080/api/v3/user/logout" +``` + +--- + +## 5. Running Tests + +```bash +mvn test +``` + +Test classes: +- `PetControllerTest` - Pet CRUD operations (13 tests) +- `OrderControllerTest` - Order operations (8 tests) +- `UserControllerTest` - User operations (11 tests) +- `RequestValidatorTest` - Validation logic (20 tests) +- `PetStoreClientTest` - Client configuration (8 tests) +- `ApiKeyStoreTest` - API key management (5 tests) + +--- + +## 6. Error Response Format + +All error responses follow a consistent format: + +```json +{ + "code": 401, + "type": "error", + "message": "Missing API key. Provide via 'api_key' header or 'Authorization: Bearer '" +} +``` + +Common error codes: +| Code | Meaning | +|------|---------| +| 400 | Bad Request - Invalid input or missing required fields | +| 401 | Unauthorized - Missing API key | +| 403 | Forbidden - Invalid API key | +| 404 | Not Found - Resource doesn't exist | +| 422 | Unprocessable Entity - Validation failed | + +--- + +## Assumptions + +1. **In-memory storage**: The data layer uses in-memory collections. Data resets on server restart. +2. **API Key authentication**: Default valid keys are `special-key` and `test-api-key`. In production, replace `ApiKeyStore` with a database-backed implementation. +3. **OAuth2 scopes**: The OpenAPI spec defines `petstore_auth` OAuth2 flows. The current implementation uses API key auth as a simplified alternative. The `SecurityContext` class models scopes for future OAuth2 integration. +4. **Validation is advisory**: The `RequestValidator` utility is provided for use in controllers but is not automatically enforced via the filter chain to maintain backward compatibility with the existing swagger-inflector routing. diff --git a/pom.xml b/pom.xml index aa27b7d0..cd3ae826 100644 --- a/pom.xml +++ b/pom.xml @@ -241,6 +241,28 @@ bugsnag + + + junit + junit + ${junit-version} + test + + + org.mockito + mockito-core + 4.11.0 + test + + + + + javax.servlet + javax.servlet-api + 3.1.0 + provided + + 1.0.0 diff --git a/src/main/java/io/swagger/petstore/client/ApiClientConfig.java b/src/main/java/io/swagger/petstore/client/ApiClientConfig.java new file mode 100644 index 00000000..708c0ff7 --- /dev/null +++ b/src/main/java/io/swagger/petstore/client/ApiClientConfig.java @@ -0,0 +1,101 @@ +/** + * Copyright 2018 SmartBear Software + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.swagger.petstore.client; + +/** + * Configuration class for the PetStore API client. + * Supports builder pattern for fluent configuration. + * + * Usage: + *

+ *   ApiClientConfig config = ApiClientConfig.builder()
+ *       .baseUrl("http://localhost:8080/api/v3")
+ *       .apiKey("special-key")
+ *       .connectTimeout(5000)
+ *       .readTimeout(10000)
+ *       .build();
+ *   PetStoreClient client = new PetStoreClient(config);
+ * 
+ */ +public class ApiClientConfig { + + private String baseUrl; + private String apiKey; + private int connectTimeout; + private int readTimeout; + + private ApiClientConfig(Builder builder) { + this.baseUrl = builder.baseUrl; + this.apiKey = builder.apiKey; + this.connectTimeout = builder.connectTimeout; + this.readTimeout = builder.readTimeout; + } + + public static Builder builder() { + return new Builder(); + } + + public String getBaseUrl() { + return baseUrl; + } + + public String getApiKey() { + return apiKey; + } + + public int getConnectTimeout() { + return connectTimeout; + } + + public int getReadTimeout() { + return readTimeout; + } + + public static class Builder { + private String baseUrl = "http://localhost:8080/api/v3"; + private String apiKey = ""; + private int connectTimeout = 5000; + private int readTimeout = 10000; + + public Builder baseUrl(String baseUrl) { + this.baseUrl = baseUrl; + return this; + } + + public Builder apiKey(String apiKey) { + this.apiKey = apiKey; + return this; + } + + public Builder connectTimeout(int millis) { + this.connectTimeout = millis; + return this; + } + + public Builder readTimeout(int millis) { + this.readTimeout = millis; + return this; + } + + public ApiClientConfig build() { + if (baseUrl == null || baseUrl.isEmpty()) { + throw new IllegalArgumentException("baseUrl is required"); + } + return new ApiClientConfig(this); + } + } +} diff --git a/src/main/java/io/swagger/petstore/client/ApiResponse.java b/src/main/java/io/swagger/petstore/client/ApiResponse.java new file mode 100644 index 00000000..e5f5f77f --- /dev/null +++ b/src/main/java/io/swagger/petstore/client/ApiResponse.java @@ -0,0 +1,80 @@ +/** + * Copyright 2018 SmartBear Software + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.swagger.petstore.client; + +/** + * Generic API response wrapper that encapsulates the HTTP status code, + * parsed response body, and raw response string. + * + * @param The type of the parsed response body + */ +public class ApiResponse { + + private int statusCode; + private T body; + private String rawBody; + + public ApiResponse() { + } + + public ApiResponse(int statusCode, T body, String rawBody) { + this.statusCode = statusCode; + this.body = body; + this.rawBody = rawBody; + } + + public int getStatusCode() { + return statusCode; + } + + public void setStatusCode(int statusCode) { + this.statusCode = statusCode; + } + + public T getBody() { + return body; + } + + public void setBody(T body) { + this.body = body; + } + + public String getRawBody() { + return rawBody; + } + + public void setRawBody(String rawBody) { + this.rawBody = rawBody; + } + + public boolean isSuccess() { + return statusCode >= 200 && statusCode < 300; + } + + public boolean isClientError() { + return statusCode >= 400 && statusCode < 500; + } + + public boolean isServerError() { + return statusCode >= 500; + } + + @Override + public String toString() { + return "ApiResponse{statusCode=" + statusCode + ", body=" + rawBody + "}"; + } +} diff --git a/src/main/java/io/swagger/petstore/client/PetStoreClient.java b/src/main/java/io/swagger/petstore/client/PetStoreClient.java new file mode 100644 index 00000000..debfa588 --- /dev/null +++ b/src/main/java/io/swagger/petstore/client/PetStoreClient.java @@ -0,0 +1,258 @@ +/** + * Copyright 2018 SmartBear Software + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.swagger.petstore.client; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Map; + +/** + * Reusable REST API client for interacting with the Petstore API. + * Provides typed methods for all petstore operations with built-in + * authentication, error handling, and response parsing. + * + * Usage: + *

+ *   PetStoreClient client = new PetStoreClient("http://localhost:8080/api/v3", "special-key");
+ *   ApiResponse<Pet> response = client.getPetById(1L);
+ *   if (response.isSuccess()) {
+ *       Pet pet = response.getBody();
+ *   }
+ * 
+ */ +public class PetStoreClient { + + private final String baseUrl; + private final String apiKey; + private final ObjectMapper objectMapper; + private int connectTimeout = 5000; + private int readTimeout = 10000; + + public PetStoreClient(String baseUrl, String apiKey) { + this.baseUrl = baseUrl.endsWith("/") ? baseUrl.substring(0, baseUrl.length() - 1) : baseUrl; + this.apiKey = apiKey; + this.objectMapper = new ObjectMapper(); + this.objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + } + + public PetStoreClient(ApiClientConfig config) { + this(config.getBaseUrl(), config.getApiKey()); + this.connectTimeout = config.getConnectTimeout(); + this.readTimeout = config.getReadTimeout(); + } + + // --- Pet Operations --- + + public ApiResponse getPetById(Long petId, Class responseType) throws IOException { + return doGet("/pet/" + petId, responseType); + } + + public ApiResponse findPetsByStatus(String status) throws IOException { + return doGet("/pet/findByStatus?status=" + encode(status), String.class); + } + + public ApiResponse findPetsByTags(List tags) throws IOException { + StringBuilder sb = new StringBuilder("/pet/findByTags?"); + for (int i = 0; i < tags.size(); i++) { + if (i > 0) sb.append("&"); + sb.append("tags=").append(encode(tags.get(i))); + } + return doGet(sb.toString(), String.class); + } + + public ApiResponse addPet(Object pet) throws IOException { + return doPost("/pet", pet); + } + + public ApiResponse updatePet(Object pet) throws IOException { + return doPut("/pet", pet); + } + + public ApiResponse deletePet(Long petId) throws IOException { + return doDelete("/pet/" + petId); + } + + // --- Store Operations --- + + public ApiResponse getInventory() throws IOException { + return doGet("/store/inventory", String.class); + } + + public ApiResponse placeOrder(Object order) throws IOException { + return doPost("/store/order", order); + } + + public ApiResponse getOrderById(Long orderId, Class responseType) throws IOException { + return doGet("/store/order/" + orderId, responseType); + } + + public ApiResponse deleteOrder(Long orderId) throws IOException { + return doDelete("/store/order/" + orderId); + } + + // --- User Operations --- + + public ApiResponse createUser(Object user) throws IOException { + return doPost("/user", user); + } + + public ApiResponse createUsersWithList(List users) throws IOException { + return doPost("/user/createWithList", users); + } + + public ApiResponse getUserByName(String username, Class responseType) throws IOException { + return doGet("/user/" + encode(username), responseType); + } + + public ApiResponse updateUser(String username, Object user) throws IOException { + return doPut("/user/" + encode(username), user); + } + + public ApiResponse deleteUser(String username) throws IOException { + return doDelete("/user/" + encode(username)); + } + + public ApiResponse loginUser(String username, String password) throws IOException { + return doGet("/user/login?username=" + encode(username) + "&password=" + encode(password), String.class); + } + + public ApiResponse logoutUser() throws IOException { + return doGet("/user/logout", String.class); + } + + // --- HTTP Methods --- + + private ApiResponse doGet(String path, Class responseType) throws IOException { + HttpURLConnection conn = createConnection(path, "GET"); + return executeRequest(conn, responseType); + } + + private ApiResponse doPost(String path, Object body) throws IOException { + HttpURLConnection conn = createConnection(path, "POST"); + writeBody(conn, body); + return executeRequest(conn, String.class); + } + + private ApiResponse doPut(String path, Object body) throws IOException { + HttpURLConnection conn = createConnection(path, "PUT"); + writeBody(conn, body); + return executeRequest(conn, String.class); + } + + private ApiResponse doDelete(String path) throws IOException { + HttpURLConnection conn = createConnection(path, "DELETE"); + return executeRequest(conn, String.class); + } + + private HttpURLConnection createConnection(String path, String method) throws IOException { + URL url = new URL(baseUrl + path); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setRequestMethod(method); + conn.setRequestProperty("Accept", "application/json"); + conn.setRequestProperty("Content-Type", "application/json"); + conn.setConnectTimeout(connectTimeout); + conn.setReadTimeout(readTimeout); + + if (apiKey != null && !apiKey.isEmpty()) { + conn.setRequestProperty("api_key", apiKey); + } + + return conn; + } + + private void writeBody(HttpURLConnection conn, Object body) throws IOException { + conn.setDoOutput(true); + String json = objectMapper.writeValueAsString(body); + try (OutputStream os = conn.getOutputStream()) { + os.write(json.getBytes(StandardCharsets.UTF_8)); + } + } + + @SuppressWarnings("unchecked") + private ApiResponse executeRequest(HttpURLConnection conn, Class responseType) throws IOException { + int statusCode = conn.getResponseCode(); + String responseBody = readResponse(conn); + + ApiResponse response = new ApiResponse<>(); + response.setStatusCode(statusCode); + response.setRawBody(responseBody); + + if (statusCode >= 200 && statusCode < 300 && responseBody != null && !responseBody.isEmpty()) { + if (responseType == String.class) { + response.setBody((T) responseBody); + } else { + T parsed = objectMapper.readValue(responseBody, responseType); + response.setBody(parsed); + } + } + + return response; + } + + private String readResponse(HttpURLConnection conn) throws IOException { + BufferedReader reader; + try { + reader = new BufferedReader(new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8)); + } catch (IOException e) { + if (conn.getErrorStream() != null) { + reader = new BufferedReader(new InputStreamReader(conn.getErrorStream(), StandardCharsets.UTF_8)); + } else { + return null; + } + } + + StringBuilder sb = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + sb.append(line); + } + reader.close(); + return sb.toString(); + } + + private String encode(String value) { + try { + return java.net.URLEncoder.encode(value, "UTF-8"); + } catch (java.io.UnsupportedEncodingException e) { + return value; + } + } + + // --- Configuration --- + + public void setConnectTimeout(int millis) { + this.connectTimeout = millis; + } + + public void setReadTimeout(int millis) { + this.readTimeout = millis; + } + + public String getBaseUrl() { + return baseUrl; + } +} diff --git a/src/main/java/io/swagger/petstore/security/ApiKeyAuthFilter.java b/src/main/java/io/swagger/petstore/security/ApiKeyAuthFilter.java new file mode 100644 index 00000000..f31f2b9f --- /dev/null +++ b/src/main/java/io/swagger/petstore/security/ApiKeyAuthFilter.java @@ -0,0 +1,117 @@ +/** + * Copyright 2018 SmartBear Software + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.swagger.petstore.security; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +/** + * Servlet filter that validates API key authentication for protected endpoints. + * The API key can be passed via the "api_key" header or "api_key" query parameter. + * + * Public endpoints (login, logout, openapi spec) are excluded from authentication. + */ +public class ApiKeyAuthFilter implements Filter { + + private static final String API_KEY_HEADER = "api_key"; + private static final String API_KEY_PARAM = "api_key"; + private static final String AUTH_HEADER = "Authorization"; + private static final String BEARER_PREFIX = "Bearer "; + + private static final Set PUBLIC_PATHS = new HashSet<>(Arrays.asList( + "/api/v3/openapi.json", + "/api/v3/openapi.yaml", + "/api/v3/user/login", + "/api/v3/user/logout" + )); + + private ApiKeyStore apiKeyStore; + + @Override + public void init(FilterConfig filterConfig) throws ServletException { + this.apiKeyStore = ApiKeyStore.getInstance(); + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + HttpServletRequest httpRequest = (HttpServletRequest) request; + HttpServletResponse httpResponse = (HttpServletResponse) response; + + String path = httpRequest.getRequestURI(); + + if (isPublicPath(path) || "OPTIONS".equalsIgnoreCase(httpRequest.getMethod())) { + chain.doFilter(request, response); + return; + } + + String apiKey = extractApiKey(httpRequest); + + if (apiKey == null || apiKey.isEmpty()) { + httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + httpResponse.setContentType("application/json"); + httpResponse.getWriter().write("{\"code\":401,\"type\":\"error\",\"message\":\"Missing API key. Provide via 'api_key' header or 'Authorization: Bearer '\"}"); + return; + } + + if (!apiKeyStore.isValidKey(apiKey)) { + httpResponse.setStatus(HttpServletResponse.SC_FORBIDDEN); + httpResponse.setContentType("application/json"); + httpResponse.getWriter().write("{\"code\":403,\"type\":\"error\",\"message\":\"Invalid API key\"}"); + return; + } + + chain.doFilter(request, response); + } + + @Override + public void destroy() { + } + + private String extractApiKey(HttpServletRequest request) { + String apiKey = request.getHeader(API_KEY_HEADER); + if (apiKey != null && !apiKey.isEmpty()) { + return apiKey; + } + + String authHeader = request.getHeader(AUTH_HEADER); + if (authHeader != null && authHeader.startsWith(BEARER_PREFIX)) { + return authHeader.substring(BEARER_PREFIX.length()); + } + + return request.getParameter(API_KEY_PARAM); + } + + private boolean isPublicPath(String path) { + for (String publicPath : PUBLIC_PATHS) { + if (path.endsWith(publicPath) || path.contains("/openapi")) { + return true; + } + } + return false; + } +} diff --git a/src/main/java/io/swagger/petstore/security/ApiKeyStore.java b/src/main/java/io/swagger/petstore/security/ApiKeyStore.java new file mode 100644 index 00000000..733a9d58 --- /dev/null +++ b/src/main/java/io/swagger/petstore/security/ApiKeyStore.java @@ -0,0 +1,58 @@ +/** + * Copyright 2018 SmartBear Software + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.swagger.petstore.security; + +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +/** + * In-memory store for valid API keys. + * In production, replace with a database or external auth service lookup. + */ +public class ApiKeyStore { + + private static final ApiKeyStore INSTANCE = new ApiKeyStore(); + private final Set validKeys = ConcurrentHashMap.newKeySet(); + + private ApiKeyStore() { + validKeys.add("special-key"); + validKeys.add("test-api-key"); + } + + public static ApiKeyStore getInstance() { + return INSTANCE; + } + + public boolean isValidKey(String apiKey) { + if (apiKey == null) { + return false; + } + return validKeys.contains(apiKey); + } + + public void addKey(String apiKey) { + validKeys.add(apiKey); + } + + public void removeKey(String apiKey) { + validKeys.remove(apiKey); + } + + public void clear() { + validKeys.clear(); + } +} diff --git a/src/main/java/io/swagger/petstore/security/SecurityContext.java b/src/main/java/io/swagger/petstore/security/SecurityContext.java new file mode 100644 index 00000000..765a2eb5 --- /dev/null +++ b/src/main/java/io/swagger/petstore/security/SecurityContext.java @@ -0,0 +1,57 @@ +/** + * Copyright 2018 SmartBear Software + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.swagger.petstore.security; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +/** + * Represents the authenticated security context for a request. + * Contains the API key and associated scopes/permissions. + */ +public class SecurityContext { + + private final String apiKey; + private final Set scopes; + + public SecurityContext(String apiKey, String... scopes) { + this.apiKey = apiKey; + this.scopes = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(scopes))); + } + + public String getApiKey() { + return apiKey; + } + + public Set getScopes() { + return scopes; + } + + public boolean hasScope(String scope) { + return scopes.contains(scope); + } + + public boolean hasReadAccess() { + return scopes.contains("read:pets"); + } + + public boolean hasWriteAccess() { + return scopes.contains("write:pets"); + } +} diff --git a/src/main/java/io/swagger/petstore/validation/RequestValidator.java b/src/main/java/io/swagger/petstore/validation/RequestValidator.java new file mode 100644 index 00000000..91d8918e --- /dev/null +++ b/src/main/java/io/swagger/petstore/validation/RequestValidator.java @@ -0,0 +1,148 @@ +/** + * Copyright 2018 SmartBear Software + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.swagger.petstore.validation; + +import io.swagger.petstore.model.Order; +import io.swagger.petstore.model.Pet; +import io.swagger.petstore.model.User; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Reusable request validation utility for all API resources. + * Validates model constraints defined in the OpenAPI specification. + */ +public class RequestValidator { + + private static final Set VALID_PET_STATUSES = new HashSet<>( + Arrays.asList("available", "pending", "sold")); + private static final Set VALID_ORDER_STATUSES = new HashSet<>( + Arrays.asList("placed", "approved", "delivered")); + + private RequestValidator() { + } + + public static ValidationResult validatePet(Pet pet) { + List errors = new ArrayList<>(); + + if (pet == null) { + errors.add("Pet object is required"); + return new ValidationResult(errors); + } + + if (pet.getName() == null || pet.getName().trim().isEmpty()) { + errors.add("Pet name is required"); + } + + if (pet.getPhotoUrls() == null || pet.getPhotoUrls().isEmpty()) { + errors.add("At least one photo URL is required"); + } + + if (pet.getStatus() != null && !VALID_PET_STATUSES.contains(pet.getStatus())) { + errors.add("Invalid pet status. Must be one of: available, pending, sold"); + } + + if (pet.getName() != null && pet.getName().length() > 100) { + errors.add("Pet name must not exceed 100 characters"); + } + + return new ValidationResult(errors); + } + + public static ValidationResult validateOrder(Order order) { + List errors = new ArrayList<>(); + + if (order == null) { + errors.add("Order object is required"); + return new ValidationResult(errors); + } + + if (order.getPetId() <= 0) { + errors.add("Valid petId is required"); + } + + if (order.getQuantity() <= 0) { + errors.add("Quantity must be greater than 0"); + } + + if (order.getStatus() != null && !VALID_ORDER_STATUSES.contains(order.getStatus())) { + errors.add("Invalid order status. Must be one of: placed, approved, delivered"); + } + + return new ValidationResult(errors); + } + + public static ValidationResult validateUser(User user) { + List errors = new ArrayList<>(); + + if (user == null) { + errors.add("User object is required"); + return new ValidationResult(errors); + } + + if (user.getUsername() == null || user.getUsername().trim().isEmpty()) { + errors.add("Username is required"); + } + + if (user.getUsername() != null && user.getUsername().length() > 50) { + errors.add("Username must not exceed 50 characters"); + } + + if (user.getEmail() != null && !user.getEmail().isEmpty()) { + if (!user.getEmail().matches("^[A-Za-z0-9+_.-]+@(.+)$")) { + errors.add("Invalid email format"); + } + } + + if (user.getPhone() != null && !user.getPhone().isEmpty()) { + if (!user.getPhone().matches("^[0-9\\-+() ]+$")) { + errors.add("Invalid phone number format"); + } + } + + return new ValidationResult(errors); + } + + public static ValidationResult validatePetStatus(String status) { + List errors = new ArrayList<>(); + if (status == null || status.trim().isEmpty()) { + errors.add("Status parameter is required"); + } else { + String[] statuses = status.split(","); + for (String s : statuses) { + if (!VALID_PET_STATUSES.contains(s.trim())) { + errors.add("Invalid status value: " + s.trim() + ". Must be one of: available, pending, sold"); + } + } + } + return new ValidationResult(errors); + } + + public static ValidationResult validateId(Long id, String fieldName) { + List errors = new ArrayList<>(); + if (id == null) { + errors.add(fieldName + " is required"); + } else if (id <= 0) { + errors.add(fieldName + " must be a positive number"); + } + return new ValidationResult(errors); + } +} diff --git a/src/main/java/io/swagger/petstore/validation/ValidationResult.java b/src/main/java/io/swagger/petstore/validation/ValidationResult.java new file mode 100644 index 00000000..e4189eb6 --- /dev/null +++ b/src/main/java/io/swagger/petstore/validation/ValidationResult.java @@ -0,0 +1,53 @@ +/** + * Copyright 2018 SmartBear Software + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.swagger.petstore.validation; + +import java.util.Collections; +import java.util.List; + +/** + * Result of a validation operation. Contains a list of validation errors. + * If the list is empty, validation passed. + */ +public class ValidationResult { + + private final List errors; + + public ValidationResult(List errors) { + this.errors = errors != null ? errors : Collections.emptyList(); + } + + public boolean isValid() { + return errors.isEmpty(); + } + + public List getErrors() { + return Collections.unmodifiableList(errors); + } + + public String getErrorMessage() { + return String.join("; ", errors); + } + + @Override + public String toString() { + if (isValid()) { + return "ValidationResult{valid=true}"; + } + return "ValidationResult{valid=false, errors=" + errors + "}"; + } +} diff --git a/src/test/java/io/swagger/petstore/client/PetStoreClientTest.java b/src/test/java/io/swagger/petstore/client/PetStoreClientTest.java new file mode 100644 index 00000000..c02ad10c --- /dev/null +++ b/src/test/java/io/swagger/petstore/client/PetStoreClientTest.java @@ -0,0 +1,94 @@ +package io.swagger.petstore.client; + +import org.junit.Test; + +import static org.junit.Assert.*; + +public class PetStoreClientTest { + + @Test + public void testClientCreation() { + PetStoreClient client = new PetStoreClient("http://localhost:8080/api/v3", "test-key"); + assertNotNull(client); + assertEquals("http://localhost:8080/api/v3", client.getBaseUrl()); + } + + @Test + public void testClientCreation_trailingSlash() { + PetStoreClient client = new PetStoreClient("http://localhost:8080/api/v3/", "test-key"); + assertEquals("http://localhost:8080/api/v3", client.getBaseUrl()); + } + + @Test + public void testClientConfig_builder() { + ApiClientConfig config = ApiClientConfig.builder() + .baseUrl("http://localhost:8080/api/v3") + .apiKey("my-key") + .connectTimeout(3000) + .readTimeout(5000) + .build(); + + assertEquals("http://localhost:8080/api/v3", config.getBaseUrl()); + assertEquals("my-key", config.getApiKey()); + assertEquals(3000, config.getConnectTimeout()); + assertEquals(5000, config.getReadTimeout()); + } + + @Test(expected = IllegalArgumentException.class) + public void testClientConfig_nullBaseUrl() { + ApiClientConfig.builder() + .baseUrl(null) + .build(); + } + + @Test(expected = IllegalArgumentException.class) + public void testClientConfig_emptyBaseUrl() { + ApiClientConfig.builder() + .baseUrl("") + .build(); + } + + @Test + public void testClientCreation_withConfig() { + ApiClientConfig config = ApiClientConfig.builder() + .baseUrl("http://petstore.example.com/api/v3") + .apiKey("config-key") + .build(); + + PetStoreClient client = new PetStoreClient(config); + assertNotNull(client); + assertEquals("http://petstore.example.com/api/v3", client.getBaseUrl()); + } + + @Test + public void testApiResponse_success() { + ApiResponse response = new ApiResponse<>(); + response.setStatusCode(200); + response.setBody("OK"); + response.setRawBody("OK"); + + assertTrue(response.isSuccess()); + assertFalse(response.isClientError()); + assertFalse(response.isServerError()); + } + + @Test + public void testApiResponse_clientError() { + ApiResponse response = new ApiResponse<>(); + response.setStatusCode(404); + + assertFalse(response.isSuccess()); + assertTrue(response.isClientError()); + assertFalse(response.isServerError()); + } + + @Test + public void testApiResponse_serverError() { + ApiResponse response = new ApiResponse<>(); + response.setStatusCode(500); + + assertFalse(response.isSuccess()); + assertFalse(response.isClientError()); + assertTrue(response.isServerError()); + } +} diff --git a/src/test/java/io/swagger/petstore/controller/OrderControllerTest.java b/src/test/java/io/swagger/petstore/controller/OrderControllerTest.java new file mode 100644 index 00000000..bf22ec70 --- /dev/null +++ b/src/test/java/io/swagger/petstore/controller/OrderControllerTest.java @@ -0,0 +1,99 @@ +package io.swagger.petstore.controller; + +import io.swagger.oas.inflector.models.RequestContext; +import io.swagger.oas.inflector.models.ResponseContext; +import io.swagger.petstore.model.Order; +import org.junit.Before; +import org.junit.Test; + +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import java.util.Arrays; +import java.util.Date; +import java.util.Map; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +public class OrderControllerTest { + + private OrderController orderController; + private RequestContext mockRequest; + + @Before + public void setUp() { + orderController = new OrderController(); + mockRequest = mock(RequestContext.class); + when(mockRequest.getHeaders()).thenReturn(new javax.ws.rs.core.MultivaluedHashMap<>()); + when(mockRequest.getAcceptableMediaTypes()).thenReturn( + Arrays.asList(MediaType.APPLICATION_JSON_TYPE)); + } + + @Test + public void testGetInventory() { + ResponseContext response = orderController.getInventory(mockRequest); + assertEquals(Response.Status.OK.getStatusCode(), response.getStatus()); + assertNotNull(response.getEntity()); + assertTrue(response.getEntity() instanceof Map); + } + + @Test + public void testGetOrderById_existing() { + ResponseContext response = orderController.getOrderById(mockRequest, 1L); + assertEquals(Response.Status.OK.getStatusCode(), response.getStatus()); + assertNotNull(response.getEntity()); + assertTrue(response.getEntity() instanceof Order); + } + + @Test + public void testGetOrderById_nonExisting() { + ResponseContext response = orderController.getOrderById(mockRequest, 999L); + assertEquals(Response.Status.NOT_FOUND.getStatusCode(), response.getStatus()); + } + + @Test + public void testGetOrderById_null() { + ResponseContext response = orderController.getOrderById(mockRequest, null); + assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus()); + } + + @Test + public void testPlaceOrder() { + Order order = createTestOrder(100L); + ResponseContext response = orderController.placeOrder(mockRequest, order); + assertEquals(Response.Status.OK.getStatusCode(), response.getStatus()); + assertNotNull(response.getEntity()); + } + + @Test + public void testPlaceOrder_null() { + ResponseContext response = orderController.placeOrder(mockRequest, (Order) null); + assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus()); + } + + @Test + public void testDeleteOrder() { + Order order = createTestOrder(200L); + orderController.placeOrder(mockRequest, order); + + ResponseContext response = orderController.deleteOrder(mockRequest, 200L); + assertEquals(Response.Status.OK.getStatusCode(), response.getStatus()); + } + + @Test + public void testDeleteOrder_nullId() { + ResponseContext response = orderController.deleteOrder(mockRequest, null); + assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus()); + } + + private Order createTestOrder(Long id) { + Order order = new Order(); + order.setId(id); + order.setPetId(1L); + order.setQuantity(2); + order.setShipDate(new Date()); + order.setStatus("placed"); + order.setComplete(false); + return order; + } +} diff --git a/src/test/java/io/swagger/petstore/controller/PetControllerTest.java b/src/test/java/io/swagger/petstore/controller/PetControllerTest.java new file mode 100644 index 00000000..2d0c066c --- /dev/null +++ b/src/test/java/io/swagger/petstore/controller/PetControllerTest.java @@ -0,0 +1,167 @@ +package io.swagger.petstore.controller; + +import io.swagger.oas.inflector.models.RequestContext; +import io.swagger.oas.inflector.models.ResponseContext; +import io.swagger.petstore.model.Category; +import io.swagger.petstore.model.Pet; +import io.swagger.petstore.model.Tag; +import org.junit.Before; +import org.junit.Test; + +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +public class PetControllerTest { + + private PetController petController; + private RequestContext mockRequest; + + @Before + public void setUp() { + petController = new PetController(); + mockRequest = mock(RequestContext.class); + when(mockRequest.getHeaders()).thenReturn(new javax.ws.rs.core.MultivaluedHashMap<>()); + when(mockRequest.getAcceptableMediaTypes()).thenReturn( + Arrays.asList(MediaType.APPLICATION_JSON_TYPE)); + } + + @Test + public void testGetPetById_existingPet() { + Pet testPet = createTestPet(300L, "GetById Test Pet"); + petController.addPet(mockRequest, testPet); + + ResponseContext response = petController.getPetById(mockRequest, 300L); + assertEquals(Response.Status.OK.getStatusCode(), response.getStatus()); + assertNotNull(response.getEntity()); + assertTrue(response.getEntity() instanceof Pet); + assertEquals("GetById Test Pet", ((Pet) response.getEntity()).getName()); + } + + @Test + public void testGetPetById_nonExistingPet() { + ResponseContext response = petController.getPetById(mockRequest, 999L); + assertEquals(Response.Status.NOT_FOUND.getStatusCode(), response.getStatus()); + } + + @Test + public void testGetPetById_nullId() { + ResponseContext response = petController.getPetById(mockRequest, null); + assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus()); + } + + @Test + public void testFindPetsByStatus_available() { + ResponseContext response = petController.findPetsByStatus(mockRequest, "available"); + assertEquals(Response.Status.OK.getStatusCode(), response.getStatus()); + assertNotNull(response.getEntity()); + assertTrue(response.getEntity() instanceof List); + List pets = (List) response.getEntity(); + assertFalse(pets.isEmpty()); + } + + @Test + public void testFindPetsByStatus_nullStatus() { + ResponseContext response = petController.findPetsByStatus(mockRequest, null); + assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus()); + } + + @Test + public void testAddPet() { + Pet newPet = createTestPet(100L, "Test Dog"); + ResponseContext response = petController.addPet(mockRequest, newPet); + assertEquals(Response.Status.OK.getStatusCode(), response.getStatus()); + assertNotNull(response.getEntity()); + } + + @Test + public void testAddPet_null() { + ResponseContext response = petController.addPet(mockRequest, (Pet) null); + assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus()); + } + + @Test + public void testUpdatePet_existing() { + Pet updatedPet = createTestPet(1L, "Updated Cat"); + ResponseContext response = petController.updatePet(mockRequest, updatedPet); + assertEquals(Response.Status.OK.getStatusCode(), response.getStatus()); + assertEquals("Updated Cat", ((Pet) response.getEntity()).getName()); + } + + @Test + public void testUpdatePet_nonExisting() { + Pet updatedPet = createTestPet(999L, "Ghost Pet"); + ResponseContext response = petController.updatePet(mockRequest, updatedPet); + assertEquals(Response.Status.NOT_FOUND.getStatusCode(), response.getStatus()); + } + + @Test + public void testDeletePet() { + Pet newPet = createTestPet(200L, "Delete Me"); + petController.addPet(mockRequest, newPet); + + ResponseContext response = petController.deletePet(mockRequest, null, 200L); + assertEquals(Response.Status.OK.getStatusCode(), response.getStatus()); + } + + @Test + public void testDeletePet_nullId() { + ResponseContext response = petController.deletePet(mockRequest, null, null); + assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus()); + } + + @Test + public void testFindPetsByTags() { + List tags = Arrays.asList("tag1"); + ResponseContext response = petController.findPetsByTags(mockRequest, tags); + assertEquals(Response.Status.OK.getStatusCode(), response.getStatus()); + assertNotNull(response.getEntity()); + assertTrue(response.getEntity() instanceof List); + } + + @Test + public void testFindPetsByTags_emptyTags() { + ResponseContext response = petController.findPetsByTags(mockRequest, new ArrayList<>()); + assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus()); + } + + @Test + public void testUpdatePetWithForm() { + Pet testPet = createTestPet(400L, "FormUpdate Pet"); + petController.addPet(mockRequest, testPet); + + ResponseContext response = petController.updatePetWithForm(mockRequest, 400L, "New Name", "pending"); + assertEquals(Response.Status.OK.getStatusCode(), response.getStatus()); + Pet pet = (Pet) response.getEntity(); + assertEquals("New Name", pet.getName()); + assertEquals("pending", pet.getStatus()); + } + + @Test + public void testUpdatePetWithForm_nullPetId() { + ResponseContext response = petController.updatePetWithForm(mockRequest, null, "Name", "available"); + assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus()); + } + + private Pet createTestPet(Long id, String name) { + Pet pet = new Pet(); + pet.setId(id); + pet.setName(name); + Category category = new Category(); + category.setId(1L); + category.setName("Dogs"); + pet.setCategory(category); + pet.setPhotoUrls(Arrays.asList("http://example.com/photo.jpg")); + Tag tag = new Tag(); + tag.setId(1L); + tag.setName("test-tag"); + pet.setTags(Arrays.asList(tag)); + pet.setStatus("available"); + return pet; + } +} diff --git a/src/test/java/io/swagger/petstore/controller/UserControllerTest.java b/src/test/java/io/swagger/petstore/controller/UserControllerTest.java new file mode 100644 index 00000000..5202ef20 --- /dev/null +++ b/src/test/java/io/swagger/petstore/controller/UserControllerTest.java @@ -0,0 +1,131 @@ +package io.swagger.petstore.controller; + +import io.swagger.oas.inflector.models.RequestContext; +import io.swagger.oas.inflector.models.ResponseContext; +import io.swagger.petstore.model.User; +import org.junit.Before; +import org.junit.Test; + +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import java.util.Arrays; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +public class UserControllerTest { + + private UserController userController; + private RequestContext mockRequest; + + @Before + public void setUp() { + userController = new UserController(); + mockRequest = mock(RequestContext.class); + when(mockRequest.getHeaders()).thenReturn(new javax.ws.rs.core.MultivaluedHashMap<>()); + when(mockRequest.getAcceptableMediaTypes()).thenReturn( + Arrays.asList(MediaType.APPLICATION_JSON_TYPE)); + } + + @Test + public void testCreateUser() { + User user = createTestUser("testuser"); + ResponseContext response = userController.createUser(mockRequest, user); + assertEquals(Response.Status.OK.getStatusCode(), response.getStatus()); + assertNotNull(response.getEntity()); + } + + @Test + public void testCreateUser_null() { + ResponseContext response = userController.createUser(mockRequest, (User) null); + assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus()); + } + + @Test + public void testGetUserByName_existing() { + ResponseContext response = userController.getUserByName(mockRequest, "user1"); + assertEquals(Response.Status.OK.getStatusCode(), response.getStatus()); + assertNotNull(response.getEntity()); + assertTrue(response.getEntity() instanceof User); + assertEquals("user1", ((User) response.getEntity()).getUsername()); + } + + @Test + public void testGetUserByName_nonExisting() { + ResponseContext response = userController.getUserByName(mockRequest, "nonexistent"); + assertEquals(Response.Status.NOT_FOUND.getStatusCode(), response.getStatus()); + } + + @Test + public void testGetUserByName_null() { + ResponseContext response = userController.getUserByName(mockRequest, null); + assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus()); + } + + @Test + public void testLoginUser() { + ResponseContext response = userController.loginUser(mockRequest, "user1", "password"); + assertEquals(Response.Status.OK.getStatusCode(), response.getStatus()); + assertNotNull(response.getEntity()); + } + + @Test + public void testLogoutUser() { + ResponseContext response = userController.logoutUser(mockRequest); + assertEquals(Response.Status.OK.getStatusCode(), response.getStatus()); + } + + @Test + public void testUpdateUser() { + User updatedUser = createTestUser("user1"); + updatedUser.setFirstName("Updated First"); + ResponseContext response = userController.updateUser(mockRequest, "user1", updatedUser); + assertEquals(Response.Status.OK.getStatusCode(), response.getStatus()); + } + + @Test + public void testUpdateUser_nonExisting() { + User updatedUser = createTestUser("ghost"); + ResponseContext response = userController.updateUser(mockRequest, "nonexistent", updatedUser); + assertEquals(Response.Status.NOT_FOUND.getStatusCode(), response.getStatus()); + } + + @Test + public void testUpdateUser_nullUsername() { + User updatedUser = createTestUser("user1"); + ResponseContext response = userController.updateUser(mockRequest, null, updatedUser); + assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus()); + } + + @Test + public void testDeleteUser_nullUsername() { + ResponseContext response = userController.deleteUser(mockRequest, null); + assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus()); + } + + @Test + public void testCreateUsersWithListInput() { + User[] users = new User[]{createTestUser("listuser1"), createTestUser("listuser2")}; + ResponseContext response = userController.createUsersWithListInput(mockRequest, users); + assertEquals(Response.Status.OK.getStatusCode(), response.getStatus()); + } + + @Test + public void testCreateUsersWithListInput_empty() { + ResponseContext response = userController.createUsersWithListInput(mockRequest, new User[]{}); + assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus()); + } + + private User createTestUser(String username) { + User user = new User(); + user.setId(100L); + user.setUsername(username); + user.setFirstName("Test"); + user.setLastName("User"); + user.setEmail("test@example.com"); + user.setPassword("password123"); + user.setPhone("123-456-7890"); + user.setUserStatus(1); + return user; + } +} diff --git a/src/test/java/io/swagger/petstore/security/ApiKeyStoreTest.java b/src/test/java/io/swagger/petstore/security/ApiKeyStoreTest.java new file mode 100644 index 00000000..745050a9 --- /dev/null +++ b/src/test/java/io/swagger/petstore/security/ApiKeyStoreTest.java @@ -0,0 +1,51 @@ +package io.swagger.petstore.security; + +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.*; + +public class ApiKeyStoreTest { + + private ApiKeyStore apiKeyStore; + + @Before + public void setUp() { + apiKeyStore = ApiKeyStore.getInstance(); + } + + @Test + public void testDefaultKeys() { + assertTrue(apiKeyStore.isValidKey("special-key")); + assertTrue(apiKeyStore.isValidKey("test-api-key")); + } + + @Test + public void testInvalidKey() { + assertFalse(apiKeyStore.isValidKey("invalid-key")); + assertFalse(apiKeyStore.isValidKey("")); + assertFalse(apiKeyStore.isValidKey(null)); + } + + @Test + public void testAddKey() { + apiKeyStore.addKey("new-key"); + assertTrue(apiKeyStore.isValidKey("new-key")); + apiKeyStore.removeKey("new-key"); + } + + @Test + public void testRemoveKey() { + apiKeyStore.addKey("temp-key"); + assertTrue(apiKeyStore.isValidKey("temp-key")); + apiKeyStore.removeKey("temp-key"); + assertFalse(apiKeyStore.isValidKey("temp-key")); + } + + @Test + public void testSingleton() { + ApiKeyStore instance1 = ApiKeyStore.getInstance(); + ApiKeyStore instance2 = ApiKeyStore.getInstance(); + assertSame(instance1, instance2); + } +} diff --git a/src/test/java/io/swagger/petstore/validation/RequestValidatorTest.java b/src/test/java/io/swagger/petstore/validation/RequestValidatorTest.java new file mode 100644 index 00000000..e4218ebf --- /dev/null +++ b/src/test/java/io/swagger/petstore/validation/RequestValidatorTest.java @@ -0,0 +1,250 @@ +package io.swagger.petstore.validation; + +import io.swagger.petstore.model.Order; +import io.swagger.petstore.model.Pet; +import io.swagger.petstore.model.User; +import org.junit.Test; + +import java.util.Arrays; +import java.util.Collections; + +import static org.junit.Assert.*; + +public class RequestValidatorTest { + + // --- Pet Validation Tests --- + + @Test + public void testValidatePet_valid() { + Pet pet = new Pet(); + pet.setName("Buddy"); + pet.setPhotoUrls(Arrays.asList("http://example.com/photo.jpg")); + pet.setStatus("available"); + + ValidationResult result = RequestValidator.validatePet(pet); + assertTrue(result.isValid()); + } + + @Test + public void testValidatePet_null() { + ValidationResult result = RequestValidator.validatePet(null); + assertFalse(result.isValid()); + assertTrue(result.getErrors().contains("Pet object is required")); + } + + @Test + public void testValidatePet_missingName() { + Pet pet = new Pet(); + pet.setPhotoUrls(Arrays.asList("http://example.com/photo.jpg")); + pet.setStatus("available"); + + ValidationResult result = RequestValidator.validatePet(pet); + assertFalse(result.isValid()); + assertTrue(result.getErrors().contains("Pet name is required")); + } + + @Test + public void testValidatePet_emptyPhotoUrls() { + Pet pet = new Pet(); + pet.setName("Buddy"); + pet.setPhotoUrls(Collections.emptyList()); + pet.setStatus("available"); + + ValidationResult result = RequestValidator.validatePet(pet); + assertFalse(result.isValid()); + assertTrue(result.getErrors().contains("At least one photo URL is required")); + } + + @Test + public void testValidatePet_invalidStatus() { + Pet pet = new Pet(); + pet.setName("Buddy"); + pet.setPhotoUrls(Arrays.asList("http://example.com/photo.jpg")); + pet.setStatus("unknown"); + + ValidationResult result = RequestValidator.validatePet(pet); + assertFalse(result.isValid()); + assertTrue(result.getErrors().get(0).contains("Invalid pet status")); + } + + @Test + public void testValidatePet_nameTooLong() { + Pet pet = new Pet(); + StringBuilder longName = new StringBuilder(); + for (int i = 0; i < 101; i++) { + longName.append("a"); + } + pet.setName(longName.toString()); + pet.setPhotoUrls(Arrays.asList("http://example.com/photo.jpg")); + + ValidationResult result = RequestValidator.validatePet(pet); + assertFalse(result.isValid()); + assertTrue(result.getErrors().contains("Pet name must not exceed 100 characters")); + } + + // --- Order Validation Tests --- + + @Test + public void testValidateOrder_valid() { + Order order = new Order(); + order.setPetId(1L); + order.setQuantity(5); + order.setStatus("placed"); + + ValidationResult result = RequestValidator.validateOrder(order); + assertTrue(result.isValid()); + } + + @Test + public void testValidateOrder_null() { + ValidationResult result = RequestValidator.validateOrder(null); + assertFalse(result.isValid()); + assertTrue(result.getErrors().contains("Order object is required")); + } + + @Test + public void testValidateOrder_invalidPetId() { + Order order = new Order(); + order.setPetId(0L); + order.setQuantity(5); + order.setStatus("placed"); + + ValidationResult result = RequestValidator.validateOrder(order); + assertFalse(result.isValid()); + assertTrue(result.getErrors().contains("Valid petId is required")); + } + + @Test + public void testValidateOrder_invalidQuantity() { + Order order = new Order(); + order.setPetId(1L); + order.setQuantity(0); + order.setStatus("placed"); + + ValidationResult result = RequestValidator.validateOrder(order); + assertFalse(result.isValid()); + assertTrue(result.getErrors().contains("Quantity must be greater than 0")); + } + + @Test + public void testValidateOrder_invalidStatus() { + Order order = new Order(); + order.setPetId(1L); + order.setQuantity(5); + order.setStatus("invalid"); + + ValidationResult result = RequestValidator.validateOrder(order); + assertFalse(result.isValid()); + assertTrue(result.getErrors().get(0).contains("Invalid order status")); + } + + // --- User Validation Tests --- + + @Test + public void testValidateUser_valid() { + User user = new User(); + user.setUsername("john_doe"); + user.setEmail("john@example.com"); + user.setPhone("123-456-7890"); + + ValidationResult result = RequestValidator.validateUser(user); + assertTrue(result.isValid()); + } + + @Test + public void testValidateUser_null() { + ValidationResult result = RequestValidator.validateUser(null); + assertFalse(result.isValid()); + assertTrue(result.getErrors().contains("User object is required")); + } + + @Test + public void testValidateUser_missingUsername() { + User user = new User(); + user.setEmail("john@example.com"); + + ValidationResult result = RequestValidator.validateUser(user); + assertFalse(result.isValid()); + assertTrue(result.getErrors().contains("Username is required")); + } + + @Test + public void testValidateUser_invalidEmail() { + User user = new User(); + user.setUsername("john_doe"); + user.setEmail("not-an-email"); + + ValidationResult result = RequestValidator.validateUser(user); + assertFalse(result.isValid()); + assertTrue(result.getErrors().contains("Invalid email format")); + } + + @Test + public void testValidateUser_invalidPhone() { + User user = new User(); + user.setUsername("john_doe"); + user.setPhone("abc-def-ghij"); + + ValidationResult result = RequestValidator.validateUser(user); + assertFalse(result.isValid()); + assertTrue(result.getErrors().contains("Invalid phone number format")); + } + + @Test + public void testValidateUser_usernameTooLong() { + User user = new User(); + StringBuilder longName = new StringBuilder(); + for (int i = 0; i < 51; i++) { + longName.append("a"); + } + user.setUsername(longName.toString()); + + ValidationResult result = RequestValidator.validateUser(user); + assertFalse(result.isValid()); + assertTrue(result.getErrors().contains("Username must not exceed 50 characters")); + } + + // --- Utility Validation Tests --- + + @Test + public void testValidatePetStatus_valid() { + ValidationResult result = RequestValidator.validatePetStatus("available"); + assertTrue(result.isValid()); + } + + @Test + public void testValidatePetStatus_multipleValid() { + ValidationResult result = RequestValidator.validatePetStatus("available,pending,sold"); + assertTrue(result.isValid()); + } + + @Test + public void testValidatePetStatus_invalid() { + ValidationResult result = RequestValidator.validatePetStatus("unknown"); + assertFalse(result.isValid()); + } + + @Test + public void testValidatePetStatus_null() { + ValidationResult result = RequestValidator.validatePetStatus(null); + assertFalse(result.isValid()); + } + + @Test + public void testValidateId_valid() { + ValidationResult result = RequestValidator.validateId(1L, "petId"); + assertTrue(result.isValid()); + } + + @Test + public void testValidateId_null() { + ValidationResult result = RequestValidator.validateId(null, "petId"); + assertFalse(result.isValid()); + } + + @Test + public void testValidateId_negative() { + ValidationResult result = RequestValidator.validateId(-1L, "petId"); + assertFalse(result.isValid()); + } +}