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

Unleash / unleash-edge / #1444

21 Jan 2025 08:37AM UTC coverage: 19.689% (-53.4%) from 73.06%
#1444

push

web-flow
dep-update: bump serde_json from 1.0.135 to 1.0.137 (#678)

Bumps [serde_json](https://github.com/serde-rs/json) from 1.0.135 to 1.0.137.
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.135...v1.0.137)

---
updated-dependencies:
- dependency-name: serde_json
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

405 of 2057 relevant lines covered (19.69%)

0.39 hits per line

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

35.44
/server/src/client_api.rs
1
use crate::cli::{EdgeArgs, EdgeMode};
2
use crate::error::EdgeError;
3
use crate::feature_cache::FeatureCache;
4
use crate::filters::{
5
    filter_client_features, name_match_filter, name_prefix_filter, project_filter, FeatureFilterSet,
6
};
7
use crate::http::broadcaster::Broadcaster;
8
use crate::http::refresher::feature_refresher::FeatureRefresher;
9
use crate::metrics::client_metrics::MetricsCache;
10
use crate::tokens::cache_key;
11
use crate::types::{
12
    self, BatchMetricsRequestBody, EdgeJsonResult, EdgeResult, EdgeToken, FeatureFilters,
13
};
14
use actix_web::web::{self, Data, Json, Query};
15
use actix_web::Responder;
16
use actix_web::{get, post, HttpRequest, HttpResponse};
17
use dashmap::DashMap;
18
use unleash_types::client_features::{ClientFeature, ClientFeatures};
19
use unleash_types::client_metrics::{ClientApplication, ClientMetrics, ConnectVia};
20

21
#[utoipa::path(
22
    context_path = "/api/client",
23
    params(FeatureFilters),
24
    responses(
25
        (status = 200, description = "Return feature toggles for this token", body = ClientFeatures),
26
        (status = 403, description = "Was not allowed to access features"),
27
        (status = 400, description = "Invalid parameters used")
28
    ),
29
    security(
30
        ("Authorization" = [])
31
    )
32
)]
33
#[get("/features")]
34
pub async fn get_features(
×
35
    edge_token: EdgeToken,
36
    features_cache: Data<FeatureCache>,
37
    token_cache: Data<DashMap<String, EdgeToken>>,
38
    filter_query: Query<FeatureFilters>,
39
    req: HttpRequest,
40
) -> EdgeJsonResult<ClientFeatures> {
41
    resolve_features(edge_token, features_cache, token_cache, filter_query, req).await
×
42
}
43

44
#[get("/streaming")]
45
pub async fn stream_features(
1✔
46
    edge_token: EdgeToken,
47
    broadcaster: Data<Broadcaster>,
48
    token_cache: Data<DashMap<String, EdgeToken>>,
49
    edge_mode: Data<EdgeMode>,
50
    filter_query: Query<FeatureFilters>,
51
) -> EdgeResult<impl Responder> {
52
    match edge_mode.get_ref() {
2✔
53
        EdgeMode::Edge(EdgeArgs {
54
            streaming: true, ..
55
        }) => {
56
            let (validated_token, _filter_set, query) =
1✔
57
                get_feature_filter(&edge_token, &token_cache, filter_query.clone())?;
58

59
            broadcaster.connect(validated_token, query).await
2✔
60
        }
61
        _ => Err(EdgeError::Forbidden(
×
62
            "This endpoint is only enabled in streaming mode".into(),
×
63
        )),
64
    }
65
}
66

67
#[utoipa::path(
68
    context_path = "/api/client",
69
    params(FeatureFilters),
70
    responses(
71
        (status = 200, description = "Return feature toggles for this token", body = ClientFeatures),
72
        (status = 403, description = "Was not allowed to access features"),
73
        (status = 400, description = "Invalid parameters used")
74
    ),
75
    security(
76
        ("Authorization" = [])
77
    )
78
)]
79
#[post("/features")]
80
pub async fn post_features(
×
81
    edge_token: EdgeToken,
82
    features_cache: Data<FeatureCache>,
83
    token_cache: Data<DashMap<String, EdgeToken>>,
84
    filter_query: Query<FeatureFilters>,
85
    req: HttpRequest,
86
) -> EdgeJsonResult<ClientFeatures> {
87
    resolve_features(edge_token, features_cache, token_cache, filter_query, req).await
×
88
}
89

90
fn get_feature_filter(
1✔
91
    edge_token: &EdgeToken,
92
    token_cache: &Data<DashMap<String, EdgeToken>>,
93
    filter_query: Query<FeatureFilters>,
94
) -> EdgeResult<(
95
    EdgeToken,
96
    FeatureFilterSet,
97
    unleash_types::client_features::Query,
98
)> {
99
    let validated_token = token_cache
3✔
100
        .get(&edge_token.token)
101
        .map(|e| e.value().clone())
2✔
102
        .ok_or(EdgeError::AuthorizationDenied)?;
1✔
103

104
    let query_filters = filter_query.into_inner();
2✔
105
    let query = unleash_types::client_features::Query {
106
        tags: None,
107
        projects: Some(validated_token.projects.clone()),
2✔
108
        name_prefix: query_filters.name_prefix.clone(),
1✔
109
        environment: validated_token.environment.clone(),
1✔
110
        inline_segment_constraints: Some(false),
1✔
111
    };
112

113
    let filter_set = if let Some(name_prefix) = query_filters.name_prefix {
2✔
114
        FeatureFilterSet::from(Box::new(name_prefix_filter(name_prefix)))
×
115
    } else {
116
        FeatureFilterSet::default()
2✔
117
    }
118
    .with_filter(project_filter(&validated_token));
2✔
119

120
    Ok((validated_token, filter_set, query))
1✔
121
}
122

123
async fn resolve_features(
×
124
    edge_token: EdgeToken,
125
    features_cache: Data<FeatureCache>,
126
    token_cache: Data<DashMap<String, EdgeToken>>,
127
    filter_query: Query<FeatureFilters>,
128
    req: HttpRequest,
129
) -> EdgeJsonResult<ClientFeatures> {
130
    let (validated_token, filter_set, query) =
×
131
        get_feature_filter(&edge_token, &token_cache, filter_query.clone())?;
132

133
    let client_features = match req.app_data::<Data<FeatureRefresher>>() {
×
134
        Some(refresher) => {
×
135
            refresher
×
136
                .features_for_filter(validated_token.clone(), &filter_set)
×
137
                .await
×
138
        }
139
        None => features_cache
×
140
            .get(&cache_key(&validated_token))
×
141
            .map(|client_features| filter_client_features(&client_features, &filter_set))
×
142
            .ok_or(EdgeError::ClientCacheError),
×
143
    }?;
144

145
    Ok(Json(ClientFeatures {
×
146
        query: Some(query),
×
147
        ..client_features
148
    }))
149
}
150
#[utoipa::path(
151
    context_path = "/api/client",
152
    params(("feature_name" = String, Path,)),
153
    responses(
154
        (status = 200, description = "Return feature toggles for this token", body = ClientFeature),
155
        (status = 403, description = "Was not allowed to access feature"),
156
        (status = 400, description = "Invalid parameters used"),
157
        (status = 404, description = "Feature did not exist or token used was not allowed to access it")
158
    ),
159
    security(
160
        ("Authorization" = [])
161
    )
