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

Unleash / unleash-edge / #1716

04 Mar 2025 12:56PM UTC coverage: 66.966% (-0.01%) from 66.979%
#1716

push

web-flow
chore: update to rust 1.85 and 2024 edition (#791)

* chore(rust): Update to 1.85 as rust-version and 2024 edition

* fix: edition 2024 changed impl Trait types. (thank you clippy/rustc)

---------

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>

1790 of 2673 relevant lines covered (66.97%)

1.79 hits per line

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

85.94
/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
3✔
47
}
48

49
#[get("/delta")]
50
pub async fn get_delta(
1✔
51
    edge_token: EdgeToken,
52
    token_cache: Data<DashMap<String, EdgeToken>>,
53
    filter_query: Query<FeatureFilters>,
54
    req: HttpRequest,
55
) -> impl Responder {
56
    let requested_revision_id = req
2✔
57
        .headers()
58
        .get("If-None-Match")
59
        .and_then(|value| value.to_str().ok())
2✔
60
        .and_then(|etag| etag.trim_matches('"').parse::<u32>().ok())
2✔
61
        .unwrap_or(0);
62

63
    match resolve_delta(
6✔
64
        edge_token,
1✔
65
        token_cache,
1✔
66
        filter_query,
1✔
67
        requested_revision_id,
68
        req,
1✔
69
    )
70
    .await
4✔
71
    {
72
        Ok(Json(None)) => HttpResponse::NotModified().finish(),
2✔
73
        Ok(Json(Some(delta))) => {
1✔
74
            let last_event_id = delta.events.last().map(|e| e.get_event_id()).unwrap_or(0); // should never occur
6✔
75

76
            HttpResponse::Ok()
6✔
77
                .insert_header(("ETag", format!("{}", last_event_id)))
4✔
78
                .json(delta)
2✔
79
        }
80
        Err(err) => HttpResponse::InternalServerError().body(format!("Error: {:?}", err)),
×
81
    }
82
}
83

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

99
            broadcaster.connect(validated_token, query).await
2✔
100
        }
101
        _ => Err(EdgeError::Forbidden(
×
102
            "This endpoint is only enabled in streaming mode".into(),
×
103
        )),
104
    }
105
}
106

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

130
fn get_feature_filter(
2✔
131
    edge_token: &EdgeToken,
132
    token_cache: &Data<DashMap<String, EdgeToken>>,
133
    filter_query: Query<FeatureFilters>,
134
) -> EdgeResult<(
135
    EdgeToken,
136
    FeatureFilterSet,
137
    unleash_types::client_features::Query,
138
)> {
139
    let validated_token = token_cache
6✔
140
        .get(&edge_token.token)
141
        .map(|e| e.value().clone())
4✔
142
        .ok_or(EdgeError::AuthorizationDenied)?;
2✔
143

144
    let query_filters = filter_query.into_inner();
4✔
145
    let query = unleash_types::client_features::Query {
146
        tags: None,
147
        projects: Some(validated_token.projects.clone()),
4✔
148
        name_prefix: query_filters.name_prefix.clone(),
2✔
149
        environment: validated_token.environment.clone(),
2✔
150
        inline_segment_constraints: Some(false),
2✔
151
    };
152

153
    let filter_set = if let Some(name_prefix) = query_filters.name_prefix {
4✔
154
        FeatureFilterSet::from(Box::new(name_prefix_filter(name_prefix)))
2✔
155
    } else {
156
        FeatureFilterSet::default()
4✔
157
    }
158
    .with_filter(project_filter(&validated_token));
4✔
159

160
    Ok((validated_token, filter_set, query))
2✔
161
}
162

163
fn get_delta_filter(
1✔
164
    edge_token: &EdgeToken,
165
    token_cache: &Data<DashMap<String, EdgeToken>>,
166
    filter_query: Query<FeatureFilters>,
167
    requested_revision_id: u32,
168
) -> EdgeResult<DeltaFilterSet> {
169
    let validated_token = token_cache
4✔
170
        .get(&edge_token.token)
171
        .map(|e| e.value().clone())
2✔
172
        .ok_or(EdgeError::AuthorizationDenied)?;
1✔
173

174
    let query_filters = filter_query.into_inner();
2✔
175

176
    let delta_filter_set = DeltaFilterSet::default().with_filter(combined_filter(
7✔
177
        requested_revision_id,
178
        validated_token.projects.clone(),
4✔
179
        query_filters.name_prefix.clone(),
2✔
180
    ));
181

182
    Ok(delta_filter_set)
1✔
183
}
184

