forgejo/models/unittest/mock_http.go
Antonin Delpeuch 0afc181d20 [GITEA] Introduce HTTP mocking utility for unit tests (#1858)
Closes #1837.

The differences in dates can be explained by commit e19b9653ea, which
changed the order in which "created_date" and "updated_date" are
considered.
2023-12-01 19:17:46 +00:00

132 lines
4.3 KiB
Go

// Copyright 2017 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package unittest
import (
"bufio"
"fmt"
"io"
"net/http"
"net/http/httptest"
"net/url"
"os"
"strings"
"testing"
"code.gitea.io/gitea/modules/log"
"github.com/stretchr/testify/assert"
)
// Mocks HTTP responses of a third-party service (such as GitHub, GitLab…)
// This has two modes:
// - live mode: the requests made to the mock HTTP server are transmitted to the live
// service, and responses are saved as test data files
// - test mode: the responses to requests to the mock HTTP server are read from the
// test data files
func NewMockWebServer(t *testing.T, liveServerBaseURL, testDataDir string, liveMode bool) *httptest.Server {
mockServerBaseURL := ""
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
path := NormalizedFullPath(r.URL)
log.Info("Mock HTTP Server: got request for path %s", r.URL.Path)
// TODO check request method (support POST?)
fixturePath := fmt.Sprintf("%s/%s", testDataDir, strings.ReplaceAll(path, "/", "_"))
if liveMode {
liveURL := fmt.Sprintf("%s%s", liveServerBaseURL, path)
request, err := http.NewRequest(r.Method, liveURL, nil)
if err != nil {
assert.Fail(t, "constructing an HTTP request to %s failed", liveURL, err)
}
for headerName, headerValues := range r.Header {
// do not pass on the encoding: let the Transport of the HTTP client handle that for us
if strings.ToLower(headerName) != "accept-encoding" {
for _, headerValue := range headerValues {
request.Header.Add(headerName, headerValue)
}
}
}
response, err := http.DefaultClient.Do(request)
if err != nil {
assert.Fail(t, "HTTP request to %s failed: %s", liveURL, err)
}
fixture, err := os.Create(fixturePath)
if err != nil {
assert.Fail(t, fmt.Sprintf("failed to open the fixture file %s for writing", fixturePath), err)
}
defer fixture.Close()
fixtureWriter := bufio.NewWriter(fixture)
for headerName, headerValues := range response.Header {
for _, headerValue := range headerValues {
if strings.ToLower(headerName) != "host" {
_, err := fixtureWriter.WriteString(fmt.Sprintf("%s: %s\n", headerName, headerValue))
if err != nil {
assert.Fail(t, "writing the header of the HTTP response to the fixture file failed", err)
}
}
}
}
if _, err := fixtureWriter.WriteString("\n"); err != nil {
assert.Fail(t, "writing the header of the HTTP response to the fixture file failed")
}
fixtureWriter.Flush()
reader := response.Body
content, err := io.ReadAll(reader)
if err != nil {
assert.Fail(t, "reading the response of the HTTP request to %s failed: %s", liveURL, err)
}
log.Info("Mock HTTP Server: writing response to %s", fixturePath)
if _, err := fixture.Write(content); err != nil {
assert.Fail(t, "writing the body of the HTTP response to the fixture file failed", err)
}
if err := fixture.Sync(); err != nil {
assert.Fail(t, "writing the body of the HTTP response to the fixture file failed", err)
}
}
fixture, err := os.ReadFile(fixturePath)
if err != nil {
assert.Fail(t, "missing mock HTTP response: "+fixturePath)
return
}
w.WriteHeader(http.StatusOK)
// parse back the fixture file into a series of HTTP headers followed by response body
lines := strings.Split(string(fixture), "\n")
for idx, line := range lines {
colonIndex := strings.Index(line, ": ")
if colonIndex != -1 {
w.Header().Set(line[0:colonIndex], line[colonIndex+2:])
} else {
// we reached the end of the headers (empty line), so what follows is the body
responseBody := strings.Join(lines[idx+1:], "\n")
// replace any mention of the live HTTP service by the mocked host
responseBody = strings.ReplaceAll(responseBody, liveServerBaseURL, mockServerBaseURL)
if _, err := w.Write([]byte(responseBody)); err != nil {
assert.Fail(t, "writing the body of the HTTP response failed", err)
}
break
}
}
}))
mockServerBaseURL = server.URL
return server
}
func NormalizedFullPath(url *url.URL) string {
// TODO normalize path (remove trailing slash?)
// TODO normalize RawQuery (order query parameters?)
if len(url.Query()) == 0 {
return url.EscapedPath()
}
return fmt.Sprintf("%s?%s", url.EscapedPath(), url.RawQuery)
}