2023-11-28 09:32:26 +01:00
|
|
|
// 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"
|
|
|
|
"slices"
|
|
|
|
"strings"
|
|
|
|
"testing"
|
|
|
|
|
|
|
|
"code.gitea.io/gitea/modules/log"
|
|
|
|
|
|
|
|
"github.com/stretchr/testify/assert"
|
2024-07-30 21:41:10 +02:00
|
|
|
"github.com/stretchr/testify/require"
|
2023-11-28 09:32:26 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
// 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 := ""
|
|
|
|
ignoredHeaders := []string{"cf-ray", "server", "date", "report-to", "nel", "x-request-id"}
|
|
|
|
|
|
|
|
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?)
|
2024-03-15 13:15:46 +01:00
|
|
|
fixturePath := fmt.Sprintf("%s/%s_%s", testDataDir, r.Method, url.PathEscape(path))
|
2023-11-28 09:32:26 +01:00
|
|
|
if liveMode {
|
|
|
|
liveURL := fmt.Sprintf("%s%s", liveServerBaseURL, path)
|
|
|
|
|
|
|
|
request, err := http.NewRequest(r.Method, liveURL, nil)
|
2024-07-30 21:41:10 +02:00
|
|
|
require.NoError(t, err, "constructing an HTTP request to %s failed", liveURL)
|
2023-11-28 09:32:26 +01:00
|
|
|
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)
|
2024-07-30 21:41:10 +02:00
|
|
|
require.NoError(t, err, "HTTP request to %s failed: %s", liveURL)
|
2024-03-15 13:15:46 +01:00
|
|
|
assert.Less(t, response.StatusCode, 400, "unexpected status code for %s", liveURL)
|
2023-11-28 09:32:26 +01:00
|
|
|
|
|
|
|
fixture, err := os.Create(fixturePath)
|
2024-07-30 21:41:10 +02:00
|
|
|
require.NoError(t, err, "failed to open the fixture file %s for writing", fixturePath)
|
2023-11-28 09:32:26 +01:00
|
|
|
defer fixture.Close()
|
|
|
|
fixtureWriter := bufio.NewWriter(fixture)
|
|
|
|
|
|
|
|
for headerName, headerValues := range response.Header {
|
|
|
|
for _, headerValue := range headerValues {
|
|
|
|
if !slices.Contains(ignoredHeaders, strings.ToLower(headerName)) {
|
|
|
|
_, err := fixtureWriter.WriteString(fmt.Sprintf("%s: %s\n", headerName, headerValue))
|
2024-07-30 21:41:10 +02:00
|
|
|
require.NoError(t, err, "writing the header of the HTTP response to the fixture file failed")
|
2023-11-28 09:32:26 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
_, err = fixtureWriter.WriteString("\n")
|
2024-07-30 21:41:10 +02:00
|
|
|
require.NoError(t, err, "writing the header of the HTTP response to the fixture file failed")
|
2023-11-28 09:32:26 +01:00
|
|
|
fixtureWriter.Flush()
|
|
|
|
|
|
|
|
log.Info("Mock HTTP Server: writing response to %s", fixturePath)
|
|
|
|
_, err = io.Copy(fixture, response.Body)
|
2024-07-30 21:41:10 +02:00
|
|
|
require.NoError(t, err, "writing the body of the HTTP response to %s failed", liveURL)
|
2023-11-28 09:32:26 +01:00
|
|
|
|
|
|
|
err = fixture.Sync()
|
2024-07-30 21:41:10 +02:00
|
|
|
require.NoError(t, err, "writing the body of the HTTP response to the fixture file failed")
|
2023-11-28 09:32:26 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
fixture, err := os.ReadFile(fixturePath)
|
2024-07-30 21:41:10 +02:00
|
|
|
require.NoError(t, err, "missing mock HTTP response: "+fixturePath)
|
2023-11-28 09:32:26 +01:00
|
|
|
|
|
|
|
w.WriteHeader(http.StatusOK)
|
|
|
|
|
|
|
|
// replace any mention of the live HTTP service by the mocked host
|
|
|
|
stringFixture := strings.ReplaceAll(string(fixture), liveServerBaseURL, mockServerBaseURL)
|
|
|
|
// parse back the fixture file into a series of HTTP headers followed by response body
|
|
|
|
lines := strings.Split(stringFixture, "\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")
|
|
|
|
_, err := w.Write([]byte(responseBody))
|
2024-07-30 21:41:10 +02:00
|
|
|
require.NoError(t, err, "writing the body of the HTTP response failed")
|
2023-11-28 09:32:26 +01:00
|
|
|
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)
|
|
|
|
}
|