Add hermetic coverage for service boundaries
This commit is contained in:
parent
19ac156f4b
commit
de4f990492
3 changed files with 137 additions and 0 deletions
|
|
@ -332,6 +332,38 @@ fn random_hex<const N: usize>() -> String {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use actix_web::{http::StatusCode, test as actix_test};
|
||||||
|
|
||||||
|
#[actix_web::test]
|
||||||
|
async fn trace_header() {
|
||||||
|
let app = actix_test::init_service(
|
||||||
|
App::new()
|
||||||
|
.wrap(from_fn(trace_request))
|
||||||
|
.route("/", web::get().to(live)),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
let trace_id = "4bf92f3577b34da6a3ce929d0e0e4736";
|
||||||
|
let request = actix_test::TestRequest::get()
|
||||||
|
.uri("/")
|
||||||
|
.insert_header(("traceparent", format!("00-{trace_id}-00f067aa0ba902b7-01")))
|
||||||
|
.to_request();
|
||||||
|
let response = actix_test::call_service(&app, request).await;
|
||||||
|
|
||||||
|
assert_eq!(response.status(), StatusCode::OK);
|
||||||
|
let trace = response
|
||||||
|
.headers()
|
||||||
|
.get("traceparent")
|
||||||
|
.unwrap()
|
||||||
|
.to_str()
|
||||||
|
.unwrap();
|
||||||
|
assert!(trace.starts_with(&format!("00-{trace_id}-")));
|
||||||
|
assert!(trace.ends_with("-01"));
|
||||||
|
assert_eq!(trace.len(), 55);
|
||||||
|
assert_eq!(
|
||||||
|
actix_test::read_body(response).await,
|
||||||
|
r#"{"status":"live"}"#
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn escapes_prometheus_labels() {
|
fn escapes_prometheus_labels() {
|
||||||
|
|
|
||||||
|
|
@ -250,3 +250,69 @@ fn generated_id(prefix: char) -> OpaqueId {
|
||||||
}
|
}
|
||||||
OpaqueId::parse(value).expect("generated identifier is valid")
|
OpaqueId::parse(value).expect("generated identifier is valid")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn response(status: StatusCode) -> Response {
|
||||||
|
http::Response::builder()
|
||||||
|
.status(status)
|
||||||
|
.body(String::new())
|
||||||
|
.unwrap()
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn classifies_statuses() {
|
||||||
|
assert!(classify(response(StatusCode::NO_CONTENT)).is_ok());
|
||||||
|
assert!(matches!(
|
||||||
|
classify(response(StatusCode::BAD_REQUEST)),
|
||||||
|
Err(WorkerError::InvalidRequest)
|
||||||
|
));
|
||||||
|
for status in [StatusCode::UNAUTHORIZED, StatusCode::FORBIDDEN] {
|
||||||
|
assert!(matches!(
|
||||||
|
classify(response(status)),
|
||||||
|
Err(WorkerError::Unauthenticated)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
assert!(matches!(
|
||||||
|
classify(response(StatusCode::CONFLICT)),
|
||||||
|
Err(WorkerError::PreconditionFailed)
|
||||||
|
));
|
||||||
|
for status in [
|
||||||
|
StatusCode::NOT_FOUND,
|
||||||
|
StatusCode::TOO_MANY_REQUESTS,
|
||||||
|
StatusCode::SERVICE_UNAVAILABLE,
|
||||||
|
] {
|
||||||
|
assert!(matches!(
|
||||||
|
classify(response(status)),
|
||||||
|
Err(WorkerError::ApiUnavailable)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn validates_base_url() {
|
||||||
|
let client = Client::new();
|
||||||
|
let valid = WorkerApiClient::new("https://api.example.test/", client.clone()).unwrap();
|
||||||
|
assert_eq!(valid.base_url, "https://api.example.test");
|
||||||
|
for url in ["http://api.example.test", "https://", "not-a-url"] {
|
||||||
|
assert!(matches!(
|
||||||
|
WorkerApiClient::new(url, client.clone()),
|
||||||
|
Err(WorkerError::InvalidConfiguration)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ids_are_unique() {
|
||||||
|
let operation = operation_id();
|
||||||
|
let second = operation_id();
|
||||||
|
let incarnation = incarnation_id();
|
||||||
|
assert!(operation.as_str().starts_with('o'));
|
||||||
|
assert!(incarnation.as_str().starts_with('w'));
|
||||||
|
assert_ne!(operation, second);
|
||||||
|
assert_ne!(operation, incarnation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
39
web/src/obs.browser.test.ts
Normal file
39
web/src/obs.browser.test.ts
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
import { afterEach, expect, test, vi } from 'vitest'
|
||||||
|
|
||||||
|
import { installClientErrorReporting } from './observability'
|
||||||
|
|
||||||
|
afterEach(() => vi.restoreAllMocks())
|
||||||
|
|
||||||
|
test('reports and cleans up', () => {
|
||||||
|
const beacon = vi.spyOn(navigator, 'sendBeacon').mockReturnValue(true)
|
||||||
|
const added = vi.spyOn(window, 'addEventListener')
|
||||||
|
const removed = vi.spyOn(window, 'removeEventListener')
|
||||||
|
const remove = installClientErrorReporting()
|
||||||
|
const error = added.mock.calls.find(([kind]) => kind === 'error')?.[1]
|
||||||
|
const rejection = added.mock.calls.find(
|
||||||
|
([kind]) => kind === 'unhandledrejection',
|
||||||
|
)?.[1]
|
||||||
|
|
||||||
|
expect(error).toEqual(expect.any(Function))
|
||||||
|
expect(rejection).toEqual(expect.any(Function))
|
||||||
|
;(error as EventListener)(new Event('error'))
|
||||||
|
;(rejection as EventListener)(new Event('unhandledrejection'))
|
||||||
|
|
||||||
|
expect(beacon.mock.calls).toEqual([
|
||||||
|
[
|
||||||
|
'/client-errors',
|
||||||
|
JSON.stringify({ kind: 'error', path: window.location.pathname }),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'/client-errors',
|
||||||
|
JSON.stringify({
|
||||||
|
kind: 'unhandled_rejection',
|
||||||
|
path: window.location.pathname,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
])
|
||||||
|
|
||||||
|
remove()
|
||||||
|
expect(removed).toHaveBeenCalledWith('error', error)
|
||||||
|
expect(removed).toHaveBeenCalledWith('unhandledrejection', rejection)
|
||||||
|
})
|
||||||
Loading…
Reference in a new issue