Class OpenApiTest

All Implemented Interfaces:
org.springframework.beans.factory.InitializingBean

@ContextConfiguration public class OpenApiTest extends BaseTest5 implements org.springframework.beans.factory.InitializingBean
  • Constructor Details

    • OpenApiTest

      public OpenApiTest()
  • Method Details

    • afterPropertiesSet

      public void afterPropertiesSet()
      Specified by:
      afterPropertiesSet in interface org.springframework.beans.factory.InitializingBean
    • testExternalDocumentationUrlIsReplaced

      @Test public void testExternalDocumentationUrlIsReplaced()
    • testInfoMatchContentOfOpenApiConfiguration

      @Test public void testInfoMatchContentOfOpenApiConfiguration() throws IOException
      Throws:
      IOException
    • testEnsureThatAllEndpointHaveADefaultGetResponseOrIsARedirection

      @Test public void testEnsureThatAllEndpointHaveADefaultGetResponseOrIsARedirection()
    • testEnsureThatAllErrorResponsesUseResponseErrorObjectWithJsonMediaType

      @Test public void testEnsureThatAllErrorResponsesUseResponseErrorObjectWithJsonMediaType()
    • testPvalueDistributionAdvertisesTheStoredHistogramContract

      @Test public void testPvalueDistributionAdvertisesTheStoredHistogramContract()
      GET /resultSets/{id}/pvalueDistribution serves the stored histogram. The spec has to say so: the default column is the uncorrected one, corrected is not on the menu at all, and bins is capped at the stored bin count instead of the old 1..1000 range.
    • testGetDatasetsCategories

      @Test public void testGetDatasetsCategories()
    • testFilterArgSchemas

      @Test public void testFilterArgSchemas()
    • testSortArgSchemas

      @Test public void testSortArgSchemas()
    • testLimitArgIs5000ForGetDatasetsAnnotations

      @Test public void testLimitArgIs5000ForGetDatasetsAnnotations()
    • testSearchableProperties

      @Test public void testSearchableProperties()
    • testExamplesFromClasspath

      @Test public void testExamplesFromClasspath() throws IOException
      Throws:
      IOException
    • testWireNamesAreCamelCaseEverywhere

      @Test public void testWireNamesAreCamelCaseEverywhere()
      The wire speaks one language, and it is camelCase.

      Every property name in every published schema, and every query parameter, must be camelCase. This is the guard for c4d2d4ceb9 / 8b2c8b09ff, which collapsed the two conventions the API used to serve at once. It exists because the first sweep grepped gemma-rest and missed GeoScrapeDryRunCandidate — a response class that lives in gemma-core but is serialized by a gemma-rest resource. A half-done rename is worse than either end state, and the half left undone was the response a downstream screening script consumed. Reading the spec instead of the source catches that class of miss regardless of which module the class lives in.

      Enum VALUES are deliberately not checked: expression_experiment, gemma_intake and friends are data, not keys, and renaming them would stop matching what is stored. This walks property names and parameter names only.

    • testPaginatedResponsesDeclareTheirPayloadType

      @Test public void testPaginatedResponsesDeclareTheirPayloadType()
      Every response container reachable from a path must say what its data payload holds.

      An endpoint that supports both offset and cursor pagination returns Object and declares its shapes with @Schema(oneOf = {...}). Java erases type arguments, so naming a raw generic container there — PaginatedResponseDataObject.class — produces a schema whose data is an array of bare object, and the payload type is gone. A client generated from that deserializes to untyped dictionaries and reads nulls for every field instead of failing, which is why this is worth a build failure rather than a code review.

      The fix is to name a bound subclass from OpenApiResponseTypes instead. Since the method's return type is Object, nothing but this test checks that the declared container is also the one the method actually builds.

    • testRetryableServiceUnavailableResponsesDeclareRetryAfter

      @Test public void testRetryableServiceUnavailableResponsesDeclareRetryAfter()
      Every 503 the API can schedule a retry for must declare the header carrying that schedule.

      Two resource classes told callers in prose to "lookup the `Retry-After` header" while no response anywhere declared one, so a generated client could not see it and gemmapy had to write the parsing by hand. Everything that throws ServiceUnavailableException passes a retry-after, and CostlyEndpointFilter sets the header itself, so the only 503s legitimately without one are the standing conditions in NO_RETRY_AFTER_503.

    • testCostlyEndpointsDocumentTheirCapacity503

      @Test public void testCostlyEndpointsDocumentTheirCapacity503()
      A Costly route can be refused when its budget is full, so its 503 belongs in the spec.

      Eight of the fourteen did not document one. The check runs off the annotation rather than a hand-kept list so a route marked @Costly later cannot quietly skip it; the operation is matched by operationId, which is the method name unless @Operation overrides it.

    • testOperationIdsAreNotAutoDisambiguated

      @Test public void testOperationIdsAreNotAutoDisambiguated()
      swagger-core disambiguates two resource methods of the same name by appending _1, and which of the pair gets the suffix depends on scan order — so the generated client's method name for one of them is not stable across builds. Name both explicitly with @Operation(operationId = ...) instead.
    • testBearerAuthIsDeclared

      @Test public void testBearerAuthIsDeclared()
      The bearer scheme POST /login mints has to be declared, or a generated client cannot express it and every caller hand-rolls the Authorization header.
    • testResponseMediaTypesCarryNoQualityParameter

      @Test public void testResponseMediaTypesCarryNoQualityParameter()
      A content map is keyed by media type, and a media type has no q parameter — that belongs in an Accept header. A key carrying one matches nothing a client sends.

      They arrive from the JAX-RS quality-of-source weight: a resource serving both JSON and TSV writes @Produces(TEXT_TAB_SEPARATED_VALUES_UTF8 + ";qs=0.9") to keep JSON the default, and swagger-core copies that into the spec as ; q=0.9. The weight has to stay in the annotation, so OpenApiFactory strips it on the way out; this pins that it happened.

    • testNonJsonResponseBodiesAreTyped

      @Test public void testNonJsonResponseBodiesAreTyped()
      A non-JSON response body has to say what it is. @Content with a mediaType and no schema defaults to type: object, which for a TSV download says the body is a JSON object — three of them did. The right declaration is @Schema(type = "string").
    • testSuccessfulJsonResponsesDeclareASchema

      @Test public void testSuccessfulJsonResponsesDeclareASchema()
      A successful JSON response must say what its body is.

      testEnsureThatAllEndpointHaveADefaultGetResponseOrIsARedirection already requires a content block, but a content block with a null schema satisfies it — which is what seven Response-returning GETs had. Returning raw jakarta.ws.rs.core.Response tells swagger-core nothing about the entity, so unless the method declares an @ApiResponse with a schema the spec publishes the media type and stops there, and a generated client hands back an untyped blob.

      The write surface was exempted behind a shrink-only list when this was written; the list is gone because it reached zero.

    • testEveryResponseHasADescription

      @Test public void testEveryResponseHasADescription()
      description is REQUIRED on a Response Object in OpenAPI 3.0, so a document with one missing is invalid. 184 responses had none and another 59 carried swagger-core's "default response" placeholder, which is the same thing wearing a hat.

      Two exclusions. GET /genes/probes/refresh publishes */* with no content swagger-core can attach a description to — swagger-api/swagger-core#4693, which testEnsureThatAllEndpointHaveADefaultGetResponseOrIsARedirection already skips for the same reason. The /custom paths are Jersey test fixtures from UnknownQueryParameterFilterTest and friends: they are on this module's test classpath, so the scan picks them up here, and they are not in the deployed spec.

    • testEveryOperationIsTagged

      @Test public void testEveryOperationIsTagged()
      Every operation must carry a tag, because a tag is how a generated client is organised.

      204 of 286 operations had none, so swagger-codegen put them all in one DefaultApi — a 170-method class with no structure, which is what gemmapy generates against today. Tags split that into one class per resource.

      The /custom paths are excluded for the same reason as in testEveryResponseHasADescription(): they are Jersey fixtures on this module's test classpath, not deployed routes.

    • testNoHibernateEntityIsPublishedAsASchema

      @Test public void testNoHibernateEntityIsPublishedAsASchema()
      No Hibernate entity may appear in the published specification.

      AdminPipelineWebService used to return PipelineJobBatch directly. Its jobs are a lazy @OneToMany, and each job holds a lazy @ManyToOne to ExpressionExperiment — so following one field pulled in the experiment graph and 44 entity schemas with 466 properties arrived in the document through it and nothing else. Nothing had marked them @JsonIgnore, and nothing initialized them either, so the same field was a serialization hazard as well as specification noise.

      The rule is the general one rather than a list of those 44: an entity reaching the wire is a missing value object, whichever entity it is.

    • testEveryParameterHasADescription

      @Test public void testEveryParameterHasADescription()
      Every parameter must be described. A parameter is what a caller actually sets, so an undescribed one is the gap they hit first — 412 of 652 had nothing.

      Only parameters the specification publishes are covered, which is the right scope: a @Parameter(hidden = true) legacy alias never reaches a client and has nothing to document. The /custom paths are test fixtures, excluded as elsewhere.

    • testUndescribedSchemaPropertiesDoNotIncrease

      @Test public void testUndescribedSchemaPropertiesDoNotIncrease()
      See Also:
      • UNDESCRIBED_PROPERTY_BUDGET
    • testCollapsedRoutesDeclareEveryParameterTheyAccept

      @Test public void testCollapsedRoutesDeclareEveryParameterTheyAccept()
      A published operation must declare every parameter the endpoint behind it accepts.

      Two JAX-RS methods can serve one (verb, path) and differ only in @Produces — GET /datasets/{dataset}/design does, one for JSON and one for TSV. swagger-core keeps a single operation for the pair, and it takes the winner's signature as well as its annotation. The JSON method wins there and accepts only {dataset}, so ?quantitationType= and ?useProcessedQuantitationType= — read by the TSV branch at runtime — were absent from the spec with nothing to indicate it.

      The file guards the response half of that merge with a comment asking that the two @Operation annotations stay identical. A comment is not enforcement, and it said nothing about parameters. This is the enforcement, and it is written as the general rule: whatever an endpoint reads, the operation says so, however many methods implement it.

      Parameters marked @Parameter(hidden = true) are excluded — a deliberately unpublished legacy alias is not a gap.

    • testPublicWireNamesAreNotRenamedOrRemoved

      @Test public void testPublicWireNamesAreNotRenamedOrRemoved()
      See Also:
      • pinnedWireNames()
    • testAdminAndInternalSurfaceIsMarkedInternal

      @Test public void testAdminAndInternalSurfaceIsMarkedInternal()
      Everything under /admin and /internal must be reachable as internal surface.

      gemmapy prunes Gemma's specification before generating, and its filter is "drop the non-GET operations" — which keeps every admin read, so its published SDK carries admin_api, admin_pipeline_api and observability_api. Dropping writes is not the same as dropping admin surface, and there was no marker to drop the right thing by.

      There is one now: the Admin, Admin/Pipeline and Internal/Pipeline tags carry x-internal: true, so a consumer collects the internal tag names from the document's own tags list and drops the operations carrying them — no hardcoded tag names, no path prefixes. This pins that every operation under those prefixes is actually covered, so a new admin endpoint cannot slip out of the marked set.

      swagger-codegen's own -Dapis=<Tag> is not a substitute. It selects API classes but generates no models unless -Dmodels is also passed, and passing both selected no APIs at all when tried — and it cannot drop the schemas that only the excluded operations reach.

    • testNothingIsBothRequiredAndNullable

      @Test public void testNothingIsBothRequiredAndNullable()
      A property cannot be both required and nullable without saying nothing.

      required means the key is always present; nullable means its value may be null. Both together is legal OpenAPI and occasionally meaningful, but in this specification it is almost always a mistake — a field marked required because it "is always set" while also carrying @Nullable means one of the two annotations is wrong about the wire.

      The required declarations here are the ones that can be proved rather than judged: a Java primitive cannot be null and Jackson always serializes it, so the key is always present. That rule is deliberately not applied wholesale — required is direction-agnostic in OpenAPI 3.0, and ExperimentalDesignValueObject and everything under it is accepted as a request body by PUT /datasets/{dataset}/design, where marking a field required would start demanding it from clients.