mcaptcha/api/v1/
survey.rs

1/*
2 * Copyright (C) 2023  Aravinth Manivannan <realaravinth@batsense.net>
3 *
4 * This program is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU Affero General Public License as
6 * published by the Free Software Foundation, either version 3 of the
7 * License, or (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 * GNU Affero General Public License for more details.
13 *
14 * You should have received a copy of the GNU Affero General Public License
15 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
16 */
17use actix_web::web::ServiceConfig;
18use actix_web::{web, HttpResponse, Responder};
19use serde::{Deserialize, Serialize};
20
21use crate::errors::*;
22use crate::AppData;
23
24pub fn services(cfg: &mut ServiceConfig) {
25    cfg.service(download);
26    cfg.service(secret);
27}
28
29pub mod routes {
30    pub struct Survey {
31        pub download: &'static str,
32        pub secret: &'static str,
33    }
34
35    impl Survey {
36        pub const fn new() -> Self {
37            Self {
38                download: "/api/v1/survey/takeout/{survey_id}/get",
39                secret: "/api/v1/survey/secret",
40            }
41        }
42
43        pub fn get_download_route(&self, survey_id: &str, page: usize) -> String {
44            format!(
45                "{}?page={}",
46                self.download.replace("{survey_id}", survey_id),
47                page
48            )
49        }
50    }
51}
52
53#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)]
54pub struct Page {
55    pub page: usize,
56}
57
58/// emits build details of the bninary
59#[my_codegen::get(path = "crate::V1_API_ROUTES.survey.download")]
60async fn download(
61    data: AppData,
62    page: web::Query<Page>,
63    psuedo_id: web::Path<uuid::Uuid>,
64) -> ServiceResult<impl Responder> {
65    const LIMIT: usize = 50;
66    let offset = LIMIT as isize * ((page.page as isize) - 1);
67    let offset = if offset < 0 { 0 } else { offset };
68    let psuedo_id = psuedo_id.into_inner();
69    let campaign_id = data
70        .db
71        .analytics_get_capmaign_id_from_psuedo_id(&psuedo_id.to_string())
72        .await?;
73    let data = data
74        .db
75        .analytics_fetch(&campaign_id, LIMIT, offset as usize)
76        .await?;
77    Ok(HttpResponse::Ok().json(data))
78}
79
80#[derive(Serialize, Deserialize)]
81struct SurveySecretUpload {
82    secret: String,
83    auth_token: String,
84}
85
86/// mCaptcha/survey upload secret route
87#[my_codegen::post(path = "crate::V1_API_ROUTES.survey.secret")]
88async fn secret(
89    data: AppData,
90    payload: web::Json<SurveySecretUpload>,
91) -> ServiceResult<impl Responder> {
92    match data.survey_secrets.get(&payload.auth_token) {
93        Some(survey_instance_url) => {
94            let payload = payload.into_inner();
95            data.survey_secrets.set(survey_instance_url, payload.secret);
96            data.survey_secrets.rm(&payload.auth_token);
97            Ok(HttpResponse::Ok())
98        }
99        None => Err(ServiceError::WrongPassword),
100    }
101}
102
103#[cfg(test)]
104pub mod tests {
105    use actix_web::{http::StatusCode, test, App};
106
107    use super::*;
108    use crate::api::v1::mcaptcha::get_random;
109    use crate::tests::*;
110    use crate::*;
111
112    #[actix_rt::test]
113    async fn survey_works_pg() {
114        let data = crate::tests::pg::get_data().await;
115        survey_registration_works(data.clone()).await;
116        survey_works(data).await;
117    }
118
119    #[actix_rt::test]
120    async fn survey_works_maria() {
121        let data = crate::tests::maria::get_data().await;
122        survey_registration_works(data.clone()).await;
123        survey_works(data).await;
124    }
125
126    pub async fn survey_registration_works(data: ArcData) {
127        let data = &data;
128        let app = get_app!(data).await;
129
130        let survey_instance_url = "http://survey_registration_works.survey.example.org";
131
132        let key = get_random(20);
133
134        let msg = SurveySecretUpload {
135            auth_token: key.clone(),
136            secret: get_random(32),
137        };
138
139        // should fail with ServiceError::WrongPassword since auth token is not loaded into
140        // keystore
141        bad_post_req_test_no_auth(
142            data,
143            V1_API_ROUTES.survey.secret,
144            &msg,
145            errors::ServiceError::WrongPassword,
146        )
147        .await;
148
149        // load auth token into key store, should succeed
150        data.survey_secrets
151            .set(key.clone(), survey_instance_url.to_owned());
152        let resp = test::call_service(
153            &app,
154            post_request!(&msg, V1_API_ROUTES.survey.secret).to_request(),
155        )
156        .await;
157        assert_eq!(resp.status(), StatusCode::OK);
158        // uploaded secret must be in keystore
159        assert_eq!(
160            data.survey_secrets.get(survey_instance_url).unwrap(),
161            msg.secret
162        );
163
164        // should fail since mCaptcha/survey secret upload auth tokens are single-use
165        bad_post_req_test_no_auth(
166            data,
167            V1_API_ROUTES.survey.secret,
168            &msg,
169            errors::ServiceError::WrongPassword,
170        )
171        .await;
172    }
173
174    pub async fn survey_works(data: ArcData) {
175        const NAME: &str = "survetuseranalytics";
176        const PASSWORD: &str = "longpassworddomain";
177        const EMAIL: &str = "survetuseranalytics@a.com";
178        let data = &data;
179
180        delete_user(data, NAME).await;
181
182        register_and_signin(data, NAME, EMAIL, PASSWORD).await;
183        // create captcha
184        let (_, _signin_resp, key) = add_levels_util(data, NAME, PASSWORD).await;
185        let app = get_app!(data).await;
186
187        let page = 1;
188        let tmp_id = uuid::Uuid::new_v4();
189        let download_rotue = V1_API_ROUTES
190            .survey
191            .get_download_route(&tmp_id.to_string(), page);
192
193        let download_req = test::call_service(
194            &app,
195            test::TestRequest::get().uri(&download_rotue).to_request(),
196        )
197        .await;
198        assert_eq!(download_req.status(), StatusCode::NOT_FOUND);
199
200        data.db
201            .analytics_create_psuedo_id_if_not_exists(&key.key)
202            .await
203            .unwrap();
204
205        let psuedo_id = data
206            .db
207            .analytics_get_psuedo_id_from_capmaign_id(&key.key)
208            .await
209            .unwrap();
210
211        for i in 0..60 {
212            println!("[{i}] Saving analytics");
213            let analytics = db_core::CreatePerformanceAnalytics {
214                time: 0,
215                difficulty_factor: 0,
216                worker_type: "wasm".into(),
217            };
218            data.db.analysis_save(&key.key, &analytics).await.unwrap();
219        }
220
221        for p in 1..3 {
222            let download_rotue = V1_API_ROUTES.survey.get_download_route(&psuedo_id, p);
223            println!("page={p}, download={download_rotue}");
224
225            let download_req = test::call_service(
226                &app,
227                test::TestRequest::get().uri(&download_rotue).to_request(),
228            )
229            .await;
230            assert_eq!(download_req.status(), StatusCode::OK);
231            let analytics: Vec<db_core::PerformanceAnalytics> =
232                test::read_body_json(download_req).await;
233            if p == 1 {
234                assert_eq!(analytics.len(), 50);
235            } else if p == 2 {
236                assert_eq!(analytics.len(), 10);
237            } else {
238                assert_eq!(analytics.len(), 0);
239            }
240        }
241
242        let download_rotue = V1_API_ROUTES.survey.get_download_route(&psuedo_id, 0);
243        data.db
244            .analytics_delete_all_records_for_campaign(&key.key)
245            .await
246            .unwrap();
247
248        let download_req = test::call_service(
249            &app,
250            test::TestRequest::get().uri(&download_rotue).to_request(),
251        )
252        .await;
253        assert_eq!(download_req.status(), StatusCode::NOT_FOUND);
254    }
255}