Merge lp:~gesha/linaro-license-protection/997211 into lp:~linaro-automation/linaro-license-protection/trunk

Proposed by Georgy Redkozubov
Status: Merged
Merged at revision: 72
Proposed branch: lp:~gesha/linaro-license-protection/997211
Merge into: lp:~linaro-automation/linaro-license-protection/trunk
Diff against target: 626 lines (+331/-178)
6 files modified
.htaccess (+8/-3)
licenses/LicenseHelper.php (+14/-27)
licenses/license.php (+7/-7)
testing/filefetcher.py (+0/-129)
testing/license_protected_file_downloader.py (+284/-0)
testing/test_click_through_license.py (+18/-12)
To merge this branch: bzr merge lp:~gesha/linaro-license-protection/997211
Reviewer Review Type Date Requested Status
Stevan Radaković code Approve
Review via email: mp+105452@code.launchpad.net

Description of the change

This branch adds proper redirection handling from license.php back to mod_rewrite.
Now correct URL is put in the location bar in cases of 404 and 403 errors instead of /licenses/license.php

To post a comment you must log in.
Revision history for this message
Stevan Radaković (stevanr) wrote :

You have some incorrect indentation in LicenseHelper.
Furthermore, is "redirectlicensephp" cookie supposed to be set in all three possible cases of redirection?

review: Needs Fixing (code)
Revision history for this message
Georgy Redkozubov (gesha) wrote :

> You have some incorrect indentation in LicenseHelper.
> Furthermore, is "redirectlicensephp" cookie supposed to be set in all three
> possible cases of redirection?

It is strictly needed for 200 and 403 cases but it is also needed to identify 403 status in .htaccess and this can't be done with server variables, so I used the same cookie.

71. By Georgy Redkozubov

Fixed indentation.

Revision history for this message
Stevan Radaković (stevanr) wrote :

Looks good now.

review: Approve (code)
72. By Georgy Redkozubov

[merge] Replace the filefetcher with newer version. Tests updated to use new filefetcher. License protection unit tests implemented, also light refactoring of license.php

73. By Georgy Redkozubov

