Page Menu
Home
Software Heritage
Search
Configure Global Search
Log In
Files
F9345721
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
48 KB
Subscribers
None
View Options
diff --git a/README-uri-scheme.md b/README-uri-scheme.md
index fb29ae5c..0889e7b2 100644
--- a/README-uri-scheme.md
+++ b/README-uri-scheme.md
@@ -1,113 +1,146 @@
URI scheme
==========
Browsing namespace
------------------
### Global
To be anchored where browsing starts (e.g., at /api/1)
* /revision/<SHA1_GIT>: show commit information
+ $curl http://localhost:6543/api/1/revision/18d8be353ed3480476f032475e7c233eff7371d5
+ {
+ "revision": {
+ "author_email": "robot@softwareheritage.org",
+ "author_name": "Software Heritage",
+ "committer_date": "Mon, 17 Jan 2000 10:23:54 GMT",
+ "committer_date_offset": 0,
+ "committer_email": "robot@softwareheritage.org",
+ "committer_name": "Software Heritage",
+ "date": "Mon, 17 Jan 2000 10:23:54 GMT",
+ "date_offset": 0,
+ "directory": "7834ef7e7c357ce2af928115c6c6a42b7e2a44e6",
+ "id": "18d8be353ed3480476f032475e7c233eff7371d5",
+ "message": "synthetic revision message",
+ "metadata": {
+ "original_artifact": [
+ {
+ "archive_type": "tar",
+ "name": "webbase-5.7.0.tar.gz",
+ "sha1": "147f73f369733d088b7a6fa9c4e0273dcd3c7ccd",
+ "sha1_git": "6a15ea8b881069adedf11feceec35588f2cfe8f1",
+ "sha256": "401d0df797110bea805d358b85bcc1ced29549d3d73f309d36484e7edf7bb912"
+ }
+ ]
+ },
+ "parents": [
+ null
+ ],
+ "synthetic": true,
+ "type": "tar"
+ }
+ }
+
* /directory/<SHA1_GIT>: show directory information (including ls)
* /directory/<SHA1_GIT>/path/to/file-or-dir: ditto, but for dir pointed by path
- note: this is the same as /dir/<SHA1_GIT'>, where <SHA1_GIT'> is the
sha1_git ID of the dir pointed by path
* /content/[<HASH_ALGO>:]<HASH>: show content information
- content is specified by HASH, according to HASH_ALGO, where HASH_ALGO is
one of: sha1, sha1_git, sha256. This means that several different URLs (at
least one per HASH_ALGO) will point to the same content
- HASH_ALGO defaults to "sha1" (?)
* /release/<SHA1_GIT>: show release information
Sample:
$ curl -X GET http://localhost:6543/api/1/release/4a1b6d7dd0a923ed90156c4e2f5db030095d8e08
{
"release": {
"author": 1,
"comment": "synthetic release message",
"date": "Sat, 04 Mar 2000 07:50:35 GMT",
"date_offset": 0,
"id": "4a1b6d7dd0a923ed90156c4e2f5db030095d8e08",
"name": "4.0.6",
"revision": "5c7814ce9978d4e16f3858925b5cea611e500eec",
"synthetic": true
}
}%
* /person/<PERSON_ID>: show person information
* /origin/<ORIGIN_ID>: show origin information
Sample:
$ curl -X GET http://localhost:6543/api/1/origin/1
{
"origin": {
"id": 1,
"lister": null,
"project": null,
"type": "ftp",
"url": "rsync://ftp.gnu.org/old-gnu/solfege"
}
}%
* /project/<PROJECT_ID>: show project information
* /organization/<ORGANIZATION_ID>: show organization information
### Occurrence
Origin/Branch do not contain `|` so it is used as a terminator.
Origin is <TYPE+URL>.
Timestamp is one of: latest or an ISO8601 date (TODO: decide the time matching
policy).
* /directory/<TIMESTAMP>/<ORIGIN>|/<BRANCH>|/path/to/file-or-dir
- Same as /directory/<SHA1_GIT> but looking up sha1 git using origin and
branch at a given timestamp
* /revision/<TIMESTAMP>/<ORIGIN>|/<BRANCH>
- Same as /revision/<SHA1_GIT> but looking up sha1 git using origin and
branch at a given timestamp
* /revision/<TIMESTAMP>/<ORIGIN>|
- Show all branches of origin at a given timestamp
* /revision/<TIMESTAMP>/<ORIGIN>|/<BRANCH>|
- Show all revisions (~git log) of origin and branch at a given timestamp
### Upload and search
* /1/api/uploadnsearch/
Post a file's content to api.
Api computes the sha1 hash and checks in the storage if such sha1 exists.
Json answer:
{'sha1': hexadecimal sha1,
'found': true or false}
Sample:
$ curl -X POST -F filename=@/path/to/file http://localhost:6543/api/1/uploadnsearch/
{
"found": false,
"sha1": "e95097ad2d607b4c89c1ce7ca1fef2a1e4450558"
}%
Search namespace
----------------
diff --git a/swh/web/ui/controller.py b/swh/web/ui/controller.py
index 0b3abd22..18e60214 100644
--- a/swh/web/ui/controller.py
+++ b/swh/web/ui/controller.py
@@ -1,391 +1,400 @@
# Copyright (C) 2015 The Software Heritage developers
# See the AUTHORS file at the top-level directory of this distribution
# License: GNU Affero General Public License version 3, or any later version
# See top-level LICENSE file for more information
import json
from flask import render_template, jsonify, request, flash
from flask import make_response
from swh.core.hashutil import ALGORITHMS
from swh.web.ui.main import app
from swh.web.ui import service
from swh.web.ui.decorators import jsonp
hash_filter_keys = ALGORITHMS
@app.route('/')
def main():
"""Home page
"""
flash('This Web app is still work in progress, use at your own risk',
'warning')
# return redirect(url_for('about'))
return render_template('home.html')
@app.route('/about')
def about():
return render_template('about.html')
@app.route('/search')
def search():
"""Search for hashes in swh-storage.
"""
q = request.args.get('q', '')
env = {'q': q, 'message': '', 'found': None}
try:
if q:
env['found'] = service.lookup_hash(q)
except ValueError:
env['message'] = 'Error: invalid query string'
return render_template('search.html', **env)
@app.route('/uploadnsearch', methods=['GET', 'POST'])
def uploadnsearch():
"""Upload and search for hashes in swh-storage.
"""
env = {'filename': None, 'message': '', 'found': None}
if request.method == 'POST':
file = request.files['filename']
try:
filename, sha1, found = service.upload_and_search(file)
message = 'The file %s with hash %s has%sbeen found.' % (
filename,
sha1,
' ' if found else ' not ')
env.update({
'filename': filename,
'sha1': sha1,
'found': found,
'message': message
})
except ValueError:
env['message'] = 'Error: invalid query string'
return render_template('upload_and_search.html', **env)
@app.route('/browse/revision/<sha1_git>')
def revision(sha1_git):
"""Show commit information.
Args:
sha1_git: the revision's sha1
Returns:
Revision information
"""
return render_template('revision.html',
sha1_git=sha1_git)
@app.route('/browse/directory/<sha1_git>')
def directory(sha1_git):
"""Show directory information.
Args:
sha1_git: the directory's sha1
Returns:
Directory information
"""
return render_template('directory.html',
sha1_git=sha1_git)
@app.route('/browse/directory/<sha1_git>/<path:p>')
def directory_at_path(sha1_git, p):
"""Show directory information for the sha1_git at path.
Args:
sha1_git: the directory's sha1
path: file or directory pointed to
Returns:
Directory information at sha1_git + path
"""
return render_template('directory.html',
sha1_git=sha1_git,
path=p)
def _origin_seen(hash, data):
"""Given an origin, compute a message string with the right information.
Args:
origin: a dictionary with keys:
- origin: a dictionary with type and url keys
- occurrence: a dictionary with a validity range
Returns:
message as a string
"""
if data is None:
return 'Content with hash %s is unknown as of now.' % hash
origin_type = data['origin_type']
origin_url = data['origin_url']
revision = data['revision']
branch = data['branch']
path = data['path']
return """The content with hash %s has been seen on origin with type '%s'
at url '%s'. The revision was identified at '%s' on branch '%s'.
The file's path referenced was '%s'.""" % (hash,
origin_type,
origin_url,
revision,
branch,
path)
@app.route('/browse/content/<hash>:<sha>')
def content(hash, sha):
"""Show content information.
Args:
hash: hash according to HASH_ALGO, where HASH_ALGO is
one of: sha1, sha1_git, sha256. This means that several different URLs (at
least one per HASH_ALGO) will point to the same content
sha: the sha with 'hash' format
Returns:
The content's information at sha1_git
"""
if hash not in hash_filter_keys:
message = 'The checksum must be one of sha1, sha1_git, sha256'
else:
q = "%s:%s" % (hash, sha)
found = service.lookup_hash(q)
if not found:
message = "Hash %s was not found." % hash
else:
origin = service.lookup_hash_origin(q)
message = _origin_seen(hash, origin)
return render_template('content.html',
hash=hash,
sha=sha,
message=message)
@app.route('/browse/release/<sha1_git>')
def release(sha1_git):
"""Show release's information.
Args:
sha1_git: sha1_git for this particular release
Returns:
Release's information
"""
return 'Release information at %s' % sha1_git
@app.route('/browse/person/<int:id>')
def person(id):
"""Show Person's information at id.
Args:
id: person's unique identifier
Returns:
Person's information
"""
return 'Person information at %s' % id
@app.route('/browse/origin/<int:id>')
def origin(id):
"""Show origin's information at id.
Args:
id: origin's unique identifier
Returns:
Origin's information
"""
return 'Origin information at %s' % id
@app.route('/browse/project/<int:id>')
def project(id):
"""Show project's information at id.
Args:
id: project's unique identifier
Returns:
Project's information
"""
return 'Project information at %s' % id
@app.route('/browse/organization/<int:id>')
def organization(id):
"""Show organization's information at id.
Args:
id: organization's unique identifier
Returns:
Organization's information
"""
return 'Organization information at %s' % id
@app.route('/browse/directory/<string:timestamp>/'
'<string:origin_type>+<path:origin_url>|/'
'<path:branch>|/<path:path>')
def directory_at_origin(timestamp, origin_type, origin_url, branch, path):
"""Show directory information at timestamp, origin-type, origin-url, branch
and path.
Those parameters are separated by the `|` terminator.
Args:
timestamp: the timestamp to look for. can be latest or some iso8601
date format. (TODO: decide the time matching policy.)
origin_type: origin's type
origin_url: origin's url (can contain `/`)
branch: branch name which can contain `/`
path: path to directory or file
Returns:
Directory information at the given parameters.
"""
return 'Directory at (%s, %s, %s, %s, %s)' % (timestamp,
origin_type,
origin_url,
branch,
path)
@app.route('/browse/revision/<string:timestamp>/'
'<string:origin_type>+<path:origin_url>|/<path:branch>')
def revision_at_origin_and_branch(timestamp, origin_type, origin_url, branch):
"""Show revision information at timestamp, origin, and branch.
Those parameters are separated by the `|` terminator.
Args:
timestamp: the timestamp to look for. can be latest or some iso8601
date format. (TODO: decide the time matching policy.)
origin_type: origin's type
origin_url: origin's url (can contain `/`)
branch: branch name which can contain /
Returns:
Revision information at the given parameters.
"""
return 'Revision at (ts=%s, type=%s, url=%s, branch=%s)' % (timestamp,
origin_type,
origin_url,
branch)
@app.route('/browse/revision/<string:timestamp>/'
'<string:origin_type>+<path:origin_url>|')
def revision_at_origin(timestamp, origin_type, origin_url):
"""Show revision information at timestamp, origin, and branch.
Those parameters are separated by the `|` terminator.
Args:
timestamp: the timestamp to look for. can be latest or iso8601
date
format. (TODO: decide the time matching policy.)
origin_type: origin's type
origin_url: origin's url (can contain `/`)
Returns:
Revision information at the given parameters.
"""
return 'Revision at (timestamp=%s, type=%s, url=%s)' % (timestamp,
origin_type,
origin_url)
@app.route('/api/1/stat/counters')
@jsonp
def api_stats():
"""Return statistics as a JSON object"""
return jsonify(service.stat_counters())
@app.errorhandler(ValueError)
def value_error_as_bad_request(error):
"""Compute a bad request and add body as payload.
"""
response = make_response(
'Bad request',
400)
response.headers['Content-Type'] = 'application/json'
response.data = json.dumps({"error": str(error)})
return response
@app.route('/api/1/search/<string:q>/')
@jsonp
def api_search(q):
"""Return search results as a JSON object"""
return jsonify({'found': service.lookup_hash(q)})
@app.route('/api/1/origin/<string:origin_id>')
@jsonp
def api_origin(origin_id):
"""Return information about origin"""
return jsonify({'origin': service.lookup_origin(origin_id)})
@app.route('/api/1/release/<string:sha1_git>')
@jsonp
def api_release(sha1_git):
"""Return information about release with id sha1_git."""
return jsonify({'release': service.lookup_release(sha1_git)})
+@app.route('/api/1/revision/<string:sha1_git>')
+@jsonp
+def api_revision(sha1_git):
+ """Return information about revision with id sha1_git.
+
+ """
+ return jsonify({'revision': service.lookup_revision(sha1_git)})
+
+
@app.route('/api/1/browse/<string:q>/')
@jsonp
def api_browse(q):
"""Return search results as a JSON object"""
return jsonify({'origin': service.lookup_hash_origin(q)})
@app.route('/api/1/uploadnsearch/', methods=['POST'])
@jsonp
def api_uploadnsearch():
"""Upload the file's content in the post body request.
Compute the hash and determine if it exists in the storage.
"""
file = request.files['filename']
filename, sha1, found = service.upload_and_search(file)
return jsonify({'sha1': sha1,
'filename': filename,
'found': found})
diff --git a/swh/web/ui/converters.py b/swh/web/ui/converters.py
index 4cbd78e2..cb391a2b 100644
--- a/swh/web/ui/converters.py
+++ b/swh/web/ui/converters.py
@@ -1,56 +1,99 @@
# Copyright (C) 2015 The Software Heritage developers
# See the AUTHORS file at the top-level directory of this distribution
# License: GNU Affero General Public License version 3, or any later version
# See top-level LICENSE file for more information
from swh.core import hashutil
def from_origin(origin):
"""Convert from an swh origin to an origin dictionary.
"""
new_origin = {}
for key, value in origin.items():
if key == 'revision':
new_origin[key] = hashutil.hash_to_hex(value)
elif key == 'path':
new_origin[key] = value.decode('utf-8')
else:
new_origin[key] = value
return new_origin
def from_release(release):
"""Convert from an swh release to a json serializable release dictionary.
Args:
release: Dict with the following keys
- id: identifier of the revision (sha1 in bytes)
- revision: identifier of the revision the release points to (sha1 in
bytes)
- comment: release's comment message (bytes)
- name: release's name (string)
- author: release's author identifier (swh's id)
- synthetic: the synthetic property (boolean)
Returns:
Release dictionary with the following keys:
- id: hexadecimal sha1 (string)
- revision: hexadecimal sha1 (string)
- comment: release's comment message (string)
- name: release's name (string)
- author: release's author identifier (swh's id)
- synthetic: the synthetic property (boolean)
"""
new_release = {}
for key, value in release.items():
if key == 'id' or key == 'revision':
new_release[key] = hashutil.hash_to_hex(value) if value else None
elif key == 'comment':
new_release[key] = value.decode('utf-8')
else:
new_release[key] = value
return new_release
+
+
+def from_revision(revision):
+ """Convert from an SWH revision to a json serializable revision dictionary.
+
+ Args:
+ revision: Dict with the following keys
+ - id: identifier of the revision (sha1 in bytes)
+ - directory: identifier of the directory the revision points to (sha1
+ in bytes)
+ - author_name, author_email: author's revision name and email
+ - committer_name, committer_email: committer's revision name and email
+ - message: revision's message
+ - date, date_offset: revision's author date
+ - committer_date, committer_date_offset: revision's commit date
+ - parents: list of parents for such revision
+ - synthetic: revision's property nature
+ - type: revision's type (git, tar or dsc at the moment)
+ - metadata: if the revision is synthetic, this can reference dynamic
+ properties.
+
+ Returns:
+ Revision dictionary with the same keys as inputs, only:
+ - sha1s are in hexadecimal strings (id, directory)
+ - bytes are decoded in string (author_name, committer_name,
+ author_email, committer_email, message)
+ - remaining keys are left as is
+
+ """
+ new_revision = {}
+ for key, value in revision.items():
+ if key in ['id', 'directory']:
+ new_revision[key] = hashutil.hash_to_hex(value) if value else None
+ elif key in ['author_name',
+ 'committer_name',
+ 'author_email',
+ 'committer_email',
+ 'message']:
+ new_revision[key] = value.decode('utf-8')
+ else:
+ new_revision[key] = value
+
+ return new_revision
diff --git a/swh/web/ui/service.py b/swh/web/ui/service.py
index a0b7031f..ae0602b1 100644
--- a/swh/web/ui/service.py
+++ b/swh/web/ui/service.py
@@ -1,115 +1,139 @@
# Copyright (C) 2015 The Software Heritage developers
# See the AUTHORS file at the top-level directory of this distribution
# License: GNU Affero General Public License version 3, or any later version
# See top-level LICENSE file for more information
from swh.core import hashutil
from swh.web.ui import converters, main, query, upload
def hash_and_search(filepath):
"""Hash the filepath's content as sha1, then search in storage if
it exists.
Args:
Filepath of the file to hash and search.
Returns:
Tuple (hex sha1, found as True or false).
The found boolean, according to whether the sha1 of the file
is present or not.
"""
hash = hashutil.hashfile(filepath)
return (hashutil.hash_to_hex(hash['sha1']),
main.storage().content_exist(hash))
def upload_and_search(file):
"""Upload a file and compute its hash.
"""
tmpdir, filename, filepath = upload.save_in_upload_folder(file)
try:
sha1, found = None, None
if filepath:
sha1, found = hash_and_search(filepath)
return filename, sha1, found
finally:
# clean up
if tmpdir:
upload.cleanup(tmpdir)
def lookup_hash(q):
"""Checks if the storage contains a given content checksum
Args: query string
Returns:
True or False, according to whether the checksum is present or not
"""
(algo, hash) = query.parse_hash(q)
return main.storage().content_exist({algo: hash})
def lookup_hash_origin(q):
"""Return information about the checksum contained in the query q.
Args: query string
Returns:
True or False, according to whether the checksum is present or not
"""
algo, h = query.parse_hash(q)
origin = main.storage().content_find_occurrence({algo: h})
return converters.from_origin(origin)
def lookup_origin(origin_id):
"""Return information about the origin with id origin_id.
Args:
origin_id as string
Returns:
origin information as dict.
"""
return main.storage().origin_get({'id': origin_id})
def lookup_release(release_sha1_git):
"""Return information about the release with sha1 release_sha1_git.
Args:
release_sha1_git: The release's sha1 as hexadecimal
Returns:
Release information as dict.
Raises:
ValueError if the identifier provided is not of sha1 nature.
"""
algo, hBinSha1 = query.parse_hash(release_sha1_git)
if algo != 'sha1': # HACK: sha1_git really but they are both sha1...
raise ValueError('Only sha1_git is supported.')
res = main.storage().release_get([hBinSha1])
if res and len(res) >= 1:
return converters.from_release(res[0])
return None
+def lookup_revision(rev_sha1_git, rev_type='git'):
+ """Return information about the revision with sha1 revision_sha1_git.
+
+ Args:
+ revision_sha1_git: The revision's sha1 as hexadecimal
+
+ Returns:
+ Revision information as dict.
+
+ Raises:
+ ValueError if the identifier provided is not of sha1 nature.
+
+ """
+ algo, hBinSha1 = query.parse_hash(rev_sha1_git)
+ if algo != 'sha1': # HACK: sha1_git really but they are both sha1...
+ raise ValueError('Only sha1_git is supported.')
+
+ res = main.storage().revision_get([hBinSha1])
+
+ if res and len(res) >= 1:
+ return converters.from_revision(res[0])
+ return None
+
+
def stat_counters():
"""Return the stat counters for Software Heritage
Returns:
A dict mapping textual labels to integer values.
"""
return main.storage().stat_counters()
diff --git a/swh/web/ui/tests/test_controller.py b/swh/web/ui/tests/test_controller.py
index 38aef1ef..8ea905de 100644
--- a/swh/web/ui/tests/test_controller.py
+++ b/swh/web/ui/tests/test_controller.py
@@ -1,220 +1,260 @@
# Copyright (C) 2015 The Software Heritage developers
# See the AUTHORS file at the top-level directory of this distribution
# License: GNU Affero General Public License version 3, or any later version
# See top-level LICENSE file for more information
import unittest
import json
from nose.tools import istest
from unittest.mock import patch
from swh.web.ui.tests import test_app
class ApiTestCase(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.app, _, _ = test_app.init_app()
@istest
def info(self):
# when
rv = self.app.get('/about')
self.assertEquals(rv.status_code, 200)
self.assertIn(b'About', rv.data)
# @istest
def search_1(self):
# when
rv = self.app.get('/search')
self.assertEquals(rv.status_code, 200) # check this api
self.assertRegexpMatches(rv.data, b'name=q value=>')
# @istest
def search_2(self):
# when
rv = self.app.get('/search?q=one-hash-to-look-for:another-one')
self.assertEquals(rv.status_code, 200) # check this api
self.assertRegexpMatches(
rv.data,
b'name=q value=one-hash-to-look-for:another-one')
@patch('swh.web.ui.controller.service')
@istest
def api_browse(self, mock_service):
# given
mock_service.lookup_hash_origin.return_value = {
'origin': 'some-origin'
}
# when
rv = self.app.get('/api/1/browse/sha1:foo/')
self.assertEquals(rv.status_code, 200)
self.assertEquals(rv.mimetype, 'application/json')
response_data = json.loads(rv.data.decode('utf-8'))
self.assertEquals(response_data, {'origin': {'origin': 'some-origin'}})
mock_service.lookup_hash_origin.assert_called_once_with('sha1:foo')
@patch('swh.web.ui.controller.service')
@istest
def api_search(self, mock_service):
# given
mock_service.lookup_hash.return_value = False
# when
rv = self.app.get('/api/1/search/sha1:blah/')
self.assertEquals(rv.status_code, 200)
self.assertEquals(rv.mimetype, 'application/json')
response_data = json.loads(rv.data.decode('utf-8'))
self.assertEquals(response_data, {'found': False})
mock_service.lookup_hash.assert_called_once_with('sha1:blah')
@patch('swh.web.ui.controller.service')
@istest
def api_1_stat_counters_raise_error(self, mock_service):
# given
mock_service.stat_counters.side_effect = ValueError(
'voluntary error to check the bad request middleware.')
# when
rv = self.app.get('/api/1/stat/counters')
# then
self.assertEquals(rv.status_code, 400)
self.assertEquals(rv.mimetype, 'application/json')
response_data = json.loads(rv.data.decode('utf-8'))
self.assertEquals(response_data, {
'error': 'voluntary error to check the bad request middleware.'})
@patch('swh.web.ui.controller.service')
@istest
def api_1_stat_counters(self, mock_service):
# given
mock_service.stat_counters.return_value = {
"content": 1770830,
"directory": 211683,
"directory_entry_dir": 209167,
"directory_entry_file": 1807094,
"directory_entry_rev": 0,
"entity": 0,
"entity_history": 0,
"occurrence": 0,
"occurrence_history": 19600,
"origin": 1096,
"person": 0,
"release": 8584,
"revision": 7792,
"revision_history": 0,
"skipped_content": 0
}
# when
rv = self.app.get('/api/1/stat/counters')
response_data = json.loads(rv.data.decode('utf-8'))
self.assertEquals(rv.status_code, 200)
self.assertEquals(rv.mimetype, 'application/json')
self.assertEquals(response_data, {
"content": 1770830,
"directory": 211683,
"directory_entry_dir": 209167,
"directory_entry_file": 1807094,
"directory_entry_rev": 0,
"entity": 0,
"entity_history": 0,
"occurrence": 0,
"occurrence_history": 19600,
"origin": 1096,
"person": 0,
"release": 8584,
"revision": 7792,
"revision_history": 0,
"skipped_content": 0
})
mock_service.stat_counters.assert_called_once_with()
@patch('swh.web.ui.controller.service')
@patch('swh.web.ui.controller.request')
@istest
def api_uploadnsearch(self, mock_request, mock_service):
# given
mock_request.files = {'filename': 'simple-filename'}
mock_service.upload_and_search.return_value = (
'simple-filename', 'some-hex-sha1', False)
# when
rv = self.app.post('/api/1/uploadnsearch/')
self.assertEquals(rv.status_code, 200)
self.assertEquals(rv.mimetype, 'application/json')
response_data = json.loads(rv.data.decode('utf-8'))
self.assertEquals(response_data, {'filename': 'simple-filename',
'sha1': 'some-hex-sha1',
'found': False})
mock_service.upload_and_search.assert_called_once_with(
'simple-filename')
@patch('swh.web.ui.controller.service')
@istest
def api_origin(self, mock_service):
# given
mock_service.lookup_origin.return_value = {
'id': 'origin-0',
'lister': 'uuid-lister-0',
'project': 'uuid-project-0',
'url': 'ftp://some/url/to/origin/0',
'type': 'ftp'}
# when
rv = self.app.get('/api/1/origin/origin-0')
# then
self.assertEquals(rv.status_code, 200)
self.assertEquals(rv.mimetype, 'application/json')
response_data = json.loads(rv.data.decode('utf-8'))
self.assertEquals(response_data, {
'origin': {
'id': 'origin-0',
'lister': 'uuid-lister-0',
'project': 'uuid-project-0',
'url': 'ftp://some/url/to/origin/0',
'type': 'ftp'
}
})
@patch('swh.web.ui.controller.service')
@istest
def api_release(self, mock_service):
# given
mock_service.lookup_release.return_value = {
'id': 'release-0',
'revision': 'revision-sha1',
'author': 'author-id',
}
# when
rv = self.app.get('/api/1/release/release-0')
# then
self.assertEquals(rv.status_code, 200)
self.assertEquals(rv.mimetype, 'application/json')
response_data = json.loads(rv.data.decode('utf-8'))
self.assertEquals(response_data, {
'release': {
'id': 'release-0',
'revision': 'revision-sha1',
'author': 'author-id',
}
})
+
+ @patch('swh.web.ui.controller.service')
+ @istest
+ def api_revision(self, mock_service):
+ # given
+ mock_revision = {
+ 'id': '18d8be353ed3480476f032475e7c233eff7371d5',
+ 'directory': '7834ef7e7c357ce2af928115c6c6a42b7e2a44e6',
+ 'author_name': 'Software Heritage',
+ 'author_email': 'robot@softwareheritage.org',
+ 'committer_name': 'Software Heritage',
+ 'committer_email': 'robot@softwareheritage.org',
+ 'message': 'synthetic revision message',
+ 'date_offset': 0,
+ 'committer_date_offset': 0,
+ 'parents': [],
+ 'type': 'tar',
+ 'synthetic': True,
+ 'metadata': {
+ 'original_artifact': [{
+ 'archive_type': 'tar',
+ 'name': 'webbase-5.7.0.tar.gz',
+ 'sha1': '147f73f369733d088b7a6fa9c4e0273dcd3c7ccd',
+ 'sha1_git': '6a15ea8b881069adedf11feceec35588f2cfe8f1',
+ 'sha256': '401d0df797110bea805d358b85bcc1ced29549d3d73f'
+ '309d36484e7edf7bb912'
+ }]
+ },
+ }
+ mock_service.lookup_revision.return_value = mock_revision
+
+ # when
+ rv = self.app.get('/api/1/revision/revision-0')
+
+ # then
+ self.assertEquals(rv.status_code, 200)
+ self.assertEquals(rv.mimetype, 'application/json')
+
+ response_data = json.loads(rv.data.decode('utf-8'))
+ self.assertEquals(response_data, {"revision": mock_revision})
diff --git a/swh/web/ui/tests/test_converters.py b/swh/web/ui/tests/test_converters.py
index f969bd30..ca7582eb 100644
--- a/swh/web/ui/tests/test_converters.py
+++ b/swh/web/ui/tests/test_converters.py
@@ -1,103 +1,170 @@
# Copyright (C) 2015 The Software Heritage developers
# See the AUTHORS file at the top-level directory of this distribution
# License: GNU Affero General Public License version 3, or any later version
# See top-level LICENSE file for more information
import datetime
import unittest
from nose.tools import istest
from swh.core import hashutil
from swh.web.ui import converters
class ConvertersTestCase(unittest.TestCase):
@istest
def from_origin(self):
# given
origin_input = {
'origin_type': 'ftp',
'origin_url': 'rsync://ftp.gnu.org/gnu/octave',
'branch': 'octave-3.4.0.tar.gz',
'revision': b'\xb0L\xaf\x10\xe9SQ`\xd9\x0e\x87KE\xaaBm\xe7b\xf1\x9f', # noqa
'path': b'octave-3.4.0/doc/interpreter/octave.html/doc_002dS_005fISREG.html' # noqa
}
expected_origin = {
'origin_type': 'ftp',
'origin_url': 'rsync://ftp.gnu.org/gnu/octave',
'branch': 'octave-3.4.0.tar.gz',
'revision': 'b04caf10e9535160d90e874b45aa426de762f19f',
'path': 'octave-3.4.0/doc/interpreter/octave.html/doc_002dS_005fISREG.html' # noqa
}
# when
actual_origin = converters.from_origin(origin_input)
# then
self.assertEqual(actual_origin, expected_origin)
@istest
def from_release(self):
release_input = {
'id': hashutil.hex_to_hash(
'aad23fa492a0c5fed0708a6703be875448c86884'),
'revision': hashutil.hex_to_hash(
'5e46d564378afc44b31bb89f99d5675195fbdf67'),
'date': datetime.datetime(2015, 1, 1, 22, 0, 0,
tzinfo=datetime.timezone.utc),
'date_offset': None,
'name': 'v0.0.1',
'comment': b'some comment on release',
'synthetic': True,
}
expected_release = {
'id': 'aad23fa492a0c5fed0708a6703be875448c86884',
'revision': '5e46d564378afc44b31bb89f99d5675195fbdf67',
'date': datetime.datetime(2015, 1, 1, 22, 0, 0,
tzinfo=datetime.timezone.utc),
'date_offset': None,
'name': 'v0.0.1',
'comment': 'some comment on release',
'synthetic': True,
}
# when
actual_release = converters.from_release(release_input)
# then
self.assertEqual(actual_release, expected_release)
@istest
def from_release_no_revision(self):
release_input = {
'id': hashutil.hex_to_hash(
'b2171ee2bdf119cd99a7ec7eff32fa8013ef9a4e'),
'revision': None,
'date': datetime.datetime(2016, 3, 2, 10, 0, 0,
tzinfo=datetime.timezone.utc),
'date_offset': 1,
'name': 'v0.1.1',
'comment': b'comment on release',
'synthetic': False,
}
expected_release = {
'id': 'b2171ee2bdf119cd99a7ec7eff32fa8013ef9a4e',
'revision': None,
'date': datetime.datetime(2016, 3, 2, 10, 0, 0,
tzinfo=datetime.timezone.utc),
'date_offset': 1,
'name': 'v0.1.1',
'comment': 'comment on release',
'synthetic': False,
}
# when
actual_release = converters.from_release(release_input)
# then
self.assertEqual(actual_release, expected_release)
+
+ @istest
+ def from_revision(self):
+ revision_input = {
+ 'id': hashutil.hex_to_hash(
+ '18d8be353ed3480476f032475e7c233eff7371d5'),
+ 'directory': hashutil.hex_to_hash(
+ '7834ef7e7c357ce2af928115c6c6a42b7e2a44e6'),
+ 'author_name': b'Software Heritage',
+ 'author_email': b'robot@softwareheritage.org',
+ 'committer_name': b'Software Heritage',
+ 'committer_email': b'robot@softwareheritage.org',
+ 'message': b'synthetic revision message',
+ 'date': datetime.datetime(2000, 1, 17, 11, 23, 54, tzinfo=None),
+ 'date_offset': 0,
+ 'committer_date': datetime.datetime(2000, 1, 17, 11, 23, 54,
+ tzinfo=None),
+ 'committer_date_offset': 0,
+ 'synthetic': True,
+ 'type': 'tar',
+ 'parents': [],
+ 'metadata': {
+ 'original_artifact': [{
+ 'archive_type': 'tar',
+ 'name': 'webbase-5.7.0.tar.gz',
+ 'sha1': '147f73f369733d088b7a6fa9c4e0273dcd3c7ccd',
+ 'sha1_git': '6a15ea8b881069adedf11feceec35588f2cfe8f1',
+ 'sha256': '401d0df797110bea805d358b85bcc1ced29549d3d73f'
+ '309d36484e7edf7bb912',
+
+ }]
+ },
+ }
+
+ expected_revision = {
+ 'id': '18d8be353ed3480476f032475e7c233eff7371d5',
+ 'directory': '7834ef7e7c357ce2af928115c6c6a42b7e2a44e6',
+ 'author_name': 'Software Heritage',
+ 'author_email': 'robot@softwareheritage.org',
+ 'committer_name': 'Software Heritage',
+ 'committer_email': 'robot@softwareheritage.org',
+ 'message': 'synthetic revision message',
+ 'date': datetime.datetime(2000, 1, 17, 11, 23, 54, tzinfo=None),
+ 'date_offset': 0,
+ 'committer_date': datetime.datetime(2000, 1, 17, 11, 23, 54,
+ tzinfo=None),
+ 'committer_date_offset': 0,
+ 'parents': [],
+ 'type': 'tar',
+ 'synthetic': True,
+ 'metadata': {
+ 'original_artifact': [{
+ 'archive_type': 'tar',
+ 'name': 'webbase-5.7.0.tar.gz',
+ 'sha1': '147f73f369733d088b7a6fa9c4e0273dcd3c7ccd',
+ 'sha1_git': '6a15ea8b881069adedf11feceec35588f2cfe8f1',
+ 'sha256': '401d0df797110bea805d358b85bcc1ced29549d3d73f'
+ '309d36484e7edf7bb912'
+ }]
+ },
+ }
+
+ # when
+ actual_revision = converters.from_revision(revision_input)
+
+ # then
+ self.assertEqual(actual_revision, expected_revision)
diff --git a/swh/web/ui/tests/test_service.py b/swh/web/ui/tests/test_service.py
index a956fe8c..7935a680 100644
--- a/swh/web/ui/tests/test_service.py
+++ b/swh/web/ui/tests/test_service.py
@@ -1,237 +1,285 @@
# Copyright (C) 2015 The Software Heritage developers
# See the AUTHORS file at the top-level directory of this distribution
# License: GNU Affero General Public License version 3, or any later version
# See top-level LICENSE file for more information
+import datetime
import unittest
from nose.tools import istest
from unittest.mock import MagicMock, patch
from swh.core.hashutil import hex_to_hash
from swh.web.ui import service
from swh.web.ui.tests import test_app
class ServiceTestCase(unittest.TestCase):
@classmethod
def setUpClass(cls):
_, _, cls.storage = test_app.init_app()
@istest
def lookup_hash_does_not_exist(self):
# given
self.storage.content_exist = MagicMock(return_value=False)
# when
actual_lookup = service.lookup_hash(
'sha1:123caf10e9535160d90e874b45aa426de762f19f')
# then
self.assertFalse(actual_lookup)
# check the function has been called with parameters
self.storage.content_exist.assert_called_with({
'sha1':
hex_to_hash('123caf10e9535160d90e874b45aa426de762f19f')})
@istest
def lookup_hash_exist(self):
# given
self.storage.content_exist = MagicMock(return_value=True)
# when
actual_lookup = service.lookup_hash(
'sha1:456caf10e9535160d90e874b45aa426de762f19f')
# then
self.assertTrue(actual_lookup)
self.storage.content_exist.assert_called_with({
'sha1':
hex_to_hash('456caf10e9535160d90e874b45aa426de762f19f')})
@istest
def lookup_hash_origin(self):
# given
self.storage.content_find_occurrence = MagicMock(return_value={
'origin_type': 'sftp',
'origin_url': 'sftp://ftp.gnu.org/gnu/octave',
'branch': 'octavio-3.4.0.tar.gz',
'revision': b'\xb0L\xaf\x10\xe9SQ`\xd9\x0e\x87KE\xaaBm\xe7b\xf1\x9f', # noqa
'path': b'octavio-3.4.0/doc/interpreter/octave.html/doc_002dS_005fISREG.html' # noqa
})
expected_origin = {
'origin_type': 'sftp',
'origin_url': 'sftp://ftp.gnu.org/gnu/octave',
'branch': 'octavio-3.4.0.tar.gz',
'revision': 'b04caf10e9535160d90e874b45aa426de762f19f',
'path': 'octavio-3.4.0/doc/interpreter/octave.html/doc'
'_002dS_005fISREG.html'
}
# when
actual_origin = service.lookup_hash_origin(
'sha1_git:456caf10e9535160d90e874b45aa426de762f19f')
# then
self.assertEqual(actual_origin, expected_origin)
self.storage.content_find_occurrence.assert_called_with(
{'sha1_git':
hex_to_hash('456caf10e9535160d90e874b45aa426de762f19f')})
@istest
def stat_counters(self):
# given
input_stats = {
"content": 1770830,
"directory": 211683,
"directory_entry_dir": 209167,
"directory_entry_file": 1807094,
"directory_entry_rev": 0,
"entity": 0,
"entity_history": 0,
"occurrence": 0,
"occurrence_history": 19600,
"origin": 1096,
"person": 0,
"release": 8584,
"revision": 7792,
"revision_history": 0,
"skipped_content": 0
}
self.storage.stat_counters = MagicMock(return_value=input_stats)
# when
actual_stats = service.stat_counters()
# then
expected_stats = input_stats
self.assertEqual(actual_stats, expected_stats)
self.storage.stat_counters.assert_called_with()
@istest
def hash_and_search(self):
# given
self.storage.content_exist = MagicMock(return_value=False)
bhash = hex_to_hash('456caf10e9535160d90e874b45aa426de762f19f')
# when
with patch(
'swh.core.hashutil.hashfile',
return_value={'sha1': bhash}):
actual_hash, actual_search = service.hash_and_search('/some/path')
# then
self.assertEqual(actual_hash,
'456caf10e9535160d90e874b45aa426de762f19f')
self.assertFalse(actual_search)
self.storage.content_exist.assert_called_with({'sha1': bhash})
@patch('swh.web.ui.service.upload')
@istest
def test_upload_and_search_upload_OK_basic_case(self, mock_upload):
# given (cf. decorators patch)
mock_upload.save_in_upload_folder.return_value = (
'/tmp/blah', 'some-filename', None)
mock_upload.cleanup.return_value = None
file = MagicMock()
file.filename = 'some-filename'
# when
actual_file, actual_hash, actual_search = service.upload_and_search(
file)
# then
self.assertEqual(actual_file, 'some-filename')
self.assertIsNone(actual_hash)
self.assertIsNone(actual_search)
mock_upload.save_in_upload_folder.assert_called_with(file)
mock_upload.cleanup.assert_called_with('/tmp/blah')
@istest
def lookup_origin(self):
# given
self.storage.origin_get = MagicMock(return_value={
'id': 'origin-id',
'lister': 'uuid-lister',
'project': 'uuid-project',
'url': 'ftp://some/url/to/origin',
'type': 'ftp'})
# when
actual_origin = service.lookup_origin('origin-id')
# then
self.assertEqual(actual_origin, {'id': 'origin-id',
'lister': 'uuid-lister',
'project': 'uuid-project',
'url': 'ftp://some/url/to/origin',
'type': 'ftp'})
self.storage.origin_get.assert_called_with({'id': 'origin-id'})
@istest
def lookup_release(self):
- import datetime
# given
self.storage.release_get = MagicMock(return_value=[{
'id': hex_to_hash('65a55bbdf3629f916219feb3dcc7393ded1bc8db'),
'revision': None,
'date': datetime.datetime(2015, 1, 1, 22, 0, 0,
tzinfo=datetime.timezone.utc),
'date_offset': None,
'name': 'v0.0.1',
'comment': b'synthetic release',
'synthetic': True,
}])
# when
actual_release = service.lookup_release(
'65a55bbdf3629f916219feb3dcc7393ded1bc8db')
# then
self.assertEqual(actual_release, {
'id': '65a55bbdf3629f916219feb3dcc7393ded1bc8db',
'revision': None,
'date': datetime.datetime(2015, 1, 1, 22, 0, 0,
tzinfo=datetime.timezone.utc),
'date_offset': None,
'name': 'v0.0.1',
'comment': 'synthetic release',
'synthetic': True,
})
self.storage.release_get.assert_called_with(
[hex_to_hash('65a55bbdf3629f916219feb3dcc7393ded1bc8db')])
@istest
def lookup_release_ko_id_checksum_not_ok_because_not_a_sha1(self):
# given
self.storage.release_get = MagicMock()
with self.assertRaises(ValueError) as cm:
# when
service.lookup_release('not-a-sha1')
self.assertIn('invalid checksum', cm.exception.args[0])
self.storage.release_get.called = False
@istest
def lookup_release_ko_id_checksum_ok_but_not_a_sha1(self):
# given
self.storage.release_get = MagicMock()
# when
with self.assertRaises(ValueError) as cm:
service.lookup_release(
'13c1d34d138ec13b5ebad226dc2528dc7506c956e4646f62d4daf5'
'1aea892abe')
self.assertIn('sha1_git supported', cm.exception.args[0])
self.storage.release_get.called = False
+
+ @istest
+ def lookup_revision(self):
+ # given
+ self.storage.revision_get = MagicMock(return_value=[{
+ 'id': hex_to_hash('18d8be353ed3480476f032475e7c233eff7371d5'),
+ 'directory': hex_to_hash(
+ '7834ef7e7c357ce2af928115c6c6a42b7e2a44e6'),
+ 'author_name': b'bill & boule',
+ 'author_email': b'bill@boule.org',
+ 'committer_name': b'boule & bill',
+ 'committer_email': b'boule@bill.org',
+ 'message': b'elegant fix for bug 31415957',
+ 'date': datetime.datetime(2000, 1, 17, 11, 23, 54),
+ 'date_offset': 0,
+ 'committer_date': datetime.datetime(2000, 1, 17, 11, 23, 54),
+ 'committer_date_offset': 0,
+ 'synthetic': False,
+ 'type': 'git',
+ 'parents': [],
+ 'metadata': [],
+ }])
+
+ # when
+ actual_revision = service.lookup_revision(
+ '18d8be353ed3480476f032475e7c233eff7371d5')
+
+ # then
+ self.assertEqual(actual_revision, {
+ 'id': '18d8be353ed3480476f032475e7c233eff7371d5',
+ 'directory': '7834ef7e7c357ce2af928115c6c6a42b7e2a44e6',
+ 'author_name': 'bill & boule',
+ 'author_email': 'bill@boule.org',
+ 'committer_name': 'boule & bill',
+ 'committer_email': 'boule@bill.org',
+ 'message': 'elegant fix for bug 31415957',
+ 'date': datetime.datetime(2000, 1, 17, 11, 23, 54),
+ 'date_offset': 0,
+ 'committer_date': datetime.datetime(2000, 1, 17, 11, 23, 54),
+ 'committer_date_offset': 0,
+ 'synthetic': False,
+ 'type': 'git',
+ 'parents': [],
+ 'metadata': [],
+ })
+
+ self.storage.revision_get.assert_called_with(
+ [hex_to_hash('18d8be353ed3480476f032475e7c233eff7371d5')])
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Fri, Jul 4, 3:29 PM (1 w, 1 d ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
3302768
Attached To
rDWAPPS Web applications
Event Timeline
Log In to Comment