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

Unleash / unleash-edge / 15526356487

09 Jun 2025 03:51AM UTC coverage: 78.199% (-0.06%) from 78.263%
15526356487

Pull #979

github

web-flow
Merge 54ca84f50 into a99ba5cca
Pull Request #979: chore: release v19.11.1

10151 of 12981 relevant lines covered (78.2%)

158.1 hits per line

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

93.64
/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")]
35✔
39
pub async fn get_features(
34✔
40
    edge_token: EdgeToken,
34✔
41
    features_cache: Data<FeatureCache>,
34✔
42
    token_cache: Data<DashMap<String, EdgeToken>>,
34✔
43
    filter_query: Query<FeatureFilters>,
34✔
44
    req: HttpRequest,
34✔
45
) -> EdgeJsonResult<ClientFeatures> {
34✔
46
    resolve_features(edge_token, features_cache, token_cache, filter_query, req).await
34✔
47
}
34✔
48

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

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

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

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

99
            broadcaster.connect(validated_token, query).await
1✔
100
        }
101
        _ => Err(EdgeError::Forbidden(
×
102
            "This endpoint is only enabled in streaming mode".into(),
×
103
        )),
×
104
    }
105
}
1✔
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")]
1✔
120
pub async fn post_features(
1✔
121
    edge_token: EdgeToken,
1✔
122
    features_cache: Data<FeatureCache>,
1✔
123
    token_cache: Data<DashMap<String, EdgeToken>>,
1✔
124
    filter_query: Query<FeatureFilters>,
1✔
125
    req: HttpRequest,
1✔
126
) -> EdgeJsonResult<ClientFeatures> {
1✔
127
    resolve_features(edge_token, features_cache, token_cache, filter_query, req).await
1✔
128
}
1✔
129

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

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

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

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

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

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

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

7✔
182
    Ok(delta_filter_set)
7✔
183
}
7✔
184

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

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

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

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

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

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

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

3✔
248
    Ok(Json(Some(delta)))
3✔
249
}
7✔
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}")]
27✔
265
pub async fn get_feature(
4✔
266
    edge_token: EdgeToken,
4✔
267
    features_cache: Data<FeatureCache>,
4✔
268
    token_cache: Data<DashMap<String, EdgeToken>>,
4✔
269
    feature_name: web::Path<String>,
4✔
270
    req: HttpRequest,
4✔
271
) -> EdgeJsonResult<ClientFeature> {
4✔
272
    let validated_token = token_cache
4✔
273
        .get(&edge_token.token)
4✔
274
        .map(|e| e.value().clone())
4✔
275
        .ok_or(EdgeError::AuthorizationDenied)?;
4✔
276

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

4✔
280
    match req.app_data::<Data<FeatureRefresher>>() {
4✔
281
        Some(refresher) => {
×
282
            refresher
×
283
                .features_for_filter(validated_token.clone(), &filter_set)
×
284
                .await
×
285
        }
286
        None => features_cache
4✔
287
            .get(&cache_key(&validated_token))
4✔
288
            .map(|client_features| filter_client_features(&client_features, &filter_set))
4✔
289
            .ok_or(EdgeError::ClientCacheError),
4✔
290
    }
291
    .map(|client_features| client_features.features.into_iter().next())?
4✔
292
    .ok_or(EdgeError::FeatureNotFound(feature_name.into_inner()))
4✔
293
    .map(Json)
4✔
294
}
4✔
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")]
29✔
308
pub async fn register(
7✔
309
    edge_token: EdgeToken,
7✔
310
    connect_via: Data<ConnectVia>,
7✔
311
    client_application: Json<ClientApplication>,
7✔
312
    metrics_cache: Data<MetricsCache>,
7✔
313
) -> EdgeResult<HttpResponse> {
7✔
314
    crate::metrics::client_metrics::register_client_application(
7✔
315
        edge_token,
7✔
316
        &connect_via,
7✔
317
        client_application.into_inner(),
7✔
318
        metrics_cache,
7✔
319
    );
7✔
320
    Ok(HttpResponse::Accepted()
7✔
321
        .append_header(("X-Edge-Version", types::EDGE_VERSION))
7✔
322
        .finish())
7✔
323
}
7✔
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")]
28✔
337
pub async fn metrics(
1✔
338
    edge_token: EdgeToken,
1✔
339
    metrics: Json<ClientMetrics>,
1✔
340
    metrics_cache: Data<MetricsCache>,
1✔
341
) -> EdgeResult<HttpResponse> {
1✔
342
    crate::metrics::client_metrics::register_client_metrics(
1✔
343
        edge_token,
1✔
344
        metrics.into_inner(),
1✔
345
        metrics_cache,
1✔
346
    );
1✔
347
    Ok(HttpResponse::Accepted().finish())
1✔
348
}
1✔
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")]
28✔
362
pub async fn post_bulk_metrics(
2✔
363
    edge_token: EdgeToken,
2✔
364
    bulk_metrics: Json<BatchMetricsRequestBody>,
2✔
365
    connect_via: Data<ConnectVia>,
2✔
366
    metrics_cache: Data<MetricsCache>,
2✔
367
) -> EdgeResult<HttpResponse> {
2✔
368
    crate::metrics::client_metrics::register_bulk_metrics(
2✔
369
        metrics_cache.get_ref(),
2✔
370
        connect_via.get_ref(),
2✔
371
        &edge_token,
2✔
372
        bulk_metrics.into_inner(),
2✔
373
    );
2✔
374
    Ok(HttpResponse::Accepted().finish())
2✔
375
}
2✔
376

377
#[post("/metrics/edge")]
27✔
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) {
27✔
395
    let client_scope = web::scope("/client")
27✔
396
        .wrap(crate::middleware::as_async_middleware::as_async_middleware(
27✔
397
            crate::middleware::validate_token::validate_token,
27✔
398
        ))
27✔
399
        .wrap(crate::middleware::as_async_middleware::as_async_middleware(
27✔
400
            crate::middleware::consumption::connection_consumption,
27✔
401
        ))
27✔
402
        .service(get_features)
27✔
403
        .service(get_delta)
27✔
404
        .service(get_feature)
27✔
405
        .service(register)
27✔
406
        .service(metrics)
27✔
407
        .service(post_bulk_metrics)
27✔
408
        .service(stream_features)
27✔
409
        .service(post_edge_instance_data);
27✔
410

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

© 2025 Coveralls, Inc