Added test for 404 (Not found) error.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file '.htaccess'
2--- .htaccess 2012-05-02 11:33:12 +0000
3+++ .htaccess 2012-05-11 12:16:20 +0000
4@@ -13,12 +13,12 @@
5 ## without port number for use in cookie domain
6 RewriteCond %{SERVER_PORT} !^80$ [OR]
7 RewriteCond %{SERVER_PORT} !^443$
8-RewriteCond %{HTTP_HOST} (.*)(\:.*)
9+RewriteCond %{HTTP_HOST} ^([^:]*)$
10 RewriteRule .* - [E=CO_DOMAIN:%1]
11
12 RewriteCond %{SERVER_PORT} !^80$ [OR]
13 RewriteCond %{SERVER_PORT} !^443$
14-RewriteCond %{HTTP_HOST} (^.*$)
15+RewriteCond %{HTTP_HOST} ^([^:]*):(.*)$
16 RewriteRule .* - [E=CO_DOMAIN:%1]
17
18 ## Let internal hosts through always.
19@@ -85,7 +85,12 @@
20 RewriteRule .* - [L]
21
22 ## Unset cookie indicating redirect from license.php
23-RewriteCond %{HTTP_COOKIE} redirectlicensephp=yes
24+## and redirect to reqested locations with status
25+RewriteCond %{HTTP_COOKIE} redirectlicensephp=403
26+RewriteRule .* - [CO=redirectlicensephp:INVALID:.%{ENV:CO_DOMAIN}:-1,L,F]
27+
28+RewriteCond %{HTTP_COOKIE} redirectlicensephp=200 [OR]
29+RewriteCond %{HTTP_COOKIE} redirectlicensephp=404
30 RewriteRule .* - [CO=redirectlicensephp:INVALID:.%{ENV:CO_DOMAIN}:-1,L]
31
32 ## Redirect to the Samsung license file protected builds.
33
34=== modified file 'licenses/LicenseHelper.php'
35--- licenses/LicenseHelper.php 2012-05-08 11:27:26 +0000
36+++ licenses/LicenseHelper.php 2012-05-11 12:16:20 +0000
37@@ -73,30 +73,17 @@
38 return $theme;
39 }
40
41- public static function status_forbidden($dir)
42- {
43- header("Status: 403");
44- header("HTTP/1.1 403 Forbidden");
45- echo "<h1>Forbidden</h1>";
46- echo "You don't have permission to access ".$dir." on this server.";
47- exit;
48- }
49-
50- public static function status_ok($dir, $domain)
51- {
52- header("Status: 200");
53- header("Location: ".$dir);
54- setcookie("redirectlicensephp", "yes", 0, "/", ".".$domain);
55- exit;
56- }
57-
58- public static function status_not_found()
59- {
60- header("Status: 404");
61- header("HTTP/1.0 404 Not Found");
62- echo "<h1>404 Not Found</h1>";
63- echo "The requested URL was not found on this server.";
64- exit;
65- }
66-
67-}
68\ No newline at end of file
69+ public static function redirect_with_status($dir, $domain, $status)
70+ {
71+ static $http = array (
72+ 200 => "HTTP/1.1 200 OK",
73+ 403 => "HTTP/1.1 403 Forbidden",
74+ 404 => "HTTP/1.1 404 Not Found"
75+ );
76+ header($http[$status]);
77+ header ("Location: $dir");
78+ header("Status: ".$status);
79+ setcookie("redirectlicensephp", $status, 0, "/", ".".$domain);
80+ exit;
81+ }
82+}
83
84=== modified file 'licenses/license.php'
85--- licenses/license.php 2012-05-04 12:48:27 +0000
86+++ licenses/license.php 2012-05-11 12:16:20 +0000
87@@ -11,7 +11,7 @@
88 $eula = '';
89
90 if (preg_match("/.*openid.*/", $fn) or preg_match("/.*restricted.*/", $fn) or preg_match("/.*private.*/", $fn)) {
91- LicenseHelper::status_ok($down, $domain);
92+ LicenseHelper::redirect_with_status($down, $domain, 200);
93 }
94
95 if (file_exists($fn) and LicenseHelper::checkFile($fn)) { // Requested download is file
96@@ -23,7 +23,7 @@
97 $repl = $down;
98 $name_only = array();
99 } else { // Requested download not found on server
100- LicenseHelper::status_not_found();
101+ LicenseHelper::redirect_with_status($down, $domain, 404);
102 }
103
104 $flist = LicenseHelper::getFilesList($search_dir);
105@@ -41,18 +41,18 @@
106 } elseif (LicenseHelper::findFileByPattern($flist, "/.*EULA.txt.*/")) {
107 // If file is requested but no special EULA for it and no EULA.txt is present,
108 // look for any EULA and if found decide that current file is not protected.
109- LicenseHelper::status_ok($down, $domain);
110+ LicenseHelper::redirect_with_status($down, $domain, 200);
111 } else {
112- LicenseHelper::status_forbidden($down);
113+ LicenseHelper::redirect_with_status($down, $domain, 403);
114 }
115 } elseif (is_dir($fn)) {
116 if (empty($flist) or LicenseHelper::findFileByPattern($flist, "/.*EULA.txt.*/")) { // Directory contains only subdirs or any EULA
117- LicenseHelper::status_ok($down, $domain);
118+ LicenseHelper::redirect_with_status($down, $domain, 200);
119 } else { // No special EULA, no EULA.txt, no OPEN-EULA.txt found
120- LicenseHelper::status_forbidden($down);
121+ LicenseHelper::redirect_with_status($down, $domain, 403);
122 }
123 } else {
124- LicenseHelper::status_forbidden($down);
125+ LicenseHelper::redirect_with_status($down, $domain, 403);
126 }
127
128 $template_content = file_get_contents($doc."/licenses/".$theme.".html");
129
130=== removed file 'testing/filefetcher.py'
131--- testing/filefetcher.py 2012-01-13 11:48:16 +0000
132+++ testing/filefetcher.py 1970-01-01 00:00:00 +0000
133@@ -1,129 +0,0 @@
134-#!/usr/bin/env python
135-
136-# Changes required to address EULA for the origen hwpacks
137-
138-import argparse
139-import os
140-import pycurl
141-import re
142-import urlparse
143-
144-
145-class LicenseProtectedFileFetcher:
146- """Fetch a file from the web that may be protected by a license redirect
147-
148- This is designed to run on snapshots.linaro.org. License HTML file are in
149- the form:
150-
151- <vendor>.html has a link to <vendor>-accept.html
152-
153- If self.get is pointed at a file that has to go through one of these
154- licenses, it should be able to automatically accept the license and
155- download the file.
156-
157- Once a license has been accepted, it will be used for all following
158- downloads.
159-
160- If self.close() is called before the object is deleted, cURL will store
161- the license accept cookie to cookies.txt, so it can be used for later
162- downloads.
163-
164- """
165- def __init__(self):
166- """Set up cURL"""
167- self.curl = pycurl.Curl()
168- self.curl.setopt(pycurl.FOLLOWLOCATION, 1)
169- self.curl.setopt(pycurl.WRITEFUNCTION, self._write_body)
170- self.curl.setopt(pycurl.HEADERFUNCTION, self._write_header)
171- self.curl.setopt(pycurl.COOKIEFILE, "cookies.txt")
172- self.curl.setopt(pycurl.COOKIEJAR, "cookies.txt")
173-
174- def _get(self, url):
175- """Clear out header and body storage, fetch URL, filling them in."""
176- self.curl.setopt(pycurl.URL, url)
177-
178- self.body = ""
179- self.header = ""
180-
181- self.curl.perform()
182-
183- def get(self, url, ignore_license=False, accept_license=True):
184- """Fetch the requested URL, ignoring license at all or
185- accepting or declining licenses, returns file body.
186-
187- Fetches the file at url. If a redirect is encountered, it is
188- expected to be to a license that has an accept or decline link.
189- Follow that link, then download original file or nolicense notice.
190-
191- """
192- self._get(url)
193-
194- if ignore_license:
195- return self.body
196-
197- location = self._get_location()
198- if location:
199- # Off to the races - we have been redirected.
200- # Expect to find a link to self.location with -accepted or
201- # -declined inserted before the .html,
202- # i.e. ste.html -> ste-accepted.html
203-
204- # Get the file from the URL (full path)
205- file = urlparse.urlparse(location).path
206-
207- # Get the file without the rest of the path
208- file = os.path.split(file)[-1]
209-
210- # Look for a link with accepted.html or declined.html
211- # in the page name. Follow it.
212- new_file = None
213- for line in self.body.splitlines():
214- if accept_license:
215- link_search = re.search("""href=.*?["'](.*?-accepted.html)""",
216- line)
217- else:
218- link_search = re.search("""href=.*?["'](.*?-declined.html)""",
219- line)
220- if link_search:
221- # Have found license decline URL!
222- new_file = link_search.group(1)
223-
224- if new_file:
225- # accept or decline the license...
226- next_url = re.sub(file, new_file, location)
227- self._get(next_url)
228-
229- # The above get *should* take us to the file requested via
230- # a redirect. If we manually need to follow that redirect,
231- # do that now.
232-
233- if accept_license and self._get_location():
234- # If we haven't been redirected to our original file,
235- # we should be able to just download it now.
236- self._get(url)
237-
238- return self.body
239-
240- def _search_header(self, field):
241- """Search header for the supplied field, return field / None"""
242- for line in self.header.splitlines():
243- search = re.search(field + ":\s+(.*?)$", line)
244- if search:
245- return search.group(1)
246- return None
247-
248- def _get_location(self):
249- """Return content of Location field in header / None"""
250- return self._search_header("Location")
251-
252- def _write_body(self, buf):
253- """Used by curl as a sink for body content"""
254- self.body += buf
255-
256- def _write_header(self, buf):
257- """Used by curl as a sink for header content"""
258- self.header += buf
259-
260- def close(self):
261- """Wrapper to close curl - this will allow curl to write out cookies"""
262- self.curl.close()
263
264=== added file 'testing/license_protected_file_downloader.py'
265--- testing/license_protected_file_downloader.py 1970-01-01 00:00:00 +0000
266+++ testing/license_protected_file_downloader.py 2012-05-11 12:16:20 +0000
267@@ -0,0 +1,284 @@
268+#!/usr/bin/env python
269+
270+import argparse
271+import os
272+import pycurl
273+import re
274+import urlparse
275+import html2text
276+from BeautifulSoup import BeautifulSoup
277+
278+class LicenseProtectedFileFetcher:
279+ """Fetch a file from the web that may be protected by a license redirect
280+
281+ This is designed to run on snapshots.linaro.org. License HTML file are in
282+ the form:
283+
284+ <vendor>.html has a link to <vendor>-accept.html
285+
286+ If self.get is pointed at a file that has to go through one of these
287+ licenses, it should be able to automatically accept the license and
288+ download the file.
289+
290+ Once a license has been accepted, it will be used for all following
291+ downloads.
292+
293+ If self.close() is called before the object is deleted, cURL will store
294+ the license accept cookie to cookies.txt, so it can be used for later
295+ downloads.
296+
297+ """
298+ def __init__(self, cookie_file="cookies.txt"):
299+ """Set up cURL"""
300+ self.curl = pycurl.Curl()
301+ self.curl.setopt(pycurl.WRITEFUNCTION, self._write_body)
302+ self.curl.setopt(pycurl.HEADERFUNCTION, self._write_header)
303+ self.curl.setopt(pycurl.FOLLOWLOCATION, 1)
304+ self.curl.setopt(pycurl.COOKIEFILE, cookie_file)
305+ self.curl.setopt(pycurl.COOKIEJAR, cookie_file)
306+ self.file_out = None
307+
308+ def _get(self, url):
309+ """Clear out header and body storage, fetch URL, filling them in."""
310+ url = url.encode("ascii")
311+ self.curl.setopt(pycurl.URL, url)
312+
313+ self.body = ""
314+ self.header = ""
315+
316+ if self.file_name:
317+ self.file_out = open(self.file_name, 'w')
318+ else:
319+ self.file_out = None
320+
321+ self.curl.perform()
322+ self._parse_headers(url)
323+
324+ if self.file_out:
325+ self.file_out.close()
326+
327+ def _parse_headers(self, url):
328+ header = {}
329+ for line in self.header.splitlines():
330+ # Header lines typically are of the form thing: value...
331+ test_line = re.search("^(.*?)\s*:\s*(.*)$", line)
332+
333+ if test_line:
334+ header[test_line.group(1)] = test_line.group(2)
335+
336+ # The location attribute is sometimes relative, but we would
337+ # like to have it as always absolute...
338+ if 'Location' in header:
339+ parsed_location = urlparse.urlparse(header["Location"])
340+
341+ # If not an absolute location...
342+ if not parsed_location.netloc:
343+ parsed_source_url = urlparse.urlparse(url)
344+ new_location = ["", "", "", "", ""]
345+
346+ new_location[0] = parsed_source_url.scheme
347+ new_location[1] = parsed_source_url.netloc
348+ new_location[2] = header["Location"]
349+
350+ # Update location with absolute URL
351+ header["Location"] = urlparse.urlunsplit(new_location)
352+
353+ self.header_text = self.header
354+ self.header = header
355+
356+ def get_headers(self, url):
357+ url = url.encode("ascii")
358+ self.curl.setopt(pycurl.URL, url)
359+
360+ self.body = ""
361+ self.header = ""
362+
363+ # Setting NOBODY causes CURL to just fetch the header.
364+ self.curl.setopt(pycurl.NOBODY, True)
365+ self.curl.perform()
366+ self.curl.setopt(pycurl.NOBODY, False)
367+
368+ self._parse_headers(url)
369+
370+ return self.header
371+
372+ def get_or_return_license(self, url, file_name=None):
373+ """Get file at the requested URL or, if behind a license, return that.
374+
375+ If the URL provided does not redirect us to a license, then return the
376+ body of that file. If we are redirected to a license click through
377+ then return (the license as plain text, url to accept the license).
378+
379+ If the user of this function accepts the license, then they should
380+ call get_protected_file."""
381+
382+ self.file_name = file_name
383+
384+ # Get the license details. If this returns None, the file isn't license
385+ # protected and we can just return the file we started to get in the
386+ # function (self.body).
387+ license_details = self._get_license(url)
388+
389+ if license_details:
390+ return license_details
391+
392+ return self.body
393+
394+ def get(self, url, file_name=None, ignore_license=False, accept_license=True):
395+ """Fetch the requested URL, accepting licenses
396+
397+ Fetches the file at url. If a redirect is encountered, it is
398+ expected to be to a license that has an accept link. Follow that link,
399+ then download the original file. Returns the fist 1MB of the file
400+ (see _write_body).
401+
402+ """
403+
404+ self.file_name = file_name
405+ if ignore_license:
406+ self._get(url)
407+ return self.body
408+
409+ license_details = self._get_license(url)
410+
411+ if license_details:
412+ # Found a license.
413+ if accept_license:
414+ # Accept the license without looking at it and
415+ # start fetching the file we originally wanted.
416+ accept_url = license_details[1]
417+ self.get_protected_file(accept_url, url)
418+ else:
419+ # We want to decline the license and return the notice.
420+ decline_url = license_details[2]
421+ self._get(decline_url)
422+
423+ else:
424+ # If we got here, there wasn't a license protecting the file
425+ # so we just fetch it.
426+ self._get(url)
427+
428+ return self.body
429+
430+ def _get_license(self, url):
431+ """Return (license, accept URL, decline URL) if found,
432+ else return None.
433+
434+ """
435+
436+ self.get_headers(url)
437+
438+ if "Location" in self.header and self.header["Location"] != url:
439+ # We have been redirected to a new location - the license file
440+ location = self.header["Location"]
441+
442+ # Fetch the license HTML
443+ self._get(location)
444+
445+ # Get the file from the URL (full path)
446+ file = urlparse.urlparse(location).path
447+
448+ # Get the file without the rest of the path
449+ file = os.path.split(file)[-1]
450+
451+ # Look for a link with accepted.html in the page name. Follow it.
452+ accept_search, decline_search = None, None
453+ for line in self.body.splitlines():
454+ if not accept_search:
455+ accept_search = re.search(
456+ """href=.*?["'](.*?-accepted.html)""",
457+ line)
458+ if not decline_search:
459+ decline_search = re.search(
460+ """href=.*?["'](.*?-declined.html)""",
461+ line)
462+
463+ if accept_search and decline_search:
464+ # Have found license accept URL!
465+ new_file = accept_search.group(1)
466+ accept_url = re.sub(file, new_file, location)
467+
468+ # Found decline URL as well.
469+ new_file_decline = decline_search.group(1)
470+ decline_url = re.sub(file, new_file_decline, location)
471+
472+ # Parse the HTML using BeautifulSoup
473+ soup = BeautifulSoup(self.body)
474+
475+ # The license is in a div with the ID license-text, so we
476+ # use this to pull just the license out of the HTML.
477+ html_license = u""
478+ for chunk in soup.findAll(id="license-text"):
479+ # Output of chunk.prettify is UTF8, but comes back
480+ # as a str, so convert it here.
481+ html_license += chunk.prettify().decode("utf-8")
482+
483+ text_license = html2text.html2text(html_license)
484+
485+ return text_license, accept_url, decline_url
486+
487+ return None
488+
489+ def get_protected_file(self, accept_url, url):
490+ """Gets the file redirected to by the accept_url"""
491+
492+ self._get(accept_url) # Accept the license
493+
494+ if not("Location" in self.header and self.header["Location"] == url):
495+ # If we got here, we don't have the file yet (weren't redirected
496+ # to it). Fetch our target file. This should work now that we have
497+ # the right cookie.
498+ self._get(url) # Download the target file
499+
500+ return self.body
501+
502+ def _write_body(self, buf):
503+ """Used by curl as a sink for body content"""
504+
505+ # If we have a target file to write to, write to it
506+ if self.file_out:
507+ self.file_out.write(buf)
508+
509+ # Only buffer first 1MB of body. This should be plenty for anything
510+ # we wish to parse internally.
511+ if len(self.body) < 1024*1024*1024:
512+ # XXX Would be nice to stop keeping the file in RAM at all and
513+ # passing large buffers around. Perhaps only keep in RAM if
514+ # file_name == None? (used for getting directory listings
515+ # normally).
516+ self.body += buf
517+
518+ def _write_header(self, buf):
519+ """Used by curl as a sink for header content"""
520+ self.header += buf
521+
522+ def register_progress_callback(self, callback):
523+ self.curl.setopt(pycurl.NOPROGRESS, 0)
524+ self.curl.setopt(pycurl.PROGRESSFUNCTION, callback)
525+
526+ def close(self):
527+ """Wrapper to close curl - this will allow curl to write out cookies"""
528+ self.curl.close()
529+
530+def main():
531+ """Download file specified on command line"""
532+ parser = argparse.ArgumentParser(description="Download a file, accepting "
533+ "any licenses required to do so.")
534+
535+ parser.add_argument('url', metavar="URL", type=str, nargs=1,
536+ help="URL of file to download.")
537+
538+ args = parser.parse_args()
539+
540+ fetcher = LicenseProtectedFileFetcher()
541+
542+ # Get file name from URL
543+ file_name = os.path.basename(urlparse.urlparse(args.url[0]).path)
544+ if not file_name:
545+ file_name = "downloaded"
546+ fetcher.get(args.url[0], file_name)
547+
548+ fetcher.close()
549+
550+if __name__ == "__main__":
551+ main()
552
553=== modified file 'testing/test_click_through_license.py'
554--- testing/test_click_through_license.py 2012-05-07 08:48:51 +0000
555+++ testing/test_click_through_license.py 2012-05-11 12:16:20 +0000
556@@ -9,7 +9,7 @@
557
558 from testtools import TestCase
559 from testtools.matchers import Mismatch
560-from filefetcher import LicenseProtectedFileFetcher
561+from license_protected_file_downloader import LicenseProtectedFileFetcher
562
563 fetcher = LicenseProtectedFileFetcher()
564 cwd = os.getcwd()
565@@ -28,6 +28,7 @@
566 never_available = '/android/~linaro-android/staging-imx53/test.txt'
567 linaro_test_file = '/android/~linaro-android/staging-panda/test.txt'
568 not_protected_test_file = '/android/~linaro-android/staging-vexpress-a9/test.txt'
569+not_found_test_file = '/android/~linaro-android/staging-vexpress-a9/notfound.txt'
570 per_file_samsung_test_file = '/android/images/origen-blob.txt'
571 per_file_ste_test_file = '/android/images/snowball-blob.txt'
572 per_file_not_protected_test_file = '/android/images/MANIFEST'
573@@ -145,19 +146,19 @@
574 self.assertThat(testfile, Contains(search))
575
576 def test_redirect_to_license_samsung(self):
577- search = "LICENSE AGREEMENT"
578- testfile = fetcher.get(host + samsung_test_file, ignore_license=True)
579- self.assertThat(testfile, Contains(search))
580+ search = "PLEASE READ THE FOLLOWING AGREEMENT CAREFULLY"
581+ testfile = fetcher.get_or_return_license(host + samsung_test_file)
582+ self.assertThat(testfile[0], Contains(search))
583
584 def test_redirect_to_license_ste(self):
585- search = "LICENSE AGREEMENT"
586- testfile = fetcher.get(host + ste_test_file, ignore_license=True)
587- self.assertThat(testfile, Contains(search))
588+ search = "PLEASE READ THE FOLLOWING AGREEMENT CAREFULLY"
589+ testfile = fetcher.get_or_return_license(host + ste_test_file)
590+ self.assertThat(testfile[0], Contains(search))
591
592 def test_redirect_to_license_linaro(self):
593- search = "LICENSE AGREEMENT"
594- testfile = fetcher.get(host + linaro_test_file, ignore_license=True)
595- self.assertThat(testfile, Contains(search))
596+ search = "Linaro license."
597+ testfile = fetcher.get_or_return_license(host + linaro_test_file)
598+ self.assertThat(testfile[0], Contains(search))
599
600 def test_decline_license_samsung(self):
601 search = "License has not been accepted"
602@@ -214,13 +215,13 @@
603 def test_license_accepted_samsung(self):
604 search = "This is protected with click-through Samsung license."
605 os.rename("%s/cookies.samsung" % docroot, "%s/cookies.txt" % docroot)
606- testfile = fetcher.get(host + samsung_test_file, ignore_license=True)
607+ testfile = fetcher.get(host + samsung_test_file)
608 self.assertThat(testfile, Contains(search))
609
610 def test_license_accepted_ste(self):
611 search = "This is protected with click-through ST-E license."
612 os.rename("%s/cookies.ste" % docroot, "%s/cookies.txt" % docroot)
613- testfile = fetcher.get(host + ste_test_file, ignore_license=True)
614+ testfile = fetcher.get(host + ste_test_file)
615 self.assertThat(testfile, Contains(search))
616
617 def test_internal_host_samsung(self):
618@@ -284,3 +285,8 @@
619 search = "Index of /android/~linaro-android"
620 testfile = fetcher.get(host + dirs_only_dir)
621 self.assertThat(testfile, Contains(search))
622+
623+ def test_not_found_file(self):
624+ search = "Not Found"
625+ testfile = fetcher.get(host + not_found_test_file)
626+ self.assertThat(testfile, Contains(search))

Subscribers

People subscribed via source and target branches