mirror of
https://github.com/Cuteminded/jellyframe.git
synced 2026-10-28 03:00:39 +01:00
143 lines
8.0 KiB
GDScript
143 lines
8.0 KiB
GDScript
extends SceneTree
|
|
|
|
var server := TCPServer.new()
|
|
var peer: StreamPeerTCP
|
|
var incoming := ""
|
|
var payload := "{}"
|
|
var code := 200
|
|
var last_request := ""
|
|
var queued_payloads: Array[String] = []
|
|
var failures := 0
|
|
|
|
func _initialize() -> void:
|
|
if server.listen(18963, "127.0.0.1") != OK:
|
|
push_error("Test port 18963 is unavailable.")
|
|
quit(1)
|
|
return
|
|
call_deferred("run_checks")
|
|
|
|
func _process(_delta: float) -> bool:
|
|
if server.is_connection_available():
|
|
peer = server.take_connection()
|
|
incoming = ""
|
|
if peer == null: return false
|
|
peer.poll()
|
|
if peer.get_available_bytes() > 0:
|
|
incoming += peer.get_utf8_string(peer.get_available_bytes())
|
|
var end := incoming.find("\r\n\r\n")
|
|
if end < 0: return false
|
|
var content_length := 0
|
|
for line in incoming.substr(0, end).split("\r\n"):
|
|
if line.to_lower().begins_with("content-length:"):
|
|
content_length = int(line.split(":")[1].strip_edges())
|
|
if incoming.substr(end + 4).to_utf8_buffer().size() < content_length: return false
|
|
last_request = incoming
|
|
if not queued_payloads.is_empty(): payload = queued_payloads.pop_front()
|
|
var response := "HTTP/1.1 %d Test\r\nContent-Type: application/json\r\nContent-Length: %d\r\nConnection: close\r\n\r\n%s" % [code, payload.to_utf8_buffer().size(), payload]
|
|
peer.put_data(response.to_utf8_buffer())
|
|
peer.disconnect_from_host()
|
|
peer = null
|
|
return false
|
|
|
|
func run_checks() -> void:
|
|
var api := JellyfinClient.new()
|
|
root.add_child(api)
|
|
api.configure("http://127.0.0.1:18963/jellyfin")
|
|
api.device_id = "test-device"
|
|
payload = JSON.stringify({"AccessToken": "fixture-token", "User": {"Id": "fixture-user"}})
|
|
var result := await api.login("a\"b", "test-only-password")
|
|
check(result.ok and api.user_id == "fixture-user", "Login consumes the user and token")
|
|
check(last_request.begins_with("POST /jellyfin/Users/AuthenticateByName "), "Login preserves the server base path")
|
|
var body: Dictionary = JSON.parse_string(last_request.split("\r\n\r\n", true, 1)[1])
|
|
check(body.Username == 'a"b' and body.Pw == "test-only-password", "Credentials are JSON encoded")
|
|
payload = '{"Items":[],"TotalRecordCount":0}'
|
|
queued_payloads = [payload, '{"Items":[{"Id":"first","SeasonId":"season-one","UserData":{"Played":true}}]}']
|
|
result = await api.series_playback("finished show")
|
|
check(result.ok and result.data.Items[0].Id == "first", "Completed series fall back to their first episode")
|
|
check(last_request.begins_with("GET /jellyfin/Shows/finished%20show/Episodes?") and "Limit=1" in last_request and "SortBy=ParentIndexNumber%2CIndexNumber" in last_request and "SortOrder=Ascending" in last_request and "IsMissing=false" in last_request, "Fallback loads the first available episode in season and episode order")
|
|
payload = '{"Items":[],"TotalRecordCount":0}'
|
|
await api.next_up("finished show")
|
|
check(last_request.begins_with("GET /jellyfin/Shows/NextUp?"), "The Next Up section does not fall back to watched episodes")
|
|
code = 500
|
|
result = await api.series_playback("offline show")
|
|
check(not result.ok and last_request.begins_with("GET /jellyfin/Shows/NextUp?"), "Failed Next Up requests do not restart a series")
|
|
code = 200
|
|
await api.library("hello & world", 24)
|
|
check("SearchTerm=hello%20%26%20world" in last_request, "Search escaping survives HTTP")
|
|
check('Token="fixture-token"' in last_request, "Authenticated calls send the token header")
|
|
check("StartIndex=24" in last_request, "Library pagination is sent")
|
|
check("Limit=25" in last_request, "Library pages request 25 titles")
|
|
check("IncludeItemTypes=Movie%2CSeries%2CVideo%2CPhoto" in last_request, "Browsing and search include photos")
|
|
await api.library("space", 48, "movies-id")
|
|
check("ParentId=movies-id" in last_request and "StartIndex=48" in last_request and "SearchTerm=space" in last_request, "Search and pagination stay inside the selected library")
|
|
await api.views()
|
|
check(last_request.begins_with("GET /jellyfin/UserViews?") and "UserId=fixture-user" in last_request, "Sidebar loads the signed-in user's libraries")
|
|
await api.seasons("show id")
|
|
check(last_request.begins_with("GET /jellyfin/Shows/show%20id/Seasons?"), "Seasons load from the selected series with an escaped ID")
|
|
payload = '{"Items":[{"Id":"next","UserData":{"PlaybackPositionTicks":100}}]}'
|
|
result = await api.next_up("show & id")
|
|
check(result.ok and last_request.begins_with("GET /jellyfin/Shows/NextUp?") and "SeriesId=show%20%26%20id" in last_request and "UserId=fixture-user" in last_request, "Next Up is scoped to the selected series and signed-in user")
|
|
check("DisableFirstEpisode" not in last_request and "EnableResumable=true" in last_request, "A partially watched first episode remains resumable")
|
|
payload = '{"Items":[{"Id":"next","UserData":{"PlaybackPositionTicks":0}}]}'
|
|
result = await api.series_playback("new show")
|
|
check(result.ok and result.data.Items[0].Id == "next" and "SeriesId=new%20show" in last_request and "DisableFirstEpisode" not in last_request, "Series playback allows the first episode for a new show")
|
|
await api.next_up("show id")
|
|
check("DisableFirstEpisode=true" in last_request and "EnableRewatching=false" in last_request, "Unstarted episodes require prior series watch history")
|
|
payload = '{"Items":[],"TotalRecordCount":0}'
|
|
await api.episodes("show id", "season & id", 100)
|
|
check("SeasonId=season%20%26%20id" in last_request and "StartIndex=100" in last_request, "Episode pagination stays inside the selected season")
|
|
check("Fields=Overview%2CUserData%2CMediaSources" in last_request and "IsMissing=false" in last_request, "Episode cards request versions and summaries and exclude missing episodes")
|
|
payload = '{"Played":true,"PlaybackPositionTicks":0}'
|
|
code = 200
|
|
result = await api.set_watched("movie id", true)
|
|
check(result.ok and result.data.Played and last_request.begins_with("POST /jellyfin/Users/fixture-user/PlayedItems/movie%20id "), "Mark watched sends an authenticated POST for the selected item")
|
|
payload = '{"Played":false,"PlaybackPositionTicks":0}'
|
|
result = await api.set_watched("episode id", false)
|
|
check(result.ok and not result.data.Played and last_request.begins_with("DELETE /jellyfin/Users/fixture-user/PlayedItems/episode%20id "), "Mark unwatched sends DELETE and returns updated user data")
|
|
code = 401
|
|
payload = ""
|
|
result = await api.request("/Users/Me")
|
|
check(not result.ok and result.status == 401, "Unauthorised responses are actionable")
|
|
code = 204
|
|
result = await api.request("/Sessions/Playing/Stopped", HTTPClient.METHOD_POST)
|
|
check(result.ok, "Empty success responses are accepted")
|
|
code = 200
|
|
payload = "<html>Wrong proxy route</html>"
|
|
result = await api.request("/Users/Me")
|
|
check(not result.ok, "Non-JSON proxy responses are rejected")
|
|
await check_playback_reports(api)
|
|
server.stop()
|
|
print("HTTP integration checks: %s" % ("PASS" if failures == 0 else "%d FAILED" % failures))
|
|
quit(1 if failures else 0)
|
|
|
|
func check_playback_reports(api: JellyfinClient) -> void:
|
|
var session := PlaybackSession.new()
|
|
session.api = api
|
|
session.item = {"Id": "fixture-item"}
|
|
session.source = JSON.parse_string('{"Id":"fixture-source","DefaultAudioStreamIndex":1}')
|
|
session.session_id = "fixture-session"
|
|
root.add_child(session)
|
|
code = 204
|
|
payload = ""
|
|
for event in ["Playing", "Progress", "Stopped"]:
|
|
await session.report(event)
|
|
var body := last_request.split("\r\n\r\n", true, 1)[1]
|
|
check('"AudioStreamIndex":1,' in body, "%s sends the parsed server audio index as a JSON integer, not 1.0" % event)
|
|
session.source["DefaultAudioStreamIndex"] = null
|
|
await session.report("Playing")
|
|
check('"AudioStreamIndex"' not in last_request, "Sources without a default audio track omit the optional index")
|
|
session.audio_index = 3
|
|
await session.report("Progress")
|
|
check('"AudioStreamIndex":3,' in last_request, "Explicit audio selection is reported as an integer")
|
|
var errors: Array[String] = []
|
|
session.reporting_failed.connect(func(message: String): errors.append(message))
|
|
code = 400
|
|
await session.report("Progress")
|
|
check(errors.size() == 1 and "HTTP 400" in errors[0], "Rejected playback reports are exposed instead of silently ignored")
|
|
session.queue_free()
|
|
|
|
func check(condition: bool, message: String) -> void:
|
|
if not condition:
|
|
failures += 1
|
|
push_error(message)
|