162
)]
163
#[get("/features/{feature_name}")]
164
pub async fn get_feature(
×
165
    edge_token: EdgeToken,
166
    features_cache: Data<FeatureCache>,
167
    token_cache: Data<DashMap<String, EdgeToken>>,
168
    feature_name: web::Path<String>,
169
    req: HttpRequest,
170
) -> EdgeJsonResult<ClientFeature> {
171
    let validated_token = token_cache
×
172
        .get(&edge_token.token)
×
173
        .map(|e| e.value().clone())
×
174
        .ok_or(EdgeError::AuthorizationDenied)?;
×
175

176
    let filter_set = FeatureFilterSet::from(Box::new(name_match_filter(feature_name.clone())))
×
177
        .with_filter(project_filter(&validated_token));
×
178

179
    match req.app_data::<Data<FeatureRefresher>>() {
×
180
        Some(refresher) => {
×
181
            refresher
×
182
                .features_for_filter(validated_token.clone(), &filter_set)
×
183
                .await
×
184
        }
185
        None => features_cache
×
186
            .get(&cache_key(&validated_token))
×
187
            .map(|client_features| filter_client_features(&client_features, &filter_set))
×
188
            .ok_or(EdgeError::ClientCacheError),
×
189
    }
190
    .map(|client_features| client_features.features.into_iter().next())?
×
191
    .ok_or(EdgeError::FeatureNotFound(feature_name.into_inner()))
×
192
    .map(Json)
193
}
194

195
#[utoipa::path(
196
    context_path = "/api/client",
197
    responses(
198
        (status = 202, description = "Accepted client application registration"),
199
        (status = 403, description = "Was not allowed to register client application"),
200
    ),
201
    request_body = ClientApplication,
202
    security(
203
        ("Authorization" = [])
204
    )
205
)]
206
#[post("/register")]
207
pub async fn register(
1✔
208
    edge_token: EdgeToken,
209
    connect_via: Data<ConnectVia>,
210
    client_application: Json<ClientApplication>,
211
    metrics_cache: Data<MetricsCache>,
212
) -> EdgeResult<HttpResponse> {
213
    crate::metrics::client_metrics::register_client_application(
214
        edge_token,
1✔
215
        &connect_via,
2✔
216
        client_application.into_inner(),
1✔
217
        metrics_cache,
1✔
218
    );
219
    Ok(HttpResponse::Accepted()
3✔
220
        .append_header(("X-Edge-Version", types::EDGE_VERSION))
1✔
221
        .finish())
222
}
223

224
#[utoipa::path(
225
    context_path = "/api/client",
226
    responses(
227
        (status = 202, description = "Accepted client metrics"),
228
        (status = 403, description = "Was not allowed to post metrics"),
229
    ),
230
    request_body = ClientMetrics,
231
    security(
232
        ("Authorization" = [])
233
    )
234
)]
235
#[post("/metrics")]
236
pub async fn metrics(
×
237
    edge_token: EdgeToken,
238
    metrics: Json<ClientMetrics>,
239
    metrics_cache: Data<MetricsCache>,
240
) -> EdgeResult<HttpResponse> {
241
    crate::metrics::client_metrics::register_client_metrics(
242
        edge_token,
×
243
        metrics.into_inner(),
×
244
        metrics_cache,
×
245
    );
246
    Ok(HttpResponse::Accepted().finish())
×
247
}
248

249
#[utoipa::path(
250
context_path = "/api/client",
251
responses(
252
(status = 202, description = "Accepted bulk metrics"),
253
(status = 403, description = "Was not allowed to post bulk metrics")
254
),
255
request_body = BatchMetricsRequestBody,
256
security(
257
("Authorization" = [])
258
)
259
)]
260
#[post("/metrics/bulk")]
261
pub async fn post_bulk_metrics(
×
262
    edge_token: EdgeToken,
263
    bulk_metrics: Json<BatchMetricsRequestBody>,
264
    connect_via: Data<ConnectVia>,
265
    metrics_cache: Data<MetricsCache>,
266
) -> EdgeResult<HttpResponse> {
267
    crate::metrics::client_metrics::register_bulk_metrics(
268
        metrics_cache.get_ref(),
×
269
        connect_via.get_ref(),
×
270
        &edge_token,
271
        bulk_metrics.into_inner(),
×
272
    );
273
    Ok(HttpResponse::Accepted().finish())
×
274
}
275
pub fn configure_client_api(cfg: &mut web::ServiceConfig) {
1✔
276
    let client_scope = web::scope("/client")
3✔
277
        .wrap(crate::middleware::as_async_middleware::as_async_middleware(
1✔
278
            crate::middleware::validate_token::validate_token,
279
        ))
280
        .service(get_features)
281
        .service(get_feature)
282
        .service(register)
283
        .service(metrics)
284
        .service(post_bulk_metrics)
285
        .service(stream_features);
286

287
    cfg.service(client_scope);
1✔
288
}
289

290
pub fn configure_experimental_post_features(
×
291
    cfg: &mut web::ServiceConfig,
292
    post_features_enabled: bool,
293
) {
294
    if post_features_enabled {
×
295
        cfg.service(post_features);
×
296
    }
297
}
298

