Skip to content

Commit

Permalink
Merge pull request #81 from sunflowerbloom/feature/ECKIT-619
Browse files Browse the repository at this point in the history
ECKIT-619 Enable PUT method on EasyCURL
  • Loading branch information
danovaro authored Nov 18, 2023
2 parents 95662f3 + 9fc8e83 commit 0f21b65
Show file tree
Hide file tree
Showing 7 changed files with 402 additions and 17 deletions.
29 changes: 23 additions & 6 deletions src/eckit/io/EasyCURL.cc
Original file line number Diff line number Diff line change
Expand Up @@ -933,14 +933,31 @@ EasyCURLResponse EasyCURL::POST(const std::string& url, const std::string& data)
return request(url);
}

namespace {

/**
* Copies the content from input `userdata` into output `buffer`
*
* Returns the number of characters copied
*/
size_t readCallback(char* buffer, size_t size [[maybe_unused]], size_t nitems [[maybe_unused]], void* userdata) {
auto data = static_cast<std::string*>(userdata);

data->copy(buffer, data->size());
return data->size();
}

} // namespace

EasyCURLResponse EasyCURL::PUT(const std::string& url, const std::string& data) {
NOTIMP;
// Disable unreachable code
#if 0
_(curl_easy_setopt(ch_->curl_, CURLOPT_CUSTOMREQUEST, NULL));
_(curl_easy_setopt(ch_->curl_, CURLOPT_PUT, 1L));
_(curl_easy_setopt(ch_->curl_, CURLOPT_UPLOAD, 1L));

// Setup callback to read PUT request body (i.e. copy the given 'data' into an internal 'buffer')
_(curl_easy_setopt(ch_->curl_, CURLOPT_READFUNCTION, readCallback));
_(curl_easy_setopt(ch_->curl_, CURLOPT_READDATA, &data));
_(curl_easy_setopt(ch_->curl_, CURLOPT_INFILESIZE, data.size()));

return request(url);
#endif
}

EasyCURLResponse EasyCURL::DELETE(const std::string& url) {
Expand Down
10 changes: 10 additions & 0 deletions tests/io/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,16 @@ ecbuild_add_test( TARGET eckit_test_filepool
SOURCES test_filepool.cc
LIBS eckit )

ecbuild_add_test( TARGET eckit_test_easycurl
SOURCES test_easycurl.cc
CONDITION HAVE_EXTRA_TESTS AND eckit_HAVE_CURL
LIBS eckit )

file(
COPY mock/MockREST.py mock/MockREST.launcher.sh
DESTINATION ${CMAKE_CURRENT_BINARY_DIR}
)

ecbuild_add_test( TARGET eckit_test_urlhandle
SOURCES test_urlhandle.cc
CONDITION HAVE_EXTRA_TESTS AND eckit_HAVE_CURL
Expand Down
8 changes: 8 additions & 0 deletions tests/io/mock/MockREST.launcher.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#!/usr/bin/env bash

set -ex

BASE_DIR="$( cd "$( dirname "$0" )" >/dev/null 2>&1 && pwd )"

export FLASK_APP=${BASE_DIR}/MockREST.py
flask run -h localhost -p 49111
148 changes: 148 additions & 0 deletions tests/io/mock/MockREST.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
#!/usr/bin/env python3

import os
import signal
from flask import Flask, request, jsonify

"""
MockREST provides a simple REST API over HTTP to support tests.
The API requires a Python environment with `flask` installed to execute -- see requirements.txt.
The API consists of the following end-points:
* /ping [GET]
- always replies "Hi!" (200)
* /insufficient-permissions [GET]
- always replies "Forbidden" (403)
* /shutdown [GET]
- shuts down the running server
* /blobs [GET]
- replies with the list of currently available blobs (in JSON)
* /blob/<key> [GET]
- replies with the blob associated with <key> (in JSON) (200)
- if <key> not found, replies with "Not found" (404)
* /blob [POST]
- creates a new blob, based on payload '{"key": "<key>", "value": "<value>"}'
- replies with "Request must be JSON" (415) is payload is not JSON
- replies with "Bad Request" (400) if payload doesn't include "key" or "value"
- replies with "Bad Request" (400) if a blob is already associated with <key>
- replies with "Created" (201), in case of success
* /blob [PUT]
- updates an existing blob, based on payload '{"key": "<key>", "value": "<value>"}'
- replies with "Request must be JSON" (415) is payload is not JSON
- replies with "Bad Request" (400) if payload doesn't include "key" or "value"
- replies with "Bad Request" (400) if no blob is associated with <key>
- replies with "OK" (200), in case of success
* /blob/<key> [DELETE]
- deletes an existing blob, associated with <key>
- if <key> not found, replies with "Not found" (404)
- replies with "" (204), in case of success
* /blob/<key>/content [GET]
- replies with the blob value (in Plain Text) (200)
- if <key> not found, replies with "Not found" (404)
"""

app = Flask(__name__)


blobs = {}


def _blob_has_all_of(blob, fields):
for field in fields:
if field not in blob:
return False, field
return True, None


@app.get("/ping")
def ping():
return "Hi!", 200


@app.get("/insufficient-permissions")
def no_permissions():
return "Forbidden", 403


@app.get("/shutdown")
def shutdown():
# terminate own running process!...
os.kill(os.getpid(), signal.SIGINT)
return jsonify(blobs)


@app.get("/blobs")
def get_blobs():
return jsonify(blobs)


@app.get("/blob/<key>")
def get_blob(key):
if key not in blobs:
return {"error": f"Not Found: Unable to find blob with key '{key}'"}, 404
blob = {"key": key, "value": blobs[key]}
return jsonify(blob)


@app.get("/blob/<key>/content")
def get_blob_content(key):
if key not in blobs:
return {"error": f"Not Found: Unable to find blob with key '{key}'"}, 404
return blobs[key]


@app.post("/blob")
def create_blob():
if not request.is_json:
return {"error": "Request must be JSON"}, 415

blob = request.get_json()

found, field = _blob_has_all_of(blob, ["key", "value"])
if not found:
return {"error": f"Bad Request: New blob must contain field '{field}'"}, 400

key = blob["key"]
if key in blobs:
return {"error": f"Bad Request: Key {blob['key']} already exists"}, 400

value = blob["value"]
blobs[key] = value
return f"Created: {blob}", 201


@app.put("/blob")
def update_blob():
if not request.is_json:
return {"error": "Request must be JSON"}, 415

blob = request.get_json()

found, field = _blob_has_all_of(blob, ["key", "value"])
if not found:
return {"error": f"Bad Request: Update blob must contain field '{field}'"}, 400

key = blob["key"]
if key not in blobs:
return {"error": f"Bad Request: Key {blob['key']} does not exist"}, 400

value = blob["value"]
blobs[key] = value
return f"OK: {blob}", 200


@app.delete("/blob/<key>")
def delete_blob(key):
if key not in blobs:
return {"error": f"Not Found (Unable to find blob with key '{key}')"}, 404
del blobs[key]
return "", 204
1 change: 1 addition & 0 deletions tests/io/mock/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Flask==3.0.0
Loading

0 comments on commit 0f21b65

Please sign in to comment.