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

geo-engine / geoengine / 10455079190

19 Aug 2024 01:48PM UTC coverage: 91.113% (+0.01%) from 91.103%
10455079190

push

github

web-flow
Merge pull request #953 from 1lutz/openapi-enums

Improve enums in OpenAPI

474 of 491 new or added lines in 5 files covered. (96.54%)

10 existing lines in 8 files now uncovered.

133051 of 146028 relevant lines covered (91.11%)

52821.88 hits per line

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

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

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

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

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

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

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

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

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

131
    fn build_request(&self) -> TestRequest {
8✔
132
        let http_method = self.get_actix_http_method();
8✔
133
        let mut req;
134

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

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

152
    async fn run(self) -> ServiceResponse {
8✔
153
        let req = self.build_request();
8✔
154
        (self.send_test_request)(req, self.ctx).await
79✔
155
    }
8✔
156

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

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

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

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

208
    let session_id = app_ctx.default_session_id().await;
2✔
209
    for (uri, path_item) in api.paths.paths {
3✔
210
        for (http_method, operation) in path_item.operations {
3✔
211
            if let Some(request_body) = operation.request_body {
2✔
212
                let with_auth = operation.security.is_some();
2✔
213

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

267
#[cfg(test)]
268
mod tests {
269
    use super::*;
270
    use crate::api::model::services::Volume;
271
    use crate::contexts::SimpleApplicationContext;
272
    use crate::ge_context;
273
    use crate::util::server::{configure_extractors, render_404, render_405};
274
    use actix_web::{http, middleware, post, web, App, HttpResponse, Responder};
275
    use serde::Deserialize;
276
    use serde_json::json;
277
    use utoipa::openapi::path::{OperationBuilder, ParameterBuilder, PathItemBuilder};
278
    use utoipa::openapi::request_body::RequestBodyBuilder;
279
    use utoipa::openapi::{
280
        ComponentsBuilder, ContentBuilder, Object, ObjectBuilder, OpenApiBuilder, PathItemType,
281
        PathsBuilder,
282
    };
283
    use utoipa::ToSchema;
284

285
    #[derive(Deserialize)]
4✔
286
    struct DummyQueryParams {
287
        #[serde(rename = "x")]
288
        _x: String,
289
    }
290

291
    #[post("/test/{id}")]
2✔
292
    #[allow(
293
        clippy::unused_async, // the function signature of request handlers requires it
294
        clippy::no_effect_underscore_binding // adding path and query parameter to ensure parameter insertion works
295
    )]
296
    async fn dummy_handler(
1✔
297
        _id: web::Path<u32>,
1✔
298
        _params: web::Query<DummyQueryParams>,
1✔
299
        _body: web::Json<Volume>,
1✔
300
    ) -> impl Responder {
1✔
301
        HttpResponse::Ok()
1✔
302
    }
1✔
303

304
    async fn dummy_send_test_request<C: SimpleApplicationContext>(
2✔
305
        req: TestRequest,
2✔
306
        ctx: C,
2✔
307
    ) -> ServiceResponse {
2✔
308
        let app = actix_web::test::init_service(
2✔
309
            App::new()
2✔
310
                .app_data(web::Data::new(ctx))
2✔
311
                .wrap(
2✔
312
                    middleware::ErrorHandlers::default()
2✔
313
                        .handler(http::StatusCode::NOT_FOUND, render_404)
2✔
314
                        .handler(http::StatusCode::METHOD_NOT_ALLOWED, render_405),
2✔
315
                )
2✔
316
                .configure(configure_extractors)
2✔
317
                .service(dummy_handler),
2✔
318
        )
2✔
319
        .await;
×
320
        actix_web::test::call_service(&app, req.to_request())
2✔
321
            .await
×
322
            .map_into_boxed_body()
2✔
323
    }
2✔
324

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

387
    #[ge_context::test(expect_panic = "BodyDeserializeError")]
3✔
388
    async fn detects_bodydeserializeerror(app_ctx: PostgresContext<NoTls>) {
1✔
389
        run_dummy_example(app_ctx, json!({"path": "note-name_field_missing"})).await;
1✔
390
    }
×
391

392
    #[ge_context::test]
3✔
393
    async fn successfull_example_run(app_ctx: PostgresContext<NoTls>) {
1✔
394
        run_dummy_example(app_ctx, json!({"name": "Files", "path": "/path/to/files"})).await;
1✔
395
    }
1✔
396
}
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