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

Unleash / unleash-edge / #1609

21 Feb 2025 07:30AM UTC coverage: 63.374% (-1.0%) from 64.335%
#1609

push

web-flow
feat: delta filtering and etag handling (#749)

* feat: delta filtering and etag handling

21 of 94 new or added lines in 3 files covered. (22.34%)

1 existing line in 1 file now uncovered.

1604 of 2531 relevant lines covered (63.37%)

1.58 hits per line

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

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

26
#[utoipa::path(
27
    context_path = "/api/client",
28
    params(FeatureFilters),
29
    responses(
30
        (status = 200, description = "Return feature toggles for this token", body = ClientFeatures),
31
        (status = 403, description = "Was not allowed to access features"),
32
        (status = 400, description = "Invalid parameters used")
33
    ),
34
    security(
35
        ("Authorization" = [])
36
    )
37
)]
38
#[get("/features")]
39
pub async fn get_features(
2✔
40
    edge_token: EdgeToken,
41
    features_cache: Data<FeatureCache>,
42
    token_cache: Data<DashMap<String, EdgeToken>>,
43
    filter_query: Query<FeatureFilters>,
44
    req: HttpRequest,
45
) -> EdgeJsonResult<ClientFeatures> {
46
    resolve_features(edge_token, features_cache, token_cache, filter_query, req).await
4✔
47
}
48
// TODO: add tests for delta api
49
#[get("/delta")]
50
pub async fn get_delta(
×
51
    edge_token: EdgeToken,
52
    token_cache: Data<DashMap<String, EdgeToken>>,
53
    filter_query: Query<FeatureFilters>,
54
    req: HttpRequest,
55
) -> impl Responder {
NEW
56
    let requested_revision_id = req
×
57
        .headers()
58
        .get("If-None-Match")
NEW
59
        .and_then(|value| value.to_str().ok())
×
NEW
60
        .and_then(|etag| etag.trim_matches('"').parse::<u32>().ok())
×
61
        .unwrap_or(0);
62

NEW
63
    let current_sdk_revision_id = requested_revision_id + 1; // TODO: Read from delta_manager
×
64

NEW
65
    match resolve_delta(
×
NEW
66
        edge_token,
×
NEW
67
        token_cache,
×
NEW
68
        filter_query,
×
69
        requested_revision_id,
NEW
70
        req,
×
71
    )
NEW
72
    .await
×
73
    {
NEW
74
        Ok(Json(None)) => HttpResponse::NotModified().finish(),
×
NEW
75
        Ok(Json(Some(delta))) => {
×
NEW
76
            let last_event_id = delta
×
77
                .events
78
                .last()
NEW
79
                .map(|e| e.get_event_id())
×
NEW
80
                .unwrap_or(current_sdk_revision_id);
×
81

NEW
82
            HttpResponse::Ok()
×
NEW
83
                .insert_header(("ETag", format!("{}", last_event_id)))
×
NEW
84
                .json(delta)
×
85
        }
NEW
86
        Err(err) => HttpResponse::InternalServerError().body(format!("Error: {:?}", err)),
×
87
    }
88
}
89

90
#[get("/streaming")]
91
pub async fn stream_features(
1✔
92
    edge_token: EdgeToken,
93
    broadcaster: Data<Broadcaster>,
94
    token_cache: Data<DashMap<String, EdgeToken>>,
95
    edge_mode: Data<EdgeMode>,
96
    filter_query: Query<FeatureFilters>,
97
) -> EdgeResult<impl Responder> {
98
    match edge_mode.get_ref() {
2✔
99
        EdgeMode::Edge(EdgeArgs {
100
            streaming: true, ..
101
        }) => {
102
            let (validated_token, _filter_set, query) =
1✔
103
                get_feature_filter(&edge_token, &token_cache, filter_query.clone())?;
104

105
            broadcaster.connect(validated_token, query).await
2✔
106
        }
107
        _ => Err(EdgeError::Forbidden(
×
108
            "This endpoint is only enabled in streaming mode".into(),
×
109
        )),
110
    }
111
}
112

113
#[utoipa::path(
114
    context_path = "/api/client",
115
    params(FeatureFilters),
116
    responses(
117
        (status = 200, description = "Return feature toggles for this token", body = ClientFeatures),
118
        (status = 403, description = "Was not allowed to access features"),
119
        (status = 400, description = "Invalid parameters used")
120
    ),
121
    security(
122
        ("Authorization" = [])
123
    )
124
)]
125
#[post("/features")]
126
pub async fn post_features(
1✔
127
    edge_token: EdgeToken,
128
    features_cache: Data<FeatureCache>,
129
    token_cache: Data<DashMap<String, EdgeToken>>,
130
    filter_query: Query<FeatureFilters>,
131
    req: HttpRequest,
132
) -> EdgeJsonResult<ClientFeatures> {
133
    resolve_features(edge_token, features_cache, token_cache, filter_query, req).await
2✔
134
}
135

136
fn get_feature_filter(
2✔
137
    edge_token: &EdgeToken,
138
    token_cache: &Data<DashMap<String, EdgeToken>>,
139
    filter_query: Query<FeatureFilters>,
140
) -> EdgeResult<(
141
    EdgeToken,
142
    FeatureFilterSet,
143
    unleash_types::client_features::Query,
144
)> {
145
    let validated_token = token_cache
7✔
146
        .get(&edge_token.token)
147
        .map(|e| e.value().clone())
5✔
148
        .ok_or(EdgeError::AuthorizationDenied)?;
3✔
149

150
    let query_filters = filter_query.into_inner();
5✔
151
    let query = unleash_types::client_features::Query {
152
        tags: None,
153
        projects: Some(validated_token.projects.clone()),
4✔
154
        name_prefix: query_filters.name_prefix.clone(),
3✔
155
        environment: validated_token.environment.clone(),
2✔
156
        inline_segment_constraints: Some(false),
2✔
157
    };
158

159
    let filter_set = if let Some(name_prefix) = query_filters.name_prefix {
4✔
160
        FeatureFilterSet::from(Box::new(name_prefix_filter(name_prefix)))
2✔
161
    } else {
162
        FeatureFilterSet::default()
4✔
163
    }
164
    .with_filter(project_filter(&validated_token));
5✔
165

166
    Ok((validated_token, filter_set, query))
3✔
167
}
168

NEW
169
fn get_delta_filter(
×
170
    edge_token: &EdgeToken,
171
    token_cache: &Data<DashMap<String, EdgeToken>>,
172
    filter_query: Query<FeatureFilters>,
173
    requested_revision_id: u32,
174
) -> EdgeResult<DeltaFilterSet> {
NEW
175
    let validated_token = token_cache
×
176
        .get(&edge_token.token)
NEW
177
        .map(|e| e.value().clone())
×
NEW
178
        .ok_or(EdgeError::AuthorizationDenied)?;
×
179

NEW
180
    let query_filters = filter_query.into_inner();
×
181

NEW
182
    let delta_filter_set = DeltaFilterSet::default().with_filter(combined_filter(
×
183
        requested_revision_id,
NEW
184
        validated_token.projects.clone(),
×
NEW
185
        query_filters.name_prefix.clone(),
×
186
    ));
187

NEW
188
    Ok(delta_filter_set)
×
189
}
190

