mirror of
https://github.com/stalwartlabs/proxy.git
synced 2026-11-01 00:12:55 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3a02690854 |
@@ -2,6 +2,15 @@
|
||||
|
||||
All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/).
|
||||
|
||||
## [1.0.3] - 2026-08-30
|
||||
|
||||
### Added
|
||||
|
||||
### Changed
|
||||
|
||||
### Fixed
|
||||
- IMAP authentication is no longer replayed with an inline initial response to backends that do not advertise `SASL-IR` (#4).
|
||||
|
||||
## [1.0.2] - 2026-07-31
|
||||
|
||||
### Added
|
||||
|
||||
Generated
+311
-245
File diff suppressed because it is too large
Load Diff
+2
-3
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "proxy"
|
||||
version = "1.0.2"
|
||||
version = "1.0.3"
|
||||
edition = "2024"
|
||||
license = "AGPL-3.0-only OR LicenseRef-SEL"
|
||||
description = "Multi-protocol e-mail proxy"
|
||||
@@ -49,8 +49,7 @@ rustls-platform-verifier = "0.7"
|
||||
[dev-dependencies]
|
||||
tempfile = "3"
|
||||
mockito = "1.7"
|
||||
testcontainers = "0.27"
|
||||
testcontainers-modules = { version = "0.15", features = ["redis", "postgres"] }
|
||||
testcontainers = "0.28"
|
||||
pwhash = "1"
|
||||
jmap-client = { version = "0.4", default-features = false, features = ["websockets", "async", "aws_lc_rs"] }
|
||||
futures-util = "0.3"
|
||||
|
||||
+92
-4
@@ -623,6 +623,55 @@ pub async fn hold_login(ctx: &Ctx, identifier: Option<&str>) {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct BackendLeg {
|
||||
pub stream: BoxedStream,
|
||||
pub residual: Vec<u8>,
|
||||
pub accepts_initial_response: bool,
|
||||
}
|
||||
|
||||
impl BackendLeg {
|
||||
pub fn new(stream: BoxedStream, residual: Vec<u8>) -> Self {
|
||||
Self {
|
||||
stream,
|
||||
residual,
|
||||
accepts_initial_response: true,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_initial_response(mut self, accepted: bool) -> Self {
|
||||
self.accepts_initial_response = accepted;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AuthFrame {
|
||||
inline: Vec<u8>,
|
||||
deferred: Option<(Vec<u8>, Vec<u8>)>,
|
||||
}
|
||||
|
||||
impl AuthFrame {
|
||||
pub fn inline(frame: Vec<u8>) -> Self {
|
||||
Self {
|
||||
inline: frame,
|
||||
deferred: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_deferred(mut self, command: Vec<u8>, response: Vec<u8>) -> Self {
|
||||
self.deferred = Some((command, response));
|
||||
self
|
||||
}
|
||||
|
||||
fn wire(&self, accepts_initial_response: bool) -> (&[u8], Option<&[u8]>) {
|
||||
match &self.deferred {
|
||||
Some((command, response)) if !accepts_initial_response => {
|
||||
(command.as_slice(), Some(response.as_slice()))
|
||||
}
|
||||
_ => (self.inline.as_slice(), None),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub enum AuthWire {
|
||||
Imap { tag: String },
|
||||
Pop3,
|
||||
@@ -715,6 +764,7 @@ pub async fn complete_backend_auth(
|
||||
backend: &mut BoxedStream,
|
||||
residual: &mut Vec<u8>,
|
||||
wire: AuthWire,
|
||||
mut pending_response: Option<&[u8]>,
|
||||
hide: bool,
|
||||
max: usize,
|
||||
) -> Result<bool> {
|
||||
@@ -723,7 +773,8 @@ pub async fn complete_backend_auth(
|
||||
let line = crate::outbound::read_line(backend, residual, max).await?;
|
||||
match classify_auth_line(&line, &wire) {
|
||||
AuthLine::Continuation => {
|
||||
crate::outbound::write_all(backend, SASL_EMPTY_RESPONSE).await?;
|
||||
let response = pending_response.take().unwrap_or(SASL_EMPTY_RESPONSE);
|
||||
crate::outbound::write_all(backend, response).await?;
|
||||
}
|
||||
AuthLine::Passthrough => {
|
||||
client_out.extend_from_slice(&line);
|
||||
@@ -763,14 +814,14 @@ pub async fn bridge_authenticated<F, Fut>(
|
||||
started: Instant,
|
||||
label: &'static str,
|
||||
wire: AuthWire,
|
||||
auth_frame: &[u8],
|
||||
auth: &AuthFrame,
|
||||
unavailable: &[u8],
|
||||
max: usize,
|
||||
open: F,
|
||||
) -> Result<()>
|
||||
where
|
||||
F: FnMut() -> Fut,
|
||||
Fut: Future<Output = Result<(BoxedStream, Vec<u8>)>>,
|
||||
Fut: Future<Output = Result<BackendLeg>>,
|
||||
{
|
||||
if let Some(id) = identifier
|
||||
&& !crate::token::valid_routing_identifier(id)
|
||||
@@ -784,7 +835,11 @@ where
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let (mut backend, mut backend_residual) = match establish(ctx, dest_id, peer, open).await {
|
||||
let BackendLeg {
|
||||
stream: mut backend,
|
||||
residual: mut backend_residual,
|
||||
accepts_initial_response,
|
||||
} = match establish(ctx, dest_id, peer, open).await {
|
||||
Ok(b) => b,
|
||||
Err(e) => {
|
||||
tracing::warn!(error = %e, protocol = label, destination = %dest_id, "backend unavailable");
|
||||
@@ -793,6 +848,7 @@ where
|
||||
}
|
||||
};
|
||||
|
||||
let (auth_frame, pending_response) = auth.wire(accepts_initial_response);
|
||||
crate::outbound::write_all(&mut backend, auth_frame).await?;
|
||||
|
||||
let hide = ctx.hide_auth_errors(dest_id);
|
||||
@@ -803,6 +859,7 @@ where
|
||||
&mut backend,
|
||||
&mut backend_residual,
|
||||
wire,
|
||||
pending_response,
|
||||
hide,
|
||||
max,
|
||||
),
|
||||
@@ -910,6 +967,37 @@ pub async fn read_crlf_line(
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn auth_frame_defers_initial_response_when_not_accepted() {
|
||||
let frame = AuthFrame::inline(b"a1 AUTHENTICATE PLAIN AGFAYgBj\r\n".to_vec())
|
||||
.with_deferred(
|
||||
b"a1 AUTHENTICATE PLAIN\r\n".to_vec(),
|
||||
b"AGFAYgBj\r\n".to_vec(),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
frame.wire(true),
|
||||
(b"a1 AUTHENTICATE PLAIN AGFAYgBj\r\n".as_slice(), None)
|
||||
);
|
||||
assert_eq!(
|
||||
frame.wire(false),
|
||||
(
|
||||
b"a1 AUTHENTICATE PLAIN\r\n".as_slice(),
|
||||
Some(b"AGFAYgBj\r\n".as_slice())
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn auth_frame_without_deferred_form_stays_inline() {
|
||||
let frame = AuthFrame::inline(b"AUTH PLAIN AGFAYgBj\r\n".to_vec());
|
||||
|
||||
assert_eq!(
|
||||
frame.wire(false),
|
||||
(b"AUTH PLAIN AGFAYgBj\r\n".as_slice(), None)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn advertises_token_handles_smtp_ehlo_continuation_lines() {
|
||||
let ehlo = b"250-mail.example\r\n250-PIPELINING\r\n250-STARTTLS\r\n250-AUTH PLAIN LOGIN\r\n250 CHUNKING\r\n";
|
||||
|
||||
+13
-7
@@ -78,10 +78,14 @@ pub async fn handle(
|
||||
let client_tls = stream.is_tls();
|
||||
let fwd = forward::ForwardInfo::new(peer, local, client_tls, None, ctx.config.server.proxy_ttl);
|
||||
|
||||
let auth_frame = format!(
|
||||
"{tag} AUTHENTICATE {} {}\r\n",
|
||||
mechanism_name(&mech),
|
||||
BASE64.encode(&raw_challenge)
|
||||
let mech_name = mechanism_name(&mech);
|
||||
let response = BASE64.encode(&raw_challenge);
|
||||
let auth = common::AuthFrame::inline(
|
||||
format!("{tag} AUTHENTICATE {mech_name} {response}\r\n").into_bytes(),
|
||||
)
|
||||
.with_deferred(
|
||||
format!("{tag} AUTHENTICATE {mech_name}\r\n").into_bytes(),
|
||||
format!("{response}\r\n").into_bytes(),
|
||||
);
|
||||
let unavailable = format!("{tag} NO [UNAVAILABLE] backend unavailable\r\n");
|
||||
|
||||
@@ -95,7 +99,7 @@ pub async fn handle(
|
||||
started,
|
||||
"imap",
|
||||
common::AuthWire::Imap { tag },
|
||||
auth_frame.as_bytes(),
|
||||
&auth,
|
||||
unavailable.as_bytes(),
|
||||
MAX_PREAUTH,
|
||||
|| open_backend(ctx, &dest_id, &fwd),
|
||||
@@ -477,7 +481,7 @@ async fn open_backend(
|
||||
ctx: &Ctx,
|
||||
dest_id: &str,
|
||||
fwd: &forward::ForwardInfo,
|
||||
) -> Result<(BoxedStream, Vec<u8>)> {
|
||||
) -> Result<common::BackendLeg> {
|
||||
let common::DialedEndpoint {
|
||||
dest,
|
||||
ep,
|
||||
@@ -490,6 +494,7 @@ async fn open_backend(
|
||||
let mut residual = Vec::new();
|
||||
|
||||
let banner = outbound::read_line(&mut backend, &mut residual, MAX_PREAUTH).await?;
|
||||
let mut sasl_ir = common::advertises_token(&banner, "SASL-IR");
|
||||
|
||||
if ep.tls == TlsMode::Starttls {
|
||||
if banner.contains(&b'[') && !common::advertises_token(&banner, "STARTTLS") {
|
||||
@@ -500,6 +505,7 @@ async fn open_backend(
|
||||
if !tagged_ok(&resp, b"P1") {
|
||||
return Err(ProxyError::backend("backend refused STARTTLS"));
|
||||
}
|
||||
sasl_ir = common::advertises_token(&resp, "SASL-IR");
|
||||
backend = outbound::tls_connect(backend, &tls_cfg, dest).await?;
|
||||
}
|
||||
|
||||
@@ -509,7 +515,7 @@ async fn open_backend(
|
||||
forward::send_imap_id(&mut backend, &mut residual, &banner, fwd, MAX_PREAUTH).await?;
|
||||
}
|
||||
|
||||
Ok((backend, residual))
|
||||
Ok(common::BackendLeg::new(backend, residual).with_initial_response(sasl_ir))
|
||||
}
|
||||
|
||||
fn tagged_ok(line: &[u8], tag: &[u8]) -> bool {
|
||||
|
||||
@@ -66,10 +66,13 @@ pub async fn handle(
|
||||
ctx.config.server.proxy_ttl,
|
||||
);
|
||||
|
||||
let auth_frame = format!(
|
||||
"AUTHENTICATE \"{}\" \"{}\"\r\n",
|
||||
mechanism_name(&mech),
|
||||
BASE64.encode(&raw_challenge)
|
||||
let auth = common::AuthFrame::inline(
|
||||
format!(
|
||||
"AUTHENTICATE \"{}\" \"{}\"\r\n",
|
||||
mechanism_name(&mech),
|
||||
BASE64.encode(&raw_challenge)
|
||||
)
|
||||
.into_bytes(),
|
||||
);
|
||||
|
||||
common::bridge_authenticated(
|
||||
@@ -82,7 +85,7 @@ pub async fn handle(
|
||||
started,
|
||||
"managesieve",
|
||||
common::AuthWire::ManageSieve,
|
||||
auth_frame.as_bytes(),
|
||||
&auth,
|
||||
b"NO \"backend unavailable\"\r\n",
|
||||
MAX_PREAUTH,
|
||||
|| open_backend(ctx, &dest_id, &fwd),
|
||||
@@ -468,7 +471,7 @@ async fn open_backend(
|
||||
ctx: &Ctx,
|
||||
dest_id: &str,
|
||||
fwd: &forward::ForwardInfo,
|
||||
) -> Result<(BoxedStream, Vec<u8>)> {
|
||||
) -> Result<common::BackendLeg> {
|
||||
let common::DialedEndpoint {
|
||||
dest,
|
||||
ep,
|
||||
@@ -509,7 +512,7 @@ async fn open_backend(
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok((backend, residual))
|
||||
Ok(common::BackendLeg::new(backend, residual))
|
||||
}
|
||||
|
||||
async fn read_capabilities(stream: &mut BoxedStream, residual: &mut Vec<u8>) -> Result<Vec<u8>> {
|
||||
|
||||
+10
-7
@@ -65,10 +65,13 @@ pub async fn handle(
|
||||
ctx.config.server.proxy_ttl,
|
||||
);
|
||||
|
||||
let auth_frame = format!(
|
||||
"AUTH {} {}\r\n",
|
||||
mechanism_name(&mech),
|
||||
BASE64.encode(&raw_challenge)
|
||||
let auth = common::AuthFrame::inline(
|
||||
format!(
|
||||
"AUTH {} {}\r\n",
|
||||
mechanism_name(&mech),
|
||||
BASE64.encode(&raw_challenge)
|
||||
)
|
||||
.into_bytes(),
|
||||
);
|
||||
|
||||
common::bridge_authenticated(
|
||||
@@ -81,7 +84,7 @@ pub async fn handle(
|
||||
started,
|
||||
"pop3",
|
||||
common::AuthWire::Pop3,
|
||||
auth_frame.as_bytes(),
|
||||
&auth,
|
||||
b"-ERR backend unavailable\r\n",
|
||||
MAX_PREAUTH,
|
||||
|| open_backend(ctx, &dest_id, &fwd),
|
||||
@@ -404,7 +407,7 @@ async fn open_backend(
|
||||
ctx: &Ctx,
|
||||
dest_id: &str,
|
||||
fwd: &forward::ForwardInfo,
|
||||
) -> Result<(BoxedStream, Vec<u8>)> {
|
||||
) -> Result<common::BackendLeg> {
|
||||
let common::DialedEndpoint {
|
||||
dest,
|
||||
ep,
|
||||
@@ -447,7 +450,7 @@ async fn open_backend(
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok((backend, residual))
|
||||
Ok(common::BackendLeg::new(backend, residual))
|
||||
}
|
||||
|
||||
async fn backend_advertises_stls(
|
||||
|
||||
+10
-7
@@ -74,10 +74,13 @@ pub async fn handle_submission(
|
||||
ctx.config.server.proxy_ttl,
|
||||
);
|
||||
|
||||
let auth_frame = format!(
|
||||
"AUTH {} {}\r\n",
|
||||
mechanism_name(&mech),
|
||||
BASE64.encode(&raw_challenge)
|
||||
let auth = common::AuthFrame::inline(
|
||||
format!(
|
||||
"AUTH {} {}\r\n",
|
||||
mechanism_name(&mech),
|
||||
BASE64.encode(&raw_challenge)
|
||||
)
|
||||
.into_bytes(),
|
||||
);
|
||||
|
||||
common::bridge_authenticated(
|
||||
@@ -90,7 +93,7 @@ pub async fn handle_submission(
|
||||
started,
|
||||
"submission",
|
||||
common::AuthWire::Smtp,
|
||||
auth_frame.as_bytes(),
|
||||
&auth,
|
||||
b"421 4.3.0 backend unavailable\r\n",
|
||||
MAX_PREAUTH,
|
||||
|| open_backend(ctx, &dest_id, &fwd),
|
||||
@@ -523,7 +526,7 @@ async fn open_backend(
|
||||
ctx: &Ctx,
|
||||
dest_id: &str,
|
||||
fwd: &forward::ForwardInfo,
|
||||
) -> Result<(BoxedStream, Vec<u8>)> {
|
||||
) -> Result<common::BackendLeg> {
|
||||
let common::DialedEndpoint {
|
||||
dest,
|
||||
ep,
|
||||
@@ -568,7 +571,7 @@ async fn open_backend(
|
||||
}
|
||||
}
|
||||
|
||||
Ok((backend, residual))
|
||||
Ok(common::BackendLeg::new(backend, residual))
|
||||
}
|
||||
|
||||
fn advertised_xclient_attrs(ehlo_reply: &[u8]) -> Vec<String> {
|
||||
|
||||
+150
-1
@@ -57,7 +57,7 @@ async fn backend_expect_mech(
|
||||
mech: &'static str,
|
||||
want_user: &'static str,
|
||||
) {
|
||||
sock.write_all(b"* OK [CAPABILITY IMAP4rev2] backend ready\r\n")
|
||||
sock.write_all(b"* OK [CAPABILITY IMAP4rev2 SASL-IR] backend ready\r\n")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -652,3 +652,152 @@ tls = "starttls"
|
||||
);
|
||||
handler.abort();
|
||||
}
|
||||
|
||||
const EXCHANGE_GREETING: &[u8] =
|
||||
b"* OK [CAPABILITY IMAP4 IMAP4rev1 AUTH=PLAIN AUTH=NTLM AUTH=GSSAPI UIDPLUS MOVE ID \
|
||||
CHILDREN IDLE NAMESPACE LITERAL+] backend ready\r\n";
|
||||
|
||||
async fn backend_without_sasl_ir(mut sock: tokio::net::TcpStream, want_user: &'static str) {
|
||||
sock.write_all(EXCHANGE_GREETING).await.unwrap();
|
||||
|
||||
let command = read_line(&mut sock).await;
|
||||
assert_eq!(
|
||||
command, "a1 AUTHENTICATE PLAIN",
|
||||
"backend without SASL-IR must not receive an inline initial response"
|
||||
);
|
||||
sock.write_all(b"+ \r\n").await.unwrap();
|
||||
|
||||
let response = read_line(&mut sock).await;
|
||||
let raw = BASE64.decode(response.trim()).unwrap();
|
||||
assert_eq!(
|
||||
raw,
|
||||
format!("\0{want_user}\0secret").into_bytes(),
|
||||
"deferred initial response should carry the credentials"
|
||||
);
|
||||
sock.write_all(b"a1 OK [CAPABILITY IMAP4rev1] authenticated\r\n")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let _ = read_until(&mut sock, b"NOOP").await;
|
||||
sock.write_all(b"a2 OK NOOP done\r\n").await.unwrap();
|
||||
}
|
||||
|
||||
async fn run_against_backend_without_sasl_ir(
|
||||
ctx: std::sync::Arc<crate::proto::common::Ctx>,
|
||||
first: &[u8],
|
||||
) -> Vec<u8> {
|
||||
let listener = ctx.config.listener["imap"].clone();
|
||||
let (mut client, server) = tcp_pair().await;
|
||||
let ctx2 = ctx.clone();
|
||||
let handler = tokio::spawn(async move {
|
||||
let _ = crate::proto::imap::handle(
|
||||
&ctx2,
|
||||
&listener,
|
||||
Box::new(server),
|
||||
"127.0.0.1:40010".parse().unwrap(),
|
||||
"127.0.0.1:143".parse().unwrap(),
|
||||
)
|
||||
.await;
|
||||
});
|
||||
|
||||
let _greeting = read_line(&mut client).await;
|
||||
send(&mut client, first).await;
|
||||
let resp = read_until(&mut client, b"a1 OK").await;
|
||||
send(&mut client, b"a2 NOOP\r\n").await;
|
||||
let mut resp = resp;
|
||||
resp.extend_from_slice(&read_until(&mut client, b"a2 OK").await);
|
||||
handler.abort();
|
||||
resp
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn imap_defers_initial_response_when_backend_lacks_sasl_ir() {
|
||||
let (port, backend) =
|
||||
dummy_backend(|sock| backend_without_sasl_ir(sock, "alice@example.com")).await;
|
||||
let mapping = mapping_file("");
|
||||
let toml = config(port, &mapping.path().display().to_string());
|
||||
let ctx = make_ctx(&toml).await;
|
||||
|
||||
let ir = BASE64.encode(b"\0alice@example.com\0secret");
|
||||
let resp = run_against_backend_without_sasl_ir(
|
||||
ctx,
|
||||
format!("a1 AUTHENTICATE PLAIN {ir}\r\n").as_bytes(),
|
||||
)
|
||||
.await;
|
||||
|
||||
assert!(
|
||||
find(&resp, b"a1 OK").is_some(),
|
||||
"expected a1 OK, got {:?}",
|
||||
String::from_utf8_lossy(&resp)
|
||||
);
|
||||
assert!(
|
||||
find(&resp, b"a2 OK").is_some(),
|
||||
"expected bridged a2 OK, got {:?}",
|
||||
String::from_utf8_lossy(&resp)
|
||||
);
|
||||
let _ = backend.await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn imap_login_defers_initial_response_when_backend_lacks_sasl_ir() {
|
||||
let (port, backend) =
|
||||
dummy_backend(|sock| backend_without_sasl_ir(sock, "bob@example.com")).await;
|
||||
let mapping = mapping_file("");
|
||||
let toml = config(port, &mapping.path().display().to_string());
|
||||
let ctx = make_ctx(&toml).await;
|
||||
|
||||
let resp =
|
||||
run_against_backend_without_sasl_ir(ctx, b"a1 LOGIN bob@example.com secret\r\n").await;
|
||||
|
||||
assert!(
|
||||
find(&resp, b"a1 OK").is_some(),
|
||||
"expected a1 OK, got {:?}",
|
||||
String::from_utf8_lossy(&resp)
|
||||
);
|
||||
let _ = backend.await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn imap_keeps_inline_initial_response_when_backend_advertises_sasl_ir() {
|
||||
let (port, backend) = dummy_backend(|mut sock: tokio::net::TcpStream| async move {
|
||||
sock.write_all(b"* OK [CAPABILITY IMAP4rev2 SASL-IR] backend ready\r\n")
|
||||
.await
|
||||
.unwrap();
|
||||
let command = read_line(&mut sock).await;
|
||||
let ir = BASE64.encode(b"\0dave@example.com\0secret");
|
||||
assert_eq!(command, format!("a1 AUTHENTICATE PLAIN {ir}"));
|
||||
sock.write_all(b"a1 OK authenticated\r\n").await.unwrap();
|
||||
let _ = read_until(&mut sock, b"NOOP").await;
|
||||
sock.write_all(b"a2 OK NOOP done\r\n").await.unwrap();
|
||||
})
|
||||
.await;
|
||||
let mapping = mapping_file("");
|
||||
let toml = config(port, &mapping.path().display().to_string());
|
||||
let ctx = make_ctx(&toml).await;
|
||||
let listener = ctx.config.listener["imap"].clone();
|
||||
|
||||
let (mut client, server) = tcp_pair().await;
|
||||
let ctx2 = ctx.clone();
|
||||
let handler = tokio::spawn(async move {
|
||||
let _ = crate::proto::imap::handle(
|
||||
&ctx2,
|
||||
&listener,
|
||||
Box::new(server),
|
||||
"127.0.0.1:40011".parse().unwrap(),
|
||||
"127.0.0.1:143".parse().unwrap(),
|
||||
)
|
||||
.await;
|
||||
});
|
||||
|
||||
let _greeting = read_line(&mut client).await;
|
||||
let ir = BASE64.encode(b"\0dave@example.com\0secret");
|
||||
send(
|
||||
&mut client,
|
||||
format!("a1 AUTHENTICATE PLAIN {ir}\r\n").as_bytes(),
|
||||
)
|
||||
.await;
|
||||
let resp = read_until(&mut client, b"a1 OK").await;
|
||||
assert!(find(&resp, b"a1 OK").is_some());
|
||||
let _ = backend.await;
|
||||
handler.abort();
|
||||
}
|
||||
|
||||
@@ -4,16 +4,28 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use testcontainers::core::{IntoContainerPort, WaitFor};
|
||||
use testcontainers::runners::AsyncRunner;
|
||||
use testcontainers_modules::redis::{REDIS_PORT, Redis};
|
||||
use testcontainers::{ContainerAsync, GenericImage};
|
||||
|
||||
use crate::config::RedisMappingConfig;
|
||||
use crate::route::store::redis::RedisStore;
|
||||
|
||||
const REDIS_PORT: u16 = 6379;
|
||||
|
||||
async fn redis_container() -> ContainerAsync<GenericImage> {
|
||||
GenericImage::new("redis", "7.4-alpine")
|
||||
.with_exposed_port(REDIS_PORT.tcp())
|
||||
.with_wait_for(WaitFor::message_on_stdout("Ready to accept connections"))
|
||||
.start()
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn redis_store_resolves_and_misses() {
|
||||
let node = Redis::default().start().await.unwrap();
|
||||
let port = node.get_host_port_ipv4(REDIS_PORT).await.unwrap();
|
||||
let node = redis_container().await;
|
||||
let port = node.get_host_port_ipv4(REDIS_PORT.tcp()).await.unwrap();
|
||||
let url = format!("redis://127.0.0.1:{port}");
|
||||
|
||||
let client = redis::Client::open(url.as_str()).unwrap();
|
||||
|
||||
+25
-5
@@ -4,16 +4,36 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use testcontainers::core::{IntoContainerPort, WaitFor};
|
||||
use testcontainers::runners::AsyncRunner;
|
||||
use testcontainers_modules::postgres::Postgres;
|
||||
use testcontainers::{ContainerAsync, GenericImage, ImageExt};
|
||||
|
||||
use crate::config::SqlMappingConfig;
|
||||
use crate::route::store::sql::SqlStore;
|
||||
|
||||
const POSTGRES_PORT: u16 = 5432;
|
||||
|
||||
async fn postgres_container() -> ContainerAsync<GenericImage> {
|
||||
GenericImage::new("postgres", "17-alpine")
|
||||
.with_exposed_port(POSTGRES_PORT.tcp())
|
||||
.with_wait_for(WaitFor::message_on_stderr(
|
||||
"database system is ready to accept connections",
|
||||
))
|
||||
.with_wait_for(WaitFor::message_on_stderr(
|
||||
"database system is ready to accept connections",
|
||||
))
|
||||
.with_env_var("POSTGRES_USER", "postgres")
|
||||
.with_env_var("POSTGRES_PASSWORD", "postgres")
|
||||
.with_env_var("POSTGRES_DB", "postgres")
|
||||
.start()
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn sql_store_resolves_and_misses() {
|
||||
let node = Postgres::default().start().await.unwrap();
|
||||
let port = node.get_host_port_ipv4(5432).await.unwrap();
|
||||
let node = postgres_container().await;
|
||||
let port = node.get_host_port_ipv4(POSTGRES_PORT.tcp()).await.unwrap();
|
||||
let url = format!("postgres://postgres:postgres@127.0.0.1:{port}/postgres");
|
||||
|
||||
sqlx::any::install_default_drivers();
|
||||
@@ -49,8 +69,8 @@ async fn sql_store_resolves_and_misses() {
|
||||
|
||||
#[tokio::test]
|
||||
async fn sql_store_upsert_and_remove() {
|
||||
let node = Postgres::default().start().await.unwrap();
|
||||
let port = node.get_host_port_ipv4(5432).await.unwrap();
|
||||
let node = postgres_container().await;
|
||||
let port = node.get_host_port_ipv4(POSTGRES_PORT.tcp()).await.unwrap();
|
||||
let url = format!("postgres://postgres:postgres@127.0.0.1:{port}/postgres");
|
||||
|
||||
sqlx::any::install_default_drivers();
|
||||
|
||||
Reference in New Issue
Block a user