• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

geo-engine / geoengine / 13812802747

12 Mar 2025 01:40PM CUT coverage: 90.004% (-0.07%) from 90.076%
13812802747

Pull #1013

github

web-flow
Merge b7927a8ca into c96026921
Pull Request #1013: Update-utoipa

889 of 1078 new or added lines in 44 files covered. (82.47%)

29 existing lines in 11 files now uncovered.

126078 of 140081 relevant lines covered (90.0%)

57458.73 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

84.4
/services/src/util/openapi_examples.rs
1
use crate::api::model::responses::ErrorResponse;
2
use crate::contexts::PostgresContext;
3
use crate::contexts::SessionId;
4
use crate::users::UserAuth;
5
use actix_web::dev::ServiceResponse;
6
use actix_web::http::{header, Method};
7
use actix_web::test::TestRequest;
8
use actix_web_httpauth::headers::authorization::Bearer;
9
use std::collections::HashMap;
10
use std::future::Future;
11
use tokio_postgres::NoTls;
12
use utoipa::openapi::path::{Parameter, ParameterIn};
13
use utoipa::openapi::schema::SchemaType;
14
use utoipa::openapi::{
15
    Components, HttpMethod, KnownFormat, OpenApi, RefOr, Schema, SchemaFormat, Type,
16
};
17
use utoipa::PartialSchema;
18
use uuid::Uuid;
19

20
use super::openapi_visitor::operations_from_path;
21

22
pub struct RunnableExample<'a, C, F, Fut>
23
where
24
    F: Fn(TestRequest, C) -> Fut,
25
    Fut: Future<Output = ServiceResponse>,
26
{
27
    pub(crate) components: &'a Components,
28
    pub(crate) http_method: &'a HttpMethod,
29
    pub(crate) uri: &'a str,
30
    pub(crate) parameters: &'a Option<Vec<Parameter>>,
31
    pub(crate) body: serde_json::Value,
32
    pub(crate) with_auth: bool,
33
    pub(crate) ctx: C,
34
    pub(crate) session_id: SessionId,
35
    pub(crate) send_test_request: &'a F,
36
}
37

38
impl<'a, C, F, Fut> RunnableExample<'a, C, F, Fut>
39
where
40
    F: Fn(TestRequest, C) -> Fut,
41
    Fut: Future<Output = ServiceResponse>,
42
{
43
    fn get_actix_http_method(&self) -> Method {
6✔
44
        match self.http_method {
6✔
NEW
45
            HttpMethod::Get => Method::GET,
×
46
            HttpMethod::Post => Method::POST,
4✔
47
            HttpMethod::Put => Method::PUT,
1✔
48
            HttpMethod::Delete => Method::DELETE,
1✔
NEW
49
            HttpMethod::Options => Method::OPTIONS,
×
NEW
50
            HttpMethod::Head => Method::HEAD,
×
NEW
51
            HttpMethod::Patch => Method::PATCH,
×
NEW
52
            HttpMethod::Trace => Method::TRACE,
×
53
        }
54
    }
6✔
55

56
    #[allow(clippy::unimplemented)]
57
    fn get_default_parameter_value(schema: &Schema) -> String {
4✔
58
        match schema {
4✔
59
            Schema::Object(obj) => match obj.schema_type {
4✔
NEW
60
                SchemaType::Type(Type::String) => match &obj.format {
×
61
                    Some(SchemaFormat::KnownFormat(format)) => match format {
×
62
                        KnownFormat::Uuid => Uuid::new_v4().to_string(),
×
63
                        _ => unimplemented!(),
×
64
                    },
65
                    None => "asdf".to_string(),
2✔
66
                    _ => unimplemented!(),
×
67
                },
68
                SchemaType::Type(Type::Integer | Type::Number) => "42".to_string(),
2✔
NEW
69
                SchemaType::Type(Type::Boolean) => "false".to_string(),
×
UNCOV
70
                _ => unimplemented!(),
×
71
            },
72
            _ => unimplemented!(),
×
73
        }
74
    }
4✔
75

76
    fn resolve_schema(&'a self, ref_or: &'a RefOr<Schema>) -> &'a Schema {
4✔
77
        match ref_or {
4✔
78
            RefOr::Ref(reference) => {
×
79
                const SCHEMA_REF_PREFIX_LEN: usize = "#/components/schemas/".len();
80
                //can_resolve_reference(reference, self.components); checked in can_resolve_api
81
                let schema_name = &reference.ref_location[SCHEMA_REF_PREFIX_LEN..];
×
82
                self.resolve_schema(
×
83
                    self.components
×
84
                        .schemas
×
85
                        .get(schema_name)
×
86
                        .expect("checked before"),
×
87
                )
×
88
            }
89
            RefOr::T(concrete) => concrete,
4✔
90
        }
91
    }
