191 lines
8.2 KiB
Go
191 lines
8.2 KiB
Go
package runtime
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.sendico.io/tech/sxchnge/product/app/api"
|
|
"git.sendico.io/tech/sxchnge/product/sessions/browser"
|
|
"git.sendico.io/tech/sxchnge/shared/database"
|
|
merrors "git.sendico.io/tech/sxchnge/shared/errors"
|
|
"git.sendico.io/tech/sxchnge/shared/logging"
|
|
"github.com/go-chi/chi/v5"
|
|
"github.com/stretchr/testify/require"
|
|
"go.uber.org/zap"
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
func testConfig(t *testing.T) *api.Config {
|
|
t.Helper()
|
|
data, err := os.ReadFile("../../config.example.yml")
|
|
require.NoError(t, err)
|
|
var raw struct {
|
|
API api.Config `yaml:"api"`
|
|
}
|
|
require.NoError(t, yaml.Unmarshal(data, &raw))
|
|
t.Setenv(raw.API.Mw.EndPointEnv, "/api/v1")
|
|
t.Setenv(raw.API.Mw.DomainEnv, "example.test")
|
|
t.Setenv(raw.API.Mw.APIProtocolEnv, "https")
|
|
return &raw.API
|
|
}
|
|
|
|
func TestInstalledVersionsShareDependenciesAndKeepScopes(t *testing.T) {
|
|
config := testConfig(t)
|
|
config.Versions = []api.Mount{{Version: "v1", Path: "/api/v1"}, {Version: "v2", Path: "/gateway/v2"}}
|
|
initialized, closed := 0, 0
|
|
deps := &api.Dependencies{}
|
|
installer := func(version string) api.Installer {
|
|
return func(scope api.Scope, got *api.Dependencies) error {
|
|
require.Same(t, deps, got)
|
|
link, err := scope.URLs.GetAPILink("accounts")
|
|
require.NoError(t, err)
|
|
require.Equal(t, "https://example.test"+scope.Mount+"/accounts", link)
|
|
scope.Router.Get("/contract", func(w http.ResponseWriter, _ *http.Request) { _, _ = w.Write([]byte(version)) })
|
|
scope.Router.Post("/accounts/session", func(w http.ResponseWriter, r *http.Request) {
|
|
browser.SetRefreshCookie(w, r, "credential", time.Now().Add(time.Hour))
|
|
})
|
|
scope.Router.Delete("/accounts/session", browser.ClearRefreshCookie)
|
|
return nil
|
|
}
|
|
}
|
|
runtime := api.RuntimeConfig{Versions: map[string]api.Installer{"v1": installer("old"), "v2": installer("new"), "v3": installer("disabled")}, Bootstrap: func(context.Context, mlogger.Logger, *api.Config, db.Factory) (*api.Dependencies, func(context.Context) error, error) {
|
|
initialized++
|
|
return deps, func(context.Context) error { closed++; return nil }, nil
|
|
}}
|
|
root, routes := chi.NewRouter(), chi.NewRouter()
|
|
srv, err := CreateAPI(context.Background(), zap.NewNop(), config, nil, root, routes, false, runtime)
|
|
require.NoError(t, err)
|
|
root.Mount("/", routes)
|
|
for target, expected := range map[string]string{"/api/v1/contract": "old", "/gateway/v2/contract": "new"} {
|
|
result := httptest.NewRecorder()
|
|
root.ServeHTTP(result, httptest.NewRequest(http.MethodGet, target, nil))
|
|
require.Equal(t, http.StatusOK, result.Code)
|
|
require.Equal(t, expected, result.Body.String())
|
|
}
|
|
t.Setenv("API_ENDPOINT", "/wrong/global")
|
|
for _, mount := range config.Versions {
|
|
for _, method := range []string{http.MethodPost, http.MethodDelete} {
|
|
result := httptest.NewRecorder()
|
|
request := httptest.NewRequest(method, mount.Path+"/accounts/session", nil)
|
|
request.Header.Set(browser.TransportHeader, browser.TransportCookie)
|
|
request.Header.Set(browser.CSRFHeader, browser.CSRFValue)
|
|
request.Header.Set("Origin", config.Mw.CORS.AllowedOrigins[0])
|
|
root.ServeHTTP(result, request)
|
|
require.Equal(t, http.StatusOK, result.Code)
|
|
cookies := result.Result().Cookies()
|
|
require.Len(t, cookies, 1)
|
|
require.Equal(t, mount.Path+"/accounts", cookies[0].Path)
|
|
if method == http.MethodDelete {
|
|
require.Equal(t, -1, cookies[0].MaxAge)
|
|
}
|
|
}
|
|
}
|
|
result := httptest.NewRecorder()
|
|
root.ServeHTTP(result, httptest.NewRequest(http.MethodGet, "/api/v3/contract", nil))
|
|
require.Equal(t, 404, result.Code)
|
|
require.Equal(t, 1, initialized)
|
|
var wg sync.WaitGroup
|
|
results := make(chan error, 3)
|
|
for range 3 {
|
|
wg.Go(func() { results <- srv.Finish(context.Background()) })
|
|
}
|
|
wg.Wait()
|
|
close(results)
|
|
for err := range results {
|
|
require.NoError(t, err)
|
|
}
|
|
require.Equal(t, 1, closed)
|
|
}
|
|
|
|
func TestInvalidMountsFailBeforeBootstrap(t *testing.T) {
|
|
for name, mounts := range map[string][]api.Mount{
|
|
"unknown": {{Version: "v9", Path: "/api/v9"}}, "duplicate": {{Version: "v1", Path: "/api/v1"}, {Version: "v1", Path: "/other"}},
|
|
"same path": {{Version: "v1", Path: "/api/v1"}, {Version: "v2", Path: "/api/v1"}},
|
|
"nested": {{Version: "v1", Path: "/api"}, {Version: "v2", Path: "/api/v2"}},
|
|
"cookie delimiter": {{Version: "v1", Path: "/api;v1"}},
|
|
"control": {{Version: "v1", Path: "/api/\x01"}},
|
|
"relative": {{Version: "v1", Path: "api/v1"}}, "wildcard": {{Version: "v1", Path: "/api/*"}},
|
|
"unclean": {{Version: "v1", Path: "/api/../v1"}}, "metrics": {{Version: "v1", Path: "/metrics"}},
|
|
"health": {{Version: "v1", Path: "/api/v1/health"}}, "root": {{Version: "v1", Path: "/"}},
|
|
} {
|
|
t.Run(name, func(t *testing.T) {
|
|
config := testConfig(t)
|
|
config.Versions = mounts
|
|
install := func(api.Scope, *api.Dependencies) error { t.Fatal("installer must not run"); return nil }
|
|
_, err := CreateAPI(context.Background(), zap.NewNop(), config, nil, chi.NewRouter(), chi.NewRouter(), false, api.RuntimeConfig{Versions: map[string]api.Installer{"v1": install, "v2": install}, Bootstrap: func(context.Context, mlogger.Logger, *api.Config, db.Factory) (*api.Dependencies, func(context.Context) error, error) {
|
|
t.Fatal("bootstrap must not run")
|
|
return nil, nil, nil
|
|
}})
|
|
require.ErrorIs(t, err, merrors.ErrInvalidArg)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestInstallFailureCleansResourcesWithoutPublishingRoutes(t *testing.T) {
|
|
config := testConfig(t)
|
|
config.Versions = []api.Mount{{Version: "v1", Path: "/api/v1"}, {Version: "v2", Path: "/api/v2"}}
|
|
closed := 0
|
|
cause := merrors.InvalidArgument("test installation failure")
|
|
routes := chi.NewRouter()
|
|
_, err := CreateAPI(context.Background(), zap.NewNop(), config, nil, chi.NewRouter(), routes, false, api.RuntimeConfig{
|
|
Bootstrap: func(context.Context, mlogger.Logger, *api.Config, db.Factory) (*api.Dependencies, func(context.Context) error, error) {
|
|
return &api.Dependencies{}, func(context.Context) error { closed++; return nil }, nil
|
|
},
|
|
Versions: map[string]api.Installer{
|
|
"v1": func(scope api.Scope, _ *api.Dependencies) error {
|
|
scope.Router.Get("/ready", func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(200) })
|
|
return nil
|
|
},
|
|
"v2": func(api.Scope, *api.Dependencies) error { return cause },
|
|
}})
|
|
require.True(t, errors.Is(err, cause))
|
|
require.Equal(t, 1, closed)
|
|
result := httptest.NewRecorder()
|
|
routes.ServeHTTP(result, httptest.NewRequest(http.MethodGet, "/api/v1/ready", nil))
|
|
require.Equal(t, 404, result.Code)
|
|
}
|
|
|
|
func TestExplicitEmptyVersionsInstallNoAPI(t *testing.T) {
|
|
config := testConfig(t)
|
|
config.Versions = []api.Mount{}
|
|
root, routes := chi.NewRouter(), chi.NewRouter()
|
|
srv, err := CreateAPI(context.Background(), zap.NewNop(), config, nil, root, routes, false, api.RuntimeConfig{Bootstrap: func(context.Context, mlogger.Logger, *api.Config, db.Factory) (*api.Dependencies, func(context.Context) error, error) {
|
|
return &api.Dependencies{}, nil, nil
|
|
}})
|
|
require.NoError(t, err)
|
|
defer srv.Finish(context.Background())
|
|
root.Mount("/", routes)
|
|
for target, status := range map[string]int{"/api/v1/accounts/login": 404, "/api/v1/health": 200, "/metrics": 200, "/api/v1/metrics": 200} {
|
|
result := httptest.NewRecorder()
|
|
root.ServeHTTP(result, httptest.NewRequest(http.MethodGet, target, nil))
|
|
require.Equal(t, status, result.Code, target)
|
|
}
|
|
}
|
|
|
|
func TestBootstrapFailureClosesReturnedResources(t *testing.T) {
|
|
config := testConfig(t)
|
|
config.Versions = []api.Mount{}
|
|
closed := 0
|
|
cause := merrors.Internal("bootstrap failure")
|
|
_, err := CreateAPI(context.Background(), zap.NewNop(), config, nil, chi.NewRouter(), chi.NewRouter(), false, api.RuntimeConfig{Bootstrap: func(context.Context, mlogger.Logger, *api.Config, db.Factory) (*api.Dependencies, func(context.Context) error, error) {
|
|
return nil, func(context.Context) error { closed++; return nil }, cause
|
|
}})
|
|
require.ErrorIs(t, err, cause)
|
|
require.Equal(t, 1, closed)
|
|
}
|
|
|
|
func TestFinishPreservesCleanupErrorAndRunsOnce(t *testing.T) {
|
|
cause := merrors.Internal("cleanup failure")
|
|
closed := 0
|
|
srv := &instance{logger: zap.NewNop(), cleanup: func(context.Context) error { closed++; return cause }}
|
|
require.ErrorIs(t, srv.Finish(context.Background()), cause)
|
|
require.ErrorIs(t, srv.Finish(context.Background()), cause)
|
|
require.Equal(t, 1, closed)
|
|
}
|