Last active
August 23, 2024 11:19
-
-
Save somoza/6b5c336c46c5aca3aa92309aa7d9be2a to your computer and use it in GitHub Desktop.
K6 script to test Phoenix Liveview entire lifecycle, included socket connection.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import http from "k6/http"; | |
import { sleep, check, fail } from "k6"; | |
import ws from "k6/ws"; | |
export default function () { | |
const protocol = "https"; // You might want to avoid Cloudflare's DDoS protection or bot mitigation | |
const host = "domain.com"; | |
const origin = `${protocol}://${host}`; | |
const path = "/path/to/test"; | |
const url = `${origin}${path}`; | |
const wsProtocol = "ws"; | |
// Prevent errors redirection / loops | |
const options = { | |
redirects: 0, | |
}; | |
const httpResponse = http.get(url, options); | |
const { name: cookie_name, value: cookie_value } = | |
getLiveViewCookie(httpResponse); | |
const WSResponse = doLiveViewUpgrade( | |
host, | |
origin, | |
wsProtocol, | |
cookie_name, | |
cookie_value, | |
httpResponse, | |
url | |
); | |
checkStatus(WSResponse, 101); | |
check(httpResponse, { | |
"status 200": (r) => r.status === 200, | |
}); | |
sleep(1); | |
} | |
function getLiveViewCookie(res) { | |
let cookies = res.cookies; | |
if (!cookies) { | |
console.error("Cookie doesn't found"); | |
return false; | |
} else { | |
const [cookieKey] = Object.keys(cookies); | |
const liveViewCookie = cookies[cookieKey]; | |
return liveViewCookie[0]; | |
} | |
} | |
function doLiveViewUpgrade( | |
host, | |
testHost, | |
wsProto, | |
cookie_name, | |
cookie_value, | |
response, | |
url, | |
opts = {} | |
) { | |
const debug = opts.debug || false; | |
// The response html contains the LV websocket connection details | |
const props = grabLVProps(response); | |
const wsCsrfToken = props.wsCsrfToken; | |
const phxSession = props.phxSession; | |
const phxStatic = props.phxStatic; | |
const topic = `lv:${props.phxId}`; | |
const ws_url = `${wsProto}://${host}/live/websocket?vsn=2.0.0&_csrf_token=${wsCsrfToken}`; | |
if (debug) console.log(`connecting ${ws_url}`); | |
// LV handshake message | |
const joinMsg = JSON.stringify( | |
encodeMsg(null, 0, topic, "phx_join", { | |
url: url, | |
params: { | |
_csrf_token: wsCsrfToken, | |
_mounts: 0, | |
}, | |
session: phxSession, | |
static: phxStatic, | |
}) | |
); | |
console.log("Cookie", `${cookie_name}=${cookie_value}`); | |
var response = ws.connect( | |
ws_url, | |
{ | |
headers: { | |
Cookie: `${cookie_name}=${cookie_value}`, | |
Origin: testHost, | |
}, | |
}, | |
function (socket) { | |
socket.on("open", () => { | |
socket.send(joinMsg); | |
if (debug) console.log(`websocket open: phx_join topic: ${topic}`); | |
}), | |
socket.on("message", (message) => { | |
checkMessage(message, `"status":"ok"`); | |
socket.close(); | |
}); | |
socket.on("error", handleWsError); | |
socket.on("close", () => { | |
// should we issue a phx_leave here? | |
if (debug) console.log("websocket disconnected"); | |
}); | |
socket.setTimeout(() => { | |
console.log("2 seconds passed, closing the socket"); | |
socket.close(); | |
fail("websocket closed"); | |
}, 2000); | |
} | |
); | |
return response; | |
} | |
function encodeMsg(id, seq, topic, event, msg) { | |
return [`${id}`, `${seq}`, topic, event, msg]; | |
} | |
function handleWsError(e) { | |
if (e.error() != "websocket: close sent") { | |
let msg = `An unexpected error occurred: ${e.error()}`; | |
if (debug) console.log(msg); | |
fail(msg); | |
} | |
} | |
function grabLVProps(response) { | |
let elem = response.html().find("meta[name='csrf-token']"); | |
let wsCsrfToken = elem.attr("content"); | |
if (!check(wsCsrfToken, { "found WS token ": (token) => !!token })) { | |
fail("websocket csrf token not found"); | |
} | |
elem = response.html().find("div[data-phx-main]"); | |
let phxSession = elem.data("phx-session"); | |
let phxStatic = elem.data("phx-static"); | |
let phxId = elem.attr("id"); | |
if (!check(phxSession, { "found phx-session": (str) => !!str })) { | |
fail("session token not found"); | |
} | |
if (!check(phxStatic, { "found phx-static": (str) => !!str })) { | |
fail("static token not found"); | |
} | |
return { wsCsrfToken, phxSession, phxStatic, phxId }; | |
} | |
export function checkStatus(response, status, msg = "request failed") { | |
if ( | |
!check(response, { | |
"status OK": (res) => res.status.toString() === `${status}`, | |
}) | |
) { | |
fail(`${msg} (Status: ${response.status.toString()}. Expected: ${status})`); | |
} | |
} | |
export function checkMessage(message, regex, msg = "unexpected ws message") { | |
if (!check(msg, { "ws msg OK": () => message.match(regex) })) { | |
console.log(message); | |
fail(`${msg} (Msg: ${message}. Expected: ${regex})`); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment