Doctests

AUTH

Running this test from the buildout directory:

bin/test test_doctests -t auth

Test Setup

Needed Imports:

>>> import transaction
>>> from plone.app.testing import setRoles
>>> from plone.app.testing import TEST_USER_ID
>>> from plone.app.testing import TEST_USER_PASSWORD

Functional Helpers:

>>> def get(url):
...     browser.open("{}/{}".format(api_url, url))
...     return browser.contents

Variables:

>>> portal = self.portal
>>> portal_url = portal.absolute_url()
>>> api_url = "{}/@@API/senaite/v1".format(portal_url)
>>> browser = self.getBrowser()
>>> setRoles(portal, TEST_USER_ID, ["LabManager", "Manager"])
>>> transaction.commit()

Login

User can login with a GET:

>>> get("login?__ac_name={}&__ac_password={}".format(TEST_USER_ID, TEST_USER_PASSWORD))
'..."authenticated": true...'

And once logged, auth route does not rise an unauthorized response 401:

>>> get("auth")
'{"_runtime": ...}'

Logout

User can logout easily too:

>>> get("users/logout")
'..."authenticated": false...'

And auth route rises an unauthorized response 401:

>>> get("auth")
Traceback (most recent call last):
[...]
HTTPError: HTTP Error 401: Unauthorized

VERSION

Running this test from the buildout directory:

bin/test test_doctests -t version

Test Setup

Needed Imports:

>>> import transaction
>>> from plone.app.testing import setRoles
>>> from plone.app.testing import TEST_USER_ID
>>> from plone.app.testing import TEST_USER_PASSWORD

Functional Helpers:

>>> def logout():
...     browser.open(portal_url + "/logout")
...     assert("You are now logged out" in browser.contents)

Variables:

>>> portal = self.portal
>>> portal_url = portal.absolute_url()
>>> browser = self.getBrowser()
>>> setRoles(portal, TEST_USER_ID, ["LabManager", "Manager"])
>>> transaction.commit()

JSON API:

>>> api_base_url = portal_url + "/@@API/senaite/v1"

Authenticated user

The version route should be visible to authenticated users:

>>> browser.open(api_base_url + "/version")
>>> browser.contents
'{"url": "http://nohost/plone/@@API/senaite/v1/version", "date": "...", "version": ..., "_runtime": ...}'

Unauthenticated user

Log out:

>>> logout()

The version route should be visible to unauthenticated users too:

>>> browser.open(api_base_url + "/version")
>>> browser.contents
'{"url": "http://nohost/plone/@@API/senaite/v1/version", "date": "...", "version": ..., "_runtime": ...}'

USERS

Running this test from the buildout directory:

bin/test test_doctests -t users

Test Setup

Needed Imports:

>>> import json
>>> import transaction
>>> from plone.app.testing import setRoles
>>> from plone.app.testing import TEST_USER_ID
>>> from plone.app.testing import TEST_USER_PASSWORD

Functional Helpers:

>>> def get(url):
...     browser.open("{}/{}".format(api_url, url))
...     return browser.contents

Variables:

>>> portal = self.portal
>>> portal_url = portal.absolute_url()
>>> api_url = "{}/@@API/senaite/v1".format(portal_url)
>>> browser = self.getBrowser()
>>> setRoles(portal, TEST_USER_ID, ["LabManager", "Manager"])
>>> transaction.commit()

Get all users

The API is capable to find SENAITE users:

>>> response = get("users")
>>> data = json.loads(response)
>>> items = data.get("items")
>>> sorted(map(lambda it: it["username"], items))
[u'test-user', u'test_analyst_0',...u'test_labmanager_1']

And for each user, the roles and groups are displayed:

>>> analyst = filter(lambda it: it["username"] == "test_analyst_0", items)[0]
>>> sorted(analyst.get("roles"))
[u'Analyst', u'Authenticated', u'Member']
>>> sorted(analyst.get("groups"))
[u'Analysts', u'AuthenticatedUsers']

As well as other properties:

>>> sorted(analyst.keys())
[u'api_url', u'authenticated', u'description', u'email', ...]

Get current user

Current user can also be retrieved easily:

