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

Unleash / unleash-edge / #1603

20 Feb 2025 01:15PM UTC coverage: 64.335% (-6.0%) from 70.33%
#1603

push

web-flow
feat: Add edge observability (#713)

get latency for own endpoints from prometheus
get latency for upstream endpoints from prometheus
get process stats from prometheus
instantiate on startup
---------

Co-authored-by: Nuno Góis <github@nunogois.com>

27 of 252 new or added lines in 6 files covered. (10.71%)

1 existing line in 1 file now uncovered.

1582 of 2459 relevant lines covered (64.34%)

1.57 hits per line

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

71.43
/server/src/client_api.rs
1
use crate::cli::{EdgeArgs, EdgeMode};
2
use crate::delta_cache::DeltaCache;
3
use crate::error::EdgeError;
4
use crate::feature_cache::FeatureCache;
5
use crate::filters::{
6
    filter_client_features, filter_delta_events, name_match_filter, name_prefix_filter,
7
    project_filter, FeatureFilterSet,
8
};
9
use crate::http::broadcaster::Broadcaster;
10
use crate::http::instance_data::InstanceDataSending;
11
use crate::http::refresher::delta_refresher::Environment;
12
use crate::http::refresher::feature_refresher::FeatureRefresher;
13
use crate::metrics::client_metrics::MetricsCache;
14
use crate::metrics::edge_metrics::EdgeInstanceData;
15
use crate::tokens::cache_key;
16
use crate::types::{
17
    self, BatchMetricsRequestBody, EdgeJsonResult, EdgeResult, EdgeToken, FeatureFilters,
18
};
19
use actix_web::web::{self, Data, Json, Query};
20
use actix_web::Responder;
21
use actix_web::{get, post, HttpRequest, HttpResponse};
22
use dashmap::DashMap;
23
use tokio::sync::RwLock;
24
use tracing::instrument;
25
use unleash_types::client_features::{ClientFeature, ClientFeatures, ClientFeaturesDelta};
26
use unleash_types::client_metrics::{ClientApplication, ClientMetrics, ConnectVia};
27

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

51
#[get("/delta")]
52
pub async fn get_delta(
×
53
    edge_token: EdgeToken,
54
    delta_cache: Data<DashMap<String, DeltaCache>>,
55
    token_cache: Data<DashMap<String, EdgeToken>>,
56
    filter_query: Query<FeatureFilters>,
57
    req: HttpRequest,
58
) -> EdgeJsonResult<ClientFeaturesDelta> {
59
    resolve_delta(edge_token, delta_cache, token_cache, filter_query, req).await
×
60
}
61

62
#[get("/streaming")]
63
pub async fn stream_features(
1✔
64
    edge_token: EdgeToken,
65
    broadcaster: Data<Broadcaster>,
66
    token_cache: Data<DashMap<String, EdgeToken>>,
67
    edge_mode: Data<EdgeMode>,
68
    filter_query: Query<FeatureFilters>,
69
) -> EdgeResult<impl Responder> {
70
    match edge_mode.get_ref() {
2✔
71
        EdgeMode::Edge(EdgeArgs {
72
            streaming: true, ..
73
        }) => {
74
            let (validated_token, _filter_set, query) =
1✔
75
                get_feature_filter(&edge_token, &token_cache, filter_query.clone())?;
76

77
            broadcaster.connect(validated_token, query).await
2✔
78
        }
79
        _ => Err(EdgeError::Forbidden(
×
80
            "This endpoint is only enabled in streaming mode".into(),
×
81
        )),
82
    }
83
}
84

85
#[utoipa::path(
86
    context_path = "/api/client",
87
    params(FeatureFilters),
88
    responses(
89
        (status = 200, description = "Return feature toggles for this token", body = ClientFeatures),
90
        (status = 403, description = "Was not allowed to access features"),
91
        (status = 400, description = "Invalid parameters used")
92
    ),
93
    security(
94
        ("Authorization" = [])
95
    )
96
)]
97
#[post("/features")]
98
pub async fn post_features(
1✔
99
    edge_token: EdgeToken,
100
    features_cache: Data<FeatureCache>,
101
    token_cache: Data<DashMap<String, EdgeToken>>,
102
    filter_query: Query<FeatureFilters>,
103
    req: HttpRequest,
104
) -> EdgeJsonResult<ClientFeatures> {
105
    resolve_features(edge_token, features_cache, token_cache, filter_query, req).await
2✔
106
}
107

108
fn get_feature_filter(
2✔
109
    edge_token: &EdgeToken,
110
    token_cache: &Data<DashMap<String, EdgeToken>>,
111
    filter_query: Query<FeatureFilters>,
112
) -> EdgeResult<(
113
    EdgeToken,
114
    FeatureFilterSet,
115
    unleash_types::client_features::Query,
116
)> {
117
    let validated_token = token_cache
6✔
118
        .get(&edge_token.token)
119
        .map(|e| e.value().clone())
4✔
120
        .ok_or(EdgeError::AuthorizationDenied)?;
2✔
121

122
    let query_filters = filter_query.into_inner();
4✔
123
    let query = unleash_types::client_features::Query {
124
        tags: None,
125
        projects: Some(validated_token.projects.clone()),
4✔
126
        name_prefix: query_filters.name_prefix.clone(),
2✔
127
        environment: validated_token.environment.clone(),
2✔
128
        inline_segment_constraints: Some(false),
2✔
129
    };
130

131
    let filter_set = if let Some(name_prefix) = query_filters.name_prefix {
4✔
132
        FeatureFilterSet::from(Box::new(name_prefix_filter(name_prefix)))
2✔
133
    } else {
134
        FeatureFilterSet::default()
4✔
135
    }
136
    .with_filter(project_filter(&validated_token));
6✔
137

138
    Ok((validated_token, filter_set, query))
3✔
139
}
140

141
async fn resolve_features(
1✔
142
    edge_token: EdgeToken,
143
    features_cache: Data<FeatureCache>,
144
    token_cache: Data<DashMap<String, EdgeToken>>,
145
    filter_query: Query<FeatureFilters>,
146
    req: HttpRequest,
147
) -> EdgeJsonResult<ClientFeatures> {
148
    let (validated_token, filter_set, query) =
1✔
149
        get_feature_filter(&edge_token, &token_cache, filter_query.clone())?;
150

151
    let client_features = match req.app_data::<Data<FeatureRefresher>>() {
6✔
152
        Some(refresher) => {
2✔
153
            refresher
9✔
154
                .features_for_filter(validated_token.clone(), &filter_set)
2✔
155
                .await
6✔
156
        }
157
        None => features_cache
5✔
158
            .get(&cache_key(&validated_token))
1✔
159
            .map(|client_features| filter_client_features(&client_features, &filter_set))
5✔
160
            .ok_or(EdgeError::ClientCacheError),
2✔
161
    }?;
162

163
    Ok(Json(ClientFeatures {
1✔
164
        query: Some(query),
1✔
165
        ..client_features
166
    }))
167
}
168

169
async fn resolve_delta(
×
170
    edge_token: EdgeToken,
171
    delta_cache: Data<DashMap<Environment, DeltaCache>>,
172
    token_cache: Data<DashMap<String, EdgeToken>>,
173
    filter_query: Query<FeatureFilters>,
174
    req: HttpRequest,
175
) -> EdgeJsonResult<ClientFeaturesDelta> {
176
    let (validated_token, filter_set, ..) =
×
177
        get_feature_filter(&edge_token, &token_cache, filter_query.clone())?;
178
    let delta = match req.app_data::<Data<FeatureRefresher>>() {
×
179
        Some(refresher) => {
×
180
            refresher
×
181
                .delta_events_for_filter(validated_token.clone(), &filter_set)
×
182
                .await
×
183
        }
184
        None => delta_cache
×
185
            .get(&cache_key(&validated_token))
×
186
            .map(|cache| filter_delta_events(cache.value(), &filter_set))
×
187
            .ok_or(EdgeError::ClientCacheError),
×
188
    }?;
189

190
    Ok(Json(delta))
×
191
}
192
#[utoipa::path(
193
    context_path = "/api/client",
