
ShopScenarioTest.pudu
Pudu95 lines5.3 KB
1/** @Test.Integration.ShopScenario.Suite — one shop wired through the factory end to end */2module Integration.ShopScenarioTest34import Std.Http as Http5import Std.Http.Server.Reply as Reply6import Std.Http.Server.Route as Route7import Std.Io as Io8import Std.Json as Json9import Std.Result as Result10import Std.Sync as Sync11import Std.Test as Test12import PuduLangHttpClient.Client as Client13import PuduLangHttpClient.Client.Json as JsonClient14import PuduLangHttpClient.Events as Events15import PuduLangHttpClient.Factory.Builder as Builder16import PuduLangHttpClient.Factory as Factory17import PuduLangHttpClient.Handlers.Authorization as Authorization18import PuduLangHttpClient.Handlers.Logging as Logging19import PuduLangHttpClient.Handlers.Metrics as Metrics20import PuduLangHttpClient.Handlers.Resilience as Resilience21import PuduLangHttpClient as HttpClient22import PuduLangHttpClient.Response as Response23import PuduLangHttpClient.Transport as Transport24import PuduLangLog.Configuration as Configuration25import PuduLangLog as Log26import PuduLangLog.Sinks.Memory as Memory27import PuduLangResilience.Retry as Retry28import Support.Loopback as Loopback2930/** @Test.Integration.ShopScenario.Product — a catalog entry read through a derived decoder */31type Product = { sku: Str, price: Int } derives Json.Encode, Json.Decode3233/** @Test.Integration.ShopScenario.Catalog — a typed client over the named catalog client */34type Catalog = { client: Client.Client }353637fn product(catalog: &Catalog, sku: Str) -> HttpClient.Outcome[Product] { JsonClient.getFromJson(&catalog.client, "products/" + sku) }383940fn main() -> Int {41 let attempts = Sync.counter(0)42 let server = Loopback.serve(&[43 Route.get("/products/:sku", fn(request: Route.Request) -> Http.Response {44 let attempt = Result.unwrapOr(Sync.increment(&attempts, 1), 0)45 if Route.header(&request, "authorization") != Some("Bearer t1") { return Reply.text(401, "who are you") }46 if attempt == 1 { return Http.Response{..Reply.text(503, "busy"), headers: [("retry-after", "0")]} }47 Reply.json(200, "\{\"sku\":\"" + match Route.param(&request, "sku") { case Some(found) => found case None => "" } + "\",\"price\":42\}")48 }),49 Route.post("/orders", |request: Route.Request| Reply.text(201, Route.body(&request)))50 ])51 let memory = Memory.create()52 let logger = Configuration.create().minimumLevel(Log.Information).writeTo(Memory.sink(&memory)).createLogger()53 let meter = Metrics.meter()54 let completions = Sync.counter(0)55 let listeners = Events.listeners().onRequestCompleted(fn(_event: Events.RequestCompleted) -> () { let _counted = Sync.increment(&completions, 1) })56 let quickRetry = Resilience.Standard{..Resilience.standardOptions(), retry: Retry.Options{..Resilience.standardOptions().retry, delay: 1, useJitter: false}}57 let resilient = match Resilience.standardWith(&quickRetry) {58 case Ok(found) => found59 case Err(_) => panic("the standard pipeline did not build")60 }61 let factory = match Factory.buildWith(&Factory.Options{listeners: listeners}, [62 Builder.defaults().observedBy(Logging.observer(logger)).withHandler(Metrics.handler(&meter)).withDefaultHeader("user-agent", "shop/1.0"),63 Builder.named("catalog").withBaseAddress(server.base + "/").withHandler(Resilience.handler(resilient)).withHandler(Authorization.bearer(|_refresh: Bool| Ok("t1"))).withTransport(Transport.defaults().withMaxConnectionsPerServer(4)),64 Builder.named("orders").withBaseAddress(server.base + "/").withTimeout(5000)65 ]) {66 case Ok(found) => found67 case Err(invalid) => panic(Factory.explain(&invalid))68 }69 let catalog = Factory.typed(&factory, "catalog", |client: Client.Client| Catalog{client: client})70 let tea = product(&catalog, "tea")71 let cake = product(&catalog, "cake")72 let ordered = Result.andThen(JsonClient.postAsJson(&Factory.createClient(&factory, "orders"), "orders", &Product{sku: "tea", price: 42}), |r: Response.Response| Response.readAs[Product](&r))73 let snapshot = Metrics.snapshot(&meter)74 let connections = Factory.statistics(&factory, "catalog")75 Factory.dispose(&factory)76 Loopback.stop(&server)77 let checks = Test.suite("Integration.ShopScenario", &[78 Test.equals("a typed client reads products after a busy answer is retried", &(tea, cake), &(Ok(Product{sku: "tea", price: 42}), Ok(Product{sku: "cake", price: 42}))),79 Test.equals("an order posted as JSON comes back as JSON", &ordered, &Ok(Product{sku: "tea", price: 42})),80 Test.equals("the server saw the retried attempt", &Result.unwrapOr(Sync.count(&attempts), 0), &3),81 Test.equals("metrics count each request once, outside the retries", &(snapshot.requests, snapshot.successful), &(3, 3)),82 Test.equals("every request was announced as completed", &Result.unwrapOr(Sync.count(&completions), 0), &3),83 Test.that("the catalog's connection was kept alive across attempts", match connections {84 case Some(stats) => stats.opened == 1 && stats.reused == 285 case None => false86 }),87 Test.that("the logical handler logged each request", Memory.withTemplate(&memory, "Start processing HTTP request \{Method\} \{Uri\}").length() == 3)88 ])89 let ran = Test.run(&checks)90 for failure in Test.failuresOf(&ran) {91 let _reported = Io.writeErrorLine(failure)92 }93 Test.report(&ran)94}95