def configure( self, template: str = "github", *, oidc_clients: Optional[List[OIDCClient]] = None, **settings: str, ) -> None: """Change the test application configuration. This cannot be used to change the database URL because the internal session is not recreated. Parameters ---------- template : `str` Settings template to use. oidc_clients : List[`gafaelfawr.config.OIDCClient`] or `None` Configuration information for clients of the OpenID Connect server. **settings : str Any additional settings to add to the settings file. """ settings_path = build_settings( self.tmp_path, template, oidc_clients, **settings, ) config_dependency.set_settings_path(str(settings_path)) self.config = config_dependency()
def initialize(tmp_path: Path) -> Config: """Do basic initialization and return a configuration. This shared logic can be used either with `SetupTest`, which assumes an ASGI application and an async test, or with non-async tests such as the tests of the command-line interface. Parameters ---------- tmp_path : `pathlib.Path` The path for temporary files. Returns ------- config : `gafaelfawr.config.Config` The generated config, using the same defaults as `SetupTest`. """ settings_path = build_settings(tmp_path, "github") config_dependency.set_settings_path(str(settings_path)) config = config_dependency() if not os.environ.get("REDIS_6379_TCP_PORT"): redis_dependency.is_mocked = True # Initialize the database. Non-SQLite databases need to be reset between # tests. should_reset = not urlparse(config.database_url).scheme == "sqlite" initialize_database(config, reset=should_reset) return config
def test_database_password(tmp_path: Path) -> None: settings_path = build_settings( tmp_path, "github", database_url="postgresql://gafaelfawr@localhost/gafaelfawr", ) os.environ["GAFAELFAWR_DATABASE_PASSWORD"] = "******" config_dependency.set_settings_path(str(settings_path)) config = config_dependency() del os.environ["GAFAELFAWR_DATABASE_PASSWORD"] expected = "postgresql://*****:*****@localhost/gafaelfawr" assert config.database_url == expected
async def startup_event() -> None: config = config_dependency() engine_args = {} if urlparse(config.database_url).scheme == "sqlite": engine_args["connect_args"] = {"check_same_thread": False} app.add_middleware( DBSessionMiddleware, db_url=config.database_url, engine_args=engine_args, ) app.add_middleware(XForwardedMiddleware, proxies=config.proxies) app.add_middleware(StateMiddleware, cookie_name=COOKIE_NAME, state_class=State)
async def test_redis_password(tmp_path: Path) -> None: redis_password_file = store_secret(tmp_path, "redis", b"some-password") settings_path = build_settings( tmp_path, "github", redis_password_file=str(redis_password_file)) config_dependency.set_settings_path(str(settings_path)) function = "gafaelfawr.dependencies.redis.create_redis_pool" with patch(function) as mock_create: redis_dependency.is_mocked = False await redis_dependency(config_dependency()) assert mock_create.call_args_list == [ call("redis://localhost:6379/0", password="******") ] redis_dependency.redis = None
async def update_service_tokens() -> None: """Update service tokens stored in Kubernetes secrets.""" config = config_dependency() logger = structlog.get_logger(config.safir.logger_name) if not config.kubernetes: logger.info("No Kubernetes secrets configured") sys.exit(0) async with ComponentFactory.standalone() as factory: kubernetes_service = factory.create_kubernetes_service() try: await kubernetes_service.update_service_secrets() except KubernetesError as e: msg = "Failed to update service token secrets" logger.error(msg, error=str(e)) sys.exit(1)
def get_logger(request: Request) -> BoundLogger: """Return a logger bound to a request. This is a convenience function that can be used where a dependency isn't available, such as in middleware. Parameters ---------- request : `fastapi.Request` The request to which to bind the logger. Returns ------- logger : `structlog.BoundLogger` The bound logger. """ config = config_dependency() return logger_dependency(request, config)
def run_app(tmp_path: Path, settings_path: Path) -> Iterator[SeleniumConfig]: """Run the application as a separate process for Selenium access. Parameters ---------- tmp_path : `pathlib.Path` The temporary directory for testing. settings_path : `pathlib.Path` The path to the settings file. """ config_dependency.set_settings_path(str(settings_path)) config = config_dependency() initialize_database(config) token_path = tmp_path / "token" app_source = APP_TEMPLATE.format( settings_path=str(settings_path), token_path=str(token_path), ) app_path = tmp_path / "testing.py" with app_path.open("w") as f: f.write(app_source) s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.bind(("127.0.0.1", 0)) port = s.getsockname()[1] cmd = ["uvicorn", "--fd", "0", "testing:app"] logging.info("Starting server with command %s", " ".join(cmd)) p = subprocess.Popen(cmd, cwd=str(tmp_path), stdin=s.fileno()) s.close() logging.info("Waiting for server to start") _wait_for_server(port) try: selenium_config = SeleniumConfig( token=Token.from_str(token_path.read_text()), url=f"http://localhost:{port}", ) yield selenium_config finally: p.terminate()
def as_cookie(self) -> str: """Build an encrypted cookie representation of the state. Returns ------- cookie : `str` The encrypted cookie value. """ data = {} if self.csrf: data["csrf"] = self.csrf if self.token: data["token"] = str(self.token) if self.return_url: data["return_url"] = self.return_url if self.state: data["state"] = self.state key = config_dependency().session_secret.encode() fernet = Fernet(key) return fernet.encrypt(json.dumps(data).encode()).decode()
def from_cookie(cls, cookie: str, request: Optional[Request]) -> State: """Reconstruct state from an encrypted cookie. Parameters ---------- cookie : `str` The encrypted cookie value. key : `bytes` The `~cryptography.fernet.Fernet` key used to decrypt it. request : `fastapi.Request` or `None` The request, used for logging. If not provided (primarily for the test suite), invalid state cookies will not be logged. Returns ------- state : `State` The state represented by the cookie. """ key = config_dependency().session_secret.encode() fernet = Fernet(key) try: data = json.loads(fernet.decrypt(cookie.encode()).decode()) token = None if "token" in data: token = Token.from_str(data["token"]) except Exception as e: if request: logger = get_logger(request) logger.warning("Discarding invalid state cookie", error=str(e)) return cls() return cls( csrf=data.get("csrf"), token=token, return_url=data.get("return_url"), state=data.get("state"), )
async def standalone(cls) -> AsyncIterator[ComponentFactory]: """Build Gafaelfawr components outside of a request. Intended for background jobs. Uses the non-request default values for the dependencies of `ComponentFactory`. Do not use this factory inside the web application or anywhere that may use the default `ComponentFactory`, since they will interfere with each other's Redis pools. Notes ----- This creates a database session directly because fastapi_sqlalchemy does not work unless an ASGI application has initialized it. Yields ------ factory : `ComponentFactory` The factory. Must be used as a context manager. """ config = config_dependency() redis = await redis_dependency(config) logger = structlog.get_logger(config.safir.logger_name) assert logger session = create_session(config, logger) try: async with AsyncClient() as client: yield cls( config=config, redis=redis, session=session, http_client=client, logger=logger, ) finally: await redis_dependency.close() session.close()
def init(settings: str) -> None: """Initialize the database storage.""" config_dependency.set_settings_path(settings) config = config_dependency() initialize_database(config)