use mockito::{mock, Mock, Matcher};
use serde_json::{json, Value};
pub struct ApiMockBuilder {
method: String,
path: String,
status: usize,
response_body: Option<Value>,
headers: Vec<(String, String)>,
}
impl ApiMockBuilder {
pub fn new(method: &str, path: &str) -> Self {
Self {
method: method.to_string(),
path: path.to_string(),
status: 200,
response_body: None,
headers: vec![("content-type".to_string(), "application/json".to_string())],
}
}
pub fn with_status(mut self, status: usize) -> Self {
self.status = status;
self
}
pub fn with_body(mut self, body: Value) -> Self {
self.response_body = Some(body);
self
}
pub fn with_header(mut self, key: &str, value: &str) -> Self {
self.headers.push((key.to_string(), value.to_string()));
self
}
pub fn build(self) -> Mock {
let mut mock = mock(self.method.as_str(), self.path.as_str())
.with_status(self.status);
for (key, value) in self.headers {
mock = mock.with_header(key.as_str(), value.as_str());
}
if let Some(body) = self.response_body {
mock = mock.with_body(body.to_string());
}
mock.create()
}
}
pub fn mock_authenticated_user() -> Mock {
ApiMockBuilder::new("GET", "/user")
.with_body(json!({
"user": {
"id": 12345,
"username": "test_user",
"email": "test@example.com",
"color": "#FF0000",
"profilePicture": null,
"initials": "TU",
"week_start_day": 1,
"global_font_support": false,
"timezone": "UTC"
}
}))
.build()
}
pub fn mock_authorized_teams() -> Mock {
ApiMockBuilder::new("GET", "/team")
.with_body(json!({
"teams": [
{
"id": "123",
"name": "Test Team",
"color": "#FF0000",
"avatar": null,
"members": []
}
]
}))
.build()
}
pub fn mock_unauthorized_error() -> Mock {
ApiMockBuilder::new("GET", "/user")
.with_status(401)
.with_body(json!({
"err": "Token is invalid",
"ECODE": "OAUTH_021"
}))
.build()
}
pub fn mock_rate_limit_error(path: &str) -> Mock {
ApiMockBuilder::new("GET", path)
.with_status(429)
.with_header("x-ratelimit-limit", "100")
.with_header("x-ratelimit-remaining", "0")
.with_header("x-ratelimit-reset", "1567780450")
.with_body(json!({
"err": "Rate limit exceeded",
"ECODE": "RATE_LIMIT"
}))
.build()
}
pub fn mock_internal_server_error(path: &str) -> Mock {
ApiMockBuilder::new("GET", path)
.with_status(500)
.with_body(json!({
"err": "Internal server error",
"ECODE": "SERVER_ERROR"
}))
.build()
}
pub fn mock_spaces(team_id: &str) -> Mock {
ApiMockBuilder::new("GET", format!("/team/{}/space", team_id).as_str())
.with_body(json!({
"spaces": [
{
"id": "789",
"name": "Test Space",
"private": false,
"statuses": [
{
"id": "open",
"status": "Open",
"type": "open",
"orderindex": 0,
"color": "#d3d3d3"
}
]
}
]
}))
.build()
}
pub fn mock_folders(space_id: &str) -> Mock {
ApiMockBuilder::new("GET", format!("/space/{}/folder", space_id).as_str())
.with_body(json!({
"folders": [
{
"id": "111",
"name": "Test Folder",
"orderindex": 0,
"override_statuses": false,
"hidden": false
}
]
}))
.build()
}
pub fn mock_lists(folder_id: &str) -> Mock {
ApiMockBuilder::new("GET", format!("/folder/{}/list", folder_id).as_str())
.with_body(json!({
"lists": [
{
"id": "222",
"name": "Test List",
"orderindex": 1,
"status": {
"status": "active",
"color": "#00FF00"
}
}
]
}))
.build()
}
pub fn mock_tasks(list_id: &str) -> Mock {
ApiMockBuilder::new("GET", format!("/list/{}/task", list_id).as_str())
.with_body(json!({
"tasks": [
{
"id": "task123",
"name": "Test Task",
"description": "Task description",
"status": {
"id": "open",
"status": "Open",
"color": "#d3d3d3"
},
"assignees": [],
"tags": [],
"priority": null,
"due_date": null,
"archived": false
}
]
}))
.build()
}
pub fn mock_create_task(list_id: &str, expected_body: Value) -> Mock {
mock("POST", format!("/list/{}/task", list_id).as_str())
.match_header("authorization", "Bearer test_token")
.match_header("content-type", "application/json")
.match_body(Matcher::Json(expected_body))
.with_status(200)
.with_header("content-type", "application/json")
.with_body(json!({
"id": "new_task_id",
"name": "New Task",
"status": {
"id": "open",
"status": "Open"
}
}).to_string())
.create()
}
pub fn mock_update_task(task_id: &str, expected_body: Value) -> Mock {
mock("PUT", format!("/task/{}", task_id).as_str())
.match_header("authorization", "Bearer test_token")
.match_header("content-type", "application/json")
.match_body(Matcher::Json(expected_body))
.with_status(200)
.with_header("content-type", "application/json")
.with_body(json!({
"id": task_id,
"name": "Updated Task",
"status": {
"id": "complete",
"status": "Complete"
}
}).to_string())
.create()
}
pub fn mock_delete_task(task_id: &str) -> Mock {
mock("DELETE", format!("/task/{}", task_id).as_str())
.match_header("authorization", "Bearer test_token")
.with_status(204)
.create()
}
pub struct MockServer {
mocks: Vec<Mock>,
}
impl MockServer {
pub fn new() -> Self {
Self { mocks: Vec::new() }
}
pub fn add_mock(mut self, mock: Mock) -> Self {
self.mocks.push(mock);
self
}
pub fn with_authenticated_user(self) -> Self {
self.add_mock(mock_authenticated_user())
}
pub fn with_teams(self) -> Self {
self.add_mock(mock_authorized_teams())
}
pub fn with_spaces(self, team_id: &str) -> Self {
self.add_mock(mock_spaces(team_id))
}
pub fn with_folders(self, space_id: &str) -> Self {
self.add_mock(mock_folders(space_id))
}
pub fn with_lists(self, folder_id: &str) -> Self {
self.add_mock(mock_lists(folder_id))
}
pub fn with_tasks(self, list_id: &str) -> Self {
self.add_mock(mock_tasks(list_id))
}
pub fn build(self) -> Vec<Mock> {
self.mocks
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_api_mock_builder() {
let _mock = ApiMockBuilder::new("GET", "/test")
.with_status(201)
.with_header("x-custom", "value")
.with_body(json!({ "test": "data" }))
.build();
}
#[test]
fn test_mock_server_builder() {
let _mocks = MockServer::new()
.with_authenticated_user()
.with_teams()
.with_spaces("123")
.build();
assert!(!_mocks.is_empty());
}
}