Skip to main content

max / makenotwork

5.6 KB · 174 lines History Blame Raw
1 //! Item reorder workflow tests: move up, move down, boundary behavior.
2
3 use serde_json::Value;
4
5 use crate::harness::TestHarness;
6
7 /// Helper: create a creator with a project and return the project ID.
8 async fn setup_project(h: &mut TestHarness) -> String {
9 h.create_creator("reorder").await;
10
11 let resp = h
12 .client
13 .post_form("/api/projects", "slug=reorder-proj&title=Reorder+Project")
14 .await;
15 let project: Value = resp.json();
16 project["id"].as_str().unwrap().to_string()
17 }
18
19 /// Helper: create an item and return its ID.
20 async fn create_item(h: &mut TestHarness, project_id: &str, title: &str) -> String {
21 let resp = h
22 .client
23 .post_form(
24 &format!("/api/projects/{}/items", project_id),
25 &format!("title={}", urlencoding::encode(title)),
26 )
27 .await;
28 let item: Value = resp.json();
29 item["id"].as_str().unwrap().to_string()
30 }
31
32 /// Helper: get item IDs in current display order via DB.
33 async fn get_item_order(h: &TestHarness, project_id: &str) -> Vec<String> {
34 let rows: Vec<(makenotwork::db::ItemId,)> = sqlx::query_as(
35 "SELECT id FROM items WHERE project_id = $1::uuid ORDER BY sort_order, created_at DESC",
36 )
37 .bind(project_id)
38 .fetch_all(&h.db)
39 .await
40 .unwrap();
41 rows.into_iter().map(|(id,)| id.to_string()).collect()
42 }
43
44 #[tokio::test]
45 async fn move_item_down_swaps_with_next() {
46 let mut h = TestHarness::new().await;
47 let project_id = setup_project(&mut h).await;
48
49 // Create 3 items — all start with sort_order=0, so order is by created_at DESC (C, B, A)
50 let a = create_item(&mut h, &project_id, "Item A").await;
51 let b = create_item(&mut h, &project_id, "Item B").await;
52 let c = create_item(&mut h, &project_id, "Item C").await;
53
54 // Initial order: C, B, A (newest first due to created_at DESC with all sort_order=0)
55 let order = get_item_order(&h, &project_id).await;
56 assert_eq!(order, vec![c.clone(), b.clone(), a.clone()]);
57
58 // Move C (first item) down — should swap with B
59 let resp = h
60 .client
61 .put_form(&format!("/api/items/{}/move", c), "direction=down")
62 .await;
63 assert_eq!(resp.status, 204);
64
65 // Order should now be: B, C, A
66 let order = get_item_order(&h, &project_id).await;
67 assert_eq!(order, vec![b.clone(), c.clone(), a.clone()]);
68 }
69
70 #[tokio::test]
71 async fn move_item_up_swaps_with_previous() {
72 let mut h = TestHarness::new().await;
73 let project_id = setup_project(&mut h).await;
74
75 let a = create_item(&mut h, &project_id, "Item A").await;
76 let b = create_item(&mut h, &project_id, "Item B").await;
77 let c = create_item(&mut h, &project_id, "Item C").await;
78
79 // Initial order: C, B, A
80 // Move B (middle) up — should swap with C
81 let resp = h
82 .client
83 .put_form(&format!("/api/items/{}/move", b), "direction=up")
84 .await;
85 assert_eq!(resp.status, 204);
86
87 // Order should now be: B, C, A
88 let order = get_item_order(&h, &project_id).await;
89 assert_eq!(order, vec![b.clone(), c.clone(), a.clone()]);
90 }
91
92 #[tokio::test]
93 async fn move_first_item_up_is_noop() {
94 let mut h = TestHarness::new().await;
95 let project_id = setup_project(&mut h).await;
96
97 let a = create_item(&mut h, &project_id, "Item A").await;
98 let b = create_item(&mut h, &project_id, "Item B").await;
99
100 // Initial order: B, A
101 // Move B (first) up — should be a no-op
102 let resp = h
103 .client
104 .put_form(&format!("/api/items/{}/move", b), "direction=up")
105 .await;
106 assert_eq!(resp.status, 204);
107
108 // Order unchanged: B, A
109 let order = get_item_order(&h, &project_id).await;
110 assert_eq!(order, vec![b, a]);
111 }
112
113 #[tokio::test]
114 async fn move_last_item_down_is_noop() {
115 let mut h = TestHarness::new().await;
116 let project_id = setup_project(&mut h).await;
117
118 let a = create_item(&mut h, &project_id, "Item A").await;
119 let b = create_item(&mut h, &project_id, "Item B").await;
120
121 // Initial order: B, A
122 // Move A (last) down — should be a no-op
123 let resp = h
124 .client
125 .put_form(&format!("/api/items/{}/move", a), "direction=down")
126 .await;
127 assert_eq!(resp.status, 204);
128
129 // Order unchanged: B, A
130 let order = get_item_order(&h, &project_id).await;
131 assert_eq!(order, vec![b, a]);
132 }
133
134 #[tokio::test]
135 async fn move_normalizes_then_swaps() {
136 let mut h = TestHarness::new().await;
137 let project_id = setup_project(&mut h).await;
138
139 let a = create_item(&mut h, &project_id, "Item A").await;
140 let b = create_item(&mut h, &project_id, "Item B").await;
141 let c = create_item(&mut h, &project_id, "Item C").await;
142
143 // All items start with sort_order=0 (cold start)
144 // Move A (last) up — should normalize [0,1,2] then swap A with B
145 let resp = h
146 .client
147 .put_form(&format!("/api/items/{}/move", a), "direction=up")
148 .await;
149 assert_eq!(resp.status, 204);
150
151 // Order should be: C, A, B
152 let order = get_item_order(&h, &project_id).await;
153 assert_eq!(order, vec![c, a, b]);
154 }
155
156 #[tokio::test]
157 async fn move_requires_ownership() {
158 let mut h = TestHarness::new().await;
159 let project_id = setup_project(&mut h).await;
160 let item_id = create_item(&mut h, &project_id, "Item").await;
161
162 // Log in as a different user
163 h.signup("other", "other@test.com", "password123").await;
164 h.client.post_form("/logout", "").await;
165 h.login("other", "password123").await;
166
167 // Try to move the first user's item — should be rejected
168 let resp = h
169 .client
170 .put_form(&format!("/api/items/{}/move", item_id), "direction=down")
171 .await;
172 assert_eq!(resp.status, 403);
173 }
174