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

Unleash / unleash-edge / #2041

04 Jun 2025 09:14AM UTC coverage: 4.469% (-63.5%) from 67.995%
#2041

push

chriswk
chore(deps): Updated zip to 3.0.0

114 of 2551 relevant lines covered (4.47%)

0.14 hits per line

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

0.0
/server/src/client_api.rs
1
use crate::cli::{EdgeArgs, EdgeMode};
2
use crate::delta_filters::{DeltaFilterSet, combined_filter};
3
use crate::error::EdgeError;
4
use crate::feature_cache::FeatureCache;
5
use crate::filters::{
6
    FeatureFilterSet, filter_client_features, name_match_filter, name_prefix_filter, project_filter,
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::Responder;
18
use actix_web::web::{self, Data, Json, Query};
19
use actix_web::{HttpRequest, HttpResponse, get, post};
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(
×
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
×
47
}
48

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 {
56
    let requested_revision_id = req
×
57
        .headers()
58
        .get("If-None-Match")
59
        .and_then(|value| value.to_str().ok())
×
60
        .and_then(|etag| etag.trim_matches('"').parse::<u32>().ok())
×
61
        .unwrap_or(0);
62

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

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

84
#[get("/streaming")]
85
pub async fn stream_features(
×
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() {
×
93
        EdgeMode::Edge(EdgeArgs {
94
            streaming: true, ..
95
        }) => {
96
            let (validated_token, _filter_set, query) =
×
97
                get_feature_filter(&edge_token, &token_cache, filter_query.clone())?;
98

99
            broadcaster.connect(validated_token, query).await
×
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(
×
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
×
128
}
129

130
fn get_feature_filter(
×
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
×
140
        .get(&edge_token.token)
141
        .map(|e| e.value().clone())
×
142
        .ok_or(EdgeError::AuthorizationDenied)?;
×
143

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

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

160
    Ok((validated_token, filter_set, query))
×
161
}
162

163
fn get_delta_filter(
×
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
×
170
        .get(&edge_token.token)
171
        .map(|e| e.value().clone())
×
172
        .ok_or(EdgeError::AuthorizationDenied)?;
×
173

174
    let query_filters = filter_query.into_inner();
×
175

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

182
    Ok(delta_filter_set)
×
183
}
184

185
async fn resolve_features(
×
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) =
×
193
        get_feature_filter(&edge_token, &token_cache, filter_query.clone())?;
194

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

207
    Ok(Json(ClientFeatures {
×
208
        query: Some(query),
×
209
        ..client_features
210
    }))
211
}
212
async fn resolve_delta(
×
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, ..) =
×
220
        get_feature_filter(&edge_token, &token_cache, filter_query.clone())?;
221

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

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

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

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

248
    Ok(Json(Some(delta)))
×
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(
×
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
×
273
        .get(&edge_token.token)
×
274
        .map(|e| e.value().clone())
×
275
        .ok_or(EdgeError::AuthorizationDenied)?;
×
276

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

280
    match req.app_data::<Data<FeatureRefresher>>() {
×
281
        Some(refresher) => {
×
282
            refresher
×
283
                .features_for_filter(validated_token.clone(), &filter_set)
×
284
                .await
×
285
        }
286
        None => features_cache
×
287
            .get(&cache_key(&validated_token))
×
288
            .map(|client_features| filter_client_features(&client_features, &filter_set))
×
289
            .ok_or(EdgeError::ClientCacheError),
×
290
    }
291
    .map(|client_features| client_features.features.into_iter().next())?
×
292
    .ok_or(EdgeError::FeatureNotFound(feature_name.into_inner()))
×
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(
×
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,
×
316
        &connect_via,
×
317
        client_application.into_inner(),
×
318
        metrics_cache,
×
319
    );
320
    Ok(HttpResponse::Accepted()
×
321
        .append_header(("X-Edge-Version", types::EDGE_VERSION))
×
322
        .finish())
×
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(
×
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,
×
344
        metrics.into_inner(),
×
345
        metrics_cache,
×
346
    );
347
    Ok(HttpResponse::Accepted().finish())
×
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(
×
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(),
×
370
        connect_via.get_ref(),
×
371
        &edge_token,
372
        bulk_metrics.into_inner(),
×
373
    );
374
    Ok(HttpResponse::Accepted().finish())
×
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) {
×
395
    let client_scope = web::scope("/client")
×
396
        .wrap(crate::middleware::as_async_middleware::as_async_middleware(
×
397
            crate::middleware::validate_token::validate_token,
398
        ))
399
        .wrap(crate::middleware::as_async_middleware::as_async_middleware(
×
400
            crate::middleware::consumption::connection_consumption,
401
        ))
402
        .service(get_features)
403
        .service(get_delta)
404
        .service(get_feature)
405
        .service(register)
406
        .service(metrics)
407
        .service(post_bulk_metrics)
408
        .service(stream_features)
409
        .service(post_edge_instance_data);
410

411
    cfg.service(client_scope);
×
412
}
413

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

423
#[cfg(test)]
424
mod tests {
425

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

433
    use super::*;
434

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

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

496
    async fn make_bulk_metrics_post_request(authorization: Option<String>) -> Request {
497
        let mut req = test::TestRequest::post()
498
            .uri("/api/client/metrics/bulk")
499
            .insert_header(ContentType::json());
500
        req = match authorization {
501
            Some(auth) => req.insert_header(("Authorization", auth)),
502
            None => req,
503
        };
504
        req.set_json(Json(BatchMetricsRequestBody {
505
            applications: vec![ClientApplication {
506
                app_name: "test_app".to_string(),
507
                connect_via: None,
508
                environment: None,
509
                projects: Some(vec![]),
510
                instance_id: None,
511
                connection_id: None,
512
                interval: 10,
513
                started: Default::default(),
514
                strategies: vec![],
515
                metadata: MetricsMetadata {
516
                    platform_name: None,
517
                    platform_version: None,
518
                    sdk_version: None,
519
                    sdk_type: 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
                    sdk_type: None,
536
                    yggdrasil_version: None,
537
                },
538
            }],
539
        }))
540
        .to_request()
541
    }
542

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

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

562
    async fn make_delta_request_with_token(token: EdgeToken) -> Request {
563
        test::TestRequest::get()
564
            .uri("/api/client/delta")
565
            .insert_header(("Authorization", token.token))
566
            .to_request()
567
    }
568

569
    async fn make_delta_request_with_token_and_etag(token: EdgeToken, etag: &str) -> Request {
570
        test::TestRequest::get()
571
            .uri("/api/client/delta")
572
            .insert_header(("Authorization", token.token))
573
            .insert_header(("If-None-Match", etag))
574
            .to_request()
575
    }
576

577
    #[actix_web::test]
578
    async fn metrics_endpoint_correctly_aggregates_data() {
579
        let metrics_cache = Arc::new(MetricsCache::default());
580

581
        let app = test::init_service(
582
            App::new()
583
                .app_data(Data::new(ConnectVia {
584
                    app_name: "test".into(),
585
                    instance_id: Ulid::new().to_string(),
586
                }))
587
                .app_data(Data::from(metrics_cache.clone()))
588
                .service(web::scope("/api/client").service(metrics)),
589
        )
590
        .await;
591

592
        let req = make_metrics_post_request().await;
593
        let _result = test::call_and_read_body(&app, req).await;
594

595
        let cache = metrics_cache.clone();
596

597
        let found_metric = cache
598
            .metrics
599
            .get(&MetricsKey {
600
                app_name: "some-app".into(),
601
                feature_name: "some-feature".into(),
602
                timestamp: DateTime::parse_from_rfc3339("1867-11-07T12:00:00Z")
603
                    .unwrap()
604
                    .with_timezone(&Utc),
605
                environment: "development".into(),
606
            })
607
            .unwrap();
608

609
        let expected = ClientMetricsEnv {
610
            app_name: "some-app".into(),
611
            feature_name: "some-feature".into(),
612
            environment: "development".into(),
613
            timestamp: DateTime::parse_from_rfc3339("1867-11-07T12:00:00Z")
614
                .unwrap()
615
                .with_timezone(&Utc),
616
            yes: 1,
617
            no: 0,
618
            variants: HashMap::new(),
619
            metadata: MetricsMetadata {
620
                platform_name: None,
621
                platform_version: None,
622
                sdk_version: None,
623
                sdk_type: None,
624
                yggdrasil_version: None,
625
            },
626
        };
627

628
        assert_eq!(found_metric.yes, expected.yes);
629
        assert_eq!(found_metric.yes, 1);
630
        assert_eq!(found_metric.no, 0);
631
        assert_eq!(found_metric.no, expected.no);
632
    }
633

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

733
    #[tokio::test]
734
    async fn response_includes_variant_stickiness_for_strategy_variants() {
735
        let features_cache = Arc::new(FeatureCache::default());
736
        let token_cache: Arc<DashMap<String, EdgeToken>> = Arc::new(DashMap::default());
737
        let app = test::init_service(
738
            App::new()
739
                .app_data(Data::from(features_cache.clone()))
740
                .app_data(Data::from(token_cache.clone()))
741
                .service(web::scope("/api/client").service(get_features)),
742
        )
743
        .await;
744

745
        features_cache.insert("production".into(), cached_client_features());
746
        let mut production_token = EdgeToken::try_from(
747
            "*:production.03fa5f506428fe80ed5640c351c7232e38940814d2923b08f5c05fa7".to_string(),
748
        )
749
        .unwrap();
750
        production_token.token_type = Some(TokenType::Client);
751
        production_token.status = TokenValidationStatus::Validated;
752
        token_cache.insert(production_token.token.clone(), production_token.clone());
753
        let req = make_features_request_with_token(production_token.clone()).await;
754
        let res: ClientFeatures = test::call_and_read_body_json(&app, req).await;
755

756
        assert_eq!(res.features.len(), cached_client_features().features.len());
757
        let strategy_variant_stickiness = res
758
            .features
759
            .iter()
760
            .find(|f| f.name == "feature_one")
761
            .unwrap()
762
            .strategies
763
            .clone()
764
            .unwrap()
765
            .iter()
766
            .find(|s| s.name == "standard")
767
            .unwrap()
768
            .variants
769
            .clone()
770
            .unwrap()
771
            .iter()
772
            .find(|v| v.name == "test")
773
            .unwrap()
774
            .stickiness
775
            .clone();
776
        assert!(strategy_variant_stickiness.is_some());
777
    }
778

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

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

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

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

959
    #[tokio::test]
960
    async fn post_request_to_client_features_does_the_same_as_get_when_mounted() {
961
        let features_cache = Arc::new(FeatureCache::default());
962
        let token_cache: Arc<DashMap<String, EdgeToken>> = Arc::new(DashMap::default());
963
        let app = test::init_service(
964
            App::new()
965
                .app_data(Data::from(features_cache.clone()))
966
                .app_data(Data::from(token_cache.clone()))
967
                .service(
968
                    web::scope("/api/client")
969
                        .service(get_features)
970
                        .service(post_features),
971
                ),
972
        )
973
        .await;
974
        let client_features = cached_client_features();
975
        let example_features = features_from_disk("../examples/features.json");
976
        features_cache.insert("development".into(), client_features.clone());
977
        features_cache.insert("production".into(), example_features.clone());
978
        let mut token = EdgeToken::try_from(
979
            "*:development.03fa5f506428fe80ed5640c351c7232e38940814d2923b08f5c05fa7".to_string(),
980
        )
981
        .unwrap();
982
        token.token_type = Some(TokenType::Client);
983
        token.status = TokenValidationStatus::Validated;
984
        token_cache.insert(token.token.clone(), token.clone());
985
        let req = make_features_request_with_token(token.clone()).await;
986
        let res: ClientFeatures = test::call_and_read_body_json(&app, req).await;
987
        assert_eq!(res.features, client_features.features);
988
        let mut production_token = EdgeToken::try_from(
989
            "*:production.03fa5f506428fe80ed5640c351c7232e38940814d2923b08f5c05fa7".to_string(),
990
        )
991
        .unwrap();
992
        production_token.token_type = Some(TokenType::Client);
993
        production_token.status = TokenValidationStatus::Validated;
994
        token_cache.insert(production_token.token.clone(), production_token.clone());
995

996
        let post_req = test::TestRequest::post()
997
            .uri("/api/client/features")
998
            .insert_header(("Authorization", production_token.clone().token))
999
            .insert_header(ContentType::json())
1000
            .to_request();
1001

1002
        let get_req = make_features_request_with_token(production_token.clone()).await;
1003
        let get_res: ClientFeatures = test::call_and_read_body_json(&app, get_req).await;
1004
        let post_res: ClientFeatures = test::call_and_read_body_json(&app, post_req).await;
1005

1006
        assert_eq!(get_res.features, post_res.features)
1007
    }
1008

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

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

1068
    #[tokio::test]
1069
    async fn client_features_endpoint_filters_correctly_when_token_has_access_to_multiple_projects()
1070
    {
1071
        let features_cache = Arc::new(FeatureCache::default());
1072
        let token_cache: Arc<DashMap<String, EdgeToken>> = Arc::new(DashMap::default());
1073
        let app = test::init_service(
1074
            App::new()
1075
                .app_data(Data::from(features_cache.clone()))
1076
                .app_data(Data::from(token_cache.clone()))
1077
                .service(web::scope("/api/client").service(get_features)),
1078
        )
1079
        .await;
1080

1081
        let mut token_a =
1082
            EdgeToken::try_from("[]:production.puff_the_magic_dragon".to_string()).unwrap();
1083
        token_a.projects = vec!["dx".into(), "eg".into()];
1084
        token_a.status = TokenValidationStatus::Validated;
1085
        token_a.token_type = Some(TokenType::Client);
1086
        token_cache.insert(token_a.token.clone(), token_a.clone());
1087

1088
        let mut token_b =
1089
            EdgeToken::try_from("[]:production.biff_the_magic_flagon".to_string()).unwrap();
1090
        token_b.projects = vec!["unleash-cloud".into()];
1091
        token_b.status = TokenValidationStatus::Validated;
1092
        token_b.token_type = Some(TokenType::Client);
1093
        token_cache.insert(token_b.token.clone(), token_b.clone());
1094

1095
        let example_features = features_from_disk("../examples/hostedexample.json");
1096
        features_cache.insert("production".into(), example_features.clone());
1097

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

1107
        let req_2 = make_features_request_with_token(token_b.clone()).await;
1108
        let res_2: ClientFeatures = test::call_and_read_body_json(&app, req_2).await;
1109
        assert!(
1110
            res_2
1111
                .features
1112
                .iter()
1113
                .all(|f| token_b.projects.contains(&f.project.clone().unwrap()))
1114
        );
1115

1116
        let req_3 = make_features_request_with_token(token_a.clone()).await;
1117
        let res_3: ClientFeatures = test::call_and_read_body_json(&app, req_3).await;
1118
        assert!(
1119
            res_3
1120
                .features
1121
                .iter()
1122
                .all(|f| token_a.projects.contains(&f.project.clone().unwrap()))
1123
        );
1124
    }
1125

1126
    #[tokio::test]
1127
    async fn when_running_in_offline_mode_with_proxy_key_should_not_filter_features() {
1128
        let features_cache = Arc::new(FeatureCache::default());
1129
        let token_cache: Arc<DashMap<String, EdgeToken>> = Arc::new(DashMap::default());
1130
        let app = test::init_service(
1131
            App::new()
1132
                .app_data(Data::from(features_cache.clone()))
1133
                .app_data(Data::from(token_cache.clone()))
1134
                .app_data(Data::new(crate::cli::EdgeMode::Offline(OfflineArgs {
1135
                    bootstrap_file: Some(PathBuf::from("../examples/features.json")),
1136
                    tokens: vec!["secret_123".into()],
1137
                    client_tokens: vec![],
1138
                    frontend_tokens: vec![],
1139
                    reload_interval: 0,
1140
                })))
1141
                .service(web::scope("/api/client").service(get_features)),
1142
        )
1143
        .await;
1144
        let token = EdgeToken::offline_token("secret-123");
1145
        token_cache.insert(token.token.clone(), token.clone());
1146
        let example_features = features_from_disk("../examples/features.json");
1147
        features_cache.insert(token.token.clone(), example_features.clone());
1148
        let req = make_features_request_with_token(token.clone()).await;
1149
        let res: ClientFeatures = test::call_and_read_body_json(&app, req).await;
1150
        assert_eq!(res.features.len(), example_features.features.len());
1151
    }
1152

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

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

1282
    #[tokio::test]
1283
    pub async fn gets_feature_by_name() {
1284
        let features_cache = Arc::new(FeatureCache::default());
1285
        let token_cache: Arc<DashMap<String, EdgeToken>> = Arc::new(DashMap::default());
1286
        let engine_cache: Arc<DashMap<String, EngineState>> = Arc::new(DashMap::default());
1287
        let features = features_from_disk("../examples/hostedexample.json");
1288
        let mut dx_token = EdgeToken::from_str("dx:development.secret123").unwrap();
1289
        dx_token.status = TokenValidationStatus::Validated;
1290
        dx_token.token_type = Some(TokenType::Client);
1291
        token_cache.insert(dx_token.token.clone(), dx_token.clone());
1292
        features_cache.insert(cache_key(&dx_token), features.clone());
1293
        let local_app = test::init_service(
1294
            App::new()
1295
                .app_data(Data::from(features_cache.clone()))
1296
                .app_data(Data::from(engine_cache.clone()))
1297
                .app_data(Data::from(token_cache.clone()))
1298
                .wrap(middleware::as_async_middleware::as_async_middleware(
1299
                    middleware::validate_token::validate_token,
1300
                ))
1301
                .service(web::scope("/api").configure(configure_client_api)),
1302
        )
1303
        .await;
1304
        let desired_toggle = "projectStatusApi";
1305
        let request = test::TestRequest::get()
1306
            .uri(format!("/api/client/features/{desired_toggle}").as_str())
1307
            .insert_header(ContentType::json())
1308
            .insert_header(("Authorization", dx_token.token.clone()))
1309
            .to_request();
1310
        let result: ClientFeature = test::call_and_read_body_json(&local_app, request).await;
1311
        assert_eq!(result.name, desired_toggle);
1312
    }
1313

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

1418
    #[tokio::test]
1419
    pub async fn can_filter_features_list_by_name_prefix() {
1420
        let features_cache = Arc::new(FeatureCache::default());
1421
        let token_cache: Arc<DashMap<String, EdgeToken>> = Arc::new(DashMap::default());
1422
        let engine_cache: Arc<DashMap<String, EngineState>> = Arc::new(DashMap::default());
1423
        let features = features_from_disk("../examples/hostedexample.json");
1424
        let mut dx_token = EdgeToken::from_str("dx:development.secret123").unwrap();
1425
        dx_token.status = TokenValidationStatus::Validated;
1426
        dx_token.token_type = Some(TokenType::Client);
1427
        token_cache.insert(dx_token.token.clone(), dx_token.clone());
1428
        features_cache.insert(cache_key(&dx_token), features.clone());
1429
        let local_app = test::init_service(
1430
            App::new()
1431
                .app_data(Data::from(features_cache.clone()))
1432
                .app_data(Data::from(engine_cache.clone()))
1433
                .app_data(Data::from(token_cache.clone()))
1434
                .wrap(middleware::as_async_middleware::as_async_middleware(
1435
                    middleware::validate_token::validate_token,
1436
                ))
1437
                .service(web::scope("/api").configure(configure_client_api)),
1438
        )
1439
        .await;
1440
        let request = test::TestRequest::get()
1441
            .uri("/api/client/features?namePrefix=embed")
1442
            .insert_header(ContentType::json())
1443
            .insert_header(("Authorization", dx_token.token.clone()))
1444
            .to_request();
1445
        let result: ClientFeatures = test::call_and_read_body_json(&local_app, request).await;
1446
        assert_eq!(result.features.len(), 2);
1447
        assert_eq!(result.query.unwrap().name_prefix.unwrap(), "embed");
1448
    }
1449

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

1527
    #[tokio::test]
1528
    async fn client_features_endpoint_works_with_overridden_token_header() {
1529
        let features_cache = Arc::new(FeatureCache::default());
1530
        let token_cache: Arc<DashMap<String, EdgeToken>> = Arc::new(DashMap::default());
1531
        let token_header = AuthHeaders::from_str("NeedsToBeTested").unwrap();
1532
        let app = test::init_service(
1533
            App::new()
1534
                .app_data(Data::from(features_cache.clone()))
1535
                .app_data(Data::from(token_cache.clone()))
1536
                .app_data(Data::new(token_header.clone()))
1537
                .service(web::scope("/api/client").service(get_features)),
1538
        )
1539
        .await;
1540
        let client_features = cached_client_features();
1541
        let example_features = features_from_disk("../examples/features.json");
1542
        features_cache.insert("development".into(), client_features.clone());
1543
        features_cache.insert("production".into(), example_features.clone());
1544
        let mut production_token = EdgeToken::try_from(
1545
            "*:production.03fa5f506428fe80ed5640c351c7232e38940814d2923b08f5c05fa7".to_string(),
1546
        )
1547
        .unwrap();
1548
        production_token.token_type = Some(TokenType::Client);
1549
        production_token.status = TokenValidationStatus::Validated;
1550
        token_cache.insert(production_token.token.clone(), production_token.clone());
1551

1552
        let request = test::TestRequest::get()
1553
            .uri("/api/client/features")
1554
            .insert_header(ContentType::json())
1555
            .insert_header(("NeedsToBeTested", production_token.token.clone()))
1556
            .to_request();
1557
        let res = test::call_service(&app, request).await;
1558
        assert_eq!(res.status(), StatusCode::OK);
1559
        let request = test::TestRequest::get()
1560
            .uri("/api/client/features")
1561
            .insert_header(ContentType::json())
1562
            .insert_header(("ShouldNotWork", production_token.token.clone()))
1563
            .to_request();
1564
        let res = test::call_service(&app, request).await;
1565
        assert_eq!(res.status(), StatusCode::FORBIDDEN);
1566
    }
1567

1568
    async fn setup_delta_test(
1569
        initial_event_id: u32,
1570
    ) -> (
1571
        Arc<FeatureRefresher>,
1572
        Arc<DashMap<String, EdgeToken>>,
1573
        EdgeToken,
1574
        DeltaHydrationEvent,
1575
        impl actix_web::dev::Service<
1576
            actix_http::Request,
1577
            Response = actix_web::dev::ServiceResponse,
1578
            Error = actix_web::Error,
1579
        >,
1580
    ) {
1581
        let unleash_client = Arc::new(UnleashClient::new("http://localhost:9999/", None).unwrap());
1582
        let delta_cache_manager = Arc::new(DeltaCacheManager::default());
1583
        let token_cache: Arc<DashMap<String, EdgeToken>> = Arc::new(DashMap::default());
1584

1585
        let mut token = EdgeToken::try_from(
1586
            "dx:development.03fa5f506428fe80ed5640c351c7232e38940814d2923b08f5c05fa7".to_string(),
1587
        )
1588
        .unwrap();
1589
        token.token_type = Some(TokenType::Client);
1590
        token.status = TokenValidationStatus::Validated;
1591
        token_cache.insert(token.token.clone(), token.clone());
1592

1593
        let feature_refresher = Arc::new(FeatureRefresher {
1594
            unleash_client: unleash_client.clone(),
1595
            delta_cache_manager: delta_cache_manager.clone(),
1596
            tokens_to_refresh: Arc::new(Default::default()),
1597
            refresh_interval: Duration::seconds(0),
1598
            strict: false,
1599
            delta: true,
1600
            ..Default::default()
1601
        });
1602

1603
        let delta_hydration_event = DeltaHydrationEvent {
1604
            event_id: initial_event_id,
1605
            features: vec![ClientFeature {
1606
                name: "feature1".to_string(),
1607
                project: Some("dx".to_string()),
1608
                enabled: false,
1609
                ..Default::default()
1610
            }],
1611
            segments: vec![],
1612
        };
1613

1614
        let app = test::init_service(
1615
            App::new()
1616
                .app_data(Data::from(feature_refresher.clone()))
1617
                .app_data(Data::from(token_cache.clone()))
1618
                .service(web::scope("/api/client").service(get_delta)),
1619
        )
1620
        .await;
1621

1622
        delta_cache_manager.insert_cache(
1623
            "development",
1624
            DeltaCache::new(delta_hydration_event.clone(), 10),
1625
        );
1626

1627
        (
1628
            feature_refresher,
1629
            token_cache,
1630
            token,
1631
            delta_hydration_event,
1632
            app,
1633
        )
1634
    }
1635

1636
    #[tokio::test]
1637
    async fn test_delta_endpoint_returns_hydration_event() {
1638
        let (_, _, token, delta_hydration_event, app) = setup_delta_test(10).await;
1639

1640
        let req = make_delta_request_with_token(token.clone()).await;
1641
        let res: ClientFeaturesDelta = test::call_and_read_body_json(&app, req).await;
1642

1643
        assert_eq!(
1644
            res.events.first().unwrap(),
1645
            &DeltaEvent::Hydration {
1646
                event_id: delta_hydration_event.event_id,
1647
                features: delta_hydration_event.features.clone(),
1648
                segments: delta_hydration_event.segments.clone()
1649
            }
1650
        );
1651
    }
1652

1653
    #[tokio::test]
1654
    async fn test_delta_endpoint_returns_not_modified_for_matching_etag() {
1655
        let (_, _, token, _, app) = setup_delta_test(10).await;
1656

1657
        let res = test::call_service(
1658
            &app,
1659
            make_delta_request_with_token_and_etag(token.clone(), "10").await,
1660
        )
1661
        .await;
1662

1663
        assert_eq!(res.status(), StatusCode::NOT_MODIFIED);
1664
    }
1665

1666
    #[tokio::test]
1667
    async fn test_delta_endpoint_returns_not_modified_for_newer_etag() {
1668
        let (_, _, token, _, app) = setup_delta_test(10).await;
1669

1670
        let res = test::call_service(
1671
            &app,
1672
            make_delta_request_with_token_and_etag(token.clone(), "11").await,
1673
        )
1674
        .await;
1675

1676
        assert_eq!(res.status(), StatusCode::NOT_MODIFIED);
1677
    }
1678

1679
    #[tokio::test]
1680
    async fn test_delta_endpoint_returns_delta_events_after_update() {
1681
        let (feature_refresher, _, token, _, app) = setup_delta_test(10).await;
1682

1683
        let delta_event = DeltaEvent::FeatureRemoved {
1684
            event_id: 11,
1685
            feature_name: "test".to_string(),
1686
            project: "dx".to_string(),
1687
        };
1688

1689
        feature_refresher
1690
            .delta_cache_manager
1691
            .update_cache("development", &vec![delta_event.clone()]);
1692

1693
        let res: ClientFeaturesDelta = test::call_and_read_body_json(
1694
            &app,
1695
            make_delta_request_with_token_and_etag(token.clone(), "10").await,
1696
        )
1697
        .await;
1698

1699
        assert_eq!(res.events.first().unwrap(), &delta_event);
1700
        assert_eq!(res.events.len(), 1);
1701

1702
        let res = test::call_service(
1703
            &app,
1704
            make_delta_request_with_token_and_etag(token.clone(), "11").await,
1705
        )
1706
        .await;
1707

1708
        assert_eq!(res.status(), StatusCode::NOT_MODIFIED);
1709

1710
        let delta_event = DeltaEvent::SegmentRemoved {
1711
            event_id: 12,
1712
            segment_id: 1,
1713
        };
1714

1715
        feature_refresher
1716
            .delta_cache_manager
1717
            .update_cache("development", &vec![delta_event.clone()]);
1718

1719
        let res = test::call_service(
1720
            &app,
1721
            make_delta_request_with_token_and_etag(token.clone(), "12").await,
1722
        )
1723
        .await;
1724

1725
        assert_eq!(res.status(), StatusCode::NOT_MODIFIED);
1726
    }
1727

1728
    #[tokio::test]
1729
    async fn test_delta_endpoint_returns_hydration_event_when_unknown_etag_lower_than_current_event_id()
1730
     {
1731
        let (_, _, token, delta_hydration_event, app) = setup_delta_test(10).await;
1732

1733
        let res: ClientFeaturesDelta = test::call_and_read_body_json(
1734
            &app,
1735
            make_delta_request_with_token_and_etag(token.clone(), "8").await,
1736
        )
1737
        .await;
1738

1739
        assert_eq!(
1740
            res.events.first().unwrap(),
1741
            &DeltaEvent::Hydration {
1742
                event_id: delta_hydration_event.event_id,
1743
                features: delta_hydration_event.features.clone(),
1744
                segments: delta_hydration_event.segments.clone()
1745
            }
1746
        );
1747
    }
1748
}
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