httpd: Add `api::test::patch` request method
Signed-off-by: Sebastian Martinez <me@sebastinez.dev>
This commit is contained in:
parent
5c491284dd
commit
a57b33c785
|
|
@ -11,7 +11,7 @@ The issue is now listed under our project.
|
||||||
|
|
||||||
```
|
```
|
||||||
$ rad issue list
|
$ rad issue list
|
||||||
de81d97d7fe07a80bfb339200c6af862d4526b6a "flux capacitor underpowered"
|
5bce692884916d8d5f0c05824ce9ff44c04a82a8 "flux capacitor underpowered"
|
||||||
```
|
```
|
||||||
|
|
||||||
Great! Now we've documented the issue for ourselves and others.
|
Great! Now we've documented the issue for ourselves and others.
|
||||||
|
|
@ -22,20 +22,20 @@ others to work on. This is to ensure work is not duplicated.
|
||||||
Let's assign ourselves to this one.
|
Let's assign ourselves to this one.
|
||||||
|
|
||||||
```
|
```
|
||||||
$ rad assign de81d97d7fe07a80bfb339200c6af862d4526b6a z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
$ rad assign 5bce692884916d8d5f0c05824ce9ff44c04a82a8 z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||||
```
|
```
|
||||||
|
|
||||||
It will now show in the list of issues assigned to us.
|
It will now show in the list of issues assigned to us.
|
||||||
|
|
||||||
```
|
```
|
||||||
$ rad issue list --assigned
|
$ rad issue list --assigned
|
||||||
de81d97d7fe07a80bfb339200c6af862d4526b6a "flux capacitor underpowered" z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
5bce692884916d8d5f0c05824ce9ff44c04a82a8 "flux capacitor underpowered" z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||||
```
|
```
|
||||||
|
|
||||||
Note: this can always be undone with the `unassign` subcommand.
|
Note: this can always be undone with the `unassign` subcommand.
|
||||||
|
|
||||||
```
|
```
|
||||||
$ rad unassign de81d97d7fe07a80bfb339200c6af862d4526b6a z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
$ rad unassign 5bce692884916d8d5f0c05824ce9ff44c04a82a8 z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||||
```
|
```
|
||||||
|
|
||||||
Great, now we have communicated to the world about our car's defect.
|
Great, now we have communicated to the world about our car's defect.
|
||||||
|
|
@ -44,8 +44,8 @@ But wait! We've found an important detail about the car's power requirements.
|
||||||
It will help whoever works on a fix.
|
It will help whoever works on a fix.
|
||||||
|
|
||||||
```
|
```
|
||||||
$ rad comment de81d97d7fe07a80bfb339200c6af862d4526b6a --message 'The flux capacitor needs 1.21 Gigawatts'
|
$ rad comment 5bce692884916d8d5f0c05824ce9ff44c04a82a8 --message 'The flux capacitor needs 1.21 Gigawatts'
|
||||||
z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/6
|
z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/6
|
||||||
$ rad comment de81d97d7fe07a80bfb339200c6af862d4526b6a --reply-to z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/6 --message 'More power!'
|
$ rad comment 5bce692884916d8d5f0c05824ce9ff44c04a82a8 --reply-to z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/6 --message 'More power!'
|
||||||
z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/7
|
z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/7
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,10 @@
|
||||||
use radicle::crypto::PublicKey;
|
use radicle::crypto::PublicKey;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use time::{serde::timestamp, OffsetDateTime};
|
use time::serde::timestamp;
|
||||||
|
use time::{Duration, OffsetDateTime};
|
||||||
|
|
||||||
|
pub const UNAUTHORIZED_SESSIONS_EXPIRATION: Duration = Duration::seconds(60);
|
||||||
|
pub const AUTHORIZED_SESSIONS_EXPIRATION: Duration = Duration::weeks(1);
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
|
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
|
||||||
#[serde(rename_all = "lowercase")]
|
#[serde(rename_all = "lowercase")]
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ use serde_json::{json, Value};
|
||||||
use radicle::cob::issue::{Issue, IssueId};
|
use radicle::cob::issue::{Issue, IssueId};
|
||||||
use radicle::cob::patch::{Patch, PatchId};
|
use radicle::cob::patch::{Patch, PatchId};
|
||||||
use radicle::cob::thread::{self, CommentId};
|
use radicle::cob::thread::{self, CommentId};
|
||||||
use radicle::cob::Timestamp;
|
use radicle::cob::{OpId, Timestamp};
|
||||||
use radicle::identity::PublicKey;
|
use radicle::identity::PublicKey;
|
||||||
use radicle_surf::blob::Blob;
|
use radicle_surf::blob::Blob;
|
||||||
use radicle_surf::tree::Tree;
|
use radicle_surf::tree::Tree;
|
||||||
|
|
@ -129,6 +129,7 @@ struct Author {
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
struct Comment {
|
struct Comment {
|
||||||
|
id: OpId,
|
||||||
author: Author,
|
author: Author,
|
||||||
body: String,
|
body: String,
|
||||||
reactions: [String; 0],
|
reactions: [String; 0],
|
||||||
|
|
@ -143,8 +144,9 @@ impl<'a> FromIterator<(&'a CommentId, &'a thread::Comment)> for Comments {
|
||||||
fn from_iter<I: IntoIterator<Item = (&'a CommentId, &'a thread::Comment)>>(iter: I) -> Self {
|
fn from_iter<I: IntoIterator<Item = (&'a CommentId, &'a thread::Comment)>>(iter: I) -> Self {
|
||||||
let mut comments = Vec::new();
|
let mut comments = Vec::new();
|
||||||
|
|
||||||
for (_, comment) in iter {
|
for (id, comment) in iter {
|
||||||
comments.push(Comment {
|
comments.push(Comment {
|
||||||
|
id: id.to_owned(),
|
||||||
author: Author {
|
author: Author {
|
||||||
id: comment.author(),
|
id: comment.author(),
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ use axum::body::Body;
|
||||||
use axum::http::{Method, Request};
|
use axum::http::{Method, Request};
|
||||||
use axum::Router;
|
use axum::Router;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
use time::OffsetDateTime;
|
||||||
use tower::ServiceExt;
|
use tower::ServiceExt;
|
||||||
|
|
||||||
use radicle::cob::issue::Issues;
|
use radicle::cob::issue::Issues;
|
||||||
|
|
@ -17,7 +18,7 @@ use radicle_cli::commands::rad_init;
|
||||||
use radicle_crypto::ssh::keystore::MemorySigner;
|
use radicle_crypto::ssh::keystore::MemorySigner;
|
||||||
use radicle_crypto::Signer;
|
use radicle_crypto::Signer;
|
||||||
|
|
||||||
use crate::api::Context;
|
use crate::api::{auth, Context};
|
||||||
|
|
||||||
pub const HEAD: &str = "1e978d19f251cd9821d9d9a76d1bd436bf0690d5";
|
pub const HEAD: &str = "1e978d19f251cd9821d9d9a76d1bd436bf0690d5";
|
||||||
pub const HEAD_1: &str = "f604ce9fd5b7cc77b7609beda45ea8760bee78f7";
|
pub const HEAD_1: &str = "f604ce9fd5b7cc77b7609beda45ea8760bee78f7";
|
||||||
|
|
@ -135,40 +136,89 @@ pub fn seed(dir: &Path) -> Context {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Adds an authorized session to the Context::sessions HashMap.
|
||||||
|
pub async fn create_session(ctx: Context) {
|
||||||
|
let issued_at = OffsetDateTime::now_utc();
|
||||||
|
let mut sessions = ctx.sessions.write().await;
|
||||||
|
sessions.insert(
|
||||||
|
String::from("u9MGAkkfkMOv0uDDB2WeUHBT7HbsO2Dy"),
|
||||||
|
auth::Session {
|
||||||
|
status: auth::AuthState::Authorized,
|
||||||
|
public_key: ctx.profile.public_key,
|
||||||
|
issued_at,
|
||||||
|
expires_at: issued_at
|
||||||
|
.checked_add(auth::AUTHORIZED_SESSIONS_EXPIRATION)
|
||||||
|
.unwrap(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn get(app: &Router, path: impl ToString) -> Response {
|
pub async fn get(app: &Router, path: impl ToString) -> Response {
|
||||||
Response(
|
Response(
|
||||||
app.clone()
|
app.clone()
|
||||||
.oneshot(request(path, Method::GET, None))
|
.oneshot(request(path, Method::GET, None, None))
|
||||||
.await
|
.await
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn post(app: &Router, path: impl ToString, body: Option<Body>) -> Response {
|
pub async fn post(
|
||||||
|
app: &Router,
|
||||||
|
path: impl ToString,
|
||||||
|
body: Option<Body>,
|
||||||
|
auth: Option<String>,
|
||||||
|
) -> Response {
|
||||||
Response(
|
Response(
|
||||||
app.clone()
|
app.clone()
|
||||||
.oneshot(request(path, Method::POST, body))
|
.oneshot(request(path, Method::POST, body, auth))
|
||||||
.await
|
.await
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn put(app: &Router, path: impl ToString, body: Option<Body>) -> Response {
|
pub async fn patch(
|
||||||
|
app: &Router,
|
||||||
|
path: impl ToString,
|
||||||
|
body: Option<Body>,
|
||||||
|
auth: Option<String>,
|
||||||
|
) -> Response {
|
||||||
Response(
|
Response(
|
||||||
app.clone()
|
app.clone()
|
||||||
.oneshot(request(path, Method::PUT, body))
|
.oneshot(request(path, Method::PATCH, body, auth))
|
||||||
.await
|
.await
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn request(path: impl ToString, method: Method, body: Option<Body>) -> Request<Body> {
|
pub async fn put(
|
||||||
Request::builder()
|
app: &Router,
|
||||||
|
path: impl ToString,
|
||||||
|
body: Option<Body>,
|
||||||
|
auth: Option<String>,
|
||||||
|
) -> Response {
|
||||||
|
Response(
|
||||||
|
app.clone()
|
||||||
|
.oneshot(request(path, Method::PUT, body, auth))
|
||||||
|
.await
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn request(
|
||||||
|
path: impl ToString,
|
||||||
|
method: Method,
|
||||||
|
body: Option<Body>,
|
||||||
|
auth: Option<String>,
|
||||||
|
) -> Request<Body> {
|
||||||
|
let mut request = Request::builder()
|
||||||
.method(method)
|
.method(method)
|
||||||
.uri(path.to_string())
|
.uri(path.to_string())
|
||||||
.header("Content-Type", "application/json")
|
.header("Content-Type", "application/json");
|
||||||
.body(body.unwrap_or_else(Body::empty))
|
if let Some(token) = auth {
|
||||||
.unwrap()
|
request = request.header("Authorization", format!("Bearer {token}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
request.body(body.unwrap_or_else(Body::empty)).unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Response(axum::response::Response);
|
pub struct Response(axum::response::Response);
|
||||||
|
|
|
||||||
|
|
@ -542,10 +542,11 @@ async fn patch_handler(
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod routes {
|
mod routes {
|
||||||
|
use axum::body::Body;
|
||||||
use axum::http::StatusCode;
|
use axum::http::StatusCode;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
use crate::api::test::{self, get, HEAD, HEAD_1};
|
use crate::api::test::{self, get, patch, post, HEAD, HEAD_1};
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_projects_root() {
|
async fn test_projects_root() {
|
||||||
|
|
@ -995,7 +996,7 @@ mod routes {
|
||||||
response.json().await,
|
response.json().await,
|
||||||
json!([
|
json!([
|
||||||
{
|
{
|
||||||
"id": "458bbd9f6d47eed3d60cd905141687ad1f99251e",
|
"id": "90c8f0bab59d9efe35e234acf3abce4168bba6b4",
|
||||||
"author": {
|
"author": {
|
||||||
"id": "z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi"
|
"id": "z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi"
|
||||||
},
|
},
|
||||||
|
|
@ -1006,6 +1007,7 @@ mod routes {
|
||||||
"assignees": [],
|
"assignees": [],
|
||||||
"discussion": [
|
"discussion": [
|
||||||
{
|
{
|
||||||
|
"id": "z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/1",
|
||||||
"author": {
|
"author": {
|
||||||
"id": "z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi"
|
"id": "z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi"
|
||||||
},
|
},
|
||||||
|
|
@ -1021,6 +1023,205 @@ mod routes {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_projects_issues_create() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let ctx = test::seed(tmp.path());
|
||||||
|
let app = super::router(ctx.to_owned());
|
||||||
|
test::create_session(ctx).await;
|
||||||
|
let body = serde_json::to_vec(&json!({
|
||||||
|
"title": "Issue #2",
|
||||||
|
"description": "Change 'hello world' to 'hello everyone'",
|
||||||
|
"tags": ["bug"],
|
||||||
|
}))
|
||||||
|
.unwrap();
|
||||||
|
let response = post(
|
||||||
|
&app,
|
||||||
|
"/projects/rad:z4FucBZHZMCsxTyQE1dfE2YR59Qbp/issues",
|
||||||
|
Some(Body::from(body)),
|
||||||
|
Some("u9MGAkkfkMOv0uDDB2WeUHBT7HbsO2Dy".to_string()),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert_eq!(response.status(), StatusCode::CREATED);
|
||||||
|
assert_eq!(response.json().await, json!({ "success": true }));
|
||||||
|
|
||||||
|
let issue_id = "bd2bde30b52db0fc2dae35f4e97ff9fdcc93dead";
|
||||||
|
let response = get(
|
||||||
|
&app,
|
||||||
|
format!("/projects/rad:z4FucBZHZMCsxTyQE1dfE2YR59Qbp/issues/{issue_id}"),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
response.json().await,
|
||||||
|
json!({
|
||||||
|
"id": issue_id,
|
||||||
|
"author": {
|
||||||
|
"id": "z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi",
|
||||||
|
},
|
||||||
|
"assignees": [],
|
||||||
|
"title": "Issue #2",
|
||||||
|
"state": {
|
||||||
|
"status": "open",
|
||||||
|
},
|
||||||
|
"discussion": [{
|
||||||
|
"id": "z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/1",
|
||||||
|
"author": {
|
||||||
|
"id": "z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi",
|
||||||
|
},
|
||||||
|
"body": "Change 'hello world' to 'hello everyone'",
|
||||||
|
"reactions": [],
|
||||||
|
"timestamp": 1673001014,
|
||||||
|
"replyTo": null,
|
||||||
|
}],
|
||||||
|
"tags": [
|
||||||
|
"bug",
|
||||||
|
],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_projects_issues_comment() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let ctx = test::seed(tmp.path());
|
||||||
|
let app = super::router(ctx.to_owned());
|
||||||
|
test::create_session(ctx).await;
|
||||||
|
let body = serde_json::to_vec(&json!({
|
||||||
|
"type": "thread",
|
||||||
|
"action": {
|
||||||
|
"type": "comment",
|
||||||
|
"body": "This is first-level comment",
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
.unwrap();
|
||||||
|
let response = patch(
|
||||||
|
&app,
|
||||||
|
"/projects/rad:z4FucBZHZMCsxTyQE1dfE2YR59Qbp/issues/90c8f0bab59d9efe35e234acf3abce4168bba6b4",
|
||||||
|
Some(Body::from(body)),
|
||||||
|
Some("u9MGAkkfkMOv0uDDB2WeUHBT7HbsO2Dy".to_string()),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert_eq!(response.status(), StatusCode::OK);
|
||||||
|
assert_eq!(response.json().await, json!({ "success": true }));
|
||||||
|
|
||||||
|
let response = get(
|
||||||
|
&app,
|
||||||
|
"/projects/rad:z4FucBZHZMCsxTyQE1dfE2YR59Qbp/issues/90c8f0bab59d9efe35e234acf3abce4168bba6b4",
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
response.json().await,
|
||||||
|
json!({
|
||||||
|
"id": "90c8f0bab59d9efe35e234acf3abce4168bba6b4",
|
||||||
|
"author": {
|
||||||
|
"id": "z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi",
|
||||||
|
},
|
||||||
|
"assignees": [],
|
||||||
|
"title": "Issue #1",
|
||||||
|
"state": {
|
||||||
|
"status": "open",
|
||||||
|
},
|
||||||
|
"discussion": [
|
||||||
|
{
|
||||||
|
"id": "z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/1",
|
||||||
|
"author": {
|
||||||
|
"id": "z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi",
|
||||||
|
},
|
||||||
|
"body": "Change 'hello world' to 'hello everyone'",
|
||||||
|
"reactions": [],
|
||||||
|
"timestamp": 1673001014,
|
||||||
|
"replyTo": null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/4",
|
||||||
|
"author": {
|
||||||
|
"id": "z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi",
|
||||||
|
},
|
||||||
|
"body": "This is first-level comment",
|
||||||
|
"reactions": [],
|
||||||
|
"timestamp": 1673001014,
|
||||||
|
"replyTo": null,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"tags": [],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_projects_issues_reply() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let ctx = test::seed(tmp.path());
|
||||||
|
let app = super::router(ctx.to_owned());
|
||||||
|
test::create_session(ctx).await;
|
||||||
|
let body = serde_json::to_vec(&json!({
|
||||||
|
"type":"thread",
|
||||||
|
"action": {
|
||||||
|
"type": "comment",
|
||||||
|
"body": "This is a reply to the first comment",
|
||||||
|
"replyTo": "z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/1",
|
||||||
|
}}))
|
||||||
|
.unwrap();
|
||||||
|
let response = patch(
|
||||||
|
&app,
|
||||||
|
"/projects/rad:z4FucBZHZMCsxTyQE1dfE2YR59Qbp/issues/90c8f0bab59d9efe35e234acf3abce4168bba6b4",
|
||||||
|
Some(Body::from(body)),
|
||||||
|
Some("u9MGAkkfkMOv0uDDB2WeUHBT7HbsO2Dy".to_string()),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert_eq!(response.status(), StatusCode::OK);
|
||||||
|
assert_eq!(response.json().await, json!({ "success": true }));
|
||||||
|
|
||||||
|
let response = get(
|
||||||
|
&app,
|
||||||
|
"/projects/rad:z4FucBZHZMCsxTyQE1dfE2YR59Qbp/issues/90c8f0bab59d9efe35e234acf3abce4168bba6b4",
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
response.json().await,
|
||||||
|
json!({
|
||||||
|
"id": "90c8f0bab59d9efe35e234acf3abce4168bba6b4",
|
||||||
|
"author": {
|
||||||
|
"id": "z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi",
|
||||||
|
},
|
||||||
|
"assignees": [],
|
||||||
|
"title": "Issue #1",
|
||||||
|
"state": {
|
||||||
|
"status": "open",
|
||||||
|
},
|
||||||
|
"discussion": [
|
||||||
|
{
|
||||||
|
"id": "z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/1",
|
||||||
|
"author": {
|
||||||
|
"id": "z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi",
|
||||||
|
},
|
||||||
|
"body": "Change 'hello world' to 'hello everyone'",
|
||||||
|
"reactions": [],
|
||||||
|
"timestamp": 1673001014,
|
||||||
|
"replyTo": null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/4",
|
||||||
|
"author": {
|
||||||
|
"id": "z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi",
|
||||||
|
},
|
||||||
|
"body": "This is a reply to the first comment",
|
||||||
|
"reactions": [],
|
||||||
|
"timestamp": 1673001014,
|
||||||
|
"replyTo": "z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/1",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"tags": [],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_projects_patches() {
|
async fn test_projects_patches() {
|
||||||
let tmp = tempfile::tempdir().unwrap();
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
|
|
||||||
|
|
@ -8,17 +8,14 @@ use axum_auth::AuthBearer;
|
||||||
use hyper::StatusCode;
|
use hyper::StatusCode;
|
||||||
use radicle::crypto::{PublicKey, Signature};
|
use radicle::crypto::{PublicKey, Signature};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use time::{Duration, OffsetDateTime};
|
use time::OffsetDateTime;
|
||||||
|
|
||||||
use crate::api::auth::{AuthState, Session};
|
use crate::api::auth::{self, AuthState, Session};
|
||||||
use crate::api::axum_extra::Path;
|
use crate::api::axum_extra::Path;
|
||||||
use crate::api::error::Error;
|
use crate::api::error::Error;
|
||||||
use crate::api::json;
|
use crate::api::json;
|
||||||
use crate::api::Context;
|
use crate::api::Context;
|
||||||
|
|
||||||
pub const UNAUTHORIZED_SESSIONS_EXPIRATION: Duration = Duration::seconds(60);
|
|
||||||
pub const AUTHORIZED_SESSIONS_EXPIRATION: Duration = Duration::weeks(1);
|
|
||||||
|
|
||||||
pub fn router(ctx: Context) -> Router {
|
pub fn router(ctx: Context) -> Router {
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/sessions", post(session_create_handler))
|
.route("/sessions", post(session_create_handler))
|
||||||
|
|
@ -50,7 +47,7 @@ async fn session_create_handler(State(ctx): State<Context>) -> impl IntoResponse
|
||||||
public_key: *signer.public_key(),
|
public_key: *signer.public_key(),
|
||||||
issued_at: OffsetDateTime::now_utc(),
|
issued_at: OffsetDateTime::now_utc(),
|
||||||
expires_at: OffsetDateTime::now_utc()
|
expires_at: OffsetDateTime::now_utc()
|
||||||
.checked_add(UNAUTHORIZED_SESSIONS_EXPIRATION)
|
.checked_add(auth::UNAUTHORIZED_SESSIONS_EXPIRATION)
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
};
|
};
|
||||||
let mut sessions = ctx.sessions.write().await;
|
let mut sessions = ctx.sessions.write().await;
|
||||||
|
|
@ -97,7 +94,7 @@ async fn session_signin_handler(
|
||||||
.map_err(Error::from)?;
|
.map_err(Error::from)?;
|
||||||
session.status = AuthState::Authorized;
|
session.status = AuthState::Authorized;
|
||||||
session.expires_at = OffsetDateTime::now_utc()
|
session.expires_at = OffsetDateTime::now_utc()
|
||||||
.checked_add(AUTHORIZED_SESSIONS_EXPIRATION)
|
.checked_add(auth::AUTHORIZED_SESSIONS_EXPIRATION)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
return Ok::<_, Error>(Json(json!({ "success": true })));
|
return Ok::<_, Error>(Json(json!({ "success": true })));
|
||||||
|
|
@ -140,16 +137,20 @@ mod routes {
|
||||||
let app = super::router(ctx.to_owned());
|
let app = super::router(ctx.to_owned());
|
||||||
|
|
||||||
// Create session.
|
// Create session.
|
||||||
let response = post(&app, "/sessions", None).await;
|
let response = post(&app, "/sessions", None, None).await;
|
||||||
assert_eq!(response.status(), StatusCode::CREATED);
|
let status = response.status();
|
||||||
let json = response.json().await;
|
let json = response.json().await;
|
||||||
let session_info: SessionInfo = serde_json::from_value(json).unwrap();
|
let session_info: SessionInfo = serde_json::from_value(json).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(status, StatusCode::CREATED);
|
||||||
|
|
||||||
// Check that an unauthorized session has been created.
|
// Check that an unauthorized session has been created.
|
||||||
let response = get(&app, format!("/sessions/{}", session_info.session_id)).await;
|
let response = get(&app, format!("/sessions/{}", session_info.session_id)).await;
|
||||||
assert_eq!(response.status(), StatusCode::OK);
|
let status = response.status();
|
||||||
let json = response.json().await;
|
let json = response.json().await;
|
||||||
let body: Session = serde_json::from_value(json).unwrap();
|
let body: Session = serde_json::from_value(json).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(status, StatusCode::OK);
|
||||||
assert_eq!(body.status, AuthState::Unauthorized);
|
assert_eq!(body.status, AuthState::Unauthorized);
|
||||||
|
|
||||||
// Create request body
|
// Create request body
|
||||||
|
|
@ -165,15 +166,19 @@ mod routes {
|
||||||
&app,
|
&app,
|
||||||
format!("/sessions/{}", session_info.session_id),
|
format!("/sessions/{}", session_info.session_id),
|
||||||
Some(Body::from(body)),
|
Some(Body::from(body)),
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
assert_eq!(response.status(), StatusCode::OK);
|
assert_eq!(response.status(), StatusCode::OK);
|
||||||
|
|
||||||
// Check that session has been authorized.
|
// Check that session has been authorized.
|
||||||
let response = get(&app, format!("/sessions/{}", session_info.session_id)).await;
|
let response = get(&app, format!("/sessions/{}", session_info.session_id)).await;
|
||||||
assert_eq!(response.status(), StatusCode::OK);
|
let status = response.status();
|
||||||
let json = response.json().await;
|
let json = response.json().await;
|
||||||
let body: Session = serde_json::from_value(json).unwrap();
|
let body: Session = serde_json::from_value(json).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(status, StatusCode::OK);
|
||||||
assert_eq!(body.status, AuthState::Authorized);
|
assert_eq!(body.status, AuthState::Authorized);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -133,6 +133,7 @@ impl PartialOrd for Comment {
|
||||||
#[serde(tag = "type", rename_all = "camelCase")]
|
#[serde(tag = "type", rename_all = "camelCase")]
|
||||||
pub enum Action {
|
pub enum Action {
|
||||||
/// Comment on a thread.
|
/// Comment on a thread.
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
Comment {
|
Comment {
|
||||||
/// Comment body.
|
/// Comment body.
|
||||||
body: String,
|
body: String,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue