using afButterusing afBedSheet::HttpPipelineusing afBedSheet::HttpResponseHeadersusing web::Cookieusing web::WebOutStreamusing web::WebModusing web::WebRequsing web::WebResusing web::WebSessionusing inetusing concurrent::Actor** A 'Butter' terminator that makes requests against a given `BedServer`.class BedTerminator : ButterMiddleware {** The 'BedServer' this terminator makes calls against. BedServer bedServer** The session used by the client. Returns 'null' if it has not yet been created. WebSession? session := BounceWebSession(){// the null thing is for bounce clients to know if the session has been created or not. Technically this is not // perfect wisp behaviour, for if an obj were to be added then immediately removed, a wisp session would still // be created - pfft! Edge case! get { &session.map.isEmpty ? null : &session }private set {}}** Create a BedTerminator attached to the given 'BedServer'internalnew make(BedServer bedServer){this.bedServer = bedServer}override ButterResponse sendRequest(Butter butter, ButterRequest req){if(!req.uri.isPathOnly)throw Err("Request URIs for Bed App testing should only be a path, e.g. `/index` vs `${req.uri}`")if(!req.uri.isPathAbs)throw Err("Request URIs for Bed App testing should start with a slash, e.g. `/index` vs `${req.uri}`")try{ bounceWebRes := BounceWebRes() Actor.locals["web.req"] = toWebReq(req, &session) Actor.locals["web.res"] = bounceWebRes httpPipeline := (HttpPipeline) bedServer.registry.dependencyByType(HttpPipeline#) httpPipeline.servicereturn bounceWebRes.toButterResponse}finally{ Actor.locals.remove("web.req") Actor.locals.remove("web.res")}}** Shuts down the associated 'BedServer' and the running web app. Void shutdown(){ bedServer.shutdown}internal WebReq toWebReq(ButterRequest req, WebSession session){ BounceWebReq {it.version = req.versionit.method = req.methodit.uri = req.uriit.headers = req.headers.mapit.session = sessionit.in = req.body.seek(0).in}}}internalclass BounceWebReq : WebReq {privatestaticconst WebMod webMod := BounceDefaultMod()override WebMod mod := webModoverride IpAddr remoteAddr(){ IpAddr("127.0.0.1")}override Int remotePort(){ 80 }override SocketOptions socketOptions(){ TcpSocket().options }override Version versionoverride Str methodoverride Uri urioverride Str:Str headersoverride WebSession sessionoverride InStream innew make(|This|in){ in(this)}}** Adapted from WispReq to mimic the same uncommitted behaviour internalclass BounceWebRes : WebRes {private Buf bufprivate WebOutStream webOutnew make(){this.buf = Buf()this.webOut = WebOutStream(buf.out)this.headers = Str:Str[:]{it.caseInsensitive = true}this.cookies = [,]}override Int statusCode := 200 { set { checkUncommitted &statusCode = it}}override Str:Str headers { get { checkUncommitted; return &headers }}override Cookie[] cookies { get { checkUncommitted; return &cookies }}override Bool isCommitted := false{private set }override WebOutStream out(){ commitreturn webOut}override Void redirect(Uri uri, Int statusCode := 303){ checkUncommittedthis.statusCode = statusCode headers["Location"] = uri.encode headers["Content-Length"] = "0" commit done}override Void sendErr(Int statusCode, Str? msg := null){// write message to buffer buf := Buf() bufOut := WebOutStream(buf.out) bufOut.docType bufOut.html bufOut.head.title.w("$statusCode ${statusMsg[statusCode]}").titleEnd.headEnd bufOut.body bufOut.h1.w(statusMsg[statusCode]).h1Endif(msg != null) bufOut.w(msg).nl bufOut.bodyEnd bufOut.htmlEnd// write response checkUncommittedthis.statusCode = statusCode headers["Content-Type"] = "text/html; charset=UTF-8" headers["Content-Length"] = buf.size.toStrthis.out.writeBuf(buf.flip) done}override Bool isDone := false{private set }override Void done(){ isDone = true}internal Void checkUncommitted(){if(isCommitted)throw Err("WebRes already committed")}internal Void commit(){if(isCommitted)return isCommitted = true}internal Void close(){ commit webOut.close}internal ButterResponse toButterResponse(){// FIXME: WebUtil.parseHeaders() can't handle more than 1 Set-Cookie, as the max-age contains a ','if(!&cookies.isEmpty) &headers["Set-Cookie"] = &cookies.first.toStrreturn ButterResponse(&statusCode, statusMsg[statusCode], &headers, buf)}}internalclass BounceWebSession : WebSession {overrideconst Str id := "69"override Str:Obj? map := Str:Obj[:]override Void delete(){ map.clear}}internalconstclass BounceDefaultMod : WebMod {}