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

Unleash / unleash-edge / #1636

25 Feb 2025 04:07AM UTC coverage: 64.48% (+0.9%) from 63.597%
#1636

push

web-flow
dep-update: bump aws-config from 1.5.16 to 1.5.17 (#772)

Bumps [aws-config](https://github.com/smithy-lang/smithy-rs) from 1.5.16 to 1.5.17.
- [Release notes](https://github.com/smithy-lang/smithy-rs/releases)
- [Changelog](https://github.com/smithy-lang/smithy-rs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/smithy-lang/smithy-rs/commits)

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

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

1661 of 2576 relevant lines covered (64.48%)

1.61 hits per line

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

53.03
/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(
1✔
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
// TODO: add tests for delta api
49
#[get("/delta")]
50
pub async fn get_delta(
×
51
    edge_token: EdgeToken,
52
    token_cache: Data<DashMap<String, EdgeToken>>,
53
    filter_query: Query<FeatureFilters>,
54
    req: HttpRequest,
55
) -> impl Responder {
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
    let current_sdk_revision_id = requested_revision_id + 1; // TODO: Read from delta_manager
×
64

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

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

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

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

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

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

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

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

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

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

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

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

188
    Ok(delta_filter_set)
×
189
}
190

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

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

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

228
    let delta_filter_set = get_delta_filter(
229
        &edge_token,
×
230
        &token_cache,
×
231
        filter_query.clone(),
×
232
        requested_revision_id,
233
    )?;
234

235
    let current_sdk_revision_id = requested_revision_id + 1; // TODO: get from delta manager
×
236
    if requested_revision_id >= current_sdk_revision_id {
×
237
        return Ok(Json(None));
×
238
    }
239

240
    let refresher = req.app_data::<Data<FeatureRefresher>>().ok_or_else(|| {
×
241
        EdgeError::ClientHydrationFailed(
×
242
            "FeatureRefresher is missing - cannot resolve delta in offline mode".to_string(),
×
243
        )
244
    })?;
245

246
    let delta = refresher
×
247
        .delta_events_for_filter(
248
            validated_token.clone(),
×
249
            &filter_set,
×
250
            &delta_filter_set,
×
251
            requested_revision_id,
252
        )
253
        .await?;
×
254

255
    if delta.events.is_empty() {
×
256
        return Ok(Json(None));
×
257
    }
258

259
    Ok(Json(Some(delta)))
×
260
}
261

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

288
    let filter_set = FeatureFilterSet::from(Box::new(name_match_filter(feature_name.clone())))
7✔
289
        .with_filter(project_filter(&validated_token));
3✔
290

291
    match req.app_data::<Data<FeatureRefresher>>() {
6✔
292
        Some(refresher) => {
×
293
            refresher
×
294
                .features_for_filter(validated_token.clone(), &filter_set)
×
295
                .await
×
296
        }
297
        None => features_cache
5✔
298
            .get(&cache_key(&validated_token))
1✔
299
            .map(|client_features| filter_client_features(&client_features, &filter_set))
3✔
300
            .ok_or(EdgeError::ClientCacheError),
2✔
301
    }
302
    .map(|client_features| client_features.features.into_iter().next())?
2✔
303
    .ok_or(EdgeError::FeatureNotFound(feature_name.into_inner()))
2✔
304
    .map(Json)
305
}
306

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

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

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

388
#[utoipa::path(context_path = "/api/client", responses((status = 202, description = "Accepted Instance data"), (status = 403, description = "Was not allowed to post instance data")), request_body = EdgeInstanceData, security(
389
("Authorization" = [])
390
)
391
)]
392
#[post("/metrics/edge")]
393
#[instrument(skip(_edge_token, instance_data, connected_instances))]
394
pub async fn post_edge_instance_data(
395
    _edge_token: EdgeToken,
396
    instance_data: Json<EdgeInstanceData>,
397
    instance_data_sending: Data<InstanceDataSending>,
398
    connected_instances: Data<RwLock<Vec<EdgeInstanceData>>>,
399
) -> EdgeResult<HttpResponse> {
400
    if let InstanceDataSending::SendInstanceData(_) = instance_data_sending.as_ref() {
×
401
        connected_instances
×
402
            .write()
403
            .await
×
404
            .push(instance_data.into_inner());
×
405
    }
406
    Ok(HttpResponse::Accepted().finish())
×
407
}
408

409
pub fn configure_client_api(cfg: &mut web::ServiceConfig) {
2✔
410
    let client_scope = web::scope("/client")
6✔
411
        .wrap(crate::middleware::as_async_middleware::as_async_middleware(
2✔
412
            crate::middleware::validate_token::validate_token,
413
        ))
414
        .service(get_features)
415
        .service(get_delta)
416
        .service(get_feature)
417
        .service(register)
418
        .service(metrics)
419
        .service(post_bulk_metrics)
420
        .service(stream_features)
421
        .service(post_edge_instance_data);
422

423
    cfg.service(client_scope);
2✔
424
}
425

426
pub fn configure_experimental_post_features(
×
427
    cfg: &mut web::ServiceConfig,
428
    post_features_enabled: bool,
429
) {
430
    if post_features_enabled {
×
431
        cfg.service(post_features);
×
432
    }
433
}
434

435
#[cfg(test)]
436
mod tests {
437

438
    use crate::metrics::client_metrics::{ApplicationKey, MetricsBatch, MetricsKey};
439
    use crate::types::{TokenType, TokenValidationStatus};
440
    use std::collections::HashMap;
441
    use std::path::PathBuf;
442
    use std::str::FromStr;
443
    use std::sync::Arc;
444

445
    use super::*;
446

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

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

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

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

559
    async fn make_features_request_with_token(token: EdgeToken) -> Request {
560
        test::TestRequest::get()
561
            .uri("/api/client/features")
562
            .insert_header(("Authorization", token.token))
563
            .to_request()
564
    }
565

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

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

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

584
        let cache = metrics_cache.clone();
585

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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