185
async fn resolve_features(
1✔
186
    edge_token: EdgeToken,
187
    features_cache: Data<FeatureCache>,
188
    token_cache: Data<DashMap<String, EdgeToken>>,
189
    filter_query: Query<FeatureFilters>,
190
    req: HttpRequest,
191
) -> EdgeJsonResult<ClientFeatures> {
192
    let (validated_token, filter_set, query) =
2✔
193
        get_feature_filter(&edge_token, &token_cache, filter_query.clone())?;
194

195
    let client_features = match req.app_data::<Data<FeatureRefresher>>() {
5✔
196
        Some(refresher) => {
1✔
197
            refresher
5✔
198
                .features_for_filter(validated_token.clone(), &filter_set)
1✔
199
                .await
4✔
200
        }
201
        None => features_cache
7✔
202
            .get(&cache_key(&validated_token))
1✔
203
            .map(|client_features| filter_client_features(&client_features, &filter_set))
3✔
204
            .ok_or(EdgeError::ClientCacheError),
6✔
205
    }?;
206

207
    Ok(Json(ClientFeatures {
3✔
208
        query: Some(query),
3✔
209
        ..client_features
210
    }))
211
}
212
async fn resolve_delta(
1✔
213
    edge_token: EdgeToken,
214
    token_cache: Data<DashMap<String, EdgeToken>>,
215
    filter_query: Query<FeatureFilters>,
216
    requested_revision_id: u32,
217
    req: HttpRequest,
218
) -> EdgeJsonResult<Option<ClientFeaturesDelta>> {
219
    let (validated_token, filter_set, ..) =
1✔
220
        get_feature_filter(&edge_token, &token_cache, filter_query.clone())?;
221

222
    let delta_filter_set = get_delta_filter(
223
        &edge_token,
1✔
224
        &token_cache,
1✔
225
        filter_query.clone(),
1✔
226
        requested_revision_id,
227
    )?;
228

229
    let refresher = req.app_data::<Data<FeatureRefresher>>().ok_or_else(|| {
2✔
230
        EdgeError::ClientHydrationFailed(
×
231
            "FeatureRefresher is missing - cannot resolve delta in offline mode".to_string(),
×
232
        )
233
    })?;
234

235
    let delta = refresher
11✔
236
        .delta_events_for_filter(
237
            validated_token.clone(),
2✔
238
            &filter_set,
2✔
239
            &delta_filter_set,
2✔
240
            requested_revision_id,
241
        )
242
        .await?;
6✔
243

244
    if delta.events.is_empty() {
3✔
245
        return Ok(Json(None));
1✔
246
    }
247

248
    Ok(Json(Some(delta)))
1✔
249
}
250

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

277
    let filter_set = FeatureFilterSet::from(Box::new(name_match_filter(feature_name.clone())))
8✔
278
        .with_filter(project_filter(&validated_token));
4✔
279

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

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

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

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

377
#[post("/metrics/edge")]
378
#[instrument(skip(_edge_token, instance_data, connected_instances))]
379
pub async fn post_edge_instance_data(
380
    _edge_token: EdgeToken,
381
    instance_data: Json<EdgeInstanceData>,
382
    instance_data_sending: Data<InstanceDataSending>,
383
    connected_instances: Data<RwLock<Vec<EdgeInstanceData>>>,
384
) -> EdgeResult<HttpResponse> {
385
    if let InstanceDataSending::SendInstanceData(_) = instance_data_sending.as_ref() {
×
386
        connected_instances
×
387
            .write()
388
            .await
×
389
            .push(instance_data.into_inner());
×
390
    }
391
    Ok(HttpResponse::Accepted().finish())
×
392
}
393

394
pub fn configure_client_api(cfg: &mut web::ServiceConfig) {
3✔
395
    let client_scope = web::scope("/client")
7✔
396
        .wrap(crate::middleware::as_async_middleware::as_async_middleware(
3✔
397
            crate::middleware::validate_token::validate_token,
398
        ))
399
        .service(get_features)
400
        .service(get_delta)
401
        .service(get_feature)
402
        .service(register)
403
        .service(metrics)
404
        .service(post_bulk_metrics)
405
        .service(stream_features)
406
        .service(post_edge_instance_data);
407

408
    cfg.service(client_scope);
2✔
409
}
410

