feat: REST endpoint to read file in gist repository

SUMMARY
    Optionally authenticated endpoint to read file in gist repository.
    Subject to gist visibility.

DESCRIPTION
    crate::api::v1::gists::read_file
	- Parses URI for gist owner, gist public ID and file path.
	- Contents of gists with private visibility are only returned
	  to owner of gists. Identity determined using session cookies.
	- Contents are Unlisted and Public visibility gists are
	  accessible by both unauthenticated and authenticated
	  users(authenticated but not owner).
master
Aravinth Manivannan 2022-02-18 22:55:01 +05:30
parent 3b4d944291
commit a8718dc73e
Signed by: realaravinth
GPG Key ID: AD9F0F08E855ED88
1 changed files with 247 additions and 52 deletions

View File

@ -20,16 +20,12 @@ use actix_web::*;
use db_core::prelude::*;
use serde::{Deserialize, Serialize};
use super::routes::GetFilePath;
use crate::data::api::v1::gists::{CreateGist, FileInfo, GistID};
use crate::errors::*;
use crate::utils::escape_spaces;
use crate::*;
//#[derive(Serialize, Deserialize, Debug, Clone)]
//pub struct File {
// pub filename: String,
// pub content: ContentType,
//}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CreateGistRequest {
#[serde(skip_serializing_if = "Option::is_none")]
@ -50,6 +46,7 @@ impl CreateGistRequest {
pub fn services(cfg: &mut web::ServiceConfig) {
cfg.service(new);
cfg.service(get_file);
}
#[my_codegen::post(
@ -77,68 +74,266 @@ async fn new(
.finish())
}
#[my_codegen::get(path = "crate::V1_API_ROUTES.gist.get_file")]
async fn get_file(
path: web::Path<GetFilePath>,
data: AppData,
id: Identity,
db: crate::DB,
) -> ServiceResult<impl Responder> {
let gist = db.get_gist(&path.gist).await?;
match gist.visibility {
GistVisibility::Public | GistVisibility::Unlisted => {
let contents = data
.read_file(
db.as_ref(),
GistID::ID(&gist.public_id),
&escape_spaces(&path.file),
)
.await?;
Ok(HttpResponse::Ok().json(contents))
}
GistVisibility::Private => {
if let Some(username) = id.identity() {
if gist.owner == username {
let contents = data
.read_file(
db.as_ref(),
GistID::ID(&gist.public_id),
&escape_spaces(&path.file),
)
.await?;
return Ok(HttpResponse::Ok().json(contents));
}
};
Err(ServiceError::GistNotFound)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::data::api::v1::gists::{ContentType, FileType};
use crate::tests::*;
use crate::utils::escape_spaces;
#[actix_rt::test]
async fn test_new_gist_works() {
let config = [
sqlx_postgres::get_data().await,
sqlx_sqlite::get_data().await,
async fn test_new_gist_works_postgres() {
let (db, data) = sqlx_postgres::get_data().await;
new_gist_test_runner(&data, &db).await;
}
#[actix_rt::test]
async fn test_new_gist_works_sqlite() {
let (db, data) = sqlx_sqlite::get_data().await;
new_gist_test_runner(&data, &db).await;
}
async fn new_gist_test_runner(data: &Arc<Data>, db: &BoxDB) {
const NAME: &str = "httpgisttestuser";
const NAME2: &str = "httpgisttestuser2";
const EMAIL: &str = "httpgisttestuser@sss.com";
const EMAIL2: &str = "httpgisttestuse2r@sss.com";
const PASSWORD: &str = "longpassword2";
let _ = futures::join!(
data.delete_user(db, NAME, PASSWORD),
data.delete_user(db, NAME2, PASSWORD),
);
let (_creds, signin_resp2) = data.register_and_signin(db, NAME2, EMAIL2, PASSWORD).await;
let cookies2 = get_cookie!(signin_resp2);
let (_creds, signin_resp) = data.register_and_signin(db, NAME, EMAIL, PASSWORD).await;
let cookies = get_cookie!(signin_resp);
let app = get_app!(data, db).await;
let files = [
FileInfo {
filename: "foo".into(),
content: FileType::File(ContentType::Text("foobar".into())),
},
FileInfo {
filename: "bar".into(),
content: FileType::File(ContentType::Text("foobar".into())),
},
FileInfo {
filename: "foo bar".into(),
content: FileType::File(ContentType::Text("foobar".into())),
},
];
for (db, data) in config.iter() {
const NAME: &str = "httpgisttestuser";
const EMAIL: &str = "httpgisttestuser@sss.com";
const PASSWORD: &str = "longpassword2";
let create_gist_msg = CreateGistRequest {
description: None,
visibility: GistVisibility::Public,
files: files.to_vec(),
};
let _ = futures::join!(data.delete_user(db, NAME, PASSWORD),);
let create_gist_resp = test::call_service(
&app,
post_request!(&create_gist_msg, V1_API_ROUTES.gist.new)
.cookie(cookies.clone())
.to_request(),
)
.await;
assert_eq!(create_gist_resp.status(), StatusCode::TEMPORARY_REDIRECT);
let (_creds, signin_resp) = data.register_and_signin(db, NAME, EMAIL, PASSWORD).await;
let cookies = get_cookie!(signin_resp);
let app = get_app!(data, db).await;
let gist_id = create_gist_resp
.headers()
.get(http::header::LOCATION)
.unwrap()
.to_str()
.unwrap();
data.gist_created_test_helper(db, gist_id, NAME).await;
data.gist_files_written_helper(db, gist_id, &files).await;
let files = [
FileInfo {
filename: "foo".into(),
content: FileType::File(ContentType::Text("foobar".into())),
},
FileInfo {
filename: "bar".into(),
content: FileType::File(ContentType::Text("foobar".into())),
},
FileInfo {
filename: "foo bar".into(),
content: FileType::File(ContentType::Text("foobar".into())),
},
];
// get gists
// 1. Public gists
let mut get_file_path = GetFilePath {
username: NAME.into(),
gist: gist_id.into(),
file: "".into(),
};
for file in files.iter() {
// with owner cookies
get_file_path.file = file.filename.clone();
let path = V1_API_ROUTES.gist.get_file_route(&get_file_path);
println!("Trying to get file {path}");
let resp = get_request!(&app, &path, cookies.clone());
assert_eq!(resp.status(), StatusCode::OK);
let content: FileInfo = test::read_body_json(resp).await;
let create_gist_msg = CreateGistRequest {
description: None,
visibility: GistVisibility::Public,
files: files.to_vec(),
let req_escaped_file = FileInfo {
filename: escape_spaces(&file.filename),
content: file.content.clone(),
};
assert_eq!(&content, &req_escaped_file);
let create_gist_resp = test::call_service(
&app,
post_request!(&create_gist_msg, V1_API_ROUTES.gist.new)
.cookie(cookies)
.to_request(),
)
.await;
assert_eq!(create_gist_resp.status(), StatusCode::TEMPORARY_REDIRECT);
// unauthenticated user
let resp = get_request!(&app, &path);
assert_eq!(resp.status(), StatusCode::OK);
let content: FileInfo = test::read_body_json(resp).await;
let req_escaped_file = FileInfo {
filename: escape_spaces(&file.filename),
content: file.content.clone(),
};
assert_eq!(&content, &req_escaped_file);
let gist_id = create_gist_resp
.headers()
.get(http::header::LOCATION)
.unwrap()
.to_str()
.unwrap();
data.gist_created_test_helper(db, gist_id, NAME).await;
data.gist_files_written_helper(db, gist_id, &files).await;
// non-owner user
let resp = get_request!(&app, &path, cookies2.clone());
assert_eq!(resp.status(), StatusCode::OK);
let content: FileInfo = test::read_body_json(resp).await;
let req_escaped_file = FileInfo {
filename: escape_spaces(&file.filename),
content: file.content.clone(),
};
assert_eq!(&content, &req_escaped_file);
}
// 2. Unlisted gists
let one_file = [files[0].clone()];
let mut msg = CreateGistRequest {
description: None,
visibility: GistVisibility::Unlisted,
files: one_file.to_vec(),
};
let create_gist_resp = test::call_service(
&app,
post_request!(&msg, V1_API_ROUTES.gist.new)
.cookie(cookies.clone())
.to_request(),
)
.await;
assert_eq!(create_gist_resp.status(), StatusCode::TEMPORARY_REDIRECT);
let unlisted = create_gist_resp
.headers()
.get(http::header::LOCATION)
.unwrap()
.to_str()
.unwrap();
get_file_path.gist = unlisted.into();
for file in one_file.iter() {
// requesting user is owner
get_file_path.file = file.filename.clone();
let path = V1_API_ROUTES.gist.get_file_route(&get_file_path);
println!("Trying to get file {path}");
let resp = get_request!(&app, &path, cookies.clone());
assert_eq!(resp.status(), StatusCode::OK);
let content: FileInfo = test::read_body_json(resp).await;
let req_escaped_file = FileInfo {
filename: escape_spaces(&file.filename),
content: file.content.clone(),
};
assert_eq!(&content, &req_escaped_file);
// unauthenticated
let resp = get_request!(&app, &path);
assert_eq!(resp.status(), StatusCode::OK);
let content: FileInfo = test::read_body_json(resp).await;
let req_escaped_file = FileInfo {
filename: escape_spaces(&file.filename),
content: file.content.clone(),
};
assert_eq!(&content, &req_escaped_file);
// requesting user is not owner
let resp = get_request!(&app, &path, cookies2.clone());
assert_eq!(resp.status(), StatusCode::OK);
let content: FileInfo = test::read_body_json(resp).await;
let req_escaped_file = FileInfo {
filename: escape_spaces(&file.filename),
content: file.content.clone(),
};
assert_eq!(&content, &req_escaped_file);
}
// 2. Private gists
msg.visibility = GistVisibility::Private;
let create_gist_resp = test::call_service(
&app,
post_request!(&msg, V1_API_ROUTES.gist.new)
.cookie(cookies.clone())
.to_request(),
)
.await;
assert_eq!(create_gist_resp.status(), StatusCode::TEMPORARY_REDIRECT);
let unlisted = create_gist_resp
.headers()
.get(http::header::LOCATION)
.unwrap()
.to_str()
.unwrap();
get_file_path.gist = unlisted.into();
for file in one_file.iter() {
get_file_path.file = file.filename.clone();
let path = V1_API_ROUTES.gist.get_file_route(&get_file_path);
println!("Trying to get file {path}");
// requesting user is owner
let resp = get_request!(&app, &path, cookies.clone());
assert_eq!(resp.status(), StatusCode::OK);
let content: FileInfo = test::read_body_json(resp).await;
let req_escaped_file = FileInfo {
filename: escape_spaces(&file.filename),
content: file.content.clone(),
};
assert_eq!(&content, &req_escaped_file);
// requesting user is unauthenticated
let resp = get_request!(&app, &path);
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
let txt: ErrorToResponse = test::read_body_json(resp).await;
assert_eq!(txt.error, format!("{}", ServiceError::GistNotFound));
// requesting user is not owner
let resp = get_request!(&app, &path, cookies2.clone());
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
let txt: ErrorToResponse = test::read_body_json(resp).await;
assert_eq!(txt.error, format!("{}", ServiceError::GistNotFound));
}
}
}