commit 923ecf283a568e0c31380144be8e973c5507ca4c Author: Luca Bilke Date: Mon Jun 24 01:03:16 2024 +0200 WIP diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..b3dfee7 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,7 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = true +indent_style = space +indent_size = 2 diff --git a/.forgejo/workflows/test.yml b/.forgejo/workflows/test.yml new file mode 100644 index 0000000..6ac2397 --- /dev/null +++ b/.forgejo/workflows/test.yml @@ -0,0 +1,23 @@ +name: test + +on: + push: + branches: + - master + - main + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: erlef/setup-beam@v1 + with: + otp-version: "26.0.2" + gleam-version: "1.2.1" + rebar3-version: "3" + # elixir-version: "1.15.4" + - run: gleam deps download + - run: gleam test + - run: gleam format --check src test diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..599be4e --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +*.beam +*.ez +/build +erl_crash.dump diff --git a/README.md b/README.md new file mode 100644 index 0000000..5d6ed92 --- /dev/null +++ b/README.md @@ -0,0 +1,34 @@ +# dumptruck + +## Development + +```sh +gleam run # Run the project +gleam test # Run the tests +gleam shell # Run an Erlang shell +``` + +## Flow + +### Upload + +take file upload +-> get hash of file +-> test against CSAM hash list +-> generate UUID +-> point UUID at hash of file +-> save file if hash doesn't exist +-> return UUID + +### Download + +take file UUID +-> search for UUID in table and get hash +-> return file if hash exists + +## TODO + +- [] Handle file upload +- [] Handle file download +- [] Testing against CSAM hash list +- [] Logging IPs (up and down) of detected CSAM uploads diff --git a/gleam.toml b/gleam.toml new file mode 100644 index 0000000..bbafad0 --- /dev/null +++ b/gleam.toml @@ -0,0 +1,28 @@ +name = "dumptruck" +version = "1.0.0" + +# Fill out these fields if you intend to generate HTML documentation or publish +# your project to the Hex package manager. +# +# description = "" +# licences = ["Apache-2.0"] +# repository = { type = "github", user = "username", repo = "project" } +# links = [{ title = "Website", href = "https://gleam.run" }] +# +# For a full reference of all the available options, you can have a look at +# https://gleam.run/writing-gleam/gleam-toml/. + +[dependencies] +gleam_stdlib = ">= 0.34.0 and < 2.0.0" +wisp = ">= 0.15.0 and < 1.0.0" +mist = ">= 1.2.0 and < 2.0.0" +gleam_http = ">= 3.6.0 and < 4.0.0" +gleam_erlang = ">= 0.25.0 and < 1.0.0" +gleam_otp = ">= 0.10.0 and < 1.0.0" +ids = ">= 0.13.0 and < 1.0.0" +envoy = ">= 1.0.1 and < 2.0.0" +gleam_crypto = ">= 1.3.0 and < 2.0.0" +gleam_pgo = ">= 0.11.0 and < 1.0.0" + +[dev-dependencies] +gleeunit = ">= 1.0.0 and < 2.0.0" diff --git a/manifest.toml b/manifest.toml new file mode 100644 index 0000000..e6b6355 --- /dev/null +++ b/manifest.toml @@ -0,0 +1,46 @@ +# This file was generated by Gleam +# You typically do not need to edit this file + +packages = [ + { name = "backoff", version = "1.1.6", build_tools = ["rebar3"], requirements = [], otp_app = "backoff", source = "hex", outer_checksum = "CF0CFFF8995FB20562F822E5CC47D8CCF664C5ECDC26A684CBE85C225F9D7C39" }, + { name = "birl", version = "1.7.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "ranger"], otp_app = "birl", source = "hex", outer_checksum = "5C66647D62BCB11FE327E7A6024907C4A17954EF22865FE0940B54A852446D01" }, + { name = "envoy", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "envoy", source = "hex", outer_checksum = "CFAACCCFC47654F7E8B75E614746ED924C65BD08B1DE21101548AC314A8B6A41" }, + { name = "exception", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "exception", source = "hex", outer_checksum = "F5580D584F16A20B7FCDCABF9E9BE9A2C1F6AC4F9176FA6DD0B63E3B20D450AA" }, + { name = "filepath", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "EFB6FF65C98B2A16378ABC3EE2B14124168C0CE5201553DE652E2644DCFDB594" }, + { name = "gleam_crypto", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_crypto", source = "hex", outer_checksum = "ADD058DEDE8F0341F1ADE3AAC492A224F15700829D9A3A3F9ADF370F875C51B7" }, + { name = "gleam_erlang", version = "0.25.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "054D571A7092D2A9727B3E5D183B7507DAB0DA41556EC9133606F09C15497373" }, + { name = "gleam_http", version = "3.6.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "8C07DF9DF8CC7F054C650839A51C30A7D3C26482AC241C899C1CEA86B22DBE51" }, + { name = "gleam_json", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "thoas"], otp_app = "gleam_json", source = "hex", outer_checksum = "9063D14D25406326C0255BDA0021541E797D8A7A12573D849462CAFED459F6EB" }, + { name = "gleam_otp", version = "0.10.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "0B04FE915ACECE539B317F9652CAADBBC0F000184D586AAAF2D94C100945D72B" }, + { name = "gleam_pgo", version = "0.11.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "pgo"], otp_app = "gleam_pgo", source = "hex", outer_checksum = "1897C299A05AE3DE6BF6CC92CD1D67FDF8504B15E8826CBEBFD0E4471A698DD3" }, + { name = "gleam_stdlib", version = "0.38.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "663CF11861179AF415A625307447775C09404E752FF99A24E2057C835319F1BE" }, + { name = "gleeunit", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "F7A7228925D3EE7D0813C922E062BFD6D7E9310F0BEE585D3A42F3307E3CFD13" }, + { name = "glisten", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_otp", "gleam_stdlib"], otp_app = "glisten", source = "hex", outer_checksum = "CF3A9383E9BA4A8CBAF2F7B799716290D02F2AC34E7A77556B49376B662B9314" }, + { name = "gramps", version = "2.0.3", build_tools = ["gleam"], requirements = ["gleam_crypto", "gleam_erlang", "gleam_http", "gleam_stdlib"], otp_app = "gramps", source = "hex", outer_checksum = "3CCAA6E081225180D95C79679D383BBF51C8D1FDC1B84DA1DA444F628C373793" }, + { name = "hpack_erl", version = "0.3.0", build_tools = ["rebar3"], requirements = [], otp_app = "hpack", source = "hex", outer_checksum = "D6137D7079169D8C485C6962DFE261AF5B9EF60FBC557344511C1E65E3D95FB0" }, + { name = "ids", version = "0.13.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_otp", "gleam_stdlib"], otp_app = "ids", source = "hex", outer_checksum = "D59147A8E7D818FEBE5CA572859CC018D0ACEFA84F5CED7CE86EDBCC10622B21" }, + { name = "logging", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "logging", source = "hex", outer_checksum = "FCB111401BDB4703A440A94FF8CC7DA521112269C065F219C2766998333E7738" }, + { name = "marceau", version = "1.2.0", build_tools = ["gleam"], requirements = [], otp_app = "marceau", source = "hex", outer_checksum = "5188D643C181EE350D8A20A3BDBD63AF7B6C505DE333CFBE05EF642ADD88A59B" }, + { name = "mist", version = "1.2.0", build_tools = ["gleam"], requirements = ["birl", "gleam_erlang", "gleam_http", "gleam_otp", "gleam_stdlib", "glisten", "gramps", "hpack_erl", "logging"], otp_app = "mist", source = "hex", outer_checksum = "109B4D64E68C104CC23BB3CC5441ECD479DD7444889DA01113B75C6AF0F0E17B" }, + { name = "opentelemetry_api", version = "1.3.0", build_tools = ["rebar3", "mix"], requirements = ["opentelemetry_semantic_conventions"], otp_app = "opentelemetry_api", source = "hex", outer_checksum = "B9E5FF775FD064FA098DBA3C398490B77649A352B40B0B730A6B7DC0BDD68858" }, + { name = "opentelemetry_semantic_conventions", version = "0.2.0", build_tools = ["rebar3", "mix"], requirements = [], otp_app = "opentelemetry_semantic_conventions", source = "hex", outer_checksum = "D61FA1F5639EE8668D74B527E6806E0503EFC55A42DB7B5F39939D84C07D6895" }, + { name = "pg_types", version = "0.4.0", build_tools = ["rebar3"], requirements = [], otp_app = "pg_types", source = "hex", outer_checksum = "B02EFA785CAECECF9702C681C80A9CA12A39F9161A846CE17B01FB20AEEED7EB" }, + { name = "pgo", version = "0.14.0", build_tools = ["rebar3"], requirements = ["backoff", "opentelemetry_api", "pg_types"], otp_app = "pgo", source = "hex", outer_checksum = "71016C22599936E042DC0012EE4589D24C71427D266292F775EBF201D97DF9C9" }, + { name = "ranger", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "ranger", source = "hex", outer_checksum = "1566C272B1D141B3BBA38B25CB761EF56E312E79EC0E2DFD4D3C19FB0CC1F98C" }, + { name = "simplifile", version = "2.0.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "95219227A43FCFE62C6E494F413A1D56FF953B68FE420698612E3D89A1EFE029" }, + { name = "thoas", version = "1.2.1", build_tools = ["rebar3"], requirements = [], otp_app = "thoas", source = "hex", outer_checksum = "E38697EDFFD6E91BD12CEA41B155115282630075C2A727E7A6B2947F5408B86A" }, + { name = "wisp", version = "0.15.0", build_tools = ["gleam"], requirements = ["exception", "gleam_crypto", "gleam_erlang", "gleam_http", "gleam_json", "gleam_stdlib", "logging", "marceau", "mist", "simplifile"], otp_app = "wisp", source = "hex", outer_checksum = "33D17A50276FE0A10E4F694E4EF7D99836954DC2D920D4B5741B1E0EBCAE403F" }, +] + +[requirements] +envoy = { version = ">= 1.0.1 and < 2.0.0" } +gleam_crypto = { version = ">= 1.3.0 and < 2.0.0" } +gleam_erlang = { version = ">= 0.25.0 and < 1.0.0" } +gleam_http = { version = ">= 3.6.0 and < 4.0.0" } +gleam_otp = { version = ">= 0.10.0 and < 1.0.0" } +gleam_pgo = { version = ">= 0.11.0 and < 1.0.0" } +gleam_stdlib = { version = ">= 0.34.0 and < 2.0.0" } +gleeunit = { version = ">= 1.0.0 and < 2.0.0" } +ids = { version = ">= 0.13.0 and < 1.0.0" } +mist = { version = ">= 1.2.0 and < 2.0.0" } +wisp = { version = ">= 0.15.0 and < 1.0.0" } diff --git a/src/app/config.gleam b/src/app/config.gleam new file mode 100644 index 0000000..5a21110 --- /dev/null +++ b/src/app/config.gleam @@ -0,0 +1,63 @@ +import envoy +import gleam/int +import gleam/result.{unwrap} + +pub type Config { + Config( + port: Int, + max_bytes: Int, + url_root: String, + secret_key_base: String, + postgres_host: String, + postgres_db: String, + postgres_user: String, + postgres_password: String, + postgres_port: Int, + postgres_pool_size: Int, + ) +} + +pub fn get_int_or(var: String, default: Int) -> Int { + case envoy.get(var) { + Ok(value) -> int.parse(value) |> unwrap(default) + Error(_) -> default + } +} + +pub fn get_string_or(var: String, default: String) -> String { + envoy.get(var) |> unwrap(default) +} + +// fn get_path_or(var: String, default: List(String)) -> List(String) { +// case envoy.get(var) { +// Ok(path_raw) -> { +// string.split(path_raw, "/") +// } +// Error(_) -> default +// } +// } + +pub fn load_config_from_env(defaults: Config) -> Config { + Config( + port: "PORT" + |> get_int_or(defaults.port), + max_bytes: "MAX_BYTES" + |> get_int_or(defaults.max_bytes), + url_root: "URL_ROOT" + |> get_string_or(defaults.url_root), + secret_key_base: "SECRET_KEY_BASE" + |> get_string_or(defaults.secret_key_base), + postgres_host: "POSTGRES_HOST" + |> get_string_or(defaults.postgres_host), + postgres_db: "POSTGRES_DB" + |> get_string_or(defaults.postgres_db), + postgres_user: "POSTGRES_USER" + |> get_string_or(defaults.postgres_user), + postgres_password: "POSTGRES_PASSWORD" + |> get_string_or(defaults.postgres_password), + postgres_pool_size: "POSTGRES_POOL_SIZE" + |> get_int_or(defaults.postgres_pool_size), + postgres_port: "POSTGRES_PORT" + |> get_int_or(defaults.postgres_port), + ) +} diff --git a/src/app/router.gleam b/src/app/router.gleam new file mode 100644 index 0000000..29d2d22 --- /dev/null +++ b/src/app/router.gleam @@ -0,0 +1,17 @@ +import app/web.{type Context} +import app/web/file +import app/web/link +import app/web/root +import gleam/pgo +import wisp.{type Request, type Response} + +pub fn handle_request(req: Request, ctx: Context) -> Response { + use req <- web.middleware(req) + + case wisp.path_segments(req) { + ["f", ..] -> file.handle_request(req, ctx) + ["l", ..] -> link.handle_request(req, ctx) + [] -> root.handle_request(req) + _ -> wisp.not_found() + } +} diff --git a/src/app/web.gleam b/src/app/web.gleam new file mode 100644 index 0000000..1c7fd95 --- /dev/null +++ b/src/app/web.gleam @@ -0,0 +1,34 @@ +import app/config.{type Config} +import gleam/pgo +import wisp.{type Request, type Response} + +pub const html_head = " + + + + +Dumptruck + + +" + +pub const html_tail = " + +" + +pub type Context { + Context(db: pgo.Connection, config: Config) +} + +pub fn middleware( + req: Request, + handle_request: fn(Request) -> Response, +) -> Response { + let req = wisp.method_override(req) + use <- wisp.log_request(req) + use <- wisp.rescue_crashes + use req <- wisp.handle_head(req) + handle_request(req) +} diff --git a/src/app/web/file.gleam b/src/app/web/file.gleam new file mode 100644 index 0000000..491fdf3 --- /dev/null +++ b/src/app/web/file.gleam @@ -0,0 +1,44 @@ +import app/config.{type Config} +import app/web.{type Context} +import gleam/http.{Get, Post} +import gleam/list +import gleam/string +import wisp.{type Request, type Response} + +const schema = " +CREATE TABLE IF NOT EXISTS `FileReference` ( + nanoid TEXT <> + PathID INT +); +CREATE TABLE IF NOT EXISTS `FilePath` ( + PathID INT <> + md5_hash BLOB + banned BOOLEAN +); +" + +type File { + Reference(md5_hash: BitArray, nanoid: String) + Path(md5_hash: String, banned: Bool) +} + +fn hash_to_path(hash: String) -> List(String) { + list.new() + |> list.append([hash |> string.slice(0, 2)]) + |> list.append([hash |> string.slice(2, 2)]) + |> list.append([hash |> string.slice(4, { hash |> string.length } - 4)]) +} + +pub fn handle_request(req: Request, ctx: Context) -> Response { + todo +} + +fn serve_file(req, uuid: String) -> Response { + use <- wisp.require_method(req, Get) + todo as "implement serve_file" +} + +fn receive_file(req: Request) -> Response { + use <- wisp.require_method(req, Post) + todo as "implement receive_file" +} diff --git a/src/app/web/link.gleam b/src/app/web/link.gleam new file mode 100644 index 0000000..27dad28 --- /dev/null +++ b/src/app/web/link.gleam @@ -0,0 +1,129 @@ +import app/web.{type Context} +import gleam/dynamic +import gleam/http.{Get, Post} +import gleam/int +import gleam/list +import gleam/pair +import gleam/pgo +import gleam/result +import gleam/string_builder +import gleam/uri.{type Uri} +import ids/nanoid +import wisp.{type Request, type Response} + +const schema = " +CREATE TABLE IF NOT EXISTS \"Link\" ( + NanoID VARCHAR(21) PRIMARY KEY, + TargetURL TEXT +); +" + +pub type Link { + Link(nano_id: String, target_url: Uri) +} + +pub fn prepare_database(ctx: Context) -> Result(Nil, pgo.QueryError) { + pgo.execute(schema, ctx.db, [], dynamic.dynamic) + |> result.map(fn(_) { Nil }) +} + +pub fn store(link: Link, ctx: Context) -> Result(String, Nil) { + let sql = + "INSERT INTO \"Link\" (NanoID, TargetURL) VALUES ($1, $2) RETURNING NanoID;" + case + pgo.execute( + sql, + ctx.db, + [pgo.text(link.nano_id), pgo.text(link.target_url |> uri.to_string)], + dynamic.string, + ) + { + Ok(target_id) -> target_id.rows |> list.first + Error(_) -> Error(Nil) + } +} + +pub fn retrieve(nano_id: String, ctx: Context) -> Result(Link, Nil) { + let sql = "SELECT NanoID,TargetURL FROM \"Link\" WHERE NanoID = $1;" + case + pgo.execute( + sql, + ctx.db, + [pgo.text(nano_id)], + dynamic.tuple2(dynamic.string, dynamic.string), + ) + { + Ok(res) -> { + let target_raw = res.rows |> list.first + let nano_id = + target_raw + |> result.map(pair.first) + let target_url = + target_raw + |> result.map(pair.second) + |> result.try(uri.parse) + + use nano_id <- result.try(nano_id) + use target_url <- result.try(target_url) + + Ok(Link(nano_id, target_url)) + } + _ -> Error(Nil) + } +} + +pub fn generate_url( + nano_id: Result(String, Nil), + ctx: Context, +) -> Result(Uri, Nil) { + use nano_id <- result.try(nano_id) + uri.parse( + ctx.config.url_root + <> ":" + <> ctx.config.port |> int.to_string + <> "/l/" + <> nano_id, + ) +} + +pub fn handle_uri(url: Uri, ctx: Context) -> Response { + nanoid.generate() + |> Link(url) + |> store(ctx) + |> generate_url(ctx) + |> result.map(fn(uri) { + uri + |> uri.to_string + |> string_builder.from_string + |> wisp.Text + |> wisp.set_body(wisp.response(200), _) + }) + |> result.unwrap(wisp.response(404)) +} + +pub fn handle_root(req: Request, ctx: Context) -> Response { + use <- wisp.require_method(req, Post) + use body <- wisp.require_string_body(req) + case uri.parse(body) { + Ok(uri) -> handle_uri(uri, ctx) + Error(_) -> wisp.bad_request() + } +} + +pub fn handle_nano_id(req: Request, nano_id: String, ctx: Context) -> Response { + use <- wisp.require_method(req, Get) + case retrieve(nano_id, ctx) { + Ok(link) -> wisp.redirect(link.target_url |> uri.to_string) + Error(_) -> wisp.not_found() + } +} + +pub fn handle_request(req: Request, ctx: Context) -> Response { + use req <- web.middleware(req) + + case wisp.path_segments(req) { + ["l"] -> handle_root(req, ctx) + ["l", nano_id] -> handle_nano_id(req, nano_id, ctx) + _ -> wisp.not_found() + } +} diff --git a/src/app/web/root.gleam b/src/app/web/root.gleam new file mode 100644 index 0000000..df7acf7 --- /dev/null +++ b/src/app/web/root.gleam @@ -0,0 +1,15 @@ +import app/web +import gleam/http.{Get} +import gleam/string_builder +import wisp.{type Request, type Response} + +pub fn handle_request(req: Request) -> Response { + use <- wisp.require_method(req, Get) + { web.html_head <> " +Welcome to Dumptruck! +POST a file to /f to get a shareable link. +POST a link to /l to get a shortened URL. + " <> web.html_tail } + |> string_builder.from_string + |> wisp.html_response(200) +} diff --git a/src/dumptruck.gleam b/src/dumptruck.gleam new file mode 100644 index 0000000..7597d00 --- /dev/null +++ b/src/dumptruck.gleam @@ -0,0 +1,46 @@ +import app/config.{Config} +import app/router +import app/web.{Context} +import gleam/erlang/process +import gleam/pgo +import mist +import wisp + +pub fn main() { + wisp.configure_logger() + let r64 = wisp.random_string(64) + let default_config = + Config( + port: 8080, + max_bytes: 1024 * 1024 * 50, + url_root: "http://localhost", + secret_key_base: r64, + postgres_host: "localhost", + postgres_db: "dumptruck", + postgres_user: "dumptruck", + postgres_password: "dumptruck", + postgres_port: 5432, + postgres_pool_size: 15, + ) + let config = config.load_config_from_env(default_config) + + let db = + pgo.connect( + pgo.Config( + ..pgo.default_config(), + host: config.postgres_host, + database: config.postgres_db, + pool_size: config.postgres_pool_size, + ), + ) + + let ctx = Context(db, config) + let entrypoint = fn(req) { router.handle_request(req, ctx) } + let assert Ok(_) = + wisp.mist_handler(entrypoint, config.secret_key_base) + |> mist.new + |> mist.port(config.port) + |> mist.start_http + + process.sleep_forever() +} diff --git a/test/app/config_test.gleam b/test/app/config_test.gleam new file mode 100644 index 0000000..5a84d28 --- /dev/null +++ b/test/app/config_test.gleam @@ -0,0 +1,59 @@ +import app/config.{type Config, Config} +import envoy +import gleam/int +import gleeunit/should +import wisp/testing + +const default_config = Config( + port: 8080, + max_bytes: 52_428_800, + url_root: "localhost", + secret_key_base: testing.default_secret_key_base, + postgres_pool_size: 15, + postgres_port: 5432, + postgres_host: "localhost", + postgres_db: "dumptruck", + postgres_user: "dumptruck", + postgres_password: "dumptruck", +) + +fn setenv(conf: Config) { + envoy.set("PORT", conf.port |> int.to_string) + envoy.set("MAX_BYTES", conf.max_bytes |> int.to_string) + envoy.set("URL_ROOT", conf.url_root) + envoy.set("SECRET_KEY_BASE", conf.secret_key_base) + envoy.set("POSTGRES_DB", conf.postgres_db) + envoy.set("POSTGRES_HOST", conf.postgres_host) + envoy.set("POSTGRES_USER", conf.postgres_user) + envoy.set("POSTGRES_PASSWORD", conf.postgres_password) + envoy.set("POSTGRES_PORT", conf.postgres_port |> int.to_string) + envoy.set("POSTGRES_POOL_SIZE", conf.postgres_pool_size |> int.to_string) +} + +pub fn get_string_or_test() { + envoy.unset("TEST_UNSET") + config.get_string_or("TEST_UNSET", "ELSE") |> should.equal("ELSE") + + envoy.set("TEST_SET", "YES") + config.get_string_or("TEST_SET", "ELSE") |> should.equal("YES") +} + +pub fn get_int_or_test() { + envoy.unset("TEST_UNSET") + config.get_int_or("TEST_UNSET", 333) |> should.equal(333) + + envoy.set("TEST_SET", "999") + config.get_int_or("TEST_SET", 333) |> should.equal(999) +} + +pub fn load_config_from_env_test() { + let test_config = + Config(..default_config, port: 1234, max_bytes: 123, url_root: "test") + + setenv(test_config) + + let conf = config.load_config_from_env(default_config) + conf.port |> should.equal(1234) + conf.max_bytes |> should.equal(123) + conf.url_root |> should.equal("test") +} diff --git a/test/app/web/link_test.gleam b/test/app/web/link_test.gleam new file mode 100644 index 0000000..1928d26 --- /dev/null +++ b/test/app/web/link_test.gleam @@ -0,0 +1,74 @@ +import app/config.{Config} +import app/web.{type Context, Context} +import app/web/link.{Link} +import gleam/dynamic +import gleam/option.{None, Some} +import gleam/pgo +import gleam/uri +import gleeunit/should + +const test_config = Config( + port: 8080, + max_bytes: 52_428_800, + url_root: "http://localhost", + secret_key_base: "cDv6ikrODZKjKbwSBBwspXInc61MYpCzQ8My9gW2QpQg3Y3lT7IJ5RJlzRN5HZK0", + postgres_db: "testdb", + postgres_user: "testuser", + postgres_password: "testpassword", + postgres_host: "localhost", + postgres_pool_size: 15, + postgres_port: 5432, +) + +fn with_context(testcase: fn(Context) -> t) -> t { + let ctx = + Context( + db: pgo.Config( + ..pgo.default_config(), + database: test_config.postgres_db, + password: test_config.postgres_password |> Some, + user: test_config.postgres_user, + host: test_config.postgres_host, + pool_size: test_config.postgres_pool_size, + port: 5432, + ) + |> pgo.connect, + config: test_config, + ) + + let ret = testcase(ctx) + ctx.db |> pgo.disconnect + ret +} + +pub fn null_test() { + use ctx <- with_context() + + pgo.execute( + "select $1", + ctx.db, + [pgo.null()], + dynamic.element(0, dynamic.optional(dynamic.int)), + ) + |> should.equal(Ok(pgo.Returned(count: 1, rows: [None]))) +} + +pub fn generate_url_test() { + use ctx <- with_context() + let test_id = Ok("testid_generate_url") + + link.generate_url(test_id, ctx) + |> should.equal(uri.parse("http://localhost:8080/l/testid_generate_url")) +} + +pub fn store_link_test() { + use ctx <- with_context() + let assert Ok(test_url) = uri.parse("http://example.com") + let test_id = "testid_store_link" + let test_link = Link(test_id, test_url) + + let assert Ok(stored_nano_id) = link.store(test_link, ctx) + stored_nano_id |> should.equal(test_id) + // let assert Ok(stored_link) = stored_nano_id |> link.retrieve(ctx) + // stored_link.target_url |> should.equal(test_url) +} diff --git a/test/docker-compose.yml b/test/docker-compose.yml new file mode 100644 index 0000000..f92c0d7 --- /dev/null +++ b/test/docker-compose.yml @@ -0,0 +1,9 @@ +services: + db: + image: postgres:latest + environment: + - POSTGRES_USER=testuser + - POSTGRES_PASSWORD=testpassword + - POSTGRES_DB=testdb + ports: + - "5432:5432" diff --git a/test/dumptruck_test.gleam b/test/dumptruck_test.gleam new file mode 100644 index 0000000..ecd12ad --- /dev/null +++ b/test/dumptruck_test.gleam @@ -0,0 +1,5 @@ +import gleeunit + +pub fn main() { + gleeunit.main() +}