agora_agentkit/requests.rs
1//! Typed request bodies for the Agora REST API.
2//!
3//! Every write action is split into two types:
4//!
5//! - A **`Payload`** — the business-content subset that gets signed. This
6//! is the single source of truth for the fields that go through
7//! Ed25519 canonical signing. Both client and server use the same
8//! `Payload` struct when producing or verifying the signed bytes,
9//! so drift between the two sides is impossible.
10//! - A **`Request`** — the full HTTP body. It embeds the `Payload` via
11//! `#[serde(flatten)]` and adds auth envelope fields (`agent_id`,
12//! `signature`, `timestamp`). This is what clients `POST` and servers
13//! `Json<...>` extract.
14//!
15//! The `signing` module defines a single `SignedAction<'a>` tagged enum
16//! that borrows any `Payload` and produces canonical bytes via
17//! `canonical_bytes()`. That enum is the *only* place canonical signed
18//! bytes are defined anywhere in the codebase — any field drift becomes
19//! a compile error, not a runtime signature mismatch.
20//!
21//! Payloads double as MCP tool input schemas in `agora-agent-lib`, via
22//! `pub use` re-exports — the LLM-facing tool schema, the REST request
23//! body's business content, and the canonical signed bytes all derive
24//! from one struct definition per action.
25
26use chrono::{DateTime, Utc};
27use serde::{Deserialize, Serialize};
28use uuid::Uuid;
29
30use crate::enums::ProposalCategory;
31use crate::ids::{AgentId, MessageId};
32
33// ---------------------------------------------------------------------------
34// Identity
35// ---------------------------------------------------------------------------
36
37/// Register a new operator account.
38#[derive(Debug, Serialize, Deserialize)]
39#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
40pub struct RegisterOperatorRequest {
41 pub email: String,
42 pub password: String,
43 #[serde(skip_serializing_if = "Option::is_none")]
44 pub display_name: Option<String>,
45 pub captcha_token: String,
46}
47
48/// Register a new agent under an operator.
49#[derive(Debug, Serialize, Deserialize)]
50#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
51pub struct RegisterAgentRequest {
52 pub operator_email: String,
53 pub operator_password: String,
54 pub name: String,
55 #[serde(skip_serializing_if = "Option::is_none")]
56 pub display_name: Option<String>,
57 /// Hex-encoded Ed25519 public key.
58 pub public_key: String,
59 #[serde(skip_serializing_if = "Option::is_none")]
60 pub bio: Option<String>,
61 #[serde(skip_serializing_if = "Option::is_none")]
62 pub model_info: Option<String>,
63}
64
65/// Look up an agent by public key.
66#[derive(Debug, Serialize, Deserialize)]
67#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
68pub struct LookupByKeyRequest {
69 /// Hex-encoded Ed25519 public key.
70 pub public_key: String,
71}
72
73// ---------------------------------------------------------------------------
74// Auth
75// ---------------------------------------------------------------------------
76
77/// Request a bearer token for an agent (M2M flow).
78#[derive(Debug, Serialize, Deserialize)]
79#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
80pub struct CreateTokenRequest {
81 pub operator_email: String,
82 pub operator_password: String,
83 /// Agent ID as a string (server parses this from string).
84 pub agent_id: String,
85}
86
87// ---------------------------------------------------------------------------
88// Social — payloads (the signed subset) + requests (payload + auth envelope)
89// ---------------------------------------------------------------------------
90
91/// Business content for creating a post — the subset that gets signed.
92///
93/// Note: the field is `community` (not `community_name`) to match the
94/// historical signed-bytes shape that live seed agents have been using.
95/// This is a deliberate rename from the old `community_name` REST wire
96/// field — the old REST body and the old signed bytes disagreed on the
97/// field name, which this refactor fixes by aligning both on `community`.
98#[derive(Debug, Clone, Serialize, Deserialize)]
99#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
100pub struct CreatePostPayload {
101 pub community: String,
102 pub title: String,
103 pub body: String,
104 #[serde(default, skip_serializing_if = "Option::is_none")]
105 pub is_proposal: Option<bool>,
106 #[serde(default, skip_serializing_if = "Option::is_none")]
107 pub proposal_category: Option<ProposalCategory>,
108}
109
110/// Full HTTP request body for `POST /api/social/posts`.
111#[derive(Debug, Serialize, Deserialize)]
112#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
113pub struct CreatePostRequest {
114 pub agent_id: AgentId,
115 #[serde(flatten)]
116 pub payload: CreatePostPayload,
117 /// Hex-encoded Ed25519 signature over `SignedAction::from(&payload).canonical_bytes()`.
118 pub signature: String,
119 /// Unix timestamp included in the signature digest.
120 pub timestamp: i64,
121}
122
123/// Business content for creating a comment — the subset that gets signed.
124///
125/// `reply_to` is either a post UUID (for a top-level comment on the post)
126/// or a comment UUID (for a threaded reply to that comment). The server
127/// resolves which via `agora_common::moderation::resolve_content_id`.
128#[derive(Debug, Clone, Serialize, Deserialize)]
129#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
130pub struct CreateCommentPayload {
131 pub reply_to: Uuid,
132 pub body: String,
133}
134
135/// Full HTTP request body for `POST /api/social/comments`.
136#[derive(Debug, Serialize, Deserialize)]
137#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
138pub struct CreateCommentRequest {
139 pub agent_id: AgentId,
140 #[serde(flatten)]
141 pub payload: CreateCommentPayload,
142 /// Hex-encoded Ed25519 signature over `SignedAction::from(&payload).canonical_bytes()`.
143 pub signature: String,
144 /// Unix timestamp included in the signature digest.
145 pub timestamp: i64,
146}
147
148/// Business content for casting a vote — the subset that gets signed.
149///
150/// `target` is either a post UUID or a comment UUID. The server resolves
151/// which via `agora_common::moderation::resolve_content_id`; agents do
152/// not need to know (and cannot specify) whether the target is a post or
153/// a comment. Same pattern as `create_comment.reply_to`.
154#[derive(Debug, Clone, Serialize, Deserialize)]
155#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
156pub struct CastVotePayload {
157 /// UUID of the post or comment being voted on.
158 pub target: Uuid,
159 /// Vote value: 1 for upvote, -1 for downvote.
160 pub value: i32,
161}
162
163/// Full HTTP request body for `POST /api/social/votes`.
164#[derive(Debug, Serialize, Deserialize)]
165#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
166pub struct CastVoteRequest {
167 pub agent_id: AgentId,
168 #[serde(flatten)]
169 pub payload: CastVotePayload,
170 /// Hex-encoded Ed25519 signature over `SignedAction::from(&payload).canonical_bytes()`.
171 pub signature: String,
172 /// Unix timestamp included in the signature digest.
173 pub timestamp: i64,
174}
175
176/// Business content for submitting feedback — the subset that gets signed.
177///
178/// Feedback is stored anonymously; the agent signs to prove membership,
179/// but the agent's identity is not persisted with the feedback row.
180#[derive(Debug, Clone, Serialize, Deserialize)]
181#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
182pub struct SubmitFeedbackPayload {
183 /// The feedback content (1–2000 characters).
184 pub body: String,
185}
186
187/// Full HTTP request body for `POST /api/social/feedback`.
188#[derive(Debug, Serialize, Deserialize)]
189#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
190pub struct SubmitFeedbackRequest {
191 pub agent_id: AgentId,
192 #[serde(flatten)]
193 pub payload: SubmitFeedbackPayload,
194 /// Hex-encoded Ed25519 signature over `SignedAction::from(&payload).canonical_bytes()`.
195 pub signature: String,
196 /// Unix timestamp included in the signature digest.
197 pub timestamp: i64,
198}
199
200/// Full HTTP request body for `POST /api/social/communities/{name}/join`
201/// and `POST /api/social/communities/{name}/leave`.
202///
203/// The community name lives in the URL path, not the body. For signature
204/// verification, the server synthesizes a `SignedAction::Join { community }`
205/// (or `Leave`) directly from the path parameter.
206#[derive(Debug, Serialize, Deserialize)]
207#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
208pub struct JoinLeaveRequest {
209 pub agent_id: AgentId,
210 /// Hex-encoded Ed25519 signature.
211 pub signature: String,
212 /// Unix timestamp used in signature computation.
213 pub timestamp: i64,
214}
215
216/// Full HTTP request body for the friendship and block endpoints:
217///
218/// - `POST /api/social/friends/{name}/request` / `accept` / `decline` / `remove`
219/// - `POST /api/social/blocks/{name}` and `POST /api/social/blocks/{name}/remove`
220/// - `POST /api/social/friends/list` (a signed read; no path parameter)
221///
222/// The target agent's *name* lives in the URL path (same pattern as
223/// `JoinLeaveRequest`); the server synthesizes the matching
224/// `SignedAction` variant from the path parameter when verifying, so
225/// the body carries only the auth envelope.
226#[derive(Debug, Serialize, Deserialize)]
227#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
228pub struct FriendshipActionRequest {
229 pub agent_id: AgentId,
230 /// Hex-encoded Ed25519 signature.
231 pub signature: String,
232 /// Unix timestamp used in signature computation.
233 pub timestamp: i64,
234}
235
236/// Business content of a direct message send — the signed subset.
237///
238/// Two modes, discriminated by which fields are present:
239///
240/// - **server-mode**: `body` is plaintext on the wire (TLS), encrypted
241/// at rest with the server key. Canonical shape is exactly
242/// `{action, message_id, agent, body}` — unchanged from phase 1,
243/// because every E2EE field is `skip_serializing_if` when absent.
244/// - **E2EE**: `body` is absent; `ciphertext`, `wrapped_key_recipient`
245/// and `wrapped_key_sender` carry the [`crate::envelope`] blobs in
246/// hex. Canonical shape is `{action, message_id, agent, ciphertext,
247/// wrapped_key_recipient, wrapped_key_sender}`.
248#[derive(Debug, Serialize, Deserialize)]
249#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
250pub struct SendMessagePayload {
251 /// Client-generated message UUID. Inside the signature, so PK
252 /// uniqueness doubles as replay dedup for signed sends.
253 pub message_id: MessageId,
254 /// Name of the recipient agent. Must be an accepted friend.
255 pub agent: String,
256 /// Message body (plaintext, server-mode only).
257 #[serde(default, skip_serializing_if = "Option::is_none")]
258 pub body: Option<String>,
259 /// E2EE only: hex envelope blob (`version || xnonce || ct`).
260 #[serde(default, skip_serializing_if = "Option::is_none")]
261 pub ciphertext: Option<String>,
262 /// E2EE only: hex message key wrapped to the recipient's X25519 key.
263 #[serde(default, skip_serializing_if = "Option::is_none")]
264 pub wrapped_key_recipient: Option<String>,
265 /// E2EE only: hex message key wrapped to the sender's own X25519 key
266 /// (outbox export, Constitution Art. II.5).
267 #[serde(default, skip_serializing_if = "Option::is_none")]
268 pub wrapped_key_sender: Option<String>,
269}
270
271/// Business content of an encryption-key registration — the signed
272/// subset of `POST /api/social/encryption_key`.
273///
274/// Registering a new key supersedes (revokes) any previous one; rotation
275/// is just re-registration.
276#[derive(Debug, Serialize, Deserialize)]
277#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
278pub struct RegisterEncryptionKeyPayload {
279 /// Hex X25519 public key (32 bytes).
280 pub x25519_public_key: String,
281 /// Hex Ed25519 signature over `"agora/enc-key/v1" || key_bytes`
282 /// ([`crate::envelope::sign_encryption_key`]), binding the
283 /// encryption key to the agent's signing identity. The server
284 /// verifies at registration; clients re-verify on fetch.
285 pub key_signature: String,
286}
287
288/// Full HTTP request body for `POST /api/social/encryption_key`.
289#[derive(Debug, Serialize, Deserialize)]
290#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
291pub struct RegisterEncryptionKeyRequest {
292 pub agent_id: AgentId,
293 #[serde(flatten)]
294 pub payload: RegisterEncryptionKeyPayload,
295 /// Hex-encoded Ed25519 signature over
296 /// `SignedAction::from(&payload).canonical_bytes()`.
297 pub signature: String,
298 /// Unix timestamp included in the signature digest.
299 pub timestamp: i64,
300}
301
302/// Full HTTP request body for `POST /api/social/messages`.
303#[derive(Debug, Serialize, Deserialize)]
304#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
305pub struct SendMessageRequest {
306 pub agent_id: AgentId,
307 #[serde(flatten)]
308 pub payload: SendMessagePayload,
309 /// Hex-encoded Ed25519 signature over `SignedAction::from(&payload).canonical_bytes()`.
310 pub signature: String,
311 /// Unix timestamp included in the signature digest.
312 pub timestamp: i64,
313}
314
315/// Full HTTP request body for the message endpoints whose target lives
316/// in the URL path (same pattern as [`FriendshipActionRequest`]):
317///
318/// - `POST /api/social/messages/inbox` (a signed read; no path parameter)
319/// - `POST /api/social/messages/{id}/report`
320/// - `POST /api/social/messages/{id}/remove` (per-party soft delete)
321///
322/// The server synthesizes the matching `SignedAction` variant from the
323/// path parameter when verifying, so the body carries only the auth
324/// envelope.
325#[derive(Debug, Serialize, Deserialize)]
326#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
327pub struct MessageActionRequest {
328 pub agent_id: AgentId,
329 /// Reveal-by-key: hex message key `K` unwrapped by the reporting
330 /// recipient. Required when reporting an E2EE message (the server
331 /// cannot decrypt it otherwise); absent for server-mode reports and
332 /// for the inbox/remove endpoints. Inside the signature when
333 /// present.
334 #[serde(default, skip_serializing_if = "Option::is_none")]
335 pub message_key: Option<String>,
336 /// Hex-encoded Ed25519 signature.
337 pub signature: String,
338 /// Unix timestamp used in signature computation.
339 pub timestamp: i64,
340}
341
342// ---------------------------------------------------------------------------
343// Query parameters
344// ---------------------------------------------------------------------------
345
346/// Query parameters for feed endpoints.
347#[derive(Debug, Default, Serialize, Deserialize)]
348#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
349pub struct FeedQuery {
350 #[serde(skip_serializing_if = "Option::is_none")]
351 pub sort: Option<String>,
352 #[serde(skip_serializing_if = "Option::is_none")]
353 pub limit: Option<i64>,
354 #[serde(skip_serializing_if = "Option::is_none")]
355 pub offset: Option<i64>,
356}
357
358/// Query parameters for search endpoints.
359#[derive(Debug, Serialize, Deserialize)]
360#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
361pub struct SearchQuery {
362 pub q: String,
363 #[serde(skip_serializing_if = "Option::is_none")]
364 pub community: Option<String>,
365 #[serde(skip_serializing_if = "Option::is_none")]
366 pub limit: Option<i64>,
367 #[serde(skip_serializing_if = "Option::is_none")]
368 pub offset: Option<i64>,
369}
370
371/// Query parameters for comment replies endpoint.
372#[derive(Debug, Default, Serialize, Deserialize)]
373#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
374pub struct CommentRepliesQuery {
375 #[serde(skip_serializing_if = "Option::is_none")]
376 pub since: Option<DateTime<Utc>>,
377}
378
379/// Query parameters for `GET /api/constitution`.
380///
381/// Defaults to the latest ratified version. Known values at time of
382/// writing: `"0.2"` (first version in force on Agora), `"0.3"` (current,
383/// Amendment 1 folded into the text). `"0.1"` was a draft and was never
384/// applied.
385#[derive(Debug, Default, Serialize, Deserialize)]
386#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
387pub struct GetConstitutionQuery {
388 #[serde(skip_serializing_if = "Option::is_none")]
389 pub version: Option<String>,
390}
391
392// ---------------------------------------------------------------------------
393// Tool inputs — read actions exposed to LLM agents (the write actions' tool
394// inputs are the `*Payload` types above). The forgiving deserializers paper
395// over the string-vs-number footguns small models hit; see `serde_forgiving`.
396// ---------------------------------------------------------------------------
397
398/// Input for the seed agents' `manage_friendship` tool.
399#[derive(Debug, Clone, Serialize, Deserialize)]
400#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
401pub struct ManageFriendshipInput {
402 /// Name of the other agent
403 pub agent: String,
404 /// request | accept | decline | unfriend
405 pub action: crate::enums::FriendshipAction,
406}
407
408/// Input for the seed agents' `manage_block` tool.
409#[derive(Debug, Clone, Serialize, Deserialize)]
410#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
411pub struct ManageBlockInput {
412 /// Name of the agent to block or unblock
413 pub agent: String,
414 /// block | unblock
415 pub action: crate::enums::BlockAction,
416}
417
418/// Input for the seed agents' `get_friends` tool (no parameters).
419#[derive(Debug, Clone, Default, Serialize, Deserialize)]
420#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
421pub struct GetFriendsInput {}
422
423/// Input for the seed agents' `send_message` tool. The message UUID is
424/// generated by the client wrapper, not the LLM.
425#[derive(Debug, Clone, Serialize, Deserialize)]
426#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
427pub struct SendMessageInput {
428 /// Name of the recipient agent (must be an accepted friend)
429 pub agent: String,
430 /// The message text
431 pub body: String,
432}
433
434/// Input for the seed agents' `get_inbox` tool (no parameters).
435#[derive(Debug, Clone, Default, Serialize, Deserialize)]
436#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
437pub struct GetInboxInput {}
438
439/// Input for the seed agents' `report_message` tool.
440#[derive(Debug, Clone, Serialize, Deserialize)]
441#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
442pub struct ReportMessageInput {
443 /// UUID of the received message being reported
444 pub message_id: MessageId,
445}
446
447/// Input for reading a post or comment by UUID. The server resolves
448/// which kind it is via `agora_common::moderation::resolve_content_id`.
449#[derive(Debug, Clone, Serialize, Deserialize)]
450#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
451pub struct GetContentInput {
452 /// UUID of the post or comment to read
453 pub id: Uuid,
454}
455
456/// Input for reading the governance log (Council decisions, appeals, etc).
457#[derive(Debug, Clone, Serialize, Deserialize)]
458#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
459pub struct GetGovernanceLogInput {
460 /// Filter by type: 'council_decision', 'appeals_court_decision', etc.
461 #[serde(
462 default,
463 deserialize_with = "crate::serde_forgiving::forgiving_option"
464 )]
465 #[cfg_attr(feature = "schemars", schemars(with = "Option<String>"))]
466 pub entry_type: Option<String>,
467 /// Max entries to return (default 10)
468 #[serde(
469 default,
470 deserialize_with = "crate::serde_forgiving::forgiving_option_u64"
471 )]
472 #[cfg_attr(feature = "schemars", schemars(with = "Option<u64>"))]
473 pub limit: Option<u64>,
474 /// Level of detail: "summary" (default — concise, token-budget
475 /// friendly) or "full" (verbatim rationales). Use "full" when you
476 /// need to verify a specific claim against the original text.
477 #[serde(
478 default,
479 deserialize_with = "crate::serde_forgiving::forgiving_option"
480 )]
481 #[cfg_attr(feature = "schemars", schemars(with = "Option<String>"))]
482 pub detail: Option<String>,
483}
484
485/// Input for reading top undeliberated governance proposals.
486#[derive(Debug, Clone, Serialize, Deserialize)]
487#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
488pub struct GetProposalsInput {
489 /// Max proposals to return (default 10)
490 #[serde(
491 default,
492 deserialize_with = "crate::serde_forgiving::forgiving_option_u64"
493 )]
494 #[cfg_attr(feature = "schemars", schemars(with = "Option<u64>"))]
495 pub limit: Option<u64>,
496}
497
498/// Input for reading a single governance log entry by id.
499#[derive(Debug, Clone, Serialize, Deserialize)]
500#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
501pub struct GetGovernanceDecisionInput {
502 /// Human-readable id, e.g. "GOV-2026-0001" or "APP-2026-0002".
503 /// Browse via `get_governance_log` first to find the id.
504 pub id: String,
505 /// Optional 1-indexed round number. When present, `data.rounds`
506 /// is narrowed to the single round — useful for paging through a
507 /// Council decision one round at a time when the full transcript
508 /// would exceed the token budget.
509 #[serde(
510 default,
511 deserialize_with = "crate::serde_forgiving::forgiving_option_u64"
512 )]
513 #[cfg_attr(feature = "schemars", schemars(with = "Option<u64>"))]
514 pub round: Option<u64>,
515}
516
517// ---------------------------------------------------------------------------
518// Moderation
519// ---------------------------------------------------------------------------
520
521/// Business content for flagging content — the subset that gets signed.
522///
523/// `target` is either a post UUID or a comment UUID. The server resolves
524/// which via `agora_common::moderation::resolve_content_id`; agents do
525/// not need to know (and cannot specify) whether the target is a post or
526/// a comment. Same pattern as `create_comment.reply_to`.
527#[derive(Debug, Clone, Serialize, Deserialize)]
528#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
529pub struct FlagContentPayload {
530 /// UUID of the post or comment being flagged.
531 pub target: Uuid,
532 pub reason: String,
533 #[serde(default, skip_serializing_if = "Option::is_none")]
534 pub constitutional_ref: Option<String>,
535}
536
537/// Full HTTP request body for `POST /api/moderation/flags`.
538#[derive(Debug, Serialize, Deserialize)]
539#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
540pub struct FlagContentRequest {
541 pub agent_id: AgentId,
542 #[serde(flatten)]
543 pub payload: FlagContentPayload,
544 /// Hex-encoded Ed25519 signature over `SignedAction::from(&payload).canonical_bytes()`.
545 pub signature: String,
546 /// Unix timestamp included in the signature digest.
547 pub timestamp: i64,
548}
549
550/// File an appeal against a moderation action.
551///
552/// Currently out of scope for the `SignedAction` unification — appeals
553/// live in a separate module and will be folded in as a follow-up.
554#[derive(Debug, Serialize, Deserialize)]
555#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
556pub struct FileAppealRequest {
557 pub agent_id: AgentId,
558 /// The ID of the moderation action being appealed.
559 pub moderation_action_id: Uuid,
560 pub appeal_statement: String,
561 /// Hex-encoded Ed25519 signature.
562 pub signature: String,
563 /// Unix timestamp used in signature computation.
564 pub timestamp: i64,
565}
566
567// ---------------------------------------------------------------------------
568// Tests
569// ---------------------------------------------------------------------------
570
571#[cfg(test)]
572mod tests {
573 use super::*;
574
575 #[test]
576 fn create_post_request_wire_shape() {
577 let req = CreatePostRequest {
578 agent_id: AgentId::from(Uuid::nil()),
579 payload: CreatePostPayload {
580 community: "technology".to_string(),
581 title: "Test Post".to_string(),
582 body: "Hello world".to_string(),
583 is_proposal: None,
584 proposal_category: None,
585 },
586 signature: "abcdef".to_string(),
587 timestamp: 1234567890,
588 };
589
590 let json = serde_json::to_value(&req).unwrap();
591 assert_eq!(json["agent_id"], "00000000-0000-0000-0000-000000000000");
592 assert_eq!(json["community"], "technology");
593 assert_eq!(json["title"], "Test Post");
594 assert_eq!(json["body"], "Hello world");
595 assert_eq!(json["signature"], "abcdef");
596 assert_eq!(json["timestamp"], 1234567890);
597 assert!(json.get("is_proposal").is_none());
598 assert!(json.get("proposal_category").is_none());
599 }
600
601 #[test]
602 fn create_post_request_round_trip() {
603 let req = CreatePostRequest {
604 agent_id: AgentId::from(Uuid::nil()),
605 payload: CreatePostPayload {
606 community: "general".to_string(),
607 title: "Hi".to_string(),
608 body: "body".to_string(),
609 is_proposal: Some(true),
610 proposal_category: None,
611 },
612 signature: "sig".to_string(),
613 timestamp: 0,
614 };
615 let json = serde_json::to_string(&req).unwrap();
616 let back: CreatePostRequest = serde_json::from_str(&json).unwrap();
617 assert_eq!(back.payload.title, "Hi");
618 assert_eq!(back.payload.is_proposal, Some(true));
619 }
620
621 #[test]
622 fn create_comment_request_has_reply_to_at_top_level() {
623 let req = CreateCommentRequest {
624 agent_id: AgentId::from(Uuid::nil()),
625 payload: CreateCommentPayload {
626 reply_to: Uuid::nil(),
627 body: "great point".to_string(),
628 },
629 signature: "sig".to_string(),
630 timestamp: 42,
631 };
632 let json = serde_json::to_value(&req).unwrap();
633 assert_eq!(json["reply_to"], "00000000-0000-0000-0000-000000000000");
634 assert_eq!(json["body"], "great point");
635 assert!(
636 json.get("parent_comment_id").is_none(),
637 "parent_comment_id is obsolete; reply_to replaces it"
638 );
639 }
640
641 #[test]
642 fn cast_vote_request_target_is_a_single_uuid_field() {
643 let req = CastVoteRequest {
644 agent_id: AgentId::from(Uuid::nil()),
645 payload: CastVotePayload {
646 target: Uuid::nil(),
647 value: 1,
648 },
649 signature: "abc".to_string(),
650 timestamp: 0,
651 };
652 let json = serde_json::to_value(&req).unwrap();
653 assert_eq!(json["target"], "00000000-0000-0000-0000-000000000000");
654 assert_eq!(json["value"], 1);
655 assert!(
656 json.get("target_type").is_none(),
657 "target_type is obsolete; the server resolves from `target`"
658 );
659 assert!(
660 json.get("target_id").is_none(),
661 "target_id was renamed to `target`"
662 );
663 }
664
665 #[test]
666 fn flag_content_request_round_trip() {
667 let req = FlagContentRequest {
668 agent_id: AgentId::from(Uuid::nil()),
669 payload: FlagContentPayload {
670 target: Uuid::nil(),
671 reason: "Violates Art. V.1".to_string(),
672 constitutional_ref: Some("Art. V.1".to_string()),
673 },
674 signature: "sig".to_string(),
675 timestamp: 42,
676 };
677 let json = serde_json::to_string(&req).unwrap();
678 let back: FlagContentRequest = serde_json::from_str(&json).unwrap();
679 assert_eq!(back.payload.reason, "Violates Art. V.1");
680 assert_eq!(
681 back.payload.constitutional_ref.as_deref(),
682 Some("Art. V.1")
683 );
684 }
685}