>>> response = get("users/current")
>>> data = json.loads(response)
>>> data.get("count")
1
>>> current = data.get("items")[0]
>>> current.get("username")
u'test-user'

and includes all properties too:

>>> sorted(current.keys())
[u'api_url', u'authenticated', u'description', u'email',...u'groups',...u'roles'...]

Get a single user

A single user can be retrieved too:

>>> get("users/test_analyst_0")
'..."username": "test_analyst_0"...'

CATALOGS

Running this test from the buildout directory:

bin/test test_doctests -t catalogs

Test Setup

Needed Imports:

>>> import json
>>> import transaction
>>> from plone.app.testing import setRoles
>>> from plone.app.testing import TEST_USER_ID
>>> from plone.app.testing import TEST_USER_PASSWORD

Functional Helpers:

>>> def get(url):
...     browser.open("{}/{}".format(api_url, url))
...     return browser.contents

Variables:

>>> portal = self.portal
>>> portal_url = portal.absolute_url()
>>> api_url = "{}/@@API/senaite/v1".format(portal_url)
>>> browser = self.getBrowser()
>>> setRoles(portal, TEST_USER_ID, ["LabManager", "Manager"])
>>> transaction.commit()

Get all catalogs

senaite.jsonapi is capable to retrieve information about the catalogs registered in the system:

>>> response = get("catalogs")
>>> data = json.loads(response)
>>> items = data.get("items")
>>> catalog_ids = map(lambda cat: cat["id"], items)
>>> sorted(catalog_ids)
[u'auditlog_catalog', ..., u'portal_catalog']

Catalogs for internal use are not included though:

>>> "uid_catalog" in catalog_ids
False
>>> "reference_catalog" in catalog_ids
False

For each catalog, indexes, schema fields and allowed portal types are listed:

>>> cat = filter(lambda it: it["id"]=="portal_catalog", items)[0]
>>> sorted(cat.get("indexes"))
[u'Analyst', u'Creator', u'Date', ...]
>>> sorted(cat.get("schema"))
[u'Analyst', u'CreationDate', u'Creator',...]
>>> sorted(cat.get("portal_types"))
[u'ARReport', u'ARTemplate', u'ARTemplates',...]

Get a single catalog

A single catalog can also be retrieved by it’s id:

>>> response = get("catalogs/portal_catalog")
>>> cat = json.loads(response)
>>> sorted(cat.get("indexes"))
[u'Analyst', u'Creator', u'Date', ...]
>>> sorted(cat.get("schema"))
[u'Analyst', u'CreationDate', u'Creator',...]
>>> sorted(cat.get("portal_types"))
[u'ARReport', u'ARTemplate', u'ARTemplates',...]

CREATE

Running this test from the buildout directory:

bin/test test_doctests -t create

Test Setup

Needed Imports:

>>> import json
>>> import transaction
>>> import urllib
>>> from DateTime import DateTime
>>> from plone.app.testing import setRoles
>>> from plone.app.testing import TEST_USER_ID
>>> from plone.app.testing import TEST_USER_PASSWORD
>>> from bika.lims import api

Functional Helpers:

>>> def get(url):
...     browser.open("{}/{}".format(api_url, url))
...     return browser.contents
>>> def post(url, data):
...     url = "{}/{}".format(api_url, url)
...     browser.post(url, urllib.urlencode(data, doseq=True))
...     return browser.contents
>>> def create(data):
...     response = post("create", data)
...     assert("items" in response)
...     response = json.loads(response)
...     items = response.get("items")
...     assert(len(items)==1)
...     item = response.get("items")[0]
...     assert("uid" in item)
...     return api.get_object(item["uid"])

Variables:

>>> portal = self.portal
>>> portal_url = portal.absolute_url()
>>> api_url = "{}/@@API/senaite/v1".format(portal_url)
>>> setup = api.get_setup()
>>> browser = self.getBrowser()
>>> setRoles(portal, TEST_USER_ID, ["LabManager", "Manager"])
>>> transaction.commit()

Create with resource

We can create an object by providing the resource and the parent uid directly in the request:

>>> clients = portal.clients
>>> clients_uid = api.get_uid(clients)
>>> url = "client/create/{}".format(clients_uid)
>>> data = {"title": "Test client 1",
...         "ClientID": "TC1"}
>>> post(url, data)
'...clients/client-1"...'

We can also omit the parent uid while defining the resource, but passing the uid of the container via post:

>>> data = {"title": "Test client 2",
...         "ClientID": "TC2",
...         "parent_uid": clients_uid}
>>> post("client/create", data)
'...clients/client-2"...'

We can use parent_path instead of parent_uid:

>>> data = {"title": "Test client 3",
...         "ClientID": "TC3",
...         "parent_path": api.get_path(clients)}
>>> post("client/create", data)
'...clients/client-3"...'

Create without resource

Or we can create an object without the resource, but with the parent uid and defining the portal_type via post:

>>> url = "create/{}".format(clients_uid)
>>> data = {"title": "Test client 4",
...         "ClientID": "TC4",
...         "portal_type": "Client"}
>>> post(url, data)
'...clients/client-4"...'

Create via post only

We can omit both the resource and container uid and pass everything via post:

>>> data = {"title": "Test client 5",
...         "ClientID": "TC5",
...         "portal_type": "Client",
...         "parent_path": api.get_path(clients)}
>>> post("create", data)
'...clients/client-5"...'
>>> data = {"title": "Test client 6",
...         "ClientID": "TC6",
...         "portal_type": "Client",
...         "parent_uid": clients_uid}
>>> post("create", data)
'...clients/client-6"...'

If we do a search now for clients, we will get all them:

>>> output = get("client")
>>> output = json.loads(output)
>>> items = output.get("items")
>>> items = map(lambda it: it.get("getClientID"), items)
>>> sorted(items)
[u'TC1', u'TC2', u'TC3', u'TC4', u'TC5', u'TC6']

Required fields

System will fail with a 400 error when trying to create an object without a required attribute:

>>> data = {"portal_type": "SampleType",
...         "parent_path": api.get_path(setup.bika_sampletypes),
...         "title": "Fresh Egg",
...         "Prefix": "FE"}
>>> post("create", data)
Traceback (most recent call last):
[...]
HTTPError: HTTP Error 400: Bad Request

Create a Client

>>> data = {"portal_type": "Client",
...         "parent_path": api.get_path(clients),
...         "title": "Omelette corp",
...         "ClientID": "EC"}
>>> client = create(data)
>>> client.getClientID()
'EC'
>>> api.get_parent(client)
<ClientFolder at /plone/clients>

Create a Client Contact

>>> data = {"portal_type": "Contact",
...         "parent_path": api.get_path(client),
...         "Firstname": "Proud",
...         "Surname": "Hen"}
>>> contact = create(data)
>>> contact.getFullname()
'Proud Hen'
>>> api.get_parent(contact)
<Client at /plone/clients/client-7>

Create a Sample Type

>>> data = {"portal_type": "SampleType",
...         "parent_path": api.get_path(setup.bika_sampletypes),
...         "title": "Fresh Egg",
...         "MinimumVolume": "10 gr",
...         "Prefix": "FE"}
>>> sample_type = create(data)
>>> sample_type.Title()
'Fresh Egg'
>>> sample_type.getPrefix()
'FE'
>>> api.get_parent(sample_type)
<SampleTypes at /plone/bika_setup/bika_sampletypes>

Create a Laboratory Contact

>>> data = {"portal_type": "LabContact",
...         "parent_path": api.get_path(setup.bika_labcontacts),
...         "Firstname": "Lab",
...         "Surname": "Chicken"}
>>> lab_contact = create(data)
>>> lab_contact.getFullname()
'Lab Chicken'
>>> api.get_parent(lab_contact)
<LabContacts at /plone/bika_setup/bika_labcontacts>

Create a Department

>>> data = {"portal_type": "Department",
...         "parent_path": api.get_path(setup.bika_departments),
...         "title": "Microbiology",
...         "Manager": api.get_uid(lab_contact)}
>>> department = create(data)
>>> department.Title()
'Microbiology'
>>> api.get_parent(department)
<Departments at /plone/bika_setup/bika_departments>

Create an Analysis Category

