Skip to content

feat: ownership on view dossier and action#347

Draft
alexandre-gauvin wants to merge 67 commits into
developfrom
improvement/multi_owner-view-dossier-actions
Draft

feat: ownership on view dossier and action#347
alexandre-gauvin wants to merge 67 commits into
developfrom
improvement/multi_owner-view-dossier-actions

Conversation

@alexandre-gauvin

@alexandre-gauvin alexandre-gauvin commented May 11, 2026

Copy link
Copy Markdown
Collaborator

View ownership was modeled and handled as a list even though each view should have exactly one owner. This made ownership semantics inconsistent across the API, persistence model, and tests.

  • Model

    • Changed View.owner from list[str] to str.
    • Kept privilege mapping keyed by owner, but now it resolves to a single username.
  • API behavior

    • Create/update/delete/favourite paths now compare owners as strings.
    • PUT /view/{id}/permission treats owner as a transfer target, not a multi-value membership set.
    • DELETE on owner is rejected to preserve single-owner semantics.
  • Test/data updates

    • Updated seeded view fixtures to use string owners.
    • Adjusted integration tests to assert string ownership and transfer behavior.
view.owner = kwargs["user"]["uname"]

if existing_view.owner != user.uname:
    return forbidden(...)

Related Issue: multi_owner-view-dossier-actions
…anging it

Related Issue: multi_owner-view-dossier-actions
Related Issue: multi_owner-view-dossier-actions
Related Issue: multi_owner-view-dossier-actions
Signed-off-by: agauvin <alexandre.gauvin@cse-cst.gc.ca>

Related Issue: multi_owner-view-dossier-actions
Signed-off-by: agauvin <alexandre.gauvin@cse-cst.gc.ca>

Related Issue: multi_owner-view-dossier-actions
Signed-off-by: agauvin <alexandre.gauvin@cse-cst.gc.ca>

Related Issue: multi_owner-view-dossier-actions
Signed-off-by: agauvin <alexandre.gauvin@cse-cst.gc.ca>

Related Issue: multi_owner-view-dossier-actions
Signed-off-by: agauvin <alexandre.gauvin@cse-cst.gc.ca>

Related Issue: multi_owner-view-dossier-actions
Signed-off-by: agauvin <alexandre.gauvin@cse-cst.gc.ca>

Related Issue: multi_owner-view-dossier-actions
Signed-off-by: agauvin <alexandre.gauvin@cse-cst.gc.ca>

Related Issue: multi_owner-view-dossier-actions
Signed-off-by: agauvin <alexandre.gauvin@cse-cst.gc.ca>

Related Issue: multi_owner-view-dossier-actions
Signed-off-by: agauvin <alexandre.gauvin@cse-cst.gc.ca>

Related Issue: multi_owner-view-dossier-actions
Signed-off-by: agauvin <alexandre.gauvin@cse-cst.gc.ca>

Related Issue: multi_owner-view-dossier-actions
Signed-off-by: agauvin <alexandre.gauvin@cse-cst.gc.ca>

Related Issue: multi_owner-view-dossier-actions
…sier)

Signed-off-by: agauvin <alexandre.gauvin@cse-cst.gc.ca>

Related Issue: multi_owner-view-dossier-actions
…ions)

Signed-off-by: agauvin <alexandre.gauvin@cse-cst.gc.ca>

Related Issue: multi_owner-view-dossier-actions
Signed-off-by: agauvin <alexandre.gauvin@cse-cst.gc.ca>

Related Issue: multi_owner-view-dossier-actions
Signed-off-by: agauvin <alexandre.gauvin@cse-cst.gc.ca>

Related Issue: multi_owner-view-dossier-actions
Signed-off-by: agauvin <alexandre.gauvin@cse-cst.gc.ca>

Related Issue: multi_owner-view-dossier-actions
Signed-off-by: agauvin <alexandre.gauvin@cse-cst.gc.ca>

Related Issue: multi_owner-view-dossier-actions
Signed-off-by: agauvin <alexandre.gauvin@cse-cst.gc.ca>

Related Issue: multi_owner-view-dossier-actions
Signed-off-by: agauvin <alexandre.gauvin@cse-cst.gc.ca>

Related Issue: multi_owner-view-dossier-actions
Signed-off-by: agauvin <alexandre.gauvin@cse-cst.gc.ca>

Related Issue: multi_owner-view-dossier-actions
Signed-off-by: agauvin <alexandre.gauvin@cse-cst.gc.ca>

Related Issue: multi_owner-view-dossier-actions
…ming

Signed-off-by: agauvin <alexandre.gauvin@cse-cst.gc.ca>

Related Issue: multi_owner-view-dossier-actions
Signed-off-by: agauvin <alexandre.gauvin@cse-cst.gc.ca>

Related Issue: multi_owner-view-dossier-actions
Signed-off-by: agauvin <alexandre.gauvin@cse-cst.gc.ca>

Related Issue: multi_owner-view-dossier-actions
Signed-off-by: agauvin <alexandre.gauvin@cse-cst.gc.ca>

Related Issue: multi_owner-view-dossier-actions
Signed-off-by: agauvin <alexandre.gauvin@cse-cst.gc.ca>

Related Issue: multi_owner-view-dossier-actions
@github-actions

github-actions Bot commented May 14, 2026

Copy link
Copy Markdown
Contributor

Static Badge

Error Output
_______________________________ test_formatting ________________________________

datastore_connection = <howler.datastore.howler_store.HowlerDatastore object at 0x7efcc788e270>
login_session = (<requests.sessions.Session object at 0x7efcc44abbc0>, 'http://localhost:5000')

    def test_formatting(datastore_connection, login_session):
        session, host = login_session
    
        api_list = get_api_data(session, f"{host}/api/")
    
        for api in api_list:
            resp = get_api_data(session, f"{host}/api/{api}/")
    
            for api in resp["apis"]:
                description: str = api["description"]
                assert len(description) > 0, f"Endpoint {api['function']} is missing its docstring!"
    
                path: str = api["path"]
                matches = re.findall(r"<(\w+)>", path)
    
                assert re.search(
                    r"\n *Variables:", description
                ), f"Endpoint {api['function']} is missing a Variables: portion of the docstring!"
    
                if len(matches) > 0:
                    desc_parts = [
                        line.strip()
                        for line in description.splitlines()
                        if any(line.strip().startswith(match) for match in matches) and "=>" in line
                    ]
    
                    variables_err = (
                        f"Endpoint {api['function']} is missing a properly formatted Variables: portion of the docstring!"
                    )
    
                    if len(desc_parts) != len(matches):
                        warnings.warn(variables_err)
    
>               assert re.search(r"\n *Arguments:", description) or re.search(
                    r"\n *Optional Arguments:", description
                ), f"Endpoint {api['function']} is missing an Arguments: portion of the docstring!"
E               AssertionError: Endpoint api.v1.action.get_permission_option is missing an Arguments: portion of the docstring!
E               assert (None or None)
E                +  where None = <function search at 0x7efccd3d7880>('\\n *Arguments:', 'Get the permission options for a given action\n\n    Variables:\n        action_id => The id of the Action to get permissions for\n    returns a dict with the possible permissions for the action and the users that have them.\n')
E                +    where <function search at 0x7efccd3d7880> = re.search
E                +  and   None = <function search at 0x7efccd3d7880>('\\n *Optional Arguments:', 'Get the permission options for a given action\n\n    Variables:\n        action_id => The id of the Action to get permissions for\n    returns a dict with the possible permissions for the action and the users that have them.\n')
E                +    where <function search at 0x7efccd3d7880> = re.search

test/integration/api/test_doc.py:52: AssertionError
_________________________ test_give_remove_membership __________________________

datastore = <howler.datastore.howler_store.HowlerDatastore object at 0x7efcc788e270>
user_session = <function user_session.<locals>.build_session at 0x7efcb4b5d440>

    def test_give_remove_membership(
        datastore: HowlerDatastore,
        user_session,
    ):
        """
        Test adding a user and removing a user from a dossier
        """
        owner_session, host = user_session("user")
        member_session, _ = user_session("huey")
    
        member_uname = get_api_data(member_session, f"{host}/api/v1/user/whoami", method="GET")["username"]
        # Create the dossier
        create_res = get_api_data(
            owner_session,
            f"{host}/api/v1/dossier/",
            method="POST",
            data=json.dumps({"title": "testremove", "type": "global", "query": "howler.hash:*"}),
        )
        dossier: Dossier = datastore.dossier.get(create_res["dossier_id"], as_obj=True)
    
        # Give|Remove every possible membership
        for request in ("PUT", "DELETE"):
            for membership in dossier.get_privilege_mapping().keys():
>               get_api_data(
                    owner_session,
                    f"{host}/api/v1/dossier/{create_res['dossier_id']}/permission",
                    method=request,
                    data=json.dumps(
                        {
                            "user_id": member_uname,
                            "privilege": membership,
                        }
                    ),
                )

test/integration/api/test_dossier.py:385: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

session = <requests.sessions.Session object at 0x7efcb4838aa0>
url = 'http://localhost:5000/api/v1/dossier/6qT0I0U3rXJpLLkbEhpXR/permission'
params = None, data = '{"user_id": "huey", "privilege": "administrator"}'
method = 'DELETE', raw = False, headers = {'content-type': 'application/json'}
files = None

    def get_api_data(  # noqa: C901
        session,
        url,
        params=None,
        data=None,
        method="GET",
        raw=False,
        headers=None,
        files=None,
    ):
        if headers is None:
            headers = {"content-type": "application/json"}
    
        with warnings.catch_warnings():
            warnings.simplefilter("ignore")
    
            if method == "GET":
                res = session.get(url, params=params, verify=False, headers=headers)
            elif method == "POST":
                res = session.post(
                    url,
                    data=data,
                    params=params,
                    verify=False,
                    headers=headers,
                    files=files,
                )
            elif method == "DELETE":
                res = session.delete(url, data=data, params=params, verify=False, headers=headers)
            elif method == "PUT":
                res = session.put(
                    url,
                    data=data,
                    params=params,
                    verify=False,
                    headers=headers,
                    files=files,
                )
            else:
                raise InvalidRequestMethod(method)
    
            if "XSRF-TOKEN" in res.cookies:
                session.headers.update({"X-XSRF-TOKEN": res.cookies["XSRF-TOKEN"]})
    
            if raw:
                return res
            else:
                if res.ok:
                    if res.status_code == 204:
                        return None
    
                    try:
                        res_data = res.json()
                        return res_data["api_response"]
                    except Exception:
                        raise APIError(f"{res.status_code}: {res.content or None}")
                else:
                    try:
                        res_data = res.json()
    
>                       raise APIError(
                            f"{res.status_code}: {res_data['api_error_message']}",
                            json=res_data,
                        )
E                       test.conftest.APIError: 400: You are not allowed to remove huey from administrator

test/conftest.py:240: APIError
_____________________________ test_owner_privilege _____________________________

datastore = <howler.datastore.howler_store.HowlerDatastore object at 0x7efcc788e270>
user_session = <function user_session.<locals>.build_session at 0x7efcb4b5d300>

    def test_owner_privilege(datastore: HowlerDatastore, user_session: dict):
        owner_session, host = user_session("user")
        member_session, _ = user_session("huey")
    
        member_uname = get_api_data(member_session, f"{host}/api/v1/user/whoami", method="GET")["username"]
        owner_uname = get_api_data(owner_session, f"{host}/api/v1/user/whoami", method="GET")["username"]
        # Create the dossier
        create_res = get_api_data(
            owner_session,
            f"{host}/api/v1/dossier/",
            method="POST",
            data=json.dumps({"title": "test_membership", "type": "global", "query": "howler.hash:*"}),
        )
        datastore.dossier.commit()
        dossier: Dossier = datastore.dossier.get(create_res["dossier_id"], as_obj=True)
        # adding|remove user to admin, member and owner
        add_permission_every_role(
            member_to_add=member_uname, create_res=create_res, member_requesting=owner_session, host=host, dossier=dossier
        )
    
        dossier = datastore.dossier.get(create_res["dossier_id"], as_obj=True)
        for membership in dossier.get_privilege_mapping().keys():
            assert member_uname in dossier.get_privilege_mapping()[membership]
    
        remove_permission_every_role(
            member_to_remove=member_uname,
            create_res=create_res,
            member_requesting=owner_session,
            host=host,
            dossier=dossier,
        )
    
        dossier = datastore.dossier.get(create_res["dossier_id"], as_obj=True)
        for membership in dossier.get_privilege_mapping().keys():
>           assert member_uname not in dossier.get_privilege_mapping()[membership]
E           AssertionError: assert 'huey' not in ['huey']

test/integration/api/test_dossier.py:447: AssertionError
__________________________________ test_admin __________________________________