4✔
92

93
    fn insert_parameters(&self, parameters: &Vec<Parameter>) -> TestRequest {
2✔
94
        let mut req = TestRequest::default();
2✔
95
        let mut uri = self.uri.to_string();
2✔
96
        let mut query_params = HashMap::new();
2✔
97
        let mut cookies = HashMap::new();
2✔
98

99
        for parameter in parameters {
6✔
100
            let schema = self.resolve_schema(
4✔
101
                parameter
4✔
102
                    .schema
4✔
103
                    .as_ref()
4✔
104
                    .expect("utoipa adds schema everytime"),
4✔
105
            );
4✔
106
            let value = Self::get_default_parameter_value(schema);
4✔
107

4✔
108
            match parameter.parameter_in {
4✔
109
                ParameterIn::Query => {
2✔
110
                    query_params.insert(parameter.name.as_str(), value);
2✔
111
                }
2✔
112
                ParameterIn::Path => {
2✔
113
                    uri = uri.replace(&format!("{{{}}}", parameter.name), value.as_str());
2✔
114
                }
2✔
115
                ParameterIn::Header => {
×
116
                    req = req.append_header((parameter.name.as_str(), value));
×
117
                }
×
118
                ParameterIn::Cookie => {
×
119
                    cookies.insert(parameter.name.as_str(), value);
×
120
                }
×
121
            }
122
        }
123
        if let Ok(cookie_str) = serde_urlencoded::to_string(cookies) {
2✔
124
            if !cookie_str.is_empty() {
2✔
125
                req = req.append_header((header::COOKIE, cookie_str.replace('&', "; ")));
×
126
            }
2✔
127
        }
×
128
        if let Ok(query_params_str) = serde_urlencoded::to_string(query_params) {
2✔
129
            if !query_params_str.is_empty() {
2✔
130
                uri = format!("{uri}?{query_params_str}");
2✔
131
            }
2✔
132
        }
×
133
        req.uri(uri.as_str())
2✔
134
    }
2✔
135

136
    fn build_request(&self) -> TestRequest {
6✔
137
        let http_method = self.get_actix_http_method();
6✔
138
        let mut req;
139

140
        if let Some(parameters) = self.parameters {
6✔
141
            req = self.insert_parameters(parameters);
2✔
142
        } else {
4✔
143
            req = TestRequest::default().uri(self.uri);
4✔
144
        }
4✔
145
        req = req.method(http_method);
6✔
146

6✔
147
        if self.with_auth {
6✔
148
            req = req.append_header((
4✔
149
                header::AUTHORIZATION,
4✔
150
                Bearer::new(self.session_id.to_string()),
4✔
151
            ));
4✔
152
        }
4✔
153
        req.append_header((header::CONTENT_TYPE, "application/json"))
6✔
154
            .set_json(&self.body)
6✔
155
    }
6✔
156

157
    async fn run(self) -> ServiceResponse {
6✔
158
        let req = self.build_request();
6✔
159
        (self.send_test_request)(req, self.ctx).await
6✔
160
    }
6✔
161

162
    /// # Panics
163
    /// Will panic if an example cannot be run due to incomplete or
164
    /// outdated OpenAPI documentation.
165
    pub(crate) async fn check_for_bad_documentation(self) {
6✔
166
        let res = self.run().await;
6✔
167

168
        if res.status() != 200 {
6✔
169
            let method = res.request().head().method.to_string();
4✔
170
            let path = res.request().path().to_string();
4✔
171
            let body: ErrorResponse = actix_web::test::read_body_json(res).await;
4✔
172

173
            match body.error.as_str() {
4✔
174
                "NotFound" | "MethodNotAllowed" => panic!(
4✔
175
                    "The handler of the example at {method} {path} wasn't reachable. \
×
176
                    Check if the http method and path parameters are correctly set in the documentation."
×
177
                ),
×
178
                "UnableToParseQueryString" => panic!(
4✔
179
                    "The example at {method} {path} threw an UnableToParseQueryString error. \
×
180
                    Check if the query parameters are correctly set in the documentation."
×
181
                ),
×
182
                "BodyDeserializeError" => panic!(
4✔
183
                    "The example at {method} {path} threw an BodyDeserializeError. \
1✔
184
                    Check if there were schema changes and update the request body accordingly."
1✔
185
                ),
1✔
186
                _ => {}
3✔
187
            }
188
        }
2✔
189
    }
5✔
190
}
191

