https://cloak.software/blog/rust-on-nails/
[ ]
Cloak
* Blog
* Developers
* Install the CLI
* Your First Secret
* Accessing secrets from the command line
* Injecting secrets into applications
* Encrypting your private key
* Pricing
* Sign Up
* Sign In
Rust on Nails
A full stack architecture for Rust web applications
You are on Nails
To build a web application you need to make architecture decisions
across a range of topics. The beauty of Ruby on Rails or Django is
that they make those decisions for you so you can start building your
web application straight away. They also back those decisions up with
great documentation.
Rust on Nails is a framework leveraging existing solutions that
fulfil the needs of a full stack development. We look at each
decision that needs to be made then pull in solutions and package
everything up so that it works together. It's an opinionated
framework and it's how Cloak was built see the Cloak Github Repo.
Here's a breakdown of the services, decisions and best practices
covered.
1. Development Environment as Code
2. The Web Server and Routing
3. Choosing a Database
4. Configuration
5. Database Migrations
6. Database Access
7. HTML Templating
8. Form Handling and Validation
9. Asset Pipeline
10. Cache busting and images
11. Layouts
12. Front End Enhancement
13. Partials and Components
14. Sending Email
15. Authentication
16. Integration Tests
Let's get started.
Development Environment as Code.
The Visual Studio Code Remote - Containers extension lets you use a
Docker container as a full-featured development environment. This
fixes the following problems
* Enables developers other than yourself to get quickly up to speed
* Stops issues such as "It works on my machine"
* Allows you to check your development environment into git.
Install the devcontainer extension in VSCode and then setup a Rust
environment.
Creating a vault
The select Open folder in container... Select Rust and Postgres. Also
select node on the next menu.
Creating a vault
How you folder structure will look.
.
+-- .cargo/
+-- .devcontainer/
+-- .env
+-- devcontainer.json
+-- docker-compose.yml
+-- Dockerfile
We are going to create a workspace for our web application. Create a
new Cargo.toml file in the root folder and add the following.
[workspace]
members = [
"app",
]
The run the following command.
$ cargo new app
You should now have a folder structure like the following.
+-- .devcontainer/
| +-- ...
+-- app/
| +--src/
| | +-- main.rs
| +-- Cargo.toml
+-- Cargo.toml
+-- Cargo.lock
Test out you development environment with
$ cargo run
Compiling app v0.1.0 (/workspace/app)
Finished dev [unoptimized + debuginfo] target(s) in 1.16s
Running `target/debug/app`
Hello, world!
The Web Server and Routing
We looked at Actix Web, Tokio Axum and Rocket. Axum was chosen as
it's very actively maintained and has the fastest incremental build
times.
Most rust web server project operate in a similar way. That is you
configure a route and a function that will respond to that route.
The functions that respond to routes can have parameters. These
parameters which might be structs, database pools or form data are
passed to the function by the framework.
Add the following to your app/Cargo.toml.
[dependencies]
axum = "0"
tokio = { version = "1", default-features = false, features = ["macros", "rt-multi-thread"] }
And update your app/src/main.rs
use axum::{response::Html, routing::get, Router};
use std::net::SocketAddr;
#[tokio::main]
async fn main() {
// build our application with a route
let app = Router::new().route("/", get(handler));
// run it
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
println!("listening on {}", addr);
axum::Server::bind(&addr)
.serve(app.into_make_service())
.await
.unwrap();
}
async fn handler() -> Html<&'static str> {
Html("
Hello, World!
")
}
Issue the command
cargo run
And you should be able to point your browser at http://localhost:3000
and see the web server deliver Hello World.
The Database
If you're not sure which database to use then use Postgres.
The architecture doesn't stop you using MySQL (MariaDB?) or other
relational databases. However a relational database is the
recommendation.
We already installed Postgres when we installed our devcontainer,
however we didn't install the Postgres command line client. To do
that, add the following to your .devcontainer/Dockerfile.
# Install psql 14
RUN sh -c 'echo "deb http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list' \
&& wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - \
&& apt-get -y update \
&& apt-get -y install postgresql-client \
&& apt-get autoremove -y && apt-get clean -y
Add the following to your .devcontainer/.env
DATABASE_URL=postgresql://postgres:[email protected]:5432/postgres?sslmode=disable
Restart your devcontainer and you should now have access to Postgres.
i.e.
psql $DATABASE_URL
psql (14.2 (Debian 14.2-1.pgdg110+1), server 14.1 (Debian 14.1-1.pgdg110+1))
Type "help" for help.
postgres=# \dt
Did not find any relations.
postgres=# \q
We will use this pattern over and over. When we add a tool to our
solution we add it to the devcontainer this ensures we can always
reproduce our development environment.
Configuration
To configure our application we pull in environment variables a
create a Rust struct. Create a app/src/config.rs.
#[derive(Clone, Debug)]
pub struct Config {
pub database_url: String,
}
impl Config {
// Initialise form oiur environment
pub fn new() -> Config {
let database_url =
std::env::var("DATABASE_URL")
.expect("DATABASE_URL not set");
Config {
database_url,
}
}
}
.
+-- .devcontainer/
| +-- ...
+-- app/
| +--src/
| | +-- main.rs
| | +-- config.rs <- Our configuration
| +-- Cargo.toml
+-- Cargo.toml
+-- Cargo.lock
Database Migrations
Dbmate is a database migration tool, to keep your database schema in
sync across multiple developers and your production servers.
Add the following to your .devcontainer/Dockerfile and rebuild your
devcontainer
RUN sudo curl -fsSL -o /usr/local/bin/dbmate https://github.com/amacneil/dbmate/releases/latest/download/dbmate-linux-amd64 \
&& sudo chmod +x /usr/local/bin/dbmate
After that we can setup our migrations folder and the create a users
migration.
$ dbmate new create_users_table
Creating migration: db/migrations/20220330110026_create_users_table.sql
Edit the SQL file that was generated for you and add the following.
-- migrate:up
CREATE TABLE users (
id SERIAL PRIMARY KEY,
email VARCHAR NOT NULL UNIQUE,
hashed_password VARCHAR NOT NULL,
reset_password_selector VARCHAR,
reset_password_verifier_hash VARCHAR,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE TABLE sessions (
id SERIAL PRIMARY KEY,
session_verifier VARCHAR NOT NULL,
user_id INT NOT NULL,
otp_code_encrypted VARCHAR NOT NULL,
otp_code_attempts INTEGER NOT NULL DEFAULT 0,
otp_code_confirmed BOOLEAN NOT NULL DEFAULT false,
otp_code_sent BOOLEAN NOT NULL DEFAULT false,
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);
-- migrate:down
DROP TABLE sessions;
DROP TABLE users;
List the migrations so we can see which have run.
$ dbmate status
[ ] 20220330110026_create_users_table.sql
Applied: 0
Pending: 1
Run our new migration.
$ dbmate up
Applying: 20220330110026_create_users_table.sql
And check that it worked.
$ psql $DATABASE_URL -c 'SELECT count(*) FROM users;'
count
-------
0
(1 row)
Your project folders should now look like this.
.
+-- .devcontainer/
| +-- ...
+-- app/
| +-- ...
+-- db/
| +-- migrations
| | +-- 20220330110026_create_users_table.sql
| +-- schema.sql
+-- Cargo.toml
+-- Cargo.lock
Database Access
SQLx is amazing. It's An async, pure Rust SQL crate featuring
compile-time checked queries without a DSL. Supports PostgreSQL,
MySQL, SQLite, and MSSQL.
Add the following to your app/Cargo.toml
sqlx = { version = "0", default-features = false, features = [ "runtime-tokio-rustls", "postgres", "macros", "chrono" ] }
Add the following code to a file called app/src/models/user.rs
use sqlx::{Error, PgPool};
pub struct User {
pub id: i32,
pub email: String,
}
impl User {
pub async fn get_users(pool: &PgPool, user_id: u32) -> Result, Error> {
Ok(sqlx::query_as!(
User,
"
SELECT
id, email
FROM
users
WHERE
id < $1
",
user_id as i32
)
.fetch_all(pool)
.await?)
}
}
You'll also need to create a file called app/src/models/mod.rs and
add the following
pub mod user;
We recommend creating a folder called models and adding a file for
each type of entity you will manipulate with SQLx.
.
+-- .devcontainer/
| +-- ...
+-- app
| +-- src/
| | +-- models/
| | | +-- main.rs
| | | +-- mod.rs
| | +-- main.rs
| | +-- config.rs
+-- db/
| +-- ...
+-- Cargo.toml
+-- Cargo.lock
Now we can tie our database and front end together. Update your app/
src/main.rs to become
mod config;
mod error;
mod models;
use axum::extract::Extension;
use axum::{response::Html, routing::get, Router};
use sqlx::PgPool;
use std::net::SocketAddr;
#[tokio::main]
async fn main() {
let config = config::Config::new();
let db_pool = PgPool::connect(&config.database_url)
.await
.expect("Problem connecting to the database");
// build our application with a route
let app = Router::new()
.route("/", get(handler))
.layer(Extension(db_pool))
.layer(Extension(config));
// run it
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
println!("listening on {}", addr);
axum::Server::bind(&addr)
.serve(app.into_make_service())
.await
.unwrap();
}
async fn handler(Extension(pool): Extension) -> Result, error::CustomError> {
let users = models::user::User::get_users(&pool, 10).await?;
let html = format!("Hello, World! We Have {} Users
", users.len());
Ok(Html(html))
}
The main thing to note here is that our handler gets passed in a
database connection auto magically by the framework.
We also need to create a place to hold our application errors. Create
a file called app/src/error.rs like so.
use axum::response::{IntoResponse, Response};
use axum::http::StatusCode;
#[derive(Debug)]
pub enum CustomError {
Database(String),
}
// So that errors get printed to the browser?
impl IntoResponse for CustomError {
fn into_response(self) -> Response {
let (status, error_message) = match self {
CustomError::Database(message) => (StatusCode::UNPROCESSABLE_ENTITY, message),
};
format!("status = {}, message = {}", status, error_message).into_response()
}
}
// Any errors from sqlx get converted to CustomError
impl From for CustomError {
fn from(err: sqlx::Error) -> CustomError {
CustomError::Database(err.to_string())
}
}
Execute cargo run and you can access the application from http://
localhost:3000 in your browser.
HTML Templating
Because Rust supports macros we can create a DSL to handle our
templating needs. This means we can leverage the Rust we already know
to create loops and if statements.
We will use markup.rs. Add the following to your app/src/Cargo.toml.
markup = "0"
Markup templating syntax looks like what we see below. We can re-use
our existing Rust knowledge for conditions and loops and get compile
time help at the same time.
markup::define! {
Page<'a>(user: &'a User, posts: &'a [Post]) {
@markup::doctype()
html {
head {
title { "Hello " @user.name }
}
body {
#main.container {
@for post in *posts {
div#{format!("post-{}", post.id)}["data-id" = post.id] {
.title { @post.title }
}
}
}
@Footer { name: &user.name, year: 2020 }
}
}
}
Footer<'a>(name: &'a str, year: u32) {
"(c) " @year " " @name
}
}
Forms
Axum has support for Handlers. We can use those in a lot of different
ways and one way is for form implementations. The example below shows
how we use a struct to handle the form data passed in to the
accept_form function.
use axum::{
extract::Form,
handler::post,
Router,
};
use serde::Deserialize;
#[derive(Deserialize)]
struct SignUp {
username: String,
password: String,
}
async fn accept_form(form: Form) {
let sign_up: SignUp = form.0;
// ...
}
let app = Router::new().route("/sign_up", post(accept_form));
Asset Pipeline
The asset pipeline provides a framework to concatenate and minify or
compress JavaScript and CSS assets. It also adds the ability to write
these assets in other languages and pre-processors such as Typescript
and Sass.
I've used Parcel on several projects and before that Webpack. I've
found Parcel to be easier to use and so that is the recommendation
for Nails.
To install parcel
$ cd app
$ npm install --save-dev parcel
Now create an app/asset-pipeline/index.ts
import './scss/index.scss'
And also app/asset-pipeline/scss/index.scss
h1 {
color: red;
}
Add a scripts section to your package.json
"scripts": {
"start": "parcel watch ./asset-pipeline/index.ts",
"release": "parcel build ./asset-pipeline/index.ts"
},
And now when you run npm run start parcel will generate your assets
into the dist folder. We should also update our ./.gitignore to
exclude the generated files.
/target
.parcel-cache
/app/dist
node_modules
Create an empty images folder in app/asset-pipeline/images then your
project should now look something like this.
.
+-- .devcontainer/
| +-- ...
+-- app
| +-- .parcel-cache/ <-- Created by parcel for caching
| | +-- ...
| +-- asset-pipeline/
| | +-- images/
| | | +-- ...
| | +-- scss/
| | | +-- index.scss
| | +-- index.ts
| +-- dist/ <-- Where parcel builds your assets
| | +-- ...
| +-- node_modules/
| | +-- ...
| +-- src/
| | +-- ...
| +-- Cargo.toml
| +-- package-lock.json
| +-- package.json
+-- db/
| +-- ...
+-- .gitignore
+-- Cargo.toml
+-- Cargo.lock
Cache Busting and Images
Cache busting is where we invalidate a cached file and force the
browser to retrieve the file from the server. We can instruct the
browser to bypass the cache by simply changing the filename. To the
browser, this is a completely new resource so it will fetch the
resource from the server. The most common way to do this is to add
the hash of the file to the URL.
What we can do with Rust is take advantage of the build.rs mechanism
which runs some code before each compile. We can generate a set of
function that let us retrieve our assets and generate the necessary
hashes at the same time. So for example to use the index.css in our
code it would be nice to be able to call something like.
get_index_css() // Returns the URL with the hash.
The code for this is quite large so I won't publish it here. Please
check out https://github.com/purton-tech/cloak/blob/main/app/build.rs
for a full implementation.
You'll also need to add the following to your app/Cargo.toml
tower-http = { version = "0", default-features = false, features = ["fs", "trace"] }
[build-dependencies]
sha1 = "0" # Use by build.rs for cache busting.
Now when your build your project a helper class will be created which
we will use in the next section.
Layouts
Layouts are pieces that fit together (for example header, footer,
menus, etc) to make a complete view. An application may have as many
layouts as needed.
In Nails a layout is just a function that takes HTML content and
returns more HTML content. Let's put together our cache busting
strategy with our asset pipeline into a Layout we can use.
Create app/src/layout.rs with the following
use axum::response::Html;
pub fn layout(title: &str, content: &str) -> Html {
let layout = Layout {
title,
content,
};
Html(layout.to_string())
}
markup::define! {
Layout<'a>(
title: &'a str,
content: &'a str,
)
{
@markup::doctype()
html[lang="en"] {
head {
meta [ charset="utf-8" ] {}
meta [ "http-equiv"="X-UA-Compatible", content="IE=edge"] {}
meta [ name="viewport", content="width=device-width, initial-scale=1" ] {}
title { {title} }
script [ src = crate::statics::get_index_js(), type="text/javascript", async=""] {}
link [ rel = "stylesheet", type="text/css" , href = crate::statics::get_index_css()] {}
}
body {
main {
{markup::raw(content)}
}
}
}
}
}
And we need to change our app/src/main.rs to include the routes for
our asset pipeline and to call out to the new layout.
mod config;
mod error;
mod models;
mod layout;
use axum::extract::Extension;
use axum::{response::Html, routing::get, Router};
use sqlx::PgPool;
use std::net::SocketAddr;
#[tokio::main]
async fn main() {
let config = config::Config::new();
let db_pool = PgPool::connect(&config.database_url)
.await
.expect("Problem connecting to the database");
// build our application with a route
let app = Router::new()
.route("/", get(handler))
.merge(statics::asset_pipeline_routes())
.merge(statics::image_routes())
.layer(Extension(db_pool))
.layer(Extension(config));
// run it
let addr = SocketAddr::from(([0, 0, 0, 0], 3000));
println!("listening on {}", addr);
axum::Server::bind(&addr)
.serve(app.into_make_service())
.await
.unwrap();
}
async fn handler(Extension(pool): Extension) -> Result, error::CustomError> {
let users = models::user::User::get_users(&pool, 10).await?;
let html = format!("Hello, World! We Have {} Users
", users.len());
Ok(layout::layout("Test", &html))
}
// Error here disabled with "rust-analyzer.diagnostics.disabled": ["macro-error"]
// in .vscode/settings.json
pub mod statics {
include!(concat!(env!("OUT_DIR"), "/statics.rs"));
}
Front end enhancement
Sometimes you need to enhance server side generated HTML. There are
many ways to do this for example Stimulus, which I've used on
multiple projects.
However all modern browser come with web components built in and as
they are pretty simple to use it makes sense to implement client side
enhancement using this technology.
An example of a very simple component create the following in app/src
/asset-pipelibne/components/hello_world.ts.
//define a class extending HTMLElement
class HelloWorld extends HTMLElement {
connectedCallback () {
this.innerHTML = 'Hello, World!'
}
}
//register the new custom element
customElements.define( 'hello-world', HelloWorld )
Include the element into your app/src/asset-pipeline/index.ts i.e.
import './scss/index.scss'
import './components/hello_world.ts'
To use the element
Partials and Components
Partials in Rails are a way to split your templating into more
manageable chunks. We can get the same thing in Rust by using
functions. So if you have a piece of template you repeat often, this
can be refactored into a function.
It's a similar process for components. With markup.rs we can create
use our existing templating to make libraries of reusable components.
Example 'Footer' component.
Footer<'a>(name: &'a str, year: u32) {
"(c) " @year " " @name
}
Sending Email
We can use the excellent library Lettre for sending emails. I
recommend installing Mailhog into your .devcontainer/
docker-compose.yml as an email catcher.
Simply add the following config.
# MailHog is an email testing tool for developers.
smtp:
image: mailhog/mailhog
Then update your .devcontainer/.env with some email configuration env
vars.
SMTP_HOST: smtp
SMTP_PORT: 1025
SMTP_USERNAME: thisisnotused
SMTP_PASSWORD: thisisnotused
SMTP_TLS_OFF: 'true'
We then update our app/src/config.rs.
use std::env;
#[derive(Clone, Debug)]
pub struct Config {
pub database_url: String,
// Configure SMTP for email.
pub smtp_config: Option,
}
#[derive(Clone, Debug)]
pub struct SmtpConfig {
// Configure SMTP for email.
pub host: String,
pub port: u16,
pub tls_off: bool,
pub username: String,
pub password: String,
}
impl SmtpConfig {
pub fn new() -> Option {
let host = env::var("SMTP_HOST");
let username = env::var("SMTP_USERNAME");
let password = env::var("SMTP_PASSWORD");
let smtp_port = env::var("SMTP_PORT");
if let (Ok(host), Ok(username), Ok(password), Ok(smtp_port)) =
(host, username, password, smtp_port)
{
Some(SmtpConfig {
host,
port: smtp_port.parse::().unwrap(),
tls_off: env::var("SMTP_TLS_OFF").is_ok(),
username,
password
})
} else {
None
}
}
}
impl Config {
// Initialise form oiur environment
pub fn new() -> Config {
let database_url =
env::var("DATABASE_URL")
.expect("DATABASE_URL not set");
Config {
database_url,
smtp_config: SmtpConfig::new(),
}
}
}
Create a helper function for email in app/src/email.rs.
use lettre::transport::smtp::authentication::Credentials;
use lettre::{Message, SmtpTransport, Transport};
pub fn send_email(config: &crate::config::Config, email: Message) {
if let Some(smtp_config) = &config.smtp_config {
let creds = Credentials::new(smtp_config.username.clone(), smtp_config.password.clone());
let sender = if smtp_config.tls_off {
SmtpTransport::builder_dangerous(smtp_config.host.clone())
.port(smtp_config.port)
.credentials(creds)
.build()
} else {
SmtpTransport::relay(&smtp_config.host)
.unwrap()
.port(smtp_config.port)
.credentials(creds)
.build()
};
// Send the email
match sender.send(&email) {
Ok(_) => println!("Email sent successfully!"),
Err(e) => panic!("Could not send email: {:?}", e),
}
}
}
We should now be able to form a message and call our helper function
and see the results in Mailhog.
Email Developers View
Authentication
Probably the quickest way to add authentication to an application is
with Barricade. Barricade handles login and registration pages and
connects to your Postgres database.
We've already created the tables that Barricade needs in the
migrations section. so we just need to add configuration
.devcontainer/docker-compose.yml.
auth:
image: purtontech/barricade:1.2.0
env_file:
- .env
depends_on:
db:
condition: service_healthy
We also need to add a health check to our db section so that we know
when the database is ready.
db:
...
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
Add the following to you .devcontainer/.env
# Barricade config
SECRET_KEY=190a5bf4b3cbb6c0991967ab1c48ab30790af876720f1835cbbf3820f4f5d949
ENABLE_EMAIL_OTP='true'
FORWARD_URL=app
FORWARD_PORT=3000
# Any requests that meet the following regular expressions
# with pass through. i.e. They don't require auth.
SKIP_AUTH_FOR=/static*
REDIRECT_URL='/'
# Send all email to mailhog
SMTP_HOST=smtp
SMTP_PORT=1025
SMTP_USERNAME=thisisnotused
SMTP_PASSWORD=thisisnotused
SMTP_TLS_OFF='true'
RESET_DOMAIN=http://localhost:7100
RESET_FROM_EMAIL_ADDRESS=[email protected]
After rebuilding your devcontainer you will need to register as a
user. Make sure you server is running again i.e.
$ cd app
$ cargo run
Expose port 9090 from your devcontainer then go to http://
localhost:9090 and sign up.
Barricade
Integration Tests
Integration tests are used to test application from top to bottom.
That means simulating the browser across important workflows as if it
was a real user. We will use Selenium as our headless browser.
Add the Selenium docker container to .devcontainer/docker-compose.yml
and restart your devcontainer. Note the No VNC and VNC comments, the
selenium container allows us to connect via VNC to the container so
we can actually see the browser as it performs the tests. The No VNC
port means we don't even have to install VNC. You can connect with a
browser to this port and use the No VNC browser client.
# Integration testing using a headless chrome browser
selenium:
image: selenium/standalone-chrome:4.1.1-20220121
shm_size: 2gb
environment:
VNC_NO_PASSWORD: 1
ports:
# VNC
- 5900:5900
# No VNC
- 7900:7900
We can write our tests in Rust using ThirtyFour which is a Selenium /
WebDriver library for Rust, for automated website UI testing.
Add the following to bottom of app/Cargo.toml.
[dev-dependencies]
# WebDriver Library for UI testing.
thirtyfour = { version = "0", default-features = false, features = [ "reqwest-rustls-tls", "tokio-runtime" ] }
We need a helper class to configure our selenium driver. Add the
following to app/tests/config.rs.
use std::env;
use thirtyfour::prelude::*;
#[derive(Clone, Debug)]
pub struct Config {
pub webdriver_url: String,
pub host: String,
}
impl Config {
pub async fn new() -> Config {
let webdriver_url: String = if env::var("WEB_DRIVER_URL").is_ok() {
env::var("WEB_DRIVER_URL").unwrap()
} else {
// Default to selenium in our dev container
"http://selenium:4444".into()
};
let host = if env::var("WEB_DRIVER_DESTINATION_HOST").is_ok() {
env::var("WEB_DRIVER_DESTINATION_HOST").unwrap()
} else {
"http://auth:9090".into()
};
Config {
webdriver_url,
host,
}
}
pub async fn get_driver(&self) -> WebDriverResult {
let mut caps = DesiredCapabilities::chrome();
caps.add_chrome_arg("--no-sandbox")?;
caps.add_chrome_arg("--disable-gpu")?;
caps.add_chrome_arg("--start-maximized")?;
WebDriver::new(&self.webdriver_url, &caps).await
}
}
Create the following example test in app/tests/example_test.rs.
pub mod config;
use thirtyfour::prelude::*;
// let's set up the sequence of steps we want the browser to take
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn run_test() -> WebDriverResult<()> {
let config = config::Config::new().await;
let driver = config.get_driver().await?;
let result = test(&driver, &config).await;
driver.quit().await?;
result?;
Ok(())
}
async fn test(driver: &WebDriver, config: &config::Config) -> WebDriverResult<()> {
let delay = std::time::Duration::new(11, 0);
driver.set_implicit_wait_timeout(delay).await?;
driver.get(&config.host).await?;
driver
.find_element(By::Id("email"))
.await?
.send_keys("[email protected]")
.await?;
Ok(())
}
Point your browser at http://localhost:7900 to view the tests. Run
the test from the app folder with cargo test
Production Example Cloak Integration Tests and Cloak
docker-compose.yml
Start mananging your secrets today for free
Sign Up
Resources
* Blog
* Documentation
* FAQ
* Status
* Pricing
Resources
* Mission
* Careers
* Contact
* Press Kit
Sign up for our service
Get control of your secrets and add security.
Check it Out
---------------------------------------------------------------------
Copyright 2016 - 2022
* [gi]
* Terms
* Privacy