nyx_agent_core/store/
webhook.rs1use sqlx::SqlitePool;
4
5use crate::store::StoreError;
6
7#[derive(Debug, Clone, PartialEq, Eq)]
8pub struct WebhookRecord {
9 pub id: String,
10 pub repo: String,
11 pub hmac_secret_ref: String,
12 pub branch_filter: Option<String>,
13 pub enabled: bool,
14}
15
16pub struct WebhookStore<'a> {
17 pool: &'a SqlitePool,
18}
19
20impl<'a> WebhookStore<'a> {
21 pub fn new(pool: &'a SqlitePool) -> Self {
22 Self { pool }
23 }
24
25 pub async fn insert(&self, w: &WebhookRecord) -> Result<(), StoreError> {
26 let enabled = i64::from(w.enabled);
27 sqlx::query!(
28 r#"
29 INSERT INTO webhooks (id, repo, hmac_secret_ref, branch_filter, enabled)
30 VALUES (?, ?, ?, ?, ?)
31 "#,
32 w.id,
33 w.repo,
34 w.hmac_secret_ref,
35 w.branch_filter,
36 enabled,
37 )
38 .execute(self.pool)
39 .await?;
40 Ok(())
41 }
42
43 pub async fn get(&self, id: &str) -> Result<Option<WebhookRecord>, StoreError> {
44 let row = sqlx::query!(
45 r#"
46 SELECT id AS "id!", repo AS "repo!",
47 hmac_secret_ref AS "hmac_secret_ref!",
48 branch_filter,
49 enabled AS "enabled!: i64"
50 FROM webhooks WHERE id = ?
51 "#,
52 id
53 )
54 .fetch_optional(self.pool)
55 .await?;
56 Ok(row.map(|r| WebhookRecord {
57 id: r.id,
58 repo: r.repo,
59 hmac_secret_ref: r.hmac_secret_ref,
60 branch_filter: r.branch_filter,
61 enabled: r.enabled != 0,
62 }))
63 }
64
65 pub async fn list_enabled_for_repo(
66 &self,
67 repo: &str,
68 ) -> Result<Vec<WebhookRecord>, StoreError> {
69 let rows = sqlx::query!(
70 r#"
71 SELECT id AS "id!", repo AS "repo!",
72 hmac_secret_ref AS "hmac_secret_ref!",
73 branch_filter,
74 enabled AS "enabled!: i64"
75 FROM webhooks WHERE repo = ? AND enabled = 1
76 "#,
77 repo
78 )
79 .fetch_all(self.pool)
80 .await?;
81 Ok(rows
82 .into_iter()
83 .map(|r| WebhookRecord {
84 id: r.id,
85 repo: r.repo,
86 hmac_secret_ref: r.hmac_secret_ref,
87 branch_filter: r.branch_filter,
88 enabled: r.enabled != 0,
89 })
90 .collect())
91 }
92}
93
94#[cfg(test)]
95mod tests {
96 use super::*;
97 use crate::store::testutil::{fresh_store, sample_repo};
98
99 fn hook(id: &str, repo: &str, enabled: bool) -> WebhookRecord {
100 WebhookRecord {
101 id: id.to_string(),
102 repo: repo.to_string(),
103 hmac_secret_ref: "secret/manager/key#1".to_string(),
104 branch_filter: Some("main".to_string()),
105 enabled,
106 }
107 }
108
109 #[tokio::test]
110 async fn insert_then_get_roundtrips() {
111 let (_tmp, s) = fresh_store().await;
112 s.repos().upsert(&sample_repo("r")).await.expect("repo");
113 let h = hook("h-1", "r", true);
114 s.webhooks().insert(&h).await.expect("insert");
115 let got = s.webhooks().get("h-1").await.expect("get").expect("row");
116 assert_eq!(got, h);
117 }
118
119 #[tokio::test]
120 async fn list_enabled_excludes_disabled_and_other_repos() {
121 let (_tmp, s) = fresh_store().await;
122 s.repos().upsert(&sample_repo("r1")).await.expect("r1");
123 s.repos().upsert(&sample_repo("r2")).await.expect("r2");
124 s.webhooks().insert(&hook("a", "r1", true)).await.expect("a");
125 s.webhooks().insert(&hook("b", "r1", false)).await.expect("b");
126 s.webhooks().insert(&hook("c", "r2", true)).await.expect("c");
127 let got: Vec<_> = s
128 .webhooks()
129 .list_enabled_for_repo("r1")
130 .await
131 .expect("list")
132 .into_iter()
133 .map(|h| h.id)
134 .collect();
135 assert_eq!(got, vec!["a".to_string()]);
136 }
137
138 #[tokio::test]
139 async fn cascade_from_repo_delete() {
140 let (_tmp, s) = fresh_store().await;
141 s.repos().upsert(&sample_repo("doomed")).await.expect("repo");
142 s.webhooks().insert(&hook("h", "doomed", true)).await.expect("insert");
143 s.repos().delete("doomed").await.expect("del");
144 assert!(
145 s.webhooks().get("h").await.expect("get").is_none(),
146 "webhook should cascade-delete with parent repo"
147 );
148 }
149}