diff --git a/modules/openapi-generator-online/src/main/java/org/openapitools/codegen/online/model/Generated.java b/modules/openapi-generator-online/src/main/java/org/openapitools/codegen/online/model/Generated.java index eae92ca52af0..845f64d478b5 100644 --- a/modules/openapi-generator-online/src/main/java/org/openapitools/codegen/online/model/Generated.java +++ b/modules/openapi-generator-online/src/main/java/org/openapitools/codegen/online/model/Generated.java @@ -20,9 +20,12 @@ import lombok.Getter; import lombok.Setter; +import java.time.Instant; + @Getter @Setter public class Generated { private String filename; private String friendlyName; + private Instant createdAt = Instant.now(); } diff --git a/modules/openapi-generator-online/src/main/java/org/openapitools/codegen/online/service/GenApiService.java b/modules/openapi-generator-online/src/main/java/org/openapitools/codegen/online/service/GenApiService.java index f10599b59376..e9044bbb4501 100644 --- a/modules/openapi-generator-online/src/main/java/org/openapitools/codegen/online/service/GenApiService.java +++ b/modules/openapi-generator-online/src/main/java/org/openapitools/codegen/online/service/GenApiService.java @@ -27,12 +27,16 @@ import org.openapitools.codegen.online.model.Generated; import org.openapitools.codegen.online.model.GeneratorInput; import org.openapitools.codegen.online.model.ResponseCode; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.io.ByteArrayResource; import org.springframework.core.io.Resource; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; import org.springframework.web.context.request.NativeWebRequest; import org.springframework.web.server.ResponseStatusException; @@ -45,14 +49,20 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.time.Instant; import java.util.*; +import java.util.concurrent.ConcurrentHashMap; @Service +@EnableScheduling public class GenApiService implements GenApiDelegate { + private static final Logger LOGGER = LoggerFactory.getLogger(GenApiService.class); + private static final long FILE_TTL_MS = 24 * 60 * 60 * 1000L; // 24 hours + private static List clients = new ArrayList<>(); private static List servers = new ArrayList<>(); - private static Map fileMap = new HashMap<>(); + private static final Map fileMap = new ConcurrentHashMap<>(); static { List extensions = CodegenConfigLoader.getAll(); @@ -80,8 +90,11 @@ public Optional getRequest() { @Override public ResponseEntity downloadFile(String fileId) { Generated g = fileMap.get(fileId); - System.out.println("looking for fileId " + fileId); - System.out.println("got filename " + g.getFilename()); + LOGGER.debug("looking for fileId {}", fileId); + if (g == null || g.getCreatedAt().plusMillis(FILE_TTL_MS).isBefore(Instant.now())) { + throw new ResponseStatusException(HttpStatus.NOT_FOUND, "File not found or has expired"); + } + LOGGER.debug("got filename {}", g.getFilename()); File file = new File(g.getFilename()); Path path = Paths.get(file.getAbsolutePath()); @@ -96,15 +109,15 @@ public ResponseEntity downloadFile(String fileId) { try { FileUtils.deleteDirectory(file.getParentFile()); } catch (IOException e) { - System.out.println("failed to delete file " + file.getAbsolutePath()); + LOGGER.error("failed to delete file {}", file.getAbsolutePath()); } return ResponseEntity .ok() .contentType(MediaType.valueOf("application/zip")) + .contentLength(resource.contentLength()) .header("Content-Disposition", "attachment; filename=\"" + g.getFriendlyName() + "-generated.zip\"") .header("Accept-Range", "bytes") - //.header("Content-Length", bytes.length) .body(resource); } @@ -152,7 +165,7 @@ public ResponseEntity generateServerForLanguage(String framework, throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Framework is required"); } String filename = Generator.generateServer(framework, generatorInput); - System.out.println("generated name: " + filename); + LOGGER.debug("generated name: {}", filename); return getResponse(filename, framework + "-server"); } @@ -173,7 +186,7 @@ private ResponseEntity getResponse(String filename, String friendl g.setFilename(filename); g.setFriendlyName(friendlyName); fileMap.put(code, g); - System.out.println(code + ", " + filename); + LOGGER.debug("{}, {}", code, filename); String link = uriBuilder.path("/api/gen/download/").path(code).toUriString(); return ResponseEntity.ok().body(new ResponseCode(code, link)); } else { @@ -181,4 +194,41 @@ private ResponseEntity getResponse(String filename, String friendl } } + /** @VisibleForTesting */ + Generated getFileEntry(String code) { + return fileMap.get(code); + } + + /** @VisibleForTesting */ + void putFileEntry(String code, Generated entry) { + fileMap.put(code, entry); + } + + /** @VisibleForTesting */ + void removeFileEntry(String code) { + fileMap.remove(code); + } + + @Scheduled(fixedDelay = 3_600_000) // run every hour + public void cleanExpiredFiles() { + Instant cutoff = Instant.now().minusMillis(FILE_TTL_MS); + fileMap.entrySet().removeIf(entry -> { + Generated g = entry.getValue(); + if (g.getCreatedAt().isBefore(cutoff)) { + File dir = new File(g.getFilename()).getParentFile(); + if (dir.exists()) { + try { + FileUtils.deleteDirectory(dir); + } catch (IOException | IllegalArgumentException e) { + LOGGER.warn("failed to delete expired file {}, will retry on next run", g.getFilename()); + return false; + } + } + LOGGER.debug("evicted expired file entry {}", entry.getKey()); + return true; + } + return false; + }); + } + } diff --git a/modules/openapi-generator-online/src/test/java/org/openapitools/codegen/online/api/GenApiControllerTest.java b/modules/openapi-generator-online/src/test/java/org/openapitools/codegen/online/api/GenApiControllerTest.java index 81dfe3425774..c1c50dcd7cfa 100644 --- a/modules/openapi-generator-online/src/test/java/org/openapitools/codegen/online/api/GenApiControllerTest.java +++ b/modules/openapi-generator-online/src/test/java/org/openapitools/codegen/online/api/GenApiControllerTest.java @@ -3,7 +3,9 @@ import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.openapitools.codegen.online.model.Generated; import org.openapitools.codegen.online.model.ResponseCode; +import org.openapitools.codegen.online.service.GenApiService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.http.HttpHeaders; @@ -12,9 +14,17 @@ import org.springframework.test.web.servlet.MockMvc; import org.springframework.util.Assert; +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + import static org.hamcrest.Matchers.hasItem; import static org.hamcrest.Matchers.not; import static org.hamcrest.text.MatchesPattern.matchesPattern; +import static org.junit.jupiter.api.Assertions.*; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; @@ -29,6 +39,7 @@ public class GenApiControllerTest { @Autowired private MockMvc mockMvc; + @Test public void clientLanguages() throws Exception { getLanguages("clients", "java"); @@ -39,7 +50,6 @@ public void serverFrameworks() throws Exception { getLanguages("servers", "spring"); } - public void getLanguages(String type, String expected) throws Exception { mockMvc.perform(get("/api/gen/" + type)) .andExpect(status().isOk()) @@ -153,13 +163,31 @@ public void generateWithOpenAPINormalizer() throws Exception { .contentType(MediaType.APPLICATION_JSON) .content(withoutOpenAPINormalizer)) .andExpect(status().isOk()).andReturn().getResponse().getContentAsString(); - String codeOfNotNormalized = new ObjectMapper().readValue(responseOfNotNormalized, ResponseCode.class).getCode(); Long lengthOfNotNormalized = Long.parseLong(mockMvc.perform(get("http://test.com:1234/api/gen/download/" + codeOfNotNormalized)) .andExpect(content().contentType("application/zip")) .andExpect(status().isOk()).andReturn().getResponse().getHeader("Content-Length")); Assert.isTrue(lengthOfNormalized <= lengthOfNotNormalized, "Using the normalizer should result in a smaller or equal file size"); + } + // Fix #3: Content-Length header is present and non-zero on download response + @Test + public void downloadHasContentLengthHeader() throws Exception { + String result = mockMvc.perform(post("http://test.com:1234/api/gen/clients/java") + .contentType(MediaType.APPLICATION_JSON) + .content("{\"openAPIUrl\": \"" + OPENAPI_URL + "\"}")) + .andExpect(status().isOk()) + .andReturn().getResponse().getContentAsString(); + + String code = new ObjectMapper().readValue(result, ResponseCode.class).getCode(); + + String contentLength = mockMvc.perform(get("http://test.com:1234/api/gen/download/" + code)) + .andExpect(status().isOk()) + .andExpect(header().exists(HttpHeaders.CONTENT_LENGTH)) + .andReturn().getResponse().getHeader(HttpHeaders.CONTENT_LENGTH); + + assertTrue(Long.parseLong(contentLength) > 0, "Content-Length should be greater than 0"); } + } diff --git a/modules/openapi-generator-online/src/test/java/org/openapitools/codegen/online/service/GenApiServiceTest.java b/modules/openapi-generator-online/src/test/java/org/openapitools/codegen/online/service/GenApiServiceTest.java new file mode 100644 index 000000000000..c011c90ef087 --- /dev/null +++ b/modules/openapi-generator-online/src/test/java/org/openapitools/codegen/online/service/GenApiServiceTest.java @@ -0,0 +1,162 @@ +package org.openapitools.codegen.online.service; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; +import org.openapitools.codegen.online.model.Generated; +import org.openapitools.codegen.online.model.ResponseCode; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + +import java.io.File; +import java.net.URL; +import java.nio.file.Files; +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import static org.junit.jupiter.api.Assertions.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@AutoConfigureMockMvc +public class GenApiServiceTest { + + private static final String OPENAPI_URL = localPetstoreUrl(); + + private static String localPetstoreUrl() { + URL resource = GenApiServiceTest.class.getClassLoader().getResource("petstore.json"); + if (resource == null) { + throw new IllegalStateException("petstore.json not found in test resources"); + } + return resource.toExternalForm(); + } + + @Autowired + private MockMvc mockMvc; + + @Autowired + private GenApiService genApiService; + + // Fix #1: TTL cleanup removes expired entries and deletes the temp directory + @Test + public void cleanExpiredFilesRemovesExpiredEntry() throws Exception { + File tempDir = Files.createTempDirectory("codegen-test").toFile(); + File bundle = new File(tempDir, "bundle.zip"); + bundle.createNewFile(); + + Generated entry = new Generated(); + entry.setFilename(bundle.getAbsolutePath()); + entry.setFriendlyName("test"); + entry.setCreatedAt(Instant.now().minusSeconds(25 * 3600)); + genApiService.putFileEntry("test-expired-key", entry); + + genApiService.cleanExpiredFiles(); + + assertNull(genApiService.getFileEntry("test-expired-key"), "Expired entry should have been evicted"); + assertFalse(tempDir.exists(), "Temp directory should have been deleted"); + } + + // Fix: entry is retained if directory deletion fails (no orphaned files) + @Test + public void cleanExpiredFilesRetainsEntryWhenDeletionFails() throws Exception { + // Point filename at a path whose "parent" is a regular file — deleteDirectory will fail + File fakeParent = Files.createTempFile("codegen-not-a-dir", ".tmp").toFile(); + try { + Generated entry = new Generated(); + entry.setFilename(new File(fakeParent, "bundle.zip").getAbsolutePath()); + entry.setFriendlyName("test"); + entry.setCreatedAt(Instant.now().minusSeconds(25 * 3600)); + genApiService.putFileEntry("test-deletion-fail-key", entry); + + genApiService.cleanExpiredFiles(); + + assertNotNull(genApiService.getFileEntry("test-deletion-fail-key"), "Entry should be retained when deletion fails"); + } finally { + fakeParent.delete(); + genApiService.removeFileEntry("test-deletion-fail-key"); + } + } + + // Fix #1: recent entry is not evicted by cleanup + @Test + public void cleanExpiredFilesKeepsRecentEntry() throws Exception { + File tempDir = Files.createTempDirectory("codegen-test").toFile(); + try { + Generated entry = new Generated(); + entry.setFilename(new File(tempDir, "bundle.zip").getAbsolutePath()); + entry.setFriendlyName("test"); + genApiService.putFileEntry("test-recent-key", entry); + + genApiService.cleanExpiredFiles(); + + assertNotNull(genApiService.getFileEntry("test-recent-key"), "Recent entry should not be evicted"); + } finally { + genApiService.removeFileEntry("test-recent-key"); + tempDir.delete(); + } + } + + // Fix #1: missing fileId returns 404 + @Test + public void downloadMissingFileReturns404() throws Exception { + mockMvc.perform(get("/api/gen/download/nonexistent-id")) + .andExpect(status().isNotFound()); + } + + // Fix #1: existing entry past TTL returns 404 (TTL enforced at request time) + @Test + public void downloadExpiredEntryReturns404() throws Exception { + String result = mockMvc.perform(post("/api/gen/clients/java") + .contentType(MediaType.APPLICATION_JSON) + .content("{\"openAPIUrl\": \"" + OPENAPI_URL + "\"}")) + .andExpect(status().isOk()) + .andReturn().getResponse().getContentAsString(); + String code = new ObjectMapper().readValue(result, ResponseCode.class).getCode(); + + genApiService.getFileEntry(code).setCreatedAt(Instant.now().minusSeconds(25 * 3600)); + + mockMvc.perform(get("/api/gen/download/" + code)) + .andExpect(status().isNotFound()); + } + + // Fix #1: concurrent generation does not lose entries (thread-safety) + @Test + public void concurrentGenerationDoesNotLoseEntries() throws Exception { + int threads = 5; + CountDownLatch latch = new CountDownLatch(threads); + ExecutorService executor = Executors.newFixedThreadPool(threads); + List codes = new ArrayList<>(); + + for (int i = 0; i < threads; i++) { + executor.submit(() -> { + try { + String result = mockMvc.perform(post("/api/gen/clients/java") + .contentType(MediaType.APPLICATION_JSON) + .content("{\"openAPIUrl\": \"" + OPENAPI_URL + "\"}")) + .andExpect(status().isOk()) + .andReturn().getResponse().getContentAsString(); + synchronized (codes) { + codes.add(new ObjectMapper().readValue(result, ResponseCode.class).getCode()); + } + } catch (Exception e) { + fail("Concurrent generation failed: " + e.getMessage()); + } finally { + latch.countDown(); + } + }); + } + + latch.await(); + executor.shutdown(); + assertEquals(threads, codes.size(), "All concurrent generations should succeed"); + assertEquals(threads, codes.stream().distinct().count(), "All codes should be unique"); + } +} diff --git a/modules/openapi-generator-online/src/test/resources/petstore.json b/modules/openapi-generator-online/src/test/resources/petstore.json new file mode 100644 index 000000000000..ad5523181b6a --- /dev/null +++ b/modules/openapi-generator-online/src/test/resources/petstore.json @@ -0,0 +1,973 @@ +{ + "swagger": "2.0", + "info": { + "description": "This is a sample server Petstore server. For this sample, you can use the api key \"special-key\" to test the authorization filters", + "version": "1.0.0", + "title": "OpenAPI Petstore", + "license": { + "name": "Apache 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0.html" + } + }, + "host": "petstore.swagger.io", + "basePath": "/v2", + "schemes": [ + "http" + ], + "paths": { + "/pet": { + "post": { + "tags": [ + "pet" + ], + "summary": "Add a new pet to the store", + "description": "", + "operationId": "addPet", + "consumes": [ + "application/json", + "application/xml" + ], + "produces": [ + "application/json", + "application/xml" + ], + "parameters": [ + { + "in": "body", + "name": "body", + "description": "Pet object that needs to be added to the store", + "required": false, + "schema": { + "$ref": "#/definitions/Pet" + } + } + ], + "responses": { + "405": { + "description": "Invalid input" + } + }, + "security": [ + { + "petstore_auth": [ + "write:pets", + "read:pets" + ] + } + ] + }, + "put": { + "tags": [ + "pet" + ], + "summary": "Update an existing pet", + "description": "", + "operationId": "updatePet", + "consumes": [ + "application/json", + "application/xml" + ], + "produces": [ + "application/json", + "application/xml" + ], + "parameters": [ + { + "in": "body", + "name": "body", + "description": "Pet object that needs to be added to the store", + "required": false, + "schema": { + "$ref": "#/definitions/Pet" + } + } + ], + "responses": { + "405": { + "description": "Validation exception" + }, + "404": { + "description": "Pet not found" + }, + "400": { + "description": "Invalid ID supplied" + } + }, + "security": [ + { + "petstore_auth": [ + "write:pets", + "read:pets" + ] + } + ] + } + }, + "/pet/findByStatus": { + "get": { + "tags": [ + "pet" + ], + "summary": "Finds Pets by status", + "description": "Multiple status values can be provided with comma separated strings", + "operationId": "findPetsByStatus", + "produces": [ + "application/json", + "application/xml" + ], + "parameters": [ + { + "name": "status", + "in": "query", + "description": "Status values that need to be considered for filter", + "required": false, + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "multi", + "default": ["available"] + } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/Pet" + } + } + }, + "400": { + "description": "Invalid status value" + } + }, + "security": [ + { + "petstore_auth": [ + "write:pets", + "read:pets" + ] + } + ] + } + }, + "/pet/findByTags": { + "get": { + "tags": [ + "pet" + ], + "summary": "Finds Pets by tags", + "description": "Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing.", + "operationId": "findPetsByTags", + "produces": [ + "application/json", + "application/xml" + ], + "parameters": [ + { + "name": "tags", + "in": "query", + "description": "Tags to filter by", + "required": false, + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "multi" + } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/Pet" + } + } + }, + "400": { + "description": "Invalid tag value" + } + }, + "security": [ + { + "petstore_auth": [ + "write:pets", + "read:pets" + ] + } + ] + } + }, + "/pet/{petId}": { + "get": { + "tags": [ + "pet" + ], + "summary": "Find pet by ID", + "description": "Returns a pet when ID < 10. ID > 10 or nonintegers will simulate API error conditions", + "operationId": "getPetById", + "produces": [ + "application/json", + "application/xml" + ], + "parameters": [ + { + "name": "petId", + "in": "path", + "description": "ID of pet that needs to be fetched", + "required": true, + "type": "integer", + "format": "int64" + } + ], + "responses": { + "404": { + "description": "Pet not found" + }, + "200": { + "description": "successful operation", + "schema": { + "$ref": "#/definitions/Pet" + } + }, + "400": { + "description": "Invalid ID supplied" + } + }, + "security": [ + { + "api_key": [] + }, + { + "petstore_auth": [ + "write:pets", + "read:pets" + ] + } + ] + }, + "post": { + "tags": [ + "pet" + ], + "summary": "Updates a pet in the store with form data", + "description": "", + "operationId": "updatePetWithForm", + "consumes": [ + "application/x-www-form-urlencoded" + ], + "produces": [ + "application/json", + "application/xml" + ], + "parameters": [ + { + "name": "petId", + "in": "path", + "description": "ID of pet that needs to be updated", + "required": true, + "type": "string" + }, + { + "name": "name", + "in": "formData", + "description": "Updated name of the pet", + "required": false, + "type": "string" + }, + { + "name": "status", + "in": "formData", + "description": "Updated status of the pet", + "required": false, + "type": "string" + } + ], + "responses": { + "405": { + "description": "Invalid input" + } + }, + "security": [ + { + "petstore_auth": [ + "write:pets", + "read:pets" + ] + } + ] + }, + "delete": { + "tags": [ + "pet" + ], + "summary": "Deletes a pet", + "description": "", + "operationId": "deletePet", + "produces": [ + "application/json", + "application/xml" + ], + "parameters": [ + { + "name": "api_key", + "in": "header", + "description": "", + "required": false, + "type": "string" + }, + { + "name": "petId", + "in": "path", + "description": "Pet id to delete", + "required": true, + "type": "integer", + "format": "int64" + } + ], + "responses": { + "400": { + "description": "Invalid pet value" + } + }, + "security": [ + { + "petstore_auth": [ + "write:pets", + "read:pets" + ] + } + ] + } + }, + "/pet/{petId}/uploadImage": { + "post": { + "tags": [ + "pet" + ], + "summary": "uploads an image", + "description": "", + "operationId": "uploadFile", + "consumes": [ + "multipart/form-data" + ], + "produces": [ + "application/json", + "application/xml" + ], + "parameters": [ + { + "name": "petId", + "in": "path", + "description": "ID of pet to update", + "required": true, + "type": "integer", + "format": "int64" + }, + { + "name": "additionalMetadata", + "in": "formData", + "description": "Additional data to pass to server", + "required": false, + "type": "string" + }, + { + "name": "file", + "in": "formData", + "description": "file to upload", + "required": false, + "type": "file" + } + ], + "responses": { + "default": { + "description": "successful operation" + } + }, + "security": [ + { + "petstore_auth": [ + "write:pets", + "read:pets" + ] + } + ] + } + }, + "/store/inventory": { + "get": { + "tags": [ + "store" + ], + "summary": "Returns pet inventories by status", + "description": "Returns a map of status codes to quantities", + "operationId": "getInventory", + "produces": [ + "application/json", + "application/xml" + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "type": "object", + "additionalProperties": { + "type": "integer", + "format": "int32" + } + } + } + }, + "security": [ + { + "api_key": [] + } + ] + } + }, + "/store/order": { + "post": { + "tags": [ + "store" + ], + "summary": "Place an order for a pet", + "description": "", + "operationId": "placeOrder", + "produces": [ + "application/json", + "application/xml" + ], + "parameters": [ + { + "in": "body", + "name": "body", + "description": "order placed for purchasing the pet", + "required": false, + "schema": { + "$ref": "#/definitions/Order" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "$ref": "#/definitions/Order" + } + }, + "400": { + "description": "Invalid Order" + } + } + } + }, + "/store/order/{orderId}": { + "get": { + "tags": [ + "store" + ], + "summary": "Find purchase order by ID", + "description": "For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions", + "operationId": "getOrderById", + "produces": [ + "application/json", + "application/xml" + ], + "parameters": [ + { + "name": "orderId", + "in": "path", + "description": "ID of pet that needs to be fetched", + "required": true, + "type": "string" + } + ], + "responses": { + "404": { + "description": "Order not found" + }, + "200": { + "description": "successful operation", + "schema": { + "$ref": "#/definitions/Order" + } + }, + "400": { + "description": "Invalid ID supplied" + } + } + }, + "delete": { + "tags": [ + "store" + ], + "summary": "Delete purchase order by ID", + "description": "For valid response try integer IDs with value < 1000. Anything above 1000 or nonintegers will generate API errors", + "operationId": "deleteOrder", + "produces": [ + "application/json", + "application/xml" + ], + "parameters": [ + { + "name": "orderId", + "in": "path", + "description": "ID of the order that needs to be deleted", + "required": true, + "type": "string" + } + ], + "responses": { + "404": { + "description": "Order not found" + }, + "400": { + "description": "Invalid ID supplied" + } + } + } + }, + "/user": { + "post": { + "tags": [ + "user" + ], + "summary": "Create user", + "description": "This can only be done by the logged in user.", + "operationId": "createUser", + "produces": [ + "application/json", + "application/xml" + ], + "parameters": [ + { + "in": "body", + "name": "body", + "description": "Created user object", + "required": false, + "schema": { + "$ref": "#/definitions/User" + } + } + ], + "responses": { + "default": { + "description": "successful operation" + } + } + } + }, + "/user/createWithArray": { + "post": { + "tags": [ + "user" + ], + "summary": "Creates list of users with given input array", + "description": "", + "operationId": "createUsersWithArrayInput", + "produces": [ + "application/json", + "application/xml" + ], + "parameters": [ + { + "in": "body", + "name": "body", + "description": "List of user object", + "required": false, + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/User" + } + } + } + ], + "responses": { + "default": { + "description": "successful operation" + } + } + } + }, + "/user/createWithList": { + "post": { + "tags": [ + "user" + ], + "summary": "Creates list of users with given input array", + "description": "", + "operationId": "createUsersWithListInput", + "produces": [ + "application/json", + "application/xml" + ], + "parameters": [ + { + "in": "body", + "name": "body", + "description": "List of user object", + "required": false, + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/User" + } + } + } + ], + "responses": { + "default": { + "description": "successful operation" + } + } + } + }, + "/user/login": { + "get": { + "tags": [ + "user" + ], + "summary": "Logs user into the system", + "description": "", + "operationId": "loginUser", + "produces": [ + "application/json", + "application/xml" + ], + "parameters": [ + { + "name": "username", + "in": "query", + "description": "The user name for login", + "required": false, + "type": "string" + }, + { + "name": "password", + "in": "query", + "description": "The password for login in clear text", + "required": false, + "type": "string" + } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "type": "string" + } + }, + "400": { + "description": "Invalid username/password supplied" + } + } + } + }, + "/user/logout": { + "get": { + "tags": [ + "user" + ], + "summary": "Logs out current logged in user session", + "description": "", + "operationId": "logoutUser", + "produces": [ + "application/json", + "application/xml" + ], + "responses": { + "default": { + "description": "successful operation" + } + } + } + }, + "/user/{username}": { + "get": { + "tags": [ + "user" + ], + "summary": "Get user by user name", + "description": "", + "operationId": "getUserByName", + "produces": [ + "application/json", + "application/xml" + ], + "parameters": [ + { + "name": "username", + "in": "path", + "description": "The name that needs to be fetched. Use user1 for testing. ", + "required": true, + "type": "string" + } + ], + "responses": { + "404": { + "description": "User not found" + }, + "200": { + "description": "successful operation", + "schema": { + "$ref": "#/definitions/User" + }, + "examples": { + "application/json": { + "id": 1, + "username": "johnp", + "firstName": "John", + "lastName": "Public", + "email": "johnp@swagger.io", + "password": "-secret-", + "phone": "0123456789", + "userStatus": 0 + } + } + }, + "400": { + "description": "Invalid username supplied" + } + } + }, + "put": { + "tags": [ + "user" + ], + "summary": "Updated user", + "description": "This can only be done by the logged in user.", + "operationId": "updateUser", + "produces": [ + "application/json", + "application/xml" + ], + "parameters": [ + { + "name": "username", + "in": "path", + "description": "name that need to be deleted", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "description": "Updated user object", + "required": false, + "schema": { + "$ref": "#/definitions/User" + } + } + ], + "responses": { + "404": { + "description": "User not found" + }, + "400": { + "description": "Invalid user supplied" + } + } + }, + "delete": { + "tags": [ + "user" + ], + "summary": "Delete user", + "description": "This can only be done by the logged in user.", + "operationId": "deleteUser", + "produces": [ + "application/json", + "application/xml" + ], + "parameters": [ + { + "name": "username", + "in": "path", + "description": "The name that needs to be deleted", + "required": true, + "type": "string" + } + ], + "responses": { + "404": { + "description": "User not found" + }, + "400": { + "description": "Invalid username supplied" + } + } + } + } + }, + "securityDefinitions": { + "api_key": { + "type": "apiKey", + "name": "api_key", + "in": "header" + }, + "petstore_auth": { + "type": "oauth2", + "authorizationUrl": "http://petstore.swagger.io/api/oauth/dialog", + "flow": "implicit", + "scopes": { + "write:pets": "modify pets in your account", + "read:pets": "read your pets" + } + } + }, + "definitions": { + "User": { + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "username": { + "type": "string" + }, + "firstName": { + "type": "string" + }, + "lastName": { + "type": "string" + }, + "email": { + "type": "string" + }, + "password": { + "type": "string" + }, + "phone": { + "type": "string" + }, + "userStatus": { + "type": "integer", + "format": "int32", + "description": "User Status" + } + }, + "xml": { + "name": "User" + } + }, + "Category": { + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + } + }, + "xml": { + "name": "Category" + } + }, + "Pet": { + "required": [ + "name", + "photoUrls" + ], + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "category": { + "$ref": "#/definitions/Category" + }, + "name": { + "type": "string", + "example": "doggie" + }, + "photoUrls": { + "type": "array", + "xml": { + "name": "photoUrl", + "wrapped": true + }, + "items": { + "type": "string" + } + }, + "tags": { + "type": "array", + "xml": { + "name": "tag", + "wrapped": true + }, + "items": { + "$ref": "#/definitions/Tag" + } + }, + "status": { + "type": "string", + "description": "pet status in the store", + "enum": [ + "available", + "pending", + "sold" + ] + } + }, + "xml": { + "name": "Pet" + } + }, + "Tag": { + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + } + }, + "xml": { + "name": "Tag" + } + }, + "Order": { + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "petId": { + "type": "integer", + "format": "int64" + }, + "quantity": { + "type": "integer", + "format": "int32" + }, + "shipDate": { + "type": "string", + "format": "date-time" + }, + "status": { + "type": "string", + "description": "Order Status", + "enum": [ + "placed", + "approved", + "delivered" + ] + }, + "complete": { + "type": "boolean" + } + }, + "xml": { + "name": "Order" + } + } + } +}