#![allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
use aoc_api::{
Error, Part, Puzzle, Session, Verdict,
http::{Response, fake::FakeTransport},
session,
};
use std::{fmt, sync::Arc, time::Duration};
const PUZZLE_PAGE: &str = include_str!("fixtures/puzzle-day.html");
const EVENTS: &str = include_str!("fixtures/events.html");
const ALREADY_COMPLETE: &str = include_str!("fixtures/submit-already-complete.html");
const CORRECT: &str = include_str!("fixtures/submit-correct.html");
const TOO_HIGH: &str = include_str!("fixtures/submit-too-high.html");
const COOLDOWN: &str = include_str!("fixtures/submit-cooldown.html");
const LOGGED_OUT: &str = include_str!("fixtures/logged-out.html");
fn puzzle() -> Puzzle {
Puzzle::at(2020, 1).expect("2020 has a day 1")
}
fn block_on<T>(future: impl Future<Output = T>) -> T {
tokio::runtime::Builder::new_current_thread()
.build()
.expect("a current-thread runtime")
.block_on(future)
}
#[test]
fn a_whole_day_can_be_solved_without_touching_the_network() {
let transport = FakeTransport::new();
transport
.push_body("1721\n979\n366\n299\n675\n1456\n")
.push_body(TOO_HIGH)
.push_body(CORRECT);
let session = Session::with_transport(transport);
block_on(async {
let input = session.input_lines(puzzle()).await.expect("the input");
assert_eq!(input.len(), 6);
let rejected = session
.submit(puzzle(), Part::One, "999999")
.await
.expect("a judged answer");
assert!(!rejected.is_correct());
let accepted = session
.submit(puzzle(), Part::One, "514579")
.await
.expect("a judged answer");
assert_eq!(accepted, Verdict::Correct);
});
assert_eq!(
session.transport().requested_urls(),
[
"https://adventofcode.com/2020/day/1/input",
"https://adventofcode.com/2020/day/1/answer",
"https://adventofcode.com/2020/day/1/answer",
]
);
}
#[test]
fn a_whole_day_can_be_solved_without_a_session_too() {
let transport = FakeTransport::new();
transport
.push_body("1721\n979\n366\n299\n675\n1456\n")
.push_body(TOO_HIGH)
.push_body(CORRECT);
block_on(async {
let input = session::input_lines(&transport, puzzle())
.await
.expect("the input");
assert_eq!(input.len(), 6);
let rejected = session::submit(&transport, puzzle(), Part::One, "999999")
.await
.expect("a judged answer");
assert!(!rejected.is_correct());
let accepted = session::submit(&transport, puzzle(), Part::One, "514579")
.await
.expect("a judged answer");
assert_eq!(accepted, Verdict::Correct);
});
assert_eq!(
transport.requested_urls(),
[
"https://adventofcode.com/2020/day/1/input",
"https://adventofcode.com/2020/day/1/answer",
"https://adventofcode.com/2020/day/1/answer",
]
);
}
fn both_ways<T: fmt::Debug>(
bodies: &[&str],
through_session: impl AsyncFnOnce(&Session<FakeTransport>) -> T,
directly: impl AsyncFnOnce(&FakeTransport) -> T,
) {
let (queued, same) = (FakeTransport::new(), FakeTransport::new());
for body in bodies {
queued.push_body(*body);
same.push_body(*body);
}
let session = Session::with_transport(queued);
let from_method = block_on(through_session(&session));
let from_function = block_on(directly(&same));
assert_eq!(format!("{from_method:?}"), format!("{from_function:?}"));
assert_eq!(session.transport().requests(), same.requests());
}
#[test]
fn every_method_makes_the_same_request_as_its_free_function() {
let input = "1721\n979\n366\n";
both_ways(
&[input],
async |session| session.input_text(puzzle()).await,
async |transport| session::input_text(transport, puzzle()).await,
);
both_ways(
&[input],
async |session| session.input_lines(puzzle()).await,
async |transport| session::input_lines(transport, puzzle()).await,
);
both_ways(
&[PUZZLE_PAGE],
async |session| session.samples(puzzle()).await,
async |transport| session::samples(transport, puzzle()).await,
);
both_ways(
&[PUZZLE_PAGE],
async |session| session.sample_text(puzzle(), 1).await,
async |transport| session::sample_text(transport, puzzle(), 1).await,
);
both_ways(
&[PUZZLE_PAGE],
async |session| session.sample_lines(puzzle(), 1).await,
async |transport| session::sample_lines(transport, puzzle(), 1).await,
);
both_ways(
&[EVENTS],
async |session| session.stars().await,
async |transport| session::stars(transport).await,
);
both_ways(
&[PUZZLE_PAGE],
async |session| session.accepted_answer(puzzle(), Part::Two).await,
async |transport| session::accepted_answer(transport, puzzle(), Part::Two).await,
);
both_ways(
&[CORRECT],
async |session| session.submit(puzzle(), Part::One, "514579").await,
async |transport| session::submit(transport, puzzle(), Part::One, "514579").await,
);
both_ways(
&[ALREADY_COMPLETE, PUZZLE_PAGE],
async |session| session.submit(puzzle(), Part::Two, "241861950").await,
async |transport| session::submit(transport, puzzle(), Part::Two, "241861950").await,
);
}
#[test]
fn a_transport_behind_a_pointer_drives_the_endpoints_as_it_is() {
struct Tool {
transport: Arc<FakeTransport>,
}
let tool = Tool {
transport: Arc::new(FakeTransport::serving("1721\n979\n366\n")),
};
let input = block_on(session::input_text(&tool.transport, puzzle())).expect("the input");
assert_eq!(input, "1721\n979\n366");
assert_eq!(
tool.transport.requested_urls(),
["https://adventofcode.com/2020/day/1/input"]
);
}
#[test]
fn a_cooldown_is_an_error_a_caller_can_branch_on() {
let session = Session::with_transport(FakeTransport::serving(COOLDOWN));
let error =
block_on(session.submit(puzzle(), Part::Two, "241861950")).expect_err("nothing was judged");
match error {
Error::Cooldown { wait } => assert_eq!(wait, Duration::from_secs(270)),
other => panic!("expected a cooldown, got {other}"),
}
}
#[test]
fn an_expired_cookie_is_an_error_a_caller_can_branch_on() {
let transport = FakeTransport::new();
transport.push(Response::new(400, LOGGED_OUT));
let session = Session::with_transport(transport);
let error = block_on(session.input_text(puzzle())).expect_err("the cookie was not accepted");
assert!(matches!(error, Error::Unauthorized), "{error}");
}
#[test]
fn a_coordinate_that_is_not_a_puzzle_never_reaches_the_transport() {
let session = Session::with_transport(FakeTransport::new());
assert!(Puzzle::at(2025, 25).is_err());
assert!(Puzzle::at(2014, 1).is_err());
assert!(session.transport().requests().is_empty());
}
#[test]
fn samples_and_accepted_answers_come_off_the_puzzle_page() {
let transport = FakeTransport::new();
transport.push_body(PUZZLE_PAGE).push_body(PUZZLE_PAGE);
let session = Session::with_transport(transport);
block_on(async {
let samples = session.samples(puzzle()).await.expect("two samples");
assert_eq!(samples.len(), 2);
assert!(samples[0].starts_with("1721"));
let answer = session
.accepted_answer(puzzle(), Part::Two)
.await
.expect("part two is solved on this page");
assert_eq!(answer, "241861950");
});
}