class HTTP_CONNECTION_TEST_SET

(source code)

description

Eiffel tests for class EL_HTTP_CONNECTION that can be executed with testing tool.

note
	description: "[
		Eiffel tests for class ${EL_HTTP_CONNECTION} that can be executed with testing tool.
	]"
	testing: "type/manual"

	author: "Finnian Reilly"
	copyright: "Copyright (c) 2001-2022 Finnian Reilly"
	contact: "finnian at eiffel hyphen loop dot com"

	license: "MIT license (See: en.wikipedia.org/wiki/MIT_License)"
	date: "2024-11-07 16:18:12 GMT (Thursday 7th November 2024)"
	revision: "75"

class
	HTTP_CONNECTION_TEST_SET

inherit
	EL_FILE_DATA_TEST_SET
		redefine
			on_clean, on_prepare
		end

	EL_MODULE_GEOLOCATION; EL_MODULE_HTML; EL_MODULE_IP_ADDRESS

	EL_MODULE_WEB; EL_MODULE_XML

	EL_SHARED_GEOGRAPHIC_INFO_TABLE; EL_SHARED_IP_ADDRESS_GEOLOCATION

	EL_SHARED_HTTP_STATUS

	SHARED_DEV_ENVIRON

create
	make

feature {NONE} -- Initialization

	make
		-- initialize `test_table'
		do
			make_named (<<
				["cached_documents",					 agent test_cached_documents],
				["cookies",								 agent test_cookies],
				["download_document_and_headers", agent test_download_document_and_headers],
				["download_image_and_headers",	 agent test_download_image_and_headers],
				["headers",								 agent test_headers],
				["http_hash_table",					 agent test_http_hash_table],
				["http_post",							 agent test_http_post],
				["image_headers",						 agent test_image_headers],
				["ip_address_info",					 agent test_ip_address_info],
				["ip_geolocation",					 agent test_ip_geolocation],
				["url_encoded",						 agent test_url_encoded],
				["web_archive",						 agent test_web_archive]
			>>)
		end

feature -- Tests

	test_cached_documents
		note
			testing: "[
				covers/{EL_CACHED_HTTP_FILE}.make,
				covers/{EL_HTTP_CONNECTION}.download
			]"
		local
			url: STRING; cached_file: EL_CACHED_HTTP_FILE
		do
			across << Http >> as protocol loop -- Https
				across Document_table as table loop
					url := protocol.item + Httpbin_url + table.key
					lio.put_labeled_string ("url", url)
					lio.put_new_line
					create cached_file.make (url, 24)

					if attached value_at_xpath (File.plain_text (cached_file.path), table.item.xpath) as value then
						assert ("xpath matches", value.starts_with (table.item.first_words))
					end
					lio.put_new_line
				end
			end
		end

	test_cookies
		-- HTTP_CONNECTION_TEST_SET.test_cookies
		local
			city_location, json_fields: EL_URI_QUERY_ZSTRING_HASH_TABLE
			url: STRING; cookies: EL_HTTP_COOKIE_TABLE
		do
			-- There is an issue with httpbin.org that prevents setting of 2 cookies with 1 call
			-- so we do a loop instead
			across new_city_location.query_string (True, False).split ('&') as nvp loop
				url := Set_cookie_url + nvp.item
				lio.put_labeled_string ("url", url)
				lio.put_new_line

				web.set_cookie_paths (Cookie_path)
				web.open (url)
				try_3_times (agent web.read_string_get)
				assert_same_string ("is redirection page", h1_text (web.last_string), "Redirecting...")
				web.close
			end

			create cookies.make_from_file (Cookie_path)
			web.open (Cookies_url)
			try_3_times (agent web.read_string_get)
			json_fields := new_json_fields (web.last_string)

			assert ("two cookies set", cookies.count = 2)
			across cookies as cookie loop
				lio.put_string_field (cookie.key, cookie.item); lio.put_string_field (" JSON", json_fields.item (cookie.key))
				lio.put_new_line

				assert ("json has cookie key", json_fields.has (cookie.key))
				assert ("cookie equals json value", json_fields.item (cookie.key) ~ cookie.item)
			end
		end

	test_download_document_and_headers
		local
			url: STRING; headers: like web.last_headers
		do
			across << Http >> as protocol loop -- Https
				across Document_table as table loop
					url := protocol.item + Httpbin_url + table.key
					lio.put_labeled_string ("url", url)
					lio.put_new_line
					web.open (url)

					try_3_times (agent web.read_string_head)
					headers := web.last_headers
					print_lines (web)
					assert_valid_headers (headers)
					if table.key ~ "xml" then
						assert_same_string ("mime_type", headers.mime_type, "application/xml")
					else
						assert_same_string ("mime_type", headers.mime_type, "text/html")
						assert_same_string ("encoding_name", headers.encoding_name, "utf-8")
					end
					try_3_times (agent web.read_string_get)
					if attached value_at_xpath (web.last_string, table.item.xpath) as value then
						assert ("xpath matches", value.starts_with (table.item.first_words))
					end
					assert ("valid content_length", headers.content_length = web.last_string.count)

					web.close
					lio.put_new_line
				end
			end
		end

	test_download_image_and_headers
		-- HTTP_CONNECTION_TEST_SET.test_download_image_and_headers
		note
			testing: "[
				covers/{EL_HTTP_CONNECTION}.read_string_head,
				covers/{EL_HTTP_HEADERS}.make, covers/{EL_HTTP_HEADERS}.set_table_field,
				covers/{EL_HTTP_HEADERS}.x_field
			]"
		local
			headers: like web.last_headers; image_url: STRING
		do
			across << "png", "jpeg", "webp", "svg" >> as image loop
				image_url := Image_url_stem + image.item
				web.open (image_url)
				try_3_times (agent web.read_string_head)
				print_lines (web)

				if attached new_image_path (image.item) as image_path then
					try_3_times (agent web.download (image_path))

					headers := web.last_headers
					assert_valid_headers (headers)
					assert ("valid content_type", headers.mime_type.starts_with ("image/" + image.item))
					assert ("valid content_length", headers.content_length = File.byte_count (image_path))
					assert ("valid encoding_name", headers.encoding_name.is_empty)
				end
				web.close
			end
		end

	test_headers
		-- HTTP_CONNECTION_TEST_SET.test_headers
		note
			testing: "[
				covers/{EL_HTTP_CONNECTION}.read_string_head,
				covers/{EL_HTTP_HEADERS}.make, covers/{EL_HTTP_HEADERS}.set_table_field,
				covers/{EL_HTTP_HEADERS}.x_field
			]"
		local
			headers: EL_HTTP_HEADERS
		do
			create headers.make (Headers_text)
			assert ("response_code OK", headers.response_code = Http_status.ok)
			assert_same_string ("mime_type", headers.mime_type, "text/html")
			assert_same_string ("encoding_name", headers.encoding_name, "UTF-8")
			across << "X-Powered-By", "powered_by" >> as key loop
				assert ("has powered by", headers.has_x_field (key.item))
				assert_same_string ("X-field", headers.x_field (key.item), "PHP/7.2.34")
			end
		end

	test_http_hash_table
		note
			testing: "covers/{EL_URI_QUERY_TABLE}.make_url"
		local
			table_1, table_2: EL_URI_QUERY_ZSTRING_HASH_TABLE
			query_string: STRING
		do
			create table_1.make_equal (2)
			table_1.set_string_general ("city", "Dún Búinne")
			table_1.set_string_general ("code", "+/xPVBTmoka3ZBeARZ8uKA==")
			query_string := table_1.url_query
			lio.put_line (query_string)
			create table_2.make_url (query_string)
			across table_2 as variable loop
				table_1.search (variable.key)
				assert ("has variable", table_1.found)
				assert ("same value", variable.item ~ table_1.found_item)
			end
		end

	test_http_post
		local
			city_location, json_fields: EL_URI_QUERY_ZSTRING_HASH_TABLE
			url: ZSTRING
		do
			city_location := new_city_location
			across << Http, Https >> as protocol loop
				url := protocol.item + Html_post_url
				lio.put_labeled_string ("url", url)
				lio.put_new_line
				web.open (url)
				web.set_post_parameters (city_location)
				web.read_string_post
				print_lines (web)

				json_fields := new_json_fields (web.last_string)
				across city_location as nvp loop
					assert ("has json value", json_fields.has (nvp.key))
					assert ("value is equal json value", json_fields.item (nvp.key) ~ nvp.item)
				end
				assert ("url echoed", json_fields.item ("url") ~ url)
				web.close
			end
		end

	test_image_headers
		-- using new web object for each request
		note
			testing: "covers/{EL_HTTP_CONNECTION}.read_string_head"
		local
			headers: like web.last_headers; image_url: STRING
		do
			across << "png", "jpeg", "webp", "svg" >> as image loop
				image_url := Image_url_stem + image.item
				web.open (image_url)
				try_3_times (agent web.read_string_head)
				print_lines (web)

				headers := web.last_headers
				assert_valid_headers (headers)
				assert ("valid content_type", headers.mime_type.starts_with ("image/" + image.item))

				web.close
			end
		end

	test_ip_address_info
		-- HTTP_CONNECTION_TEST_SET.test_ip_address_info
		note
			testing: "[
				covers/{EL_SETTABLE_FROM_JSON_STRING}.set_from_json,
				covers/{EL_IP_ADDRESS_ROUTINES}.to_number, covers/{EL_IP_ADDRESS_ROUTINES}.to_string,
				covers/{EL_IP_ADDRESS_INFO_TABLE}.new_info,
				covers/{EL_CODE_REPRESENTATION}.to_value, covers/{EL_CODE_REPRESENTATION}.to_string
			]"
		local
			info: EL_IP_ADDRESS_GEOGRAPHIC_INFO; ip_number: NATURAL
		do
			ip_number := IP_address.to_number (www_eiffel_loop_com)
			info := Geographic_info_table.item (ip_number)
			assert_same_string ("same asn code", info.asn_, "AS8560")
			assert_same_string ("same location", info.location, "United Kingdom, England")
			assert_same_string ("same country", info.country_, "GB")
			assert_same_string ("same country code", info.country_code_, "GB")
			assert_same_string ("same 3 letter country code", info.country_code_iso3_, "GBR")
			assert_same_string ("same top level domain", info.country_tld_, ".uk")
			assert_same_string ("same continent", info.continent_code_, "EU")
			assert_same_string ("same currency", info.currency_, "GBP")
			assert_same_string ("same region code", info.region_code_, "ENG")
			assert_same_string ("same version", info.version_, "IPv4")

			assert ("same location", info.location ~ IP_country_region_table.item (ip_number))
			assert ("same ip", info.ip_address ~ Www_eiffel_loop_com)
			assert ("same area", info.country_area.to_integer_32 = 244820)

			-- `utc_offset' can vary depending if daylight saving is in effect
			assert ("same UTC offset", ("+0000, +0100").has_substring (info.utc_offset_))

			lio.put_integer_field ("size of info", info.deep_physical_size)
			lio.put_new_line
		end

	test_ip_geolocation
		-- HTTP_CONNECTION_TEST_SET.test_ip_geolocation
		note
			testing: "[
				covers/{EL_GEOLOCATION_ROUTINES}.for_number
			]"
		local
			ip: NATURAL
		do
		-- shetinin-school.ru has address 31.31.196.245
			ip := Ip_address.to_number ("31.31.196.245")
			assert_same_string (Void, Geolocation.for_number (ip), "Russia, Moscow")
		--	fasthosts.co.uk has address 213.171.195.48
			ip := Ip_address.to_number ("213.171.195.48")
			assert_same_string (Void, Geolocation.for_number (ip), "United Kingdom of Great Britain and Northern Ireland")
		end

	test_url_encoded
		-- HTTP_CONNECTION_TEST_SET.test_url_encoded
		note
			testing: "[
				covers/{EL_HTTP_CONNECTION}.read_string_head,
				covers/{EL_HTTP_HEADERS}.make, covers/{EL_HTTP_HEADERS}.set_table_field,
				covers/{EL_HTTP_HEADERS}.x_field
			]"
		local
			url: STRING; s: EL_STRING_8_ROUTINES; powered_by_title: STRING
		do
			across << "einführung.html", "einf%%C3%%BChrung.html" >> as name loop
				url := "https://myching.software/de/manual/" + name.item
				web.open (url)
				web.read_string_head
				if attached web.last_headers as header then
					assert ("Response OK", header.response_code = Http_status.ok)
					assert_same_string ("content_type", header.content_type, "text/html; charset=UTF-8")
					across << "X-Powered-By", "powered_by" >> as key loop
						assert_same_string (Void, header.x_field (key.item), "Eiffel-Loop Fast-CGI servlets")
					end
				end
				web.close
			end
		end

	test_web_archive
		-- HTTP_CONNECTION_TEST_SET.test_web_archive
		local
			url_parts: EL_STRING_8_LIST; archive: EL_WEB_ARCHIVE_HTTP_CONNECTION
			url: STRING
		do
			create archive.make
			url := "http://www.emotionaliching.com/lofting/bx101010.html"

			create url_parts.make_split (url, '/')
			url_parts [3].append (":80") -- append port number to domain

			if attached Archive.wayback (url) as closest and then closest.status.to_boolean then
				assert ("status OK", closest.status = Http_status.ok)
				assert ("available OK", closest.available)
				assert ("timestamp OK", closest.timestamp = 20130124193934)
				assert ("valid wayback ending", closest.url.ends_with (url_parts.joined ('/')))
				assert ("valid wayback start", closest.url.starts_with ("http://web"))
			end
		end