411
pub fn configure_experimental_post_features(
×
412
    cfg: &mut web::ServiceConfig,
413
    post_features_enabled: bool,
414
) {
415
    if post_features_enabled {
×
416
        cfg.service(post_features);
×
417
    }
418
}
419

420
#[cfg(test)]
421
mod tests {
422

423
    use crate::metrics::client_metrics::{ApplicationKey, MetricsBatch, MetricsKey};
424
    use crate::types::{TokenType, TokenValidationStatus};
425
    use std::collections::HashMap;
426
    use std::path::PathBuf;
427
    use std::str::FromStr;
428
    use std::sync::Arc;
429

430
    use super::*;
431

432
    use crate::auth::token_validator::TokenValidator;
433
    use crate::cli::{OfflineArgs, TokenHeader};
434
    use crate::delta_cache::{DeltaCache, DeltaHydrationEvent};
435
    use crate::delta_cache_manager::DeltaCacheManager;
436
    use crate::http::unleash_client::{ClientMetaInformation, UnleashClient};
437
    use crate::middleware;
438
    use crate::tests::{features_from_disk, upstream_server};
439
    use actix_http::{Request, StatusCode};
440
    use actix_web::{
441
        http::header::ContentType,
442
        test,
443
        web::{self, Data},
444
        App, ResponseError,
445
    };
446
    use chrono::{DateTime, Duration, TimeZone, Utc};
447
    use maplit::hashmap;
448
    use ulid::Ulid;
449
    use unleash_types::client_features::{
450
        ClientFeature, Constraint, DeltaEvent, Operator, Strategy, StrategyVariant,
451
    };
452
    use unleash_types::client_metrics::{
453
        ClientMetricsEnv, ConnectViaBuilder, MetricBucket, MetricsMetadata, ToggleStats,
454
    };
455
    use unleash_yggdrasil::EngineState;
456

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

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

533
    async fn make_register_post_request(application: ClientApplication) -> Request {
534
        test::TestRequest::post()
535
            .uri("/api/client/register")
536
            .insert_header(ContentType::json())
537
            .insert_header((
538
                "Authorization",
539
                "*:development.03fa5f506428fe80ed5640c351c7232e38940814d2923b08f5c05fa7",
540
            ))
541
            .set_json(Json(application))
542
            .to_request()
543
    }
544

545
    async fn make_features_request_with_token(token: EdgeToken) -> Request {
546
        test::TestRequest::get()
547
            .uri("/api/client/features")
548
            .insert_header(("Authorization", token.token))
549
            .to_request()
550
    }
551

552
    async fn make_delta_request_with_token(token: EdgeToken) -> Request {
553
        test::TestRequest::get()
554
            .uri("/api/client/delta")
555
            .insert_header(("Authorization", token.token))
556
            .to_request()
557
    }
558

559
    async fn make_delta_request_with_token_and_etag(token: EdgeToken, etag: &str) -> Request {
560
        test::TestRequest::get()
561
            .uri("/api/client/delta")
562
            .insert_header(("Authorization", token.token))
563
            .insert_header(("If-None-Match", etag))
564
            .to_request()
565
    }
566

567
    #[actix_web::test]
568
    async fn metrics_endpoint_correctly_aggregates_data() {
569
        let metrics_cache = Arc::new(MetricsCache::default());
570

571
        let app = test::init_service(
572
            App::new()
573
                .app_data(Data::new(ConnectVia {
574
                    app_name: "test".into(),
575
                    instance_id: Ulid::new().to_string(),
576
                }))
577
                .app_data(Data::from(metrics_cache.clone()))
578
                .service(web::scope("/api/client").service(metrics)),
579
        )
580
        .await;
581

582
        let req = make_metrics_post_request().await;
583
        let _result = test::call_and_read_body(&app, req).await;
584

585
        let cache = metrics_cache.clone();
586

587
        let found_metric = cache
588
            .metrics
589
            .get(&MetricsKey {
590
                app_name: "some-app".into(),
591
                feature_name: "some-feature".into(),
592
                timestamp: DateTime::parse_from_rfc3339("1867-11-07T12:00:00Z")
593
                    .unwrap()
594
                    .with_timezone(&Utc),
595
                environment: "development".into(),
596
            })
597
            .unwrap();
598

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

617
        assert_eq!(found_metric.yes, expected.yes);
618
        assert_eq!(found_metric.yes, 1);
619
        assert_eq!(found_metric.no, 0);
620
        assert_eq!(found_metric.no, expected.no);
621
    }
622

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

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

734
        features_cache.insert("production".into(), cached_client_features());
735
        let mut production_token = EdgeToken::try_from(
736
            "*:production.03fa5f506428fe80ed5640c351c7232e38940814d2923b08f5c05fa7".to_string(),
737
        )
738
        .unwrap();
739
        production_token.token_type = Some(TokenType::Client);
740
        production_token.status = TokenValidationStatus::Validated;
741
        token_cache.insert(production_token.token.clone(), production_token.clone());
742
        let req = make_features_request_with_token(production_token.clone()).await;
743
        let res: ClientFeatures = test::call_and_read_body_json(&app, req).await;
744

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

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

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

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

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

948
    #[tokio::test]
949
    async fn post_request_to_client_features_does_the_same_as_get_when_mounted() {
950
        let features_cache = Arc::new(FeatureCache::default());
951
        let token_cache: Arc<DashMap<String, EdgeToken>> = Arc::new(DashMap::default());
952
        let app = test::init_service(
953
            App::new()
954
                .app_data(Data::from(features_cache.clone()))
955
                .app_data(Data::from(token_cache.clone()))
956
                .service(
957
                    web::scope("/api/client")
958
                        .service(get_features)
959
                        .service(post_features),
960
                ),
961
        )
962
        .await;
963
        let client_features = cached_client_features();
964
        let example_features = features_from_disk("../examples/features.json");
965
        features_cache.insert("development".into(), client_features.clone());
966
        features_cache.insert("production".into(), example_features.clone());
967
        let mut token = EdgeToken::try_from(
968
            "*:development.03fa5f506428fe80ed5640c351c7232e38940814d2923b08f5c05fa7".to_string(),
969
        )
970
        .unwrap();
971
        token.token_type = Some(TokenType::Client);
972
        token.status = TokenValidationStatus::Validated;
973
        token_cache.insert(token.token.clone(), token.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, client_features.features);
977
        let mut production_token = EdgeToken::try_from(
978
            "*:production.03fa5f506428fe80ed5640c351c7232e38940814d2923b08f5c05fa7".to_string(),
979
        )
980
        .unwrap();
981
        production_token.token_type = Some(TokenType::Client);
982
        production_token.status = TokenValidationStatus::Validated;
983
        token_cache.insert(production_token.token.clone(), production_token.clone());
984

985
        let post_req = test::TestRequest::post()
986
            .uri("/api/client/features")
987
            .insert_header(("Authorization", production_token.clone().token))
988
            .insert_header(ContentType::json())
989
            .to_request();
990

991
        let get_req = make_features_request_with_token(production_token.clone()).await;
992
        let get_res: ClientFeatures = test::call_and_read_body_json(&app, get_req).await;
993
        let post_res: ClientFeatures = test::call_and_read_body_json(&app, post_req).await;
994

995
        assert_eq!(get_res.features, post_res.features)
996
    }
997

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

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

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

1068
        let mut token_a =
1069
            EdgeToken::try_from("[]:production.puff_the_magic_dragon".to_string()).unwrap();
1070
        token_a.projects = vec!["dx".into(), "eg".into()];
1071
        token_a.status = TokenValidationStatus::Validated;
1072
        token_a.token_type = Some(TokenType::Client);
1073
        token_cache.insert(token_a.token.clone(), token_a.clone());
1074

1075
        let mut token_b =
1076
            EdgeToken::try_from("[]:production.biff_the_magic_flagon".to_string()).unwrap();
1077
        token_b.projects = vec!["unleash-cloud".into()];
1078
        token_b.status = TokenValidationStatus::Validated;
1079
        token_b.token_type = Some(TokenType::Client);
1080
        token_cache.insert(token_b.token.clone(), token_b.clone());
1081

1082
        let example_features = features_from_disk("../examples/hostedexample.json");
1083
        features_cache.insert("production".into(), example_features.clone());
1084

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

1092
        let req_2 = make_features_request_with_token(token_b.clone()).await;
1093
        let res_2: ClientFeatures = test::call_and_read_body_json(&app, req_2).await;
1094
        assert!(res_2
1095
            .features
1096
            .iter()
1097
            .all(|f| token_b.projects.contains(&f.project.clone().unwrap())));
1098

1099
        let req_3 = make_features_request_with_token(token_a.clone()).await;
1100
        let res_3: ClientFeatures = test::call_and_read_body_json(&app, req_3).await;
1101
        assert!(res_3
1102
            .features
1103
            .iter()
1104
            .all(|f| token_a.projects.contains(&f.project.clone().unwrap())));
1105
    }
