diff --git a/docs/spec/hurl.grammar b/docs/spec/hurl.grammar index 7377f6921..d93d14d70 100644 --- a/docs/spec/hurl.grammar +++ b/docs/spec/hurl.grammar @@ -418,6 +418,8 @@ variable-name: [A-Za-z] [A-Za-z_-0-9]* filter: count-filter + | days-after-now + | days-before-now | format-filter | html-escape-filter | html-unescape-filter @@ -432,6 +434,10 @@ filter: count-filter: "count" +days-after-now: "daysAfterNow" + +days-before-now: "daysBeforeNow" + format-filter: "format" html-escape-filter: "htmlEscape" diff --git a/integration/ssl/letsencrypt.hurl b/integration/ssl/letsencrypt.hurl index a684a1498..da6e5c607 100644 --- a/integration/ssl/letsencrypt.hurl +++ b/integration/ssl/letsencrypt.hurl @@ -1,2 +1,7 @@ GET https://hurl.dev HTTP 200 +[Asserts] +certificate "Subject" == "CN=hurl.dev" +certificate "Issuer" == "C=US, O=Let's Encrypt, CN=R3" +certificate "Expire-Date" daysAfterNow > 30 +certificate "Serial-Number" == "03:db:c4:f3:c8:5e:13:66:cc:52:a3:69:2c:fa:04:a0:7b:b1" diff --git a/integration/tests_ok/assert_header.html b/integration/tests_ok/assert_header.html index 2c44b1f91..7ea330fa1 100644 --- a/integration/tests_ok/assert_header.html +++ b/integration/tests_ok/assert_header.html @@ -11,6 +11,7 @@ header "ETag" == "\"33a64df551425fcc55e4d42a148795d9f25f89d4\"" header "Expires" == "Wed, 21 Oct 2015 07:28:00 GMT" header "Expires" toDate "%a, %d %b %Y %H:%M:%S GMT" format "%Y" == "2015" +header "Expires" toDate "%a, %d %b %Y %H:%M:%S GMT" daysBeforeNow > 1000 header "Set-Cookie" exists header "Set-Cookie" count == 3 header "Set-Cookie" includes "cookie1=value1; Path=/" diff --git a/integration/tests_ok/assert_header.hurl b/integration/tests_ok/assert_header.hurl index a8bc8f222..38b2f18d4 100644 --- a/integration/tests_ok/assert_header.hurl +++ b/integration/tests_ok/assert_header.hurl @@ -11,6 +11,7 @@ header "Header1" == "value1" header "ETag" == "\"33a64df551425fcc55e4d42a148795d9f25f89d4\"" header "Expires" == "Wed, 21 Oct 2015 07:28:00 GMT" header "Expires" toDate "%a, %d %b %Y %H:%M:%S GMT" format "%Y" == "2015" +header "Expires" toDate "%a, %d %b %Y %H:%M:%S GMT" daysBeforeNow > 1000 header "Set-Cookie" exists header "Set-Cookie" count == 3 header "Set-Cookie" includes "cookie1=value1; Path=/" diff --git a/integration/tests_ok/assert_header.json b/integration/tests_ok/assert_header.json index 1a439988c..cfac9f1d1 100644 --- a/integration/tests_ok/assert_header.json +++ b/integration/tests_ok/assert_header.json @@ -1 +1 @@ -{"entries":[{"request":{"method":"GET","url":"http://localhost:8000/assert-header"},"response":{"status":200,"headers":[{"name":"Content-Type","value":"text/html; charset=utf-8"},{"name":"Set-Cookie","value":"cookie1=value1; Path=/"},{"name":"Set-Cookie","value":"cookie2=value2; Path=/"}],"asserts":[{"query":{"type":"header","name":"Custom"},"predicate":{"not":true,"type":"exist"}},{"query":{"type":"header","name":"Content-Type"},"predicate":{"type":"exist"}},{"query":{"type":"header","name":"Header1"},"predicate":{"type":"equal","value":"value1"}},{"query":{"type":"header","name":"ETag"},"predicate":{"type":"equal","value":"\"33a64df551425fcc55e4d42a148795d9f25f89d4\""}},{"query":{"type":"header","name":"Expires"},"predicate":{"type":"equal","value":"Wed, 21 Oct 2015 07:28:00 GMT"}},{"query":{"type":"header","name":"Expires"},"filters":[{"type":"toDate","fmt":"%a, %d %b %Y %H:%M:%S GMT"},{"type":"format","fmt":"%Y"}],"predicate":{"type":"equal","value":"2015"}},{"query":{"type":"header","name":"Set-Cookie"},"predicate":{"type":"exist"}},{"query":{"type":"header","name":"Set-Cookie"},"filters":[{"type":"count"}],"predicate":{"type":"equal","value":3}},{"query":{"type":"header","name":"Set-Cookie"},"predicate":{"type":"include","value":"cookie1=value1; Path=/"}},{"query":{"type":"header","name":"Set-Cookie"},"predicate":{"not":true,"type":"include","value":"cookie4=value4; Path=/"}}]}}]} +{"entries":[{"request":{"method":"GET","url":"http://localhost:8000/assert-header"},"response":{"status":200,"headers":[{"name":"Content-Type","value":"text/html; charset=utf-8"},{"name":"Set-Cookie","value":"cookie1=value1; Path=/"},{"name":"Set-Cookie","value":"cookie2=value2; Path=/"}],"asserts":[{"query":{"type":"header","name":"Custom"},"predicate":{"not":true,"type":"exist"}},{"query":{"type":"header","name":"Content-Type"},"predicate":{"type":"exist"}},{"query":{"type":"header","name":"Header1"},"predicate":{"type":"equal","value":"value1"}},{"query":{"type":"header","name":"ETag"},"predicate":{"type":"equal","value":"\"33a64df551425fcc55e4d42a148795d9f25f89d4\""}},{"query":{"type":"header","name":"Expires"},"predicate":{"type":"equal","value":"Wed, 21 Oct 2015 07:28:00 GMT"}},{"query":{"type":"header","name":"Expires"},"filters":[{"type":"toDate","fmt":"%a, %d %b %Y %H:%M:%S GMT"},{"type":"format","fmt":"%Y"}],"predicate":{"type":"equal","value":"2015"}},{"query":{"type":"header","name":"Expires"},"filters":[{"type":"toDate","fmt":"%a, %d %b %Y %H:%M:%S GMT"},{"type":"daysBeforeNow"}],"predicate":{"type":"greater","value":1000}},{"query":{"type":"header","name":"Set-Cookie"},"predicate":{"type":"exist"}},{"query":{"type":"header","name":"Set-Cookie"},"filters":[{"type":"count"}],"predicate":{"type":"equal","value":3}},{"query":{"type":"header","name":"Set-Cookie"},"predicate":{"type":"include","value":"cookie1=value1; Path=/"}},{"query":{"type":"header","name":"Set-Cookie"},"predicate":{"not":true,"type":"include","value":"cookie4=value4; Path=/"}}]}}]} diff --git a/integration/tests_ok/assert_header.out.pattern b/integration/tests_ok/assert_header.out.pattern index e34602609..fac28e14b 100644 --- a/integration/tests_ok/assert_header.out.pattern +++ b/integration/tests_ok/assert_header.out.pattern @@ -1 +1 @@ -{"cookies":[{"domain":"localhost","expires":"0","https":"FALSE","include_subdomain":"FALSE","name":"cookie1","path":"/","value":"value1"},{"domain":"localhost","expires":"0","https":"FALSE","include_subdomain":"FALSE","name":"cookie2","path":"/","value":"value2"},{"domain":"localhost","expires":"0","https":"FALSE","include_subdomain":"FALSE","name":"cookie3","path":"/","value":"value3"}],"entries":[{"asserts":[{"line":3,"success":true},{"line":3,"success":true},{"line":4,"success":true},{"line":5,"success":true},{"line":6,"success":true},{"line":8,"success":true},{"line":9,"success":true},{"line":10,"success":true},{"line":11,"success":true},{"line":12,"success":true},{"line":13,"success":true},{"line":14,"success":true},{"line":15,"success":true},{"line":16,"success":true},{"line":17,"success":true}],"calls":[{"request":{"cookies":[],"headers":[{"name":"Host","value":"localhost:8000"},{"name":"Accept","value":"*/*"},{"name":"User-Agent","value":"hurl/~~~"}],"method":"GET","queryString":[],"url":"http://localhost:8000/assert-header"},"response":{"cookies":[{"name":"cookie1","path":"/","value":"value1"},{"name":"cookie2","path":"/","value":"value2"},{"name":"cookie3","path":"/","value":"value3"}],"headers":[{"name":"Server","value":"Werkzeug/~~~ Python/~~~"},{"name":"Date","value":"~~~"},{"name":"Content-Type","value":"text/html; charset=utf-8"},{"name":"Header1","value":"value1"},{"name":"ETag","value":"\"33a64df551425fcc55e4d42a148795d9f25f89d4\""},{"name":"Expires","value":"~~~"},{"name":"Set-Cookie","value":"cookie1=value1; Path=/"},{"name":"Set-Cookie","value":"cookie2=value2; Path=/"},{"name":"Set-Cookie","value":"cookie3=value3; Path=/"},{"name":"Server","value":"Flask Server"},{"name":"Content-Length","value":"0"},{"name":"Connection","value":"close"}],"httpVersion":"HTTP/1.1","status":200},"timings":{"app_connect":~~~,"begin_call":"~~~","connect":~~~,"end_call":"~~~","name_lookup":~~~,"pre_transfert":~~~,"start_transfert":~~~,"total":~~~}}],"captures":[],"index":1,"time":~~~}],"filename":"tests_ok/assert_header.hurl","success":true,"time":~~~} +{"cookies":[{"domain":"localhost","expires":"0","https":"FALSE","include_subdomain":"FALSE","name":"cookie1","path":"/","value":"value1"},{"domain":"localhost","expires":"0","https":"FALSE","include_subdomain":"FALSE","name":"cookie2","path":"/","value":"value2"},{"domain":"localhost","expires":"0","https":"FALSE","include_subdomain":"FALSE","name":"cookie3","path":"/","value":"value3"}],"entries":[{"asserts":[{"line":3,"success":true},{"line":3,"success":true},{"line":4,"success":true},{"line":5,"success":true},{"line":6,"success":true},{"line":8,"success":true},{"line":9,"success":true},{"line":10,"success":true},{"line":11,"success":true},{"line":12,"success":true},{"line":13,"success":true},{"line":14,"success":true},{"line":15,"success":true},{"line":16,"success":true},{"line":17,"success":true},{"line":18,"success":true}],"calls":[{"request":{"cookies":[],"headers":[{"name":"Host","value":"localhost:8000"},{"name":"Accept","value":"*/*"},{"name":"User-Agent","value":"hurl/~~~"}],"method":"GET","queryString":[],"url":"http://localhost:8000/assert-header"},"response":{"cookies":[{"name":"cookie1","path":"/","value":"value1"},{"name":"cookie2","path":"/","value":"value2"},{"name":"cookie3","path":"/","value":"value3"}],"headers":[{"name":"Server","value":"Werkzeug/~~~ Python/~~~"},{"name":"Date","value":"~~~"},{"name":"Content-Type","value":"text/html; charset=utf-8"},{"name":"Header1","value":"value1"},{"name":"ETag","value":"\"33a64df551425fcc55e4d42a148795d9f25f89d4\""},{"name":"Expires","value":"~~~"},{"name":"Set-Cookie","value":"cookie1=value1; Path=/"},{"name":"Set-Cookie","value":"cookie2=value2; Path=/"},{"name":"Set-Cookie","value":"cookie3=value3; Path=/"},{"name":"Server","value":"Flask Server"},{"name":"Content-Length","value":"0"},{"name":"Connection","value":"close"}],"httpVersion":"HTTP/1.1","status":200},"timings":{"app_connect":~~~,"begin_call":"~~~","connect":~~~,"end_call":"~~~","name_lookup":~~~,"pre_transfert":~~~,"start_transfert":~~~,"total":~~~}}],"captures":[],"index":1,"time":~~~}],"filename":"tests_ok/assert_header.hurl","success":true,"time":~~~} diff --git a/packages/hurl/src/runner/filter.rs b/packages/hurl/src/runner/filter.rs index d6d302865..c1cb33839 100644 --- a/packages/hurl/src/runner/filter.rs +++ b/packages/hurl/src/runner/filter.rs @@ -17,7 +17,7 @@ */ use std::collections::HashMap; -use chrono::NaiveDateTime; +use chrono::{NaiveDateTime, Utc}; use hurl_core::ast::{Filter, FilterValue, RegexValue, SourceInfo, Template}; use percent_encoding::AsciiSet; @@ -49,6 +49,8 @@ fn eval_filter( ) -> Result { match &filter.value { FilterValue::Count => eval_count(value, &filter.source_info, in_assert), + FilterValue::DaysAfterNow => eval_days_after_now(value, &filter.source_info, in_assert), + FilterValue::DaysBeforeNow => eval_days_before_now(value, &filter.source_info, in_assert), FilterValue::Format { fmt, .. } => { eval_format(value, fmt, variables, &filter.source_info, in_assert) } @@ -133,6 +135,42 @@ fn eval_count(value: &Value, source_info: &SourceInfo, assert: bool) -> Result Result { + match value { + Value::Date(value) => { + let diff = value.signed_duration_since(Utc::now()); + Ok(Value::Integer(diff.num_days())) + } + v => Err(Error { + source_info: source_info.clone(), + inner: RunnerError::FilterInvalidInput(v._type()), + assert, + }), + } +} + +fn eval_days_before_now( + value: &Value, + source_info: &SourceInfo, + assert: bool, +) -> Result { + match value { + Value::Date(value) => { + let diff = Utc::now().signed_duration_since(*value); + Ok(Value::Integer(diff.num_days())) + } + v => Err(Error { + source_info: source_info.clone(), + inner: RunnerError::FilterInvalidInput(v._type()), + assert, + }), + } +} + fn eval_format( value: &Value, fmt: &Template, @@ -347,6 +385,7 @@ fn eval_to_int(value: &Value, source_info: &SourceInfo, assert: bool) -> Result< pub mod tests { use chrono::offset::Utc; use chrono::prelude::*; + use chrono::Duration; use hurl_core::ast::{FilterValue, SourceInfo, Template, TemplateElement, Whitespace}; use super::*; @@ -407,6 +446,54 @@ pub mod tests { ); } + #[test] + pub fn eval_filter_days_after_before_now() { + let variables = HashMap::new(); + + let now = Utc::now(); + assert_eq!( + eval_filter( + &Filter { + source_info: SourceInfo::new(1, 1, 1, 1), + value: FilterValue::DaysAfterNow, + }, + &Value::Date(now), + &variables, + false, + ) + .unwrap(), + Value::Integer(0) + ); + + let now_plus_30hours = now + Duration::hours(30); + assert_eq!( + eval_filter( + &Filter { + source_info: SourceInfo::new(1, 1, 1, 1), + value: FilterValue::DaysAfterNow, + }, + &Value::Date(now_plus_30hours), + &variables, + false, + ) + .unwrap(), + Value::Integer(1) + ); + assert_eq!( + eval_filter( + &Filter { + source_info: SourceInfo::new(1, 1, 1, 1), + value: FilterValue::DaysBeforeNow, + }, + &Value::Date(now_plus_30hours), + &variables, + false, + ) + .unwrap(), + Value::Integer(-1) + ); + } + #[test] pub fn eval_filter_format() { // let naivedatetime_utc = NaiveDate::from_ymd_opt(2000, 1, 12).unwrap().and_hms_opt(2, 0, 0).unwrap(); diff --git a/packages/hurl_core/src/ast/core.rs b/packages/hurl_core/src/ast/core.rs index 4b505a96c..cf6167262 100644 --- a/packages/hurl_core/src/ast/core.rs +++ b/packages/hurl_core/src/ast/core.rs @@ -887,6 +887,8 @@ pub struct Filter { #[derive(Clone, Debug, PartialEq, Eq)] pub enum FilterValue { Count, + DaysAfterNow, + DaysBeforeNow, Format { space0: Whitespace, fmt: Template, diff --git a/packages/hurl_core/src/format/html.rs b/packages/hurl_core/src/format/html.rs index a5c4cf146..2ebc8fac7 100644 --- a/packages/hurl_core/src/format/html.rs +++ b/packages/hurl_core/src/format/html.rs @@ -1148,6 +1148,12 @@ impl Htmlable for FilterValue { fn to_html(&self) -> String { match self { FilterValue::Count => "count".to_string(), + FilterValue::DaysAfterNow => { + "daysAfterNow".to_string() + } + FilterValue::DaysBeforeNow => { + "daysBeforeNow".to_string() + } FilterValue::Format { space0, fmt } => { let mut buffer = "format".to_string(); buffer.push_str(space0.to_html().as_str()); diff --git a/packages/hurl_core/src/parser/filter.rs b/packages/hurl_core/src/parser/filter.rs index b15ef3012..701c465af 100644 --- a/packages/hurl_core/src/parser/filter.rs +++ b/packages/hurl_core/src/parser/filter.rs @@ -52,6 +52,8 @@ pub fn filter(reader: &mut Reader) -> ParseResult<'static, Filter> { let value = choice( &[ count_filter, + days_after_now_filter, + days_before_now_filter, format_filter, html_decode_filter, html_encode_filter, @@ -89,6 +91,16 @@ fn count_filter(reader: &mut Reader) -> ParseResult<'static, FilterValue> { Ok(FilterValue::Count) } +fn days_after_now_filter(reader: &mut Reader) -> ParseResult<'static, FilterValue> { + try_literal("daysAfterNow", reader)?; + Ok(FilterValue::DaysAfterNow) +} + +fn days_before_now_filter(reader: &mut Reader) -> ParseResult<'static, FilterValue> { + try_literal("daysBeforeNow", reader)?; + Ok(FilterValue::DaysBeforeNow) +} + fn format_filter(reader: &mut Reader) -> ParseResult<'static, FilterValue> { try_literal("format", reader)?; let space0 = one_or_more_spaces(reader)?; diff --git a/packages/hurlfmt/src/format/json.rs b/packages/hurlfmt/src/format/json.rs index d351efea7..03f34218b 100644 --- a/packages/hurlfmt/src/format/json.rs +++ b/packages/hurlfmt/src/format/json.rs @@ -553,6 +553,18 @@ impl ToJson for FilterValue { FilterValue::Count => { attributes.push(("type".to_string(), JValue::String("count".to_string()))); } + FilterValue::DaysAfterNow => { + attributes.push(( + "type".to_string(), + JValue::String("daysAfterNow".to_string()), + )); + } + FilterValue::DaysBeforeNow => { + attributes.push(( + "type".to_string(), + JValue::String("daysBeforeNow".to_string()), + )); + } FilterValue::Format { fmt, .. } => { attributes.push(("type".to_string(), JValue::String("format".to_string()))); attributes.push(("fmt".to_string(), JValue::String(fmt.to_string()))); diff --git a/packages/hurlfmt/src/format/token.rs b/packages/hurlfmt/src/format/token.rs index f0d956cb4..bf177ac7f 100644 --- a/packages/hurlfmt/src/format/token.rs +++ b/packages/hurlfmt/src/format/token.rs @@ -1167,6 +1167,8 @@ impl Tokenizable for Filter { fn tokenize(&self) -> Vec { match self.value.clone() { FilterValue::Count => vec![Token::FilterType(String::from("count"))], + FilterValue::DaysAfterNow => vec![Token::FilterType(String::from("daysAfterNow"))], + FilterValue::DaysBeforeNow => vec![Token::FilterType(String::from("daysBeforeNow"))], FilterValue::Format { space0, fmt } => { let mut tokens: Vec = vec![Token::FilterType(String::from("format"))]; tokens.append(&mut space0.tokenize());