feature -- Unused

	test_http_errors
		local
			url: STRING
		do
			url := "http://localhost/en/home/my-ching.html"
			web.open (url)
			web.read_string_get
			if web.has_error then
				web.put_error (lio)
			end
			lio.put_labeled_substitution ("PAGE ERROR", "%S %S", [web.page_error_code, web.page_error_name])
			lio.put_new_line

			web.close
		end

	test_open_url
		local
			url: EL_URL
		do
			-- This works on command line: curl -I http://www.académie-française.fr/

			-- If libcurl is built with IDN support, the server name part of the URL can use an
			-- "international name" by using the current encoding (according to locale) or UTF-8
			-- (when winidn is used; or a Windows Unicode build using libidn2)

			create url.make ("http://www.académie-française.fr/")
			web.open_url (url)
			web.read_string_head
			assert_same_string ("location", web.last_headers.location, "https://www.academie-francaise.fr/")
			web.close
		end

feature {NONE} -- Events

	on_prepare
		do
			Precursor
			File_system.make_directory (Cookie_path.parent)
			Web.set_log_output (lio)
		end

	on_clean
		do
			Precursor
			Web.set_silent_output
		end

feature {NONE} -- Implementation

	assert_document_retrieved (expected_leading_string_and_count: IMMUTABLE_STRING_8; doc_text: STRING; line_count: INTEGER)
		local
			list: EL_STRING_8_LIST
		do
			list := expected_leading_string_and_count.to_string_8
			assert ("2 items", list.count = 2)
			assert ("retrieved", doc_text.starts_with (list.first) and line_count = list.last.to_integer)
		end

	assert_valid_headers (headers: like web.last_headers)
		do
			assert ("valid date_stamp", headers.date_stamp.date ~ create {DATE}.make_now)
			if headers.response_code = Http_status.ok then
				assert ("valid server", is_server_name (headers.server))
			else
				lio.put_integer_field (Http_status.name (headers.response_code), headers.response_code)
				lio.put_new_line
				failed ("valid response_code")
			end
		end

	element_text (name: STRING; a_text: STRING): ZSTRING
		local
			text: ZSTRING
		do
			text := a_text
			Result := text.substring_between_general (Html.open_tag (name), Html.closed_tag (name), 1)
		end

	em_text (text: ZSTRING): ZSTRING
		do
			Result := element_text ("em", text)
		end

	h1_text (text: STRING): ZSTRING
		do
			Result := element_text ("h1", text)
		end

	is_server_name (text: STRING): BOOLEAN
		local
			parts: LIST [STRING]
		do
			parts := text.split ('/')
			Result := parts.count = 2 and then across parts.last.split ('.') as n all n.item.is_natural end
		end

	print_lines (a_web: like web)
		do
			across a_web.last_string.split ('%N') as line loop
				lio.put_line (line.item)
			end
			lio.put_new_line
		end

	title_text (text: ZSTRING): ZSTRING
		do
			Result := element_text ("title", text)
		end

	try_3_times (web_operation: PROCEDURE)
		-- try 3 times if error is gateway timeout
		require
			target_is_web: web_operation.target = web
		local
			done: BOOLEAN
		do
			across 1 |..| 3 as n until done loop
				lio.put_integer_field ("Try number", n.item)
				lio.put_new_line
				web_operation.apply
				if web.has_error then
					done := not web.is_gateway_timeout
				else
					done := (web.last_headers.response_code) = Http_status.ok
				end
			end
		end

	value_at_xpath (text, xpath: STRING): ZSTRING
		local
			xdoc: EL_XML_DOC_CONTEXT
		do
			if XML.is_xml_declaration (text) then
				create xdoc.make_from_string (text)

			elseif HTML.is_document (text) then
				create xdoc.make_from_xhtml (text)
			else
				create xdoc.make_from_fragment (text, {CODE_PAGE_CONSTANTS}.Utf8)
			end
			Result := xdoc @ xpath
			Result.left_adjust
		end

