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

geo-engine / geoengine / 13810845605

12 Mar 2025 12:02PM UTC coverage: 90.08% (+0.004%) from 90.076%
13810845605

Pull #1026

github

web-flow
Merge 560f1fe47 into c96026921
Pull Request #1026: Ubuntu 24 LTS

2350 of 2476 new or added lines in 108 files covered. (94.91%)

6 existing lines in 4 files now uncovered.

126337 of 140250 relevant lines covered (90.08%)

57392.1 hits per line

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

83.45
/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::{Method, header};
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::{
14
    Components, KnownFormat, OpenApi, PathItemType, RefOr, Schema, SchemaFormat, SchemaType,
15
};
16
use uuid::Uuid;
17

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

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

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

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

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

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

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

133
    fn build_request(&self) -> TestRequest {
6✔
134
        let http_method = self.get_actix_http_method();
6✔
135
        let mut req;
136

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

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

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

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

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

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

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

211
    for (uri, path_item) in api.paths.paths {
80✔
212
        for (http_method, operation) in path_item.operations {
168✔
213
            if let Some(request_body) = operation.request_body {
91✔
214
                let with_auth = operation.security.is_some();
26✔
215

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

277
#[cfg(test)]
278
mod tests {
279
    use super::*;
280
    use crate::api::model::services::Volume;
281
    use crate::ge_context;
282
    use crate::util::server::{configure_extractors, render_404, render_405};
283
    use actix_web::{App, HttpResponse, Responder, http, middleware, post, web};
284
    use serde::Deserialize;
285
    use serde_json::json;
286
    use utoipa::ToSchema;
287
    use utoipa::openapi::path::{OperationBuilder, ParameterBuilder, PathItemBuilder};
288
    use utoipa::openapi::request_body::RequestBodyBuilder;
289
    use utoipa::openapi::{
290
        ComponentsBuilder, ContentBuilder, Object, ObjectBuilder, OpenApiBuilder, PathItemType,
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
        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
                                PathItemType::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::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::String).into(),
2✔
364
                                            ))),
2✔
365
                                    )
2✔
366
                                    .request_body(Some(
2✔
367
                                        RequestBodyBuilder::new()
2✔
368
                                            .content(
2✔
369
                                                "application/json",
2✔
370
                                                ContentBuilder::new()
2✔
371
                                                    .schema(Volume::schema().1)
2✔
372
                                                    .example(Some(example))
2✔
373
                                                    .into(),
2✔
374
                                            )
2✔
375
                                            .into(),
2✔
376
                                    )),
2✔
377
                            )
2✔
378
                            .into(),
2✔
379
                    ),
2✔
380
                )
2✔
381
                .components(Some(
2✔
382
                    ComponentsBuilder::new()
2✔
383
                        .schemas_from_iter([
2✔
384
                            ("Schema1", Schema::default()),
2✔
385
                            ("Schema2", Schema::default()),
2✔
386
                            ("Schema3", Schema::default()),
2✔
387
                        ])
2✔
388
                        .into(),
2✔
389
                ))
2✔
390
                .into(),
2✔
391
            dummy_send_test_request,
2✔
392
        )
2✔
393
        .await;
2✔
394
    }
1✔
395

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

401
    #[ge_context::test]
1✔
402
    async fn successfull_example_run(app_ctx: PostgresContext<NoTls>) {
1✔
403
        run_dummy_example(app_ctx, json!({"name": "Files", "path": "/path/to/files"})).await;
1✔
404
    }
1✔
405
}
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

© 2026 Coveralls, Inc