Trading card city builder game?

add api for creating new field

eldridge.cam 1643bd1e 7fede6f6

verified
Waiting for spindle ...
+272 -2
+12
.sqlx/query-4cdc9fb57670878f0024a6b490079ac90dcdc9631e3444399bc82d72e6d24fa2.json
··· 1 + { 2 + "db_name": "PostgreSQL", 3 + "query": "INSERT INTO fields (account_id, name) VALUES ('foxfriends', 'Field 1')", 4 + "describe": { 5 + "columns": [], 6 + "parameters": { 7 + "Left": [] 8 + }, 9 + "nullable": [] 10 + }, 11 + "hash": "4cdc9fb57670878f0024a6b490079ac90dcdc9631e3444399bc82d72e6d24fa2" 12 + }
+34
.sqlx/query-54b77f4805439e169ab88acd84b999629ff47128c346969b463a786f6afb4dd5.json
··· 1 + { 2 + "db_name": "PostgreSQL", 3 + "query": "INSERT INTO fields (name, account_id) VALUES ($1, $2) RETURNING id, name", 4 + "describe": { 5 + "columns": [ 6 + { 7 + "ordinal": 0, 8 + "name": "id", 9 + "type_info": "Int8" 10 + }, 11 + { 12 + "ordinal": 1, 13 + "name": "name", 14 + "type_info": "Text" 15 + } 16 + ], 17 + "parameters": { 18 + "Left": [ 19 + "Text", 20 + { 21 + "Custom": { 22 + "name": "citext", 23 + "kind": "Simple" 24 + } 25 + } 26 + ] 27 + }, 28 + "nullable": [ 29 + false, 30 + false 31 + ] 32 + }, 33 + "hash": "54b77f4805439e169ab88acd84b999629ff47128c346969b463a786f6afb4dd5" 34 + }
+20
.sqlx/query-a826246de180b0e3be96a50c355cb86389f8fb706366de54e5011afaa2d1c0f1.json
··· 1 + { 2 + "db_name": "PostgreSQL", 3 + "query": "INSERT INTO fields (account_id, name) VALUES ('foxfriends', 'Field 1') RETURNING id", 4 + "describe": { 5 + "columns": [ 6 + { 7 + "ordinal": 0, 8 + "name": "id", 9 + "type_info": "Int8" 10 + } 11 + ], 12 + "parameters": { 13 + "Left": [] 14 + }, 15 + "nullable": [ 16 + false 17 + ] 18 + }, 19 + "hash": "a826246de180b0e3be96a50c355cb86389f8fb706366de54e5011afaa2d1c0f1" 20 + }
+12
Cargo.lock
··· 146 146 checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8" 147 147 dependencies = [ 148 148 "axum-core 0.5.6", 149 + "axum-macros", 149 150 "base64", 150 151 "bytes", 151 152 "form_urlencoded", ··· 258 259 "tower-layer", 259 260 "tower-service", 260 261 "tracing", 262 + ] 263 + 264 + [[package]] 265 + name = "axum-macros" 266 + version = "0.5.0" 267 + source = "registry+https://github.com/rust-lang/crates.io-index" 268 + checksum = "604fde5e028fea851ce1d8570bbdc034bec850d157f7569d10f347d06808c05c" 269 + dependencies = [ 270 + "proc-macro2", 271 + "quote", 272 + "syn", 261 273 ] 262 274 263 275 [[package]]
+1 -1
packages/cartography/Cargo.toml
··· 12 12 13 13 [dependencies] 14 14 anyhow = "1.0.101" 15 - axum = { version = "0.8.8", features = ["ws"] } 15 + axum = { version = "0.8.8", features = ["ws", "macros"] } 16 16 axum-extra = { version = "0.12.5", features = ["typed-header"] } 17 17 clap = { version = "4.5.58", features = ["derive"] } 18 18 derive_more = { version = "2.1.1", features = ["error", "display"] }
+185
packages/cartography/src/api/operations/create_field.rs
··· 1 + use crate::api::errors::internal_server_error; 2 + use crate::api::middleware::authorization::Authorization; 3 + use crate::dto::*; 4 + use axum::extract::Path; 5 + use axum::{Extension, Json}; 6 + 7 + #[derive(serde::Serialize, utoipa::ToSchema)] 8 + #[cfg_attr(test, derive(serde::Deserialize))] 9 + pub struct CreateFieldResponse { 10 + field: Field, 11 + } 12 + 13 + #[derive(serde::Deserialize, utoipa::ToSchema)] 14 + #[cfg_attr(test, derive(serde::Serialize))] 15 + pub struct CreateFieldRequest { 16 + name: String, 17 + } 18 + 19 + #[utoipa::path( 20 + post, 21 + path = "/api/v1/players/{player_id}/fields", 22 + description = "Create field.", 23 + tag = "Player", 24 + security(("trust" = [])), 25 + request_body = CreateFieldRequest, 26 + responses( 27 + (status = OK, description = "Field created", body = CreateFieldResponse), 28 + ), 29 + params( 30 + ("player_id" = AccountIdOrMe, Path, description = "The ID of the player to create this field for.") 31 + ) 32 + )] 33 + pub async fn create_field( 34 + db: Extension<sqlx::PgPool>, 35 + authorization: Extension<Authorization>, 36 + Path(account_id): Path<AccountIdOrMe>, 37 + request: Json<CreateFieldRequest>, 38 + ) -> axum::response::Result<Json<CreateFieldResponse>> { 39 + let account_id = authorization.resolve_account_id(&account_id)?; 40 + authorization.require_authorization(account_id)?; 41 + 42 + let mut conn = db.begin().await.map_err(internal_server_error)?; 43 + let field = sqlx::query_as!( 44 + Field, 45 + "INSERT INTO fields (name, account_id) VALUES ($1, $2) RETURNING id, name", 46 + request.name, 47 + account_id, 48 + ) 49 + .fetch_one(&mut *conn) 50 + .await 51 + .map_err(internal_server_error)?; 52 + 53 + Ok(Json(CreateFieldResponse { field })) 54 + } 55 + 56 + #[cfg(test)] 57 + mod tests { 58 + use crate::test::prelude::*; 59 + use axum::http::{Request, StatusCode}; 60 + use sqlx::PgPool; 61 + 62 + use super::*; 63 + 64 + #[sqlx::test( 65 + migrator = "MIGRATOR", 66 + fixtures(path = "../../../fixtures", scripts("account")) 67 + )] 68 + pub fn create_field(pool: PgPool) { 69 + let app = crate::app::Config::test(pool).into_router(); 70 + 71 + let request = Request::post("/api/v1/players/@me/fields") 72 + .header("Authorization", "Trust foxfriends") 73 + .json(CreateFieldRequest { 74 + name: "New Field".to_owned(), 75 + }) 76 + .unwrap(); 77 + 78 + let Ok(response) = app.oneshot(request).await; 79 + assert_success!(response); 80 + 81 + let response: CreateFieldResponse = response.json().await.unwrap(); 82 + assert_eq!(response.field.name, "New Field"); 83 + } 84 + 85 + #[sqlx::test( 86 + migrator = "MIGRATOR", 87 + fixtures(path = "../../../fixtures", scripts("account")) 88 + )] 89 + pub fn create_multiple_fields(pool: PgPool) { 90 + sqlx::query!("INSERT INTO fields (account_id, name) VALUES ('foxfriends', 'Field 1')") 91 + .execute(&pool) 92 + .await 93 + .unwrap(); 94 + let app = crate::app::Config::test(pool).into_router(); 95 + 96 + let request = Request::post("/api/v1/players/@me/fields") 97 + .header("Authorization", "Trust foxfriends") 98 + .json(CreateFieldRequest { 99 + name: "Field 2".to_owned(), 100 + }) 101 + .unwrap(); 102 + 103 + let Ok(response) = app.oneshot(request).await; 104 + assert_success!(response); 105 + 106 + let response: CreateFieldResponse = response.json().await.unwrap(); 107 + assert_eq!(response.field.name, "Field 2"); 108 + } 109 + 110 + #[sqlx::test( 111 + migrator = "MIGRATOR", 112 + fixtures(path = "../../../fixtures", scripts("account")) 113 + )] 114 + pub fn create_field_same_name(pool: PgPool) { 115 + let field = sqlx::query!( 116 + "INSERT INTO fields (account_id, name) VALUES ('foxfriends', 'Field 1') RETURNING id" 117 + ) 118 + .fetch_one(&pool) 119 + .await 120 + .unwrap(); 121 + let app = crate::app::Config::test(pool).into_router(); 122 + 123 + let request = Request::post("/api/v1/players/@me/fields") 124 + .header("Authorization", "Trust foxfriends") 125 + .json(CreateFieldRequest { 126 + name: "Field 1".to_owned(), 127 + }) 128 + .unwrap(); 129 + 130 + let Ok(response) = app.oneshot(request).await; 131 + assert_success!(response); 132 + 133 + let response: CreateFieldResponse = response.json().await.unwrap(); 134 + assert_ne!(response.field.id, field.id); 135 + assert_eq!(response.field.name, "Field 1"); 136 + } 137 + 138 + #[sqlx::test( 139 + migrator = "MIGRATOR", 140 + fixtures(path = "../../../fixtures", scripts("account")) 141 + )] 142 + pub fn create_field_body_required(pool: PgPool) { 143 + let app = crate::app::Config::test(pool).into_router(); 144 + 145 + let request = Request::post("/api/v1/players/foxfriends/fields") 146 + .header("Authorization", "Trust not-foxfriends") 147 + .json(serde_json::json!({})) 148 + .unwrap(); 149 + 150 + let Ok(response) = app.oneshot(request).await; 151 + assert_eq!(response.status(), StatusCode::UNPROCESSABLE_ENTITY); 152 + } 153 + 154 + #[sqlx::test(migrator = "MIGRATOR")] 155 + pub fn create_field_unauthorized(pool: PgPool) { 156 + let app = crate::app::Config::test(pool).into_router(); 157 + 158 + let request = Request::post("/api/v1/players/@me/fields") 159 + .json(CreateFieldRequest { 160 + name: "Field".to_owned(), 161 + }) 162 + .unwrap(); 163 + 164 + let Ok(response) = app.oneshot(request).await; 165 + assert_eq!(response.status(), StatusCode::UNAUTHORIZED); 166 + } 167 + 168 + #[sqlx::test( 169 + migrator = "MIGRATOR", 170 + fixtures(path = "../../../fixtures", scripts("account")) 171 + )] 172 + pub fn create_field_wrong_user_forbidden(pool: PgPool) { 173 + let app = crate::app::Config::test(pool).into_router(); 174 + 175 + let request = Request::post("/api/v1/players/foxfriends/fields") 176 + .header("Authorization", "Trust not-foxfriends") 177 + .json(CreateFieldRequest { 178 + name: "Field".to_owned(), 179 + }) 180 + .unwrap(); 181 + 182 + let Ok(response) = app.oneshot(request).await; 183 + assert_eq!(response.status(), StatusCode::FORBIDDEN); 184 + } 185 + }
+2
packages/cartography/src/api/operations/mod.rs
··· 8 8 mod list_card_types; 9 9 pub use list_card_types::*; 10 10 11 + mod create_field; 11 12 mod list_fields; 13 + pub use create_field::*; 12 14 pub use list_fields::*; 13 15 14 16 mod get_pack;
+5
packages/cartography/src/app.rs
··· 15 15 operations::list_card_types, 16 16 17 17 operations::list_fields, 18 + operations::create_field, 18 19 19 20 operations::list_packs, 20 21 operations::get_pack, ··· 100 101 .route( 101 102 "/api/v1/players/{player_id}/fields", 102 103 axum::routing::get(operations::list_fields), 104 + ) 105 + .route( 106 + "/api/v1/players/{player_id}/fields", 107 + axum::routing::post(operations::create_field), 103 108 ) 104 109 .route( 105 110 "/api/v1/players/{player_id}/packs",
+1 -1
packages/cartography/src/test.rs
··· 86 86 async fn handle( 87 87 &mut self, 88 88 _msg: TakeCollection, 89 - ctx: &mut kameo::prelude::Context<Self, Self::Reply>, 89 + _ctx: &mut kameo::prelude::Context<Self, Self::Reply>, 90 90 ) -> Self::Reply { 91 91 std::mem::take(&mut self.0) 92 92 }