feature {NONE} -- Factory

	new_city_location: EL_URI_QUERY_ZSTRING_HASH_TABLE
		do
			create Result.make_equal (2)
			Result.set_string ("city", "Köln")
			Result.set_string ("district", "Köln-Altstadt-Süd")
		end

	new_image_path (name: STRING): FILE_PATH
		do
			Result := Work_area_absolute_dir + "image"
			Result.add_extension (name)
		end

	new_json_fields (json_data: STRING): EL_URI_QUERY_ZSTRING_HASH_TABLE
		local
			pair_list: JSON_NAME_VALUE_LIST; s: EL_STRING_8_ROUTINES
			value: EL_COOKIE_STRING_8
		do
			create pair_list.make (json_data)
			create Result.make_equal (pair_list.count)
			from pair_list.start until pair_list.after loop
				create value.make_encoded (pair_list.item_value (False).to_latin_1)
				if not s.is_character (value, '{') then
					Result.set_string (pair_list.item_name (True), value.decoded)
				end
				pair_list.forth
			end
		end

feature {NONE} -- Text manifest

	Headers_text: STRING = "[
		HTTP/1.1 200 OK
		Accept-Ranges: bytes
		access-control-allow-origin: *
		age: 0
		alt-svc: h3=":443"; ma=2592000, h3-29=":443"; ma=2592000, h3-Q050=":443"
		cache-control: no-cache, no-store, must-revalidate, max-age=0
		Connection: Keep-Alive
		Content-Length: 62128
		content-type: text/html; charset=UTF-8
		Date: Thu, 02 Sep 2021 10:48:51 GMT
		etag: "234ea00d6e2defa13d5ca7844fdd1821-ssl"
		host-header: c2hhcmVkLmJsdWVob3N0LmNvbQ==
		Keep-Alive: timeout=5, max=100
		Last-Modified: Fri, 25 Sep 2020 01:40:31 GMT
		Link: <https://www.ichingmeditations.com/rb>; rel=shorturl, <https://www.ichingmeditations.com/wp-json/>
		memento-datetime: Mon, 14 Jan 2013 21:39:36 GMT
		location: http://www.laetusinpraesens.org
		Referrer-Policy: no-referrer-when-downgrade
		Permissions-Policy: interest-cohort=()
		Server: Apache
		Set-Cookie: PHPSESSID=0208ad1dda50c9c00f6d9ea60285266e; path=/; secure; HttpOnly
		strict-transport-security: max-age=31536000; includeSubDomains; preload
		Upgrade: h2,h2c
		vary: User-Agent
		x-content-type-options: nosniff
		x-frame-options: SAMEORIGIN
		x-nf-request-id: 01FEK1KJWRMEY7HVCHM7B3V8FY
		X-Pingback: https://www.ichingmeditations.com/xmlrpc.php
		x-powered-by: PHP/7.2.34
	]"

feature {NONE} -- Constants

	Cookie_path: FILE_PATH
		once
			Result := Work_area_absolute_dir + {STRING_32} "Gefäß/cookie.txt"
		end

	Cookies_url: STRING = "http://httpbin.org/cookies"

	Document_table: EL_HASH_TABLE [TUPLE [xpath, first_words: STRING], STRING]
		-- table of predicates to test if document was retrieved
		once
			create Result.make_assignments (<<
				["html",			["/html/body/div/p", "Availing himself"]],
				["links/10/0",	["/html/body/a", "1"]],
				["xml",			["/slideshow/slide[1]/title", "Wake up"]]
			>>)
		end

	Html_post_url: STRING = "://httpbin.org/post"

	Http: STRING = "http"

	Httpbin_url: STRING = "://httpbin.org/"

	Https: STRING = "https"

	Image_url_stem: STRING = "http://httpbin.org/image/"

	Set_cookie_url: STRING = "http://httpbin.org/cookies/set?"

	www_eiffel_loop_com: STRING = "77.68.64.12"

end