datastore = <howler.datastore.howler_store.HowlerDatastore object at 0x7efcc788e270>
user_session = <function user_session.<locals>.build_session at 0x7efcb4b5d8a0>
login_session = (<requests.sessions.Session object at 0x7efcc77852e0>, 'http://localhost:5000')

    def test_admin(datastore: HowlerDatastore, user_session: Callable[[str], tuple[requests.Session, str]], login_session):
        """
        Test Admin privilege on view dossier and actions. This will attempt on adding, removing member from positions and
        verify that the permission an admin have are the intended ones.
        """
        admin_session, host = user_session("user")
        member_session, _ = user_session("huey")
        owner_session, _ = login_session
    
        member_uname = get_api_data(member_session, f"{host}/api/v1/user/whoami", method="GET")["username"]
        owner_uname = get_api_data(owner_session, f"{host}/api/v1/user/whoami", method="GET")["username"]
        admin_uname = get_api_data(admin_session, f"{host}/api/v1/user/whoami", method="GET")["username"]
        # Create the dossier
        create_res = get_api_data(
            owner_session,
            f"{host}/api/v1/dossier/",
            method="POST",
            data=json.dumps({"title": "test_membership", "type": "global", "query": "howler.hash:*"}),
        )
        datastore.dossier.commit()
        dossier: Dossier = datastore.dossier.get(create_res["dossier_id"], as_obj=True)
        # giving admin to admin
        get_api_data(
            owner_session,
            f"{host}/api/v1/dossier/{create_res['dossier_id']}/permission",
            method="PUT",
            data=json.dumps(
                {
                    "user_id": admin_uname,
                    "privilege": "administrator",
                }
            ),
        )
        assert owner_uname not in dossier.get_privilege_mapping()["administrator"]  # ensure user is admin
    
        # Admin should be able to add|remove member and other admin
        for method in ["PUT", "DELETE"]:
>           get_api_data(
                admin_session,
                f"{host}/api/v1/dossier/{create_res['dossier_id']}/permission",
                method=method,
                data=json.dumps(
                    {
                        "user_id": member_uname,
                        "privilege": "administrator",
                    }
                ),
            )

test/integration/api/test_dossier.py:578: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

session = <requests.sessions.Session object at 0x7efcc7785d90>
url = 'http://localhost:5000/api/v1/dossier/1bcsv6q31nYCkZn5WwsXzy/permission'
params = None, data = '{"user_id": "huey", "privilege": "administrator"}'
method = 'DELETE', raw = False, headers = {'content-type': 'application/json'}
files = None

    def get_api_data(  # noqa: C901
        session,
        url,
        params=None,
        data=None,
        method="GET",
        raw=False,
        headers=None,
        files=None,
    ):
        if headers is None:
            headers = {"content-type": "application/json"}
    
        with warnings.catch_warnings():
            warnings.simplefilter("ignore")
    
            if method == "GET":
                res = session.get(url, params=params, verify=False, headers=headers)
            elif method == "POST":
                res = session.post(
                    url,
                    data=data,
                    params=params,
                    verify=False,
                    headers=headers,
                    files=files,
                )
            elif method == "DELETE":
                res = session.delete(url, data=data, params=params, verify=False, headers=headers)
            elif method == "PUT":
                res = session.put(
                    url,
                    data=data,
                    params=params,
                    verify=False,
                    headers=headers,
                    files=files,
                )
            else:
                raise InvalidRequestMethod(method)
    
            if "XSRF-TOKEN" in res.cookies:
                session.headers.update({"X-XSRF-TOKEN": res.cookies["XSRF-TOKEN"]})
    
            if raw:
                return res
            else:
                if res.ok:
                    if res.status_code == 204:
                        return None
    
                    try:
                        res_data = res.json()
                        return res_data["api_response"]
                    except Exception:
                        raise APIError(f"{res.status_code}: {res.content or None}")
                else:
                    try:
                        res_data = res.json()
    
>                       raise APIError(
                            f"{res.status_code}: {res_data['api_error_message']}",
                            json=res_data,
                        )
E                       test.conftest.APIError: 400: You are not allowed to remove huey from administrator

test/conftest.py:240: APIError
_________________________________ test_member __________________________________

datastore = <howler.datastore.howler_store.HowlerDatastore object at 0x7efcc788e270>
user_session = <function user_session.<locals>.build_session at 0x7efcb4b5e020>

    def test_member(datastore: HowlerDatastore, user_session: dict):
        owner_session, host = user_session("user")
        member_session, _ = user_session("huey")
        member_uname = get_api_data(member_session, f"{host}/api/v1/user/whoami", method="GET")["username"]
        owner_uname = get_api_data(owner_session, f"{host}/api/v1/user/whoami", method="GET")["username"]
        # Create the dossier
        create_res = get_api_data(
            owner_session,
            f"{host}/api/v1/dossier/",
            method="POST",
            data=json.dumps({"title": "test_membership", "type": "global", "query": "howler.hash:*"}),
        )
        # Giving membership to member
        datastore.dossier.commit()
        get_api_data(
            owner_session,
            f"{host}/api/v1/dossier/{create_res['dossier_id']}/permission",
            method="PUT",
            data=json.dumps(
                {
                    "user_id": member_uname,
                    "privilege": "member",
                }
            ),
        )
        datastore.dossier.commit()
        dossier = datastore.dossier.get(create_res["dossier_id"], as_obj=True)
        assert member_uname in dossier.get_privilege_mapping()["member"]  # ensure the membership was given
    
        # Member should not be able to add admin/owner/member
        add_permission_every_role(
            create_res=create_res, host=host, member_requesting=member_session, member_to_add=member_uname, dossier=dossier
        )
        datastore.dossier.commit()
        dossier = datastore.dossier.get(create_res["dossier_id"], as_obj=True)
        for membership in ["owner", "administrator"]:
            assert member_uname not in dossier.get_privilege_mapping()[membership]
    
        # Member should not be able to remove admin/owner/member
        # adding owner into every role
        add_permission_every_role(
            create_res=create_res, host=host, member_requesting=owner_session, member_to_add=owner_uname, dossier=dossier
        )
        # verify owner is in every role
        datastore.dossier.commit()
        dossier = datastore.dossier.get(create_res["dossier_id"], as_obj=True)
        for membership in dossier.get_privilege_mapping().keys():
            assert owner_uname in dossier.get_privilege_mapping()[membership]
    
        remove_permission_every_role(
            create_res=create_res,
            host=host,
            member_requesting=member_session,
            member_to_remove=member_uname,
            dossier=dossier,
        )
        # ensure owner is still in every role
        datastore.dossier.commit()
        dossier = datastore.dossier.get(create_res["dossier_id"], as_obj=True)
        for membership in dossier.get_privilege_mapping().keys():
            assert owner_uname in dossier.get_privilege_mapping()[membership]
        # Member should not be able to delete dossier
        total = datastore.dossier.search("dossier_id:*")["total"]
        try:
            get_api_data(
                member_session,
                f"{host}/api/v1/dossier/{create_res['dossier_id']}",
                method="DELETE",
            )
        except Exception:
            # intended fail
            pass
    
        assert total == datastore.dossier.search("dossier_id:*")["total"]  # Should not have deleted
    
        # Member should be able to update dossier
>       modifying_dossier(
            member_requesting=member_session, create_res=create_res, host=host, dossier_name="MEMBER_CHANGED_NAME"
        )

test/integration/api/test_dossier.py:752: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
test/integration/api/test_dossier.py:351: in modifying_dossier
    get_api_data(
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

session = <requests.sessions.Session object at 0x7efcb4838aa0>
url = 'http://localhost:5000/api/v1/dossier/65ZN8QUn29qj7JHpX0oZbd'
params = None, data = '{"title": "MEMBER_CHANGED_NAME", "query": "howler.id:*"}'
method = 'PUT', raw = False, headers = {'content-type': 'application/json'}
files = None

    def get_api_data(  # noqa: C901
        session,
        url,
        params=None,
        data=None,
        method="GET",
        raw=False,
        headers=None,
        files=None,
    ):
        if headers is None:
            headers = {"content-type": "application/json"}
    
        with warnings.catch_warnings():
            warnings.simplefilter("ignore")
    
            if method == "GET":
                res = session.get(url, params=params, verify=False, headers=headers)
            elif method == "POST":
                res = session.post(
                    url,
                    data=data,
                    params=params,
                    verify=False,
                    headers=headers,
                    files=files,
                )
            elif method == "DELETE":
                res = session.delete(url, data=data, params=params, verify=False, headers=headers)
            elif method == "PUT":
                res = session.put(
                    url,
                    data=data,
                    params=params,
                    verify=False,
                    headers=headers,
                    files=files,
                )
            else:
                raise InvalidRequestMethod(method)
    
            if "XSRF-TOKEN" in res.cookies:
                session.headers.update({"X-XSRF-TOKEN": res.cookies["XSRF-TOKEN"]})
    
            if raw:
                return res
            else:
                if res.ok:
                    if res.status_code == 204:
                        return None
    
                    try:
                        res_data = res.json()
                        return res_data["api_response"]
                    except Exception:
                        raise APIError(f"{res.status_code}: {res.content or None}")
                else:
                    try:
                        res_data = res.json()
    
>                       raise APIError(
                            f"{res.status_code}: {res_data['api_error_message']}",
                            json=res_data,
                        )
E                       test.conftest.APIError: 403: Only the members of a dossier and administrators can edit a global dossier.

test/conftest.py:240: APIError
_________________________ test_give_remove_membership __________________________

datastore = <howler.datastore.howler_store.HowlerDatastore object at 0x7efcc788e270>
user_session = <function user_session.<locals>.build_session at 0x7efcc4177c40>

    def test_give_remove_membership(
        datastore: HowlerDatastore,
        user_session,
    ):
        """
        Test adding a user and removing a user from a view
        """
        owner_session, host = user_session()
        member_session, _ = user_session("huey")
    
        member_uname = get_api_data(member_session, f"{host}/api/v1/user/whoami", method="GET")["username"]
        # Create the view
        create_res = get_api_data(
            owner_session,
            f"{host}/api/v1/view/",
            method="POST",
            data=json.dumps({"title": "testremove", "type": "global", "query": "howler.hash:*"}),
        )
        view: View = datastore.view.get(create_res["view_id"], as_obj=True)
    
        # Give|Remove every possible membership
        for request in ("PUT", "DELETE"):
            for membership in view.get_privilege_mapping().keys():
                if membership == "owner" and request == "DELETE":
                    with pytest.raises(APIError):
                        get_api_data(
                            owner_session,
                            f"{host}/api/v1/view/{create_res['view_id']}/permission",
                            method=request,
                            data=json.dumps(
                                {
                                    "user_id": member_uname,
                                    "privilege": membership,
                                }
                            ),
                        )
                    return
    
>               get_api_data(
                    owner_session,
                    f"{host}/api/v1/view/{create_res['view_id']}/permission",
                    method=request,
                    data=json.dumps(
                        {
                            "user_id": member_uname,
                            "privilege": membership,
                        }
                    ),
                )

test/integration/api/test_view.py:284: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

session = <requests.sessions.Session object at 0x7efcb482f290>
url = 'http://localhost:5000/api/v1/view/3o2oeFWryhvNk70YJ6x40z/permission'
params = None, data = '{"user_id": "huey", "privilege": "administrator"}'
method = 'DELETE', raw = False, headers = {'content-type': 'application/json'}
files = None

    def get_api_data(  # noqa: C901
        session,
        url,
        params=None,
        data=None,
        method="GET",
        raw=False,
        headers=None,
        files=None,
    ):
        if headers is None:
            headers = {"content-type": "application/json"}
    
        with warnings.catch_warnings():
            warnings.simplefilter("ignore")
    
            if method == "GET":
                res = session.get(url, params=params, verify=False, headers=headers)
            elif method == "POST":
                res = session.post(
                    url,
                    data=data,
                    params=params,
                    verify=False,
                    headers=headers,
                    files=files,
                )
            elif method == "DELETE":
                res = session.delete(url, data=data, params=params, verify=False, headers=headers)
            elif method == "PUT":
                res = session.put(
                    url,
                    data=data,
                    params=params,
                    verify=False,
                    headers=headers,
                    files=files,
                )
            else:
                raise InvalidRequestMethod(method)
    
            if "XSRF-TOKEN" in res.cookies:
                session.headers.update({"X-XSRF-TOKEN": res.cookies["XSRF-TOKEN"]})
    
            if raw:
                return res
            else:
                if res.ok:
                    if res.status_code == 204:
                        return None
    
                    try:
                        res_data = res.json()
                        return res_data["api_response"]
                    except Exception:
                        raise APIError(f"{res.status_code}: {res.content or None}")
                else:
                    try:
                        res_data = res.json()
    
>                       raise APIError(
                            f"{res.status_code}: {res_data['api_error_message']}",
                            json=res_data,
                        )
E                       test.conftest.APIError: 400: You are not allowed to give administrator on view 3o2oeFWryhvNk70YJ6x40z

test/conftest.py:240: APIError
_____________________________ test_owner_privilege _____________________________

datastore = <howler.datastore.howler_store.HowlerDatastore object at 0x7efcc788e270>
user_session = <function user_session.<locals>.build_session at 0x7efcc4170900>

    def test_owner_privilege(datastore: HowlerDatastore, user_session: dict):
        owner_session, host = user_session()
        member_session, _ = user_session("huey")
    
        member_uname = get_api_data(member_session, f"{host}/api/v1/user/whoami", method="GET")["username"]
        owner_uname = get_api_data(owner_session, f"{host}/api/v1/user/whoami", method="GET")["username"]
        # Create the view
        create_res = get_api_data(
            owner_session,
            f"{host}/api/v1/view/",
            method="POST",
            data=json.dumps({"title": "test_membership", "type": "global", "query": "howler.hash:*"}),
        )
        datastore.view.commit()
        view: View = datastore.view.get(create_res["view_id"], as_obj=True)
        # adding|remove user to admin, member and owner
>       add_permission_every_role(
            member_to_add=member_uname, create_res=create_res, member_requesting=owner_session, host=host, view=view
        )

test/integration/api/test_view.py:325: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

member_to_add = 'huey'
member_requesting = <requests.sessions.Session object at 0x7efcb482e150>
create_res = {'admin': [], 'member': [], 'owner': 'user', 'query': 'howler.hash:*', ...}
host = 'http://localhost:5000'
view = <View {"view_id": "3DymPwUbvd6jKInP2E2lG6", "title": "test_membership", "query": "howler.hash:*", "type": "global", "owner": "user", "admin": [], "member": [], "settings": {"advance_on_triage": false}}>

    def add_permission_every_role(member_to_add: str, member_requesting, create_res, host, view):
        try:
            for membership in view.get_privilege_mapping().keys():
                resp = get_api_data(
                    member_requesting,
                    f"{host}/api/v1/view/{create_res['view_id']}/permission",
                    method="PUT",
                    data=json.dumps(
                        {
                            "user_id": member_to_add,
                            "privilege": membership,
                        }
                    ),
                )
                if membership == "owner":
                    assert resp["owner"] == member_to_add
                    continue
>               assert member_to_add in resp[membership]
                                        ^^^^^^^^^^^^^^^^
E               KeyError: 'administrator'

test/integration/api/test_view.py:205: KeyError
_________________________________ test_member __________________________________

datastore = <howler.datastore.howler_store.HowlerDatastore object at 0x7efcc788e270>
user_session = <function user_session.<locals>.build_session at 0x7efcc4170fe0>

    def test_member(datastore: HowlerDatastore, user_session: dict):
        owner_session, host = user_session()
        member_session, _ = user_session("huey")
        member_uname = get_api_data(member_session, f"{host}/api/v1/user/whoami", method="GET")["username"]
        owner_uname = get_api_data(owner_session, f"{host}/api/v1/user/whoami", method="GET")["username"]
        # Create the view
        create_res = get_api_data(
            owner_session,
            f"{host}/api/v1/view/",
            method="POST",
            data=json.dumps({"title": "test_membership", "type": "global", "query": "howler.hash:*"}),
        )
        # Giving membership to member
        datastore.view.commit()
        get_api_data(
            owner_session,
            f"{host}/api/v1/view/{create_res['view_id']}/permission",
            method="PUT",
            data=json.dumps(
                {
                    "user_id": member_uname,
                    "privilege": "member",
                }
            ),
        )
        datastore.view.commit()
        view = datastore.view.get(create_res["view_id"], as_obj=True)
        assert member_uname in view.get_privilege_mapping()["member"]  # ensure the membership was given
    
        # Member should not be able to add admin/owner/member
        add_permission_every_role(
            create_res=create_res, host=host, member_requesting=member_session, member_to_add=member_uname, view=view
        )
        datastore.view.commit()
        view = datastore.view.get(create_res["view_id"], as_obj=True)
        for membership in ["owner", "administrator"]:
            value = view.get_privilege_mapping()[membership]
            if membership == "owner":
                assert value != member_uname
            else:
                assert member_uname not in value
    
        # Member should not be able to remove admin/owner/member
        # adding owner into every role
>       add_permission_every_role(
            create_res=create_res, host=host, member_requesting=owner_session, member_to_add=owner_uname, view=view
        )

test/integration/api/test_view.py:609: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

member_to_add = 'user'
member_requesting = <requests.sessions.Session object at 0x7efcc446b6e0>
create_res = {'admin': [], 'member': [], 'owner': 'user', 'query': 'howler.hash:*', ...}
host = 'http://localhost:5000'
view = <View {"view_id": "23MDjUTLnfE2m0U0cBRfDx", "title": "test_membership", "query": "howler.hash:*", "type": "global", "owner": "user", "admin": [], "member": ["huey"], "settings": {"advance_on_triage": false}}>

    def add_permission_every_role(member_to_add: str, member_requesting, create_res, host, view):
        try:
            for membership in view.get_privilege_mapping().keys():
                resp = get_api_data(
                    member_requesting,
                    f"{host}/api/v1/view/{create_res['view_id']}/permission",
                    method="PUT",
                    data=json.dumps(
                        {
                            "user_id": member_to_add,
                            "privilege": membership,
                        }
                    ),
                )
                if membership == "owner":
                    assert resp["owner"] == member_to_add
                    continue
>               assert member_to_add in resp[membership]
                                        ^^^^^^^^^^^^^^^^
E               KeyError: 'administrator'

test/integration/api/test_view.py:205: KeyError
=============================== warnings summary ===============================
.venv/lib/python3.12/site-packages/passlib/utils/__init__.py:854
  /home/runner/work/howler/howler/api/.venv/lib/python3.12/site-packages/passlib/utils/__init__.py:854: DeprecationWarning: 'crypt' is deprecated and slated for removal in Python 3.13
    from crypt import crypt as _crypt

test/integration/api/test_actions.py: 1 warning
test/integration/cronjobs/test_retention.py: 4 warnings
test/integration/cronjobs/test_rules.py: 2 warnings
test/integration/service/test_action_service.py: 3 warnings
test/integration/test_datastore.py: 5 warnings
test/unit/actions/test_add_to_bundle.py: 2 warnings
test/unit/actions/test_init.py: 2 warnings
test/unit/actions/test_remove_from_bundle.py: 1 warning
  /home/runner/work/howler/howler/api/howler/datastore/collection.py:343: GeneralAvailabilityWarning: This API is in technical preview and may be changed or removed in a future release. Elastic will work to fix any issues, but features in technical preview are not subject to the support SLA of official GA features.
    ret_val = func(*args, **kwargs)

test/integration/api/test_doc.py::test_formatting
  /home/runner/work/howler/howler/api/test/integration/api/test_doc.py:50: UserWarning: Endpoint api.v1.action.give_privilege is missing a properly formatted Variables: portion of the docstring!
    warnings.warn(variables_err)

test/integration/api/test_doc.py::test_formatting
  /home/runner/work/howler/howler/api/test/integration/api/test_doc.py:50: UserWarning: Endpoint api.v1.action.revoke_privilege is missing a properly formatted Variables: portion of the docstring!
    warnings.warn(variables_err)

test/integration/api/test_doc.py::test_formatting
  /home/runner/work/howler/howler/api/test/integration/api/test_doc.py:50: UserWarning: Endpoint api.v1.action.get_permission_option is missing a properly formatted Variables: portion of the docstring!
    warnings.warn(variables_err)

-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
==================================== PASSES ====================================
_____________________________ test_get_operations ______________________________
---------------------------- Captured stderr setup -----------------------------
26/06/12 13:43:39 INFO howler.api.odm.random_data | 10 total hits in datastore
------------------------------ Captured log setup ------------------------------
INFO     howler.api.odm.random_data:random_data.py:598 10 total hits in datastore
_________________________ test_valid_action_on_triage __________________________
----------------------------- Captured stderr call -----------------------------
26/06/12 13:44:06 INFO howler.api.services.action_service | Running action 2RCmSM2Tr75JvAKQCSOMdn on bulk query howler.id:(3CVQxFQABiBboV198XcVCX)
26/06/12 13:44:06 INFO howler.api.services.action_service | add_label (success): Label 'demoted' added to category 'generic' for all matching hits.
26/06/12 13:44:06 INFO howler.api.cronjobs.action_queue_worker | Action batch complete: 1 item(s) processed for trigger=demote
26/06/12 13:44:17 INFO howler.api.services.action_service | Running action 64CQxSkMCFjA6IvH3bV5Wj on bulk query howler.id:(3ZWeYOXr6WJRSpLZHrXQsO)
26/06/12 13:44:17 INFO howler.api.services.action_service | add_label (success): Label 'promoted' added to category 'generic' for all matching hits.
26/06/12 13:44:17 INFO howler.api.cronjobs.action_queue_worker | Action batch complete: 1 item(s) processed for trigger=promote
------------------------------ Captured log call -------------------------------
INFO     howler.api.services.action_service:action_service.py:170 Running action 2RCmSM2Tr75JvAKQCSOMdn on bulk query howler.id:(3CVQxFQABiBboV198XcVCX)
INFO     howler.api.services.action_service:action_service.py:200 add_label (success): Label 'demoted' added to category 'generic' for all matching hits.
INFO     howler.api.cronjobs.action_queue_worker:action_queue_worker.py:57 Action batch complete: 1 item(s) processed for trigger=demote
INFO     howler.api.services.action_service:action_service.py:170 Running action 64CQxSkMCFjA6IvH3bV5Wj on bulk query howler.id:(3ZWeYOXr6WJRSpLZHrXQsO)
INFO     howler.api.services.action_service:action_service.py:200 add_label (success): Label 'promoted' added to category 'generic' for all matching hits.
INFO     howler.api.cronjobs.action_queue_worker:action_queue_worker.py:57 Action batch complete: 1 item(s) processed for trigger=promote
_______________________ test_execute_transition_multiple _______________________
----------------------------- Captured stderr call -----------------------------
26/06/12 13:44:42 INFO howler.api.services.action_service | Running action 2RCmSM2Tr75JvAKQCSOMdn on bulk query howler.id:(2U2aSGWJEd93KPYiik4OX4 OR 350HnUIJ8oY1UAnTnmirSb OR 5BtgpBT0SRFkkH84ybuwB7 OR Lb4f33zZJfIu5FTzrMsat)
26/06/12 13:44:43 INFO howler.api.services.action_service | add_label (success): Label 'demoted' added to category 'generic' for all matching hits.
26/06/12 13:44:43 INFO howler.api.cronjobs.action_queue_worker | Action batch complete: 4 item(s) processed for trigger=demote
------------------------------ Captured log call -------------------------------
INFO     howler.api.services.action_service:action_service.py:170 Running action 2RCmSM2Tr75JvAKQCSOMdn on bulk query howler.id:(2U2aSGWJEd93KPYiik4OX4 OR 350HnUIJ8oY1UAnTnmirSb OR 5BtgpBT0SRFkkH84ybuwB7 OR Lb4f33zZJfIu5FTzrMsat)
INFO     howler.api.services.action_service:action_service.py:200 add_label (success): Label 'demoted' added to category 'generic' for all matching hits.
INFO     howler.api.cronjobs.action_queue_worker:action_queue_worker.py:57 Action batch complete: 4 item(s) processed for trigger=demote
______________________________ test_get_analytics ______________________________
---------------------------- Captured stderr setup -----------------------------
26/06/12 13:44:52 WARNING howler.api.odm.random_data | For better test data using sigma rules, execute howler/external/generate_sigma_rules.py.
------------------------------ Captured log setup ------------------------------
WARNING  howler.api.odm.random_data:random_data.py:760 For better test data using sigma rules, execute howler/external/generate_sigma_rules.py.
___________________________ test_bearer_token_direct ___________________________
----------------------------- Captured stderr call -----------------------------
26/06/12 13:44:55 INFO howler.api.cronjobs.action_queue_worker | Action batch complete: 12 item(s) processed for trigger=promote
------------------------------ Captured log call -------------------------------
INFO     howler.api.cronjobs.action_queue_worker:action_queue_worker.py:57 Action batch complete: 12 item(s) processed for trigger=promote
______________________________ test_impersonation ______________________________
----------------------------- Captured stdout call -----------------------------
Non 200 status: {"error": "invalid_grant", "error_description": "Invalid user credentials"}
_________________________ test_create_bundle_from_map __________________________
---------------------------- Captured stderr setup -----------------------------
26/06/12 13:45:07 INFO howler.api.odm.random_data | 20 total hits in datastore
------------------------------ Captured log setup ------------------------------
INFO     howler.api.odm.random_data:random_data.py:598 20 total hits in datastore
____________________________ test_comments_analytic ____________________________
---------------------------- Captured stderr setup -----------------------------
26/06/12 13:45:15 INFO howler.api.odm.random_data | 2 total hits in datastore
------------------------------ Captured log setup ------------------------------
INFO     howler.api.odm.random_data:random_data.py:598 2 total hits in datastore
____________________________ test_create_tools_hits ____________________________
---------------------------- Captured stderr setup -----------------------------
26/06/12 13:45:30 INFO howler.api.odm.random_data | 28 total hits in datastore
------------------------------ Captured log setup ------------------------------
INFO     howler.api.odm.random_data:random_data.py:598 28 total hits in datastore
_____________________ test_create_hits_nonstandard_hashes ______________________
----------------------------- Captured stderr call -----------------------------
26/06/12 13:45:44 INFO howler.api.cronjobs.action_queue_worker | Action batch complete: 2 item(s) processed for trigger=create
------------------------------ Captured log call -------------------------------
INFO     howler.api.cronjobs.action_queue_worker:action_queue_worker.py:57 Action batch complete: 2 item(s) processed for trigger=create
___________________________ test_add_labels_existing ___________________________
----------------------------- Captured stderr call -----------------------------
26/06/12 13:45:57 INFO howler.api.cronjobs.action_queue_worker | Action batch complete: 2 item(s) processed for trigger=create
------------------------------ Captured log call -------------------------------
INFO     howler.api.cronjobs.action_queue_worker:action_queue_worker.py:57 Action batch complete: 2 item(s) processed for trigger=create
___________________________ test_add_labels_missing ____________________________
----------------------------- Captured stderr call -----------------------------
26/06/12 13:46:06 INFO howler.api.cronjobs.action_queue_worker | Action batch complete: 9 item(s) processed for trigger=add_label
------------------------------ Captured log call -------------------------------
INFO     howler.api.cronjobs.action_queue_worker:action_queue_worker.py:57 Action batch complete: 9 item(s) processed for trigger=add_label
__________________________ test_full_transition_flow ___________________________
----------------------------- Captured stderr call -----------------------------
26/06/12 13:46:16 INFO howler.api.cronjobs.action_queue_worker | Action batch complete: 9 item(s) processed for trigger=remove_label
26/06/12 13:46:19 INFO howler.api.cronjobs.action_queue_worker | Action batch complete: 1 item(s) processed for trigger=create
------------------------------ Captured log call -------------------------------
INFO     howler.api.cronjobs.action_queue_worker:action_queue_worker.py:57 Action batch complete: 9 item(s) processed for trigger=remove_label
INFO     howler.api.cronjobs.action_queue_worker:action_queue_worker.py:57 Action batch complete: 1 item(s) processed for trigger=create
_______________________________ test_deep_search _______________________________
---------------------------- Captured stderr setup -----------------------------
26/06/12 13:46:26 INFO howler.api.odm.random_data | 17 total hits in datastore
------------------------------ Captured log setup ------------------------------
INFO     howler.api.odm.random_data:random_data.py:598 17 total hits in datastore
--------------------------- Captured stderr teardown ---------------------------
26/06/12 13:46:30 INFO howler.api.cronjobs.action_queue_worker | Action batch complete: 1 item(s) processed for trigger=demote
---------------------------- Captured log teardown -----------------------------
INFO     howler.api.cronjobs.action_queue_worker:action_queue_worker.py:57 Action batch complete: 1 item(s) processed for trigger=demote
________________________________ test_add_user _________________________________
---------------------------- Captured stderr setup -----------------------------
26/06/12 13:46:32 INFO howler.api.cronjobs.action_queue_worker | Action batch complete: 1 item(s) processed for trigger=create
------------------------------ Captured log setup ------------------------------
INFO     howler.api.cronjobs.action_queue_worker:action_queue_worker.py:57 Action batch complete: 1 item(s) processed for trigger=create
________________________ test_find_analytics_with_hits _________________________
---------------------------- Captured stderr setup -----------------------------
26/06/12 13:46:47 INFO howler.api.odm.random_data | 50 total hits in datastore
------------------------------ Captured log setup ------------------------------
INFO     howler.api.odm.random_data:random_data.py:598 50 total hits in datastore
______________________ test_remove_analytics_without_hits ______________________
---------------------------- Captured stderr setup -----------------------------
26/06/12 13:46:57 INFO howler.api.odm.random_data | 50 total hits in datastore
------------------------------ Captured log setup ------------------------------
INFO     howler.api.odm.random_data:random_data.py:598 50 total hits in datastore
______ test_remove_analytics_without_hits_does_not_remove_rule_analytics _______
---------------------------- Captured stderr setup -----------------------------
26/06/12 13:47:05 INFO howler.api.odm.random_data | 50 total hits in datastore
------------------------------ Captured log setup ------------------------------
INFO     howler.api.odm.random_data:random_data.py:598 50 total hits in datastore
____________________ test_no_hits_find_analytics_with_hits _____________________
---------------------------- Captured stderr setup -----------------------------
26/06/12 13:47:13 INFO howler.api.odm.random_data | 50 total hits in datastore
------------------------------ Captured log setup ------------------------------
INFO     howler.api.odm.random_data:random_data.py:598 50 total hits in datastore
________________ test_only_valid_remove_analytics_without_hits _________________
---------------------------- Captured stderr setup -----------------------------
26/06/12 13:47:22 INFO howler.api.odm.random_data | 50 total hits in datastore
------------------------------ Captured log setup ------------------------------
INFO     howler.api.odm.random_data:random_data.py:598 50 total hits in datastore
__________________ test_no_hits_remove_analytics_without_hits __________________
---------------------------- Captured stderr setup -----------------------------
26/06/12 13:47:30 INFO howler.api.odm.random_data | 50 total hits in datastore
------------------------------ Captured log setup ------------------------------
INFO     howler.api.odm.random_data:random_data.py:598 50 total hits in datastore
_________________ test_too_many_analytics_does_not_run_cleanup _________________
----------------------------- Captured stderr call -----------------------------
26/06/12 13:47:32 WARNING howler.api.cronjobs.retention | Aggregation search for matched analytics did not run or returned no results. There is likely an issue with the query. Skipping cleanup.
------------------------------ Captured log call -------------------------------
WARNING  howler.api.cronjobs.retention:retention.py:62 Aggregation search for matched analytics did not run or returned no results. There is likely an issue with the query. Skipping cleanup.
______________________________ test_registration _______________________________
----------------------------- Captured stderr call -----------------------------
26/06/12 13:47:32 WARNING howler.api.odm.random_data | For better test data using sigma rules, execute howler/external/generate_sigma_rules.py.
26/06/12 13:47:32 INFO howler.api.cronjobs.rules | Skipping registration, running in a test environment
26/06/12 13:47:32 INFO howler.api.cronjobs.rules | Registering new rule: 4xOgfzyH2TrfA9PM95hBhx on interval 0 0 * * *
26/06/12 13:47:32 INFO howler.api.cronjobs.rules | Initialized 1 rules
26/06/12 13:47:32 INFO howler.api.cronjobs.rules | Updating existing rule: 4xOgfzyH2TrfA9PM95hBhx on interval 0 0 * * *
26/06/12 13:47:32 INFO howler.api.cronjobs.rules | Initialized 1 rules
26/06/12 13:47:32 INFO howler.api.cronjobs.rules | Initialized 3 rules
------------------------------ Captured log call -------------------------------
WARNING  howler.api.odm.random_data:random_data.py:760 For better test data using sigma rules, execute howler/external/generate_sigma_rules.py.
INFO     howler.api.cronjobs.rules:rules.py:221 Skipping registration, running in a test environment
INFO     howler.api.cronjobs.rules:rules.py:231 Registering new rule: 4xOgfzyH2TrfA9PM95hBhx on interval 0 0 * * *
INFO     howler.api.cronjobs.rules:rules.py:263 Initialized 1 rules
INFO     howler.api.cronjobs.rules:rules.py:226 Updating existing rule: 4xOgfzyH2TrfA9PM95hBhx on interval 0 0 * * *
INFO     howler.api.cronjobs.rules:rules.py:263 Initialized 1 rules
INFO     howler.api.cronjobs.rules:rules.py:263 Initialized 3 rules
________________________________ test_executor _________________________________
----------------------------- Captured stderr call -----------------------------
26/06/12 13:47:34 INFO howler.api.odm.random_data | 12 total hits in datastore
26/06/12 13:47:34 INFO howler.api.cronjobs.rules | Executing rule development.exe (2484Jba7DnN8UAIVFPysGf)
26/06/12 13:47:34 INFO howler.api.cronjobs.rules | Executing rule market_one.pdf (2LbfuKdnDqrxc515iNV1iV)
26/06/12 13:47:38 INFO howler.api.cronjobs.rules | Executing rule website_are_with.gif (3izK9TQW25y28Hrq8bifS3)
26/06/12 13:47:40 ERROR howler.api.cronjobs.rules | Invalid rule D0puXcy2uzNzHQ8cfoipi! Skipping
------------------------------ Captured log call -------------------------------
INFO     howler.api.odm.random_data:random_data.py:598 12 total hits in datastore
INFO     howler.api.cronjobs.rules:rules.py:118 Executing rule development.exe (2484Jba7DnN8UAIVFPysGf)
INFO     howler.api.cronjobs.rules:rules.py:118 Executing rule market_one.pdf (2LbfuKdnDqrxc515iNV1iV)
INFO     howler.api.cronjobs.rules:rules.py:118 Executing rule website_are_with.gif (3izK9TQW25y28Hrq8bifS3)
ERROR    howler.api.cronjobs.rules:rules.py:115 Invalid rule D0puXcy2uzNzHQ8cfoipi! Skipping
_______________________________ test_validation ________________________________
----------------------------- Captured stderr call -----------------------------
26/06/12 13:47:43 WARNING howler.odm.models.config | Name is missing from configuration
26/06/12 13:47:43 WARNING howler.odm.models.config | Name is missing from configuration
26/06/12 13:47:43 WARNING howler.odm.models.config | Name is missing from configuration
------------------------------ Captured log call -------------------------------
WARNING  howler.odm.models.config:config.py:51 Name is missing from configuration
_______________________________ test_get_plugins _______________________________
----------------------------- Captured stderr call -----------------------------
26/06/12 13:47:43 ERROR howler.api.plugins | Exception when loading plugin no-existy
Traceback (most recent call last):
  File "/home/runner/work/howler/howler/api/howler/plugins/__init__.py", line 20, in get_plugins
    PLUGINS[plugin] = importlib.import_module(f"{plugin}.config").config
                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/hostedtoolcache/Python/3.12.13/x64/lib/python3.12/importlib/__init__.py", line 90, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "<frozen importlib._bootstrap>", line 1387, in _gcd_import
  File "<frozen importlib._bootstrap>", line 1360, in _find_and_load
  File "<frozen importlib._bootstrap>", line 1310, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 488, in _call_with_frames_removed
  File "<frozen importlib._bootstrap>", line 1387, in _gcd_import
  File "<frozen importlib._bootstrap>", line 1360, in _find_and_load
  File "<frozen importlib._bootstrap>", line 1324, in _find_and_load_unlocked
ModuleNotFoundError: No module named 'no-existy'
------------------------------ Captured log call -------------------------------
ERROR    howler.api.plugins:__init__.py:22 Exception when loading plugin no-existy
Traceback (most recent call last):
  File "/home/runner/work/howler/howler/api/howler/plugins/__init__.py", line 20, in get_plugins
    PLUGINS[plugin] = importlib.import_module(f"{plugin}.config").config
                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/hostedtoolcache/Python/3.12.13/x64/lib/python3.12/importlib/__init__.py", line 90, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "<frozen importlib._bootstrap>", line 1387, in _gcd_import
  File "<frozen importlib._bootstrap>", line 1360, in _find_and_load
  File "<frozen importlib._bootstrap>", line 1310, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 488, in _call_with_frames_removed
  File "<frozen importlib._bootstrap>", line 1387, in _gcd_import
  File "<frozen importlib._bootstrap>", line 1360, in _find_and_load
  File "<frozen importlib._bootstrap>", line 1324, in _find_and_load_unlocked
ModuleNotFoundError: No module named 'no-existy'
________________________________ test_odm_mods _________________________________
----------------------------- Captured stderr call -----------------------------
26/06/12 13:47:43 INFO howler.api.datastore | Modifying hit odm with function from plugin test-plugin
26/06/12 13:47:43 INFO howler.api.datastore | Modifying hit odm with function from plugin test-plugin
------------------------------ Captured log call -------------------------------
INFO     howler.api.datastore:howler_store.py:44 Modifying hit odm with function from plugin test-plugin
_______________________________ test_auth_hooks ________________________________
----------------------------- Captured stderr call -----------------------------
26/06/12 13:47:44 INFO howler.api.v1.clue | No custom clue token logic provided, continuing with howler credentials
------------------------------ Captured log call -------------------------------
INFO     howler.api.v1.clue:clue.py:40 No custom clue token logic provided, continuing with howler credentials
_______________________________ test_route_hook ________________________________
----------------------------- Captured stderr call -----------------------------
26/06/12 13:47:44 INFO howler.api.app | Checking plugins for additional routes
26/06/12 13:47:44 INFO howler.api.cronjobs.rules | Skipping registration, running in a test environment
26/06/12 13:47:44 INFO howler.api.app | Checking plugins for additional routes
26/06/12 13:47:44 INFO howler.api.app | Enabling additional endpoint: /api/v1/test
26/06/12 13:47:44 INFO howler.api.cronjobs.rules | Skipping registration, running in a test environment
------------------------------ Captured log call -------------------------------
INFO     howler.api.app:app.py:151 Checking plugins for additional routes
INFO     apscheduler.scheduler:base.py:1090 Added job "execute" to job store "default"
INFO     howler.api.cronjobs.rules:rules.py:221 Skipping registration, running in a test environment
INFO     apscheduler.scheduler:base.py:1090 Added job "execute" to job store "default"
INFO     howler.api.app:app.py:151 Checking plugins for additional routes
INFO     howler.api.app:app.py:157 Enabling additional endpoint: /api/v1/test
INFO     howler.api.cronjobs.rules:rules.py:221 Skipping registration, running in a test environment
________________ TestCreateBundleContract.test_status_code_201 _________________
---------------------------- Captured stderr setup -----------------------------
26/06/12 13:47:46 INFO howler.api.odm.random_data | 5 total hits in datastore
------------------------------ Captured log setup ------------------------------
INFO     howler.api.odm.random_data:random_data.py:598 5 total hits in datastore
_____________________________ test_execute_action ______________________________
----------------------------- Captured stderr call -----------------------------
26/06/12 13:48:58 INFO howler.api.cronjobs.action_queue_worker | Action batch complete: 16 item(s) processed for trigger=create
26/06/12 13:49:04 INFO howler.api.services.action_service | Running action 22LfmVN4Z0kewr9VEQGCNB on bulk query howler.id:(4dDRSwrXhYXPOYK3yD2Gff)
26/06/12 13:49:04 INFO howler.api.services.action_service | add_label (success): Label 'promoted' added to category 'generic' for all matching hits.
26/06/12 13:49:04 INFO howler.api.cronjobs.action_queue_worker | Action batch complete: 1 item(s) processed for trigger=promote
------------------------------ Captured log call -------------------------------
INFO     howler.api.cronjobs.action_queue_worker:action_queue_worker.py:57 Action batch complete: 16 item(s) processed for trigger=create
INFO     howler.api.services.action_service:action_service.py:170 Running action 22LfmVN4Z0kewr9VEQGCNB on bulk query howler.id:(4dDRSwrXhYXPOYK3yD2Gff)
INFO     howler.api.services.action_service:action_service.py:200 add_label (success): Label 'promoted' added to category 'generic' for all matching hits.
INFO     howler.api.cronjobs.action_queue_worker:action_queue_worker.py:57 Action batch complete: 1 item(s) processed for trigger=promote
___________________ test_process_action_batch_create_trigger ___________________
----------------------------- Captured stderr call -----------------------------
26/06/12 13:49:11 INFO howler.api.services.action_service | Running action 7VzadNllOwnHxpVh0tEKfP on bulk query howler.id:(1Sz4qGiu12bn5dIXvpEvNs)
26/06/12 13:49:11 INFO howler.api.services.action_service | add_label (success): Label 'batch_created' added to category 'generic' for all matching hits.
------------------------------ Captured log call -------------------------------
INFO     elastic_transport.transport:_transport.py:349 POST http://localhost:9200/howler-action/_search [status:200 duration:0.002s]
INFO     elastic_transport.transport:_transport.py:349 POST http://localhost:9200/howler-hit/_search [status:200 duration:0.002s]
INFO     howler.api.services.action_service:action_service.py:170 Running action 7VzadNllOwnHxpVh0tEKfP on bulk query howler.id:(1Sz4qGiu12bn5dIXvpEvNs)
INFO     elastic_transport.transport:_transport.py:349 POST http://localhost:9200/howler-hit/_search [status:200 duration:0.002s]
INFO     elastic_transport.transport:_transport.py:349 POST http://localhost:9200/howler-hit/_search [status:200 duration:0.001s]
INFO     elastic_transport.transport:_transport.py:349 POST http://localhost:9200/howler-hit/_update_by_query?wait_for_completion=false [status:200 duration:0.001s]
INFO     elastic_transport.transport:_transport.py:349 GET http://localhost:9200/_tasks/2ORTX8m-TAe8O5AVHQToBA%3A41168?timeout=10s&wait_for_completion=true [status:200 duration:0.004s]
INFO     howler.api.services.action_service:action_service.py:200 add_label (success): Label 'batch_created' added to category 'generic' for all matching hits.
_________________ test_process_action_batch_no_matching_action _________________
------------------------------ Captured log call -------------------------------
DEBUG    urllib3.connectionpool:connectionpool.py:544 http://localhost:9200 "POST /howler-action/_search HTTP/1.1" 200 None
INFO     elastic_transport.transport:_transport.py:349 POST http://localhost:9200/howler-action/_search [status:200 duration:0.002s]
________________ test_process_action_batch_coalesces_duplicates ________________
----------------------------- Captured stderr call -----------------------------
26/06/12 13:49:15 INFO howler.api.cronjobs.action_queue_worker | Action batch complete: 1 item(s) processed for trigger=demote
26/06/12 13:49:16 INFO howler.api.services.action_service | Running action 2h9wVrP7m6oPEpUvGvkCni on bulk query howler.id:(7jvtigELzkyoQ6SbnQS79x OR 7XQTEMVB2O4p3qVyWm8frN)
26/06/12 13:49:16 INFO howler.api.services.action_service | add_label (success): Label 'coalesced' added to category 'generic' for all matching hits.
------------------------------ Captured log call -------------------------------
INFO     howler.api.cronjobs.action_queue_worker:action_queue_worker.py:57 Action batch complete: 1 item(s) processed for trigger=demote
INFO     elastic_transport.transport:_transport.py:349 POST http://localhost:9200/howler-action/_search [status:200 duration:0.002s]
INFO     elastic_transport.transport:_transport.py:349 POST http://localhost:9200/howler-hit/_search [status:200 duration:0.003s]
INFO     howler.api.services.action_service:action_service.py:170 Running action 2h9wVrP7m6oPEpUvGvkCni on bulk query howler.id:(7jvtigELzkyoQ6SbnQS79x OR 7XQTEMVB2O4p3qVyWm8frN)
INFO     elastic_transport.transport:_transport.py:349 POST http://localhost:9200/howler-hit/_search [status:200 duration:0.002s]
INFO     elastic_transport.transport:_transport.py:349 POST http://localhost:9200/howler-hit/_search [status:200 duration:0.002s]
INFO     elastic_transport.transport:_transport.py:349 POST http://localhost:9200/howler-hit/_update_by_query?wait_for_completion=false [status:200 duration:0.002s]
INFO     elastic_transport.transport:_transport.py:349 GET http://localhost:9200/_tasks/2ORTX8m-TAe8O5AVHQToBA%3A41640?timeout=10s&wait_for_completion=true [status:200 duration:0.006s]
INFO     howler.api.services.action_service:action_service.py:200 add_label (success): Label 'coalesced' added to category 'generic' for all matching hits.
______________________________ test_lucene_match _______________________________
----------------------------- Captured stdout call -----------------------------


----------------------------- Captured stderr call -----------------------------
26/06/12 13:49:22 INFO howler.api.integration.service.test_lucene_service | Executing saved test queries
26/06/12 13:49:22 INFO howler.api.integration.service.test_lucene_service | Executing randomly generated test queries
26/06/12 13:49:22 INFO howler.api.integration.service.test_lucene_service | 	Generated query: (howler.status:("open" AND "on-hold") AND email.parent.destination:("151.70.8.225" OR "4.196.185.71") OR process.parent.parent.code_signature.subject_name:("innovation_more_performs.exe" OR "academia.ppt")) AND threat.indicator.file.pe.imphash:("website" OR "cutting")
26/06/12 13:49:23 INFO howler.api.integration.service.test_lucene_service | 	Generated query: cbs.sharepoint.modified.application:("the" OR "marketplace") OR process.user.id:("authority" AND "stays") OR source.original.mac:("e8:c6:f7:f7:aa:5b" OR "d7:81:58:94:c5:8d") OR threat.group.reference:("work" OR "Canada")
26/06/12 13:49:23 INFO howler.api.integration.service.test_lucene_service | 	Generated query: azure.tenant_id:("1O15Gd3oKH4zDMoKjEBC7f" OR "5nchOUdpJWKkF824frP4oj") AND destination.geo.postal_code:("working" OR "engaging") OR event.duration:2482 OR process.uptime:(2646 OR 520102477) OR user_agent.os.name:("innovation_feedback.gif" OR "of.doc")
------------------------------ Captured log call -------------------------------
INFO     howler.api.integration.service.test_lucene_service:test_lucene_service.py:63 Executing saved test queries
INFO     howler.api.integration.service.test_lucene_service:test_lucene_service.py:69 Executing randomly generated test queries
INFO     howler.api.integration.service.test_lucene_service:test_lucene_service.py:76 	Generated query: (howler.status:("open" AND "on-hold") AND email.parent.destination:("151.70.8.225" OR "4.196.185.71") OR process.parent.parent.code_signature.subject_name:("innovation_more_performs.exe" OR "academia.ppt")) AND threat.indicator.file.pe.imphash:("website" OR "cutting")
INFO     howler.api.integration.service.test_lucene_service:test_lucene_service.py:76 	Generated query: cbs.sharepoint.modified.application:("the" OR "marketplace") OR process.user.id:("authority" AND "stays") OR source.original.mac:("e8:c6:f7:f7:aa:5b" OR "d7:81:58:94:c5:8d") OR threat.group.reference:("work" OR "Canada")
INFO     howler.api.integration.service.test_lucene_service:test_lucene_service.py:76 	Generated query: azure.tenant_id:("1O15Gd3oKH4zDMoKjEBC7f" OR "5nchOUdpJWKkF824frP4oj") AND destination.geo.postal_code:("working" OR "engaging") OR event.duration:2482 OR process.uptime:(2646 OR 520102477) OR user_agent.os.name:("innovation_feedback.gif" OR "of.doc")
____________________________ test_es[update_fails] _____________________________
----------------------------- Captured stderr call -----------------------------
26/06/12 13:49:24 WARNING howler.api.datastore | Update - elasticsearch.NotFoundError: document_missing_exception {'error': {'root_cause': [{'type': 'document_missing_exception', 'reason': '[to_update_doesnt_exist]: document missing', 'index_uuid': 'OCHrvLMnTGqT5tdFvJopMw', 'shard': '0', 'index': 'howler-tnbusvixwl_hot'}], 'type': 'document_missing_exception', 'reason': '[to_update_doesnt_exist]: document missing', 'index_uuid': 'OCHrvLMnTGqT5tdFvJopMw', 'shard': '0', 'index': 'howler-tnbusvixwl_hot'}, 'status': 404}
26/06/12 13:49:24 WARNING howler.api.datastore | Update - elasticsearch.NotFoundError: document_missing_exception {'error': {'root_cause': [{'type': 'document_missing_exception', 'reason': '[to_update_doesnt_exist]: document missing', 'index_uuid': 'OCHrvLMnTGqT5tdFvJopMw', 'shard': '0', 'index': 'howler-tnbusvixwl_hot'}], 'type': 'document_missing_exception', 'reason': '[to_update_doesnt_exist]: document missing', 'index_uuid': 'OCHrvLMnTGqT5tdFvJopMw', 'shard': '0', 'index': 'howler-tnbusvixwl_hot'}, 'status': 404}
26/06/12 13:49:24 WARNING howler.api.datastore | Update - elasticsearch.BadRequestError: illegal_argument_exception {'error': {'root_cause': [{'type': 'illegal_argument_exception', 'reason': 'failed to execute script'}], 'type': 'illegal_argument_exception', 'reason': 'failed to execute script', 'caused_by': {'type': 'script_exception', 'reason': 'compile error', 'script_stack': ['ctx._source.RTGE$%^Y#$Gavsdfvbkl = params ...', '                ^---- HERE'], 'script': 'ctx._source.RTGE$%^Y#$Gavsdfvbkl = params.value0', 'lang': 'painless', 'position': {'offset': 16, 'start': 0, 'end': 41}, 'caused_by': {'type': 'illegal_argument_exception', 'reason': "invalid sequence of tokens near ['$'].", 'caused_by': {'type': 'no_viable_alt_exception', 'reason': None}}}}, 'status': 400}
26/06/12 13:49:24 WARNING howler.api.datastore | Update - elasticsearch.BadRequestError: illegal_argument_exception {'error': {'root_cause': [{'type': 'illegal_argument_exception', 'reason': 'failed to execute script'}], 'type': 'illegal_argument_exception', 'reason': 'failed to execute script', 'caused_by': {'type': 'script_exception', 'reason': 'compile error', 'script_stack': ['ctx._source.RTGE$%^Y#$Gavsdfvbkl = params ...', '                ^---- HERE'], 'script': 'ctx._source.RTGE$%^Y#$Gavsdfvbkl = params.value0', 'lang': 'painless', 'position': {'offset': 16, 'start': 0, 'end': 41}, 'caused_by': {'type': 'illegal_argument_exception', 'reason': "invalid sequence of tokens near ['$'].", 'caused_by': {'type': 'no_viable_alt_exception', 'reason': None}}}}, 'status': 400}
26/06/12 13:49:24 WARNING howler.api.datastore | Update - elasticsearch.ConflictError: ConflictError(409, 'version_conflict_engine_exception', '[to_update]: version conflict, required seqNo [36], primary term [2]. current document has seqNo [36] and primary term [1]')
26/06/12 13:49:24 WARNING howler.api.datastore | Update - elasticsearch.ConflictError: ConflictError(409, 'version_conflict_engine_exception', '[to_update]: version conflict, required seqNo [36], primary term [2]. current document has seqNo [36] and primary term [1]')
------------------------------ Captured log call -------------------------------
WARNING  howler.api.datastore:collection.py:1496 Update - elasticsearch.NotFoundError: document_missing_exception {'error': {'root_cause': [{'type': 'document_missing_exception', 'reason': '[to_update_doesnt_exist]: document missing', 'index_uuid': 'OCHrvLMnTGqT5tdFvJopMw', 'shard': '0', 'index': 'howler-tnbusvixwl_hot'}], 'type': 'document_missing_exception', 'reason': '[to_update_doesnt_exist]: document missing', 'index_uuid': 'OCHrvLMnTGqT5tdFvJopMw', 'shard': '0', 'index': 'howler-tnbusvixwl_hot'}, 'status': 404}
WARNING  howler.api.datastore:collection.py:1498 Update - elasticsearch.BadRequestError: illegal_argument_exception {'error': {'root_cause': [{'type': 'illegal_argument_exception', 'reason': 'failed to execute script'}], 'type': 'illegal_argument_exception', 'reason': 'failed to execute script', 'caused_by': {'type': 'script_exception', 'reason': 'compile error', 'script_stack': ['ctx._source.RTGE$%^Y#$Gavsdfvbkl = params ...', '                ^---- HERE'], 'script': 'ctx._source.RTGE$%^Y#$Gavsdfvbkl = params.value0', 'lang': 'painless', 'position': {'offset': 16, 'start': 0, 'end': 41}, 'caused_by': {'type': 'illegal_argument_exception', 'reason': "invalid sequence of tokens near ['$'].", 'caused_by': {'type': 'no_viable_alt_exception', 'reason': None}}}}, 'status': 400}
WARNING  howler.api.datastore:collection.py:1501 Update - elasticsearch.ConflictError: ConflictError(409, 'version_conflict_engine_exception', '[to_update]: version conflict, required seqNo [36], primary term [2]. current document has seqNo [36] and primary term [1]')
___________________________ test_fix_shards[shrink] ____________________________
----------------------------- Captured stderr call -----------------------------
26/06/12 13:49:24 INFO howler.api.datastore | Current shards (2) is bigger then target shards (1), we will be shrinking the index.
26/06/12 13:49:24 INFO howler.api.datastore | Current shards (2) is bigger then target shards (1), we will be shrinking the index.
26/06/12 13:49:24 INFO howler.api.datastore | Waiting for HOWLER-TEST_FIX_SHARDS_15F80D97 status to be GREEN.
26/06/12 13:49:24 INFO howler.api.datastore | Waiting for HOWLER-TEST_FIX_SHARDS_15F80D97 status to be GREEN.
26/06/12 13:49:24 INFO howler.api.datastore | Set a datastore wide write block on Elastic.
26/06/12 13:49:24 INFO howler.api.datastore | Set a datastore wide write block on Elastic.
26/06/12 13:49:24 INFO howler.api.datastore | Relocating index to node 502458C973F8.
26/06/12 13:49:24 INFO howler.api.datastore | Relocating index to node 502458C973F8.
26/06/12 13:49:24 INFO howler.api.datastore | Cloning HOWLER-TEST_FIX_SHARDS_15F80D97_HOT into HOWLER-TEST_FIX_SHARDS_15F80D97__FIX_SHARDS.
26/06/12 13:49:24 INFO howler.api.datastore | Cloning HOWLER-TEST_FIX_SHARDS_15F80D97_HOT into HOWLER-TEST_FIX_SHARDS_15F80D97__FIX_SHARDS.
26/06/12 13:49:25 INFO howler.api.datastore | Waiting for HOWLER-TEST_FIX_SHARDS_15F80D97__FIX_SHARDS status to be GREEN.
26/06/12 13:49:25 INFO howler.api.datastore | Waiting for HOWLER-TEST_FIX_SHARDS_15F80D97__FIX_SHARDS status to be GREEN.
26/06/12 13:49:25 INFO howler.api.datastore | Make HOWLER-TEST_FIX_SHARDS_15F80D97__FIX_SHARDS the current alias for HOWLER-TEST_FIX_SHARDS_15F80D97 and delete HOWLER-TEST_FIX_SHARDS_15F80D97_HOT.
26/06/12 13:49:25 INFO howler.api.datastore | Make HOWLER-TEST_FIX_SHARDS_15F80D97__FIX_SHARDS the current alias for HOWLER-TEST_FIX_SHARDS_15F80D97 and delete HOWLER-TEST_FIX_SHARDS_15F80D97_HOT.
26/06/12 13:49:25 INFO howler.api.datastore | Perform shard fix operation from HOWLER-TEST_FIX_SHARDS_15F80D97__FIX_SHARDS to HOWLER-TEST_FIX_SHARDS_15F80D97_HOT.
26/06/12 13:49:25 INFO howler.api.datastore | Perform shard fix operation from HOWLER-TEST_FIX_SHARDS_15F80D97__FIX_SHARDS to HOWLER-TEST_FIX_SHARDS_15F80D97_HOT.
26/06/12 13:49:25 INFO howler.api.datastore | Make HOWLER-TEST_FIX_SHARDS_15F80D97_HOT the current alias for HOWLER-TEST_FIX_SHARDS_15F80D97 and delete HOWLER-TEST_FIX_SHARDS_15F80D97__FIX_SHARDS.
26/06/12 13:49:25 INFO howler.api.datastore | Make HOWLER-TEST_FIX_SHARDS_15F80D97_HOT the current alias for HOWLER-TEST_FIX_SHARDS_15F80D97 and delete HOWLER-TEST_FIX_SHARDS_15F80D97__FIX_SHARDS.
------------------------------ Captured log call -------------------------------
INFO     howler.api.datastore:collection.py:585 Current shards (2) is bigger then target shards (1), we will be shrinking the index.
INFO     howler.api.datastore:collection.py:621 Waiting for HOWLER-TEST_FIX_SHARDS_15F80D97 status to be GREEN.
INFO     howler.api.datastore:collection.py:625 Set a datastore wide write block on Elastic.
INFO     howler.api.datastore:collection.py:632 Relocating index to node 502458C973F8.
INFO     howler.api.datastore:collection.py:644 Cloning HOWLER-TEST_FIX_SHARDS_15F80D97_HOT into HOWLER-TEST_FIX_SHARDS_15F80D97__FIX_SHARDS.
INFO     howler.api.datastore:collection.py:654 Waiting for HOWLER-TEST_FIX_SHARDS_15F80D97__FIX_SHARDS status to be GREEN.
INFO     howler.api.datastore:collection.py:659 Make HOWLER-TEST_FIX_SHARDS_15F80D97__FIX_SHARDS the current alias for HOWLER-TEST_FIX_SHARDS_15F80D97 and delete HOWLER-TEST_FIX_SHARDS_15F80D97_HOT.
INFO     howler.api.datastore:collection.py:678 Perform shard fix operation from HOWLER-TEST_FIX_SHARDS_15F80D97__FIX_SHARDS to HOWLER-TEST_FIX_SHARDS_15F80D97_HOT.
INFO     howler.api.datastore:collection.py:682 Make HOWLER-TEST_FIX_SHARDS_15F80D97_HOT the current alias for HOWLER-TEST_FIX_SHARDS_15F80D97 and delete HOWLER-TEST_FIX_SHARDS_15F80D97__FIX_SHARDS.
____________________________ test_fix_shards[match] ____________________________
----------------------------- Captured stderr call -----------------------------
26/06/12 13:49:25 INFO howler.api.datastore | Current shards (2) is equal to the target shards (2), only housekeeping operations will be performed.
26/06/12 13:49:25 INFO howler.api.datastore | Current shards (2) is equal to the target shards (2), only housekeeping operations will be performed.
------------------------------ Captured log call -------------------------------
INFO     howler.api.datastore:collection.py:612 Current shards (2) is equal to the target shards (2), only housekeeping operations will be performed.
____________________________ test_fix_shards[grow] _____________________________
----------------------------- Captured stderr call -----------------------------
26/06/12 13:49:25 INFO howler.api.datastore | Current shards (2) is smaller then target shards (4), we will be splitting the index.
26/06/12 13:49:25 INFO howler.api.datastore | Current shards (2) is smaller then target shards (4), we will be splitting the index.
26/06/12 13:49:25 INFO howler.api.datastore | Waiting for HOWLER-TEST_FIX_SHARDS_50962ACB status to be GREEN.
26/06/12 13:49:25 INFO howler.api.datastore | Waiting for HOWLER-TEST_FIX_SHARDS_50962ACB status to be GREEN.
26/06/12 13:49:25 INFO howler.api.datastore | Set a datastore wide write block on Elastic.
26/06/12 13:49:25 INFO howler.api.datastore | Set a datastore wide write block on Elastic.
26/06/12 13:49:25 INFO howler.api.datastore | Cloning HOWLER-TEST_FIX_SHARDS_50962ACB_HOT into HOWLER-TEST_FIX_SHARDS_50962ACB__FIX_SHARDS.
26/06/12 13:49:25 INFO howler.api.datastore | Cloning HOWLER-TEST_FIX_SHARDS_50962ACB_HOT into HOWLER-TEST_FIX_SHARDS_50962ACB__FIX_SHARDS.
26/06/12 13:49:25 INFO howler.api.datastore | Waiting for HOWLER-TEST_FIX_SHARDS_50962ACB__FIX_SHARDS status to be GREEN.
26/06/12 13:49:25 INFO howler.api.datastore | Waiting for HOWLER-TEST_FIX_SHARDS_50962ACB__FIX_SHARDS status to be GREEN.
26/06/12 13:49:25 INFO howler.api.datastore | Make HOWLER-TEST_FIX_SHARDS_50962ACB__FIX_SHARDS the current alias for HOWLER-TEST_FIX_SHARDS_50962ACB and delete HOWLER-TEST_FIX_SHARDS_50962ACB_HOT.
26/06/12 13:49:25 INFO howler.api.datastore | Make HOWLER-TEST_FIX_SHARDS_50962ACB__FIX_SHARDS the current alias for HOWLER-TEST_FIX_SHARDS_50962ACB and delete HOWLER-TEST_FIX_SHARDS_50962ACB_HOT.
26/06/12 13:49:25 INFO howler.api.datastore | Perform shard fix operation from HOWLER-TEST_FIX_SHARDS_50962ACB__FIX_SHARDS to HOWLER-TEST_FIX_SHARDS_50962ACB_HOT.
26/06/12 13:49:25 INFO howler.api.datastore | Perform shard fix operation from HOWLER-TEST_FIX_SHARDS_50962ACB__FIX_SHARDS to HOWLER-TEST_FIX_SHARDS_50962ACB_HOT.
26/06/12 13:49:25 INFO howler.api.datastore | Make HOWLER-TEST_FIX_SHARDS_50962ACB_HOT the current alias for HOWLER-TEST_FIX_SHARDS_50962ACB and delete HOWLER-TEST_FIX_SHARDS_50962ACB__FIX_SHARDS.
26/06/12 13:49:25 INFO howler.api.datastore | Make HOWLER-TEST_FIX_SHARDS_50962ACB_HOT the current alias for HOWLER-TEST_FIX_SHARDS_50962ACB and delete HOWLER-TEST_FIX_SHARDS_50962ACB__FIX_SHARDS.
------------------------------ Captured log call -------------------------------
INFO     howler.api.datastore:collection.py:601 Current shards (2) is smaller then target shards (4), we will be splitting the index.
INFO     howler.api.datastore:collection.py:621 Waiting for HOWLER-TEST_FIX_SHARDS_50962ACB status to be GREEN.
INFO     howler.api.datastore:collection.py:625 Set a datastore wide write block on Elastic.
INFO     howler.api.datastore:collection.py:644 Cloning HOWLER-TEST_FIX_SHARDS_50962ACB_HOT into HOWLER-TEST_FIX_SHARDS_50962ACB__FIX_SHARDS.
INFO     howler.api.datastore:collection.py:654 Waiting for HOWLER-TEST_FIX_SHARDS_50962ACB__FIX_SHARDS status to be GREEN.
INFO     howler.api.datastore:collection.py:659 Make HOWLER-TEST_FIX_SHARDS_50962ACB__FIX_SHARDS the current alias for HOWLER-TEST_FIX_SHARDS_50962ACB and delete HOWLER-TEST_FIX_SHARDS_50962ACB_HOT.
INFO     howler.api.datastore:collection.py:678 Perform shard fix operation from HOWLER-TEST_FIX_SHARDS_50962ACB__FIX_SHARDS to HOWLER-TEST_FIX_SHARDS_50962ACB_HOT.
INFO     howler.api.datastore:collection.py:682 Make HOWLER-TEST_FIX_SHARDS_50962ACB_HOT the current alias for HOWLER-TEST_FIX_SHARDS_50962ACB and delete HOWLER-TEST_FIX_SHARDS_50962ACB__FIX_SHARDS.
_____________________________ test_reindex_success _____________________________
----------------------------- Captured stderr call -----------------------------
26/06/12 13:49:26 WARNING howler.api.datastore | Beginning Reindex
26/06/12 13:49:26 WARNING howler.api.datastore | Beginning Reindex
26/06/12 13:49:26 WARNING howler.api.datastore | Block writes to source index howler-test_reindex_success_9ce34197_hot
26/06/12 13:49:26 WARNING howler.api.datastore | Block writes to source index howler-test_reindex_success_9ce34197_hot
26/06/12 13:49:26 WARNING howler.api.datastore | Source index howler-test_reindex_success_9ce34197_hot contains 2 document(s)
26/06/12 13:49:26 WARNING howler.api.datastore | Source index howler-test_reindex_success_9ce34197_hot contains 2 document(s)
26/06/12 13:49:26 WARNING howler.api.datastore | Creating new index with name howler-test_reindex_success_9ce34197_hot__reindex
26/06/12 13:49:26 WARNING howler.api.datastore | Creating new index with name howler-test_reindex_success_9ce34197_hot__reindex
26/06/12 13:49:26 WARNING howler.api.datastore | Beginning reindex from howler-test_reindex_success_9ce34197_hot to howler-test_reindex_success_9ce34197_hot__reindex
26/06/12 13:49:26 WARNING howler.api.datastore | Beginning reindex from howler-test_reindex_success_9ce34197_hot to howler-test_reindex_success_9ce34197_hot__reindex
26/06/12 13:49:26 WARNING howler.api.datastore | Reindex taskId: 2ORTX8m-TAe8O5AVHQToBA:43407
26/06/12 13:49:26 WARNING howler.api.datastore | Reindex taskId: 2ORTX8m-TAe8O5AVHQToBA:43407
26/06/12 13:49:26 WARNING howler.api.datastore | Committing reindexed data in index howler-test_reindex_success_9ce34197_hot__reindex
26/06/12 13:49:26 WARNING howler.api.datastore | Committing reindexed data in index howler-test_reindex_success_9ce34197_hot__reindex
26/06/12 13:49:26 WARNING howler.api.datastore | Document count validated: 2 document(s) in both howler-test_reindex_success_9ce34197_hot and howler-test_reindex_success_9ce34197_hot__reindex
26/06/12 13:49:26 WARNING howler.api.datastore | Document count validated: 2 document(s) in both howler-test_reindex_success_9ce34197_hot and howler-test_reindex_success_9ce34197_hot__reindex
26/06/12 13:49:26 WARNING howler.api.datastore | Deleting old index howler-test_reindex_success_9ce34197_hot
26/06/12 13:49:26 WARNING howler.api.datastore | Deleting old index howler-test_reindex_success_9ce34197_hot
26/06/12 13:49:26 WARNING howler.api.datastore | Block writes to reindex target howler-test_reindex_success_9ce34197_hot__reindex
26/06/12 13:49:26 WARNING howler.api.datastore | Block writes to reindex target howler-test_reindex_success_9ce34197_hot__reindex
26/06/12 13:49:26 WARNING howler.api.datastore | Renaming reindexed index from howler-test_reindex_success_9ce34197_hot__reindex to howler-test_reindex_success_9ce34197_hot
26/06/12 13:49:26 WARNING howler.api.datastore | Renaming reindexed index from howler-test_reindex_success_9ce34197_hot__reindex to howler-test_reindex_success_9ce34197_hot
26/06/12 13:49:26 WARNING howler.api.datastore | Unblock writes to the index
26/06/12 13:49:26 WARNING howler.api.datastore | Unblock writes to the index
------------------------------ Captured log call -------------------------------
WARNING  howler.api.datastore:collection.py:737 Beginning Reindex
WARNING  howler.api.datastore:collection.py:768 Block writes to source index howler-test_reindex_success_9ce34197_hot
WARNING  howler.api.datastore:collection.py:778 Source index howler-test_reindex_success_9ce34197_hot contains 2 document(s)
WARNING  howler.api.datastore:collection.py:781 Creating new index with name howler-test_reindex_success_9ce34197_hot__reindex
WARNING  howler.api.datastore:collection.py:790 Beginning reindex from howler-test_reindex_success_9ce34197_hot to howler-test_reindex_success_9ce34197_hot__reindex
WARNING  howler.api.datastore:collection.py:797 Reindex taskId: 2ORTX8m-TAe8O5AVHQToBA:43407
WARNING  howler.api.datastore:collection.py:804 Committing reindexed data in index howler-test_reindex_success_9ce34197_hot__reindex
WARNING  howler.api.datastore:collection.py:922 Document count validated: 2 document(s) in both howler-test_reindex_success_9ce34197_hot and howler-test_reindex_success_9ce34197_hot__reindex
WARNING  howler.api.datastore:collection.py:813 Deleting old index howler-test_reindex_success_9ce34197_hot
WARNING  howler.api.datastore:collection.py:817 Block writes to reindex target howler-test_reindex_success_9ce34197_hot__reindex
WARNING  howler.api.datastore:collection.py:824 Renaming reindexed index from howler-test_reindex_success_9ce34197_hot__reindex to howler-test_reindex_success_9ce34197_hot
WARNING  howler.api.datastore:collection.py:850 Unblock writes to the index
_________________ test_reindex_refuses_existing_reindex_index __________________
----------------------------- Captured stderr call -----------------------------
26/06/12 13:49:26 WARNING howler.api.datastore | Beginning Reindex
26/06/12 13:49:26 WARNING howler.api.datastore | Beginning Reindex
------------------------------ Captured log call -------------------------------
WARNING  howler.api.datastore:collection.py:737 Beginning Reindex
_____________________ test_reindex_failure_preserves_data ______________________
----------------------------- Captured stderr call -----------------------------
26/06/12 13:49:27 WARNING howler.api.datastore | Beginning Reindex
26/06/12 13:49:27 WARNING howler.api.datastore | Beginning Reindex
26/06/12 13:49:27 WARNING howler.api.datastore | Block writes to source index howler-test_reindex_fail_1dd6c9ba_hot
26/06/12 13:49:27 WARNING howler.api.datastore | Block writes to source index howler-test_reindex_fail_1dd6c9ba_hot
26/06/12 13:49:27 WARNING howler.api.datastore | Source index howler-test_reindex_fail_1dd6c9ba_hot contains 2 document(s)
26/06/12 13:49:27 WARNING howler.api.datastore | Source index howler-test_reindex_fail_1dd6c9ba_hot contains 2 document(s)
26/06/12 13:49:27 WARNING howler.api.datastore | Creating new index with name howler-test_reindex_fail_1dd6c9ba_hot__reindex
26/06/12 13:49:27 WARNING howler.api.datastore | Creating new index with name howler-test_reindex_fail_1dd6c9ba_hot__reindex
26/06/12 13:49:27 WARNING howler.api.datastore | Beginning reindex from howler-test_reindex_fail_1dd6c9ba_hot to howler-test_reindex_fail_1dd6c9ba_hot__reindex
26/06/12 13:49:27 WARNING howler.api.datastore | Beginning reindex from howler-test_reindex_fail_1dd6c9ba_hot to howler-test_reindex_fail_1dd6c9ba_hot__reindex
26/06/12 13:49:27 WARNING howler.api.datastore | Reindex taskId: 2ORTX8m-TAe8O5AVHQToBA:43761
26/06/12 13:49:27 WARNING howler.api.datastore | Reindex taskId: 2ORTX8m-TAe8O5AVHQToBA:43761
26/06/12 13:49:27 ERROR howler.api.datastore | Reindex failure on document example: document_parsing_exception - [1:57] failed to parse field [field_3] of type [integer] in document with id 'example'. Preview of field's value: 'not-an-int'
26/06/12 13:49:27 ERROR howler.api.datastore | Reindex failure on document example: document_parsing_exception - [1:57] failed to parse field [field_3] of type [integer] in document with id 'example'. Preview of field's value: 'not-an-int'
26/06/12 13:49:27 WARNING howler.api.datastore | Unblock writes to source index howler-test_reindex_fail_1dd6c9ba_hot after failed reindex
26/06/12 13:49:27 WARNING howler.api.datastore | Unblock writes to source index howler-test_reindex_fail_1dd6c9ba_hot after failed reindex
26/06/12 13:49:27 WARNING howler.api.datastore | Beginning reindex cleanup
26/06/12 13:49:27 WARNING howler.api.datastore | Beginning reindex cleanup
26/06/12 13:49:27 WARNING howler.api.datastore | Restoring aliases for howler-test_reindex_fail_1dd6c9ba_hot and removing leftover index howler-test_reindex_fail_1dd6c9ba_hot__reindex
26/06/12 13:49:27 WARNING howler.api.datastore | Restoring aliases for howler-test_reindex_fail_1dd6c9ba_hot and removing leftover index howler-test_reindex_fail_1dd6c9ba_hot__reindex
26/06/12 13:49:27 WARNING howler.api.datastore | Restoring aliases to howler-test_reindex_fail_1dd6c9ba_hot
26/06/12 13:49:27 WARNING howler.api.datastore | Restoring aliases to howler-test_reindex_fail_1dd6c9ba_hot
26/06/12 13:49:27 WARNING howler.api.datastore | Deleting leftover reindex index howler-test_reindex_fail_1dd6c9ba_hot__reindex
26/06/12 13:49:27 WARNING howler.api.datastore | Deleting leftover reindex index howler-test_reindex_fail_1dd6c9ba_hot__reindex
26/06/12 13:49:27 WARNING howler.api.datastore | Unblock write to the index
26/06/12 13:49:27 WARNING howler.api.datastore | Unblock write to the index
------------------------------ Captured log call -------------------------------
WARNING  howler.api.datastore:collection.py:737 Beginning Reindex
WARNING  howler.api.datastore:collection.py:768 Block writes to source index howler-test_reindex_fail_1dd6c9ba_hot
WARNING  howler.api.datastore:collection.py:778 Source index howler-test_reindex_fail_1dd6c9ba_hot contains 2 document(s)
WARNING  howler.api.datastore:collection.py:781 Creating new index with name howler-test_reindex_fail_1dd6c9ba_hot__reindex
WARNING  howler.api.datastore:collection.py:790 Beginning reindex from howler-test_reindex_fail_1dd6c9ba_hot to howler-test_reindex_fail_1dd6c9ba_hot__reindex
WARNING  howler.api.datastore:collection.py:797 Reindex taskId: 2ORTX8m-TAe8O5AVHQToBA:43761
ERROR    howler.api.datastore:collection.py:891 Reindex failure on document example: document_parsing_exception - [1:57] failed to parse field [field_3] of type [integer] in document with id 'example'. Preview of field's value: 'not-an-int'
WARNING  howler.api.datastore:collection.py:858 Unblock writes to source index howler-test_reindex_fail_1dd6c9ba_hot after failed reindex
WARNING  howler.api.datastore:collection.py:953 Beginning reindex cleanup
WARNING  howler.api.datastore:collection.py:968 Restoring aliases for howler-test_reindex_fail_1dd6c9ba_hot and removing leftover index howler-test_reindex_fail_1dd6c9ba_hot__reindex
WARNING  howler.api.datastore:collection.py:994 Restoring aliases to howler-test_reindex_fail_1dd6c9ba_hot
WARNING  howler.api.datastore:collection.py:997 Deleting leftover reindex index howler-test_reindex_fail_1dd6c9ba_hot__reindex
WARNING  howler.api.datastore:collection.py:1000 Unblock write to the index
_______________ test_reindex_cleanup_errors_when_source_missing ________________
----------------------------- Captured stderr call -----------------------------
26/06/12 13:49:27 WARNING howler.api.datastore | Beginning reindex cleanup
26/06/12 13:49:27 WARNING howler.api.datastore | Beginning reindex cleanup
------------------------------ Captured log call -------------------------------
WARNING  howler.api.datastore:collection.py:953 Beginning reindex cleanup
_________________________ test_reindex_allow_failures __________________________
----------------------------- Captured stderr call -----------------------------
26/06/12 13:49:27 WARNING howler.api.datastore | Beginning Reindex
26/06/12 13:49:27 WARNING howler.api.datastore | Beginning Reindex
26/06/12 13:49:27 WARNING howler.api.datastore | Block writes to source index howler-test_reindex_allow_e2d8eb64_hot
26/06/12 13:49:27 WARNING howler.api.datastore | Block writes to source index howler-test_reindex_allow_e2d8eb64_hot
26/06/12 13:49:27 WARNING howler.api.datastore | Source index howler-test_reindex_allow_e2d8eb64_hot contains 2 document(s)
26/06/12 13:49:27 WARNING howler.api.datastore | Source index howler-test_reindex_allow_e2d8eb64_hot contains 2 document(s)
26/06/12 13:49:27 WARNING howler.api.datastore | Creating new index with name howler-test_reindex_allow_e2d8eb64_hot__reindex
26/06/12 13:49:27 WARNING howler.api.datastore | Creating new index with name howler-test_reindex_allow_e2d8eb64_hot__reindex
26/06/12 13:49:27 WARNING howler.api.datastore | Beginning reindex from howler-test_reindex_allow_e2d8eb64_hot to howler-test_reindex_allow_e2d8eb64_hot__reindex
26/06/12 13:49:27 WARNING howler.api.datastore | Beginning reindex from howler-test_reindex_allow_e2d8eb64_hot to howler-test_reindex_allow_e2d8eb64_hot__reindex
26/06/12 13:49:27 WARNING howler.api.datastore | Reindex taskId: 2ORTX8m-TAe8O5AVHQToBA:44057
26/06/12 13:49:27 WARNING howler.api.datastore | Reindex taskId: 2ORTX8m-TAe8O5AVHQToBA:44057
26/06/12 13:49:27 ERROR howler.api.datastore | Reindex failure on document example: document_parsing_exception - [1:57] failed to parse field [field_3] of type [integer] in document with id 'example'. Preview of field's value: 'not-an-int'
26/06/12 13:49:27 ERROR howler.api.datastore | Reindex failure on document example: document_parsing_exception - [1:57] failed to parse field [field_3] of type [integer] in document with id 'example'. Preview of field's value: 'not-an-int'
26/06/12 13:49:27 WARNING howler.api.datastore | Reindex of howler-test_reindex_allow_e2d8eb64_hot into howler-test_reindex_allow_e2d8eb64_hot__reindex reported 1 document failure(s) and 0 version conflict(s). Proceeding anyway because allow_failures is set (DESTRUCTIVE).
26/06/12 13:49:27 WARNING howler.api.datastore | Reindex of howler-test_reindex_allow_e2d8eb64_hot into howler-test_reindex_allow_e2d8eb64_hot__reindex reported 1 document failure(s) and 0 version conflict(s). Proceeding anyway because allow_failures is set (DESTRUCTIVE).
26/06/12 13:49:27 WARNING howler.api.datastore | Committing reindexed data in index howler-test_reindex_allow_e2d8eb64_hot__reindex
26/06/12 13:49:27 WARNING howler.api.datastore | Committing reindexed data in index howler-test_reindex_allow_e2d8eb64_hot__reindex
26/06/12 13:49:27 WARNING howler.api.datastore | Document count mismatch reindexing howler-test_reindex_allow_e2d8eb64_hot into howler-test_reindex_allow_e2d8eb64_hot__reindex: source has 2 document(s) but reindex target has 1. Proceeding anyway because allow_failures is set (DESTRUCTIVE).
26/06/12 13:49:27 WARNING howler.api.datastore | Document count mismatch reindexing howler-test_reindex_allow_e2d8eb64_hot into howler-test_reindex_allow_e2d8eb64_hot__reindex: source has 2 document(s) but reindex target has 1. Proceeding anyway because allow_failures is set (DESTRUCTIVE).
26/06/12 13:49:27 WARNING howler.api.datastore | Deleting old index howler-test_reindex_allow_e2d8eb64_hot
26/06/12 13:49:27 WARNING howler.api.datastore | Deleting old index howler-test_reindex_allow_e2d8eb64_hot
26/06/12 13:49:27 WARNING howler.api.datastore | Block writes to reindex target howler-test_reindex_allow_e2d8eb64_hot__reindex
26/06/12 13:49:27 WARNING howler.api.datastore | Block writes to reindex target howler-test_reindex_allow_e2d8eb64_hot__reindex
26/06/12 13:49:27 WARNING howler.api.datastore | Renaming reindexed index from howler-test_reindex_allow_e2d8eb64_hot__reindex to howler-test_reindex_allow_e2d8eb64_hot
26/06/12 13:49:27 WARNING howler.api.datastore | Renaming reindexed index from howler-test_reindex_allow_e2d8eb64_hot__reindex to howler-test_reindex_allow_e2d8eb64_hot
26/06/12 13:49:27 WARNING howler.api.datastore | Unblock writes to the index
26/06/12 13:49:27 WARNING howler.api.datastore | Unblock writes to the index
------------------------------ Captured log call -------------------------------
WARNING  howler.api.datastore:collection.py:737 Beginning Reindex
WARNING  howler.api.datastore:collection.py:768 Block writes to source index howler-test_reindex_allow_e2d8eb64_hot
WARNING  howler.api.datastore:collection.py:778 Source index howler-test_reindex_allow_e2d8eb64_hot contains 2 document(s)
WARNING  howler.api.datastore:collection.py:781 Creating new index with name howler-test_reindex_allow_e2d8eb64_hot__reindex
WARNING  howler.api.datastore:collection.py:790 Beginning reindex from howler-test_reindex_allow_e2d8eb64_hot to howler-test_reindex_allow_e2d8eb64_hot__reindex
WARNING  howler.api.datastore:collection.py:797 Reindex taskId: 2ORTX8m-TAe8O5AVHQToBA:44057
ERROR    howler.api.datastore:collection.py:891 Reindex failure on document example: document_parsing_exception - [1:57] failed to parse field [field_3] of type [integer] in document with id 'example'. Preview of field's value: 'not-an-int'
WARNING  howler.api.datastore:collection.py:901 Reindex of howler-test_reindex_allow_e2d8eb64_hot into howler-test_reindex_allow_e2d8eb64_hot__reindex reported 1 document failure(s) and 0 version conflict(s). Proceeding anyway because allow_failures is set (DESTRUCTIVE).
WARNING  howler.api.datastore:collection.py:804 Committing reindexed data in index howler-test_reindex_allow_e2d8eb64_hot__reindex
WARNING  howler.api.datastore:collection.py:931 Document count mismatch reindexing howler-test_reindex_allow_e2d8eb64_hot into howler-test_reindex_allow_e2d8eb64_hot__reindex: source has 2 document(s) but reindex target has 1. Proceeding anyway because allow_failures is set (DESTRUCTIVE).
WARNING  howler.api.datastore:collection.py:813 Deleting old index howler-test_reindex_allow_e2d8eb64_hot
WARNING  howler.api.datastore:collection.py:817 Block writes to reindex target howler-test_reindex_allow_e2d8eb64_hot__reindex
WARNING  howler.api.datastore:collection.py:824 Renaming reindexed index from howler-test_reindex_allow_e2d8eb64_hot__reindex to howler-test_reindex_allow_e2d8eb64_hot
WARNING  howler.api.datastore:collection.py:850 Unblock writes to the index
___________________________ test_get_namespace_field ___________________________
---------------------------- Captured stderr setup -----------------------------
26/06/12 13:49:30 INFO howler.api.odm.random_data | 14 total hits in datastore
26/06/12 13:49:32 INFO howler.api.odm.random_data | 24 total hits in datastore
------------------------------ Captured log setup ------------------------------
INFO     howler.api.odm.random_data:random_data.py:598 14 total hits in datastore
INFO     howler.api.odm.random_data:random_data.py:598 24 total hits in datastore
_______________ test_get_apps_list_calls_discovery_when_enabled ________________
----------------------------- Captured stderr call -----------------------------
26/06/12 13:49:54 INFO howler.api.helper.discover | Skipping discovery, running in a test environment
------------------------------ Captured log call -------------------------------
INFO     howler.api.helper.discover:discover.py:27 Skipping discovery, running in a test environment
________ TestEnqueueActionExecution.test_enqueue_fallback_on_push_error ________
----------------------------- Captured stderr call -----------------------------
26/06/12 13:49:55 ERROR howler.api.services.action_service | Failed to enqueue action execution, falling back to direct execution
Traceback (most recent call last):
  File "/home/runner/work/howler/howler/api/howler/services/action_service.py", line 68, in enqueue_action_execution
    _get_action_queue(trigger).push(
  File "/opt/hostedtoolcache/Python/3.12.13/x64/lib/python3.12/unittest/mock.py", line 1139, in __call__
    return self._mock_call(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/hostedtoolcache/Python/3.12.13/x64/lib/python3.12/unittest/mock.py", line 1143, in _mock_call
    return self._execute_mock_call(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/hostedtoolcache/Python/3.12.13/x64/lib/python3.12/unittest/mock.py", line 1198, in _execute_mock_call
    raise effect
ConnectionError: Redis down
------------------------------ Captured log call -------------------------------
ERROR    howler.api.services.action_service:action_service.py:75 Failed to enqueue action execution, falling back to direct execution
Traceback (most recent call last):
  File "/home/runner/work/howler/howler/api/howler/services/action_service.py", line 68, in enqueue_action_execution
    _get_action_queue(trigger).push(
  File "/opt/hostedtoolcache/Python/3.12.13/x64/lib/python3.12/unittest/mock.py", line 1139, in __call__
    return self._mock_call(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/hostedtoolcache/Python/3.12.13/x64/lib/python3.12/unittest/mock.py", line 1143, in _mock_call
    return self._execute_mock_call(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/hostedtoolcache/Python/3.12.13/x64/lib/python3.12/unittest/mock.py", line 1198, in _execute_mock_call
    raise effect
ConnectionError: Redis down
_____ TestProcessActionBatch.test_process_batch_bulk_error_does_not_crash ______
----------------------------- Captured stderr call -----------------------------
26/06/12 13:49:55 ERROR howler.api.services.action_service | Error processing action batch for trigger=create user=admin
Traceback (most recent call last):
  File "/home/runner/work/howler/howler/api/howler/services/action_service.py", line 110, in process_action_batch
    bulk_execute_on_query(query, trigger=trigger, user=user_data)
  File "/opt/hostedtoolcache/Python/3.12.13/x64/lib/python3.12/unittest/mock.py", line 1139, in __call__
    return self._mock_call(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/hostedtoolcache/Python/3.12.13/x64/lib/python3.12/unittest/mock.py", line 1143, in _mock_call
    return self._execute_mock_call(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/hostedtoolcache/Python/3.12.13/x64/lib/python3.12/unittest/mock.py", line 1198, in _execute_mock_call
    raise effect
Exception: ES is down
------------------------------ Captured log call -------------------------------
ERROR    howler.api.services.action_service:action_service.py:112 Error processing action batch for trigger=create user=admin
Traceback (most recent call last):
  File "/home/runner/work/howler/howler/api/howler/services/action_service.py", line 110, in process_action_batch
    bulk_execute_on_query(query, trigger=trigger, user=user_data)
  File "/opt/hostedtoolcache/Python/3.12.13/x64/lib/python3.12/unittest/mock.py", line 1139, in __call__
    return self._mock_call(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/hostedtoolcache/Python/3.12.13/x64/lib/python3.12/unittest/mock.py", line 1143, in _mock_call
    return self._execute_mock_call(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/hostedtoolcache/Python/3.12.13/x64/lib/python3.12/unittest/mock.py", line 1198, in _execute_mock_call
    raise effect
Exception: ES is down
____________________________ test_get_nbgallery_nb _____________________________
----------------------------- Captured stderr call -----------------------------
26/06/12 13:49:55 INFO howler.api.services.notebook_service | Plugin %s does not modify the notebook access token.
26/06/12 13:49:55 INFO howler.api.services.notebook_service | No custom notebook token logic provided, continuing with howler credentials
------------------------------ Captured log call -------------------------------
INFO     howler.api.services.notebook_service:notebook_service.py:25 Plugin %s does not modify the notebook access token.
INFO     howler.api.services.notebook_service:notebook_service.py:30 No custom notebook token logic provided, continuing with howler credentials
_ TestParseUserDataAccessControl.test_changed_user_triggers_add_access_control_and_save _
----------------------------- Captured stderr call -----------------------------
26/06/12 13:49:55 INFO howler.api.services.user_service | Creating new user alice
------------------------------ Captured log call -------------------------------
INFO     howler.api.services.user_service:user_service.py:202 Creating new user alice
_ TestParseUserDataAccessControl.test_unchanged_user_without_access_control_triggers_save _
----------------------------- Captured stderr call -----------------------------
26/06/12 13:49:55 INFO howler.api.services.user_service | Adding access control for user alice
------------------------------ Captured log call -------------------------------
INFO     howler.api.services.user_service:user_service.py:211 Adding access control for user alice
___________________________ test_decode_invalid_json ___________________________
----------------------------- Captured stderr call -----------------------------
26/06/12 13:49:56 WARNING howler.queue | Invalid data on queue: not-valid-json
------------------------------ Captured log call -------------------------------
WARNING  howler.queue:__init__.py:156 Invalid data on queue: not-valid-json
_________________ test_retry_call_retries_on_connection_error __________________
----------------------------- Captured stderr call -----------------------------
26/06/12 13:49:56 WARNING howler.queue | No connection to Redis, reconnecting... [conn error]
26/06/12 13:49:56 WARNING howler.queue | No connection to Redis, reconnecting... [conn error]
26/06/12 13:49:56 INFO howler.queue | Reconnected to Redis!
------------------------------ Captured log call -------------------------------
WARNING  howler.queue:__init__.py:63 No connection to Redis, reconnecting... [conn error]
WARNING  howler.queue:__init__.py:63 No connection to Redis, reconnecting... [conn error]
INFO     howler.queue:__init__.py:59 Reconnected to Redis!
_________________ test_retry_call_retries_on_connection_reset __________________
----------------------------- Captured stderr call -----------------------------
26/06/12 13:49:56 WARNING howler.queue | No connection to Redis, reconnecting... []
26/06/12 13:49:56 INFO howler.queue | Reconnected to Redis!
------------------------------ Captured log call -------------------------------
WARNING  howler.queue:__init__.py:63 No connection to Redis, reconnecting... []
INFO     howler.queue:__init__.py:59 Reconnected to Redis!

---------

Signed-off-by: agauvin <alexandre.gauvin@cse-cst.gc.ca>

Related Issue: multi_owner-view-dossier-actions
Signed-off-by: agauvin <alexandre.gauvin@cse-cst.gc.ca>

Related Issue: multi_owner-view-dossier-actions
@alexandre-gauvin alexandre-gauvin marked this pull request as ready for review May 14, 2026 13:35
@alexandre-gauvin alexandre-gauvin changed the title feat: SPBK-4047 feat: support membership and multiple ownership for views, dossiers, and actions May 14, 2026

@cccs-mdr cccs-mdr left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assuming the UI side will come in a separate PR?

Comment thread api/howler/odm/models/action.py Outdated
Comment thread api/howler/odm/models/dossier.py Outdated
Comment thread api/howler/odm/models/dossier.py Outdated
Comment thread api/howler/odm/models/view.py Outdated

from opentelemetry import trace
from passlib.hash import bcrypt
from passlib.handlers.bcrypt import bcrypt

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should not be changed - I've resolve this in another branch, merging develop should resolve the original error.

Comment thread api/howler/api/v1/dossier.py Outdated
Comment thread api/howler/api/v1/dossier.py Outdated
Comment thread api/howler/api/v1/action.py Outdated
Comment thread api/howler/api/v1/action.py Outdated
Comment thread api/howler/api/v1/auth.py
from authlib.integrations.base_client import OAuthError
from flask import current_app, request
from passlib.hash import bcrypt
from passlib.handlers.bcrypt import bcrypt

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same deal - merge develop and this change should not be necessary

@alexandre-gauvin alexandre-gauvin marked this pull request as draft May 20, 2026 13:06
alexandre-gauvin and others added 8 commits May 21, 2026 13:57
Signed-off-by: agauvin <alexandre.gauvin@cse-cst.gc.ca>

Related Issue: multi_owner-view-dossier-actions
Signed-off-by: agauvin <alexandre.gauvin@cse-cst.gc.ca>

Related Issue: multi_owner-view-dossier-actions
Signed-off-by: agauvin <alexandre.gauvin@cse-cst.gc.ca>

Related Issue: multi_owner-view-dossier-actions
Signed-off-by: agauvin <alexandre.gauvin@cse-cst.gc.ca>

Related Issue: multi_owner-view-dossier-actions
Signed-off-by: agauvin <alexandre.gauvin@cse-cst.gc.ca>

Related Issue: multi_owner-view-dossier-actions
Related Issue: multi_owner-view-dossier-actions
Copilot AI changed the title feat: support membership and multiple ownership for views, dossiers, and actions Store view owner as a string Jun 10, 2026
@alexandre-gauvin alexandre-gauvin changed the title Store view owner as a string feat: ownership on view dossier and action Jun 10, 2026
…dossier.d

Signed-off-by: agauvin <alexandre.gauvin@cse-cst.gc.ca>

Related Issue: multi_owner-view-dossier-actions
Signed-off-by: agauvin <alexandre.gauvin@cse-cst.gc.ca>

Related Issue: multi_owner-view-dossier-actions
…object as well

Signed-off-by: agauvin <alexandre.gauvin@cse-cst.gc.ca>

Related Issue: multi_owner-view-dossier-actions
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants