Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test(webserver): add smtp local unittest #1434

Merged
merged 2 commits into from
Feb 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/docker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ concurrency:
cancel-in-progress: true

env:
RUST_TOOLCHAIN: 1.73.0
RUST_TOOLCHAIN: 1.76.0

jobs:
release-docker:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ concurrency:
cancel-in-progress: true

env:
RUST_TOOLCHAIN: 1.73.0
RUST_TOOLCHAIN: 1.76.0

jobs:
release-binary:
Expand Down
2 changes: 1 addition & 1 deletion ci/prepare_build_environment.ps1
Original file line number Diff line number Diff line change
@@ -1 +1 @@
choco install --yes protoc
choco install --yes protoc
8 changes: 8 additions & 0 deletions ci/prepare_build_environment.sh
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,11 @@ if [[ "$OSTYPE" == "linux"* ]]; then
install_protobuf_centos
fi
fi


install_mailtutan() {
# For local smtp test.
cargo install mailtutan
}

install_mailtutan
Original file line number Diff line number Diff line change
Expand Up @@ -233,10 +233,9 @@ export const MailForm: React.FC<MailFormProps> = ({
<FormLabel required>SMTP Username</FormLabel>
<FormControl>
<Input
type="email"
type="string"
placeholder="[email protected]"
autoCapitalize="none"
autoComplete="email"
autoCorrect="off"
className="w-80 min-w-max"
{...field}
Expand Down
2 changes: 1 addition & 1 deletion ee/tabby-webserver/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,4 @@ validator = { version = "0.16.1", features = ["derive"] }
[dev-dependencies]
assert_matches = "1.5.0"
tokio = { workspace = true, features = ["macros"] }
tabby-db = { path = "../../ee/tabby-db", features = ["testutils"] }
tabby-db = { path = "../../ee/tabby-db", features = ["testutils"] }
7 changes: 4 additions & 3 deletions ee/tabby-webserver/src/schema/email.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use anyhow::Result;
use async_trait::async_trait;
use juniper::{GraphQLEnum, GraphQLInputObject, GraphQLObject};
use tokio::task::JoinHandle;
use validator::Validate;

#[derive(GraphQLEnum, Clone, Debug)]
Expand All @@ -17,7 +18,7 @@
Login,
}

#[derive(GraphQLObject)]
#[derive(GraphQLObject, Clone)]

Check warning on line 21 in ee/tabby-webserver/src/schema/email.rs

View check run for this annotation

Codecov / codecov/patch

ee/tabby-webserver/src/schema/email.rs#L21

Added line #L21 was not covered by tests
pub struct EmailSetting {
pub smtp_username: String,
pub smtp_server: String,
Expand All @@ -41,9 +42,9 @@

#[async_trait]
pub trait EmailService: Send + Sync {
async fn get_email_setting(&self) -> Result<Option<EmailSetting>>;
async fn read_email_setting(&self) -> Result<Option<EmailSetting>>;
async fn update_email_setting(&self, input: EmailSettingInput) -> Result<()>;
async fn delete_email_setting(&self) -> Result<()>;

async fn send_invitation_email(&self, email: String, code: String) -> Result<()>;
async fn send_invitation_email(&self, email: String, code: String) -> Result<JoinHandle<()>>;
}
2 changes: 1 addition & 1 deletion ee/tabby-webserver/src/schema/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ impl Query {
"Only admin can access server settings",
));
};
let val = ctx.locator.email().get_email_setting().await?;
let val = ctx.locator.email().read_email_setting().await?;
Ok(val)
}

Expand Down
157 changes: 117 additions & 40 deletions ee/tabby-webserver/src/service/email.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@
client::{Tls, TlsParameters},
AsyncSmtpTransportBuilder,
},
Address, AsyncSmtpTransport, AsyncTransport, Tokio1Executor, Transport,
Address, AsyncSmtpTransport, AsyncTransport, Tokio1Executor,
};
use tabby_db::{DbConn, DbEnum};
use tokio::sync::RwLock;
use tokio::{sync::RwLock, task::JoinHandle};
use tracing::warn;