194
    params(("feature_name" = String, Path,)),
195
    responses(
196
        (status = 200, description = "Return feature toggles for this token", body = ClientFeature),
197
        (status = 403, description = "Was not allowed to access feature"),
198
        (status = 400, description = "Invalid parameters used"),
199
        (status = 404, description = "Feature did not exist or token used was not allowed to access it")
200
    ),
201
    security(
202
        ("Authorization" = [])
203
    )
204
)]
205
#[get("/features/{feature_name}")]
206
pub async fn get_feature(
1✔
207
    edge_token: EdgeToken,
208
    features_cache: Data<FeatureCache>,
209
    token_cache: Data<DashMap<String, EdgeToken>>,
210
    feature_name: web::Path<String>,
211
    req: HttpRequest,
212
) -> EdgeJsonResult<ClientFeature> {
213
    let validated_token = token_cache
5✔
214
        .get(&edge_token.token)
1✔
215
        .map(|e| e.value().clone())
2✔
216
        .ok_or(EdgeError::AuthorizationDenied)?;
2✔
217

218
    let filter_set = FeatureFilterSet::from(Box::new(name_match_filter(feature_name.clone())))
8✔
219
        .with_filter(project_filter(&validated_token));
4✔
220

221
    match req.app_data::<Data<FeatureRefresher>>() {
8✔
222
        Some(refresher) => {
×
223
            refresher
×
224
                .features_for_filter(validated_token.clone(), &filter_set)
×
225
                .await
×
226
        }
227
        None => features_cache
7✔
228
            .get(&cache_key(&validated_token))
1✔
229
            .map(|client_features| filter_client_features(&client_features, &filter_set))
3✔
230
            .ok_or(EdgeError::ClientCacheError),
2✔
231
    }
232
    .map(|client_features| client_features.features.into_iter().next())?
2✔
233
    .ok_or(EdgeError::FeatureNotFound(feature_name.into_inner()))
2✔
234
    .map(Json)
235
}
236

237
#[utoipa::path(
238
    context_path = "/api/client",
239
    responses(
240
        (status = 202, description = "Accepted client application registration"),
241
        (status = 403, description = "Was not allowed to register client application"),
242
    ),
243
    request_body = ClientApplication,
244
    security(
245
        ("Authorization" = [])
246
    )
247
)]
248
#[post("/register")]
249
pub async fn register(
2✔
250
    edge_token: EdgeToken,
251
    connect_via: Data<ConnectVia>,
252
    client_application: Json<ClientApplication>,
253
    metrics_cache: Data<MetricsCache>,
254
) -> EdgeResult<HttpResponse> {
255
    crate::metrics::client_metrics::register_client_application(
256
        edge_token,
2✔
257
        &connect_via,
4✔
258
        client_application.into_inner(),
2✔
259
        metrics_cache,
2✔
260
    );
261
    Ok(HttpResponse::Accepted()
6✔
262
        .append_header(("X-Edge-Version", types::EDGE_VERSION))
2✔
263
        .finish())
264
}
265

266
#[utoipa::path(
267
    context_path = "/api/client",
268
    responses(
269
        (status = 202, description = "Accepted client metrics"),
270
        (status = 403, description = "Was not allowed to post metrics"),
271
    ),
272
    request_body = ClientMetrics,
273
    security(
274
        ("Authorization" = [])
275
    )
276
)]
277
#[post("/metrics")]
278
pub async fn metrics(
1✔
279
    edge_token: EdgeToken,
280
    metrics: Json<ClientMetrics>,
281
    metrics_cache: Data<MetricsCache>,
282
) -> EdgeResult<HttpResponse> {
283
    crate::metrics::client_metrics::register_client_metrics(
284
        edge_token,
1✔
285
        metrics.into_inner(),
1✔
286
        metrics_cache,
1✔
287
    );
288
    Ok(HttpResponse::Accepted().finish())
3✔
289
}
290

291
#[utoipa::path(
292
context_path = "/api/client",
293
responses(
294
(status = 202, description = "Accepted bulk metrics"),
295
(status = 403, description = "Was not allowed to post bulk metrics")
296
),
297
request_body = BatchMetricsRequestBody,
298
security(
299
("Authorization" = [])
300
)
301
)]
302
#[post("/metrics/bulk")]
303
pub async fn post_bulk_metrics(
1✔
304
    edge_token: EdgeToken,
305
    bulk_metrics: Json<BatchMetricsRequestBody>,
306
    connect_via: Data<ConnectVia>,
307
    metrics_cache: Data<MetricsCache>,
308
) -> EdgeResult<HttpResponse> {
309
    crate::metrics::client_metrics::register_bulk_metrics(
310
        metrics_cache.get_ref(),
1✔
311
        connect_via.get_ref(),
1✔
312
        &edge_token,
313
        bulk_metrics.into_inner(),
1✔
314
    );
315
    Ok(HttpResponse::Accepted().finish())
2✔
316
}
317

318
#[utoipa::path(context_path = "/api/client", responses((status = 202, description = "Accepted Instance data"), (status = 403, description = "Was not allowed to post instance data")), request_body = EdgeInstanceData, security(
319
("Authorization" = [])
320
)
321
)]
322
#[post("/metrics/edge")]
323
#[instrument(skip(_edge_token, instance_data, connected_instances))]
324
pub async fn post_edge_instance_data(
325
    _edge_token: EdgeToken,
326
    instance_data: Json<EdgeInstanceData>,
327
    instance_data_sending: Data<InstanceDataSending>,
328
    connected_instances: Data<RwLock<Vec<EdgeInstanceData>>>,
329
) -> EdgeResult<HttpResponse> {
NEW
330
    if let InstanceDataSending::SendInstanceData(_) = instance_data_sending.as_ref() {
×
NEW
331
        connected_instances
×
332
            .write()
NEW
333
            .await
×
NEW
334
            .push(instance_data.into_inner());
×
335
    }
NEW
336
    Ok(HttpResponse::Accepted().finish())
×
337
}
338

339
pub fn configure_client_api(cfg: &mut web::ServiceConfig) {
3✔
340
    let client_scope = web::scope("/client")
9✔
341
        .wrap(crate::middleware::as_async_middleware::as_async_middleware(
3✔
342
            crate::middleware::validate_token::validate_token,
343
        ))
344
        .service(get_features)
345
        .service(get_delta)
346
        .service(get_feature)
347
        .service(register)
348
        .service(metrics)
349
        .service(post_bulk_metrics)
350
        .service(stream_features)
351
        .service(post_edge_instance_data);
352

353
    cfg.service(client_scope);
3✔
354
}
355

356
pub fn configure_experimental_post_features(
×
357
    cfg: &mut web::ServiceConfig,
358
    post_features_enabled: bool,
359
) {
360
    if post_features_enabled {
×
361
        cfg.service(post_features);
×
362
    }
363
}
364

