Skip to content

Commit fb49bb9

Browse files
roman-e2be2b-bot[bot]
authored andcommitted
refactor(shared): move the OpenAPI document handler to packages/shared
GitOrigin-RevId: 9a9b3d97b843cc8955bd145fdf7e6bd928461e99
1 parent df758d1 commit fb49bb9

4 files changed

Lines changed: 97 additions & 86 deletions

File tree

‎packages/api/internal/openapispec/handler_test.go‎

Lines changed: 0 additions & 78 deletions
This file was deleted.

‎packages/api/main.go‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ import (
3333
"github.com/e2b-dev/infra/packages/api/internal/middleware/idempotency"
3434
"github.com/e2b-dev/infra/packages/api/internal/middleware/ratelimit"
3535
"github.com/e2b-dev/infra/packages/api/internal/oauth"
36-
"github.com/e2b-dev/infra/packages/api/internal/openapispec"
3736
"github.com/e2b-dev/infra/packages/api/internal/utils"
3837
"github.com/e2b-dev/infra/packages/auth/pkg/auth"
3938
sqlcdb "github.com/e2b-dev/infra/packages/db/client"
@@ -50,6 +49,7 @@ import (
5049
metricsMiddleware "github.com/e2b-dev/infra/packages/shared/pkg/middleware/otel/metrics"
5150
tracingMiddleware "github.com/e2b-dev/infra/packages/shared/pkg/middleware/otel/tracing"
5251
"github.com/e2b-dev/infra/packages/shared/pkg/mtls"
52+
"github.com/e2b-dev/infra/packages/shared/pkg/openapispec"
5353
"github.com/e2b-dev/infra/packages/shared/pkg/removedroutes"
5454
"github.com/e2b-dev/infra/packages/shared/pkg/telemetry"
5555
sharedutils "github.com/e2b-dev/infra/packages/shared/pkg/utils"

packages/api/internal/openapispec/handler.go renamed to packages/shared/pkg/openapispec/handler.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,28 @@
1-
// Package openapispec serves the OpenAPI document the API validates requests
2-
// against, so a client can learn which operations this deployment supports.
1+
// Package openapispec serves the OpenAPI document a service validates requests
2+
// against, so a client can learn which operations the deployment supports.
33
package openapispec
44

55
import (
66
"bytes"
77
"crypto/sha256"
88
"encoding/hex"
9+
"encoding/json"
910
"fmt"
1011
"net/http"
1112
"time"
1213

13-
"github.com/getkin/kin-openapi/openapi3"
1414
"github.com/gin-gonic/gin"
1515
)
1616

17-
// Path is where the document is served. It is not an operation in the spec,
18-
// so it must be registered before the request validator, which rejects paths
19-
// the spec does not declare.
17+
// Path is where the API serves its document. A document is not an operation
18+
// in its own spec, so its route must be registered before the request
19+
// validator, which rejects paths the spec does not declare.
2020
const Path = "/openapi.json"
2121

2222
// Handler serves doc as JSON. The document is rendered once. Its ETag is a
2323
// hash of the rendered bytes, so a client that sends it back in
2424
// If-None-Match gets a 304 until the document changes.
25-
func Handler(doc *openapi3.T) (gin.HandlerFunc, error) {
25+
func Handler(doc json.Marshaler) (gin.HandlerFunc, error) {
2626
body, err := doc.MarshalJSON()
2727
if err != nil {
2828
return nil, fmt.Errorf("render the OpenAPI document: %w", err)
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package openapispec
2+
3+
import (
4+
"encoding/json"
5+
"maps"
6+
"net/http"
7+
"net/http/httptest"
8+
"testing"
9+
10+
"github.com/gin-gonic/gin"
11+
"github.com/stretchr/testify/assert"
12+
"github.com/stretchr/testify/require"
13+
)
14+
15+
const (
16+
document = `{"openapi":"3.0.0","info":{"title":"API","version":"1"},"paths":{"/sandboxes":{"post":{"security":[{"ApiKeyAuth":[]}],"responses":{"201":{"description":"Created"}}}}}}`
17+
changedDocument = `{"openapi":"3.0.0","info":{"title":"API","version":"1"},"paths":{}}`
18+
)
19+
20+
// serve sends one request to a Handler serving doc at Path, for GET and HEAD
21+
// as the services register it.
22+
func serve(t *testing.T, doc string, method string, header http.Header) *httptest.ResponseRecorder {
23+
t.Helper()
24+
25+
handler, err := Handler(json.RawMessage(doc))
26+
require.NoError(t, err)
27+
28+
engine := gin.New()
29+
engine.GET(Path, handler)
30+
engine.HEAD(Path, handler)
31+
32+
req := httptest.NewRequestWithContext(t.Context(), method, Path, nil)
33+
maps.Copy(req.Header, header)
34+
actualResponse := httptest.NewRecorder()
35+
engine.ServeHTTP(actualResponse, req)
36+
37+
return actualResponse
38+
}
39+
40+
func Test_Handler(t *testing.T) {
41+
t.Parallel()
42+
43+
// A client reads the served document to learn the deployment's
44+
// operations and their security.
45+
t.Run("should serve the document as JSON", func(t *testing.T) {
46+
t.Parallel()
47+
48+
// Act
49+
actualResponse := serve(t, document, http.MethodGet, nil)
50+
51+
// Assert
52+
require.Equal(t, http.StatusOK, actualResponse.Code)
53+
assert.Equal(t, "application/json", actualResponse.Header().Get("Content-Type"))
54+
assert.Equal(t, "no-cache", actualResponse.Header().Get("Cache-Control"))
55+
assert.JSONEq(t, document, actualResponse.Body.String())
56+
})
57+
58+
t.Run("should answer HEAD with the ETag and no body", func(t *testing.T) {
59+
t.Parallel()
60+
61+
// Act
62+
actualResponse := serve(t, document, http.MethodHead, nil)
63+
64+
// Assert
65+
assert.Equal(t, http.StatusOK, actualResponse.Code)
66+
assert.NotEmpty(t, actualResponse.Header().Get("ETag"))
67+
assert.Empty(t, actualResponse.Body.Bytes())
68+
})
69+
70+
// A client refreshing the document pays a 304 until the document changes.
71+
t.Run("should answer 304 to its ETag until the document changes", func(t *testing.T) {
72+
t.Parallel()
73+
74+
// Arrange
75+
etag := serve(t, document, http.MethodGet, nil).Header().Get("ETag")
76+
require.NotEmpty(t, etag)
77+
ifNoneMatch := http.Header{"If-None-Match": {etag}}
78+
79+
// Act
80+
unchanged := serve(t, document, http.MethodGet, ifNoneMatch)
81+
changed := serve(t, changedDocument, http.MethodGet, ifNoneMatch)
82+
83+
// Assert
84+
assert.Equal(t, http.StatusNotModified, unchanged.Code)
85+
assert.Empty(t, unchanged.Body.Bytes())
86+
assert.Equal(t, http.StatusOK, changed.Code)
87+
assert.NotEqual(t, etag, changed.Header().Get("ETag"))
88+
})
89+
}

0 commit comments

Comments
 (0)