|
| 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