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