Skip to content

Commit

Permalink
add api testing independent of alfalfa-client and fix some bugs revea…
Browse files Browse the repository at this point in the history
…led by tests
  • Loading branch information
TShapinsky committed Dec 28, 2023
1 parent 8796ba7 commit 7b77a08
Show file tree
Hide file tree
Showing 8 changed files with 597 additions and 9 deletions.
15 changes: 11 additions & 4 deletions alfalfa_web/server/api-v2.js
Original file line number Diff line number Diff line change
Expand Up @@ -593,7 +593,14 @@ router.get("/runs/:runId/points/:pointId", (req, res, next) => {
* description: The point was successfully updated
*/
router.put("/runs/:runId/points/:pointId", (req, res, next) => {
// TODO Confirm that point isn't an OUTPUT type
const { value } = req.body;

if (req.point.point_type == "OUTPUT") {
return res
.status(400)
.json({ message: `Point '${req.point.ref_id}' is of type '${req.point.point_type}' and cannot be written to` });
}

if (value !== null) {
const error = validate(
{ value },
Expand Down Expand Up @@ -687,7 +694,7 @@ router.delete("/runs/:runId", (req, res, next) => {
* schema:
* $ref: '#/components/schemas/Error'
*/
router.post("/runs/:runId/start", async (req, res, next) => {
router.post("/runs/:runId/start", (req, res, next) => {
const { body } = req;

const timeValidator = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/;
Expand Down Expand Up @@ -777,7 +784,7 @@ router.post("/runs/:runId/advance", (req, res, next) => {
*/
router.post("/runs/:runId/stop", (req, res, next) => {
// If the run is already stopping or stopped there is no need to send message
if (["STOPPING", "STOPPED", "COMPLETE"].includes(req.run.status)) {
if (["STOPPING", "COMPLETE", "ERROR", "READY"].includes(req.run.status)) {
res.sendStatus(204);
}
api
Expand Down Expand Up @@ -1220,7 +1227,7 @@ router.get("/simulations", async (req, res, next) => {
.catch(next);
});

router.get("*", (req, res) => res.status(404).json({ message: "Page not found" }));
router.all("*", (req, res) => res.status(404).json({ message: "Page not found" }));

router.use(errorHandler);

Expand Down
9 changes: 5 additions & 4 deletions alfalfa_web/server/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ class AlfalfaAPI {
getPointsById = async (run, pointIds) => {
return Promise.all(
pointIds.map((pointId) => {
this.getPointById(run, pointId);
return this.getPointById(run, pointId);
})
);
};
Expand Down Expand Up @@ -173,7 +173,7 @@ class AlfalfaAPI {

removeRun = async (run) => {
// Delete run
const { deletedCount } = await this.run.deleteOne({ _id: run._id });
const { deletedCount } = await this.runs.deleteOne({ _id: run._id });

if (deletedCount == 1) {
// Delete points
Expand Down Expand Up @@ -258,7 +258,8 @@ class AlfalfaAPI {
};

listModels = async () => {
return this.getModels().map(this.formatModel);
const models = await this.getModels();
return models.map(this.formatModel);
};

getModels = async () => {
Expand All @@ -285,7 +286,7 @@ class AlfalfaAPI {
new GetObjectCommand({
Bucket: process.env.S3_BUCKET,
Key: `uploads/${model.ref_id}/${model.model_name}`,
ResponseContentDisposition: `attachment; filename="${run.ref_id}.tar.gz"`
ResponseContentDisposition: `attachment; filename="${model.ref_id}.tar.gz"`
}),
{
expiresIn: 86400
Expand Down
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ extras = True
addopts =
--cov alfalfa_worker --cov-report term-missing
--verbose
-m "not integration and not fmu and not docker and not scale"
-m "not integration and not fmu and not docker and not scale and not api"
markers =
integration: marks tests as integration tests (deselect with '-m "not integration"')
fmu: mark tests that require fmu support, e.g., pyfmi (deselect with '-m "not fmu"')
Expand Down
1 change: 1 addition & 0 deletions tests/api/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""This module includes tests for the mechanical aspects of the api, but not necessarily all of the functionality."""
37 changes: 37 additions & 0 deletions tests/api/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import os
from pathlib import Path

import pytest
from alfalfa_client import AlfalfaClient
from requests import HTTPError


@pytest.fixture
def base_url():
return 'http://localhost/api/v2'


@pytest.fixture
def alfalfa_client():
return AlfalfaClient()


@pytest.fixture
def model_path():
return Path(os.path.dirname(__file__)) / "models" / "small_office"


@pytest.fixture
def model_id(alfalfa_client: AlfalfaClient, model_path):
return alfalfa_client.upload_model(model_path)


@pytest.fixture
def run_id(alfalfa_client: AlfalfaClient, model_path):
run_id = alfalfa_client.submit(model_path)
yield run_id
try:
if alfalfa_client.status(run_id) not in ["COMPLETE", "ERROR", "STOPPING", "READY"]:
alfalfa_client.stop(run_id)
except HTTPError:
pass
122 changes: 122 additions & 0 deletions tests/api/test_model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
from collections import OrderedDict
from io import StringIO
from uuid import uuid4

import pytest
import requests
from requests_toolbelt import MultipartEncoder


@pytest.mark.api
def test_model_upload_download(base_url):

# Send request with empty data expecting a 400 error
response = requests.post(f"{base_url}/models/upload")

assert response.status_code == 400
response_body = response.json()
assert "message" in response_body

# Send request with proper data expecting a 200
model_name = "test_model.zip"
request_body = {"modelName": model_name}
response = requests.post(f"{base_url}/models/upload", json=request_body)

assert response.status_code == 200
response_body = response.json()
assert "payload" in response_body
payload = response_body["payload"]
assert "url" in payload
assert "modelId" in payload
model_id = payload["modelId"]

# upload a model to the s3 bucket with the pre-signed request
form_data = OrderedDict(payload["fields"])
file_contents = "This is a test string to be used as a file stand in"
form_data['file'] = ('filename', StringIO(file_contents))

encoder = MultipartEncoder(fields=form_data)
response = requests.post(payload['url'], data=encoder, headers={'Content-Type': encoder.content_type})
assert response.status_code == 204

# download model and check it is the same
response = requests.get(f"{base_url}/models/{model_id}/download")
assert response.status_code == 200

contents = b""
for chunk in response.iter_content():
contents += chunk
assert contents.decode('utf-8') == file_contents, "Downloaded model does not match uploaded model"


@pytest.mark.api
def test_model_retrieval(base_url):
# create a model
model_name = "test_model.zip"
request_body = {"modelName": model_name}
response = requests.post(f"{base_url}/models/upload", json=request_body)
response_body = response.json()

model_id = response_body["payload"]["modelId"]
response_body["payload"]["url"]

# request all models
response = requests.get(f"{base_url}/models")

assert response.status_code == 200
response_body = response.json()
assert "payload" in response_body
payload = response_body["payload"]
assert len(payload) > 0, "No models in model list"

contains_model = False
for model in payload:
if model["id"] == model_id:
assert model["modelName"] == model_name
contains_model = True

assert contains_model, "Could not find uploaded model in list"

# request only uploaded model
response = requests.get(f"{base_url}/models/{model_id}")

assert response.status_code == 200
response_body = response.json()
assert "payload" in response_body
payload = response_body["payload"]
assert payload["id"] == model_id
assert payload["modelName"] == model_name
assert "created" in payload
assert "modified" in payload

# request with invalid model id
model_id = "not_a_model_id"
response = requests.get(f"{base_url}/models/{model_id}")

assert response.status_code == 400
response_body = response.json()
assert "message" in response_body


@pytest.mark.api
def test_model_not_found(base_url):
# request non-existent model
response = requests.get(f"{base_url}/models/{uuid4()}")

assert response.status_code == 404
response_body = response.json()
assert "message" in response_body

# download non-existent model
response = requests.get(f"{base_url}/models/{uuid4()}/download")

assert response.status_code == 404
response_body = response.json()
assert "message" in response_body

# create run from model which does not exist
response = requests.post(f"{base_url}/models/{uuid4()}/createRun")

assert response.status_code == 404
response_body = response.json()
assert "message" in response_body
Loading

0 comments on commit 7b77a08

Please sign in to comment.