299
#[cfg(test)]
300
mod tests {
301

302
    use crate::metrics::client_metrics::{ApplicationKey, MetricsBatch, MetricsKey};
303
    use crate::types::{TokenType, TokenValidationStatus};
304
    use std::collections::HashMap;
305
    use std::path::PathBuf;
306
    use std::str::FromStr;
307
    use std::sync::Arc;
308

309
    use super::*;
310

311
    use crate::auth::token_validator::TokenValidator;
312
    use crate::cli::{OfflineArgs, TokenHeader};
313
    use crate::http::unleash_client::{ClientMetaInformation, UnleashClient};
314
    use crate::middleware;
315
    use crate::tests::{features_from_disk, upstream_server};
316
    use actix_http::{Request, StatusCode};
317
    use actix_web::{
318
        http::header::ContentType,
319
        test,
320
        web::{self, Data},
321
        App, ResponseError,
322
    };
323
    use chrono::{DateTime, Duration, TimeZone, Utc};
324
    use maplit::hashmap;
325
    use ulid::Ulid;
326
    use unleash_types::client_features::{
327
        ClientFeature, Constraint, Operator, Strategy, StrategyVariant,
328
    };
329
    use unleash_types::client_metrics::{
330
        ClientMetricsEnv, ConnectViaBuilder, MetricBucket, MetricsMetadata, ToggleStats,
331
    };
332
    use unleash_yggdrasil::EngineState;
333

334
    async fn make_metrics_post_request() -> Request {
335
        test::TestRequest::post()
336
            .uri("/api/client/metrics")
337
            .insert_header(ContentType::json())
338
            .insert_header((
339
                "Authorization",
340
                "*:development.03fa5f506428fe80ed5640c351c7232e38940814d2923b08f5c05fa7",
341
            ))
342
            .set_json(Json(ClientMetrics {
343
                app_name: "some-app".into(),
344
                instance_id: Some("some-instance".into()),
345
                bucket: MetricBucket {
346
                    start: Utc.with_ymd_and_hms(1867, 11, 7, 12, 0, 0).unwrap(),
347
                    stop: Utc.with_ymd_and_hms(1934, 11, 7, 12, 0, 0).unwrap(),
348
                    toggles: hashmap! {
349
                        "some-feature".to_string() => ToggleStats {
350
                            yes: 1,
351
                            no: 0,
352
                            variants: hashmap! {}
353
                        }
354
                    },
355
                },
356
                environment: Some("development".into()),
357
                metadata: MetricsMetadata {
358
                    platform_name: Some("test".into()),
359
                    platform_version: Some("1.0".into()),
360
                    sdk_version: Some("1.0".into()),
361
                    yggdrasil_version: None,
362
                },
363
            }))
364
            .to_request()
365
    }
366

367
    async fn make_bulk_metrics_post_request(authorization: Option<String>) -> Request {
368
        let mut req = test::TestRequest::post()
369
            .uri("/api/client/metrics/bulk")
370
            .insert_header(ContentType::json());
371
        req = match authorization {
372
            Some(auth) => req.insert_header(("Authorization", auth)),
373
            None => req,
374
        };
375
        req.set_json(Json(BatchMetricsRequestBody {
376
            applications: vec![ClientApplication {
377
                app_name: "test_app".to_string(),
378
                connect_via: None,
379
                environment: None,
380
                instance_id: None,
381
                interval: 10,
382
                started: Default::default(),
383
                strategies: vec![],
384
                metadata: MetricsMetadata {
385
                    platform_name: None,
386
                    platform_version: None,
387
                    sdk_version: None,
388
                    yggdrasil_version: None,
389
                },
390
            }],
391
            metrics: vec![ClientMetricsEnv {
392
                feature_name: "".to_string(),
393
                app_name: "".to_string(),
394
                environment: "".to_string(),
395
                timestamp: Default::default(),
396
                yes: 0,
397
                no: 0,
398
                variants: Default::default(),
399
                metadata: MetricsMetadata {
400
                    platform_name: None,
401
                    platform_version: None,
402
                    sdk_version: None,
403
                    yggdrasil_version: None,
404
                },
405
            }],
406
        }))
407
        .to_request()
408
    }
409

410
    async fn make_register_post_request(application: ClientApplication) -> Request {
411
        test::TestRequest::post()
412
            .uri("/api/client/register")
413
            .insert_header(ContentType::json())
414
            .insert_header((
415
                "Authorization",
416
                "*:development.03fa5f506428fe80ed5640c351c7232e38940814d2923b08f5c05fa7",
417
            ))
418
            .set_json(Json(application))
419
            .to_request()
420
    }
421

422
    async fn make_features_request_with_token(token: EdgeToken) -> Request {
423
        test::TestRequest::get()
424
            .uri("/api/client/features")
425
            .insert_header(("Authorization", token.token))
426
            .to_request()
427
    }
428

429
    #[actix_web::test]
430
    async fn metrics_endpoint_correctly_aggregates_data() {
431
        let metrics_cache = Arc::new(MetricsCache::default());
432

433
        let app = test::init_service(
434
            App::new()
435
                .app_data(Data::new(ConnectVia {
436
                    app_name: "test".into(),
437
                    instance_id: Ulid::new().to_string(),
438
                }))
439
                .app_data(Data::from(metrics_cache.clone()))
440
                .service(web::scope("/api/client").service(metrics)),
441
        )
442
        .await;
443

444
        let req = make_metrics_post_request().await;
445
        let _result = test::call_and_read_body(&app, req).await;
446

447
        let cache = metrics_cache.clone();
448

449
        let found_metric = cache
450
            .metrics
451
            .get(&MetricsKey {
452
                app_name: "some-app".into(),
453
                feature_name: "some-feature".into(),
454
                timestamp: DateTime::parse_from_rfc3339("1867-11-07T12:00:00Z")
455
                    .unwrap()
456
                    .with_timezone(&Utc),
457
                environment: "development".into(),
458
            })
459
            .unwrap();
460

461
        let expected = ClientMetricsEnv {
462
            app_name: "some-app".into(),
463
            feature_name: "some-feature".into(),
464
            environment: "development".into(),
465
            timestamp: DateTime::parse_from_rfc3339("1867-11-07T12:00:00Z")
466
                .unwrap()
467
                .with_timezone(&Utc),
468
            yes: 1,
469
            no: 0,
470
            variants: HashMap::new(),
471
            metadata: MetricsMetadata {
472
                platform_name: None,
473
                platform_version: None,
474
                sdk_version: None,
475
                yggdrasil_version: None,
476
            },
477
        };
478

479
        assert_eq!(found_metric.yes, expected.yes);
480
        assert_eq!(found_metric.yes, 1);
481
        assert_eq!(found_metric.no, 0);
482
        assert_eq!(found_metric.no, expected.no);
483
    }
484

485
    fn cached_client_features() -> ClientFeatures {
486
        ClientFeatures {
487
            version: 2,
488
            features: vec![
489
                ClientFeature {
490
                    name: "feature_one".into(),
491
                    feature_type: Some("release".into()),
492
                    description: Some("test feature".into()),
493
                    created_at: Some(Utc::now()),
494
                    dependencies: None,
495
                    last_seen_at: None,
496
                    enabled: true,
497
                    stale: Some(false),
498
                    impression_data: Some(false),
499
                    project: Some("default".into()),
500
                    strategies: Some(vec![
501
                        Strategy {
502
                            variants: Some(vec![StrategyVariant {
503
                                name: "test".into(),
504
                                payload: None,
505
                                weight: 7,
506
                                stickiness: Some("sticky-on-something".into()),
507
                            }]),
508
                            name: "standard".into(),
509
                            sort_order: Some(500),
510
                            segments: None,
511
                            constraints: None,
512
                            parameters: None,
513
                        },
514
                        Strategy {
515
                            variants: None,
516
                            name: "gradualRollout".into(),
517
                            sort_order: Some(100),
518
                            segments: None,
519
                            constraints: None,
520
                            parameters: None,
521
                        },
522
                    ]),
523
                    variants: None,
524
                },
525
                ClientFeature {
526
                    name: "feature_two_no_strats".into(),
527
                    feature_type: None,
528
                    dependencies: None,
529
                    description: None,
530
                    created_at: Some(Utc.with_ymd_and_hms(2022, 12, 5, 12, 31, 0).unwrap()),
531
                    last_seen_at: None,
532
                    enabled: true,
533
                    stale: None,
534
                    impression_data: None,
535
                    project: Some("default".into()),
536
                    strategies: None,
537
                    variants: None,
538
                },
539
                ClientFeature {
540
                    name: "feature_three".into(),
541
                    feature_type: Some("release".into()),
542
                    description: None,
543
                    dependencies: None,
544
                    created_at: None,
545
                    last_seen_at: None,
546
                    enabled: true,
547
                    stale: None,
548
                    impression_data: None,
549
                    project: Some("default".into()),
550
                    strategies: Some(vec![
551
                        Strategy {
552
                            name: "gradualRollout".to_string(),
553
                            sort_order: None,
554
                            segments: None,
555
                            variants: None,
556
                            constraints: Some(vec![Constraint {
557
                                context_name: "version".to_string(),
558
                                operator: Operator::SemverGt,
559
                                case_insensitive: false,
560
                                inverted: false,
561
                                values: None,
562
                                value: Some("1.5.0".into()),
563
                            }]),
564
                            parameters: None,
565
                        },
566
                        Strategy {
567
                            name: "".to_string(),
568
                            sort_order: None,
569
                            segments: None,
570
                            constraints: None,
571
                            parameters: None,
572
                            variants: None,
573
                        },
574
                    ]),
575
                    variants: None,
576
                },
577
            ],
578
            segments: None,
579
            query: None,
580
            meta: None,
581
        }
582
    }
583

584
    #[tokio::test]
585
    async fn response_includes_variant_stickiness_for_strategy_variants() {
586
        let features_cache = Arc::new(FeatureCache::default());
587
        let token_cache: Arc<DashMap<String, EdgeToken>> = Arc::new(DashMap::default());
588
        let app = test::init_service(
589
            App::new()
590
                .app_data(Data::from(features_cache.clone()))
591
                .app_data(Data::from(token_cache.clone()))
592
                .service(web::scope("/api/client").service(get_features)),
593
        )
594
        .await;
595

596
        features_cache.insert("production".into(), cached_client_features());
597
        let mut production_token = EdgeToken::try_from(
598
            "*:production.03fa5f506428fe80ed5640c351c7232e38940814d2923b08f5c05fa7".to_string(),
599
        )
600
        .unwrap();
601
        production_token.token_type = Some(TokenType::Client);
602
        production_token.status = TokenValidationStatus::Validated;
603
        token_cache.insert(production_token.token.clone(), production_token.clone());
604
        let req = make_features_request_with_token(production_token.clone()).await;
605
        let res: ClientFeatures = test::call_and_read_body_json(&app, req).await;
606

607
        assert_eq!(res.features.len(), cached_client_features().features.len());
608
        let strategy_variant_stickiness = res
609
            .features
610
            .iter()
611
            .find(|f| f.name == "feature_one")
612
            .unwrap()
613
            .strategies
614
            .clone()
615
            .unwrap()
616
            .iter()
617
            .find(|s| s.name == "standard")
618
            .unwrap()
619
            .variants
620
            .clone()
621
            .unwrap()
622
            .iter()
623
            .find(|v| v.name == "test")
624
            .unwrap()
625
            .stickiness
626
            .clone();
627
        assert!(strategy_variant_stickiness.is_some());
628
    }
629

630
    #[tokio::test]
631
    async fn register_endpoint_correctly_aggregates_applications() {
632
        let metrics_cache = Arc::new(MetricsCache::default());
633
        let our_app = ConnectVia {
634
            app_name: "test".into(),
635
            instance_id: Ulid::new().to_string(),
636
        };
637
        let app = test::init_service(
638
            App::new()
639
                .app_data(Data::new(our_app.clone()))
640
                .app_data(Data::from(metrics_cache.clone()))
641
                .service(web::scope("/api/client").service(register)),
642
        )
643
        .await;
644
        let mut client_app = ClientApplication::new("test_application", 15);
645
        client_app.instance_id = Some("test_instance".into());
646
        let req = make_register_post_request(client_app.clone()).await;
647
        let res = test::call_service(&app, req).await;
648
        assert_eq!(res.status(), actix_http::StatusCode::ACCEPTED);
649
        assert_eq!(metrics_cache.applications.len(), 1);
650
        let application_key = ApplicationKey {
651
            app_name: client_app.app_name.clone(),
652
            instance_id: client_app.instance_id.unwrap(),
653
        };
654
        let saved_app = metrics_cache
655
            .applications
656
            .get(&application_key)
657
            .unwrap()
658
            .value()
659
            .clone();
660
        assert_eq!(saved_app.app_name, client_app.app_name);
661
        assert_eq!(saved_app.connect_via, Some(vec![our_app]));
662
    }
663

664
    #[tokio::test]
665
    async fn bulk_metrics_endpoint_correctly_accepts_data() {
666
        let metrics_cache = MetricsCache::default();
667
        let connect_via = ConnectViaBuilder::default()
668
            .app_name("unleash-edge".into())
669
            .instance_id("test".into())
670
            .build()
671
            .unwrap();
672
        let app = test::init_service(
673
            App::new()
674
                .app_data(Data::new(connect_via))
675
                .app_data(web::Data::new(metrics_cache))
676
                .service(web::scope("/api/client").service(post_bulk_metrics)),
677
        )
678
        .await;
679
        let token = EdgeToken::from_str("*:development.somestring").unwrap();
680
        let req = make_bulk_metrics_post_request(Some(token.token.clone())).await;
681
        let call = test::call_service(&app, req).await;
682
        assert_eq!(call.status(), StatusCode::ACCEPTED);
683
    }
684
    #[tokio::test]
685
    async fn bulk_metrics_endpoint_correctly_refuses_metrics_without_auth_header() {
686
        let mut token = EdgeToken::from_str("*:development.somestring").unwrap();
687
        token.status = TokenValidationStatus::Validated;
688
        token.token_type = Some(TokenType::Client);
689
        let upstream_token_cache = Arc::new(DashMap::default());
690
        let upstream_features_cache = Arc::new(FeatureCache::default());
691
        let upstream_engine_cache = Arc::new(DashMap::default());
692
        upstream_token_cache.insert(token.token.clone(), token.clone());
693
        let srv = upstream_server(
694
            upstream_token_cache,
695
            upstream_features_cache,
696
            upstream_engine_cache,
697
        )
698
        .await;
699
        let req = reqwest::Client::new();
700
        let status = req
701
            .post(srv.url("/api/client/metrics/bulk").as_str())
702
            .body(
703
                serde_json::to_string(&crate::types::BatchMetricsRequestBody {
704
                    applications: vec![],
705
                    metrics: vec![],
706
                })
707
                .unwrap(),
708
            )
709
            .send()
710
            .await;
711
        assert!(status.is_ok());
712
        assert_eq!(
713
            status.unwrap().status().as_u16(),
714
            StatusCode::FORBIDDEN.as_u16()
715
        );
716
        let client = UnleashClient::new(srv.url("/").as_str(), None).unwrap();
717
        let successful = client
718
            .send_bulk_metrics_to_client_endpoint(MetricsBatch::default(), &token.token)
719
            .await;
720
        assert!(successful.is_ok());
721
    }
722

723
    #[tokio::test]
724
    async fn bulk_metrics_endpoint_correctly_refuses_metrics_with_frontend_token() {
725
        let mut frontend_token = EdgeToken::from_str("*:development.frontend").unwrap();
726
        frontend_token.status = TokenValidationStatus::Validated;
727
        frontend_token.token_type = Some(TokenType::Frontend);
728
        let upstream_token_cache = Arc::new(DashMap::default());
729
        let upstream_features_cache = Arc::new(FeatureCache::default());
730
        let upstream_engine_cache = Arc::new(DashMap::default());
731
        upstream_token_cache.insert(frontend_token.token.clone(), frontend_token.clone());
732
        let srv = upstream_server(
733
            upstream_token_cache,
734
            upstream_features_cache,
735
            upstream_engine_cache,
736
        )
737
        .await;
738
        let client = UnleashClient::new(srv.url("/").as_str(), None).unwrap();
739
        let status = client
740
            .send_bulk_metrics_to_client_endpoint(MetricsBatch::default(), &frontend_token.token)
741
            .await;
742
        assert_eq!(status.expect_err("").status_code(), StatusCode::FORBIDDEN);
743
    }
744
    #[tokio::test]
745
    async fn register_endpoint_returns_version_header() {
746
        let metrics_cache = Arc::new(MetricsCache::default());
747
        let our_app = ConnectVia {
748
            app_name: "test".into(),
749
            instance_id: Ulid::new().to_string(),
750
        };
751
        let app = test::init_service(
752
            App::new()
753
                .app_data(Data::new(our_app.clone()))
754
                .app_data(Data::from(metrics_cache.clone()))
755
                .service(web::scope("/api/client").service(register)),
756
        )
757
        .await;
758
        let mut client_app = ClientApplication::new("test_application", 15);
759
        client_app.instance_id = Some("test_instance".into());
760
        let req = make_register_post_request(client_app.clone()).await;
761
        let res = test::call_service(&app, req).await;
762
        assert_eq!(res.status(), StatusCode::ACCEPTED);
763
        assert_eq!(
764
            res.headers().get("X-Edge-Version").unwrap(),
765
            types::EDGE_VERSION
766
        );
767
    }
768

769
    #[tokio::test]
770
    async fn client_features_endpoint_correctly_returns_cached_features() {
771
        let features_cache = Arc::new(FeatureCache::default());
772
        let token_cache: Arc<DashMap<String, EdgeToken>> = Arc::new(DashMap::default());
773
        let app = test::init_service(
774
            App::new()
775
                .app_data(Data::from(features_cache.clone()))
776
                .app_data(Data::from(token_cache.clone()))
777
                .service(web::scope("/api/client").service(get_features)),
778
        )
779
        .await;
780
        let client_features = cached_client_features();
781
        let example_features = features_from_disk("../examples/features.json");
782
        features_cache.insert("development".into(), client_features.clone());
783
        features_cache.insert("production".into(), example_features.clone());
784
        let mut token = EdgeToken::try_from(
785
            "*:development.03fa5f506428fe80ed5640c351c7232e38940814d2923b08f5c05fa7".to_string(),
786
        )
787
        .unwrap();
788
        token.token_type = Some(TokenType::Client);
789
        token.status = TokenValidationStatus::Validated;
790
        token_cache.insert(token.token.clone(), token.clone());
791
        let req = make_features_request_with_token(token.clone()).await;
792
        let res: ClientFeatures = test::call_and_read_body_json(&app, req).await;
793
        assert_eq!(res.features, client_features.features);
794
        let mut production_token = EdgeToken::try_from(
795
            "*:production.03fa5f506428fe80ed5640c351c7232e38940814d2923b08f5c05fa7".to_string(),
796
        )
797
        .unwrap();
798
        production_token.token_type = Some(TokenType::Client);
799
        production_token.status = TokenValidationStatus::Validated;
800
        token_cache.insert(production_token.token.clone(), production_token.clone());
801
        let req = make_features_request_with_token(production_token.clone()).await;
802
        let res: ClientFeatures = test::call_and_read_body_json(&app, req).await;
803
        assert_eq!(res.features.len(), example_features.features.len());
804
    }
805

806
    #[tokio::test]
807
    async fn post_request_to_client_features_does_the_same_as_get_when_mounted() {
808
        let features_cache = Arc::new(FeatureCache::default());
809
        let token_cache: Arc<DashMap<String, EdgeToken>> = Arc::new(DashMap::default());
810
        let app = test::init_service(
811
            App::new()
812
                .app_data(Data::from(features_cache.clone()))
813
                .app_data(Data::from(token_cache.clone()))
814
                .service(
815
                    web::scope("/api/client")
816
                        .service(get_features)
817
                        .service(post_features),
818
                ),
819
        )
820
        .await;
821
        let client_features = cached_client_features();
822
        let example_features = features_from_disk("../examples/features.json");
823
        features_cache.insert("development".into(), client_features.clone());
824
        features_cache.insert("production".into(), example_features.clone());
825
        let mut token = EdgeToken::try_from(
826
            "*:development.03fa5f506428fe80ed5640c351c7232e38940814d2923b08f5c05fa7".to_string(),
827
        )
828
        .unwrap();
829
        token.token_type = Some(TokenType::Client);
830
        token.status = TokenValidationStatus::Validated;
831
        token_cache.insert(token.token.clone(), token.clone());
832
        let req = make_features_request_with_token(token.clone()).await;
833
        let res: ClientFeatures = test::call_and_read_body_json(&app, req).await;
834
        assert_eq!(res.features, client_features.features);
835
        let mut production_token = EdgeToken::try_from(
836
            "*:production.03fa5f506428fe80ed5640c351c7232e38940814d2923b08f5c05fa7".to_string(),
837
        )
838
        .unwrap();
839
        production_token.token_type = Some(TokenType::Client);
840
        production_token.status = TokenValidationStatus::Validated;
841
        token_cache.insert(production_token.token.clone(), production_token.clone());
842

843
        let post_req = test::TestRequest::post()
844
            .uri("/api/client/features")
845
            .insert_header(("Authorization", production_token.clone().token))
846
            .insert_header(ContentType::json())
847
            .to_request();
848

849
        let get_req = make_features_request_with_token(production_token.clone()).await;
850
        let get_res: ClientFeatures = test::call_and_read_body_json(&app, get_req).await;
851
        let post_res: ClientFeatures = test::call_and_read_body_json(&app, post_req).await;
852

853
        assert_eq!(get_res.features, post_res.features)
854
    }
855

856
    #[tokio::test]
857
    async fn client_features_endpoint_filters_on_project_access_in_token() {
858
        let features_cache = Arc::new(FeatureCache::default());
859
        let token_cache: Arc<DashMap<String, EdgeToken>> = Arc::new(DashMap::default());
860
        let app = test::init_service(
861
            App::new()
862
                .app_data(Data::from(features_cache.clone()))
863
                .app_data(Data::from(token_cache.clone()))
864
                .service(web::scope("/api/client").service(get_features)),
865
        )
866
        .await;
867
        let mut edge_token = EdgeToken::try_from(
868
            "demo-app:production.03fa5f506428fe80ed5640c351c7232e38940814d2923b08f5c05fa7"
869
                .to_string(),
870
        )
871
        .unwrap();
872
        edge_token.token_type = Some(TokenType::Client);
873
        token_cache.insert(edge_token.token.clone(), edge_token.clone());
874
        let example_features = features_from_disk("../examples/features.json");
875
        features_cache.insert("production".into(), example_features.clone());
876
        let req = make_features_request_with_token(edge_token.clone()).await;
877
        let res: ClientFeatures = test::call_and_read_body_json(&app, req).await;
878
        assert_eq!(res.features.len(), 5);
879
        assert!(res
880
            .features
881
            .iter()
882
            .all(|t| t.project == Some("demo-app".into())));
883
    }
884

885
    #[tokio::test]
886
    async fn client_features_endpoint_filters_when_multiple_projects_in_token() {
887
        let features_cache = Arc::new(FeatureCache::default());
888
        let token_cache: Arc<DashMap<String, EdgeToken>> = Arc::new(DashMap::default());
889
        let app = test::init_service(
890
            App::new()
891
                .app_data(Data::from(features_cache.clone()))
892
                .app_data(Data::from(token_cache.clone()))
893
                .service(web::scope("/api/client").service(get_features)),
894
        )
895
        .await;
896
        let mut token =
897
            EdgeToken::try_from("[]:production.puff_the_magic_dragon".to_string()).unwrap();
898
        token.projects = vec!["dx".into(), "eg".into(), "unleash-cloud".into()];
899
        token.status = TokenValidationStatus::Validated;
900
        token.token_type = Some(TokenType::Client);
901
        token_cache.insert(token.token.clone(), token.clone());
902
        let example_features = features_from_disk("../examples/hostedexample.json");
903
        features_cache.insert("production".into(), example_features.clone());
904
        let req = make_features_request_with_token(token.clone()).await;
905
        let res: ClientFeatures = test::call_and_read_body_json(&app, req).await;
906
        assert_eq!(res.features.len(), 24);
907
        assert!(res
908
            .features
909
            .iter()
910
            .all(|f| token.projects.contains(&f.project.clone().unwrap())));
911
    }
912

913
    #[tokio::test]
914
    async fn client_features_endpoint_filters_correctly_when_token_has_access_to_multiple_projects()
915
    {
916
        let features_cache = Arc::new(FeatureCache::default());
917
        let token_cache: Arc<DashMap<String, EdgeToken>> = Arc::new(DashMap::default());
918
        let app = test::init_service(
919
            App::new()
920
                .app_data(Data::from(features_cache.clone()))
921
                .app_data(Data::from(token_cache.clone()))
922
                .service(web::scope("/api/client").service(get_features)),
923
        )
924
        .await;
925

926
        let mut token_a =
927
            EdgeToken::try_from("[]:production.puff_the_magic_dragon".to_string()).unwrap();
928
        token_a.projects = vec!["dx".into(), "eg".into()];
929
        token_a.status = TokenValidationStatus::Validated;
930
        token_a.token_type = Some(TokenType::Client);
931
        token_cache.insert(token_a.token.clone(), token_a.clone());
932

933
        let mut token_b =
934
            EdgeToken::try_from("[]:production.biff_the_magic_flagon".to_string()).unwrap();
935
        token_b.projects = vec!["unleash-cloud".into()];
936
        token_b.status = TokenValidationStatus::Validated;
937
        token_b.token_type = Some(TokenType::Client);
938
        token_cache.insert(token_b.token.clone(), token_b.clone());
939

940
        let example_features = features_from_disk("../examples/hostedexample.json");
941
        features_cache.insert("production".into(), example_features.clone());
942

943
        let req_1 = make_features_request_with_token(token_a.clone()).await;
944
        let res_1: ClientFeatures = test::call_and_read_body_json(&app, req_1).await;
945
        assert!(res_1
946
            .features
947
            .iter()
948
            .all(|f| token_a.projects.contains(&f.project.clone().unwrap())));
949

950
        let req_2 = make_features_request_with_token(token_b.clone()).await;
951
        let res_2: ClientFeatures = test::call_and_read_body_json(&app, req_2).await;
952
        assert!(res_2
953
            .features
954
            .iter()
955
            .all(|f| token_b.projects.contains(&f.project.clone().unwrap())));
956

957
        let req_3 = make_features_request_with_token(token_a.clone()).await;
958
        let res_3: ClientFeatures = test::call_and_read_body_json(&app, req_3).await;
959
        assert!(res_3
960
            .features
961
            .iter()
962
            .all(|f| token_a.projects.contains(&f.project.clone().unwrap())));
963
    }
964

965
    #[tokio::test]
966
    async fn when_running_in_offline_mode_with_proxy_key_should_not_filter_features() {
967
        let features_cache = Arc::new(FeatureCache::default());
968
        let token_cache: Arc<DashMap<String, EdgeToken>> = Arc::new(DashMap::default());
969
        let app = test::init_service(
970
            App::new()
971
                .app_data(Data::from(features_cache.clone()))
972
                .app_data(Data::from(token_cache.clone()))
973
                .app_data(Data::new(crate::cli::EdgeMode::Offline(OfflineArgs {
974
                    bootstrap_file: Some(PathBuf::from("../examples/features.json")),
975
                    tokens: vec!["secret_123".into()],
976
                    client_tokens: vec![],
977
                    frontend_tokens: vec![],
978
                    reload_interval: 0,
979
                })))
980
                .service(web::scope("/api/client").service(get_features)),
981
        )
982
        .await;
983
        let token = EdgeToken::offline_token("secret-123");
984
        token_cache.insert(token.token.clone(), token.clone());
985
        let example_features = features_from_disk("../examples/features.json");
986
        features_cache.insert(token.token.clone(), example_features.clone());
987
        let req = make_features_request_with_token(token.clone()).await;
988
        let res: ClientFeatures = test::call_and_read_body_json(&app, req).await;
989
        assert_eq!(res.features.len(), example_features.features.len());
990
    }
991

992
    #[tokio::test]
993
    async fn calling_client_features_endpoint_with_new_token_hydrates_from_upstream_when_dynamic() {
994
        let upstream_features_cache = Arc::new(FeatureCache::default());
995
        let upstream_token_cache: Arc<DashMap<String, EdgeToken>> = Arc::new(DashMap::default());
996
        let upstream_engine_cache: Arc<DashMap<String, EngineState>> = Arc::new(DashMap::default());
997
        let server = upstream_server(
998
            upstream_token_cache.clone(),
999
            upstream_features_cache.clone(),
1000
            upstream_engine_cache.clone(),
1001
        )
1002
        .await;
1003
        let upstream_features = features_from_disk("../examples/hostedexample.json");
1004
        let mut upstream_known_token = EdgeToken::from_str("dx:development.secret123").unwrap();
1005
        upstream_known_token.status = TokenValidationStatus::Validated;
1006
        upstream_known_token.token_type = Some(TokenType::Client);
1007
        upstream_token_cache.insert(
1008
            upstream_known_token.token.clone(),
1009
            upstream_known_token.clone(),
1010
        );
1011
        upstream_features_cache.insert(cache_key(&upstream_known_token), upstream_features.clone());
1012
        let unleash_client = Arc::new(UnleashClient::new(server.url("/").as_str(), None).unwrap());
1013
        let features_cache: Arc<FeatureCache> = Arc::new(FeatureCache::default());
1014
        let token_cache: Arc<DashMap<String, EdgeToken>> = Arc::new(DashMap::default());
1015
        let engine_cache: Arc<DashMap<String, EngineState>> = Arc::new(DashMap::default());
1016
        let feature_refresher = Arc::new(FeatureRefresher {
1017
            unleash_client: unleash_client.clone(),
1018
            tokens_to_refresh: Arc::new(Default::default()),
1019
            features_cache: features_cache.clone(),
1020
            engine_cache: engine_cache.clone(),
1021
            refresh_interval: Duration::seconds(6000),
1022
            persistence: None,
1023
            strict: false,
1024
            streaming: false,
1025
            client_meta_information: ClientMetaInformation::test_config(),
1026
            delta: false,
1027
            delta_diff: false,
1028
        });
1029
        let token_validator = Arc::new(TokenValidator {
1030
            unleash_client: unleash_client.clone(),
1031
            token_cache: token_cache.clone(),
1032
            persistence: None,
1033
        });
1034
        let local_app = test::init_service(
1035
            App::new()
1036
                .app_data(Data::from(token_validator.clone()))
1037
                .app_data(Data::from(features_cache.clone()))
1038
                .app_data(Data::from(engine_cache.clone()))
1039
                .app_data(Data::from(token_cache.clone()))
1040
                .app_data(Data::from(feature_refresher.clone()))
1041
                .wrap(middleware::as_async_middleware::as_async_middleware(
1042
                    middleware::validate_token::validate_token,
1043
                ))
1044
                .service(web::scope("/api").configure(configure_client_api)),
1045
        )
1046
        .await;
1047
        let req = test::TestRequest::get()
1048
            .uri("/api/client/features")
1049
            .insert_header(ContentType::json())
1050
            .insert_header(("Authorization", upstream_known_token.token.clone()))
1051
            .to_request();
1052
        let res = test::call_service(&local_app, req).await;
1053
        assert_eq!(res.status(), StatusCode::OK);
1054
    }
1055

1056
    #[tokio::test]
1057
    async fn calling_client_features_endpoint_with_new_token_does_not_hydrate_when_strict() {
1058
        let upstream_features_cache = Arc::new(FeatureCache::default());
1059
        let upstream_token_cache: Arc<DashMap<String, EdgeToken>> = Arc::new(DashMap::default());
1060
        let upstream_engine_cache: Arc<DashMap<String, EngineState>> = Arc::new(DashMap::default());
1061
        let server = upstream_server(
1062
            upstream_token_cache.clone(),
1063
            upstream_features_cache.clone(),
1064
            upstream_engine_cache.clone(),
1065
        )
1066
        .await;
1067
        let upstream_features = features_from_disk("../examples/hostedexample.json");
1068
        let mut upstream_known_token = EdgeToken::from_str("dx:development.secret123").unwrap();
1069
        upstream_known_token.status = TokenValidationStatus::Validated;
1070
        upstream_known_token.token_type = Some(TokenType::Client);
1071
        upstream_token_cache.insert(
1072
            upstream_known_token.token.clone(),
1073
            upstream_known_token.clone(),
1074
        );
1075
        upstream_features_cache.insert(cache_key(&upstream_known_token), upstream_features.clone());
1076
        let unleash_client = Arc::new(UnleashClient::new(server.url("/").as_str(), None).unwrap());
1077
        let features_cache: Arc<FeatureCache> = Arc::new(FeatureCache::default());
1078
        let token_cache: Arc<DashMap<String, EdgeToken>> = Arc::new(DashMap::default());
1079
        let engine_cache: Arc<DashMap<String, EngineState>> = Arc::new(DashMap::default());
1080
        let feature_refresher = Arc::new(FeatureRefresher {
1081
            unleash_client: unleash_client.clone(),
1082
            features_cache: features_cache.clone(),
1083
            engine_cache: engine_cache.clone(),
1084
            refresh_interval: Duration::seconds(6000),
1085
            ..Default::default()
1086
        });
1087
        let token_validator = Arc::new(TokenValidator {
1088
            unleash_client: unleash_client.clone(),
1089
            token_cache: token_cache.clone(),
1090
            persistence: None,
1091
        });
1092
        let local_app = test::init_service(
1093
            App::new()
1094
                .app_data(Data::from(token_validator.clone()))
1095
                .app_data(Data::from(features_cache.clone()))
1096
                .app_data(Data::from(engine_cache.clone()))
1097
                .app_data(Data::from(token_cache.clone()))
1098
                .app_data(Data::from(feature_refresher.clone()))
1099
                .wrap(middleware::as_async_middleware::as_async_middleware(
1100
                    middleware::validate_token::validate_token,
1101
                ))
1102
                .service(web::scope("/api").configure(configure_client_api)),
1103
        )
1104
        .await;
1105
        let req = test::TestRequest::get()
1106
            .uri("/api/client/features")
1107
            .insert_header(ContentType::json())
1108
            .insert_header(("Authorization", upstream_known_token.token.clone()))
1109
            .to_request();
1110
        let res = test::call_service(&local_app, req).await;
1111
        assert_eq!(res.status(), StatusCode::FORBIDDEN);
1112
    }
1113

1114
    #[tokio::test]
1115
    pub async fn gets_feature_by_name() {
1116
        let features_cache = Arc::new(FeatureCache::default());
1117
        let token_cache: Arc<DashMap<String, EdgeToken>> = Arc::new(DashMap::default());
1118
        let engine_cache: Arc<DashMap<String, EngineState>> = Arc::new(DashMap::default());
1119
        let features = features_from_disk("../examples/hostedexample.json");
1120
        let mut dx_token = EdgeToken::from_str("dx:development.secret123").unwrap();
1121
        dx_token.status = TokenValidationStatus::Validated;
1122
        dx_token.token_type = Some(TokenType::Client);
1123
        token_cache.insert(dx_token.token.clone(), dx_token.clone());
1124
        features_cache.insert(cache_key(&dx_token), features.clone());
1125
        let local_app = test::init_service(
1126
            App::new()
1127
                .app_data(Data::from(features_cache.clone()))
1128
                .app_data(Data::from(engine_cache.clone()))
1129
                .app_data(Data::from(token_cache.clone()))
1130
                .wrap(middleware::as_async_middleware::as_async_middleware(
1131
                    middleware::validate_token::validate_token,
1132
                ))
1133
                .service(web::scope("/api").configure(configure_client_api)),
1134
        )
1135
        .await;
1136
        let desired_toggle = "projectStatusApi";
1137
        let request = test::TestRequest::get()
1138
            .uri(format!("/api/client/features/{desired_toggle}").as_str())
1139
            .insert_header(ContentType::json())
1140
            .insert_header(("Authorization", dx_token.token.clone()))
1141
            .to_request();
1142
        let result: ClientFeature = test::call_and_read_body_json(&local_app, request).await;
1143
        assert_eq!(result.name, desired_toggle);
1144
    }
1145

1146
    #[tokio::test]
1147
    pub async fn token_with_no_access_to_named_feature_yields_404() {
1148
        let features_cache = Arc::new(FeatureCache::default());
1149
        let token_cache: Arc<DashMap<String, EdgeToken>> = Arc::new(DashMap::default());
1150
        let engine_cache: Arc<DashMap<String, EngineState>> = Arc::new(DashMap::default());
1151
        let features = features_from_disk("../examples/hostedexample.json");
1152
        let mut dx_token = EdgeToken::from_str("dx:development.secret123").unwrap();
1153
        dx_token.status = TokenValidationStatus::Validated;
1154
        dx_token.token_type = Some(TokenType::Client);
1155
        token_cache.insert(dx_token.token.clone(), dx_token.clone());
1156
        features_cache.insert(cache_key(&dx_token), features.clone());
1157
        let local_app = test::init_service(
1158
            App::new()
1159
                .app_data(Data::from(features_cache.clone()))
1160
                .app_data(Data::from(engine_cache.clone()))
1161
                .app_data(Data::from(token_cache.clone()))
1162
                .wrap(middleware::as_async_middleware::as_async_middleware(
1163
                    middleware::validate_token::validate_token,
1164
                ))
1165
                .service(web::scope("/api").configure(configure_client_api)),
1166
        )
1167
        .await;
1168
        let desired_toggle = "serviceAccounts";
1169
        let request = test::TestRequest::get()
1170
            .uri(format!("/api/client/features/{desired_toggle}").as_str())
1171
            .insert_header(ContentType::json())
1172
            .insert_header(("Authorization", dx_token.token.clone()))
1173
            .to_request();
1174
        let result = test::call_service(&local_app, request).await;
1175
        assert_eq!(result.status(), StatusCode::NOT_FOUND);
1176
    }
1177
    #[tokio::test]
1178
    pub async fn still_subsumes_tokens_after_moving_registration_to_initial_hydration_when_dynamic()
1179
    {
1180
        let upstream_features_cache: Arc<FeatureCache> = Arc::new(FeatureCache::default());
1181
        let upstream_token_cache: Arc<DashMap<String, EdgeToken>> = Arc::new(DashMap::default());
1182
        let upstream_engine_cache: Arc<DashMap<String, EngineState>> = Arc::new(DashMap::default());
1183
        let server = upstream_server(
1184
            upstream_token_cache.clone(),
1185
            upstream_features_cache.clone(),
1186
            upstream_engine_cache.clone(),
1187
        )
1188
        .await;
1189
        let upstream_features = features_from_disk("../examples/hostedexample.json");
1190
        let mut upstream_dx_token = EdgeToken::from_str("dx:development.secret123").unwrap();
1191
        upstream_dx_token.status = TokenValidationStatus::Validated;
1192
        upstream_dx_token.token_type = Some(TokenType::Client);
1193
        upstream_token_cache.insert(upstream_dx_token.token.clone(), upstream_dx_token.clone());
1194
        let mut upstream_eg_token = EdgeToken::from_str("eg:development.secret321").unwrap();
1195
        upstream_eg_token.status = TokenValidationStatus::Validated;
1196
        upstream_eg_token.token_type = Some(TokenType::Client);
1197
        upstream_token_cache.insert(upstream_eg_token.token.clone(), upstream_eg_token.clone());
1198
        upstream_features_cache.insert(cache_key(&upstream_dx_token), upstream_features.clone());
1199
        let unleash_client = Arc::new(UnleashClient::new(server.url("/").as_str(), None).unwrap());
1200
        let features_cache: Arc<FeatureCache> = Arc::new(FeatureCache::default());
1201
        let token_cache: Arc<DashMap<String, EdgeToken>> = Arc::new(DashMap::default());
1202
        let engine_cache: Arc<DashMap<String, EngineState>> = Arc::new(DashMap::default());
1203
        let feature_refresher = Arc::new(FeatureRefresher {
1204
            unleash_client: unleash_client.clone(),
1205
            features_cache: features_cache.clone(),
1206
            engine_cache: engine_cache.clone(),
1207
            refresh_interval: Duration::seconds(6000),
1208
            strict: false,
1209
            ..Default::default()
1210
        });
1211
        let token_validator = Arc::new(TokenValidator {
1212
            unleash_client: unleash_client.clone(),
1213
            token_cache: token_cache.clone(),
1214
            persistence: None,
1215
        });
1216
        let local_app = test::init_service(
1217
            App::new()
1218
                .app_data(Data::from(token_validator.clone()))
1219
                .app_data(Data::from(features_cache.clone()))
1220
                .app_data(Data::from(engine_cache.clone()))
1221
                .app_data(Data::from(token_cache.clone()))
1222
                .app_data(Data::from(feature_refresher.clone()))
1223
                .wrap(middleware::as_async_middleware::as_async_middleware(
1224
                    middleware::validate_token::validate_token,
1225
                ))
1226
                .service(web::scope("/api").configure(configure_client_api)),
1227
        )
1228
        .await;
1229
        let dx_req = test::TestRequest::get()
1230
            .uri("/api/client/features")
1231
            .insert_header(ContentType::json())
1232
            .insert_header(("Authorization", upstream_dx_token.token.clone()))
1233
            .to_request();
1234
        let res: ClientFeatures = test::call_and_read_body_json(&local_app, dx_req).await;
1235
        assert!(!res.features.is_empty());
1236
        let eg_req = test::TestRequest::get()
1237
            .uri("/api/client/features")
1238
            .insert_header(ContentType::json())
1239
            .insert_header(("Authorization", upstream_eg_token.token.clone()))
1240
            .to_request();
1241
        let eg_res: ClientFeatures = test::call_and_read_body_json(&local_app, eg_req).await;
1242
        assert!(!eg_res.features.is_empty());
1243
        assert_eq!(feature_refresher.tokens_to_refresh.len(), 2);
1244
        assert_eq!(features_cache.len(), 1);
1245
    }
1246

1247
    #[tokio::test]
1248
    pub async fn can_filter_features_list_by_name_prefix() {
1249
        let features_cache = Arc::new(FeatureCache::default());
1250
        let token_cache: Arc<DashMap<String, EdgeToken>> = Arc::new(DashMap::default());
1251
        let engine_cache: Arc<DashMap<String, EngineState>> = Arc::new(DashMap::default());
1252
        let features = features_from_disk("../examples/hostedexample.json");
1253
        let mut dx_token = EdgeToken::from_str("dx:development.secret123").unwrap();
1254
        dx_token.status = TokenValidationStatus::Validated;
1255
        dx_token.token_type = Some(TokenType::Client);
1256
        token_cache.insert(dx_token.token.clone(), dx_token.clone());
1257
        features_cache.insert(cache_key(&dx_token), features.clone());
1258
        let local_app = test::init_service(
1259
            App::new()
1260
                .app_data(Data::from(features_cache.clone()))
1261
                .app_data(Data::from(engine_cache.clone()))
1262
                .app_data(Data::from(token_cache.clone()))
1263
                .wrap(middleware::as_async_middleware::as_async_middleware(
1264
                    middleware::validate_token::validate_token,
1265
                ))
1266
                .service(web::scope("/api").configure(configure_client_api)),
1267
        )
1268
        .await;
1269
        let request = test::TestRequest::get()
1270
            .uri("/api/client/features?namePrefix=embed")
1271
            .insert_header(ContentType::json())
1272
            .insert_header(("Authorization", dx_token.token.clone()))
1273
            .to_request();
1274
        let result: ClientFeatures = test::call_and_read_body_json(&local_app, request).await;
1275
        assert_eq!(result.features.len(), 2);
1276
        assert_eq!(result.query.unwrap().name_prefix.unwrap(), "embed");
1277
    }
1278

1279
    #[tokio::test]
1280
    pub async fn only_gets_correct_feature_by_name() {
1281
        let features_cache = Arc::new(FeatureCache::default());
1282
        let token_cache: Arc<DashMap<String, EdgeToken>> = Arc::new(DashMap::default());
1283
        let engine_cache: Arc<DashMap<String, EngineState>> = Arc::new(DashMap::default());
1284
        let features = ClientFeatures {
1285
            version: 2,
1286
            query: None,
1287
            features: vec![
1288
                ClientFeature {
1289
                    name: "edge-flag-1".into(),
1290
                    feature_type: None,
1291
                    dependencies: None,
1292
                    description: None,
1293
                    created_at: None,
1294
                    last_seen_at: None,
1295
                    enabled: true,
1296
                    stale: None,
1297
                    impression_data: None,
1298
                    project: Some("dx".into()),
1299
                    strategies: None,
1300
                    variants: None,
1301
                },
1302
                ClientFeature {
1303
                    name: "edge-flag-3".into(),
1304
                    feature_type: None,
1305
                    dependencies: None,
1306
                    description: None,
1307
                    created_at: None,
1308
                    last_seen_at: None,
1309
                    enabled: true,
1310
                    stale: None,
1311
                    impression_data: None,
1312
                    project: Some("eg".into()),
1313
                    strategies: None,
1314
                    variants: None,
1315
                },
1316
            ],
1317
            segments: None,
1318
            meta: None,
1319
        };
1320
        let mut dx_token = EdgeToken::from_str("dx:development.secret123").unwrap();
1321
        dx_token.status = TokenValidationStatus::Validated;
1322
        dx_token.token_type = Some(TokenType::Client);
1323
        let mut eg_token = EdgeToken::from_str("eg:development.secret123").unwrap();
1324
        eg_token.status = TokenValidationStatus::Validated;
1325
        eg_token.token_type = Some(TokenType::Client);
1326
        token_cache.insert(dx_token.token.clone(), dx_token.clone());
1327
        token_cache.insert(eg_token.token.clone(), eg_token.clone());
1328
        features_cache.insert(cache_key(&dx_token), features.clone());
1329
        let local_app = test::init_service(
1330
            App::new()
1331
                .app_data(Data::from(features_cache.clone()))
1332
                .app_data(Data::from(engine_cache.clone()))
1333
                .app_data(Data::from(token_cache.clone()))
1334
                .wrap(middleware::as_async_middleware::as_async_middleware(
1335
                    middleware::validate_token::validate_token,
1336
                ))
1337
                .service(web::scope("/api").configure(configure_client_api)),
1338
        )
1339
        .await;
1340
        let successful_request = test::TestRequest::get()
1341
            .uri("/api/client/features/edge-flag-3")
1342
            .insert_header(ContentType::json())
1343
            .insert_header(("Authorization", eg_token.token.clone()))
1344
            .to_request();
1345
        let res = test::call_service(&local_app, successful_request).await;
1346
        assert_eq!(res.status(), StatusCode::OK);
1347
        let request = test::TestRequest::get()
1348
            .uri("/api/client/features/edge-flag-3")
1349
            .insert_header(ContentType::json())
1350
            .insert_header(("Authorization", dx_token.token.clone()))
1351
            .to_request();
1352
        let res = test::call_service(&local_app, request).await;
1353
        assert_eq!(res.status(), StatusCode::NOT_FOUND);
1354
    }
1355

1356
    #[tokio::test]
1357
    async fn client_features_endpoint_works_with_overridden_token_header() {
1358
        let features_cache = Arc::new(FeatureCache::default());
1359
        let token_cache: Arc<DashMap<String, EdgeToken>> = Arc::new(DashMap::default());
1360
        let token_header = TokenHeader::from_str("NeedsToBeTested").unwrap();
1361
        println!("token_header: {:?}", token_header);
1362
        let app = test::init_service(
1363
            App::new()
1364
                .app_data(Data::from(features_cache.clone()))
1365
                .app_data(Data::from(token_cache.clone()))
1366
                .app_data(Data::new(token_header.clone()))
1367
                .service(web::scope("/api/client").service(get_features)),
1368
        )
1369
        .await;
1370
        let client_features = cached_client_features();
1371
        let example_features = features_from_disk("../examples/features.json");
1372
        features_cache.insert("development".into(), client_features.clone());
1373
        features_cache.insert("production".into(), example_features.clone());
1374
        let mut production_token = EdgeToken::try_from(
1375
            "*:production.03fa5f506428fe80ed5640c351c7232e38940814d2923b08f5c05fa7".to_string(),
1376
        )
1377
        .unwrap();
1378
        production_token.token_type = Some(TokenType::Client);
1379
        production_token.status = TokenValidationStatus::Validated;
1380
        token_cache.insert(production_token.token.clone(), production_token.clone());
1381

1382
        let request = test::TestRequest::get()
1383
            .uri("/api/client/features")
1384
            .insert_header(ContentType::json())
1385
            .insert_header(("NeedsToBeTested", production_token.token.clone()))
1386
            .to_request();
1387
        let res = test::call_service(&app, request).await;
1388
        assert_eq!(res.status(), StatusCode::OK);
1389
        let request = test::TestRequest::get()
1390
            .uri("/api/client/features")
1391
            .insert_header(ContentType::json())
1392
            .insert_header(("ShouldNotWork", production_token.token.clone()))
1393
            .to_request();
1394
        let res = test::call_service(&app, request).await;
1395
        assert_eq!(res.status(), StatusCode::FORBIDDEN);
1396
    }
1397
}
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