feat: add webhook auth guard and IRC password/username support
Add WEBHOOK_SECRET env var for authenticating incoming Owncast webhooks via a ?secret= query parameter. Requests with a missing or incorrect secret are rejected with 401. If unset, all requests are accepted (with a startup warning). Also includes previously uncommitted work: - IRC server password support (IRC_PASSWORD env var, PASS command) - IRC username/ident field in config - IRC_PASSWORD and SELinux volume flag in docker-compose.yml Made-with: Cursor
This commit is contained in:
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -1127,6 +1127,7 @@ dependencies = [
|
|||||||
"tokio-test",
|
"tokio-test",
|
||||||
"tokio-tungstenite",
|
"tokio-tungstenite",
|
||||||
"toml 0.8.23",
|
"toml 0.8.23",
|
||||||
|
"tower",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -29,3 +29,4 @@ futures-util = "0.3"
|
|||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tokio-test = "0.4"
|
tokio-test = "0.4"
|
||||||
|
tower = { version = "0.5", features = ["util"] }
|
||||||
|
|||||||
13
README.md
13
README.md
@@ -29,6 +29,7 @@ Or create a `.env` file (git-ignored):
|
|||||||
|
|
||||||
```
|
```
|
||||||
OWNCAST_ACCESS_TOKEN=your-token-here
|
OWNCAST_ACCESS_TOKEN=your-token-here
|
||||||
|
WEBHOOK_SECRET=some-random-secret
|
||||||
```
|
```
|
||||||
|
|
||||||
**4. Configure the Owncast webhook**
|
**4. Configure the Owncast webhook**
|
||||||
@@ -36,9 +37,11 @@ OWNCAST_ACCESS_TOKEN=your-token-here
|
|||||||
In your Owncast admin, go to **Integrations > Webhooks** and add a webhook pointing to:
|
In your Owncast admin, go to **Integrations > Webhooks** and add a webhook pointing to:
|
||||||
|
|
||||||
```
|
```
|
||||||
http://<bridge-host>:9078/webhook
|
http://<bridge-host>:9078/webhook?secret=some-random-secret
|
||||||
```
|
```
|
||||||
|
|
||||||
|
If `WEBHOOK_SECRET` is set, the bridge rejects any request that doesn't include a matching `?secret=` query parameter. If unset, all requests are accepted (a warning is logged at startup).
|
||||||
|
|
||||||
Select the events: **Chat Message**, **Stream Started**, **Stream Stopped**.
|
Select the events: **Chat Message**, **Stream Started**, **Stream Stopped**.
|
||||||
|
|
||||||
**5. Run it**
|
**5. Run it**
|
||||||
@@ -82,7 +85,13 @@ See [`config.example.toml`](config.example.toml) for all options. The only requi
|
|||||||
| `bridge` | `owncast_prefix` | `[OC]` | Prefix for Owncast messages in IRC |
|
| `bridge` | `owncast_prefix` | `[OC]` | Prefix for Owncast messages in IRC |
|
||||||
| `control` | `socket_path` | `/tmp/owncast-irc-bridge.sock` | Unix socket for `bridge-ctl` |
|
| `control` | `socket_path` | `/tmp/owncast-irc-bridge.sock` | Unix socket for `bridge-ctl` |
|
||||||
|
|
||||||
The access token is always read from the `OWNCAST_ACCESS_TOKEN` environment variable (not the config file).
|
Secrets are always read from environment variables (not the config file):
|
||||||
|
|
||||||
|
| Variable | Required | Description |
|
||||||
|
|----------|----------|-------------|
|
||||||
|
| `OWNCAST_ACCESS_TOKEN` | Yes | Owncast integration API token |
|
||||||
|
| `IRC_PASSWORD` | No | IRC server password (PASS command) |
|
||||||
|
| `WEBHOOK_SECRET` | No | Shared secret for webhook authentication |
|
||||||
|
|
||||||
## Runtime Control
|
## Runtime Control
|
||||||
|
|
||||||
|
|||||||
17
chat-summaries/2026-03-12_22-00-summary.md
Normal file
17
chat-summaries/2026-03-12_22-00-summary.md
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# IRC Server Password Support
|
||||||
|
|
||||||
|
## Task
|
||||||
|
Add support for connecting to password-protected IRC servers by sending the PASS command during connection, and allow setting a custom IRC username.
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
- **src/config.rs**: Added `username: Option<String>` field to `IrcConfig`. Added `BridgeConfig::irc_server_password()` method that reads `IRC_PASSWORD` from the environment (returns `Option<String>`). Updated `default_for_test()` and tests for the new field and method.
|
||||||
|
- **src/irc_task.rs**: Set `password` and `username` on the `irc` crate's `Config` when building the IRC connection, sourcing the password from `BridgeConfig::irc_server_password()` and username from `IrcConfig.username`.
|
||||||
|
- **config.toml**: Removed stale `server_password` field, added comment about `IRC_PASSWORD` env var.
|
||||||
|
- **config.example.toml**: Added commented-out `username` field and `IRC_PASSWORD` env var documentation.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
Set the `IRC_PASSWORD` environment variable before running the bridge to authenticate with the IRC server. Optionally set `username` in `[irc]` config for a custom ident.
|
||||||
|
|
||||||
|
## Follow-up
|
||||||
|
- None identified.
|
||||||
40
chat-summaries/2026-03-13_12-00-summary.md
Normal file
40
chat-summaries/2026-03-13_12-00-summary.md
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
# Webhook Authentication Guard
|
||||||
|
|
||||||
|
**Date:** 2026-03-13
|
||||||
|
|
||||||
|
## Task
|
||||||
|
|
||||||
|
Add a shared secret (`WEBHOOK_SECRET`) to the webhook endpoint so only requests with a matching `?secret=` query parameter are accepted. This prevents unauthorized parties from injecting events into the bridge.
|
||||||
|
|
||||||
|
## Changes Made
|
||||||
|
|
||||||
|
### `src/config.rs`
|
||||||
|
- Added `webhook_secret()` static method to `BridgeConfig` — reads `WEBHOOK_SECRET` env var, returns `Option<String>`.
|
||||||
|
|
||||||
|
### `src/webhook.rs`
|
||||||
|
- Added `WebhookQuery` struct for axum query parameter extraction.
|
||||||
|
- Added `secret: Option<String>` field to `WebhookState`.
|
||||||
|
- Updated `handle_webhook` to validate the secret before processing: returns 401 if configured secret doesn't match.
|
||||||
|
- Updated `run_webhook_server` signature to accept `secret: Option<String>`; logs a warning at startup if unset.
|
||||||
|
- Added 4 integration tests using `tower::ServiceExt::oneshot`: correct secret (200), wrong secret (401), missing secret (401), no secret configured (200).
|
||||||
|
|
||||||
|
### `src/main.rs`
|
||||||
|
- Reads `WEBHOOK_SECRET` via `config::BridgeConfig::webhook_secret()`.
|
||||||
|
- Passes the secret to `webhook::run_webhook_server()`.
|
||||||
|
|
||||||
|
### `docker-compose.yml`
|
||||||
|
- Added `WEBHOOK_SECRET=${WEBHOOK_SECRET}` to environment section.
|
||||||
|
|
||||||
|
### `config.example.toml`
|
||||||
|
- Added comment documenting the `WEBHOOK_SECRET` env var.
|
||||||
|
|
||||||
|
### `README.md`
|
||||||
|
- Updated webhook URL example to include `?secret=` parameter.
|
||||||
|
- Added environment variables table documenting all three secrets.
|
||||||
|
|
||||||
|
### `Cargo.toml`
|
||||||
|
- Added `tower` (0.5, `util` feature) as dev dependency for handler tests.
|
||||||
|
|
||||||
|
## Follow-up Items
|
||||||
|
|
||||||
|
- None. All 65 tests pass.
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
[irc]
|
[irc]
|
||||||
server = "irc.zeronode.net"
|
server = "irc.zeronode.net"
|
||||||
|
# Set IRC_PASSWORD env var for server password (PASS command)
|
||||||
|
# username = "myuser"
|
||||||
port = 6667
|
port = 6667
|
||||||
tls = false
|
tls = false
|
||||||
nick = "owncast-bridge"
|
nick = "owncast-bridge"
|
||||||
@@ -8,6 +10,7 @@ channel = "#BowlAfterBowl"
|
|||||||
[owncast]
|
[owncast]
|
||||||
url = "https://owncast.bowlafterbowl.com"
|
url = "https://owncast.bowlafterbowl.com"
|
||||||
# Set OWNCAST_ACCESS_TOKEN env var for the token
|
# Set OWNCAST_ACCESS_TOKEN env var for the token
|
||||||
|
# Set WEBHOOK_SECRET env var to require ?secret=<token> on incoming webhooks
|
||||||
webhook_port = 9078
|
webhook_port = 9078
|
||||||
websocket_enabled = true
|
websocket_enabled = true
|
||||||
health_poll_interval_secs = 30
|
health_poll_interval_secs = 30
|
||||||
|
|||||||
@@ -5,8 +5,10 @@ services:
|
|||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
- OWNCAST_ACCESS_TOKEN=${OWNCAST_ACCESS_TOKEN}
|
- OWNCAST_ACCESS_TOKEN=${OWNCAST_ACCESS_TOKEN}
|
||||||
|
- IRC_PASSWORD=${IRC_PASSWORD}
|
||||||
|
- WEBHOOK_SECRET=${WEBHOOK_SECRET}
|
||||||
- RUST_LOG=info
|
- RUST_LOG=info
|
||||||
volumes:
|
volumes:
|
||||||
- ./config.toml:/etc/owncast-irc-bridge/config.toml:ro
|
- ./config.toml:/etc/owncast-irc-bridge/config.toml:ro,z
|
||||||
ports:
|
ports:
|
||||||
- "9078:9078"
|
- "9078:9078"
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ pub struct IrcConfig {
|
|||||||
pub tls: bool,
|
pub tls: bool,
|
||||||
#[serde(default = "default_nick")]
|
#[serde(default = "default_nick")]
|
||||||
pub nick: String,
|
pub nick: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub username: Option<String>,
|
||||||
pub channel: String,
|
pub channel: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,10 +62,18 @@ impl BridgeConfig {
|
|||||||
Ok(config)
|
Ok(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn irc_server_password() -> Option<String> {
|
||||||
|
std::env::var("IRC_PASSWORD").ok()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn owncast_access_token(&self) -> anyhow::Result<String> {
|
pub fn owncast_access_token(&self) -> anyhow::Result<String> {
|
||||||
std::env::var("OWNCAST_ACCESS_TOKEN")
|
std::env::var("OWNCAST_ACCESS_TOKEN")
|
||||||
.map_err(|_| anyhow::anyhow!("OWNCAST_ACCESS_TOKEN env var not set"))
|
.map_err(|_| anyhow::anyhow!("OWNCAST_ACCESS_TOKEN env var not set"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn webhook_secret() -> Option<String> {
|
||||||
|
std::env::var("WEBHOOK_SECRET").ok()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_irc_port() -> u16 { 6667 }
|
fn default_irc_port() -> u16 { 6667 }
|
||||||
@@ -100,6 +110,7 @@ impl BridgeConfig {
|
|||||||
port: 6667,
|
port: 6667,
|
||||||
tls: false,
|
tls: false,
|
||||||
nick: "test-bot".to_string(),
|
nick: "test-bot".to_string(),
|
||||||
|
username: None,
|
||||||
channel: "#test".to_string(),
|
channel: "#test".to_string(),
|
||||||
},
|
},
|
||||||
owncast: OwncastConfig {
|
owncast: OwncastConfig {
|
||||||
@@ -134,6 +145,7 @@ url = "https://owncast.example.com"
|
|||||||
assert_eq!(config.irc.port, 6667);
|
assert_eq!(config.irc.port, 6667);
|
||||||
assert_eq!(config.irc.tls, false);
|
assert_eq!(config.irc.tls, false);
|
||||||
assert_eq!(config.irc.nick, "owncast-bridge");
|
assert_eq!(config.irc.nick, "owncast-bridge");
|
||||||
|
assert!(config.irc.username.is_none());
|
||||||
assert_eq!(config.irc.channel, "#test");
|
assert_eq!(config.irc.channel, "#test");
|
||||||
assert_eq!(config.owncast.url, "https://owncast.example.com");
|
assert_eq!(config.owncast.url, "https://owncast.example.com");
|
||||||
assert_eq!(config.owncast.webhook_port, 9078);
|
assert_eq!(config.owncast.webhook_port, 9078);
|
||||||
@@ -153,6 +165,7 @@ server = "irc.example.com"
|
|||||||
port = 6697
|
port = 6697
|
||||||
tls = true
|
tls = true
|
||||||
nick = "mybot"
|
nick = "mybot"
|
||||||
|
username = "myident"
|
||||||
channel = "#mychan"
|
channel = "#mychan"
|
||||||
|
|
||||||
[owncast]
|
[owncast]
|
||||||
@@ -173,12 +186,21 @@ socket_path = "/var/run/bridge.sock"
|
|||||||
assert_eq!(config.irc.port, 6697);
|
assert_eq!(config.irc.port, 6697);
|
||||||
assert!(config.irc.tls);
|
assert!(config.irc.tls);
|
||||||
assert_eq!(config.irc.nick, "mybot");
|
assert_eq!(config.irc.nick, "mybot");
|
||||||
|
assert_eq!(config.irc.username.as_deref(), Some("myident"));
|
||||||
assert_eq!(config.owncast.webhook_port, 8888);
|
assert_eq!(config.owncast.webhook_port, 8888);
|
||||||
assert!(config.owncast.websocket_enabled);
|
assert!(config.owncast.websocket_enabled);
|
||||||
assert_eq!(config.bridge.message_buffer_size, 50);
|
assert_eq!(config.bridge.message_buffer_size, 50);
|
||||||
assert_eq!(config.control.socket_path, "/var/run/bridge.sock");
|
assert_eq!(config.control.socket_path, "/var/run/bridge.sock");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_irc_server_password_from_env() {
|
||||||
|
std::env::set_var("IRC_PASSWORD", "secret123");
|
||||||
|
assert_eq!(BridgeConfig::irc_server_password().as_deref(), Some("secret123"));
|
||||||
|
std::env::remove_var("IRC_PASSWORD");
|
||||||
|
assert!(BridgeConfig::irc_server_password().is_none());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_access_token_from_env() {
|
fn test_access_token_from_env() {
|
||||||
std::env::set_var("OWNCAST_ACCESS_TOKEN", "test-token-123");
|
std::env::set_var("OWNCAST_ACCESS_TOKEN", "test-token-123");
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use irc::client::prelude::*;
|
|||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
use tracing::{error, info};
|
use tracing::{error, info};
|
||||||
|
|
||||||
use crate::config::IrcConfig;
|
use crate::config::{BridgeConfig, IrcConfig};
|
||||||
use crate::events::{BridgeEvent, Source};
|
use crate::events::{BridgeEvent, Source};
|
||||||
|
|
||||||
pub async fn run_irc_task(
|
pub async fn run_irc_task(
|
||||||
@@ -52,6 +52,8 @@ async fn connect_and_run(
|
|||||||
port: Some(config.port),
|
port: Some(config.port),
|
||||||
use_tls: Some(config.tls),
|
use_tls: Some(config.tls),
|
||||||
channels: vec![config.channel.clone()],
|
channels: vec![config.channel.clone()],
|
||||||
|
password: BridgeConfig::irc_server_password(),
|
||||||
|
username: config.username.clone(),
|
||||||
..Config::default()
|
..Config::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
let cli = Cli::parse();
|
let cli = Cli::parse();
|
||||||
let config = config::BridgeConfig::load(&cli.config)?;
|
let config = config::BridgeConfig::load(&cli.config)?;
|
||||||
let access_token = config.owncast_access_token()?;
|
let access_token = config.owncast_access_token()?;
|
||||||
|
let webhook_secret = config::BridgeConfig::webhook_secret();
|
||||||
|
|
||||||
info!("Starting owncast-irc-bridge");
|
info!("Starting owncast-irc-bridge");
|
||||||
|
|
||||||
@@ -64,7 +65,7 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
let webhook_port = config.owncast.webhook_port;
|
let webhook_port = config.owncast.webhook_port;
|
||||||
let webhook_event_tx = event_tx.clone();
|
let webhook_event_tx = event_tx.clone();
|
||||||
let _webhook_handle = tokio::spawn(async move {
|
let _webhook_handle = tokio::spawn(async move {
|
||||||
if let Err(e) = webhook::run_webhook_server(webhook_port, webhook_event_tx).await {
|
if let Err(e) = webhook::run_webhook_server(webhook_port, webhook_event_tx, webhook_secret).await {
|
||||||
tracing::error!(error = %e, "Webhook server failed");
|
tracing::error!(error = %e, "Webhook server failed");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
111
src/webhook.rs
111
src/webhook.rs
@@ -1,4 +1,4 @@
|
|||||||
use axum::{extract::State, http::StatusCode, routing::post, Json, Router};
|
use axum::{extract::{Query, State}, http::StatusCode, routing::post, Json, Router};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
use tracing::{info, warn};
|
use tracing::{info, warn};
|
||||||
@@ -67,15 +67,32 @@ impl WebhookPayload {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct WebhookQuery {
|
||||||
|
secret: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
struct WebhookState {
|
struct WebhookState {
|
||||||
event_tx: mpsc::Sender<BridgeEvent>,
|
event_tx: mpsc::Sender<BridgeEvent>,
|
||||||
|
secret: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_webhook(
|
async fn handle_webhook(
|
||||||
State(state): State<WebhookState>,
|
State(state): State<WebhookState>,
|
||||||
|
Query(query): Query<WebhookQuery>,
|
||||||
Json(payload): Json<WebhookPayload>,
|
Json(payload): Json<WebhookPayload>,
|
||||||
) -> StatusCode {
|
) -> StatusCode {
|
||||||
|
if let Some(ref expected) = state.secret {
|
||||||
|
match query.secret {
|
||||||
|
Some(ref provided) if provided == expected => {}
|
||||||
|
_ => {
|
||||||
|
warn!("Webhook request rejected: invalid or missing secret");
|
||||||
|
return StatusCode::UNAUTHORIZED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
info!(event_type = %payload.event_type, "Received webhook");
|
info!(event_type = %payload.event_type, "Received webhook");
|
||||||
|
|
||||||
match payload.into_bridge_event() {
|
match payload.into_bridge_event() {
|
||||||
@@ -93,8 +110,13 @@ async fn handle_webhook(
|
|||||||
pub async fn run_webhook_server(
|
pub async fn run_webhook_server(
|
||||||
port: u16,
|
port: u16,
|
||||||
event_tx: mpsc::Sender<BridgeEvent>,
|
event_tx: mpsc::Sender<BridgeEvent>,
|
||||||
|
secret: Option<String>,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
let state = WebhookState { event_tx };
|
if secret.is_none() {
|
||||||
|
warn!("WEBHOOK_SECRET is not set — webhook endpoint accepts all requests");
|
||||||
|
}
|
||||||
|
|
||||||
|
let state = WebhookState { event_tx, secret };
|
||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
.route("/webhook", post(handle_webhook))
|
.route("/webhook", post(handle_webhook))
|
||||||
.with_state(state);
|
.with_state(state);
|
||||||
@@ -110,6 +132,91 @@ pub async fn run_webhook_server(
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use axum::body::Body;
|
||||||
|
use axum::http::Request;
|
||||||
|
use tower::ServiceExt;
|
||||||
|
|
||||||
|
fn build_app(secret: Option<String>) -> (Router, mpsc::Receiver<BridgeEvent>) {
|
||||||
|
let (event_tx, rx) = mpsc::channel(16);
|
||||||
|
let state = WebhookState { event_tx, secret };
|
||||||
|
let app = Router::new()
|
||||||
|
.route("/webhook", post(handle_webhook))
|
||||||
|
.with_state(state);
|
||||||
|
(app, rx)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn chat_body() -> String {
|
||||||
|
serde_json::json!({
|
||||||
|
"type": "CHAT",
|
||||||
|
"eventData": {
|
||||||
|
"user": { "displayName": "test", "isBot": false },
|
||||||
|
"body": "hi",
|
||||||
|
"id": "msg1",
|
||||||
|
"visible": true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_auth_correct_secret_returns_200() {
|
||||||
|
let (app, _rx) = build_app(Some("mysecret".to_string()));
|
||||||
|
let resp = app
|
||||||
|
.oneshot(
|
||||||
|
Request::post("/webhook?secret=mysecret")
|
||||||
|
.header("content-type", "application/json")
|
||||||
|
.body(Body::from(chat_body()))
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(resp.status(), StatusCode::OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_auth_wrong_secret_returns_401() {
|
||||||
|
let (app, _rx) = build_app(Some("mysecret".to_string()));
|
||||||
|
let resp = app
|
||||||
|
.oneshot(
|
||||||
|
Request::post("/webhook?secret=wrong")
|
||||||
|
.header("content-type", "application/json")
|
||||||
|
.body(Body::from(chat_body()))
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_auth_missing_secret_returns_401() {
|
||||||
|
let (app, _rx) = build_app(Some("mysecret".to_string()));
|
||||||
|
let resp = app
|
||||||
|
.oneshot(
|
||||||
|
Request::post("/webhook")
|
||||||
|
.header("content-type", "application/json")
|
||||||
|
.body(Body::from(chat_body()))
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_auth_no_secret_configured_accepts_all() {
|
||||||
|
let (app, _rx) = build_app(None);
|
||||||
|
let resp = app
|
||||||
|
.oneshot(
|
||||||
|
Request::post("/webhook")
|
||||||
|
.header("content-type", "application/json")
|
||||||
|
.body(Body::from(chat_body()))
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(resp.status(), StatusCode::OK);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_parse_chat_event() {
|
fn test_parse_chat_event() {
|
||||||
|
|||||||
Reference in New Issue
Block a user