Skip to main content

forge_server/surface/
connectors.rs

1//! `connectors()`: the Slack and Gotify subscription state.
2
3use std::collections::BTreeMap;
4
5use forge_primitives::GotifySubscription;
6use forge_primitives::slack::SlackSubscription;
7
8use super::ViewSurface;
9
10/// What the inbound connectors are watching.
11pub struct ConnectorsView {
12    pub gotify: GotifyView,
13    pub slack: SlackView,
14}
15
16/// The Gotify stream's liveness and one project's subscriptions.
17pub struct GotifyView {
18    pub connected: bool,
19    pub subscriptions: Vec<GotifySubscription>,
20}
21
22/// Slack's per-workspace pump liveness and one project's subscriptions.
23pub struct SlackView {
24    /// Workspace label to whether that workspace's pump is live.
25    pub connected_workspaces: BTreeMap<String, bool>,
26    /// `true` when the stored subscription set failed to load.
27    pub load_failed: bool,
28    pub subscriptions: Vec<SlackSubscription>,
29}
30
31impl ViewSurface {
32    /// `project` scopes the subscription lists to that project's own.
33    /// `None` reports only the connection status, the way an unstamped
34    /// session reads.
35    pub fn connectors(&self, project: Option<&str>) -> ConnectorsView {
36        let workspace = &self.workspace;
37        ConnectorsView {
38            gotify: GotifyView {
39                connected: workspace.gotify_connected(),
40                subscriptions: project
41                    .map(|name| workspace.gotify_subscriptions_for_project(name))
42                    .unwrap_or_default(),
43            },
44            slack: SlackView {
45                connected_workspaces: workspace.slack_connected_workspaces(),
46                load_failed: workspace.slack_subscription_load_failed(),
47                subscriptions: project
48                    .map(|name| workspace.slack_subscriptions_for_project(name))
49                    .unwrap_or_default(),
50            },
51        }
52    }
53}
54
55#[cfg(test)]
56mod tests {
57    use std::sync::Arc;
58
59    use super::*;
60
61    /// Catches a field wired to the sibling connector's read, or the
62    /// project scope being dropped, which would show one connector's
63    /// state under the other's heading.
64    ///
65    /// The two liveness flags are seeded to opposite values on purpose:
66    /// two false-valued bools read as agreement, which is what let a
67    /// read wired to the wrong connector pass unnoticed.
68    #[test]
69    fn connectors_agrees_with_the_calls_it_replaces() {
70        let (workspace, _dir) = crate::surface::testing::workspace_with_connector_subs();
71        workspace.seed_test_gotify_connected(true);
72
73        let view = ViewSurface::new(Arc::clone(&workspace)).connectors(Some("forge"));
74
75        assert_ne!(
76            workspace.gotify_connected(),
77            workspace.slack_subscription_load_failed(),
78            "the fixture gives the two connectors' flags different values, or the comparisons below agree on a false",
79        );
80        assert_ne!(
81            view.gotify.connected, view.slack.load_failed,
82            "gotify's liveness and slack's load failure are read from different flags, not one value under both headings",
83        );
84        assert_eq!(
85            view.gotify.connected,
86            workspace.gotify_connected(),
87            "gotify's liveness is the same flag",
88        );
89        assert_eq!(
90            view.gotify.subscriptions,
91            workspace.gotify_subscriptions_for_project("forge"),
92            "gotify's rows are the project's own",
93        );
94        assert_eq!(
95            view.slack.connected_workspaces,
96            workspace.slack_connected_workspaces(),
97            "slack's per-workspace liveness is the same map",
98        );
99        assert_eq!(
100            view.slack.load_failed,
101            workspace.slack_subscription_load_failed(),
102            "slack's load failure is the same flag",
103        );
104        assert_eq!(
105            view.slack.subscriptions,
106            workspace.slack_subscriptions_for_project("forge"),
107            "slack's rows are the project's own",
108        );
109        assert_eq!(
110            (view.gotify.subscriptions.len(), view.slack.subscriptions.len()),
111            (1, 1),
112            "the seeded subscriptions are what the read returns, so the rows above are not empty on both sides",
113        );
114
115        let other = ViewSurface::new(Arc::clone(&workspace)).connectors(Some("elsewhere"));
116        assert!(
117            other.gotify.subscriptions.is_empty() && other.slack.subscriptions.is_empty(),
118            "another project's scope returns none of forge's rows",
119        );
120        let unscoped = ViewSurface::new(Arc::clone(&workspace)).connectors(None);
121        assert!(
122            unscoped.gotify.subscriptions.is_empty() && unscoped.slack.subscriptions.is_empty(),
123            "an unstamped session is scoped to no project rather than every project",
124        );
125    }
126}