refactor/tighten-architecture #26
@@ -51,8 +51,9 @@ impl WebhookTokenRepository for SqliteWebhookTokenRepository {
|
||||
|
||||
async fn find_by_token_hash(&self, hash: &str) -> Result<Option<WebhookToken>, DomainError> {
|
||||
let row = sqlx::query(
|
||||
"SELECT id, user_id, token_hash, provider, label, created_at, last_used_at \
|
||||
FROM webhook_tokens WHERE token_hash = ?",
|
||||
"SELECT t.id, t.user_id, t.token_hash, t.provider, t.label, t.created_at, t.last_used_at \
|
||||
FROM webhook_tokens t JOIN users u ON u.id = t.user_id \
|
||||
WHERE t.token_hash = ? AND u.deleted_at IS NULL",
|
||||
)
|
||||
.bind(hash)
|
||||
.fetch_optional(&self.pool)
|
||||
|
||||
@@ -349,3 +349,61 @@ async fn the_service_actor_is_not_deletable_and_offers_no_delete_ui() {
|
||||
let (_, _, body) = send(&router, req).await;
|
||||
assert!(!body.contains("is_service_account"), "{body}");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn a_deleted_users_webhook_token_stops_ingesting_at_once() {
|
||||
let (router, pool, alice, _bob) = two_users().await;
|
||||
let (status, _, body) = send(
|
||||
&router,
|
||||
json_request(
|
||||
Method::POST,
|
||||
"/api/v1/settings/webhook-tokens",
|
||||
Some(&alice.token),
|
||||
serde_json::json!({ "provider": "jellyfin", "label": null }),
|
||||
),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(status, 200, "{body}");
|
||||
let token = serde_json::from_str::<serde_json::Value>(&body).unwrap()["token"]
|
||||
.as_str()
|
||||
.unwrap()
|
||||
.to_string();
|
||||
|
||||
let hook = |token: &str| {
|
||||
Request::builder()
|
||||
.method(Method::POST)
|
||||
.uri("/api/v1/webhooks/jellyfin")
|
||||
.header(header::AUTHORIZATION, format!("Bearer {token}"))
|
||||
.header(header::CONTENT_TYPE, "application/json")
|
||||
.body(Body::from(
|
||||
serde_json::json!({
|
||||
"NotificationType": "PlaybackStop", "ItemType": "Movie",
|
||||
"Name": "Heat", "Year": 1995, "PlayedToCompletion": true,
|
||||
"Provider_tmdb": "949"
|
||||
})
|
||||
.to_string(),
|
||||
))
|
||||
.unwrap()
|
||||
};
|
||||
let events = || async {
|
||||
sqlx::query_scalar::<_, i64>("SELECT COUNT(*) FROM watch_events")
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.unwrap()
|
||||
};
|
||||
|
||||
let (status, _, body) = send(&router, hook(&token)).await;
|
||||
assert_eq!(status, 200, "live token: {body}");
|
||||
assert_eq!(events().await, 1);
|
||||
|
||||
let (status, _, _) = send(
|
||||
&router,
|
||||
delete_json("/api/v1/users/me", &alice.token, PASSWORD),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(status, 204);
|
||||
|
||||
let (status, _, body) = send(&router, hook(&token)).await;
|
||||
assert_eq!(status, 401, "{body}");
|
||||
assert_eq!(events().await, 1, "no event ingested after deletion");
|
||||
}
|
||||
|
||||
@@ -99,7 +99,11 @@ impl WebhookTokenRepository for InMemoryStore {
|
||||
|
||||
async fn find_by_token_hash(&self, hash: &str) -> Result<Option<WebhookToken>, DomainError> {
|
||||
let store = self.webhook_tokens.read().unwrap();
|
||||
Ok(store.iter().find(|t| t.token_hash() == hash).cloned())
|
||||
let gone = self.tombstoned.read().unwrap();
|
||||
Ok(store
|
||||
.iter()
|
||||
.find(|t| t.token_hash() == hash && !gone.contains(&t.user_id().value()))
|
||||
.cloned())
|
||||
}
|
||||
|
||||
async fn list_by_user(&self, user_id: &UserId) -> Result<Vec<WebhookToken>, DomainError> {
|
||||
|
||||
Reference in New Issue
Block a user