Merge pull request #284 from taskchampion/issue128
Add integration test for replica/server interactions
This commit is contained in:
136
Cargo.lock
generated
136
Cargo.lock
generated
@@ -1,5 +1,7 @@
|
|||||||
# This file is automatically @generated by Cargo.
|
# This file is automatically @generated by Cargo.
|
||||||
# It is not intended for manual editing.
|
# It is not intended for manual editing.
|
||||||
|
version = 3
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "actix-codec"
|
name = "actix-codec"
|
||||||
version = "0.3.0"
|
version = "0.3.0"
|
||||||
@@ -12,7 +14,7 @@ dependencies = [
|
|||||||
"futures-sink",
|
"futures-sink",
|
||||||
"log",
|
"log",
|
||||||
"pin-project 0.4.28",
|
"pin-project 0.4.28",
|
||||||
"tokio 0.2.25",
|
"tokio",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -23,7 +25,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "177837a10863f15ba8d3ae3ec12fac1099099529ed20083a27fdfe247381d0dc"
|
checksum = "177837a10863f15ba8d3ae3ec12fac1099099529ed20083a27fdfe247381d0dc"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"actix-codec",
|
"actix-codec",
|
||||||
"actix-rt 1.1.1",
|
"actix-rt",
|
||||||
"actix-service",
|
"actix-service",
|
||||||
"actix-utils",
|
"actix-utils",
|
||||||
"derive_more",
|
"derive_more",
|
||||||
@@ -43,7 +45,7 @@ checksum = "452299e87817ae5673910e53c243484ca38be3828db819b6011736fc6982e874"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"actix-codec",
|
"actix-codec",
|
||||||
"actix-connect",
|
"actix-connect",
|
||||||
"actix-rt 1.1.1",
|
"actix-rt",
|
||||||
"actix-service",
|
"actix-service",
|
||||||
"actix-threadpool",
|
"actix-threadpool",
|
||||||
"actix-utils",
|
"actix-utils",
|
||||||
@@ -92,16 +94,6 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "actix-macros"
|
|
||||||
version = "0.2.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "c2f86cd6857c135e6e9fe57b1619a88d1f94a7df34c00e11fe13e64fd3438837"
|
|
||||||
dependencies = [
|
|
||||||
"quote",
|
|
||||||
"syn",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "actix-router"
|
name = "actix-router"
|
||||||
version = "0.2.7"
|
version = "0.2.7"
|
||||||
@@ -121,24 +113,13 @@ version = "1.1.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "143fcc2912e0d1de2bcf4e2f720d2a60c28652ab4179685a1ee159e0fb3db227"
|
checksum = "143fcc2912e0d1de2bcf4e2f720d2a60c28652ab4179685a1ee159e0fb3db227"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"actix-macros 0.1.3",
|
"actix-macros",
|
||||||
"actix-threadpool",
|
"actix-threadpool",
|
||||||
"copyless",
|
"copyless",
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"tokio 0.2.25",
|
"tokio",
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "actix-rt"
|
|
||||||
version = "2.2.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "bc7d7cd957c9ed92288a7c3c96af81fa5291f65247a76a34dac7b6af74e52ba0"
|
|
||||||
dependencies = [
|
|
||||||
"actix-macros 0.2.1",
|
|
||||||
"futures-core",
|
|
||||||
"tokio 1.6.2",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -148,13 +129,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "45407e6e672ca24784baa667c5d32ef109ccdd8d5e0b5ebb9ef8a67f4dfb708e"
|
checksum = "45407e6e672ca24784baa667c5d32ef109ccdd8d5e0b5ebb9ef8a67f4dfb708e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"actix-codec",
|
"actix-codec",
|
||||||
"actix-rt 1.1.1",
|
"actix-rt",
|
||||||
"actix-service",
|
"actix-service",
|
||||||
"actix-utils",
|
"actix-utils",
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"log",
|
"log",
|
||||||
"mio 0.6.23",
|
"mio",
|
||||||
"mio-uds",
|
"mio-uds",
|
||||||
"num_cpus",
|
"num_cpus",
|
||||||
"slab",
|
"slab",
|
||||||
@@ -177,8 +158,8 @@ version = "1.0.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "47239ca38799ab74ee6a8a94d1ce857014b2ac36f242f70f3f75a66f691e791c"
|
checksum = "47239ca38799ab74ee6a8a94d1ce857014b2ac36f242f70f3f75a66f691e791c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"actix-macros 0.1.3",
|
"actix-macros",
|
||||||
"actix-rt 1.1.1",
|
"actix-rt",
|
||||||
"actix-server",
|
"actix-server",
|
||||||
"actix-service",
|
"actix-service",
|
||||||
"log",
|
"log",
|
||||||
@@ -219,7 +200,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "2e9022dec56632d1d7979e59af14f0597a28a830a9c1c7fec8b2327eb9f16b5a"
|
checksum = "2e9022dec56632d1d7979e59af14f0597a28a830a9c1c7fec8b2327eb9f16b5a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"actix-codec",
|
"actix-codec",
|
||||||
"actix-rt 1.1.1",
|
"actix-rt",
|
||||||
"actix-service",
|
"actix-service",
|
||||||
"bitflags",
|
"bitflags",
|
||||||
"bytes 0.5.6",
|
"bytes 0.5.6",
|
||||||
@@ -240,9 +221,9 @@ checksum = "e641d4a172e7faa0862241a20ff4f1f5ab0ab7c279f00c2d4587b77483477b86"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"actix-codec",
|
"actix-codec",
|
||||||
"actix-http",
|
"actix-http",
|
||||||
"actix-macros 0.1.3",
|
"actix-macros",
|
||||||
"actix-router",
|
"actix-router",
|
||||||
"actix-rt 1.1.1",
|
"actix-rt",
|
||||||
"actix-server",
|
"actix-server",
|
||||||
"actix-service",
|
"actix-service",
|
||||||
"actix-testing",
|
"actix-testing",
|
||||||
@@ -400,7 +381,7 @@ checksum = "b381e490e7b0cfc37ebc54079b0413d8093ef43d14a4e4747083f7fa47a9e691"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"actix-codec",
|
"actix-codec",
|
||||||
"actix-http",
|
"actix-http",
|
||||||
"actix-rt 1.1.1",
|
"actix-rt",
|
||||||
"actix-service",
|
"actix-service",
|
||||||
"base64 0.13.0",
|
"base64 0.13.0",
|
||||||
"bytes 0.5.6",
|
"bytes 0.5.6",
|
||||||
@@ -1252,7 +1233,7 @@ dependencies = [
|
|||||||
"http",
|
"http",
|
||||||
"indexmap",
|
"indexmap",
|
||||||
"slab",
|
"slab",
|
||||||
"tokio 0.2.25",
|
"tokio",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-futures",
|
"tracing-futures",
|
||||||
@@ -1430,7 +1411,7 @@ dependencies = [
|
|||||||
"itoa",
|
"itoa",
|
||||||
"pin-project 1.0.7",
|
"pin-project 1.0.7",
|
||||||
"socket2",
|
"socket2",
|
||||||
"tokio 0.2.25",
|
"tokio",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
"tracing",
|
"tracing",
|
||||||
"want",
|
"want",
|
||||||
@@ -1741,7 +1722,7 @@ dependencies = [
|
|||||||
"serde_json",
|
"serde_json",
|
||||||
"shlex",
|
"shlex",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"tokio 0.2.25",
|
"tokio",
|
||||||
"toml",
|
"toml",
|
||||||
"warp",
|
"warp",
|
||||||
]
|
]
|
||||||
@@ -1791,25 +1772,12 @@ dependencies = [
|
|||||||
"kernel32-sys",
|
"kernel32-sys",
|
||||||
"libc",
|
"libc",
|
||||||
"log",
|
"log",
|
||||||
"miow 0.2.2",
|
"miow",
|
||||||
"net2",
|
"net2",
|
||||||
"slab",
|
"slab",
|
||||||
"winapi 0.2.8",
|
"winapi 0.2.8",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "mio"
|
|
||||||
version = "0.7.13"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "8c2bdb6314ec10835cd3293dd268473a835c02b7b352e788be788b3c6ca6bb16"
|
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
"log",
|
|
||||||
"miow 0.3.7",
|
|
||||||
"ntapi",
|
|
||||||
"winapi 0.3.9",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mio-extras"
|
name = "mio-extras"
|
||||||
version = "2.0.6"
|
version = "2.0.6"
|
||||||
@@ -1818,7 +1786,7 @@ checksum = "52403fe290012ce777c4626790c8951324a2b9e3316b3143779c72b029742f19"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"lazycell",
|
"lazycell",
|
||||||
"log",
|
"log",
|
||||||
"mio 0.6.23",
|
"mio",
|
||||||
"slab",
|
"slab",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -1830,7 +1798,7 @@ checksum = "afcb699eb26d4332647cc848492bbc15eafb26f08d0304550d5aa1f612e066f0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"iovec",
|
"iovec",
|
||||||
"libc",
|
"libc",
|
||||||
"mio 0.6.23",
|
"mio",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1845,15 +1813,6 @@ dependencies = [
|
|||||||
"ws2_32-sys",
|
"ws2_32-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "miow"
|
|
||||||
version = "0.3.7"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21"
|
|
||||||
dependencies = [
|
|
||||||
"winapi 0.3.9",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "net2"
|
name = "net2"
|
||||||
version = "0.2.37"
|
version = "0.2.37"
|
||||||
@@ -1913,21 +1872,12 @@ dependencies = [
|
|||||||
"fsevent-sys",
|
"fsevent-sys",
|
||||||
"inotify",
|
"inotify",
|
||||||
"libc",
|
"libc",
|
||||||
"mio 0.6.23",
|
"mio",
|
||||||
"mio-extras",
|
"mio-extras",
|
||||||
"walkdir",
|
"walkdir",
|
||||||
"winapi 0.3.9",
|
"winapi 0.3.9",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "ntapi"
|
|
||||||
version = "0.3.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "3f6bb902e437b6d86e03cce10a7e2af662292c5dfef23b65899ea3ac9354ad44"
|
|
||||||
dependencies = [
|
|
||||||
"winapi 0.3.9",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-integer"
|
name = "num-integer"
|
||||||
version = "0.1.44"
|
version = "0.1.44"
|
||||||
@@ -2470,6 +2420,18 @@ dependencies = [
|
|||||||
"winapi 0.3.9",
|
"winapi 0.3.9",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "replica-server-tests"
|
||||||
|
version = "0.3.0"
|
||||||
|
dependencies = [
|
||||||
|
"actix-rt",
|
||||||
|
"actix-web",
|
||||||
|
"anyhow",
|
||||||
|
"taskchampion",
|
||||||
|
"taskchampion-sync-server",
|
||||||
|
"tempfile",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "resolv-conf"
|
name = "resolv-conf"
|
||||||
version = "0.7.0"
|
version = "0.7.0"
|
||||||
@@ -3002,7 +2964,7 @@ dependencies = [
|
|||||||
name = "taskchampion-sync-server"
|
name = "taskchampion-sync-server"
|
||||||
version = "0.3.0"
|
version = "0.3.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"actix-rt 2.2.0",
|
"actix-rt",
|
||||||
"actix-web",
|
"actix-web",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"clap",
|
"clap",
|
||||||
@@ -3209,7 +3171,7 @@ dependencies = [
|
|||||||
"lazy_static",
|
"lazy_static",
|
||||||
"libc",
|
"libc",
|
||||||
"memchr",
|
"memchr",
|
||||||
"mio 0.6.23",
|
"mio",
|
||||||
"mio-uds",
|
"mio-uds",
|
||||||
"pin-project-lite 0.1.12",
|
"pin-project-lite 0.1.12",
|
||||||
"signal-hook-registry",
|
"signal-hook-registry",
|
||||||
@@ -3218,22 +3180,6 @@ dependencies = [
|
|||||||
"winapi 0.3.9",
|
"winapi 0.3.9",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "tokio"
|
|
||||||
version = "1.6.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "aea337f72e96efe29acc234d803a5981cd9a2b6ed21655cd7fc21cfe021e8ec7"
|
|
||||||
dependencies = [
|
|
||||||
"autocfg",
|
|
||||||
"libc",
|
|
||||||
"mio 0.7.13",
|
|
||||||
"once_cell",
|
|
||||||
"parking_lot",
|
|
||||||
"pin-project-lite 0.2.6",
|
|
||||||
"signal-hook-registry",
|
|
||||||
"winapi 0.3.9",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio-macros"
|
name = "tokio-macros"
|
||||||
version = "0.2.6"
|
version = "0.2.6"
|
||||||
@@ -3254,7 +3200,7 @@ dependencies = [
|
|||||||
"futures-util",
|
"futures-util",
|
||||||
"log",
|
"log",
|
||||||
"pin-project 0.4.28",
|
"pin-project 0.4.28",
|
||||||
"tokio 0.2.25",
|
"tokio",
|
||||||
"tungstenite",
|
"tungstenite",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -3269,7 +3215,7 @@ dependencies = [
|
|||||||
"futures-sink",
|
"futures-sink",
|
||||||
"log",
|
"log",
|
||||||
"pin-project-lite 0.1.12",
|
"pin-project-lite 0.1.12",
|
||||||
"tokio 0.2.25",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3351,7 +3297,7 @@ dependencies = [
|
|||||||
"rand 0.7.3",
|
"rand 0.7.3",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"tokio 0.2.25",
|
"tokio",
|
||||||
"url",
|
"url",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -3370,7 +3316,7 @@ dependencies = [
|
|||||||
"resolv-conf",
|
"resolv-conf",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"tokio 0.2.25",
|
"tokio",
|
||||||
"trust-dns-proto",
|
"trust-dns-proto",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -3579,7 +3525,7 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_urlencoded 0.6.1",
|
"serde_urlencoded 0.6.1",
|
||||||
"tokio 0.2.25",
|
"tokio",
|
||||||
"tokio-tungstenite",
|
"tokio-tungstenite",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
"tracing",
|
"tracing",
|
||||||
|
|||||||
@@ -3,5 +3,6 @@
|
|||||||
members = [
|
members = [
|
||||||
"taskchampion",
|
"taskchampion",
|
||||||
"cli",
|
"cli",
|
||||||
"sync-server"
|
"sync-server",
|
||||||
|
"replica-server-tests"
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -27,11 +27,12 @@ Assuming that continues, it is unlikely that TaskChampion will ever be recommend
|
|||||||
|
|
||||||
## Structure
|
## Structure
|
||||||
|
|
||||||
There are three crates here:
|
There are four crates here:
|
||||||
|
|
||||||
* [taskchampion](./taskchampion) - the core of the tool
|
* [taskchampion](./taskchampion) - the core of the tool
|
||||||
* [taskchampion-cli](./cli) - the command-line binary
|
* [taskchampion-cli](./cli) - the command-line binary
|
||||||
* [taskchampion-sync-server](./sync-server) - the server against which `task sync` operates
|
* [taskchampion-sync-server](./sync-server) - the server against which `task sync` operates
|
||||||
|
* [replica-server-tests](./replica-server-tests) - integration tests covering both _taskchampion-cli_ and _taskchampion-sync-server_
|
||||||
|
|
||||||
## Documentation Generation
|
## Documentation Generation
|
||||||
|
|
||||||
|
|||||||
18
replica-server-tests/Cargo.toml
Normal file
18
replica-server-tests/Cargo.toml
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
[package]
|
||||||
|
name = "replica-server-tests"
|
||||||
|
version = "0.3.0"
|
||||||
|
authors = ["Dustin J. Mitchell <dustin@mozilla.com>"]
|
||||||
|
edition = "2018"
|
||||||
|
publish = false
|
||||||
|
|
||||||
|
[dependencies.taskchampion-sync-server]
|
||||||
|
path = "../sync-server"
|
||||||
|
|
||||||
|
[dependencies.taskchampion]
|
||||||
|
path = "../taskchampion"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
anyhow = "1.0"
|
||||||
|
actix-web = "^3.3.2"
|
||||||
|
actix-rt = "^1.1.1"
|
||||||
|
tempfile = "3"
|
||||||
1
replica-server-tests/src/lib.rs
Normal file
1
replica-server-tests/src/lib.rs
Normal file
@@ -0,0 +1 @@
|
|||||||
|
// test-only crate
|
||||||
84
replica-server-tests/tests/cross-sync.rs
Normal file
84
replica-server-tests/tests/cross-sync.rs
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
use actix_web::{App, HttpServer};
|
||||||
|
use taskchampion::{Replica, ServerConfig, Status, StorageConfig, Uuid};
|
||||||
|
use taskchampion_sync_server::{storage::InMemoryStorage, Server};
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn cross_sync() -> anyhow::Result<()> {
|
||||||
|
let server = Server::new(Box::new(InMemoryStorage::new()));
|
||||||
|
let httpserver =
|
||||||
|
HttpServer::new(move || App::new().service(server.service())).bind("0.0.0.0:0")?;
|
||||||
|
|
||||||
|
// bind was to :0, so the kernel will have selected an unused port
|
||||||
|
let port = httpserver.addrs()[0].port();
|
||||||
|
|
||||||
|
httpserver.run();
|
||||||
|
|
||||||
|
// set up two replicas, and demonstrate replication between them
|
||||||
|
let mut rep1 = Replica::new(StorageConfig::InMemory.into_storage()?);
|
||||||
|
let mut rep2 = Replica::new(StorageConfig::InMemory.into_storage()?);
|
||||||
|
|
||||||
|
let client_key = Uuid::new_v4();
|
||||||
|
let encryption_secret = b"abc123".to_vec();
|
||||||
|
let make_server = || {
|
||||||
|
ServerConfig::Remote {
|
||||||
|
origin: format!("http://127.0.0.1:{}", port),
|
||||||
|
client_key,
|
||||||
|
encryption_secret: encryption_secret.clone(),
|
||||||
|
}
|
||||||
|
.into_server()
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut serv1 = make_server()?;
|
||||||
|
let mut serv2 = make_server()?;
|
||||||
|
|
||||||
|
// add some tasks on rep1
|
||||||
|
let t1 = rep1.new_task(Status::Pending, "test 1".into())?;
|
||||||
|
let t2 = rep1.new_task(Status::Pending, "test 2".into())?;
|
||||||
|
|
||||||
|
// modify t1
|
||||||
|
let mut t1 = t1.into_mut(&mut rep1);
|
||||||
|
t1.start()?;
|
||||||
|
let t1 = t1.into_immut();
|
||||||
|
|
||||||
|
rep1.sync(&mut serv1)?;
|
||||||
|
rep2.sync(&mut serv2)?;
|
||||||
|
|
||||||
|
// those tasks should exist on rep2 now
|
||||||
|
let t12 = rep2
|
||||||
|
.get_task(t1.get_uuid())?
|
||||||
|
.expect("expected task 1 on rep2");
|
||||||
|
let t22 = rep2
|
||||||
|
.get_task(t2.get_uuid())?
|
||||||
|
.expect("expected task 2 on rep2");
|
||||||
|
|
||||||
|
assert_eq!(t12.get_description(), "test 1");
|
||||||
|
assert_eq!(t12.is_active(), true);
|
||||||
|
assert_eq!(t22.get_description(), "test 2");
|
||||||
|
assert_eq!(t22.is_active(), false);
|
||||||
|
|
||||||
|
// make non-conflicting changes on the two replicas
|
||||||
|
let mut t2 = t2.into_mut(&mut rep1);
|
||||||
|
t2.set_status(Status::Completed)?;
|
||||||
|
let t2 = t2.into_immut();
|
||||||
|
|
||||||
|
let mut t12 = t12.into_mut(&mut rep2);
|
||||||
|
t12.set_status(Status::Completed)?;
|
||||||
|
|
||||||
|
// sync those changes back and forth
|
||||||
|
rep1.sync(&mut serv1)?; // rep1 -> server
|
||||||
|
rep2.sync(&mut serv2)?; // server -> rep2, rep2 -> server
|
||||||
|
rep1.sync(&mut serv1)?; // server -> rep1
|
||||||
|
|
||||||
|
let t1 = rep1
|
||||||
|
.get_task(t1.get_uuid())?
|
||||||
|
.expect("expected task 1 on rep1");
|
||||||
|
assert_eq!(t1.get_status(), Status::Completed);
|
||||||
|
|
||||||
|
let t22 = rep2
|
||||||
|
.get_task(t2.get_uuid())?
|
||||||
|
.expect("expected task 2 on rep2");
|
||||||
|
assert_eq!(t22.get_status(), Status::Completed);
|
||||||
|
|
||||||
|
// note that we just drop the server here..
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ name = "taskchampion-sync-server"
|
|||||||
version = "0.3.0"
|
version = "0.3.0"
|
||||||
authors = ["Dustin J. Mitchell <dustin@mozilla.com>"]
|
authors = ["Dustin J. Mitchell <dustin@mozilla.com>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
|
publish = false
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
@@ -20,5 +21,5 @@ env_logger = "^0.8.3"
|
|||||||
rusqlite = { version = "0.25", features = ["bundled"] }
|
rusqlite = { version = "0.25", features = ["bundled"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
actix-rt = "^2.2.0"
|
actix-rt = "^1.1.1"
|
||||||
tempfile = "3"
|
tempfile = "3"
|
||||||
|
|||||||
@@ -77,9 +77,8 @@ pub(crate) async fn service(
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use crate::api::ServerState;
|
|
||||||
use crate::app_scope;
|
|
||||||
use crate::storage::{InMemoryStorage, Storage};
|
use crate::storage::{InMemoryStorage, Storage};
|
||||||
|
use crate::Server;
|
||||||
use actix_web::{http::StatusCode, test, App};
|
use actix_web::{http::StatusCode, test, App};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
@@ -88,16 +87,16 @@ mod test {
|
|||||||
let client_key = Uuid::new_v4();
|
let client_key = Uuid::new_v4();
|
||||||
let version_id = Uuid::new_v4();
|
let version_id = Uuid::new_v4();
|
||||||
let parent_version_id = Uuid::new_v4();
|
let parent_version_id = Uuid::new_v4();
|
||||||
let server_box: Box<dyn Storage> = Box::new(InMemoryStorage::new());
|
let storage: Box<dyn Storage> = Box::new(InMemoryStorage::new());
|
||||||
|
|
||||||
// set up the storage contents..
|
// set up the storage contents..
|
||||||
{
|
{
|
||||||
let mut txn = server_box.txn().unwrap();
|
let mut txn = storage.txn().unwrap();
|
||||||
txn.new_client(client_key, Uuid::nil()).unwrap();
|
txn.new_client(client_key, Uuid::nil()).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
let server_state = ServerState::new(server_box);
|
let server = Server::new(storage);
|
||||||
let mut app = test::init_service(App::new().service(app_scope(server_state))).await;
|
let mut app = test::init_service(App::new().service(server.service())).await;
|
||||||
|
|
||||||
let uri = format!("/v1/client/add-version/{}", parent_version_id);
|
let uri = format!("/v1/client/add-version/{}", parent_version_id);
|
||||||
let req = test::TestRequest::post()
|
let req = test::TestRequest::post()
|
||||||
@@ -125,16 +124,16 @@ mod test {
|
|||||||
let client_key = Uuid::new_v4();
|
let client_key = Uuid::new_v4();
|
||||||
let version_id = Uuid::new_v4();
|
let version_id = Uuid::new_v4();
|
||||||
let parent_version_id = Uuid::new_v4();
|
let parent_version_id = Uuid::new_v4();
|
||||||
let server_box: Box<dyn Storage> = Box::new(InMemoryStorage::new());
|
let storage: Box<dyn Storage> = Box::new(InMemoryStorage::new());
|
||||||
|
|
||||||
// set up the storage contents..
|
// set up the storage contents..
|
||||||
{
|
{
|
||||||
let mut txn = server_box.txn().unwrap();
|
let mut txn = storage.txn().unwrap();
|
||||||
txn.new_client(client_key, version_id).unwrap();
|
txn.new_client(client_key, version_id).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
let server_state = ServerState::new(server_box);
|
let server = Server::new(storage);
|
||||||
let mut app = test::init_service(App::new().service(app_scope(server_state))).await;
|
let mut app = test::init_service(App::new().service(server.service())).await;
|
||||||
|
|
||||||
let uri = format!("/v1/client/add-version/{}", parent_version_id);
|
let uri = format!("/v1/client/add-version/{}", parent_version_id);
|
||||||
let req = test::TestRequest::post()
|
let req = test::TestRequest::post()
|
||||||
@@ -159,9 +158,9 @@ mod test {
|
|||||||
async fn test_bad_content_type() {
|
async fn test_bad_content_type() {
|
||||||
let client_key = Uuid::new_v4();
|
let client_key = Uuid::new_v4();
|
||||||
let parent_version_id = Uuid::new_v4();
|
let parent_version_id = Uuid::new_v4();
|
||||||
let server_box: Box<dyn Storage> = Box::new(InMemoryStorage::new());
|
let storage: Box<dyn Storage> = Box::new(InMemoryStorage::new());
|
||||||
let server_state = ServerState::new(server_box);
|
let server = Server::new(storage);
|
||||||
let mut app = test::init_service(App::new().service(app_scope(server_state))).await;
|
let mut app = test::init_service(App::new().service(server.service())).await;
|
||||||
|
|
||||||
let uri = format!("/v1/client/add-version/{}", parent_version_id);
|
let uri = format!("/v1/client/add-version/{}", parent_version_id);
|
||||||
let req = test::TestRequest::post()
|
let req = test::TestRequest::post()
|
||||||
@@ -178,9 +177,9 @@ mod test {
|
|||||||
async fn test_empty_body() {
|
async fn test_empty_body() {
|
||||||
let client_key = Uuid::new_v4();
|
let client_key = Uuid::new_v4();
|
||||||
let parent_version_id = Uuid::new_v4();
|
let parent_version_id = Uuid::new_v4();
|
||||||
let server_box: Box<dyn Storage> = Box::new(InMemoryStorage::new());
|
let storage: Box<dyn Storage> = Box::new(InMemoryStorage::new());
|
||||||
let server_state = ServerState::new(server_box);
|
let server = Server::new(storage);
|
||||||
let mut app = test::init_service(App::new().service(app_scope(server_state))).await;
|
let mut app = test::init_service(App::new().service(server.service())).await;
|
||||||
|
|
||||||
let uri = format!("/v1/client/add-version/{}", parent_version_id);
|
let uri = format!("/v1/client/add-version/{}", parent_version_id);
|
||||||
let req = test::TestRequest::post()
|
let req = test::TestRequest::post()
|
||||||
|
|||||||
@@ -45,8 +45,8 @@ pub(crate) async fn service(
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use crate::api::ServerState;
|
use crate::api::ServerState;
|
||||||
use crate::app_scope;
|
|
||||||
use crate::storage::{InMemoryStorage, Storage};
|
use crate::storage::{InMemoryStorage, Storage};
|
||||||
|
use crate::Server;
|
||||||
use actix_web::{http::StatusCode, test, App};
|
use actix_web::{http::StatusCode, test, App};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
@@ -55,18 +55,18 @@ mod test {
|
|||||||
let client_key = Uuid::new_v4();
|
let client_key = Uuid::new_v4();
|
||||||
let version_id = Uuid::new_v4();
|
let version_id = Uuid::new_v4();
|
||||||
let parent_version_id = Uuid::new_v4();
|
let parent_version_id = Uuid::new_v4();
|
||||||
let server_box: Box<dyn Storage> = Box::new(InMemoryStorage::new());
|
let storage: Box<dyn Storage> = Box::new(InMemoryStorage::new());
|
||||||
|
|
||||||
// set up the storage contents..
|
// set up the storage contents..
|
||||||
{
|
{
|
||||||
let mut txn = server_box.txn().unwrap();
|
let mut txn = storage.txn().unwrap();
|
||||||
txn.new_client(client_key, Uuid::new_v4()).unwrap();
|
txn.new_client(client_key, Uuid::new_v4()).unwrap();
|
||||||
txn.add_version(client_key, version_id, parent_version_id, b"abcd".to_vec())
|
txn.add_version(client_key, version_id, parent_version_id, b"abcd".to_vec())
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
let server_state = ServerState::new(server_box);
|
let server = Server::new(storage);
|
||||||
let mut app = test::init_service(App::new().service(app_scope(server_state))).await;
|
let mut app = test::init_service(App::new().service(server.service())).await;
|
||||||
|
|
||||||
let uri = format!("/v1/client/get-child-version/{}", parent_version_id);
|
let uri = format!("/v1/client/get-child-version/{}", parent_version_id);
|
||||||
let req = test::TestRequest::get()
|
let req = test::TestRequest::get()
|
||||||
@@ -97,9 +97,9 @@ mod test {
|
|||||||
async fn test_client_not_found() {
|
async fn test_client_not_found() {
|
||||||
let client_key = Uuid::new_v4();
|
let client_key = Uuid::new_v4();
|
||||||
let parent_version_id = Uuid::new_v4();
|
let parent_version_id = Uuid::new_v4();
|
||||||
let server_box: Box<dyn Storage> = Box::new(InMemoryStorage::new());
|
let storage: Box<dyn Storage> = Box::new(InMemoryStorage::new());
|
||||||
let server_state = ServerState::new(server_box);
|
let server = Server::new(storage);
|
||||||
let mut app = test::init_service(App::new().service(app_scope(server_state))).await;
|
let mut app = test::init_service(App::new().service(server.service())).await;
|
||||||
|
|
||||||
let uri = format!("/v1/client/get-child-version/{}", parent_version_id);
|
let uri = format!("/v1/client/get-child-version/{}", parent_version_id);
|
||||||
let req = test::TestRequest::get()
|
let req = test::TestRequest::get()
|
||||||
@@ -116,15 +116,15 @@ mod test {
|
|||||||
async fn test_version_not_found() {
|
async fn test_version_not_found() {
|
||||||
let client_key = Uuid::new_v4();
|
let client_key = Uuid::new_v4();
|
||||||
let parent_version_id = Uuid::new_v4();
|
let parent_version_id = Uuid::new_v4();
|
||||||
let server_box: Box<dyn Storage> = Box::new(InMemoryStorage::new());
|
let storage: Box<dyn Storage> = Box::new(InMemoryStorage::new());
|
||||||
|
|
||||||
// create the client, but not the version
|
// create the client, but not the version
|
||||||
{
|
{
|
||||||
let mut txn = server_box.txn().unwrap();
|
let mut txn = storage.txn().unwrap();
|
||||||
txn.new_client(client_key, Uuid::new_v4()).unwrap();
|
txn.new_client(client_key, Uuid::new_v4()).unwrap();
|
||||||
}
|
}
|
||||||
let server_state = ServerState::new(server_box);
|
let server = Server::new(storage);
|
||||||
let mut app = test::init_service(App::new().service(app_scope(server_state))).await;
|
let mut app = test::init_service(App::new().service(server.service())).await;
|
||||||
|
|
||||||
let uri = format!("/v1/client/get-child-version/{}", parent_version_id);
|
let uri = format!("/v1/client/get-child-version/{}", parent_version_id);
|
||||||
let req = test::TestRequest::get()
|
let req = test::TestRequest::get()
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ pub(crate) const CLIENT_KEY_HEADER: &str = "X-Client-Key";
|
|||||||
pub(crate) const PARENT_VERSION_ID_HEADER: &str = "X-Parent-Version-Id";
|
pub(crate) const PARENT_VERSION_ID_HEADER: &str = "X-Parent-Version-Id";
|
||||||
|
|
||||||
/// The type containing a reference to the Storage object in the Actix state.
|
/// The type containing a reference to the Storage object in the Actix state.
|
||||||
pub(crate) type ServerState = Arc<Box<dyn Storage>>;
|
pub(crate) type ServerState = Arc<dyn Storage>;
|
||||||
|
|
||||||
pub(crate) fn api_scope() -> Scope {
|
pub(crate) fn api_scope() -> Scope {
|
||||||
web::scope("")
|
web::scope("")
|
||||||
|
|||||||
@@ -1,29 +1,9 @@
|
|||||||
#![deny(clippy::all)]
|
#![deny(clippy::all)]
|
||||||
|
|
||||||
use crate::storage::{SqliteStorage, Storage};
|
use actix_web::{middleware::Logger, App, HttpServer};
|
||||||
use actix_web::{get, middleware::Logger, web, App, HttpServer, Responder, Scope};
|
|
||||||
use api::{api_scope, ServerState};
|
|
||||||
use clap::Arg;
|
use clap::Arg;
|
||||||
|
use taskchampion_sync_server::storage::SqliteStorage;
|
||||||
mod api;
|
use taskchampion_sync_server::Server;
|
||||||
mod server;
|
|
||||||
mod storage;
|
|
||||||
|
|
||||||
// TODO: use hawk to sign requests
|
|
||||||
|
|
||||||
#[get("/")]
|
|
||||||
async fn index() -> impl Responder {
|
|
||||||
format!("TaskChampion sync server v{}", env!("CARGO_PKG_VERSION"))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Return a scope defining the URL rules for this server, with access to
|
|
||||||
/// the given ServerState.
|
|
||||||
pub(crate) fn app_scope(server_state: ServerState) -> Scope {
|
|
||||||
web::scope("")
|
|
||||||
.data(server_state)
|
|
||||||
.service(index)
|
|
||||||
.service(api_scope())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[actix_web::main]
|
#[actix_web::main]
|
||||||
async fn main() -> anyhow::Result<()> {
|
async fn main() -> anyhow::Result<()> {
|
||||||
@@ -56,33 +36,26 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
let data_dir = matches.value_of("data-dir").unwrap();
|
let data_dir = matches.value_of("data-dir").unwrap();
|
||||||
let port = matches.value_of("port").unwrap();
|
let port = matches.value_of("port").unwrap();
|
||||||
|
|
||||||
let server_box: Box<dyn Storage> = Box::new(SqliteStorage::new(data_dir)?);
|
let server = Server::new(Box::new(SqliteStorage::new(data_dir)?));
|
||||||
let server_state = ServerState::new(server_box);
|
|
||||||
|
|
||||||
log::warn!("Serving on port {}", port);
|
log::warn!("Serving on port {}", port);
|
||||||
HttpServer::new(move || {
|
HttpServer::new(move || App::new().wrap(Logger::default()).service(server.service()))
|
||||||
App::new()
|
.bind(format!("0.0.0.0:{}", port))?
|
||||||
.wrap(Logger::default())
|
.run()
|
||||||
.service(app_scope(server_state.clone()))
|
.await?;
|
||||||
})
|
|
||||||
.bind(format!("0.0.0.0:{}", port))?
|
|
||||||
.run()
|
|
||||||
.await?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::api::ServerState;
|
|
||||||
use crate::storage::{InMemoryStorage, Storage};
|
|
||||||
use actix_web::{test, App};
|
use actix_web::{test, App};
|
||||||
|
use taskchampion_sync_server::storage::{InMemoryStorage, Storage};
|
||||||
|
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
async fn test_index_get() {
|
async fn test_index_get() {
|
||||||
let server_box: Box<dyn Storage> = Box::new(InMemoryStorage::new());
|
let server = Server::new(Box::new(InMemoryStorage::new()));
|
||||||
let server_state = ServerState::new(server_box);
|
let mut app = test::init_service(App::new().service(server.service())).await;
|
||||||
let mut app = test::init_service(App::new().service(app_scope(server_state))).await;
|
|
||||||
|
|
||||||
let req = test::TestRequest::get().uri("/").to_request();
|
let req = test::TestRequest::get().uri("/").to_request();
|
||||||
let resp = test::call_service(&mut app, req).await;
|
let resp = test::call_service(&mut app, req).await;
|
||||||
37
sync-server/src/lib.rs
Normal file
37
sync-server/src/lib.rs
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
#![deny(clippy::all)]
|
||||||
|
|
||||||
|
mod api;
|
||||||
|
mod server;
|
||||||
|
pub mod storage;
|
||||||
|
|
||||||
|
use crate::storage::Storage;
|
||||||
|
use actix_web::{get, web, Responder, Scope};
|
||||||
|
use api::{api_scope, ServerState};
|
||||||
|
|
||||||
|
#[get("/")]
|
||||||
|
async fn index() -> impl Responder {
|
||||||
|
format!("TaskChampion sync server v{}", env!("CARGO_PKG_VERSION"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A Server represents a sync server.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Server {
|
||||||
|
storage: ServerState,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Server {
|
||||||
|
/// Create a new sync server with the given storage implementation.
|
||||||
|
pub fn new(storage: Box<dyn Storage>) -> Self {
|
||||||
|
Self {
|
||||||
|
storage: storage.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get an Actix-web service for this server.
|
||||||
|
pub fn service(&self) -> Scope {
|
||||||
|
web::scope("")
|
||||||
|
.data(self.storage.clone())
|
||||||
|
.service(index)
|
||||||
|
.service(api_scope())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,10 +10,11 @@ struct Inner {
|
|||||||
versions: HashMap<(Uuid, Uuid), Version>,
|
versions: HashMap<(Uuid, Uuid), Version>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) struct InMemoryStorage(Mutex<Inner>);
|
pub struct InMemoryStorage(Mutex<Inner>);
|
||||||
|
|
||||||
impl InMemoryStorage {
|
impl InMemoryStorage {
|
||||||
pub(crate) fn new() -> Self {
|
#[allow(clippy::new_without_default)]
|
||||||
|
pub fn new() -> Self {
|
||||||
Self(Mutex::new(Inner {
|
Self(Mutex::new(Inner {
|
||||||
clients: HashMap::new(),
|
clients: HashMap::new(),
|
||||||
versions: HashMap::new(),
|
versions: HashMap::new(),
|
||||||
|
|||||||
@@ -1,27 +1,28 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(debug_assertions)]
|
||||||
mod inmemory;
|
mod inmemory;
|
||||||
#[cfg(test)]
|
|
||||||
pub(crate) use inmemory::InMemoryStorage;
|
#[cfg(debug_assertions)]
|
||||||
|
pub use inmemory::InMemoryStorage;
|
||||||
|
|
||||||
mod sqlite;
|
mod sqlite;
|
||||||
pub(crate) use self::sqlite::SqliteStorage;
|
pub use self::sqlite::SqliteStorage;
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
|
#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
|
||||||
pub(crate) struct Client {
|
pub struct Client {
|
||||||
pub(crate) latest_version_id: Uuid,
|
pub latest_version_id: Uuid,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
|
#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
|
||||||
pub(crate) struct Version {
|
pub struct Version {
|
||||||
pub(crate) version_id: Uuid,
|
pub version_id: Uuid,
|
||||||
pub(crate) parent_version_id: Uuid,
|
pub parent_version_id: Uuid,
|
||||||
pub(crate) history_segment: Vec<u8>,
|
pub history_segment: Vec<u8>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) trait StorageTxn {
|
pub trait StorageTxn {
|
||||||
/// Get information about the given client
|
/// Get information about the given client
|
||||||
fn get_client(&mut self, client_key: Uuid) -> anyhow::Result<Option<Client>>;
|
fn get_client(&mut self, client_key: Uuid) -> anyhow::Result<Option<Client>>;
|
||||||
|
|
||||||
@@ -58,7 +59,7 @@ pub(crate) trait StorageTxn {
|
|||||||
|
|
||||||
/// A trait for objects able to act as storage. Most of the interesting behavior is in the
|
/// A trait for objects able to act as storage. Most of the interesting behavior is in the
|
||||||
/// [`crate::storage::StorageTxn`] trait.
|
/// [`crate::storage::StorageTxn`] trait.
|
||||||
pub(crate) trait Storage: Send + Sync {
|
pub trait Storage: Send + Sync {
|
||||||
/// Begin a transaction
|
/// Begin a transaction
|
||||||
fn txn<'a>(&'a self) -> anyhow::Result<Box<dyn StorageTxn + 'a>>;
|
fn txn<'a>(&'a self) -> anyhow::Result<Box<dyn StorageTxn + 'a>>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ impl ToSql for Client {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// An on-disk storage backend which uses SQLite
|
/// An on-disk storage backend which uses SQLite
|
||||||
pub(crate) struct SqliteStorage {
|
pub struct SqliteStorage {
|
||||||
db_file: std::path::PathBuf,
|
db_file: std::path::PathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user