191
async fn resolve_features(
2✔
192
    edge_token: EdgeToken,
193
    features_cache: Data<FeatureCache>,
194
    token_cache: Data<DashMap<String, EdgeToken>>,
195
    filter_query: Query<FeatureFilters>,
196
    req: HttpRequest,
197
) -> EdgeJsonResult<ClientFeatures> {
198
    let (validated_token, filter_set, query) =
3✔
199
        get_feature_filter(&edge_token, &token_cache, filter_query.clone())?;
200

201
    let client_features = match req.app_data::<Data<FeatureRefresher>>() {
6✔
202
        Some(refresher) => {
1✔
203
            refresher
5✔
204
                .features_for_filter(validated_token.clone(), &filter_set)
1✔
205
                .await
4✔
206
        }
207
        None => features_cache
6✔
208
            .get(&cache_key(&validated_token))
1✔
209
            .map(|client_features| filter_client_features(&client_features, &filter_set))
3✔
210
            .ok_or(EdgeError::ClientCacheError),
2✔
211
    }?;
212

213
    Ok(Json(ClientFeatures {
1✔
214
        query: Some(query),
2✔
215
        ..client_features
216
    }))
217
}
UNCOV
218
async fn resolve_delta(
×
219
    edge_token: EdgeToken,
220
    token_cache: Data<DashMap<String, EdgeToken>>,
221
    filter_query: Query<FeatureFilters>,
222
    requested_revision_id: u32,
223
    req: HttpRequest,
224
) -> EdgeJsonResult<Option<ClientFeaturesDelta>> {
225
    let (validated_token, filter_set, ..) =
×
226
        get_feature_filter(&edge_token, &token_cache, filter_query.clone())?;
227

NEW
228
    let delta_filter_set = get_delta_filter(&edge_token, &token_cache, filter_query.clone(), requested_revision_id)?;
×
229

NEW
230
    let current_sdk_revision_id = requested_revision_id + 1; // TODO: get from delta manager
×
NEW
231
    if requested_revision_id >= current_sdk_revision_id {
×
NEW
232
        return Ok(Json(None));
×
233
    }
234

NEW
235
    let refresher = req.app_data::<Data<FeatureRefresher>>().ok_or_else(|| {
×
NEW
236
        EdgeError::ClientHydrationFailed(
×
NEW
237
            "FeatureRefresher is missing - cannot resolve delta in offline mode".to_string(),
×
238
        )
239
    })?;
240

NEW
241
    let delta = refresher
×
242
        .delta_events_for_filter(
NEW
243
            validated_token.clone(),
×
NEW
244
            &filter_set,
×
NEW
245
            &delta_filter_set,
×
246
            requested_revision_id,
247
        )
NEW
248
        .await?;
×
249

NEW
250
    if delta.events.is_empty() {
×
NEW
251
        return Ok(Json(None));
×
252
    }
253

NEW
254
    Ok(Json(Some(delta)))
×
255
}
256

257
#[utoipa::path(
258
    context_path = "/api/client",
259
    params(("feature_name" = String, Path,)),
260
    responses(
261
        (status = 200, description = "Return feature toggles for this token", body = ClientFeature),
262
        (status = 403, description = "Was not allowed to access feature"),
263
        (status = 400, description = "Invalid parameters used"),
264
        (status = 404, description = "Feature did not exist or token used was not allowed to access it")
265
    ),
266
    security(
267
        ("Authorization" = [])
268
    )
269
)]
270
#[get("/features/{feature_name}")]
271
pub async fn get_feature(
1✔
272
    edge_token: EdgeToken,
273
    features_cache: Data<FeatureCache>,
274
    token_cache: Data<DashMap<String, EdgeToken>>,
275
    feature_name: web::Path<String>,
276
    req: HttpRequest,
277
) -> EdgeJsonResult<ClientFeature> {
278
    let validated_token = token_cache
5✔
279
        .get(&edge_token.token)
2✔
280
        .map(|e| e.value().clone())
4✔
281
        .ok_or(EdgeError::AuthorizationDenied)?;
2✔
282

283
    let filter_set = FeatureFilterSet::from(Box::new(name_match_filter(feature_name.clone())))
8✔
284
        .with_filter(project_filter(&validated_token));
4✔
285

286
    match req.app_data::<Data<FeatureRefresher>>() {
12✔
287
        Some(refresher) => {
×
288
            refresher
×
289
                .features_for_filter(validated_token.clone(), &filter_set)
×
290
                .await
×
291
        }
292
        None => features_cache
10✔
293
            .get(&cache_key(&validated_token))
2✔
294
            .map(|client_features| filter_client_features(&client_features, &filter_set))
6✔
295
            .ok_or(EdgeError::ClientCacheError),
4✔
296
    }
297
    .map(|client_features| client_features.features.into_iter().next())?
4✔
298
    .ok_or(EdgeError::FeatureNotFound(feature_name.into_inner()))
4✔
299
    .map(Json)
300
}
301

302
#[utoipa::path(
303
    context_path = "/api/client",
304
    responses(
305
        (status = 202, description = "Accepted client application registration"),
306
        (status = 403, description = "Was not allowed to register client application"),
307
    ),
308
    request_body = ClientApplication,
309
    security(
310
        ("Authorization" = [])
311
    )
312
)]
313
#[post("/register")]
314
pub async fn register(
2✔
315
    edge_token: EdgeToken,
316
    connect_via: Data<ConnectVia>,
317
    client_application: Json<ClientApplication>,
318
    metrics_cache: Data<MetricsCache>,
319
) -> EdgeResult<HttpResponse> {
320
    crate::metrics::client_metrics::register_client_application(
321
        edge_token,
2✔
322
        &connect_via,
4✔
323
        client_application.into_inner(),
2✔
324
        metrics_cache,
2✔
325
    );
326
    Ok(HttpResponse::Accepted()
6✔
327
        .append_header(("X-Edge-Version", types::EDGE_VERSION))
2✔
328
        .finish())
329
}
330

331
#[utoipa::path(
332
    context_path = "/api/client",
333
    responses(
334
        (status = 202, description = "Accepted client metrics"),
335
        (status = 403, description = "Was not allowed to post metrics"),
336
    ),
337
    request_body = ClientMetrics,
338
    security(
339
        ("Authorization" = [])
340
    )
341
)]
342
#[post("/metrics")]
343
pub async fn metrics(
1✔
344
    edge_token: EdgeToken,
345
    metrics: Json<ClientMetrics>,
346
    metrics_cache: Data<MetricsCache>,
347
) -> EdgeResult<HttpResponse> {
348
    crate::metrics::client_metrics::register_client_metrics(
349
        edge_token,
1✔
350
        metrics.into_inner(),
1✔
351
        metrics_cache,
1✔
352
    );
353
    Ok(HttpResponse::Accepted().finish())
3✔
354
}
355

356
#[utoipa::path(
357
context_path = "/api/client",
358
responses(
359
(status = 202, description = "Accepted bulk metrics"),
360
(status = 403, description = "Was not allowed to post bulk metrics")
361
),
362
request_body = BatchMetricsRequestBody,
363
security(
364
("Authorization" = [])
365
)
366
)]
367
#[post("/metrics/bulk")]
368
pub async fn post_bulk_metrics(
1✔
369
    edge_token: EdgeToken,
370
    bulk_metrics: Json<BatchMetricsRequestBody>,
371
    connect_via: Data<ConnectVia>,
372
    metrics_cache: Data<MetricsCache>,
373
) -> EdgeResult<HttpResponse> {
374
    crate::metrics::client_metrics::register_bulk_metrics(
375
        metrics_cache.get_ref(),
1✔
376
        connect_via.get_ref(),
1✔
377
        &edge_token,
378
        bulk_metrics.into_inner(),
1✔
379
    );
380
    Ok(HttpResponse::Accepted().finish())
2✔
381
}
382

383
#[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(
384
("Authorization" = [])
385
)
386
)]
387
#[post("/metrics/edge")]
388
#[instrument(skip(_edge_token, instance_data, connected_instances))]
389
pub async fn post_edge_instance_data(
390
    _edge_token: EdgeToken,
391
    instance_data: Json<EdgeInstanceData>,
392
    instance_data_sending: Data<InstanceDataSending>,
393
    connected_instances: Data<RwLock<Vec<EdgeInstanceData>>>,
394
) -> EdgeResult<HttpResponse> {
395
    if let InstanceDataSending::SendInstanceData(_) = instance_data_sending.as_ref() {
×
396
        connected_instances
×
397
            .write()
398
            .await
×
399
            .push(instance_data.into_inner());
×
400
    }
401
    Ok(HttpResponse::Accepted().finish())
×
402
}
403

