From 8aeb8036ffbf501fa14720418111612c2542aeb8 Mon Sep 17 00:00:00 2001 From: thatmattlove Date: Wed, 8 Dec 2021 15:24:07 -0700 Subject: [PATCH] Closes #175: Remove usage of `hyperglass.env.json` --- hyperglass/ui/.eslintignore | 4 + hyperglass/ui/.gitignore | 1 + hyperglass/ui/.prettierignore | 3 +- hyperglass/ui/components/favicon.tsx | 11 ++ hyperglass/ui/components/index.ts | 1 + hyperglass/ui/components/meta.tsx | 24 +--- hyperglass/ui/favicon-formats.ts | 1 + hyperglass/ui/next.config.js | 3 - hyperglass/ui/nextdev.js | 30 ++--- hyperglass/ui/pages/_app.tsx | 22 +--- hyperglass/ui/pages/_document.tsx | 17 ++- hyperglass/ui/pages/index.tsx | 25 +---- hyperglass/ui/types/config.ts | 8 -- hyperglass/ui/util/config.ts | 13 +-- hyperglass/util/files.py | 45 ++++++-- hyperglass/util/frontend.py | 161 ++++++++++++++------------- hyperglass/util/tests/test_files.py | 50 +++++++++ 17 files changed, 224 insertions(+), 195 deletions(-) create mode 100644 hyperglass/ui/.eslintignore create mode 100644 hyperglass/ui/components/favicon.tsx create mode 100644 hyperglass/ui/favicon-formats.ts create mode 100644 hyperglass/util/tests/test_files.py diff --git a/hyperglass/ui/.eslintignore b/hyperglass/ui/.eslintignore new file mode 100644 index 0000000..4c7afc3 --- /dev/null +++ b/hyperglass/ui/.eslintignore @@ -0,0 +1,4 @@ +node_modules +dist +.next/ +favicon-formats.ts diff --git a/hyperglass/ui/.gitignore b/hyperglass/ui/.gitignore index 167c7e7..4a9306a 100644 --- a/hyperglass/ui/.gitignore +++ b/hyperglass/ui/.gitignore @@ -1,4 +1,5 @@ .DS_Store +.env* *.tsbuildinfo # dev/test files TODO.txt diff --git a/hyperglass/ui/.prettierignore b/hyperglass/ui/.prettierignore index b1e1b18..6b48447 100644 --- a/hyperglass/ui/.prettierignore +++ b/hyperglass/ui/.prettierignore @@ -5,4 +5,5 @@ yarn.lock package-lock.json .eslintrc tsconfig.json -.next/ \ No newline at end of file +.next/ +favicon-formats.ts diff --git a/hyperglass/ui/components/favicon.tsx b/hyperglass/ui/components/favicon.tsx new file mode 100644 index 0000000..c743934 --- /dev/null +++ b/hyperglass/ui/components/favicon.tsx @@ -0,0 +1,11 @@ +import type { Favicon as FaviconProps } from '~/types'; + +/** + * Render a `` element to reference a server-side favicon. + */ +export const Favicon = (props: FaviconProps): JSX.Element => { + const { image_format, dimensions, prefix, rel } = props; + const [w, h] = dimensions; + const src = `/images/favicons/${prefix}-${w}x${h}.${image_format}`; + return ; +}; diff --git a/hyperglass/ui/components/index.ts b/hyperglass/ui/components/index.ts index 4ae1917..ee7d018 100644 --- a/hyperglass/ui/components/index.ts +++ b/hyperglass/ui/components/index.ts @@ -2,6 +2,7 @@ export * from './card'; export * from './codeBlock'; export * from './countdown'; export * from './debugger'; +export * from './favicon'; export * from './footer'; export * from './form'; export * from './greeting'; diff --git a/hyperglass/ui/components/meta.tsx b/hyperglass/ui/components/meta.tsx index 844f6b1..6f9c61e 100644 --- a/hyperglass/ui/components/meta.tsx +++ b/hyperglass/ui/components/meta.tsx @@ -4,7 +4,7 @@ import { useTheme } from '@chakra-ui/react'; import { useConfig } from '~/context'; import { googleFontUrl } from '~/util'; -export const Meta: React.FC = () => { +export const Meta = (): JSX.Element => { const config = useConfig(); const { fonts } = useTheme(); const [location, setLocation] = useState('/'); @@ -12,22 +12,6 @@ export const Meta: React.FC = () => { const { siteTitle: title = 'hyperglass', siteDescription: description = 'Network Looking Glass', - siteKeywords: keywords = [ - 'hyperglass', - 'looking glass', - 'lg', - 'peer', - 'peering', - 'ipv4', - 'ipv6', - 'transit', - 'community', - 'communities', - 'bgp', - 'routing', - 'network', - 'isp', - ], } = useConfig(); const siteName = `${title} - ${description}`; @@ -42,8 +26,7 @@ export const Meta: React.FC = () => { return ( - {title} - + {title} @@ -52,8 +35,7 @@ export const Meta: React.FC = () => { - - + ); }; diff --git a/hyperglass/ui/favicon-formats.ts b/hyperglass/ui/favicon-formats.ts new file mode 100644 index 0000000..048d683 --- /dev/null +++ b/hyperglass/ui/favicon-formats.ts @@ -0,0 +1 @@ +import type { Favicon } from '~/types';export default [{"dimensions": [64, 64], "image_format": "ico", "prefix": "favicon", "rel": null}, {"dimensions": [16, 16], "image_format": "png", "prefix": "favicon", "rel": "icon"}, {"dimensions": [32, 32], "image_format": "png", "prefix": "favicon", "rel": "icon"}, {"dimensions": [64, 64], "image_format": "png", "prefix": "favicon", "rel": "icon"}, {"dimensions": [96, 96], "image_format": "png", "prefix": "favicon", "rel": "icon"}, {"dimensions": [180, 180], "image_format": "png", "prefix": "favicon", "rel": "icon"}, {"dimensions": [57, 57], "image_format": "png", "prefix": "apple-touch-icon", "rel": "apple-touch-icon"}, {"dimensions": [60, 60], "image_format": "png", "prefix": "apple-touch-icon", "rel": "apple-touch-icon"}, {"dimensions": [72, 72], "image_format": "png", "prefix": "apple-touch-icon", "rel": "apple-touch-icon"}, {"dimensions": [76, 76], "image_format": "png", "prefix": "apple-touch-icon", "rel": "apple-touch-icon"}, {"dimensions": [114, 114], "image_format": "png", "prefix": "apple-touch-icon", "rel": "apple-touch-icon"}, {"dimensions": [120, 120], "image_format": "png", "prefix": "apple-touch-icon", "rel": "apple-touch-icon"}, {"dimensions": [144, 144], "image_format": "png", "prefix": "apple-touch-icon", "rel": "apple-touch-icon"}, {"dimensions": [152, 152], "image_format": "png", "prefix": "apple-touch-icon", "rel": "apple-touch-icon"}, {"dimensions": [167, 167], "image_format": "png", "prefix": "apple-touch-icon", "rel": "apple-touch-icon"}, {"dimensions": [180, 180], "image_format": "png", "prefix": "apple-touch-icon", "rel": "apple-touch-icon"}, {"dimensions": [70, 70], "image_format": "png", "prefix": "mstile", "rel": null}, {"dimensions": [270, 270], "image_format": "png", "prefix": "mstile", "rel": null}, {"dimensions": [310, 310], "image_format": "png", "prefix": "mstile", "rel": null}, {"dimensions": [310, 150], "image_format": "png", "prefix": "mstile", "rel": null}, {"dimensions": [196, 196], "image_format": "png", "prefix": "favicon", "rel": "shortcut icon"}] as Favicon[]; \ No newline at end of file diff --git a/hyperglass/ui/next.config.js b/hyperglass/ui/next.config.js index 0ff8539..c9ee564 100644 --- a/hyperglass/ui/next.config.js +++ b/hyperglass/ui/next.config.js @@ -1,12 +1,9 @@ -const envData = require('/tmp/hyperglass.env.json'); - /** * @type {import('next').NextConfig} */ const nextConfig = { reactStrictMode: true, poweredByHeader: false, - env: { ...envData }, typescript: { ignoreBuildErrors: true, }, diff --git a/hyperglass/ui/nextdev.js b/hyperglass/ui/nextdev.js index 27f45cc..d28824b 100644 --- a/hyperglass/ui/nextdev.js +++ b/hyperglass/ui/nextdev.js @@ -2,21 +2,9 @@ const express = require('express'); const proxyMiddleware = require('http-proxy-middleware'); const next = require('next'); -const config = require('/tmp/hyperglass.env.json'); - -const { - env: { NODE_ENV }, - hyperglass: { url }, -} = config; - -const devProxy = { - '/api/query/': { target: url + 'api/query/', pathRewrite: { '^/api/query/': '' } }, - '/ui/props/': { target: url + 'ui/props/', pathRewrite: { '^/ui/props/': '' } }, - '/images': { target: url + 'images', pathRewrite: { '^/images': '' } }, -}; const port = parseInt(process.env.PORT, 10) || 3000; -const dev = NODE_ENV !== 'production'; +const dev = process.env.NODE_ENV !== 'production'; const app = next({ dir: '.', // base directory where everything is, could move to src later dev, @@ -30,8 +18,20 @@ app .then(() => { server = express(); + const devProxy = { + '/api/query/': { + target: process.env.HYPERGLASS_URL + 'api/query/', + pathRewrite: { '^/api/query/': '' }, + }, + '/ui/props/': { + target: process.env.HYPERGLASS_URL + 'ui/props/', + pathRewrite: { '^/ui/props/': '' }, + }, + '/images': { target: process.env.HYPERGLASS_URL + 'images', pathRewrite: { '^/images': '' } }, + }; + // Set up the proxy. - if (dev && devProxy) { + if (dev) { Object.keys(devProxy).forEach(context => { server.use(proxyMiddleware(context, devProxy[context])); }); @@ -44,7 +44,7 @@ app if (err) { throw err; } - console.log(`> Ready on port ${port} [${NODE_ENV}]`); + console.log(`> Ready on port ${port} [${process.env.NODE_ENV}]`); }); }) .catch(err => { diff --git a/hyperglass/ui/pages/_app.tsx b/hyperglass/ui/pages/_app.tsx index 0de9912..1afd2eb 100644 --- a/hyperglass/ui/pages/_app.tsx +++ b/hyperglass/ui/pages/_app.tsx @@ -1,4 +1,3 @@ -import Head from 'next/head'; import { QueryClient, QueryClientProvider } from 'react-query'; import type { AppProps } from 'next/app'; @@ -9,24 +8,9 @@ const App = (props: AppProps): JSX.Element => { const { Component, pageProps } = props; return ( - <> - - hyperglass - - - - - - - - - - - - + + + ); }; diff --git a/hyperglass/ui/pages/_document.tsx b/hyperglass/ui/pages/_document.tsx index 3f517c9..5949556 100644 --- a/hyperglass/ui/pages/_document.tsx +++ b/hyperglass/ui/pages/_document.tsx @@ -1,4 +1,6 @@ import Document, { Html, Head, Main, NextScript } from 'next/document'; +import { Favicon } from '~/components'; +import favicons from '../favicon-formats'; import type { DocumentContext, DocumentInitialProps } from 'next/document'; @@ -12,10 +14,23 @@ class MyDocument extends Document { return ( + + + + + + + + - + {favicons.map((favicon, idx) => ( + + ))}
diff --git a/hyperglass/ui/pages/index.tsx b/hyperglass/ui/pages/index.tsx index b16e7e2..d97c7cf 100644 --- a/hyperglass/ui/pages/index.tsx +++ b/hyperglass/ui/pages/index.tsx @@ -1,34 +1,20 @@ -import Head from 'next/head'; import dynamic from 'next/dynamic'; import { Meta, Loading, If, LoadError } from '~/components'; import { HyperglassProvider } from '~/context'; import { useHyperglassConfig } from '~/hooks'; -import { getFavicons } from '~/util'; -import type { GetStaticProps } from 'next'; -import type { FaviconComponent } from '~/types'; +import type { NextPage } from 'next'; const Layout = dynamic(() => import('~/components').then(i => i.Layout), { loading: Loading, }); -interface TIndex { - favicons: FaviconComponent[]; -} - -const Index = (props: TIndex): JSX.Element => { - const { favicons } = props; +const Index: NextPage = () => { const { data, error, isLoading, ready, refetch, showError, isLoadingInitial } = useHyperglassConfig(); return ( <> - - {favicons.map((icon, idx) => { - const { rel, href, type } = icon; - return ; - })} - @@ -45,11 +31,4 @@ const Index = (props: TIndex): JSX.Element => { ); }; -export const getStaticProps: GetStaticProps = async () => { - const favicons = await getFavicons(); - return { - props: { favicons }, - }; -}; - export default Index; diff --git a/hyperglass/ui/types/config.ts b/hyperglass/ui/types/config.ts index 4a6f01d..3f0da36 100644 --- a/hyperglass/ui/types/config.ts +++ b/hyperglass/ui/types/config.ts @@ -1,8 +1,6 @@ import type { Theme } from './theme'; import type { CamelCasedPropertiesDeep, CamelCasedProperties } from 'type-fest'; -// export type QueryFields = 'query_type' | 'query_target' | 'query_location' | 'query_vrf'; - type Side = 'left' | 'right'; export type ParsedDataField = [string, keyof Route, 'left' | 'right' | 'center' | null]; @@ -189,12 +187,6 @@ export interface Favicon { prefix: string; } -export interface FaviconComponent { - rel: string; - href: string; - type: string; -} - export type Config = CamelCasedPropertiesDeep<_ConfigDeep> & CamelCasedProperties<_ConfigShallow>; export type ThemeConfig = CamelCasedProperties<_ThemeConfig>; export type Content = CamelCasedProperties<_Content>; diff --git a/hyperglass/ui/util/config.ts b/hyperglass/ui/util/config.ts index fcc7df0..17fcd00 100644 --- a/hyperglass/ui/util/config.ts +++ b/hyperglass/ui/util/config.ts @@ -1,6 +1,6 @@ import { isObject } from '~/types'; -import type { Config, FaviconComponent } from '~/types'; +import type { Config } from '~/types'; export class ConfigLoadError extends Error { public url: string = '/ui/props/'; @@ -56,14 +56,3 @@ export async function getHyperglassConfig(): Promise { } throw new ConfigLoadError('Unknown Error'); } - -export async function getFavicons(): Promise { - const { favicons: faviconConfig } = process.env.hyperglass; - return faviconConfig.map(icon => { - const { image_format, dimensions, prefix } = icon; - const [w, h] = dimensions; - const rel = icon.rel ?? ''; - const src = `/images/favicons/${prefix}-${w}x${h}.${image_format}`; - return { rel, href: src, type: `image/${image_format}` }; - }); -} diff --git a/hyperglass/util/files.py b/hyperglass/util/files.py index 5dc2c76..b2e7954 100644 --- a/hyperglass/util/files.py +++ b/hyperglass/util/files.py @@ -2,8 +2,8 @@ # Standard Library import shutil +import typing as t from queue import Queue -from typing import List, Tuple, Union, Iterable, Optional, Generator from pathlib import Path from threading import Thread @@ -11,14 +11,8 @@ from threading import Thread from hyperglass.log import log -async def move_files(src: Path, dst: Path, files: Iterable[Path]) -> Tuple[str]: # noqa: C901 - """Move iterable of files from source to destination. - - Arguments: - src {Path} -- Current directory of files - dst {Path} -- Target destination directory - files {Iterable} -- Iterable of files - """ +async def move_files(src: Path, dst: Path, files: t.Iterable[Path]) -> t.Tuple[str]: # noqa: C901 + """Move iterable of files from source to destination.""" def error(*args, **kwargs): msg = ", ".join(args) @@ -39,7 +33,7 @@ async def move_files(src: Path, dst: Path, files: Iterable[Path]) -> Tuple[str]: except TypeError: raise error("{p} is not a valid path", p=dst) - if not isinstance(files, (List, Tuple, Generator)): + if not isinstance(files, (t.List, t.Tuple, t.Generator)): raise error( "{fa} must be an iterable (list, tuple, or generator). Received {f}", fa="Files argument", @@ -95,7 +89,7 @@ class FileCopy(Thread): pass -def copyfiles(src_files: Iterable[Path], dst_files: Iterable[Path]): +def copyfiles(src_files: t.Iterable[Path], dst_files: t.Iterable[Path]): """Copy iterable of files from source to destination with threading.""" queue = Queue() threads = () @@ -131,7 +125,7 @@ def copyfiles(src_files: Iterable[Path], dst_files: Iterable[Path]): return True -def check_path(path: Union[Path, str], mode: str = "r", create: bool = False) -> Optional[Path]: +def check_path(path: t.Union[Path, str], mode: str = "r", create: bool = False) -> t.Optional[Path]: """Verify if a path exists and is accessible.""" result = None @@ -157,3 +151,30 @@ def check_path(path: Union[Path, str], mode: str = "r", create: bool = False) -> pass return result + + +def dotenv_to_dict(dotenv: t.Union[Path, str]) -> t.Dict[str, str]: + """Convert a .env file to a Python dict.""" + if not isinstance(dotenv, (Path, str)): + raise TypeError("Argument 'file' must be a Path object or string") + result = {} + data = "" + if isinstance(dotenv, Path): + if not dotenv.exists(): + raise FileNotFoundError("{!r} does not exist", str(dotenv)) + with dotenv.open("r") as f: + data = f.read() + else: + data = dotenv + + for line in (line for line in (line.strip() for line in data.splitlines()) if line): + parts = line.split("=") + if len(parts) != 2: + raise TypeError( + f"Line {line!r} is improperly formatted. " + "Expected a key/value pair such as 'key=value'" + ) + key, value = line.split("=") + result[key.strip()] = value.strip() + + return result diff --git a/hyperglass/util/frontend.py b/hyperglass/util/frontend.py index 430ceb3..8134f28 100644 --- a/hyperglass/util/frontend.py +++ b/hyperglass/util/frontend.py @@ -14,7 +14,7 @@ from pathlib import Path from hyperglass.log import log # Local -from .files import copyfiles, check_path +from .files import copyfiles, check_path, dotenv_to_dict if t.TYPE_CHECKING: # Project @@ -239,6 +239,26 @@ def migrate_images(app_path: Path, params: "UIParameters"): return copyfiles(src_files, dst_files) +def write_favicon_formats(formats: t.Tuple[t.Dict[str, t.Any]]) -> None: + """Create a TypeScript file in the `ui` directory containing favicon formats. + + This file should stay the same, unless the favicons library updates + supported formats. + """ + # Standard Library + from collections import OrderedDict + + file = Path(__file__).parent.parent / "ui" / "favicon-formats.ts" + + # Sort each favicon definition to ensure the result stays the same + # time the UI build runs. + ordered = json.dumps([OrderedDict(sorted(fmt.items())) for fmt in formats]) + data = "import type {{ Favicon }} from '~/types';export default {} as Favicon[];".format( + ordered + ) + file.write_text(data) + + async def build_frontend( # noqa: C901 dev_mode: bool, dev_url: str, @@ -249,19 +269,7 @@ async def build_frontend( # noqa: C901 timeout: int = 180, full: bool = False, ) -> bool: - """Perform full frontend UI build process. - - Securely creates temporary file, writes frontend configuration - parameters to file as JSON. Then writes the name of the temporary - file to /tmp/hyperglass.env.json as {"configFile": }. - - Webpack reads /tmp/hyperglass.env.json, loads the temporary file, - and sets its contents to Node environment variables during the build - process. - - After the build is successful, the temporary file is automatically - closed during garbage collection. - """ + """Perform full frontend UI build process.""" # Standard Library import hashlib @@ -273,24 +281,18 @@ async def build_frontend( # noqa: C901 # Create temporary file. json file extension is added for easy # webpack JSON parsing. - env_file = Path("/tmp/hyperglass.env.json") # noqa: S108 + dot_env_file = Path(__file__).parent.parent / "ui" / ".env" + env_config = {} package_json = await read_package_json() - env_vars = { - "hyperglass": {"version": __version__}, - "env": {}, - } - # Set NextJS production/development mode and base URL based on # developer_mode setting. if dev_mode: - env_vars["env"].update({"NODE_ENV": "development"}) - env_vars["hyperglass"].update({"url": dev_url}) + env_config.update({"HYPERGLASS_URL": dev_url, "NODE_ENV": "development"}) else: - env_vars["env"].update({"NODE_ENV": "production"}) - env_vars["hyperglass"].update({"url": prod_url}) + env_config.update({"HYPERGLASS_URL": prod_url, "NODE_ENV": "production"}) # Check if hyperglass/ui/node_modules has been initialized. If not, # initialize it. @@ -310,71 +312,70 @@ async def build_frontend( # noqa: C901 images_dir = app_path / "static" / "images" favicon_dir = images_dir / "favicons" - try: - if not favicon_dir.exists(): - favicon_dir.mkdir() - async with Favicons( - source=params.web.logo.favicon, - output_directory=favicon_dir, - base_url="/images/favicons/", - ) as favicons: - await favicons.generate() - log.debug("Generated {} favicons", favicons.completed) - env_vars["hyperglass"].update({"favicons": favicons.formats()}) - build_data = { - "params": params.export_dict(), - "version": __version__, - "package_json": package_json, - } - build_json = json.dumps(build_data, default=str) - # Create SHA256 hash from all parameters passed to UI, use as - # build identifier. - build_id = hashlib.sha256(build_json.encode()).hexdigest() + if not favicon_dir.exists(): + favicon_dir.mkdir() - # Read hard-coded environment file from last build. If build ID - # matches this build's ID, don't run a new build. - if env_file.exists() and not force: - with env_file.open("r") as ef: - ef_id = json.load(ef).get("buildId", "empty") - log.debug("Previous Build ID: {}", ef_id) + async with Favicons( + source=params.web.logo.favicon, + output_directory=favicon_dir, + base_url="/images/favicons/", + ) as favicons: + await favicons.generate() + log.debug("Generated {} favicons", favicons.completed) + write_favicon_formats(favicons.formats()) - if ef_id == build_id: - log.debug("UI parameters unchanged since last build, skipping UI build...") - return True + build_data = { + "params": params.export_dict(), + "version": __version__, + "package_json": package_json, + } - env_vars["buildId"] = build_id + build_json = json.dumps(build_data, default=str) - env_file.write_text(json.dumps(env_vars, default=str)) - log.debug("Wrote UI environment file '{}'", str(env_file)) + # Create SHA256 hash from all parameters passed to UI, use as + # build identifier. + build_id = hashlib.sha256(build_json.encode()).hexdigest() - # Initiate Next.JS export process. - if any((not dev_mode, force, full)): - log.info("Starting UI build") - initialize_result = await node_initial(timeout, dev_mode) - build_result = await build_ui(app_path=app_path) + # Read hard-coded environment file from last build. If build ID + # matches this build's ID, don't run a new build. + if dot_env_file.exists() and not force: + env_data = dotenv_to_dict(dot_env_file) + env_build_id = env_data.get("HYPERGLASS_BUILD_ID", "None") + log.debug("Previous Build ID: {!r}", env_build_id) - if initialize_result: - log.debug(initialize_result) - elif initialize_result == "": - log.debug("Re-initialized node_modules") + if env_build_id == build_id: + log.debug("UI parameters unchanged since last build, skipping UI build...") + return True - if build_result: - log.success("Completed UI build") - elif dev_mode and not force: - log.debug("Running in developer mode, did not build new UI files") + env_config.update({"HYPERGLASS_BUILD_ID": build_id}) - migrate_images(app_path, params) + dot_env_file.write_text("\n".join(f"{k}={v}" for k, v in env_config.items())) + log.debug("Wrote UI environment file {!r}", str(dot_env_file)) - generate_opengraph( - params.web.opengraph.image, - 1200, - 630, - images_dir, - params.web.theme.colors.black, - ) + # Initiate Next.JS export process. + if any((not dev_mode, force, full)): + log.info("Starting UI build") + initialize_result = await node_initial(timeout, dev_mode) + build_result = await build_ui(app_path=app_path) - except Exception as err: - log.error(err) - raise RuntimeError(str(err)) from None + if initialize_result: + log.debug(initialize_result) + elif initialize_result == "": + log.debug("Re-initialized node_modules") + + if build_result: + log.success("Completed UI build") + elif dev_mode and not force: + log.debug("Running in developer mode, did not build new UI files") + + migrate_images(app_path, params) + + generate_opengraph( + params.web.opengraph.image, + 1200, + 630, + images_dir, + params.web.theme.colors.black, + ) return True diff --git a/hyperglass/util/tests/test_files.py b/hyperglass/util/tests/test_files.py new file mode 100644 index 0000000..ed557ad --- /dev/null +++ b/hyperglass/util/tests/test_files.py @@ -0,0 +1,50 @@ +"""Test file-related utilities.""" + +# Standard Library +import tempfile +from pathlib import Path + +# Third Party +import pytest + +# Local +from ..files import dotenv_to_dict + +ENV_TEST = """KEY1=VALUE1 +KEY2=VALUE2 +KEY3=VALUE3 + """ + + +def test_dotenv_to_dict_string(): + result = dotenv_to_dict(ENV_TEST) + assert result.get("KEY1") == "VALUE1" + assert result.get("KEY2") == "VALUE2" + assert result.get("KEY3") == "VALUE3" + + +def test_dotenv_to_dict_file(): + _, filename = tempfile.mkstemp() + file = Path(filename) + with file.open("w+") as f: + f.write(ENV_TEST) + result = dotenv_to_dict(file) + assert result.get("KEY1") == "VALUE1" + assert result.get("KEY2") == "VALUE2" + assert result.get("KEY3") == "VALUE3" + file.unlink() + + +def test_dotenv_to_dict_raises_type_error(): + with pytest.raises(TypeError): + dotenv_to_dict(True) + + +def test_dotenv_to_dict_raises_filenotfounderror(): + with pytest.raises(FileNotFoundError): + dotenv_to_dict(Path("/tmp/not-a-thing")) # noqa: S108 + + +def test_dotenv_invalid_format(): + with pytest.raises(TypeError): + dotenv_to_dict("this should raise an error")