>>> data = {"portal_type": "AnalysisCategory",
...         "parent_path": api.get_path(setup.bika_analysiscategories),
...         "title": "Microbiology identification",
...         "Department": api.get_uid(department)}
>>> category = create(data)
>>> category.Title()
'Microbiology identification'
>>> api.get_parent(category)
<AnalysisCategories at /plone/bika_setup/bika_analysiscategories>
>>> category.getDepartment()
<Department at /plone/bika_setup/bika_departments/department-1>

Create an Analysis Service

>>> data = {"portal_type": "AnalysisService",
...         "parent_path": api.get_path(setup.bika_analysisservices),
...         "title": "Salmonella",
...         "Keyword": "Sal",
...         "ScientificName": True,
...         "Price": 15,
...         "Category": api.get_uid(category),
...         "Accredited": True}
>>> sal = create(data)
>>> sal.Title()
'Salmonella'
>>> sal.getKeyword()
'Sal'
>>> sal.getScientificName()
True
>>> sal.getAccredited()
True
>>> sal.getCategory()
<AnalysisCategory at /plone/bika_setup/bika_analysiscategories/analysiscategory-1>
>>> data = {"portal_type": "AnalysisService",
...         "parent_path": api.get_path(setup.bika_analysisservices),
...         "title": "Escherichia coli",
...         "Keyword": "Ecoli",
...         "ScientificName": True,
...         "Price": 15,
...         "Category": api.get_uid(category)}
>>> ecoli = create(data)
>>> ecoli.Title()
'Escherichia coli'
>>> ecoli.getKeyword()
'Ecoli'
>>> ecoli.getScientificName()
True
>>> ecoli.getPrice()
'15.00'
>>> ecoli.getCategory()
<AnalysisCategory at /plone/bika_setup/bika_analysiscategories/analysiscategory-1>

Creating a Sample

The creation of a Sample (AnalysisRequest portal type) is handled differently from the rest of objects, an specific function in senaite.core must be used instead of the plone’s default creation.

>>> data = {"portal_type": "AnalysisRequest",
...         "parent_uid": api.get_uid(client),
...         "Contact": api.get_uid(contact),
...         "DateSampled": DateTime().ISO8601(),
...         "SampleType": api.get_uid(sample_type),
...         "Analyses": map(api.get_uid, [sal, ecoli]) }
>>> sample = create(data)
>>> sample
<AnalysisRequest at /plone/clients/client-7/FE-0001>
>>> analyses = sample.getAnalyses(full_objects=True)
>>> sorted(map(lambda an: an.getKeyword(), analyses))
['Ecoli', 'Sal']
>>> sample.getSampleType()
<SampleType at /plone/bika_setup/bika_sampletypes/sampletype-2>
>>> sample.getClient()
<Client at /plone/clients/client-7>
>>> sample.getContact()
<Contact at /plone/clients/client-7/contact-1>

Creation restrictions

We get a 401 error if we try to create an object inside portal root:

>>> data = {"title": "My clients folder",
...         "portal_type": "ClientsFolder",
...         "parent_path": api.get_path(portal)}
>>> post("create", data)
Traceback (most recent call last):
[...]
HTTPError: HTTP Error 401: Unauthorized

We get a 401 error if we try to create an object inside setup folder:

>>> data = {"title": "My Analysis Categories folder",
...         "portal_type": "AnalysisCategories",
...         "parent_path": api.get_path(setup)}
>>> post("create", data)
Traceback (most recent call last):
[...]
HTTPError: HTTP Error 401: Unauthorized

We get a 401 error when we try to create an object from a type that is not allowed by the container:

>>> data = {"title": "My Method",
...         "portal_type": "Method",
...         "parent_path": api.get_path(clients)}
>>> post("create", data)
Traceback (most recent call last):
[...]
HTTPError: HTTP Error 401: Unauthorized

READ

Running this test from the buildout directory:

bin/test test_doctests -t read

Test Setup

Needed Imports:

>>> import json
>>> import transaction
>>> from plone.app.testing import setRoles
>>> from plone.app.testing import TEST_USER_ID
>>> from plone.app.testing import TEST_USER_PASSWORD
>>> from bika.lims import api

Functional Helpers:

>>> def get(url):
...     browser.open("{}/{}".format(api_url, url))
...     return browser.contents
>>> def get_count(response):
...     data = json.loads(response)
...     return data.get("count")
>>> def get_items_ids(response, sort=True):
...     data = json.loads(response)
...     items = data.get("items")
...     items = map(lambda it: it["id"], items)
...     if sort:
...         return sorted(items)
...     return items
>>> def init_data():
...     api.create(portal.clients, "Client", title="Happy Hills", ClientID="HH")
...     api.create(portal.clients, "Client", title="ACME", ClientID="AC")
...     api.create(portal.clients, "Client", title="Fill the gap", ClientID="FG")
...     transaction.commit()

Variables:

>>> portal = self.portal
>>> portal_url = portal.absolute_url()
>>> api_url = "{}/@@API/senaite/v1".format(portal_url)
>>> browser = self.getBrowser()
>>> setRoles(portal, TEST_USER_ID, ["LabManager", "Manager"])
>>> transaction.commit()

Initialize the instance with some objects for testing:

>>> init_data()

Get resource objects

We can get the objects from a resource type:

>>> response = get("client")
>>> get_count(response)
3
>>> get_items_ids(response)
[u'client-1', u'client-2', u'client-3']

Get by uid

We can directly fetch a given object by it’s UID and resource:

>>> client = api.create(portal.clients, "Client", title="Woow", ClientID="WO")
>>> uid = api.get_uid(client)
>>> transaction.commit()
>>> response = get("client/{}".format(uid))
>>> get_count(response)
1
>>> response
'..."title": "Woow"...'

Even with only the uid:

>>> response = get(uid)
>>> response
'..."title": "Woow"...'

but with no items in the response:

>>> "items" in response
False
>>> sorted(json.loads(response).keys())
[u'AccountName', u'AccountNumber', u'AccountType',...]

UPDATE

Running this test from the buildout directory:

bin/test test_doctests -t update

Test Setup

Needed Imports:

>>> import json
>>> import transaction
>>> import urllib
>>> from plone.app.testing import setRoles
>>> from plone.app.testing import TEST_USER_ID
>>> from plone.app.testing import TEST_USER_PASSWORD
>>> from bika.lims import api

Functional Helpers:

>>> def get(url):
...     browser.open("{}/{}".format(api_url, url))
...     return browser.contents
>>> def post(url, data):
...     url = "{}/{}".format(api_url, url)
...     browser.post(url, urllib.urlencode(data, doseq=True))
...     return browser.contents
>>> def get_item_object(response):
...     assert("items" in response)
...     response = json.loads(response)
...     items = response.get("items")
...     assert(len(items)==1)
...     item = response.get("items")[0]
...     assert("uid" in item)
...     return api.get_object(item["uid"])
>>> def create(data):
...     response = post("create", data)
...     return get_item_object(response)

Variables:

>>> portal = self.portal
>>> portal_url = portal.absolute_url()
>>> api_url = "{}/@@API/senaite/v1".format(portal_url)
>>> setup = api.get_setup()
>>> browser = self.getBrowser()
>>> setRoles(portal, TEST_USER_ID, ["LabManager", "Manager"])
>>> transaction.commit()

Initialize the instance with some objects for testing:

>>> clients = api.get_portal().clients
>>> data = {"portal_type": "Client",
...         "parent_path": api.get_path(clients),
...         "title": "Chicken corp",
...         "ClientID": "CC"}
>>> client1 = create(data)
>>> data = {"portal_type": "Client",
...         "parent_path": api.get_path(clients),
...         "title": "Beef Corp",
...         "ClientID": "BC"}
>>> client2 = create(data)
>>> data = {"portal_type": "Client",
...         "parent_path": api.get_path(clients),
...         "title": "Octopus Corp",
...         "ClientID": "OC"}
>>> client3 = create(data)

Update by resource and uid

We can update an object by providing the resource and the uid of the object:

>>> client_uid = api.get_uid(client1)
>>> data = {"ClientID": "CC1"}
>>> response = post("client/update/{}".format(client_uid), data)
>>> obj = get_item_object(response)
>>> obj.getClientID()
'CC1'

Update by uid without resource

Even easier, we can update with only the uid:

>>> data = {"ClientID": "CC2"}
>>> response = post("update/{}".format(client_uid), data)
>>> obj = get_item_object(response)
>>> obj.getClientID()
'CC2'

Update via post only

When updating by resource (without an UID explicitly set), the system expects a the data to passed via POST to contain the item to be updated.

The object to be updated can be send in the HTTP POST body by using the uid:

>>> data = {"uid": client_uid,
...         "ClientID": "CC3"}
>>> response = post("update", data)
>>> obj = get_item_object(response)
>>> obj.getClientID()
'CC3'

By using the path, as the physical path of the object:

>>> data = {"path": api.get_path(client1),
...         "ClientID": "CC4"}
>>> response = post("update", data)
>>> obj = get_item_object(response)
>>> obj.getClientID()
'CC4'

Or by using the id of the object together with parent_path, as the physical path of the container object:

>>> data = {"id": api.get_id(client1),
...         "parent_path": api.get_path(clients),
...         "ClientID": "CC5"}
>>> response = post("update", data)
>>> obj = get_item_object(response)
>>> obj.getClientID()
'CC5'

Do a transition

We can transition the objects by using the keyord transition in the data sent via POST:

>>> api.is_active(client1)
True
>>> data = {"uid": api.get_uid(client1),
...         "transition": "deactivate"}
>>> response = post("update", data)
>>> obj = get_item_object(response)
>>> api.is_active(obj)
False

We can update and transition at same time:

>>> data = {"uid": api.get_uid(client1),
...         "ClientID": "CC6",
...         "transition": "activate"}
>>> response = post("update", data)
>>> obj = get_item_object(response)
>>> api.is_active(obj)
True
>>> obj.getClientID()
'CC6'

Update restrictions

We get a 401 error if we try to update an object from inside portal root:

>>> data = {"title": "My clients folder",
...         "uid": api.get_uid(clients),}
>>> post("update", data)
Traceback (most recent call last):
[...]
HTTPError: HTTP Error 401: Unauthorized

We get a 401 error if we try to update an object from inside setup folder:

>>> cats_uid = api.get_uid(api.get_setup().bika_analysiscategories)
>>> data = {"title": "My Analysis Categories folder",
...         "uid": cats_uid,}
>>> post("update", data)
Traceback (most recent call last):
[...]
HTTPError: HTTP Error 401: Unauthorized

We cannot update the id of an object:

>>> original_id = api.get_id(client1)
>>> data = {"id": "client-123123",
...         "uid": client_uid }
>>> response = post("update", data)
>>> obj = get_item_object(response)
>>> api.get_id(obj) == original_id
True

PUSH

Running this test from the buildout directory:

bin/test test_doctests -t push

Test Setup

Needed Imports:

>>> import json
>>> import transaction
>>> import urllib
>>> from plone.app.testing import setRoles
>>> from plone.app.testing import TEST_USER_ID
>>> from plone.app.testing import TEST_USER_PASSWORD
>>> from senaite.jsonapi.interfaces import IPushConsumer
>>> from zope.component import getGlobalSiteManager
>>> from zope.interface import implements

Functional Helpers:

>>> def post(url, data):
...     url = "{}/{}".format(api_url, url)
...     browser.post(url, urllib.urlencode(data, doseq=True))
...     return browser.contents

Variables:

>>> portal = self.portal
>>> portal_url = portal.absolute_url()
>>> api_url = "{}/@@API/senaite/v1".format(portal_url)
>>> browser = self.getBrowser()
>>> setRoles(portal, TEST_USER_ID, ["LabManager", "Manager"])
>>> transaction.commit()

Create a dummy IPushConsumer adapter:

>>> class DummyConsumerAdapter(object):
...     implements(IPushConsumer)
...
...     def __init__(self, record):
...         self.record = record
...
...     def process(self):
...         if not self.record.get("target"):
...             return False
...         return True
>>> sm = getGlobalSiteManager()
>>> sm.registerAdapter(DummyConsumerAdapter, (dict,), IPushConsumer, name="dummy")
>>> transaction.commit()

Push with success

>>> post("push", {"consumer": "dummy", "target": "defined"})
'..."success": true...'

Push without success

If an adapter is registered, but it rises an exception, the outcome is failed:

>>> post("push", {"consumer": "dummy"})
'..."success": false...'

Non-registered adapter

>>> post("push", {"consumer": "zummy"})
Traceback (most recent call last):
[...]
HTTPError: HTTP Error 500: Internal Server Error