365
#[cfg(test)]
366
mod tests {
367

368
    use crate::metrics::client_metrics::{ApplicationKey, MetricsBatch, MetricsKey};
369
    use crate::types::{TokenType, TokenValidationStatus};
370
    use std::collections::HashMap;
371
    use std::path::PathBuf;
372
    use std::str::FromStr;
373
    use std::sync::Arc;
374

375
    use super::*;
376

377
    use crate::auth::token_validator::TokenValidator;
378
    use crate::cli::{OfflineArgs, TokenHeader};
379
    use crate::http::unleash_client::{ClientMetaInformation, UnleashClient};
380
    use crate::middleware;
381
    use crate::tests::{features_from_disk, upstream_server};
382
    use actix_http::{Request, StatusCode};
383
    use actix_web::{
384
        http::header::ContentType,
385
        test,
386
        web::{self, Data},
387
        App, ResponseError,
388
    };
389
    use chrono::{DateTime, Duration, TimeZone, Utc};
390
    use maplit::hashmap;
391
    use ulid::Ulid;
392
    use unleash_types::client_features::{
393
        ClientFeature, Constraint, Operator, Strategy, StrategyVariant,
394
    };
395
    use unleash_types::client_metrics::{
396
        ClientMetricsEnv, ConnectViaBuilder, MetricBucket, MetricsMetadata, ToggleStats,
397
    };
398
    use unleash_yggdrasil::EngineState;
399

400
    async fn make_metrics_post_request() -> Request {
401
        test::TestRequest::post()
402
            .uri("/api/client/metrics")
403
            .insert_header(ContentType::json())
404
            .insert_header((
405
                "Authorization",
406
                "*:development.03fa5f506428fe80ed5640c351c7232e38940814d2923b08f5c05fa7",
407
            ))
408
            .set_json(Json(ClientMetrics {
409
                app_name: "some-app".into(),
410
                instance_id: Some("some-instance".into()),
411
                bucket: MetricBucket {
412
                    start: Utc.with_ymd_and_hms(1867, 11, 7, 12, 0, 0).unwrap(),
413
                    stop: Utc.with_ymd_and_hms(1934, 11, 7, 12, 0, 0).unwrap(),
414
                    toggles: hashmap! {
415
                        "some-feature".to_string() => ToggleStats {
416
                            yes: 1,
417
                            no: 0,
418
                            variants: hashmap! {}
419
                        }
420
                    },
421
                },
422
                environment: Some("development".into()),
423
                metadata: MetricsMetadata {
424
                    platform_name: Some("test".into()),
425
                    platform_version: Some("1.0".into()),
426
                    sdk_version: Some("1.0".into()),
427
                    yggdrasil_version: None,
428
                },
429
            }))
430
            .to_request()
431
    }
432

433
    async fn make_bulk_metrics_post_request(authorization: Option<String>) -> Request {
434
        let mut req = test::TestRequest::post()
435
            .uri("/api/client/metrics/bulk")
436
            .insert_header(ContentType::json());
437
        req = match authorization {
438
            Some(auth) => req.insert_header(("Authorization", auth)),
439
            None => req,
440
        };
441
        req.set_json(Json(BatchMetricsRequestBody {
442
            applications: vec![ClientApplication {
443
                app_name: "test_app".to_string(),
444
                connect_via: None,
445
                environment: None,
446
                instance_id: None,
447
                interval: 10,
448
                started: Default::default(),
449
                strategies: vec![],
450
                metadata: MetricsMetadata {
451
                    platform_name: None,
452
                    platform_version: None,
453
                    sdk_version: None,
454
                    yggdrasil_version: None,
455
                },
456
            }],
457
            metrics: vec![ClientMetricsEnv {
458
                feature_name: "".to_string(),
459
                app_name: "".to_string(),
460
                environment: "".to_string(),
461
                timestamp: Default::default(),
462
                yes: 0,
463
                no: 0,
464
                variants: Default::default(),
465
                metadata: MetricsMetadata {
466
                    platform_name: None,
467
                    platform_version: None,
468
                    sdk_version: None,
469
                    yggdrasil_version: None,
470
                },
471
            }],
472
        }))
473
        .to_request()
474
    }
475

476
    async fn make_register_post_request(application: ClientApplication) -> Request {
477
        test::TestRequest::post()
478
            .uri("/api/client/register")
479
            .insert_header(ContentType::json())
480
            .insert_header((
481
                "Authorization",
482
                "*:development.03fa5f506428fe80ed5640c351c7232e38940814d2923b08f5c05fa7",
483
            ))
484
            .set_json(Json(application))
485
            .to_request()
486
    }
487

488
    async fn make_features_request_with_token(token: EdgeToken) -> Request {
489
        test::TestRequest::get()
490
            .uri("/api/client/features")
491
            .insert_header(("Authorization", token.token))
492
            .to_request()
493
    }
494

495
    #[actix_web::test]
496
    async fn metrics_endpoint_correctly_aggregates_data() {
497
        let metrics_cache = Arc::new(MetricsCache::default());
498

499
        let app = test::init_service(
500
            App::new()
501
                .app_data(Data::new(ConnectVia {
502
                    app_name: "test".into(),
503
                    instance_id: Ulid::new().to_string(),
504
                }))
505
                .app_data(Data::from(metrics_cache.clone()))
506
                .service(web::scope("/api/client").service(metrics)),
507
        )
508
        .await;
509

510
        let req = make_metrics_post_request().await;
511
        let _result = test::call_and_read_body(&app, req).await;
512

513
        let cache = metrics_cache.clone();
514

515
        let found_metric = cache
516
            .metrics
517
            .get(&MetricsKey {
518
                app_name: "some-app".into(),
519
                feature_name: "some-feature".into(),
520
                timestamp: DateTime::parse_from_rfc3339("1867-11-07T12:00:00Z")
521
                    .unwrap()
522
                    .with_timezone(&Utc),
523
                environment: "development".into(),
524
            })
525
            .unwrap();
526

527
        let expected = ClientMetricsEnv {
528
            app_name: "some-app".into(),
529
            feature_name: "some-feature".into(),
530
            environment: "development".into(),
531
            timestamp: DateTime::parse_from_rfc3339("1867-11-07T12:00:00Z")
532
                .unwrap()
533
                .with_timezone(&Utc),
534
            yes: 1,
535
            no: 0,
536
            variants: HashMap::new(),
537
            metadata: MetricsMetadata {
538
                platform_name: None,
539
                platform_version: None,
540
                sdk_version: None,
541
                yggdrasil_version: None,
542
            },
543
        };
544

545
        assert_eq!(found_metric.yes, expected.yes);
546
        assert_eq!(found_metric.yes, 1);
547
        assert_eq!(found_metric.no, 0);
548
        assert_eq!(found_metric.no, expected.no);
549
    }
550

551
    fn cached_client_features() -> ClientFeatures {
552
        ClientFeatures {
553
            version: 2,
554
            features: vec![
555
                ClientFeature {
556
                    name: "feature_one".into(),
557
                    feature_type: Some("release".into()),
558
                    description: Some("test feature".into()),
559
                    created_at: Some(Utc::now()),
560
                    dependencies: None,
561
                    last_seen_at: None,
562
                    enabled: true,
563
                    stale: Some(false),
564
                    impression_data: Some(false),
565
                    project: Some("default".into()),
566
                    strategies: Some(vec![
567
                        Strategy {
568
                            variants: Some(vec![StrategyVariant {
569
                                name: "test".into(),
570
                                payload: None,
571
                                weight: 7,
572
                                stickiness: Some("sticky-on-something".into()),
573
                            }]),
574
                            name: "standard".into(),
575
                            sort_order: Some(500),
576
                            segments: None,
577
                            constraints: None,
578
                            parameters: None,
579
                        },
580
                        Strategy {
581
                            variants: None,
582
                            name: "gradualRollout".into(),
583
                            sort_order: Some(100),
584
                            segments: None,
585
                            constraints: None,
586
                            parameters: None,
587
                        },
588
                    ]),
589
                    variants: None,
590
                },
591
                ClientFeature {
592
                    name: "feature_two_no_strats".into(),
593
                    feature_type: None,
594
                    dependencies: None,
595
                    description: None,
596
                    created_at: Some(Utc.with_ymd_and_hms(2022, 12, 5, 12, 31, 0).unwrap()),
597
                    last_seen_at: None,
598
                    enabled: true,
599
                    stale: None,
600
                    impression_data: None,
601
                    project: Some("default".into()),
602
                    strategies: None,
603
                    variants: None,
604
                },
605
                ClientFeature {
606
                    name: "feature_three".into(),
607
                    feature_type: Some("release".into()),
608
                    description: None,
609
                    dependencies: None,
610
                    created_at: None,
611
                    last_seen_at: None,
612
                    enabled: true,
613
                    stale: None,
614
                    impression_data: None,
615
                    project: Some("default".into()),
616
                    strategies: Some(vec![
617
                        Strategy {
618
                            name: "gradualRollout".to_string(),
619
                            sort_order: None,
620
                            segments: None,
621
                            variants: None,
622
                            constraints: Some(vec![Constraint {
623
                                context_name: "version".to_string(),
624
                                operator: Operator::SemverGt,
625
                                case_insensitive: false,
626
                                inverted: false,
627
                                values: None,
628
                                value: Some("1.5.0".into()),
629
                            }]),
630
                            parameters: None,
631
                        },
632
                        Strategy {
633
                            name: "".to_string(),
634
                            sort_order: None,
635
                            segments: None,
636
                            constraints: None,
637
                            parameters: None,
638
                            variants: None,
639
                        },
640
                    ]),
641
                    variants: None,
642
                },
643
            ],
644
            segments: None,
645
            query: None,
646
            meta: None,
647
        }
648
    }
649

650
    #[tokio::test]
651
    async fn response_includes_variant_stickiness_for_strategy_variants() {
652
        let features_cache = Arc::new(FeatureCache::default());
653
        let token_cache: Arc<DashMap<String, EdgeToken>> = Arc::new(DashMap::default());
654
        let app = test::init_service(
655
            App::new()
656
                .app_data(Data::from(features_cache.clone()))
657
                .app_data(Data::from(token_cache.clone()))
658
                .service(web::scope("/api/client").service(get_features)),
659
        )
660
        .await;
661

662
        features_cache.insert("production".into(), cached_client_features());
663
        let mut production_token = EdgeToken::try_from(
664
            "*:production.03fa5f506428fe80ed5640c351c7232e38940814d2923b08f5c05fa7".to_string(),
665
        )
666
        .unwrap();
667
        production_token.token_type = Some(TokenType::Client);
668
        production_token.status = TokenValidationStatus::Validated;
669
        token_cache.insert(production_token.token.clone(), production_token.clone());
670
        let req = make_features_request_with_token(production_token.clone()).await;
671
        let res: ClientFeatures = test::call_and_read_body_json(&app, req).await;
672

673
        assert_eq!(res.features.len(), cached_client_features().features.len());
674
        let strategy_variant_stickiness = res
675
            .features
676
            .iter()
677
            .find(|f| f.name == "feature_one")
678
            .unwrap()
679
            .strategies
680
            .clone()
681
            .unwrap()
682
            .iter()
683
            .find(|s| s.name == "standard")
684
            .unwrap()
685
            .variants
686
            .clone()
687
            .unwrap()
688
            .iter()
689
            .find(|v| v.name == "test")
690
            .unwrap()
691
            .stickiness
692
            .clone();
693
        assert!(strategy_variant_stickiness.is_some());
694
    }
695

696
    #[tokio::test]
697
    async fn register_endpoint_correctly_aggregates_applications() {
698
        let metrics_cache = Arc::new(MetricsCache::default());
699
        let our_app = ConnectVia {
700
            app_name: "test".into(),
701
            instance_id: Ulid::new().to_string(),
702
        };
703
        let app = test::init_service(
704
            App::new()
705
                .app_data(Data::new(our_app.clone()))
706
                .app_data(Data::from(metrics_cache.clone()))
707
                .service(web::scope("/api/client").service(register)),
708
        )
709
        .await;
710
        let mut client_app = ClientApplication::new("test_application", 15);
711
        client_app.instance_id = Some("test_instance".into());
712
        let req = make_register_post_request(client_app.clone()).await;
713
        let res = test::call_service(&app, req).await;
714
        assert_eq!(res.status(), actix_http::StatusCode::ACCEPTED);
715
        assert_eq!(metrics_cache.applications.len(), 1);
716
        let application_key = ApplicationKey {
717
            app_name: client_app.app_name.clone(),
718
            instance_id: client_app.instance_id.unwrap(),
719
        };
720
        let saved_app = metrics_cache
721
            .applications
722
            .get(&application_key)
723
            .unwrap()
724
            .value()
725
            .clone();
726
        assert_eq!(saved_app.app_name, client_app.app_name);
727
        assert_eq!(saved_app.connect_via, Some(vec![our_app]));
728
    }
729

730
    #[tokio::test]
731
    async fn bulk_metrics_endpoint_correctly_accepts_data() {
732
        let metrics_cache = MetricsCache::default();
733
        let connect_via = ConnectViaBuilder::default()
734
            .app_name("unleash-edge".into())
735
            .instance_id("test".into())
736
            .build()
737
            .unwrap();
738
        let app = test::init_service(
739
            App::new()
740
                .app_data(Data::new(connect_via))
741
                .app_data(web::Data::new(metrics_cache))
742
                .service(web::scope("/api/client").service(post_bulk_metrics)),
743
        )
744
        .await;
745
        let token = EdgeToken::from_str("*:development.somestring").unwrap();
746
        let req = make_bulk_metrics_post_request(Some(token.token.clone())).await;
747
        let call = test::call_service(&app, req).await;
748
        assert_eq!(call.status(), StatusCode::ACCEPTED);
749
    }
750
    #[tokio::test]
751
    async fn bulk_metrics_endpoint_correctly_refuses_metrics_without_auth_header() {
752
        let mut token = EdgeToken::from_str("*:development.somestring").unwrap();
753
        token.status = TokenValidationStatus::Validated;
754
        token.token_type = Some(TokenType::Client);
755
        let upstream_token_cache = Arc::new(DashMap::default());
756
        let upstream_features_cache = Arc::new(FeatureCache::default());
757
        let upstream_delta_cache = Arc::new(DashMap::default());
758
        let upstream_engine_cache = Arc::new(DashMap::default());
759
        upstream_token_cache.insert(token.token.clone(), token.clone());
760
        let srv = upstream_server(
761
            upstream_token_cache,
762
            upstream_features_cache,
763
            upstream_delta_cache,
764
            upstream_engine_cache,
765
        )
766
        .await;
767
        let req = reqwest::Client::new();
768
        let status = req
769
            .post(srv.url("/api/client/metrics/bulk").as_str())
770
            .body(
771
                serde_json::to_string(&crate::types::BatchMetricsRequestBody {
772
                    applications: vec![],
773
                    metrics: vec![],
774
                })
775
                .unwrap(),
776
            )
777
            .send()
778
            .await;
779
        assert!(status.is_ok());
780
        assert_eq!(
781
            status.unwrap().status().as_u16(),
782
            StatusCode::FORBIDDEN.as_u16()
783
        );
784
        let client = UnleashClient::new(srv.url("/").as_str(), None).unwrap();
785
        let successful = client
786
            .send_bulk_metrics_to_client_endpoint(MetricsBatch::default(), &token.token)
787
            .await;
788
        assert!(successful.is_ok());
789
    }
790

791
    #[tokio::test]
792
    async fn bulk_metrics_endpoint_correctly_refuses_metrics_with_frontend_token() {
793
        let mut frontend_token = EdgeToken::from_str("*:development.frontend").unwrap();
794
        frontend_token.status = TokenValidationStatus::Validated;
795
        frontend_token.token_type = Some(TokenType::Frontend);
796
        let upstream_token_cache = Arc::new(DashMap::default());
797
        let upstream_features_cache = Arc::new(FeatureCache::default());
798
        let upstream_delta_cache = Arc::new(DashMap::default());
799
        let upstream_engine_cache = Arc::new(DashMap::default());
800
        upstream_token_cache.insert(frontend_token.token.clone(), frontend_token.clone());
801
        let srv = upstream_server(
802
            upstream_token_cache,
803
            upstream_features_cache,
804
            upstream_delta_cache,
805
            upstream_engine_cache,
806
        )
807
        .await;
808
        let client = UnleashClient::new(srv.url("/").as_str(), None).unwrap();
809
        let status = client
810
            .send_bulk_metrics_to_client_endpoint(MetricsBatch::default(), &frontend_token.token)
811
            .await;
812
        assert_eq!(status.expect_err("").status_code(), StatusCode::FORBIDDEN);
813
    }
814
    #[tokio::test]
815
    async fn register_endpoint_returns_version_header() {
816
        let metrics_cache = Arc::new(MetricsCache::default());
817
        let our_app = ConnectVia {
818
            app_name: "test".into(),
819
            instance_id: Ulid::new().to_string(),
820
        };
821
        let app = test::init_service(
822
            App::new()
823
                .app_data(Data::new(our_app.clone()))
824
                .app_data(Data::from(metrics_cache.clone()))
825
                .service(web::scope("/api/client").service(register)),
826
        )
827
        .await;
828
        let mut client_app = ClientApplication::new("test_application", 15);
829
        client_app.instance_id = Some("test_instance".into());
830
        let req = make_register_post_request(client_app.clone()).await;
831
        let res = test::call_service(&app, req).await;
832
        assert_eq!(res.status(), StatusCode::ACCEPTED);
833
        assert_eq!(
834
            res.headers().get("X-Edge-Version").unwrap(),
835
            types::EDGE_VERSION
836
        );
837
    }
838

839
    #[tokio::test]
840
    async fn client_features_endpoint_correctly_returns_cached_features() {
841
        let features_cache = Arc::new(FeatureCache::default());
842
        let token_cache: Arc<DashMap<String, EdgeToken>> = Arc::new(DashMap::default());
843
        let app = test::init_service(
844
            App::new()
845
                .app_data(Data::from(features_cache.clone()))
846
                .app_data(Data::from(token_cache.clone()))
847
                .service(web::scope("/api/client").service(get_features)),
848
        )
849
        .await;
850
        let client_features = cached_client_features();
851
        let example_features = features_from_disk("../examples/features.json");
852
        features_cache.insert("development".into(), client_features.clone());
853
        features_cache.insert("production".into(), example_features.clone());
854
        let mut token = EdgeToken::try_from(
855
            "*:development.03fa5f506428fe80ed5640c351c7232e38940814d2923b08f5c05fa7".to_string(),
856
        )
857
        .unwrap();
858
        token.token_type = Some(TokenType::Client);
859
        token.status = TokenValidationStatus::Validated;
860
        token_cache.insert(token.token.clone(), token.clone());
861
        let req = make_features_request_with_token(token.clone()).await;
862
        let res: ClientFeatures = test::call_and_read_body_json(&app, req).await;
863
        assert_eq!(res.features, client_features.features);
864
        let mut production_token = EdgeToken::try_from(
865
            "*:production.03fa5f506428fe80ed5640c351c7232e38940814d2923b08f5c05fa7".to_string(),
866
        )
867
        .unwrap();
868
        production_token.token_type = Some(TokenType::Client);
869
        production_token.status = TokenValidationStatus::Validated;
870
        token_cache.insert(production_token.token.clone(), production_token.clone());
871
        let req = make_features_request_with_token(production_token.clone()).await;
872
        let res: ClientFeatures = test::call_and_read_body_json(&app, req).await;
873
        assert_eq!(res.features.len(), example_features.features.len());
874
    }
875

876
    #[tokio::test]
877
    async fn post_request_to_client_features_does_the_same_as_get_when_mounted() {
878
        let features_cache = Arc::new(FeatureCache::default());
879
        let token_cache: Arc<DashMap<String, EdgeToken>> = Arc::new(DashMap::default());
880
        let app = test::init_service(
881
            App::new()
882
                .app_data(Data::from(features_cache.clone()))
883
                .app_data(Data::from(token_cache.clone()))
884
                .service(
885
                    web::scope("/api/client")
886
                        .service(get_features)
887
                        .service(post_features),
888
                ),
889
        )
890
        .await;
891
        let client_features = cached_client_features();
892
        let example_features = features_from_disk("../examples/features.json");
893
        features_cache.insert("development".into(), client_features.clone());
894
        features_cache.insert("production".into(), example_features.clone());
895
        let mut token = EdgeToken::try_from(
896
            "*:development.03fa5f506428fe80ed5640c351c7232e38940814d2923b08f5c05fa7".to_string(),
897
        )
898
        .unwrap();
899
        token.token_type = Some(TokenType::Client);
900
        token.status = TokenValidationStatus::Validated;
901
        token_cache.insert(token.token.clone(), token.clone());
902
        let req = make_features_request_with_token(token.clone()).await;
903
        let res: ClientFeatures = test::call_and_read_body_json(&app, req).await;
904
        assert_eq!(res.features, client_features.features);
905
        let mut production_token = EdgeToken::try_from(
906
            "*:production.03fa5f506428fe80ed5640c351c7232e38940814d2923b08f5c05fa7".to_string(),
907
        )
908
        .unwrap();
909
        production_token.token_type = Some(TokenType::Client);
910
        production_token.status = TokenValidationStatus::Validated;
911
        token_cache.insert(production_token.token.clone(), production_token.clone());
912

913
        let post_req = test::TestRequest::post()
914
            .uri("/api/client/features")
915
            .insert_header(("Authorization", production_token.clone().token))
916
            .insert_header(ContentType::json())
917
            .to_request();
918

919
        let get_req = make_features_request_with_token(production_token.clone()).await;
920
        let get_res: ClientFeatures = test::call_and_read_body_json(&app, get_req).await;
921
        let post_res: ClientFeatures = test::call_and_read_body_json(&app, post_req).await;
922

923
        assert_eq!(get_res.features, post_res.features)
924
    }
925

926
    #[tokio::test]
927
    async fn client_features_endpoint_filters_on_project_access_in_token() {
928
        let features_cache = Arc::new(FeatureCache::default());
929
        let token_cache: Arc<DashMap<String, EdgeToken>> = Arc::new(DashMap::default());
930
        let app = test::init_service(
931
            App::new()
932
                .app_data(Data::from(features_cache.clone()))
933
                .app_data(Data::from(token_cache.clone()))
934
                .service(web::scope("/api/client").service(get_features)),
935
        )
936
        .await;
937
        let mut edge_token = EdgeToken::try_from(
938
            "demo-app:production.03fa5f506428fe80ed5640c351c7232e38940814d2923b08f5c05fa7"
939
                .to_string(),
940
        )
941
        .unwrap();
942
        edge_token.token_type = Some(TokenType::Client);
943
        token_cache.insert(edge_token.token.clone(), edge_token.clone());
944
        let example_features = features_from_disk("../examples/features.json");
945
        features_cache.insert("production".into(), example_features.clone());
946
        let req = make_features_request_with_token(edge_token.clone()).await;
947
        let res: ClientFeatures = test::call_and_read_body_json(&app, req).await;
948
        assert_eq!(res.features.len(), 5);
949
        assert!(res
950
            .features
951
            .iter()
952
            .all(|t| t.project == Some("demo-app".into())));
953
    }
954

955
    #[tokio::test]
956
    async fn client_features_endpoint_filters_when_multiple_projects_in_token() {
957
        let features_cache = Arc::new(FeatureCache::default());
958
        let token_cache: Arc<DashMap<String, EdgeToken>> = Arc::new(DashMap::default());
959
        let app = test::init_service(
960
            App::new()
961
                .app_data(Data::from(features_cache.clone()))
962
                .app_data(Data::from(token_cache.clone()))
963
                .service(web::scope("/api/client").service(get_features)),
964
        )
965
        .await;
966
        let mut token =
967
            EdgeToken::try_from("[]:production.puff_the_magic_dragon".to_string()).unwrap();
968
        token.projects = vec!["dx".into(), "eg".into(), "unleash-cloud".into()];
969
        token.status = TokenValidationStatus::Validated;
970
        token.token_type = Some(TokenType::Client);
971
        token_cache.insert(token.token.clone(), token.clone());
972
        let example_features = features_from_disk("../examples/hostedexample.json");
973
        features_cache.insert("production".into(), example_features.clone());
974
        let req = make_features_request_with_token(token.clone()).await;
975
        let res: ClientFeatures = test::call_and_read_body_json(&app, req).await;
976
        assert_eq!(res.features.len(), 24);
977
        assert!(res
978
            .features
979
            .iter()
980
            .all(|f| token.projects.contains(&f.project.clone().unwrap())));
981
    }
982

983
    #[tokio::test]
984
    async fn client_features_endpoint_filters_correctly_when_token_has_access_to_multiple_projects()
985
    {
986
        let features_cache = Arc::new(FeatureCache::default());
987
        let token_cache: Arc<DashMap<String, EdgeToken>> = Arc::new(DashMap::default());
988
        let app = test::init_service(
989
            App::new()
990
                .app_data(Data::from(features_cache.clone()))
991
                .app_data(Data::from(token_cache.clone()))
992
                .service(web::scope("/api/client").service(get_features)),
993
        )
994
        .await;
995

996
        let mut token_a =
997
            EdgeToken::try_from("[]:production.puff_the_magic_dragon".to_string()).unwrap();
998
        token_a.projects = vec!["dx".into(), "eg".into()];
999
        token_a.status = TokenValidationStatus::Validated;
1000
        token_a.token_type = Some(TokenType::Client);
1001
        token_cache.insert(token_a.token.clone(), token_a.clone());
1002

1003
        let mut token_b =
1004
            EdgeToken::try_from("[]:production.biff_the_magic_flagon".to_string()).unwrap();
1005
        token_b.projects = vec!["unleash-cloud".into()];
1006
        token_b.status = TokenValidationStatus::Validated;
1007
        token_b.token_type = Some(TokenType::Client);
1008
        token_cache.insert(token_b.token.clone(), token_b.clone());
1009

1010
        let example_features = features_from_disk("../examples/hostedexample.json");
1011
        features_cache.insert("production".into(), example_features.clone());
1012

1013
        let req_1 = make_features_request_with_token(token_a.clone()).await;
1014
        let res_1: ClientFeatures = test::call_and_read_body_json(&app, req_1).await;
1015
        assert!(res_1
1016
            .features
1017
            .iter()
1018
            .all(|f| token_a.projects.contains(&f.project.clone().unwrap())));
1019

1020
        let req_2 = make_features_request_with_token(token_b.clone()).await;
1021
        let res_2: ClientFeatures = test::call_and_read_body_json(&app, req_2).await;
1022
        assert!(res_2
1023
            .features
1024
            .iter()
1025
            .all(|f| token_b.projects.contains(&f.project.clone().unwrap())));
1026

1027
        let req_3 = make_features_request_with_token(token_a.clone()).await;
1028
        let res_3: ClientFeatures = test::call_and_read_body_json(&app, req_3).await;
1029
        assert!(res_3
1030
            .features
1031
            .iter()
1032
            .all(|f| token_a.projects.contains(&f.project.clone().unwrap())));
1033
    }
1034

1035
    #[tokio::test]
1036
    async fn when_running_in_offline_mode_with_proxy_key_should_not_filter_features() {
1037
        let features_cache = Arc::new(FeatureCache::default());
1038
        let token_cache: Arc<DashMap<String, EdgeToken>> = Arc::new(DashMap::default());
1039
        let app = test::init_service(
1040
            App::new()
1041
                .app_data(Data::from(features_cache.clone()))
1042
                .app_data(Data::from(token_cache.clone()))
1043
                .app_data(Data::new(crate::cli::EdgeMode::Offline(OfflineArgs {
1044
                    bootstrap_file: Some(PathBuf::from("../examples/features.json")),
1045
                    tokens: vec!["secret_123".into()],
1046
                    client_tokens: vec![],
1047
                    frontend_tokens: vec![],
1048
                    reload_interval: 0,
1049
                })))
1050
                .service(web::scope("/api/client").service(get_features)),
1051
        )
1052
        .await;
1053
        let token = EdgeToken::offline_token("secret-123");
1054
        token_cache.insert(token.token.clone(), token.clone());
1055
        let example_features = features_from_disk("../examples/features.json");
1056
        features_cache.insert(token.token.clone(), example_features.clone());
1057
        let req = make_features_request_with_token(token.clone()).await;
1058
        let res: ClientFeatures = test::call_and_read_body_json(&app, req).await;
1059
        assert_eq!(res.features.len(), example_features.features.len());
1060
    }
1061

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

1129
    #[tokio::test]
1130
    async fn calling_client_features_endpoint_with_new_token_does_not_hydrate_when_strict() {
1131
        let upstream_features_cache = Arc::new(FeatureCache::default());
1132
        let upstream_token_cache: Arc<DashMap<String, EdgeToken>> = Arc::new(DashMap::default());
1133
        let upstream_delta_cache: Arc<DashMap<String, DeltaCache>> = Arc::new(DashMap::default());
1134
        let upstream_engine_cache: Arc<DashMap<String, EngineState>> = Arc::new(DashMap::default());
1135
        let server = upstream_server(
1136
            upstream_token_cache.clone(),
1137
            upstream_features_cache.clone(),
1138
            upstream_delta_cache.clone(),
1139
            upstream_engine_cache.clone(),
1140
        )
1141
        .await;
1142
        let upstream_features = features_from_disk("../examples/hostedexample.json");
1143
        let mut upstream_known_token = EdgeToken::from_str("dx:development.secret123").unwrap();
1144
        upstream_known_token.status = TokenValidationStatus::Validated;
1145
        upstream_known_token.token_type = Some(TokenType::Client);
1146
        upstream_token_cache.insert(
1147
            upstream_known_token.token.clone(),
1148
            upstream_known_token.clone(),
1149
        );
1150
        upstream_features_cache.insert(cache_key(&upstream_known_token), upstream_features.clone());
1151
        let unleash_client = Arc::new(UnleashClient::new(server.url("/").as_str(), None).unwrap());
1152
        let features_cache: Arc<FeatureCache> = Arc::new(FeatureCache::default());
1153
        let token_cache: Arc<DashMap<String, EdgeToken>> = Arc::new(DashMap::default());
1154
        let engine_cache: Arc<DashMap<String, EngineState>> = Arc::new(DashMap::default());
1155
        let feature_refresher = Arc::new(FeatureRefresher {
1156
            unleash_client: unleash_client.clone(),
1157
            features_cache: features_cache.clone(),
1158
            engine_cache: engine_cache.clone(),
1159
            refresh_interval: Duration::seconds(6000),
1160
            ..Default::default()
1161
        });
1162
        let token_validator = Arc::new(TokenValidator {
1163
            unleash_client: unleash_client.clone(),
1164
            token_cache: token_cache.clone(),
1165
            persistence: None,
1166
        });
1167
        let local_app = test::init_service(
1168
            App::new()
1169
                .app_data(Data::from(token_validator.clone()))
1170
                .app_data(Data::from(features_cache.clone()))
1171
                .app_data(Data::from(engine_cache.clone()))
1172
                .app_data(Data::from(token_cache.clone()))
1173
                .app_data(Data::from(feature_refresher.clone()))
1174
                .wrap(middleware::as_async_middleware::as_async_middleware(
1175
                    middleware::validate_token::validate_token,
1176
                ))
1177
                .service(web::scope("/api").configure(configure_client_api)),
1178
        )
1179
        .await;
1180
        let req = test::TestRequest::get()
1181
            .uri("/api/client/features")
1182
            .insert_header(ContentType::json())
1183
            .insert_header(("Authorization", upstream_known_token.token.clone()))
1184
            .to_request();
1185
        let res = test::call_service(&local_app, req).await;
1186
        assert_eq!(res.status(), StatusCode::FORBIDDEN);
1187
    }
1188

1189
    #[tokio::test]
1190
    pub async fn gets_feature_by_name() {
1191
        let features_cache = Arc::new(FeatureCache::default());
1192
        let token_cache: Arc<DashMap<String, EdgeToken>> = Arc::new(DashMap::default());
1193
        let engine_cache: Arc<DashMap<String, EngineState>> = Arc::new(DashMap::default());
1194
        let features = features_from_disk("../examples/hostedexample.json");
1195
        let mut dx_token = EdgeToken::from_str("dx:development.secret123").unwrap();
1196
        dx_token.status = TokenValidationStatus::Validated;
1197
        dx_token.token_type = Some(TokenType::Client);
1198
        token_cache.insert(dx_token.token.clone(), dx_token.clone());
1199
        features_cache.insert(cache_key(&dx_token), features.clone());
1200
        let local_app = test::init_service(
1201
            App::new()
1202
                .app_data(Data::from(features_cache.clone()))
1203
                .app_data(Data::from(engine_cache.clone()))
1204
                .app_data(Data::from(token_cache.clone()))
1205
                .wrap(middleware::as_async_middleware::as_async_middleware(
1206
                    middleware::validate_token::validate_token,
1207
                ))
1208
                .service(web::scope("/api").configure(configure_client_api)),
1209
        )
1210
        .await;
1211
        let desired_toggle = "projectStatusApi";
1212
        let request = test::TestRequest::get()
1213
            .uri(format!("/api/client/features/{desired_toggle}").as_str())
1214
            .insert_header(ContentType::json())
1215
            .insert_header(("Authorization", dx_token.token.clone()))
1216
            .to_request();
1217
        let result: ClientFeature = test::call_and_read_body_json(&local_app, request).await;
1218
        assert_eq!(result.name, desired_toggle);
1219
    }
1220

1221
    #[tokio::test]
1222
    pub async fn token_with_no_access_to_named_feature_yields_404() {
1223
        let features_cache = Arc::new(FeatureCache::default());
1224
        let token_cache: Arc<DashMap<String, EdgeToken>> = Arc::new(DashMap::default());
1225
        let engine_cache: Arc<DashMap<String, EngineState>> = Arc::new(DashMap::default());
1226
        let features = features_from_disk("../examples/hostedexample.json");
1227
        let mut dx_token = EdgeToken::from_str("dx:development.secret123").unwrap();
1228
        dx_token.status = TokenValidationStatus::Validated;
1229
        dx_token.token_type = Some(TokenType::Client);
1230
        token_cache.insert(dx_token.token.clone(), dx_token.clone());
1231
        features_cache.insert(cache_key(&dx_token), features.clone());
1232
        let local_app = test::init_service(
1233
            App::new()
1234
                .app_data(Data::from(features_cache.clone()))
1235
                .app_data(Data::from(engine_cache.clone()))
1236
                .app_data(Data::from(token_cache.clone()))
1237
                .wrap(middleware::as_async_middleware::as_async_middleware(
1238
                    middleware::validate_token::validate_token,
1239
                ))
1240
                .service(web::scope("/api").configure(configure_client_api)),
1241
        )
1242
        .await;
1243
        let desired_toggle = "serviceAccounts";
1244
        let request = test::TestRequest::get()
1245
            .uri(format!("/api/client/features/{desired_toggle}").as_str())
1246
            .insert_header(ContentType::json())
1247
            .insert_header(("Authorization", dx_token.token.clone()))
1248
            .to_request();
1249
        let result = test::call_service(&local_app, request).await;
1250
        assert_eq!(result.status(), StatusCode::NOT_FOUND);
1251
    }
1252
    #[tokio::test]
1253
    pub async fn still_subsumes_tokens_after_moving_registration_to_initial_hydration_when_dynamic()
1254
    {
1255
        let upstream_features_cache: Arc<FeatureCache> = Arc::new(FeatureCache::default());
1256
        let upstream_token_cache: Arc<DashMap<String, EdgeToken>> = Arc::new(DashMap::default());
1257
        let upstream_delta_cache: Arc<DashMap<String, DeltaCache>> = Arc::new(DashMap::default());
1258
        let upstream_engine_cache: Arc<DashMap<String, EngineState>> = Arc::new(DashMap::default());
1259
        let server = upstream_server(
1260
            upstream_token_cache.clone(),
1261
            upstream_features_cache.clone(),
1262
            upstream_delta_cache.clone(),
1263
            upstream_engine_cache.clone(),
1264
        )
1265
        .await;
1266
        let upstream_features = features_from_disk("../examples/hostedexample.json");
1267
        let mut upstream_dx_token = EdgeToken::from_str("dx:development.secret123").unwrap();
1268
        upstream_dx_token.status = TokenValidationStatus::Validated;
1269
        upstream_dx_token.token_type = Some(TokenType::Client);
1270
        upstream_token_cache.insert(upstream_dx_token.token.clone(), upstream_dx_token.clone());
1271
        let mut upstream_eg_token = EdgeToken::from_str("eg:development.secret321").unwrap();
1272
        upstream_eg_token.status = TokenValidationStatus::Validated;
1273
        upstream_eg_token.token_type = Some(TokenType::Client);
1274
        upstream_token_cache.insert(upstream_eg_token.token.clone(), upstream_eg_token.clone());
1275
        upstream_features_cache.insert(cache_key(&upstream_dx_token), upstream_features.clone());
1276
        let unleash_client = Arc::new(UnleashClient::new(server.url("/").as_str(), None).unwrap());
1277
        let features_cache: Arc<FeatureCache> = Arc::new(FeatureCache::default());
1278
        let token_cache: Arc<DashMap<String, EdgeToken>> = Arc::new(DashMap::default());
1279
        let engine_cache: Arc<DashMap<String, EngineState>> = Arc::new(DashMap::default());
1280
        let feature_refresher = Arc::new(FeatureRefresher {
1281
            unleash_client: unleash_client.clone(),
1282
            features_cache: features_cache.clone(),
1283
            engine_cache: engine_cache.clone(),
1284
            refresh_interval: Duration::seconds(6000),
1285
            strict: false,
1286
            ..Default::default()
1287
        });
1288
        let token_validator = Arc::new(TokenValidator {
1289
            unleash_client: unleash_client.clone(),
1290
            token_cache: token_cache.clone(),
1291
            persistence: None,
1292
        });
1293
        let local_app = test::init_service(
1294
            App::new()
1295
                .app_data(Data::from(token_validator.clone()))
1296
                .app_data(Data::from(features_cache.clone()))
1297
                .app_data(Data::from(engine_cache.clone()))
1298
                .app_data(Data::from(token_cache.clone()))
1299
                .app_data(Data::from(feature_refresher.clone()))
1300
                .wrap(middleware::as_async_middleware::as_async_middleware(
1301
                    middleware::validate_token::validate_token,
1302
                ))
1303
                .service(web::scope("/api").configure(configure_client_api)),
1304
        )
1305
        .await;
1306
        let dx_req = test::TestRequest::get()
1307
            .uri("/api/client/features")
1308
            .insert_header(ContentType::json())
1309
            .insert_header(("Authorization", upstream_dx_token.token.clone()))
1310
            .to_request();
1311
        let res: ClientFeatures = test::call_and_read_body_json(&local_app, dx_req).await;
1312
        assert!(!res.features.is_empty());
1313
        let eg_req = test::TestRequest::get()
1314
            .uri("/api/client/features")
1315
            .insert_header(ContentType::json())
1316
            .insert_header(("Authorization", upstream_eg_token.token.clone()))
1317
            .to_request();
1318
        let eg_res: ClientFeatures = test::call_and_read_body_json(&local_app, eg_req).await;
1319
        assert!(!eg_res.features.is_empty());
1320
        assert_eq!(feature_refresher.tokens_to_refresh.len(), 2);
1321
        assert_eq!(features_cache.len(), 1);
1322
    }
1323

1324
    #[tokio::test]
1325
    pub async fn can_filter_features_list_by_name_prefix() {
1326
        let features_cache = Arc::new(FeatureCache::default());
1327
        let token_cache: Arc<DashMap<String, EdgeToken>> = Arc::new(DashMap::default());
1328
        let engine_cache: Arc<DashMap<String, EngineState>> = Arc::new(DashMap::default());
1329
        let features = features_from_disk("../examples/hostedexample.json");
1330
        let mut dx_token = EdgeToken::from_str("dx:development.secret123").unwrap();
1331
        dx_token.status = TokenValidationStatus::Validated;
1332
        dx_token.token_type = Some(TokenType::Client);
1333
        token_cache.insert(dx_token.token.clone(), dx_token.clone());
1334
        features_cache.insert(cache_key(&dx_token), features.clone());
1335
        let local_app = test::init_service(
1336
            App::new()
1337
                .app_data(Data::from(features_cache.clone()))
1338
                .app_data(Data::from(engine_cache.clone()))
1339
                .app_data(Data::from(token_cache.clone()))
1340
                .wrap(middleware::as_async_middleware::as_async_middleware(
1341
                    middleware::validate_token::validate_token,
1342
                ))
1343
                .service(web::scope("/api").configure(configure_client_api)),
1344
        )
1345
        .await;
1346
        let request = test::TestRequest::get()
1347
            .uri("/api/client/features?namePrefix=embed")
1348
            .insert_header(ContentType::json())
1349
            .insert_header(("Authorization", dx_token.token.clone()))
1350
            .to_request();
1351
        let result: ClientFeatures = test::call_and_read_body_json(&local_app, request).await;
1352
        assert_eq!(result.features.len(), 2);
1353
        assert_eq!(result.query.unwrap().name_prefix.unwrap(), "embed");
1354
    }
1355

1356
    #[tokio::test]
1357
    pub async fn only_gets_correct_feature_by_name() {
1358
        let features_cache = Arc::new(FeatureCache::default());
1359
        let token_cache: Arc<DashMap<String, EdgeToken>> = Arc::new(DashMap::default());
1360
        let engine_cache: Arc<DashMap<String, EngineState>> = Arc::new(DashMap::default());
1361
        let features = ClientFeatures {
1362
            version: 2,
1363
            query: None,
1364
            features: vec![
1365
                ClientFeature {
1366
                    name: "edge-flag-1".into(),
1367
                    feature_type: None,
1368
                    dependencies: None,
1369
                    description: None,
1370
                    created_at: None,
1371
                    last_seen_at: None,
1372
                    enabled: true,
1373
                    stale: None,
1374
                    impression_data: None,
1375
                    project: Some("dx".into()),
1376
                    strategies: None,
1377
                    variants: None,
1378
                },
1379
                ClientFeature {
1380
                    name: "edge-flag-3".into(),
1381
                    feature_type: None,
1382
                    dependencies: None,
1383
                    description: None,
1384
                    created_at: None,
1385
                    last_seen_at: None,
1386
                    enabled: true,
1387
                    stale: None,
1388
                    impression_data: None,
1389
                    project: Some("eg".into()),
1390
                    strategies: None,
1391
                    variants: None,
1392
                },
1393
            ],
1394
            segments: None,
1395
            meta: None,
1396
        };
1397
        let mut dx_token = EdgeToken::from_str("dx:development.secret123").unwrap();
1398
        dx_token.status = TokenValidationStatus::Validated;
1399
        dx_token.token_type = Some(TokenType::Client);
1400
        let mut eg_token = EdgeToken::from_str("eg:development.secret123").unwrap();
1401
        eg_token.status = TokenValidationStatus::Validated;
1402
        eg_token.token_type = Some(TokenType::Client);
1403
        token_cache.insert(dx_token.token.clone(), dx_token.clone());
1404
        token_cache.insert(eg_token.token.clone(), eg_token.clone());
1405
        features_cache.insert(cache_key(&dx_token), features.clone());
1406
        let local_app = test::init_service(
1407
            App::new()
1408
                .app_data(Data::from(features_cache.clone()))
1409
                .app_data(Data::from(engine_cache.clone()))
1410
                .app_data(Data::from(token_cache.clone()))
1411
                .wrap(middleware::as_async_middleware::as_async_middleware(
1412
                    middleware::validate_token::validate_token,
1413
                ))
1414
                .service(web::scope("/api").configure(configure_client_api)),
1415
        )
1416
        .await;
1417
        let successful_request = test::TestRequest::get()
1418
            .uri("/api/client/features/edge-flag-3")
1419
            .insert_header(ContentType::json())
1420
            .insert_header(("Authorization", eg_token.token.clone()))
1421
            .to_request();
1422
        let res = test::call_service(&local_app, successful_request).await;
1423
        assert_eq!(res.status(), StatusCode::OK);
1424
        let request = test::TestRequest::get()
1425
            .uri("/api/client/features/edge-flag-3")
1426
            .insert_header(ContentType::json())
1427
            .insert_header(("Authorization", dx_token.token.clone()))
1428
            .to_request();
1429
        let res = test::call_service(&local_app, request).await;
1430
        assert_eq!(res.status(), StatusCode::NOT_FOUND);
1431
    }
1432

1433
    #[tokio::test]
1434
    async fn client_features_endpoint_works_with_overridden_token_header() {
1435
        let features_cache = Arc::new(FeatureCache::default());
1436
        let token_cache: Arc<DashMap<String, EdgeToken>> = Arc::new(DashMap::default());
1437
        let token_header = TokenHeader::from_str("NeedsToBeTested").unwrap();
1438
        let app = test::init_service(
1439
            App::new()
1440
                .app_data(Data::from(features_cache.clone()))
1441
                .app_data(Data::from(token_cache.clone()))
1442
                .app_data(Data::new(token_header.clone()))
1443
                .service(web::scope("/api/client").service(get_features)),
1444
        )
1445
        .await;
1446
        let client_features = cached_client_features();
1447
        let example_features = features_from_disk("../examples/features.json");
1448
        features_cache.insert("development".into(), client_features.clone());
1449
        features_cache.insert("production".into(), example_features.clone());
1450
        let mut production_token = EdgeToken::try_from(
1451
            "*:production.03fa5f506428fe80ed5640c351c7232e38940814d2923b08f5c05fa7".to_string(),
1452
        )
1453
        .unwrap();
1454
        production_token.token_type = Some(TokenType::Client);
1455
        production_token.status = TokenValidationStatus::Validated;
1456
        token_cache.insert(production_token.token.clone(), production_token.clone());
1457

1458
        let request = test::TestRequest::get()
1459
            .uri("/api/client/features")
1460
            .insert_header(ContentType::json())
1461
            .insert_header(("NeedsToBeTested", production_token.token.clone()))
1462
            .to_request();
1463
        let res = test::call_service(&app, request).await;
1464
        assert_eq!(res.status(), StatusCode::OK);
1465
        let request = test::TestRequest::get()
1466
            .uri("/api/client/features")
1467
            .insert_header(ContentType::json())
1468
            .insert_header(("ShouldNotWork", production_token.token.clone()))
1469
            .to_request();
1470
        let res = test::call_service(&app, request).await;
1471
        assert_eq!(res.status(), StatusCode::FORBIDDEN);
1472
    }
1473
}
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