#
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}