192
/// Runs all example requests against the provided test server to check for bad documentation,
193
/// for example due to incompatible schema changes between the time of writing the request body
194
/// and now. It can also detect if the query parameters are not documented correctly or the
195
/// request path changed.
196
///
197
/// # Panics
198
///
199
/// panics if the creation of an anonymous session fails or the example contains a Ref which is not yet supported.
200
pub async fn can_run_examples<F, Fut>(
3✔
201
    app_ctx: PostgresContext<NoTls>,
3✔
202
    api: OpenApi,
3✔
203
    send_test_request: F,
3✔
204
) where
3✔
205
    F: Fn(TestRequest, PostgresContext<NoTls>) -> Fut
3✔
206
        + Send
3✔
207
        + std::panic::UnwindSafe
3✔
208
        + 'static
3✔
209
        + Clone,
3✔
210
    Fut: Future<Output = ServiceResponse>,
3✔
211
{
3✔
212
    let components = api.components.expect("api has at least one component");
3✔
213

214
    for (uri, path_item) in api.paths.paths {
80✔
215
        for (http_method, operation) in operations_from_path(&path_item) {
91✔
216
            if let Some(request_body) = &operation.request_body {
91✔
217
                let with_auth = operation.security.is_some();
26✔
218

219
                for content in request_body.content.clone().into_values() {
26✔
220
                    if let Some(example) = content.example {
26✔
221
                        RunnableExample {
222
                            components: &components,
4✔
223
                            http_method,
4✔
224
                            uri: uri.as_str(),
4✔
225
                            parameters: &operation.parameters,
4✔
226
                            body: example,
4✔
227
                            with_auth,
4✔
228
                            session_id: app_ctx
4✔
229
                                .create_anonymous_session()
4✔
230
                                .await
4✔
231
                                .expect("creating an anonymous session should always work")
4✔
232
                                .id,
4✔
233
                            ctx: app_ctx.clone(),
4✔
234
                            send_test_request: &send_test_request,
4✔
235
                        }
4✔
236
                        .check_for_bad_documentation()
4✔
237
                        .await;
4✔
238
                    } else {
239
                        for example in content.examples.into_values() {
22✔
240
                            match example {
2✔
241
                                RefOr::Ref(_reference) => {
×
242
                                    // This never happened during testing.
×
243
                                    // It is undocumented how the references would look like.
×
244
                                    panic!("checking pro examples with references is not yet implemented")
×
245
                                }
246
                                RefOr::T(concrete) => {
2✔
247
                                    if let Some(body) = concrete.value {
2✔
248
                                        RunnableExample {
249
                                            components: &components,
2✔
250
                                            http_method,
2✔
251
                                            uri: uri.as_str(),
2✔
252
                                            parameters: &operation.parameters,
2✔
253
                                            body,
2✔
254
                                            with_auth,
2✔
255
                                            session_id: app_ctx
2✔
256
                                                .create_anonymous_session()
2✔
257
                                                .await
2✔
258
                                                .expect("creating an anonymous session should always work")
2✔
259
                                                .id,
2✔
260
                                            ctx: app_ctx.clone(),
2✔
261
                                            send_test_request: &send_test_request,
2✔
262
                                        }
2✔
263
                                        .check_for_bad_documentation()
2✔
264
                                        .await;
2✔
265
                                    } else {
×
266
                                        //skip external examples
×
267
                                    }
×
268
                                }
269
                            }
270
                        }
271
                    }
272
                }
273
            }
65✔
274
        }
275
    }
276
}
2✔
277

278
#[cfg(test)]
279
mod tests {
280
    use super::*;
281
    use crate::api::model::services::Volume;
282
    use crate::ge_context;
283
    use crate::util::server::{configure_extractors, render_404, render_405};
284
    use actix_web::{http, middleware, post, web, App, HttpResponse, Responder};
285
    use serde::Deserialize;
286
    use serde_json::json;
287
    use utoipa::openapi::path::{OperationBuilder, ParameterBuilder, PathItemBuilder};
288
    use utoipa::openapi::request_body::RequestBodyBuilder;
289
    use utoipa::openapi::{
290
        ComponentsBuilder, ContentBuilder, HttpMethod, Object, ObjectBuilder, OpenApiBuilder,
291
        PathsBuilder,
292
    };
293

294
    #[derive(Deserialize)]
295
    struct DummyQueryParams {
296
        #[serde(rename = "x")]
297
        _x: String,
298
    }
299

300
    #[post("/test/{id}")]
2✔
301
    #[allow(
302
        clippy::unused_async, // the function signature of request handlers requires it
303
        clippy::no_effect_underscore_binding // adding path and query parameter to ensure parameter insertion works
304
    )]
