blob: 6312490c8bd36758078d745f3f95bb308168658b [file] [log] [blame] [edit]
// Package api implements the REST API for the scrap exchange service.
package api
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/http/httptest"
"testing"
"github.com/go-chi/chi/v5"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"go.skia.org/infra/go/metrics2"
"go.skia.org/infra/go/testutils"
"go.skia.org/infra/scrap/go/scrap"
"go.skia.org/infra/scrap/go/scrap/mocks"
)
// hash used by some tests.
const hash = "01234567890abcdef"
const scrapName = "@MyScrapName"
var errMyMockError = errors.New("my error returned from mock ScrapExchange")
// validScrapBody returns an io.Reader that contains a valid serialized
// scrap.ScrapBody.
func validScrapBody(t *testing.T) io.Reader {
var b bytes.Buffer
scrapBody := scrap.ScrapBody{
Type: scrap.SVG,
Body: "<svg></svg>",
}
err := json.NewEncoder(&b).Encode(scrapBody)
require.NoError(t, err)
return &b
}
// validScrapName returns an io.Reader that contains a valid serialized
// scrap.Name.
func validScrapName(t *testing.T) io.Reader {
var b bytes.Buffer
scrapName := scrap.Name{
Hash: hash,
}
err := json.NewEncoder(&b).Encode(scrapName)
require.NoError(t, err)
return &b
}
// makeRequest makes the HTTP request to Api with the given scrapExchange.
func makeRequest(scrapExchange *mocks.ScrapExchange, w http.ResponseWriter, r *http.Request) {
a := &Api{
scrapExchange: scrapExchange,
}
// Make the request through a chi.Router so the URL paths get parsed and
// routed correctly.
router := chi.NewRouter()
a.AddHandlers(router, AddProtectedEndpoints)
router.ServeHTTP(w, r)
}
func testMethodAndPathReturnExpectedStatusCode(t *testing.T, method string, path string, expectedCode int) {
a := New(nil)
// Make the request through a chi.Router so the URL paths get parsed and
// routed correctly.
router := chi.NewRouter()
a.AddHandlers(router, DoNotAddProtectedEndpoints)
w := httptest.NewRecorder()
r := httptest.NewRequest(method, path, nil)
router.ServeHTTP(w, r)
require.Equal(t, expectedCode, w.Code, "path: %s method: %s", path, method)
}
func TestAddHandlers_DoNotEnableProtectedEndpoints_ProtectedEndpointsReturn4xxStatusCodes(t *testing.T) {
testMethodAndPathReturnExpectedStatusCode(t, "POST", "/_/scraps/", http.StatusNotFound)
testMethodAndPathReturnExpectedStatusCode(t, "DELETE", fmt.Sprintf("/_/scraps/svg/%s", hash), http.StatusMethodNotAllowed)
testMethodAndPathReturnExpectedStatusCode(t, "PUT", fmt.Sprintf("/_/names/svg/%s", scrapName), http.StatusMethodNotAllowed)
testMethodAndPathReturnExpectedStatusCode(t, "DELETE", fmt.Sprintf("/_/names/svg/%s", scrapName), http.StatusMethodNotAllowed)
}
func TestWriteJSON_InvalidJSON_ReportsError(t *testing.T) {
w := httptest.NewRecorder()
notSerializable := struct {
C complex128
}{
C: 12 + 3i,
}
writeJSON(w, notSerializable)
require.Equal(t, http.StatusInternalServerError, w.Code)
require.Equal(t, "Failed to encode JSON response.\n", w.Body.String())
require.Equal(t, "text/plain; charset=utf-8", w.Header().Get("Content-Type"))
}
func TestScrapCreateHandler_InvalidJSON_ReturnsBadRequest(t *testing.T) {
w := httptest.NewRecorder()
b := bytes.NewBufferString("]This is not valid json[")
r := httptest.NewRequest("POST", "/_/scraps/", b)
makeRequest(nil, w, r)
require.Equal(t, http.StatusBadRequest, w.Code)
require.Equal(t, "text/plain; charset=utf-8", w.Header().Get("Content-Type"))
}
func TestScrapCreateHandler_WriteSucceeds_Success(t *testing.T) {
callMetric := metrics2.GetCounter(scrapsCreateCallMetric)
callMetric.Reset()
w := httptest.NewRecorder()
r := httptest.NewRequest("POST", "/_/scraps/", validScrapBody(t))
scrapExchange := &mocks.ScrapExchange{}
scrapID := scrap.ScrapID{
Hash: hash,
}
scrapExchange.On("CreateScrap", testutils.AnyContext, mock.AnythingOfType("scrap.ScrapBody")).Return(scrapID, nil)
makeRequest(scrapExchange, w, r)
require.Equal(t, http.StatusOK, w.Code)
require.Equal(t, fmt.Sprintf("{\"Hash\":\"%s\"}\n", hash), w.Body.String())
require.Equal(t, "application/json", w.Header().Get("Content-Type"))
require.Equal(t, int64(1), callMetric.Get())
}
func TestScrapCreateHandler_WriteFails_ReturnsInternalServerError(t *testing.T) {
w := httptest.NewRecorder()
r := httptest.NewRequest("POST", "/_/scraps/", validScrapBody(t))
scrapExchange := &mocks.ScrapExchange{}
scrapExchange.On("CreateScrap", testutils.AnyContext, mock.AnythingOfType("scrap.ScrapBody")).Return(scrap.ScrapID{}, errMyMockError)
makeRequest(scrapExchange, w, r)
require.Equal(t, http.StatusInternalServerError, w.Code)
require.Equal(t, "text/plain; charset=utf-8", w.Header().Get("Content-Type"))
}
func TestScrapGetHandler_UnknownType_ReturnsBadRequest(t *testing.T) {
w := httptest.NewRecorder()
r := httptest.NewRequest("GET", fmt.Sprintf("/_/scraps/unknowntype/%s", hash), nil)
makeRequest(nil, w, r)
require.Equal(t, http.StatusBadRequest, w.Code)
require.Equal(t, "text/plain; charset=utf-8", w.Header().Get("Content-Type"))
require.Equal(t, "Unknown type.\n", w.Body.String())
}
func TestScrapGetHandler_HappyPath_Success(t *testing.T) {
callMetric := metrics2.GetCounter(scrapsGetCallMetric)
callMetric.Reset()
w := httptest.NewRecorder()
r := httptest.NewRequest("GET", fmt.Sprintf("/_/scraps/svg/%s", hash), nil)
scrapExchange := &mocks.ScrapExchange{}
scrapBody := scrap.ScrapBody{
Type: scrap.SVG,
Body: "<svg></svg>",
}
scrapExchange.On("LoadScrap", testutils.AnyContext, scrap.SVG, hash).Return(scrapBody, nil)
makeRequest(scrapExchange, w, r)
require.Equal(t, http.StatusOK, w.Code)
require.Equal(t, "application/json", w.Header().Get("Content-Type"))
require.Equal(t, int64(1), callMetric.Get())
}
func TestScrapGetHandler_LoadFails_ReturnsBadRequest(t *testing.T) {
w := httptest.NewRecorder()
r := httptest.NewRequest("GET", fmt.Sprintf("/_/scraps/svg/%s", hash), nil)
scrapExchange := &mocks.ScrapExchange{}
scrapExchange.On("LoadScrap", testutils.AnyContext, scrap.SVG, hash).Return(scrap.ScrapBody{}, errMyMockError)
makeRequest(scrapExchange, w, r)
require.Equal(t, http.StatusBadRequest, w.Code)
require.Equal(t, "text/plain; charset=utf-8", w.Header().Get("Content-Type"))
require.Equal(t, "Failed to load scrap.\n", w.Body.String())
}
func TestScrapDeleteHandler_HappyPath_Success(t *testing.T) {
callMetric := metrics2.GetCounter(scrapsDeleteCallMetric)
callMetric.Reset()
w := httptest.NewRecorder()
r := httptest.NewRequest("DELETE", fmt.Sprintf("/_/scraps/svg/%s", hash), nil)
scrapExchange := &mocks.ScrapExchange{}
scrapExchange.On("DeleteScrap", testutils.AnyContext, scrap.SVG, hash).Return(nil)
makeRequest(scrapExchange, w, r)
require.Equal(t, http.StatusOK, w.Code)
require.Equal(t, int64(1), callMetric.Get())
}
func TestScrapDeleteHandler_DeleteFails_ReturnsBadRequest(t *testing.T) {
w := httptest.NewRecorder()
r := httptest.NewRequest("DELETE", fmt.Sprintf("/_/scraps/svg/%s", hash), nil)
scrapExchange := &mocks.ScrapExchange{}
scrapExchange.On("DeleteScrap", testutils.AnyContext, scrap.SVG, hash).Return(errMyMockError)
makeRequest(scrapExchange, w, r)
require.Equal(t, http.StatusBadRequest, w.Code)
require.Equal(t, "text/plain; charset=utf-8", w.Header().Get("Content-Type"))
require.Equal(t, "Failed to delete scrap.\n", w.Body.String())
}
func TestRawGetHandler_HappyPath_Success(t *testing.T) {
callMetric := metrics2.GetCounter(rawGetCallMetric)
callMetric.Reset()
w := httptest.NewRecorder()
r := httptest.NewRequest("GET", fmt.Sprintf("/_/raw/svg/%s", hash), nil)
scrapExchange := &mocks.ScrapExchange{}
scrapBody := scrap.ScrapBody{
Type: scrap.SVG,
Body: "<svg></svg>",
}
scrapExchange.On("LoadScrap", testutils.AnyContext, scrap.SVG, hash).Return(scrapBody, nil)
makeRequest(scrapExchange, w, r)
require.Equal(t, http.StatusOK, w.Code)
require.Equal(t, scrapBody.Body, w.Body.String())
require.Equal(t, "image/svg+xml", w.Header().Get("Content-Type"))
require.Equal(t, int64(1), callMetric.Get())
}
func TestRawGetHandler_LoadFails_ReturnsBadRequest(t *testing.T) {
w := httptest.NewRecorder()
r := httptest.NewRequest("GET", fmt.Sprintf("/_/raw/svg/%s", hash), nil)
scrapExchange := &mocks.ScrapExchange{}
scrapExchange.On("LoadScrap", testutils.AnyContext, scrap.SVG, hash).Return(scrap.ScrapBody{}, errMyMockError)
makeRequest(scrapExchange, w, r)
require.Equal(t, http.StatusBadRequest, w.Code)
require.Equal(t, "text/plain; charset=utf-8", w.Header().Get("Content-Type"))
require.Equal(t, "Failed to load scrap.\n", w.Body.String())
}
func TestTemplateGetHandler_LoadFails_ReturnsBadRequest(t *testing.T) {
w := httptest.NewRecorder()
r := httptest.NewRequest("GET", fmt.Sprintf("/_/tmpl/svg/%s/js", hash), nil)
scrapExchange := &mocks.ScrapExchange{}
scrapExchange.On("Expand", testutils.AnyContext, scrap.SVG, hash, scrap.JS, w).Return(errMyMockError)
makeRequest(scrapExchange, w, r)
require.Equal(t, http.StatusBadRequest, w.Code)
require.Equal(t, "text/plain; charset=utf-8", w.Header().Get("Content-Type"))
require.Equal(t, "Failed to expand scrap.\n", w.Body.String())
}
func TestTemplateGetHandler_HappyPath_Success(t *testing.T) {
callMetric := metrics2.GetCounter(templateGetCallMetric)
callMetric.Reset()
w := httptest.NewRecorder()
r := httptest.NewRequest("GET", fmt.Sprintf("/_/tmpl/svg/%s/js", hash), nil)
const code = "// This is JS code!"
scrapExchange := &mocks.ScrapExchange{}
scrapExchange.On("Expand", testutils.AnyContext, scrap.SVG, hash, scrap.JS, w).Run(func(args mock.Arguments) {
_, err := w.Write([]byte(code))
assert.NoError(t, err)
}).Return(nil)
makeRequest(scrapExchange, w, r)
require.Equal(t, http.StatusOK, w.Code)
require.Equal(t, "text/plain", w.Header().Get("Content-Type"))
require.Equal(t, code, w.Body.String())
require.Equal(t, int64(1), callMetric.Get())
}
func TestTemplateGetHandler_InvalidLang_ReturnsBadRequest(t *testing.T) {
w := httptest.NewRecorder()
r := httptest.NewRequest("GET", fmt.Sprintf("/_/tmpl/svg/%s/unknownlanguage", hash), nil)
scrapExchange := &mocks.ScrapExchange{}
makeRequest(scrapExchange, w, r)
require.Equal(t, http.StatusBadRequest, w.Code)
require.Equal(t, "text/plain; charset=utf-8", w.Header().Get("Content-Type"))
require.Equal(t, "Unknown language.\n", w.Body.String())
}
func TestNamePutHandler_HappyPath_Success(t *testing.T) {
callMetric := metrics2.GetCounter(namesPutCallMetric)
callMetric.Reset()
w := httptest.NewRecorder()
r := httptest.NewRequest("PUT", fmt.Sprintf("/_/names/svg/%s", scrapName), validScrapName(t))
scrapExchange := &mocks.ScrapExchange{}
scrapExchange.On("PutName", testutils.AnyContext, scrap.SVG, scrapName, mock.AnythingOfType("scrap.Name")).Return(nil)
makeRequest(scrapExchange, w, r)
require.Equal(t, http.StatusOK, w.Code)
require.Equal(t, int64(1), callMetric.Get())
}
func TestNamePutHandler_PutFails_ReturnsInternalServerError(t *testing.T) {
w := httptest.NewRecorder()
r := httptest.NewRequest("PUT", fmt.Sprintf("/_/names/svg/%s", scrapName), validScrapName(t))
scrapExchange := &mocks.ScrapExchange{}
scrapExchange.On("PutName", testutils.AnyContext, scrap.SVG, scrapName, mock.AnythingOfType("scrap.Name")).Return(errMyMockError)
makeRequest(scrapExchange, w, r)
require.Equal(t, http.StatusInternalServerError, w.Code)
require.Equal(t, "text/plain; charset=utf-8", w.Header().Get("Content-Type"))
require.Equal(t, "Failed to write name.\n", w.Body.String())
}
func TestNameGetHandler_HappyPath_Success(t *testing.T) {
callMetric := metrics2.GetCounter(namesGetCallMetric)
callMetric.Reset()
w := httptest.NewRecorder()
r := httptest.NewRequest("GET", fmt.Sprintf("/_/names/svg/%s", scrapName), nil)
scrapExchange := &mocks.ScrapExchange{}
scrapBody := scrap.Name{
Hash: hash,
Description: "A description of the scrap.",
}
scrapExchange.On("GetName", testutils.AnyContext, scrap.SVG, scrapName).Return(scrapBody, nil)
makeRequest(scrapExchange, w, r)
require.Equal(t, http.StatusOK, w.Code)
require.Equal(t, fmt.Sprintf("{\"Hash\":\"%s\",\"Description\":\"A description of the scrap.\"}\n", hash), w.Body.String())
require.Equal(t, "application/json", w.Header().Get("Content-Type"))
require.Equal(t, int64(1), callMetric.Get())
}
func TestNameGetHandler_GetFails_ReturnsInternalServerError(t *testing.T) {
w := httptest.NewRecorder()
r := httptest.NewRequest("GET", fmt.Sprintf("/_/names/svg/%s", scrapName), nil)
scrapExchange := &mocks.ScrapExchange{}
scrapExchange.On("GetName", testutils.AnyContext, scrap.SVG, scrapName).Return(scrap.Name{}, errMyMockError)
makeRequest(scrapExchange, w, r)
require.Equal(t, http.StatusInternalServerError, w.Code)
require.Equal(t, "text/plain; charset=utf-8", w.Header().Get("Content-Type"))
require.Equal(t, "Failed to retrieve Name.\n", w.Body.String())
}
func TestNameDeleteHandler_HappyPath_Success(t *testing.T) {
callMetric := metrics2.GetCounter(namesDeleteCallMetric)
callMetric.Reset()
w := httptest.NewRecorder()
r := httptest.NewRequest("DELETE", fmt.Sprintf("/_/names/svg/%s", scrapName), nil)
scrapExchange := &mocks.ScrapExchange{}
scrapExchange.On("DeleteName", testutils.AnyContext, scrap.SVG, scrapName).Return(nil)
makeRequest(scrapExchange, w, r)
require.Equal(t, http.StatusOK, w.Code)
require.Equal(t, int64(1), callMetric.Get())
}
func TestNameDeleteHandler_DeleteNameFails_ReturnsInternalServerError(t *testing.T) {
w := httptest.NewRecorder()
r := httptest.NewRequest("DELETE", fmt.Sprintf("/_/names/svg/%s", scrapName), nil)
scrapExchange := &mocks.ScrapExchange{}
scrapExchange.On("DeleteName", testutils.AnyContext, scrap.SVG, scrapName).Return(errMyMockError)
makeRequest(scrapExchange, w, r)
require.Equal(t, http.StatusInternalServerError, w.Code)
require.Equal(t, "text/plain; charset=utf-8", w.Header().Get("Content-Type"))
require.Equal(t, "Failed to delete Name.\n", w.Body.String())
}
func TestNameListHandler_HappyPath_Success(t *testing.T) {
callMetric := metrics2.GetCounter(namesListCallMetric)
callMetric.Reset()
w := httptest.NewRecorder()
r := httptest.NewRequest("GET", "/_/names/svg/", nil)
scrapExchange := &mocks.ScrapExchange{}
body := []string{
scrapName,
"AnotherScrap",
}
scrapExchange.On("ListNames", testutils.AnyContext, scrap.SVG).Return(body, nil)
makeRequest(scrapExchange, w, r)
require.Equal(t, http.StatusOK, w.Code)
require.Equal(t, "application/json", w.Header().Get("Content-Type"))
expected, err := json.Marshal(body)
require.NoError(t, err)
require.Equal(t, string(expected)+"\n", w.Body.String())
require.Equal(t, int64(1), callMetric.Get())
}
func TestNameListHandler_ListFails_ReturnsInternalServerError(t *testing.T) {
w := httptest.NewRecorder()
r := httptest.NewRequest("GET", "/_/names/svg/", nil)
scrapExchange := &mocks.ScrapExchange{}
scrapExchange.On("ListNames", testutils.AnyContext, scrap.SVG).Return(nil, errMyMockError)
makeRequest(scrapExchange, w, r)
require.Equal(t, http.StatusInternalServerError, w.Code)
require.Equal(t, "text/plain; charset=utf-8", w.Header().Get("Content-Type"))
require.Equal(t, "Failed to load Names.\n", w.Body.String())
}