1106

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

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

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

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

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

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

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

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

1533
        let request = test::TestRequest::get()
1534
            .uri("/api/client/features")
1535
            .insert_header(ContentType::json())
1536
            .insert_header(("NeedsToBeTested", production_token.token.clone()))
1537
            .to_request();
1538
        let res = test::call_service(&app, request).await;
1539
        assert_eq!(res.status(), StatusCode::OK);
1540
        let request = test::TestRequest::get()
1541
            .uri("/api/client/features")
1542
            .insert_header(ContentType::json())
1543
            .insert_header(("ShouldNotWork", production_token.token.clone()))
1544
            .to_request();
1545
        let res = test::call_service(&app, request).await;
1546
        assert_eq!(res.status(), StatusCode::FORBIDDEN);
1547
    }
1548

1549
    async fn setup_delta_test(
1550
        initial_event_id: u32,
1551
    ) -> (
1552
        Arc<FeatureRefresher>,
1553
        Arc<DashMap<String, EdgeToken>>,
1554
        EdgeToken,
1555
        DeltaHydrationEvent,
1556
        impl actix_web::dev::Service<
1557
            actix_http::Request,
1558
            Response = actix_web::dev::ServiceResponse,
1559
            Error = actix_web::Error,
1560
        >,
1561
    ) {
1562
        let unleash_client = Arc::new(UnleashClient::new("http://localhost:9999/", None).unwrap());
1563
        let delta_cache_manager = Arc::new(DeltaCacheManager::default());
1564
        let token_cache: Arc<DashMap<String, EdgeToken>> = Arc::new(DashMap::default());
1565

1566
        let mut token = EdgeToken::try_from(
1567
            "dx:development.03fa5f506428fe80ed5640c351c7232e38940814d2923b08f5c05fa7".to_string(),
1568
        )
1569
        .unwrap();
1570
        token.token_type = Some(TokenType::Client);
1571
        token.status = TokenValidationStatus::Validated;
1572
        token_cache.insert(token.token.clone(), token.clone());
1573

1574
        let feature_refresher = Arc::new(FeatureRefresher {
1575
            unleash_client: unleash_client.clone(),
1576
            delta_cache_manager: delta_cache_manager.clone(),
1577
            tokens_to_refresh: Arc::new(Default::default()),
1578
            refresh_interval: Duration::seconds(0),
1579
            strict: false,
1580
            delta: true,
1581
            ..Default::default()
1582
        });
1583

1584
        let delta_hydration_event = DeltaHydrationEvent {
1585
            event_id: initial_event_id,
1586
            features: vec![ClientFeature {
1587
                name: "feature1".to_string(),
1588
                project: Some("dx".to_string()),
1589
                enabled: false,
1590
                ..Default::default()
1591
            }],
1592
            segments: vec![],
1593
        };
1594

1595
        let app = test::init_service(
1596
            App::new()
1597
                .app_data(Data::from(feature_refresher.clone()))
1598
                .app_data(Data::from(token_cache.clone()))
1599
                .service(web::scope("/api/client").service(get_delta)),
1600
        )
1601
        .await;
1602

1603
        delta_cache_manager.insert_cache(
1604
            "development",
1605
            DeltaCache::new(delta_hydration_event.clone(), 10),
1606
        );
1607

1608
        (
1609
            feature_refresher,
1610
            token_cache,
1611
            token,
1612
            delta_hydration_event,
1613
            app,
1614
        )
1615
    }