use crate::schema::{
Expand Down Expand Up @@ -57,6 +57,14 @@
}

impl EmailServiceImpl {
fn new(db: DbConn) -> Self {
Self {
db,
smtp_server: Default::default(),
from: Default::default(),
}
}

/// (Re)initialize the SMTP server connection using new credentials
async fn reset_smtp_connection(
&self,
Expand All @@ -69,12 +77,15 @@
auth_method: AuthMethod,
) -> Result<()> {
let mut smtp_server = self.smtp_server.write().await;
*smtp_server = Some(
make_smtp_builder(host, port as u16, encryption)?

let mut builder = make_smtp_builder(host, port as u16, encryption)?;
let mechanism = auth_mechanism(auth_method);
if !mechanism.is_empty() {
builder = builder

Check warning on line 84 in ee/tabby-webserver/src/service/email.rs

View check run for this annotation

Codecov / codecov/patch

ee/tabby-webserver/src/service/email.rs#L84

Added line #L84 was not covered by tests
.credentials(Credentials::new(username, password))
.authentication(auth_mechanism(auth_method))
.build(),
);
.authentication(mechanism);

Check warning on line 86 in ee/tabby-webserver/src/service/email.rs

View check run for this annotation

Codecov / codecov/patch

ee/tabby-webserver/src/service/email.rs#L86

Added line #L86 was not covered by tests
}
*smtp_server = Some(builder.build());
*self.from.write().await = from_address.into();
Ok(())
}
Expand All @@ -84,49 +95,42 @@
*self.smtp_server.write().await = None;
}

async fn send_mail_in_background(
async fn send_email_in_background(
&self,
to: String,
subject: String,
message: String,
) -> Result<()> {
) -> Result<JoinHandle<()>> {
let smtp_server = self.smtp_server.clone();
let from = self.from.read().await.clone();
let address_from = to_address(from)?;
let address_to = to_address(to)?;
let msg = MessageBuilder::new()
.subject(subject)
.from(Mailbox::new(Some("Tabby Server".to_owned()), address_from))
.from(Mailbox::new(Some("Tabby Admin".to_owned()), address_from))
.to(Mailbox::new(None, address_to))
.body(message)
.map_err(anyhow::Error::msg)?;

tokio::spawn(async move {
let Some(smtp_server) = &*(smtp_server.read().await) else {
// Not enabled.
return;
};
if let Some(smtp_server) = &*(smtp_server.read().await) {
// Not enabled.
match smtp_server.send(msg).await.map_err(anyhow::Error::msg) {
Ok(_) => {}
Err(err) => {
warn!("Failed to send mail due to {}", err);
}
};
});

Ok(())
}

Check warning on line 123 in ee/tabby-webserver/src/service/email.rs

View check run for this annotation

Codecov / codecov/patch

ee/tabby-webserver/src/service/email.rs#L123

Added line #L123 was not covered by tests
Ok(tokio::spawn(async move {}))
}
}