404
pub fn configure_client_api(cfg: &mut web::ServiceConfig) {
3✔
405
    let client_scope = web::scope("/client")
7✔
406
        .wrap(crate::middleware::as_async_middleware::as_async_middleware(
2✔
407
            crate::middleware::validate_token::validate_token,
408
        ))
409
        .service(get_features)
410
        .service(get_delta)
411
        .service(get_feature)
412
        .service(register)
413
        .service(metrics)
414
        .service(post_bulk_metrics)
415
        .service(stream_features)
416
        .service(post_edge_instance_data);
417

418
    cfg.service(client_scope);
2✔
419
}
420

421
pub fn configure_experimental_post_features(
×
422
    cfg: &mut web::ServiceConfig,
423
    post_features_enabled: bool,
424
) {
425
    if post_features_enabled {
×
426
        cfg.service(post_features);
×
427
    }
428
}
429

430
#[cfg(test)]
431
mod tests {
432

433
    use crate::metrics::client_metrics::{ApplicationKey, MetricsBatch, MetricsKey};
434
    use crate::types::{TokenType, TokenValidationStatus};
435
    use std::collections::HashMap;
436
    use std::path::PathBuf;
437
    use std::str::FromStr;
438
    use std::sync::Arc;
439

440
    use super::*;
441

442
    use crate::auth::token_validator::TokenValidator;
443
    use crate::cli::{OfflineArgs, TokenHeader};
444
    use crate::delta_cache::DeltaCache;
445
    use crate::http::unleash_client::{ClientMetaInformation, UnleashClient};
446
    use crate::middleware;
447
    use crate::tests::{features_from_disk, upstream_server};
448
    use actix_http::{Request, StatusCode};
449
    use actix_web::{
450
        http::header::ContentType,
451
        test,
452
        web::{self, Data},
453
        App, ResponseError,
454
    };
455
    use chrono::{DateTime, Duration, TimeZone, Utc};
456
    use maplit::hashmap;
457
    use ulid::Ulid;
458
    use unleash_types::client_features::{
459
        ClientFeature, Constraint, Operator, Strategy, StrategyVariant,
460
    };
461
    use unleash_types::client_metrics::{
462
        ClientMetricsEnv, ConnectViaBuilder, MetricBucket, MetricsMetadata, ToggleStats,
463
    };
464
    use unleash_yggdrasil::EngineState;
465

466
    async fn make_metrics_post_request() -> Request {
467
        test::TestRequest::post()
468
            .uri("/api/client/metrics")
469
            .insert_header(ContentType::json())
470
            .insert_header((
471
                "Authorization",
472
                "*:development.03fa5f506428fe80ed5640c351c7232e38940814d2923b08f5c05fa7",
473
            ))
474
            .set_json(Json(ClientMetrics {
475
                app_name: "some-app".into(),
476
                instance_id: Some("some-instance".into()),
477
                bucket: MetricBucket {
478
                    start: Utc.with_ymd_and_hms(1867, 11, 7, 12, 0, 0).unwrap(),
479
                    stop: Utc.with_ymd_and_hms(1934, 11, 7, 12, 0, 0).unwrap(),
480
                    toggles: hashmap! {
481
                        "some-feature".to_string() => ToggleStats {
482
                            yes: 1,
483
                            no: 0,
484
                            variants: hashmap! {}
485
                        }
486
                    },
487
                },
488
                environment: Some("development".into()),
489
                metadata: MetricsMetadata {
490
                    platform_name: Some("test".into()),
491
                    platform_version: Some("1.0".into()),
492
                    sdk_version: Some("1.0".into()),
493
                    yggdrasil_version: None,
494
                },
495
            }))
496
            .to_request()
497
    }
498

499
    async fn make_bulk_metrics_post_request(authorization: Option<String>) -> Request {
500
        let mut req = test::TestRequest::post()
501
            .uri("/api/client/metrics/bulk")
502
            .insert_header(ContentType::json());
503
        req = match authorization {
504
            Some(auth) => req.insert_header(("Authorization", auth)),
505
            None => req,
506
        };
507
        req.set_json(Json(BatchMetricsRequestBody {
508
            applications: vec![ClientApplication {
509
                app_name: "test_app".to_string(),
510
                connect_via: None,
511
                environment: None,
512
                instance_id: None,
513
                interval: 10,
514
                started: Default::default(),
515
                strategies: vec![],
516
                metadata: MetricsMetadata {
517
                    platform_name: None,
518
                    platform_version: None,
519
                    sdk_version: None,
520
                    yggdrasil_version: None,
521
                },
522
            }],
523
            metrics: vec![ClientMetricsEnv {
524
                feature_name: "".to_string(),
525
                app_name: "".to_string(),
526
                environment: "".to_string(),
527
                timestamp: Default::default(),
528
                yes: 0,
529
                no: 0,
530
                variants: Default::default(),
531
                metadata: MetricsMetadata {
532
                    platform_name: None,
533
                    platform_version: None,
534
                    sdk_version: None,
535
                    yggdrasil_version: None,
536
                },
537
            }],
538
        }))
539
        .to_request()
540
    }
541

542
    async fn make_register_post_request(application: ClientApplication) -> Request {
543
        test::TestRequest::post()
544
            .uri("/api/client/register")
545
            .insert_header(ContentType::json())
546
            .insert_header((
547
                "Authorization",
548
                "*:development.03fa5f506428fe80ed5640c351c7232e38940814d2923b08f5c05fa7",
549
            ))
550
            .set_json(Json(application))
551
            .to_request()
552
    }
553

554
    async fn make_features_request_with_token(token: EdgeToken) -> Request {
555
        test::TestRequest::get()
556
            .uri("/api/client/features")
557
            .insert_header(("Authorization", token.token))
558
            .to_request()
559
    }
560

561
    #[actix_web::test]
562
    async fn metrics_endpoint_correctly_aggregates_data() {
563
        let metrics_cache = Arc::new(MetricsCache::default());
564

565
        let app = test::init_service(
566
            App::new()
567
                .app_data(Data::new(ConnectVia {
568
                    app_name: "test".into(),
569
                    instance_id: Ulid::new().to_string(),
570
                }))
571
                .app_data(Data::from(metrics_cache.clone()))
572
                .service(web::scope("/api/client").service(metrics)),
573
        )
574
        .await;
575

576
        let req = make_metrics_post_request().await;
577
        let _result = test::call_and_read_body(&app, req).await;
578

579
        let cache = metrics_cache.clone();
580

581
        let found_metric = cache
582
            .metrics
583
            .get(&MetricsKey {
584
                app_name: "some-app".into(),
585
                feature_name: "some-feature".into(),
586
                timestamp: DateTime::parse_from_rfc3339("1867-11-07T12:00:00Z")
587
                    .unwrap()
588
                    .with_timezone(&Utc),
589
                environment: "development".into(),
590
            })
591
            .unwrap();
592

593
        let expected = ClientMetricsEnv {
594
            app_name: "some-app".into(),
595
            feature_name: "some-feature".into(),
596
            environment: "development".into(),
597
            timestamp: DateTime::parse_from_rfc3339("1867-11-07T12:00:00Z")
598
                .unwrap()
599
                .with_timezone(&Utc),
600
            yes: 1,
601
            no: 0,
602
            variants: HashMap::new(),
603
            metadata: MetricsMetadata {
604
                platform_name: None,
605
                platform_version: None,
606
                sdk_version: None,
607
                yggdrasil_version: None,
608
            },
609
        };
610

611
        assert_eq!(found_metric.yes, expected.yes);
612
        assert_eq!(found_metric.yes, 1);
613
        assert_eq!(found_metric.no, 0);
614
        assert_eq!(found_metric.no, expected.no);
615
    }
616

617
    fn cached_client_features() -> ClientFeatures {
618
        ClientFeatures {
619
            version: 2,
620
            features: vec![
621
                ClientFeature {
622
                    name: "feature_one".into(),
623
                    feature_type: Some("release".into()),
624
                    description: Some("test feature".into()),
625
                    created_at: Some(Utc::now()),
626
                    dependencies: None,
627
                    last_seen_at: None,
628
                    enabled: true,
629
                    stale: Some(false),
630
                    impression_data: Some(false),
631
                    project: Some("default".into()),
632
                    strategies: Some(vec![
633
                        Strategy {
634
                            variants: Some(vec![StrategyVariant {
635
                                name: "test".into(),
636
                                payload: None,
637
                                weight: 7,
638
                                stickiness: Some("sticky-on-something".into()),
639
                            }]),
640
                            name: "standard".into(),
641
                            sort_order: Some(500),
642
                            segments: None,
643
                            constraints: None,
644
                            parameters: None,
645
                        },
646
                        Strategy {
647
                            variants: None,
648
                            name: "gradualRollout".into(),
649
                            sort_order: Some(100),
650
                            segments: None,
651
                            constraints: None,
652
                            parameters: None,
653
                        },
654
                    ]),
655
                    variants: None,
656
                },
657
                ClientFeature {
658
                    name: "feature_two_no_strats".into(),
659
                    feature_type: None,
660
                    dependencies: None,
661
                    description: None,
662
                    created_at: Some(Utc.with_ymd_and_hms(2022, 12, 5, 12, 31, 0).unwrap()),
663
                    last_seen_at: None,
664
                    enabled: true,
665
                    stale: None,
666
                    impression_data: None,
667
                    project: Some("default".into()),
668
                    strategies: None,
669
                    variants: None,
670
                },
671
                ClientFeature {
672
                    name: "feature_three".into(),
673
                    feature_type: Some("release".into()),
674
                    description: None,
675
                    dependencies: None,
676
                    created_at: None,
677
                    last_seen_at: None,
678
                    enabled: true,
679
                    stale: None,
680
                    impression_data: None,
681
                    project: Some("default".into()),
682
                    strategies: Some(vec![
683
                        Strategy {
684
                            name: "gradualRollout".to_string(),
685
                            sort_order: None,
686
                            segments: None,
687
                            variants: None,
688
                            constraints: Some(vec![Constraint {
689
                                context_name: "version".to_string(),
690
                                operator: Operator::SemverGt,
691
                                case_insensitive: false,
692
                                inverted: false,
693
                                values: None,
694
                                value: Some("1.5.0".into()),
695
                            }]),
696
                            parameters: None,
697
                        },
698
                        Strategy {
699
                            name: "".to_string(),
700
                            sort_order: None,
701
                            segments: None,
702
                            constraints: None,
703
                            parameters: None,
704
                            variants: None,
705
                        },
706
                    ]),
707
                    variants: None,
708
                },
709
            ],
710
            segments: None,
711
            query: None,
712
            meta: None,
713
        }
714
    }
715

716
    #[tokio::test]
717
    async fn response_includes_variant_stickiness_for_strategy_variants() {
718
        let features_cache = Arc::new(FeatureCache::default());
719
        let token_cache: Arc<DashMap<String, EdgeToken>> = Arc::new(DashMap::default());
720
        let app = test::init_service(
721
            App::new()
722
                .app_data(Data::from(features_cache.clone()))
723
                .app_data(Data::from(token_cache.clone()))
724
                .service(web::scope("/api/client").service(get_features)),
725
        )
726
        .await;
727

728
        features_cache.insert("production".into(), cached_client_features());
729
        let mut production_token = EdgeToken::try_from(
730
            "*:production.03fa5f506428fe80ed5640c351c7232e38940814d2923b08f5c05fa7".to_string(),
731
        )
732
        .unwrap();
733
        production_token.token_type = Some(TokenType::Client);
734
        production_token.status = TokenValidationStatus::Validated;
735
        token_cache.insert(production_token.token.clone(), production_token.clone());
736
        let req = make_features_request_with_token(production_token.clone()).await;
737
        let res: ClientFeatures = test::call_and_read_body_json(&app, req).await;
738

739
        assert_eq!(res.features.len(), cached_client_features().features.len());
740
        let strategy_variant_stickiness = res
741
            .features
742
            .iter()
743
            .find(|f| f.name == "feature_one")
744
            .unwrap()
745
            .strategies
746
            .clone()
747
            .unwrap()
748
            .iter()
749
            .find(|s| s.name == "standard")
750
            .unwrap()
751
            .variants
752
            .clone()
753
            .unwrap()
754
            .iter()
755
            .find(|v| v.name == "test")
756
            .unwrap()
757
            .stickiness
758
            .clone();
759
        assert!(strategy_variant_stickiness.is_some());
760
    }
761

762
    #[tokio::test]
763
    async fn register_endpoint_correctly_aggregates_applications() {
764
        let metrics_cache = Arc::new(MetricsCache::default());
765
        let our_app = ConnectVia {
766
            app_name: "test".into(),
767
            instance_id: Ulid::new().to_string(),
768
        };
769
        let app = test::init_service(
770
            App::new()
771
                .app_data(Data::new(our_app.clone()))
772
                .app_data(Data::from(metrics_cache.clone()))
773
                .service(web::scope("/api/client").service(register)),
774
        )
775
        .await;
776
        let mut client_app = ClientApplication::new("test_application", 15);
777
        client_app.instance_id = Some("test_instance".into());
778
        let req = make_register_post_request(client_app.clone()).await;
779
        let res = test::call_service(&app, req).await;
780
        assert_eq!(res.status(), actix_http::StatusCode::ACCEPTED);
781
        assert_eq!(metrics_cache.applications.len(), 1);
782
        let application_key = ApplicationKey {
783
            app_name: client_app.app_name.clone(),
784
            instance_id: client_app.instance_id.unwrap(),
785
        };
786
        let saved_app = metrics_cache
787
            .applications
788
            .get(&application_key)
789
            .unwrap()
790
            .value()
791
            .clone();
792
        assert_eq!(saved_app.app_name, client_app.app_name);
793
        assert_eq!(saved_app.connect_via, Some(vec![our_app]));
794
    }
795

796
    #[tokio::test]
797
    async fn bulk_metrics_endpoint_correctly_accepts_data() {
798
        let metrics_cache = MetricsCache::default();
799
        let connect_via = ConnectViaBuilder::default()
800
            .app_name("unleash-edge".into())
801
            .instance_id("test".into())
802
            .build()
803
            .unwrap();
804
        let app = test::init_service(
805
            App::new()
806
                .app_data(Data::new(connect_via))
807
                .app_data(web::Data::new(metrics_cache))
808
                .service(web::scope("/api/client").service(post_bulk_metrics)),
809
        )
810
        .await;
811
        let token = EdgeToken::from_str("*:development.somestring").unwrap();
812
        let req = make_bulk_metrics_post_request(Some(token.token.clone())).await;
813
        let call = test::call_service(&app, req).await;
814
        assert_eq!(call.status(), StatusCode::ACCEPTED);
815
    }
816
    #[tokio::test]
817
    async fn bulk_metrics_endpoint_correctly_refuses_metrics_without_auth_header() {
818
        let mut token = EdgeToken::from_str("*:development.somestring").unwrap();
819
        token.status = TokenValidationStatus::Validated;
820
        token.token_type = Some(TokenType::Client);
821
        let upstream_token_cache = Arc::new(DashMap::default());
822
        let upstream_features_cache = Arc::new(FeatureCache::default());
823
        let upstream_delta_cache = Arc::new(DashMap::default());
824
        let upstream_engine_cache = Arc::new(DashMap::default());
825
        upstream_token_cache.insert(token.token.clone(), token.clone());
826
        let srv = upstream_server(
827
            upstream_token_cache,
828
            upstream_features_cache,
829
            upstream_delta_cache,
830
            upstream_engine_cache,
831
        )
832
        .await;
833
        let req = reqwest::Client::new();
834
        let status = req
835
            .post(srv.url("/api/client/metrics/bulk").as_str())
836
            .body(
837
                serde_json::to_string(&crate::types::BatchMetricsRequestBody {
838
                    applications: vec![],
839
                    metrics: vec![],
840
                })
841
                .unwrap(),
842
            )
843
            .send()
844
            .await;
845
        assert!(status.is_ok());
846
        assert_eq!(
847
            status.unwrap().status().as_u16(),
848
            StatusCode::FORBIDDEN.as_u16()
849
        );
850
        let client = UnleashClient::new(srv.url("/").as_str(), None).unwrap();
851
        let successful = client
852
            .send_bulk_metrics_to_client_endpoint(MetricsBatch::default(), &token.token)
853
            .await;
854
        assert!(successful.is_ok());
855
    }
856

857
    #[tokio::test]
858
    async fn bulk_metrics_endpoint_correctly_refuses_metrics_with_frontend_token() {
859
        let mut frontend_token = EdgeToken::from_str("*:development.frontend").unwrap();
860
        frontend_token.status = TokenValidationStatus::Validated;
861
        frontend_token.token_type = Some(TokenType::Frontend);
862
        let upstream_token_cache = Arc::new(DashMap::default());
863
        let upstream_features_cache = Arc::new(FeatureCache::default());
864
        let upstream_delta_cache = Arc::new(DashMap::default());
865
        let upstream_engine_cache = Arc::new(DashMap::default());
866
        upstream_token_cache.insert(frontend_token.token.clone(), frontend_token.clone());
867
        let srv = upstream_server(
868
            upstream_token_cache,
869
            upstream_features_cache,
870
            upstream_delta_cache,
871
            upstream_engine_cache,
872
        )
873
        .await;
874
        let client = UnleashClient::new(srv.url("/").as_str(), None).unwrap();
875
        let status = client
876
            .send_bulk_metrics_to_client_endpoint(MetricsBatch::default(), &frontend_token.token)
877
            .await;
878
        assert_eq!(status.expect_err("").status_code(), StatusCode::FORBIDDEN);
879
    }
880
    #[tokio::test]
881
    async fn register_endpoint_returns_version_header() {
882
        let metrics_cache = Arc::new(MetricsCache::default());
883
        let our_app = ConnectVia {
884
            app_name: "test".into(),
885
            instance_id: Ulid::new().to_string(),
886
        };
887
        let app = test::init_service(
888
            App::new()
889
                .app_data(Data::new(our_app.clone()))
890
                .app_data(Data::from(metrics_cache.clone()))
891
                .service(web::scope("/api/client").service(register)),
892
        )
893
        .await;
894
        let mut client_app = ClientApplication::new("test_application", 15);
895
        client_app.instance_id = Some("test_instance".into());
896
        let req = make_register_post_request(client_app.clone()).await;
897
        let res = test::call_service(&app, req).await;
898
        assert_eq!(res.status(), StatusCode::ACCEPTED);
899
        assert_eq!(
900
            res.headers().get("X-Edge-Version").unwrap(),
901
            types::EDGE_VERSION
902
        );
903
    }
904

905
    #[tokio::test]
906
    async fn client_features_endpoint_correctly_returns_cached_features() {
907
        let features_cache = Arc::new(FeatureCache::default());
908
        let token_cache: Arc<DashMap<String, EdgeToken>> = Arc::new(DashMap::default());
909
        let app = test::init_service(
910
            App::new()
911
                .app_data(Data::from(features_cache.clone()))
912
                .app_data(Data::from(token_cache.clone()))
913
                .service(web::scope("/api/client").service(get_features)),
914
        )
915
        .await;
916
        let client_features = cached_client_features();
917
        let example_features = features_from_disk("../examples/features.json");
918
        features_cache.insert("development".into(), client_features.clone());
919
        features_cache.insert("production".into(), example_features.clone());
920
        let mut token = EdgeToken::try_from(
921
            "*:development.03fa5f506428fe80ed5640c351c7232e38940814d2923b08f5c05fa7".to_string(),
922
        )
923
        .unwrap();
924
        token.token_type = Some(TokenType::Client);
925
        token.status = TokenValidationStatus::Validated;
926
        token_cache.insert(token.token.clone(), token.clone());
927
        let req = make_features_request_with_token(token.clone()).await;
928
        let res: ClientFeatures = test::call_and_read_body_json(&app, req).await;
929
        assert_eq!(res.features, client_features.features);
930
        let mut production_token = EdgeToken::try_from(
931
            "*:production.03fa5f506428fe80ed5640c351c7232e38940814d2923b08f5c05fa7".to_string(),
932
        )
933
        .unwrap();
934
        production_token.token_type = Some(TokenType::Client);
935
        production_token.status = TokenValidationStatus::Validated;
936
        token_cache.insert(production_token.token.clone(), production_token.clone());
937
        let req = make_features_request_with_token(production_token.clone()).await;
938
        let res: ClientFeatures = test::call_and_read_body_json(&app, req).await;
939
        assert_eq!(res.features.len(), example_features.features.len());
940
    }
941

942
    #[tokio::test]
943
    async fn post_request_to_client_features_does_the_same_as_get_when_mounted() {
944
        let features_cache = Arc::new(FeatureCache::default());
945
        let token_cache: Arc<DashMap<String, EdgeToken>> = Arc::new(DashMap::default());
946
        let app = test::init_service(
947
            App::new()
948
                .app_data(Data::from(features_cache.clone()))
949
                .app_data(Data::from(token_cache.clone()))
950
                .service(
951
                    web::scope("/api/client")
952
                        .service(get_features)
953
                        .service(post_features),
954
                ),
955
        )
956
        .await;
957
        let client_features = cached_client_features();
958
        let example_features = features_from_disk("../examples/features.json");
959
        features_cache.insert("development".into(), client_features.clone());
960
        features_cache.insert("production".into(), example_features.clone());
961
        let mut token = EdgeToken::try_from(
962
            "*:development.03fa5f506428fe80ed5640c351c7232e38940814d2923b08f5c05fa7".to_string(),
963
        )
964
        .unwrap();
965
        token.token_type = Some(TokenType::Client);
966
        token.status = TokenValidationStatus::Validated;
967
        token_cache.insert(token.token.clone(), token.clone());
968
        let req = make_features_request_with_token(token.clone()).await;
969
        let res: ClientFeatures = test::call_and_read_body_json(&app, req).await;
970
        assert_eq!(res.features, client_features.features);
971
        let mut production_token = EdgeToken::try_from(
972
            "*:production.03fa5f506428fe80ed5640c351c7232e38940814d2923b08f5c05fa7".to_string(),
973
        )
974
        .unwrap();
975
        production_token.token_type = Some(TokenType::Client);
976
        production_token.status = TokenValidationStatus::Validated;
977
        token_cache.insert(production_token.token.clone(), production_token.clone());
978

979
        let post_req = test::TestRequest::post()
980
            .uri("/api/client/features")
981
            .insert_header(("Authorization", production_token.clone().token))
982
            .insert_header(ContentType::json())
983
            .to_request();
984

985
        let get_req = make_features_request_with_token(production_token.clone()).await;
986
        let get_res: ClientFeatures = test::call_and_read_body_json(&app, get_req).await;
987
        let post_res: ClientFeatures = test::call_and_read_body_json(&app, post_req).await;
988

989
        assert_eq!(get_res.features, post_res.features)
990
    }
991

992
    #[tokio::test]
993
    async fn client_features_endpoint_filters_on_project_access_in_token() {
994
        let features_cache = Arc::new(FeatureCache::default());
995
        let token_cache: Arc<DashMap<String, EdgeToken>> = Arc::new(DashMap::default());
996
        let app = test::init_service(
997
            App::new()
998
                .app_data(Data::from(features_cache.clone()))
999
                .app_data(Data::from(token_cache.clone()))
1000
                .service(web::scope("/api/client").service(get_features)),
1001
        )
1002
        .await;
1003
        let mut edge_token = EdgeToken::try_from(
1004
            "demo-app:production.03fa5f506428fe80ed5640c351c7232e38940814d2923b08f5c05fa7"
1005
                .to_string(),
1006
        )
1007
        .unwrap();
1008
        edge_token.token_type = Some(TokenType::Client);
1009
        token_cache.insert(edge_token.token.clone(), edge_token.clone());
1010
        let example_features = features_from_disk("../examples/features.json");
1011
        features_cache.insert("production".into(), example_features.clone());
1012
        let req = make_features_request_with_token(edge_token.clone()).await;
1013
        let res: ClientFeatures = test::call_and_read_body_json(&app, req).await;
1014
        assert_eq!(res.features.len(), 5);
1015
        assert!(res
1016
            .features
1017
            .iter()
1018
            .all(|t| t.project == Some("demo-app".into())));
1019
    }
1020

1021
    #[tokio::test]
1022
    async fn client_features_endpoint_filters_when_multiple_projects_in_token() {
1023
        let features_cache = Arc::new(FeatureCache::default());
1024
        let token_cache: Arc<DashMap<String, EdgeToken>> = Arc::new(DashMap::default());
1025
        let app = test::init_service(
1026
            App::new()
1027
                .app_data(Data::from(features_cache.clone()))
1028
                .app_data(Data::from(token_cache.clone()))
1029
                .service(web::scope("/api/client").service(get_features)),
1030
        )
1031
        .await;
1032
        let mut token =
1033
            EdgeToken::try_from("[]:production.puff_the_magic_dragon".to_string()).unwrap();
1034
        token.projects = vec!["dx".into(), "eg".into(), "unleash-cloud".into()];
1035
        token.status = TokenValidationStatus::Validated;
1036
        token.token_type = Some(TokenType::Client);
1037
        token_cache.insert(token.token.clone(), token.clone());
1038
        let example_features = features_from_disk("../examples/hostedexample.json");
1039
        features_cache.insert("production".into(), example_features.clone());
1040
        let req = make_features_request_with_token(token.clone()).await;
1041
        let res: ClientFeatures = test::call_and_read_body_json(&app, req).await;
1042
        assert_eq!(res.features.len(), 24);
1043
        assert!(res
1044
            .features
1045
            .iter()
1046
            .all(|f| token.projects.contains(&f.project.clone().unwrap())));
1047
    }
1048

1049
    #[tokio::test]
1050
    async fn client_features_endpoint_filters_correctly_when_token_has_access_to_multiple_projects()
1051
    {
1052
        let features_cache = Arc::new(FeatureCache::default());
1053
        let token_cache: Arc<DashMap<String, EdgeToken>> = Arc::new(DashMap::default());
1054
        let app = test::init_service(
1055
            App::new()
1056
                .app_data(Data::from(features_cache.clone()))
1057
                .app_data(Data::from(token_cache.clone()))
1058
                .service(web::scope("/api/client").service(get_features)),
1059
        )
1060
        .await;
1061

1062
        let mut token_a =
1063
            EdgeToken::try_from("[]:production.puff_the_magic_dragon".to_string()).unwrap();
1064
        token_a.projects = vec!["dx".into(), "eg".into()];
1065
        token_a.status = TokenValidationStatus::Validated;
1066
        token_a.token_type = Some(TokenType::Client);
1067
        token_cache.insert(token_a.token.clone(), token_a.clone());
1068

1069
        let mut token_b =
1070
            EdgeToken::try_from("[]:production.biff_the_magic_flagon".to_string()).unwrap();
1071
        token_b.projects = vec!["unleash-cloud".into()];
1072
        token_b.status = TokenValidationStatus::Validated;
1073
        token_b.token_type = Some(TokenType::Client);
1074
        token_cache.insert(token_b.token.clone(), token_b.clone());
1075

1076
        let example_features = features_from_disk("../examples/hostedexample.json");
1077
        features_cache.insert("production".into(), example_features.clone());
1078

1079
        let req_1 = make_features_request_with_token(token_a.clone()).await;
1080
        let res_1: ClientFeatures = test::call_and_read_body_json(&app, req_1).await;
1081
        assert!(res_1
1082
            .features
1083
            .iter()
1084
            .all(|f| token_a.projects.contains(&f.project.clone().unwrap())));
1085

1086
        let req_2 = make_features_request_with_token(token_b.clone()).await;
1087
        let res_2: ClientFeatures = test::call_and_read_body_json(&app, req_2).await;
1088
        assert!(res_2
1089
            .features
1090
            .iter()
1091
            .all(|f| token_b.projects.contains(&f.project.clone().unwrap())));
1092

1093
        let req_3 = make_features_request_with_token(token_a.clone()).await;
1094
        let res_3: ClientFeatures = test::call_and_read_body_json(&app, req_3).await;
1095
        assert!(res_3
1096
            .features
1097
            .iter()
1098
            .all(|f| token_a.projects.contains(&f.project.clone().unwrap())));
1099
    }
1100

1101
    #[tokio::test]
1102
    async fn when_running_in_offline_mode_with_proxy_key_should_not_filter_features() {
1103
        let features_cache = Arc::new(FeatureCache::default());
1104
        let token_cache: Arc<DashMap<String, EdgeToken>> = Arc::new(DashMap::default());
1105
        let app = test::init_service(
1106
            App::new()
1107
                .app_data(Data::from(features_cache.clone()))
1108
                .app_data(Data::from(token_cache.clone()))
1109
                .app_data(Data::new(crate::cli::EdgeMode::Offline(OfflineArgs {
1110
                    bootstrap_file: Some(PathBuf::from("../examples/features.json")),
1111
                    tokens: vec!["secret_123".into()],
1112
                    client_tokens: vec![],
1113
                    frontend_tokens: vec![],
1114
                    reload_interval: 0,
1115
                })))
1116
                .service(web::scope("/api/client").service(get_features)),
1117
        )
1118
        .await;
1119
        let token = EdgeToken::offline_token("secret-123");
1120
        token_cache.insert(token.token.clone(), token.clone());
1121
        let example_features = features_from_disk("../examples/features.json");
1122
        features_cache.insert(token.token.clone(), example_features.clone());
1123
        let req = make_features_request_with_token(token.clone()).await;
1124
        let res: ClientFeatures = test::call_and_read_body_json(&app, req).await;
1125
        assert_eq!(res.features.len(), example_features.features.len());
1126
    }
1127

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

1195
    #[tokio::test]
1196
    async fn calling_client_features_endpoint_with_new_token_does_not_hydrate_when_strict() {
1197
        let upstream_features_cache = Arc::new(FeatureCache::default());
1198
        let upstream_token_cache: Arc<DashMap<String, EdgeToken>> = Arc::new(DashMap::default());
1199
        let upstream_delta_cache: Arc<DashMap<String, DeltaCache>> = Arc::new(DashMap::default());
1200
        let upstream_engine_cache: Arc<DashMap<String, EngineState>> = Arc::new(DashMap::default());
1201
        let server = upstream_server(
1202
            upstream_token_cache.clone(),
1203
            upstream_features_cache.clone(),
1204
            upstream_delta_cache.clone(),
1205
            upstream_engine_cache.clone(),
1206
        )
1207
        .await;
1208
        let upstream_features = features_from_disk("../examples/hostedexample.json");
1209
        let mut upstream_known_token = EdgeToken::from_str("dx:development.secret123").unwrap();
1210
        upstream_known_token.status = TokenValidationStatus::Validated;
1211
        upstream_known_token.token_type = Some(TokenType::Client);
1212
        upstream_token_cache.insert(
1213
            upstream_known_token.token.clone(),
1214
            upstream_known_token.clone(),
1215
        );
1216
        upstream_features_cache.insert(cache_key(&upstream_known_token), upstream_features.clone());
1217
        let unleash_client = Arc::new(UnleashClient::new(server.url("/").as_str(), None).unwrap());
1218
        let features_cache: Arc<FeatureCache> = Arc::new(FeatureCache::default());
1219
        let token_cache: Arc<DashMap<String, EdgeToken>> = Arc::new(DashMap::default());
1220
        let engine_cache: Arc<DashMap<String, EngineState>> = Arc::new(DashMap::default());
1221
        let feature_refresher = Arc::new(FeatureRefresher {
1222
            unleash_client: unleash_client.clone(),
1223
            features_cache: features_cache.clone(),
1224
            engine_cache: engine_cache.clone(),
1225
            refresh_interval: Duration::seconds(6000),
1226
            ..Default::default()
1227
        });
1228
        let token_validator = Arc::new(TokenValidator {
1229
            unleash_client: unleash_client.clone(),
1230
            token_cache: token_cache.clone(),
1231
            persistence: None,
1232
        });
1233
        let local_app = test::init_service(
1234
            App::new()
1235
                .app_data(Data::from(token_validator.clone()))
1236
                .app_data(Data::from(features_cache.clone()))
1237
                .app_data(Data::from(engine_cache.clone()))
1238
                .app_data(Data::from(token_cache.clone()))
1239
                .app_data(Data::from(feature_refresher.clone()))
1240
                .wrap(middleware::as_async_middleware::as_async_middleware(
1241
                    middleware::validate_token::validate_token,
1242
                ))
1243
                .service(web::scope("/api").configure(configure_client_api)),
1244
        )
1245
        .await;
1246
        let req = test::TestRequest::get()
1247
            .uri("/api/client/features")
1248
            .insert_header(ContentType::json())
1249
            .insert_header(("Authorization", upstream_known_token.token.clone()))
1250
            .to_request();
1251
        let res = test::call_service(&local_app, req).await;
1252
        assert_eq!(res.status(), StatusCode::FORBIDDEN);
1253
    }
1254

1255
    #[tokio::test]
1256
    pub async fn gets_feature_by_name() {
1257
        let features_cache = Arc::new(FeatureCache::default());
1258
        let token_cache: Arc<DashMap<String, EdgeToken>> = Arc::new(DashMap::default());
1259
        let engine_cache: Arc<DashMap<String, EngineState>> = Arc::new(DashMap::default());
1260
        let features = features_from_disk("../examples/hostedexample.json");
1261
        let mut dx_token = EdgeToken::from_str("dx:development.secret123").unwrap();
1262
        dx_token.status = TokenValidationStatus::Validated;
1263
        dx_token.token_type = Some(TokenType::Client);
1264
        token_cache.insert(dx_token.token.clone(), dx_token.clone());
1265
        features_cache.insert(cache_key(&dx_token), features.clone());
1266
        let local_app = test::init_service(
1267
            App::new()
1268
                .app_data(Data::from(features_cache.clone()))
1269
                .app_data(Data::from(engine_cache.clone()))
1270
                .app_data(Data::from(token_cache.clone()))
1271
                .wrap(middleware::as_async_middleware::as_async_middleware(
1272
                    middleware::validate_token::validate_token,
1273
                ))
1274
                .service(web::scope("/api").configure(configure_client_api)),
1275
        )
1276
        .await;
1277
        let desired_toggle = "projectStatusApi";
1278
        let request = test::TestRequest::get()
1279
            .uri(format!("/api/client/features/{desired_toggle}").as_str())
1280
            .insert_header(ContentType::json())
1281
            .insert_header(("Authorization", dx_token.token.clone()))
1282
            .to_request();
1283
        let result: ClientFeature = test::call_and_read_body_json(&local_app, request).await;
1284
        assert_eq!(result.name, desired_toggle);
1285
    }
1286

1287
    #[tokio::test]
1288
    pub async fn token_with_no_access_to_named_feature_yields_404() {
1289
        let features_cache = Arc::new(FeatureCache::default());
1290
        let token_cache: Arc<DashMap<String, EdgeToken>> = Arc::new(DashMap::default());
1291
        let engine_cache: Arc<DashMap<String, EngineState>> = Arc::new(DashMap::default());
1292
        let features = features_from_disk("../examples/hostedexample.json");
1293
        let mut dx_token = EdgeToken::from_str("dx:development.secret123").unwrap();
1294
        dx_token.status = TokenValidationStatus::Validated;
1295
        dx_token.token_type = Some(TokenType::Client);
1296
        token_cache.insert(dx_token.token.clone(), dx_token.clone());
1297
        features_cache.insert(cache_key(&dx_token), features.clone());
1298
        let local_app = test::init_service(
1299
            App::new()
1300
                .app_data(Data::from(features_cache.clone()))
1301
                .app_data(Data::from(engine_cache.clone()))
1302
                .app_data(Data::from(token_cache.clone()))
1303
                .wrap(middleware::as_async_middleware::as_async_middleware(
1304
                    middleware::validate_token::validate_token,
1305
                ))
1306
                .service(web::scope("/api").configure(configure_client_api)),
1307
        )
1308
        .await;
1309
        let desired_toggle = "serviceAccounts";
1310
        let request = test::TestRequest::get()
1311
            .uri(format!("/api/client/features/{desired_toggle}").as_str())
1312
            .insert_header(ContentType::json())
1313
            .insert_header(("Authorization", dx_token.token.clone()))
1314
            .to_request();
1315
        let result = test::call_service(&local_app, request).await;
1316
        assert_eq!(result.status(), StatusCode::NOT_FOUND);
1317
    }
1318
    #[tokio::test]
1319
    pub async fn still_subsumes_tokens_after_moving_registration_to_initial_hydration_when_dynamic()
1320
    {
1321
        let upstream_features_cache: Arc<FeatureCache> = Arc::new(FeatureCache::default());
1322
        let upstream_token_cache: Arc<DashMap<String, EdgeToken>> = Arc::new(DashMap::default());
1323
        let upstream_delta_cache: Arc<DashMap<String, DeltaCache>> = Arc::new(DashMap::default());
1324
        let upstream_engine_cache: Arc<DashMap<String, EngineState>> = Arc::new(DashMap::default());
1325
        let server = upstream_server(
1326
            upstream_token_cache.clone(),
1327
            upstream_features_cache.clone(),
1328
            upstream_delta_cache.clone(),
1329
            upstream_engine_cache.clone(),
1330
        )
1331
        .await;
1332
        let upstream_features = features_from_disk("../examples/hostedexample.json");
1333
        let mut upstream_dx_token = EdgeToken::from_str("dx:development.secret123").unwrap();
1334
        upstream_dx_token.status = TokenValidationStatus::Validated;
1335
        upstream_dx_token.token_type = Some(TokenType::Client);
1336
        upstream_token_cache.insert(upstream_dx_token.token.clone(), upstream_dx_token.clone());
1337
        let mut upstream_eg_token = EdgeToken::from_str("eg:development.secret321").unwrap();
1338
        upstream_eg_token.status = TokenValidationStatus::Validated;
1339
        upstream_eg_token.token_type = Some(TokenType::Client);
1340
        upstream_token_cache.insert(upstream_eg_token.token.clone(), upstream_eg_token.clone());
1341
        upstream_features_cache.insert(cache_key(&upstream_dx_token), upstream_features.clone());
1342
        let unleash_client = Arc::new(UnleashClient::new(server.url("/").as_str(), None).unwrap());
1343
        let features_cache: Arc<FeatureCache> = Arc::new(FeatureCache::default());
1344
        let token_cache: Arc<DashMap<String, EdgeToken>> = Arc::new(DashMap::default());
1345
        let engine_cache: Arc<DashMap<String, EngineState>> = Arc::new(DashMap::default());
1346
        let feature_refresher = Arc::new(FeatureRefresher {
1347
            unleash_client: unleash_client.clone(),
1348
            features_cache: features_cache.clone(),
1349
            engine_cache: engine_cache.clone(),
1350
            refresh_interval: Duration::seconds(6000),
1351
            strict: false,
1352
            ..Default::default()
1353
        });
1354
        let token_validator = Arc::new(TokenValidator {
1355
            unleash_client: unleash_client.clone(),
1356
            token_cache: token_cache.clone(),
1357
            persistence: None,
1358
        });
1359
        let local_app = test::init_service(
1360
            App::new()
1361
                .app_data(Data::from(token_validator.clone()))
1362
                .app_data(Data::from(features_cache.clone()))
1363
                .app_data(Data::from(engine_cache.clone()))
1364
                .app_data(Data::from(token_cache.clone()))
1365
                .app_data(Data::from(feature_refresher.clone()))
1366
                .wrap(middleware::as_async_middleware::as_async_middleware(
1367
                    middleware::validate_token::validate_token,
1368
                ))
1369
                .service(web::scope("/api").configure(configure_client_api)),
1370
        )
1371
        .await;
1372
        let dx_req = test::TestRequest::get()
1373
            .uri("/api/client/features")
1374
            .insert_header(ContentType::json())
1375
            .insert_header(("Authorization", upstream_dx_token.token.clone()))
1376
            .to_request();
1377
        let res: ClientFeatures = test::call_and_read_body_json(&local_app, dx_req).await;
1378
        assert!(!res.features.is_empty());
1379
        let eg_req = test::TestRequest::get()
1380
            .uri("/api/client/features")
1381
            .insert_header(ContentType::json())
1382
            .insert_header(("Authorization", upstream_eg_token.token.clone()))
1383
            .to_request();
1384
        let eg_res: ClientFeatures = test::call_and_read_body_json(&local_app, eg_req).await;
1385
        assert!(!eg_res.features.is_empty());
1386
        assert_eq!(feature_refresher.tokens_to_refresh.len(), 2);
1387
        assert_eq!(features_cache.len(), 1);
1388
    }
1389

1390
    #[tokio::test]
1391
    pub async fn can_filter_features_list_by_name_prefix() {
1392
        let features_cache = Arc::new(FeatureCache::default());
1393
        let token_cache: Arc<DashMap<String, EdgeToken>> = Arc::new(DashMap::default());
1394
        let engine_cache: Arc<DashMap<String, EngineState>> = Arc::new(DashMap::default());
1395
        let features = features_from_disk("../examples/hostedexample.json");
1396
        let mut dx_token = EdgeToken::from_str("dx:development.secret123").unwrap();
1397
        dx_token.status = TokenValidationStatus::Validated;
1398
        dx_token.token_type = Some(TokenType::Client);
1399
        token_cache.insert(dx_token.token.clone(), dx_token.clone());
1400
        features_cache.insert(cache_key(&dx_token), features.clone());
1401
        let local_app = test::init_service(
1402
            App::new()
1403
                .app_data(Data::from(features_cache.clone()))
1404
                .app_data(Data::from(engine_cache.clone()))
1405
                .app_data(Data::from(token_cache.clone()))
1406
                .wrap(middleware::as_async_middleware::as_async_middleware(
1407
                    middleware::validate_token::validate_token,
1408
                ))
1409
                .service(web::scope("/api").configure(configure_client_api)),
1410
        )
1411
        .await;
1412
        let request = test::TestRequest::get()
1413
            .uri("/api/client/features?namePrefix=embed")
1414
            .insert_header(ContentType::json())
1415
            .insert_header(("Authorization", dx_token.token.clone()))
1416
            .to_request();
1417
        let result: ClientFeatures = test::call_and_read_body_json(&local_app, request).await;
1418
        assert_eq!(result.features.len(), 2);
1419
        assert_eq!(result.query.unwrap().name_prefix.unwrap(), "embed");
1420
    }
1421

1422
    #[tokio::test]
1423
    pub async fn only_gets_correct_feature_by_name() {
1424
        let features_cache = Arc::new(FeatureCache::default());
1425
        let token_cache: Arc<DashMap<String, EdgeToken>> = Arc::new(DashMap::default());
1426
        let engine_cache: Arc<DashMap<String, EngineState>> = Arc::new(DashMap::default());
1427
        let features = ClientFeatures {
1428
            version: 2,
1429
            query: None,
1430
            features: vec![
1431
                ClientFeature {
1432
                    name: "edge-flag-1".into(),
1433
                    feature_type: None,
1434
                    dependencies: None,
1435
                    description: None,
1436
                    created_at: None,
1437
                    last_seen_at: None,
1438
                    enabled: true,
1439
                    stale: None,
1440
                    impression_data: None,
1441
                    project: Some("dx".into()),
1442
                    strategies: None,
1443
                    variants: None,
1444
                },
1445
                ClientFeature {
1446
                    name: "edge-flag-3".into(),
1447
                    feature_type: None,
1448
                    dependencies: None,
1449
                    description: None,
1450
                    created_at: None,
1451
                    last_seen_at: None,
1452
                    enabled: true,
1453
                    stale: None,
1454
                    impression_data: None,
1455
                    project: Some("eg".into()),
1456
                    strategies: None,
1457
                    variants: None,
1458
                },
1459
            ],
1460
            segments: None,
1461
            meta: None,
1462
        };
1463
        let mut dx_token = EdgeToken::from_str("dx:development.secret123").unwrap();
1464
        dx_token.status = TokenValidationStatus::Validated;
1465
        dx_token.token_type = Some(TokenType::Client);
1466
        let mut eg_token = EdgeToken::from_str("eg:development.secret123").unwrap();
1467
        eg_token.status = TokenValidationStatus::Validated;
1468
        eg_token.token_type = Some(TokenType::Client);
1469
        token_cache.insert(dx_token.token.clone(), dx_token.clone());
1470
        token_cache.insert(eg_token.token.clone(), eg_token.clone());
1471
        features_cache.insert(cache_key(&dx_token), features.clone());
1472
        let local_app = test::init_service(
1473
            App::new()
1474
                .app_data(Data::from(features_cache.clone()))
1475
                .app_data(Data::from(engine_cache.clone()))
1476
                .app_data(Data::from(token_cache.clone()))
1477
                .wrap(middleware::as_async_middleware::as_async_middleware(
1478
                    middleware::validate_token::validate_token,
1479
                ))
1480
                .service(web::scope("/api").configure(configure_client_api)),
1481
        )
1482
        .await;
1483
        let successful_request = test::TestRequest::get()
1484
            .uri("/api/client/features/edge-flag-3")
1485
            .insert_header(ContentType::json())
1486
            .insert_header(("Authorization", eg_token.token.clone()))
1487
            .to_request();
1488
        let res = test::call_service(&local_app, successful_request).await;
1489
        assert_eq!(res.status(), StatusCode::OK);
1490
        let request = test::TestRequest::get()
1491
            .uri("/api/client/features/edge-flag-3")
1492
            .insert_header(ContentType::json())
1493
            .insert_header(("Authorization", dx_token.token.clone()))
1494
            .to_request();
1495
        let res = test::call_service(&local_app, request).await;
1496
        assert_eq!(res.status(), StatusCode::NOT_FOUND);
1497
    }
1498

1499
    #[tokio::test]
1500
    async fn client_features_endpoint_works_with_overridden_token_header() {
1501
        let features_cache = Arc::new(FeatureCache::default());
1502
        let token_cache: Arc<DashMap<String, EdgeToken>> = Arc::new(DashMap::default());
1503
        let token_header = TokenHeader::from_str("NeedsToBeTested").unwrap();
1504
        let app = test::init_service(
1505
            App::new()
1506
                .app_data(Data::from(features_cache.clone()))
1507
                .app_data(Data::from(token_cache.clone()))
1508
                .app_data(Data::new(token_header.clone()))
1509
                .service(web::scope("/api/client").service(get_features)),
1510
        )
1511
        .await;
1512
        let client_features = cached_client_features();
1513
        let example_features = features_from_disk("../examples/features.json");
1514
        features_cache.insert("development".into(), client_features.clone());
1515
        features_cache.insert("production".into(), example_features.clone());
1516
        let mut production_token = EdgeToken::try_from(
1517
            "*:production.03fa5f506428fe80ed5640c351c7232e38940814d2923b08f5c05fa7".to_string(),
1518
        )
1519
        .unwrap();
1520
        production_token.token_type = Some(TokenType::Client);
1521
        production_token.status = TokenValidationStatus::Validated;
1522
        token_cache.insert(production_token.token.clone(), production_token.clone());
1523

1524
        let request = test::TestRequest::get()
1525
            .uri("/api/client/features")
1526
            .insert_header(ContentType::json())
1527
            .insert_header(("NeedsToBeTested", production_token.token.clone()))
1528
            .to_request();
1529
        let res = test::call_service(&app, request).await;
1530
        assert_eq!(res.status(), StatusCode::OK);
1531
        let request = test::TestRequest::get()
1532
            .uri("/api/client/features")
1533
            .insert_header(ContentType::json())
1534
            .insert_header(("ShouldNotWork", production_token.token.clone()))
1535
            .to_request();
1536
        let res = test::call_service(&app, request).await;
1537
        assert_eq!(res.status(), StatusCode::FORBIDDEN);
1538
    }
1539
}
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