pvoutput-client 0.0.3

Client for the PVOutput.org API
Documentation
use http::{HeaderMap, HeaderValue};
use jiff::Timestamp;
use proptest::property_test;

use crate::{
	Client,
	types::{
		error::RateLimitError,
		response::{RateLimit, SearchResult},
	},
};

mod error;
mod get;
mod post;

const RATE_LIMIT_REMAINING: u16 = 200;
const RATE_LIMIT_LIMIT: u16 = 300;
const RATE_LIMIT_RESET: Timestamp = Timestamp::constant(1_474_243_200, 0);

#[test]
fn rate_limit() {
	let headers = rate_limit_headers();

	let rate_limit = RateLimit::try_from(&headers);
	assert!(rate_limit.is_ok());
	let rate_limit = rate_limit.unwrap();

	assert_eq!(rate_limit.remaining, RATE_LIMIT_REMAINING);
	assert_eq!(rate_limit.limit, RATE_LIMIT_LIMIT);
	assert_eq!(rate_limit.reset, RATE_LIMIT_RESET);
	assert_eq!(rate_limit.used(), RATE_LIMIT_LIMIT - RATE_LIMIT_REMAINING);
}

#[property_test]
#[test]
fn arbitrary_rate_limit(
	a: u16,
	b: u16,
	#[strategy = Timestamp::MIN.as_second()..=Timestamp::MAX.as_second()] reset_seconds: i64,
) {
	let remaining = a.min(b);
	let limit = a.max(b);

	let mut headers = HeaderMap::new();
	headers.insert("X-Rate-Limit-Remaining", remaining.into());
	headers.insert("X-Rate-Limit-Limit", limit.into());
	headers.insert("X-Rate-Limit-Reset", reset_seconds.into());

	let rate_limit = RateLimit::try_from(&headers);
	assert!(rate_limit.is_ok());
	let rate_limit = rate_limit.unwrap();

	assert_eq!(rate_limit.remaining, remaining);
	assert_eq!(rate_limit.limit, limit);
	assert_eq!(rate_limit.reset, Timestamp::constant(reset_seconds, 0));
	assert_eq!(rate_limit.used(), limit - remaining);
}

#[test]
fn invalid_rate_limit() {
	let mut headers = rate_limit_headers();

	// non-ascii
	headers.insert("X-Rate-Limit-Remaining", HeaderValue::from_str("\u{1F430}").unwrap());
	let rate_limit = RateLimit::try_from(&headers);
	assert!(matches!(rate_limit, Err(RateLimitError::NonAsciiHeader)));

	// invalid integer
	headers.insert("X-Rate-Limit-Remaining", HeaderValue::from_str("xyzzy").unwrap());
	let rate_limit = RateLimit::try_from(&headers);
	assert!(matches!(rate_limit, Err(RateLimitError::InvalidInteger(_))));

	// invalid timestamp
	let mut headers = rate_limit_headers();
	headers.insert("X-Rate-Limit-Reset", (Timestamp::MAX.as_second() + 1).into());
	let rate_limit = RateLimit::try_from(&headers);
	assert!(matches!(rate_limit, Err(RateLimitError::InvalidTimestamp(_))));

	// missing header
	headers.remove("X-Rate-Limit-Reset");
	let rate_limit = RateLimit::try_from(&headers);
	assert!(matches!(rate_limit, Err(RateLimitError::MissingValues)));
}

#[test]
fn search() {
	let mut response = Client::reader(include_str!("../../../cases/search.csv"));
	let results: Vec<SearchResult> = response.deserialize().collect::<Result<_, _>>().unwrap();
	assert_eq!(results.len(), 7);
	assert_eq!(results[0].name, "Albany Creek Enphase");
	assert_eq!(results[0].last_output.map(|s| s.get_days()), Some(0));
	assert_eq!(results[3].last_output.map(|s| s.get_days()), Some(1));
	assert_eq!(results[4].last_output.map(|s| s.get_days()), Some(5));
	assert_eq!(results[5].last_output.map(|s| s.get_days()), None);
	assert_eq!(results[6].last_output.map(|s| s.get_weeks()), Some(524));
}

fn rate_limit_headers() -> HeaderMap {
	let mut headers = HeaderMap::new();
	headers.insert("X-Rate-Limit-Remaining", RATE_LIMIT_REMAINING.into());
	headers.insert("X-Rate-Limit-Limit", RATE_LIMIT_LIMIT.into());
	headers.insert("X-Rate-Limit-Reset", RATE_LIMIT_RESET.as_second().into());

	headers
}