pub async fn new_email_service(db: DbConn) -> Result<impl EmailService> {
let creds = db.read_email_setting().await?;
let service = EmailServiceImpl {
db,
smtp_server: Default::default(),
from: Default::default(),
};
let setting = db.read_email_setting().await?;
let service = EmailServiceImpl::new(db);

// Optionally initialize the SMTP connection when the service is created
if let Some(setting) = creds {
if let Some(setting) = setting {
let encryption = Encryption::from_enum_str(&setting.encryption)?;
let auth_method = AuthMethod::from_enum_str(&setting.auth_method)?;
service
Expand All @@ -146,18 +150,18 @@

#[async_trait]
impl EmailService for EmailServiceImpl {
async fn get_email_setting(&self) -> Result<Option<EmailSetting>> {
let creds = self.db.read_email_setting().await?;
let Some(creds) = creds else {
async fn read_email_setting(&self) -> Result<Option<EmailSetting>> {
let setting = self.db.read_email_setting().await?;
let Some(setting) = setting else {
return Ok(None);
};
let creds = creds.try_into();
let Ok(creds) = creds else {
let setting = setting.try_into();
let Ok(setting) = setting else {
self.db.delete_email_setting().await?;
warn!("Email settings are corrupt, and have been deleted. Please reset them.");
return Ok(None);
};
Ok(Some(creds))
Ok(Some(setting))
}

async fn update_email_setting(&self, input: EmailSettingInput) -> Result<()> {
Expand Down Expand Up @@ -203,10 +207,10 @@
Ok(())
}

async fn send_invitation_email(&self, email: String, code: String) -> Result<()> {
async fn send_invitation_email(&self, email: String, code: String) -> Result<JoinHandle<()>> {
let network_setting = self.db.read_network_setting().await?;
let external_url = network_setting.external_url;
self.send_mail_in_background(
self.send_email_in_background(
email,
"You've been invited to join a Tabby workspace!".into(),
format!("Welcome to Tabby! You have been invited to join a Tabby Server, where you can tap into AI-driven code completions and chat assistants.\n\nGo to {external_url}/auth/signup?invitationCode={code} to join!"),
Expand All @@ -223,30 +227,103 @@

#[cfg(test)]
mod tests {
use std::time::Duration;

use serde::Deserialize;
use tokio::process::{Child, Command};

use super::*;

#[tokio::test]
async fn test_update_email_with_service() {
let db: DbConn = DbConn::new_in_memory().await.unwrap();
let service = EmailServiceImpl {
db,
smtp_server: Default::default(),
from: Default::default(),
};
let service = EmailServiceImpl::new(db);

let update_input = EmailSettingInput {
smtp_username: "[email protected]".into(),
from_address: "test".into(),
smtp_server: "smtp://example.com".into(),
smtp_port: 578,
encryption: Encryption::SslTls,
auth_method: AuthMethod::Plain,
auth_method: AuthMethod::None,
smtp_password: Some("123456".to_owned()),
};
service.update_email_setting(update_input).await.unwrap();
let setting = service.get_email_setting().await.unwrap().unwrap();
let setting = service.read_email_setting().await.unwrap().unwrap();
assert_eq!(setting.smtp_username, "[email protected]");

service.delete_email_setting().await.unwrap();
}

fn default_email_setting() -> EmailSetting {
EmailSetting {
smtp_username: "tabby".into(),
smtp_server: "127.0.0.1".into(),
smtp_port: 1025,
from_address: "tabby@localhost".into(),
encryption: Encryption::None,
auth_method: AuthMethod::None,
}
}

async fn start_smtp_server() -> Child {
let mut cmd = Command::new("mailtutan");
cmd.kill_on_drop(true);

let child = cmd
.spawn()
.expect("You need to run `cargo install mailtutan` before running this test");
tokio::time::sleep(Duration::from_secs(1)).await;
child
}

#[derive(Deserialize)]
struct Mail {
sender: String,
}

async fn read_mails() -> Vec<Mail> {
reqwest::get("http://localhost:1080/api/messages")
.await
.unwrap()
.json()
.await

Check warning on line 290 in ee/tabby-webserver/src/service/email.rs

View check run for this annotation

Codecov / codecov/patch

ee/tabby-webserver/src/service/email.rs#L290

Added line #L290 was not covered by tests
.unwrap()
}

/*
* Requires https://github.com/mailtutan/mailtutan
*/
#[tokio::test]
async fn test_send_email() {
let email_setting = default_email_setting();
let smtp_password = "fake";
let mut child = start_smtp_server().await;

let db: DbConn = DbConn::new_in_memory().await.unwrap();
db.update_email_setting(
email_setting.smtp_username,
Some(smtp_password.into()),
email_setting.smtp_server,
email_setting.smtp_port,
email_setting.from_address.clone(),
email_setting.encryption.as_enum_str().into(),
email_setting.auth_method.as_enum_str().into(),
)
.await
.unwrap();

let service = new_email_service(db).await.unwrap();

let handle = service
.send_invitation_email("user@localhost".into(), "12345".into())
.await
.unwrap();

handle.await.unwrap();

let mails = read_mails().await;
assert!(mails[0].sender.contains(&email_setting.from_address));
child.kill().await.unwrap();
}
}
Loading