305
    async fn dummy_handler(
1✔
306
        _id: web::Path<u32>,
1✔
307
        _params: web::Query<DummyQueryParams>,
1✔
308
        _body: web::Json<Volume>,
1✔
309
    ) -> impl Responder {
1✔
310
        HttpResponse::Ok()
1✔
311
    }
1✔
312

313
    async fn dummy_send_test_request(
2✔
314
        req: TestRequest,
2✔
315
        ctx: PostgresContext<NoTls>,
2✔
316
    ) -> ServiceResponse {
2✔
317
        let app = actix_web::test::init_service(
2✔
318
            App::new()
2✔
319
                .app_data(web::Data::new(ctx))
2✔
320
                .wrap(
2✔
321
                    middleware::ErrorHandlers::default()
2✔
322
                        .handler(http::StatusCode::NOT_FOUND, render_404)
2✔
323
                        .handler(http::StatusCode::METHOD_NOT_ALLOWED, render_405),
2✔
324
                )
2✔
325
                .configure(configure_extractors)
2✔
326
                .service(dummy_handler),
2✔
327
        )
2✔
328
        .await;
2✔
329
        actix_web::test::call_service(&app, req.to_request())
2✔
330
            .await
2✔
331
            .map_into_boxed_body()
2✔
332
    }
2✔
333

334
    async fn run_dummy_example(app_ctx: PostgresContext<NoTls>, example: serde_json::Value) {
2✔
335
        Box::pin(can_run_examples(
2✔
336
            app_ctx,
2✔
337
            OpenApiBuilder::new()
2✔
338
                .paths(
2✔
339
                    PathsBuilder::new().path(
2✔
340
                        "/test/{id}",
2✔
341
                        PathItemBuilder::new()
2✔
342
                            .operation(
2✔
343
                                HttpMethod::Post,
2✔
344
                                OperationBuilder::new()
2✔
345
                                    .parameter(
2✔
346
                                        ParameterBuilder::new()
2✔
347
                                            .name("id")
2✔
348
                                            .parameter_in(ParameterIn::Path)
2✔
349
                                            .schema(Some(RefOr::T(
2✔
350
                                                ObjectBuilder::new()
2✔
351
                                                    .schema_type(SchemaType::Type(Type::Integer))
2✔
352
                                                    .format(Some(SchemaFormat::KnownFormat(
2✔
353
                                                        KnownFormat::Int32,
2✔
354
                                                    )))
2✔
355
                                                    .into(),
2✔
356
                                            ))),
2✔
357
                                    )
2✔
358
                                    .parameter(
2✔
359
                                        ParameterBuilder::new()
2✔
360
                                            .name("x")
2✔
361
                                            .parameter_in(ParameterIn::Query)
2✔
362
                                            .schema(Some(RefOr::T(
2✔
363
                                                Object::with_type(SchemaType::Type(Type::String))
2✔
364
                                                    .into(),
2✔
365
                                            ))),
2✔
366
                                    )
2✔
367
                                    .request_body(Some(
2✔
368
                                        RequestBodyBuilder::new()
2✔
369
                                            .content(
2✔
370
                                                "application/json",
2✔
371
                                                ContentBuilder::new()
2✔
372
                                                    .schema(Some(Volume::schema()))
2✔
373
                                                    .example(Some(example))
2✔
374
                                                    .into(),
2✔
375
                                            )
2✔
376
                                            .into(),
2✔
377
                                    )),
2✔
378
                            )
2✔
379
                            .into(),
2✔
380
                    ),
2✔
381
                )
2✔
382
                .components(Some(
2✔
383
                    ComponentsBuilder::new()
2✔
384
                        .schemas_from_iter([
2✔
385
                            ("Schema1", Schema::default()),
2✔
386
                            ("Schema2", Schema::default()),
2✔
387
                            ("Schema3", Schema::default()),
2✔
388
                        ])
2✔
389
                        .into(),
2✔
390
                ))
2✔
391
                .into(),
2✔
392
            dummy_send_test_request,
2✔
393
        ))
2✔
394
        .await;
2✔
395
    }
1✔
396

397
    #[ge_context::test(expect_panic = "BodyDeserializeError")]
1✔
398
    async fn detects_bodydeserializeerror(app_ctx: PostgresContext<NoTls>) {
1✔
399
        run_dummy_example(app_ctx, json!({"path": "note-name_field_missing"})).await;
1✔
400
    }
×
401

402
    #[ge_context::test]
1✔
403
    async fn successfull_example_run(app_ctx: PostgresContext<NoTls>) {
1✔
404
        run_dummy_example(app_ctx, json!({"name": "Files", "path": "/path/to/files"})).await;
1✔
405
    }
1✔
406
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc