blob: 378429f3e86f068bc03191fdb57854bd7f15124a [file]
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
#
import unittest
from unittest.mock import patch
from openserverless import app
from openserverless.error.api_error import AuthorizationError
AUTH_HEADER = {"Authorization": "Basic dXNlcjpwYXNz"}
def make_user_data(login="myuser", env=None, userenv=None):
return {
"login": login,
"env": env if env is not None else [{"key": "APIHOST", "value": "https://ops.example.test"}],
"userenv": userenv if userenv is not None else [],
}
class BuildRouteTestCase(unittest.TestCase):
"""
Base test case that mocks the two external collaborators used by every
route in openserverless/rest/build.py: OpenwhiskAuthorize (CouchDB backed
login) and BuildService (Kubernetes backed build orchestration).
"""
def setUp(self):
self.client = app.test_client()
auth_patcher = patch("openserverless.rest.build.OpenwhiskAuthorize")
self.mock_authorize_class = auth_patcher.start()
self.addCleanup(auth_patcher.stop)
self.mock_authorize = self.mock_authorize_class.return_value
build_service_patcher = patch("openserverless.rest.build.BuildService")
self.mock_build_service_class = build_service_patcher.start()
self.addCleanup(build_service_patcher.stop)
self.mock_build_service = self.mock_build_service_class.return_value
self.mock_build_service.id = "550e8400-e29b-41d4-a716-446655440000"
self.mock_build_service.job_name = "build-myuser-abc123"
def authorized_as(self, login="myuser", env=None, userenv=None):
self.mock_authorize.login.return_value = make_user_data(
login=login, env=env, userenv=userenv
)
def unauthorized(self):
self.mock_authorize.login.side_effect = AuthorizationError("invalid")
class AuthorizationSharedBehaviorTest(BuildRouteTestCase):
"""authorize() is shared by every build route; exercised via build/status."""
def test_missing_authorization_header_returns_401(self):
response = self.client.get("/system/api/v1/build/status?id=abc")
self.assertEqual(401, response.status_code)
self.assertEqual("ko", response.json["status"])
self.assertEqual("Missing authorization header", response.json["message"])
self.mock_authorize.login.assert_not_called()
def test_invalid_authorization_returns_401(self):
self.unauthorized()
response = self.client.get(
"/system/api/v1/build/status?id=abc", headers=AUTH_HEADER
)
self.assertEqual(401, response.status_code)
self.assertEqual("Invalid authorization", response.json["message"])
class BuildStartTest(BuildRouteTestCase):
def post(self, json_body=None, headers=AUTH_HEADER):
if json_body is None:
return self.client.post(
"/system/api/v1/build/start",
data="null",
content_type="application/json",
headers=headers,
)
return self.client.post(
"/system/api/v1/build/start", json=json_body, headers=headers
)
def test_missing_auth_header_returns_401(self):
response = self.post({"source": "s", "target": "myuser:tag", "kind": "python"})
self.assertEqual(401, response.status_code)
self.mock_build_service_class.assert_not_called()
def test_empty_user_environment_returns_401(self):
self.authorized_as(env=[], userenv=[])
response = self.post({"source": "s", "target": "myuser:tag", "kind": "python"})
self.assertEqual(401, response.status_code)
self.assertEqual("User environment not found", response.json["message"])
def test_no_json_payload_returns_400(self):
self.authorized_as()
response = self.post(json_body=None)
self.assertEqual(400, response.status_code)
self.assertEqual("No JSON payload provided for build.", response.json["message"])
def test_missing_source_returns_400(self):
self.authorized_as()
response = self.post({"target": "myuser:tag", "kind": "python"})
self.assertEqual(400, response.status_code)
self.assertEqual("No source provided for build.", response.json["message"])
def test_missing_target_returns_400(self):
self.authorized_as()
response = self.post({"source": "s", "kind": "python"})
self.assertEqual(400, response.status_code)
self.assertEqual("No target provided for build.", response.json["message"])
def test_missing_kind_returns_400(self):
self.authorized_as()
response = self.post({"source": "s", "target": "myuser:tag"})
self.assertEqual(400, response.status_code)
self.assertEqual("No kind provided for build.", response.json["message"])
def test_target_user_mismatch_returns_400_when_strict_check_enabled(self):
self.authorized_as(login="myuser")
with patch.dict("os.environ", {"STRICT_USER_CHECK": "true"}):
response = self.post(
{"source": "s", "target": "otheruser:tag", "kind": "python"}
)
self.assertEqual(400, response.status_code)
self.assertEqual("Invalid target for the build.", response.json["message"])
self.mock_build_service_class.assert_not_called()
def test_target_user_mismatch_allowed_when_strict_check_disabled(self):
self.authorized_as(login="myuser")
self.mock_build_service.build.return_value = (True, self.mock_build_service.job_name)
with patch.dict("os.environ", {"STRICT_USER_CHECK": "false"}):
response = self.post(
{"source": "s", "target": "otheruser:tag", "kind": "python"}
)
self.assertEqual(200, response.status_code)
def test_successful_build_returns_200_with_job_details(self):
self.authorized_as(login="myuser")
self.mock_build_service.build.return_value = (True, self.mock_build_service.job_name)
response = self.post({"source": "s", "target": "myuser:tag", "kind": "python"})
self.assertEqual(200, response.status_code)
self.assertEqual("ok", response.json["status"])
self.assertIn(self.mock_build_service.job_name, response.json["message"])
self.assertEqual(self.mock_build_service.id, response.json["id"])
self.assertEqual(self.mock_build_service.job_name, response.json["job_name"])
self.mock_build_service.init.assert_called_once_with(
build_config={"source": "s", "target": "myuser:tag", "kind": "python"}
)
self.mock_build_service.build.assert_called_once_with("myuser:tag")
def test_build_failure_returns_500_with_service_message(self):
self.authorized_as(login="myuser")
self.mock_build_service.build.return_value = (False, "buildkit exploded")
response = self.post({"source": "s", "target": "myuser:tag", "kind": "python"})
self.assertEqual(500, response.status_code)
self.assertEqual("buildkit exploded", response.json["message"])
def test_build_failure_without_message_uses_default(self):
self.authorized_as(login="myuser")
self.mock_build_service.build.return_value = (False, None)
response = self.post({"source": "s", "target": "myuser:tag", "kind": "python"})
self.assertEqual(500, response.status_code)
self.assertEqual("Build process failed.", response.json["message"])
class BuildStatusTest(BuildRouteTestCase):
def get(self, query_string="", headers=AUTH_HEADER):
url = "/system/api/v1/build/status"
if query_string:
url = f"{url}?{query_string}"
return self.client.get(url, headers=headers)
def test_empty_user_environment_returns_401(self):
self.authorized_as(env=[])
response = self.get("id=abc")
self.assertEqual(401, response.status_code)
self.assertEqual("User environment not found", response.json["message"])
def test_missing_id_returns_400(self):
self.authorized_as()
response = self.get()
self.assertEqual(400, response.status_code)
self.assertEqual("No id provided for build status.", response.json["message"])
def test_job_not_found_returns_404_with_service_message(self):
self.authorized_as()
self.mock_build_service.get_job_status.return_value = (False, "Build job not found")
response = self.get("id=missing-id")
self.assertEqual(404, response.status_code)
self.assertEqual("Build job not found", response.json["message"])
def test_job_not_found_without_message_uses_default(self):
self.authorized_as()
self.mock_build_service.get_job_status.return_value = (False, None)
response = self.get("id=missing-id")
self.assertEqual(404, response.status_code)
self.assertEqual("Build job not found.", response.json["message"])
def test_successful_status_returns_200_with_job_data(self):
self.authorized_as()
status_result = {
"id": "abc",
"job_name": "build-myuser-abc",
"phase": "Succeeded",
"active": 0,
"succeeded": 1,
"failed": 0,
"startTime": "2026-08-15T00:00:00Z",
"completionTime": "2026-08-15T00:05:00Z",
}
self.mock_build_service.get_job_status.return_value = (True, status_result)
response = self.get("id=abc")
self.assertEqual(200, response.status_code)
self.assertEqual("Succeeded", response.json["phase"])
self.assertEqual("build-myuser-abc", response.json["job_name"])
self.mock_build_service.get_job_status.assert_called_once_with("abc")
class BuildCleanupTest(BuildRouteTestCase):
def post(self, json_body=None, headers=AUTH_HEADER):
if json_body is None:
return self.client.post(
"/system/api/v1/build/cleanup",
data="null",
content_type="application/json",
headers=headers,
)
return self.client.post(
"/system/api/v1/build/cleanup", json=json_body, headers=headers
)
def test_empty_user_environment_returns_401(self):
self.authorized_as(env=[])
response = self.post({})
self.assertEqual(401, response.status_code)
self.assertEqual("User environment not found", response.json["message"])
def test_no_json_payload_returns_400(self):
self.authorized_as()
response = self.post(json_body=None)
self.assertEqual(400, response.status_code)
self.assertEqual(
"No JSON payload provided for cleanup.", response.json["message"]
)
def test_cleanup_failure_returns_500(self):
self.authorized_as()
self.mock_build_service.delete_old_build_jobs.return_value = -1
response = self.post({})
self.assertEqual(500, response.status_code)
self.assertEqual(
"Failed to clean up old build jobs.", response.json["message"]
)
def test_cleanup_uses_default_max_age_hours(self):
self.authorized_as()
self.mock_build_service.delete_old_build_jobs.return_value = 3
response = self.post({})
self.assertEqual(200, response.status_code)
self.assertEqual("Cleaned up 3 jobs successfully.", response.json["message"])
self.mock_build_service.delete_old_build_jobs.assert_called_once_with(
max_age_hours=24
)
def test_cleanup_uses_provided_max_age_hours(self):
self.authorized_as()
self.mock_build_service.delete_old_build_jobs.return_value = 0
response = self.post({"max_age_hours": 2})
self.assertEqual(200, response.status_code)
self.mock_build_service.delete_old_build_jobs.assert_called_once_with(
max_age_hours=2
)
if __name__ == "__main__":
unittest.main()