1616

1617
    #[tokio::test]
1618
    async fn test_delta_endpoint_returns_hydration_event() {
1619
        let (_, _, token, delta_hydration_event, app) = setup_delta_test(10).await;
1620

1621
        let req = make_delta_request_with_token(token.clone()).await;
1622
        let res: ClientFeaturesDelta = test::call_and_read_body_json(&app, req).await;
1623

1624
        assert_eq!(
1625
            res.events.first().unwrap(),
1626
            &DeltaEvent::Hydration {
1627
                event_id: delta_hydration_event.event_id,
1628
                features: delta_hydration_event.features.clone(),
1629
                segments: delta_hydration_event.segments.clone()
1630
            }
1631
        );
1632
    }
1633

1634
    #[tokio::test]
1635
    async fn test_delta_endpoint_returns_not_modified_for_matching_etag() {
1636
        let (_, _, token, _, app) = setup_delta_test(10).await;
1637

1638
        let res = test::call_service(
1639
            &app,
1640
            make_delta_request_with_token_and_etag(token.clone(), "10").await,
1641
        )
1642
        .await;
1643

1644
        assert_eq!(res.status(), StatusCode::NOT_MODIFIED);
1645
    }
1646

1647
    #[tokio::test]
1648
    async fn test_delta_endpoint_returns_not_modified_for_newer_etag() {
1649
        let (_, _, token, _, app) = setup_delta_test(10).await;
1650

1651
        let res = test::call_service(
1652
            &app,
1653
            make_delta_request_with_token_and_etag(token.clone(), "11").await,
1654
        )
1655
        .await;
1656

1657
        assert_eq!(res.status(), StatusCode::NOT_MODIFIED);
1658
    }
1659

1660
    #[tokio::test]
1661
    async fn test_delta_endpoint_returns_delta_events_after_update() {
1662
        let (feature_refresher, _, token, _, app) = setup_delta_test(10).await;
1663

1664
        let delta_event = DeltaEvent::FeatureRemoved {
1665
            event_id: 11,
1666
            feature_name: "test".to_string(),
1667
            project: "dx".to_string(),
1668
        };
1669

1670
        feature_refresher
1671
            .delta_cache_manager
1672
            .update_cache("development", &vec![delta_event.clone()]);
1673

1674
        let res: ClientFeaturesDelta = test::call_and_read_body_json(
1675
            &app,
1676
            make_delta_request_with_token_and_etag(token.clone(), "10").await,
1677
        )
1678
        .await;
1679

1680
        assert_eq!(res.events.first().unwrap(), &delta_event);
1681
        assert_eq!(res.events.len(), 1);
1682

1683
        let res = test::call_service(
1684
            &app,
1685
            make_delta_request_with_token_and_etag(token.clone(), "11").await,
1686
        )
1687
        .await;
1688

1689
        assert_eq!(res.status(), StatusCode::NOT_MODIFIED);
1690

1691
        let delta_event = DeltaEvent::SegmentRemoved {
1692
            event_id: 12,
1693
            segment_id: 1,
1694
        };
1695

1696
        feature_refresher
1697
            .delta_cache_manager
1698
            .update_cache("development", &vec![delta_event.clone()]);
1699

1700
        let res = test::call_service(
1701
            &app,
1702
            make_delta_request_with_token_and_etag(token.clone(), "12").await,
1703
        )
1704
        .await;
1705

1706
        assert_eq!(res.status(), StatusCode::NOT_MODIFIED);
1707
    }
1708

1709
    #[tokio::test]
1710
    async fn test_delta_endpoint_returns_hydration_event_when_unknown_etag_lower_than_current_event_id(
1711
    ) {
1712
        let (_, _, token, delta_hydration_event, app) = setup_delta_test(10).await;
1713

1714
        let res: ClientFeaturesDelta = test::call_and_read_body_json(
1715
            &app,
1716
            make_delta_request_with_token_and_etag(token.clone(), "8").await,
1717
        )
1718
        .await;
1719

1720
        assert_eq!(
1721
            res.events.first().unwrap(),
1722
            &DeltaEvent::Hydration {
1723
                event_id: delta_hydration_event.event_id,
1724
                features: delta_hydration_event.features.clone(),
1725
                segments: delta_hydration_event.segments.clone()
1726
            }
1727
        );
1728
    }
1729
}
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