package org.swisspush.reststorage; import io.vertx.ext.unit.Async; import io.vertx.ext.unit.TestContext; import io.vertx.ext.unit.junit.VertxUnitRunner; import org.junit.Test; import org.junit.runner.RunWith; import org.swisspush.reststorage.util.LockMode; import org.swisspush.reststorage.util.StatusCode; import java.util.HashMap; import java.util.Map; import static com.jayway.awaitility.Awaitility.await; import static com.jayway.restassured.RestAssured.*; import static java.util.concurrent.TimeUnit.SECONDS; import static org.hamcrest.core.Is.is; /** * Testclass to test the lock mechanism. * * @author https://github.com/ljucam [Mario Ljuca] */ @RunWith(VertxUnitRunner.class) public class LockTest extends AbstractTestCase { private final String LOCK_HEADER = "x-lock"; private final String LOCK_MODE_HEADER = "x-lock-mode"; private final String LOCK_EXPIRE_AFTER_HEADER = "x-lock-expire-after"; @Test public void testPutSilentLockDifferentOwner(TestContext context) { Async async = context.async(); String path = "/lock/test/testcontent"; String content = "{\"content\":\"unlocked\"}"; String lockedContent = "{\"content\":\"locked\"}"; String foreignContent = "{\"content\":\"foreign\"}"; // perform a PUT of normal content given().body(content).put(path).then().assertThat().statusCode(StatusCode.OK.getStatusCode()); get(path).then().assertThat().body("content", is("unlocked")); // perform a PUT with lock given() .body(lockedContent) .headers(createLockHeaders("test", LockMode.SILENT, 10)) .put(path) .then() .assertThat().statusCode(StatusCode.OK.getStatusCode()); get(path).then().assertThat().body("content", is("locked")); // perform PUT without ownership for the lock given().body(foreignContent).put(path).then().assertThat().statusCode(StatusCode.OK.getStatusCode()); get(path).then().assertThat().body("content", is("locked")); // perform PUT with different ownership given() .body(foreignContent) .headers(createLockHeaders("test2", LockMode.SILENT, 10)) .put(path) .then() .assertThat().statusCode(StatusCode.OK.getStatusCode()); get(path).then().assertThat().body("content", is("locked")); async.complete(); } @Test public void testPutSilentLockSameOwner(TestContext context) { Async async = context.async(); String path = "/lock/test/testcontent"; String content = "{\"content\":\"unlocked\"}"; String lockedContent = "{\"content\":\"locked\"}"; String newLockedContent = "{\"content\":\"new\"}"; // perform a PUT of normal content given().body(content).put(path).then().assertThat().statusCode(StatusCode.OK.getStatusCode()); get(path).then().assertThat().body("content", is("unlocked")); // perform the first PUT with lock given() .body(lockedContent) .headers(createLockHeaders("test", LockMode.SILENT, 10)) .put(path) .then() .assertThat().statusCode(StatusCode.OK.getStatusCode()); get(path).then().assertThat().body("content", is("locked")); // perform the scond PUT with same lock owner given() .body(newLockedContent) .headers(createLockHeaders("test", LockMode.SILENT, 10)) .put(path) .then() .assertThat().statusCode(StatusCode.OK.getStatusCode()); get(path).then().assertThat().body("content", is("new")); async.complete(); } @Test public void testPutSilentLockExpire(TestContext context) { Async async = context.async(); long expireAfter = 5; String path = "/lock/test/testcontent"; String content = "{\"content\":\"unlocked\"}"; String lockedContent = "{\"content\":\"locked\"}"; // perform a PUT of normal content given().body(content).put(path).then().assertThat().statusCode(StatusCode.OK.getStatusCode()); get(path).then().assertThat().body("content", is("unlocked")); // perform a PUT with lock given() .body(lockedContent) .headers(createLockHeaders("test", LockMode.SILENT, expireAfter)) .put(path) .then() .assertThat().statusCode(StatusCode.OK.getStatusCode()); get(path).then().assertThat().body("content", is("locked")); // perform PUT without lock (wait at most 2 x expire time) await().atMost(expireAfter * 2, SECONDS).until(() -> { given().body(content).put(path).then().assertThat().statusCode(StatusCode.OK.getStatusCode()); return get(path).getBody().jsonPath().getString("content"); }, is("unlocked") ); async.complete(); } @Test public void testPutRejectLockDifferentOwner(TestContext context ) { Async async = context.async(); String path = "/lock/test/testcontent"; String content = "{\"content\":\"unlocked\"}"; String lockedContent = "{\"content\":\"locked\"}"; String foreignContent = "{\"content\":\"foreign\"}"; // perform a PUT of normal content given().body(content).put(path).then().assertThat().statusCode(StatusCode.OK.getStatusCode()); get(path).then().assertThat().body("content", is("unlocked")); // perform a PUT with lock given() .body(lockedContent) .headers(createLockHeaders("test", LockMode.REJECT, 10)) .put(path) .then() .assertThat().statusCode(StatusCode.OK.getStatusCode()); get(path).then().assertThat().body("content", is("locked")); // perform PUT without ownership for the lock given().body(foreignContent).put(path).then().assertThat().statusCode(StatusCode.CONFLICT.getStatusCode()); get(path).then().assertThat().body("content", is("locked")); // perform PUT with different ownership given() .body(foreignContent) .headers(createLockHeaders("test2", LockMode.SILENT, 10)) .put(path) .then() .assertThat().statusCode(StatusCode.CONFLICT.getStatusCode()); get(path).then().assertThat().body("content", is("locked")); async.complete(); } @Test public void testPutRejectLockSameOwner(TestContext context) { Async async = context.async(); String path = "/lock/test/testcontent"; String content = "{\"content\":\"unlocked\"}"; String lockedContent = "{\"content\":\"locked\"}"; String newLockedContent = "{\"content\":\"new\"}"; // perform a PUT of normal content given().body(content).put(path).then().assertThat().statusCode(StatusCode.OK.getStatusCode()); get(path).then().assertThat().body("content", is("unlocked")); // perform the first PUT with lock given() .body(lockedContent) .headers(createLockHeaders("test", LockMode.REJECT, 10)) .put(path) .then() .assertThat().statusCode(StatusCode.OK.getStatusCode()); get(path).then().assertThat().body("content", is("locked")); // perform the scond PUT with same lock owner given() .body(newLockedContent) .headers(createLockHeaders("test", LockMode.REJECT, 10)) .put(path) .then() .assertThat().statusCode(StatusCode.OK.getStatusCode()); get(path).then().assertThat().body("content", is("new")); async.complete(); } @Test public void testPutRejectLockExpire(TestContext context) { Async async = context.async(); long expireAfter = 5; String path = "/lock/test/testcontent"; String content = "{\"content\":\"unlocked\"}"; String lockedContent = "{\"content\":\"locked\"}"; // perform a PUT of normal content given().body(content).put(path).then().assertThat().statusCode(StatusCode.OK.getStatusCode()); get(path).then().assertThat().body("content", is("unlocked")); // perform a PUT with lock given() .body(lockedContent) .headers(createLockHeaders("test", LockMode.REJECT, expireAfter)) .put(path) .then() .assertThat().statusCode(StatusCode.OK.getStatusCode()); get(path).then().assertThat().body("content", is("locked")); // perform PUT without lock (should return conflict) given().body(content).put(path).then().assertThat().statusCode(StatusCode.CONFLICT.getStatusCode()); // perform PUT without lock (wait at most 2 x expire time) await().atMost(expireAfter * 2, SECONDS).until(() -> { given().body(content).put(path); return get(path).getBody().jsonPath().getString("content"); }, is("unlocked") ); async.complete(); } @Test public void testDeleteSilentLockSameOwner(TestContext context) { Async async = context.async(); String path = "/lock/test/testcontent"; String lockedContent = "{\"content\":\"locked\"}"; // perform a PUT with lock given() .body(lockedContent) .headers(createLockHeaders("test", LockMode.SILENT, 10)) .put(path) .then() .assertThat().statusCode(StatusCode.OK.getStatusCode()); get(path).then().assertThat().body("content", is("locked")); // perform DELETE with same lock owner with() .headers(createLockHeaders("test", LockMode.SILENT, 10)) .delete(path) .then().assertThat().statusCode(StatusCode.OK.getStatusCode()); get(path).then().assertThat().statusCode(StatusCode.NOT_FOUND.getStatusCode()); async.complete(); } @Test public void testDeleteSilentLockDifferentOwner(TestContext context) { Async async = context.async(); String path = "/lock/test/testcontent"; String lockedContent = "{\"content\":\"locked\"}"; // perform a PUT without lock given().body(lockedContent).put(path).then().assertThat().statusCode(StatusCode.OK.getStatusCode()); get(path).then().assertThat().body("content", is("locked")); // perform DELETE with lock with() .headers(createLockHeaders("test", LockMode.SILENT, 10)) .delete(path) .then().assertThat().statusCode(StatusCode.OK.getStatusCode()); get(path).then().assertThat().statusCode(StatusCode.NOT_FOUND.getStatusCode()); // perform a PUT without lock given().body(lockedContent).put(path).then().assertThat().statusCode(StatusCode.OK.getStatusCode()); get(path).then().assertThat().statusCode(StatusCode.NOT_FOUND.getStatusCode()); async.complete(); } @Test public void testDeleteRejectLockSameOwner(TestContext context) { Async async = context.async(); String path = "/lock/test/testcontent"; String lockedContent = "{\"content\":\"unlocked\"}"; // perform a PUT with lock given() .body(lockedContent) .headers(createLockHeaders("test", LockMode.REJECT, 10)) .put(path) .then() .assertThat().statusCode(StatusCode.OK.getStatusCode()); get(path).then().assertThat().body("content", is("unlocked")); // perform DELETE with same lock owner with() .headers(createLockHeaders("test", LockMode.REJECT, 10)) .delete(path) .then().assertThat().statusCode(StatusCode.OK.getStatusCode()); get(path).then().assertThat().statusCode(StatusCode.NOT_FOUND.getStatusCode()); async.complete(); } @Test public void testDeleteRejectLockDifferentOwner(TestContext context) { Async async = context.async(); String path = "/lock/test/testcontent"; String lockedContent = "{\"content\":\"unlocked\"}"; // perform a PUT without lock given().body(lockedContent).put(path).then().assertThat().statusCode(StatusCode.OK.getStatusCode()); get(path).then().assertThat().body("content", is("unlocked")); // perform DELETE with lock with() .headers(createLockHeaders("test", LockMode.REJECT, 10)) .delete(path) .then().assertThat().statusCode(StatusCode.OK.getStatusCode()); get(path).then().assertThat().statusCode(StatusCode.NOT_FOUND.getStatusCode()); // perform a PUT without lock given().body(lockedContent).put(path).then().assertThat().statusCode(StatusCode.CONFLICT.getStatusCode()); get(path).then().assertThat().statusCode(StatusCode.NOT_FOUND.getStatusCode()); async.complete(); } @Test public void testDeleteRejectLockExpire(TestContext context) { Async async = context.async(); long expireAfter = 5; String path = "/lock/test/testcontent"; String lockedContent = "{\"content\":\"unlocked\"}"; String newContent = "{\"content\":\"new\"}"; // perform a PUT without lock given().body(lockedContent).put(path).then().assertThat().statusCode(StatusCode.OK.getStatusCode()); get(path).then().assertThat().body("content", is("unlocked")); // perform DELETE with lock with() .headers(createLockHeaders("test", LockMode.REJECT, expireAfter)) .delete(path) .then().assertThat().statusCode(StatusCode.OK.getStatusCode()); get(path).then().assertThat().statusCode(StatusCode.NOT_FOUND.getStatusCode()); // perform PUT without lock (should return conflict) given().body(newContent).put(path).then().assertThat().statusCode(StatusCode.CONFLICT.getStatusCode()); // perform PUT without lock (wait at most 2 x expire time) await().atMost(expireAfter * 2, SECONDS).until(() -> { given().body(newContent).put(path); if ( given().body(newContent).put(path).getStatusCode() == StatusCode.OK.getStatusCode() ) { return get(path).getBody().jsonPath().getString("content"); } else { return "notfound"; } }, is("new") ); async.complete(); } @Test public void testDeleteSilentLockExpire(TestContext context) { Async async = context.async(); long expireAfter = 5; String path = "/lock/test/testcontent"; String lockedContent = "{\"content\":\"unlocked\"}"; String newContent = "{\"content\":\"new\"}"; // perform a PUT without lock given().body(lockedContent).put(path).then().assertThat().statusCode(StatusCode.OK.getStatusCode()); get(path).then().assertThat().body("content", is("unlocked")); // perform DELETE with lock with() .headers(createLockHeaders("test", LockMode.SILENT, expireAfter)) .delete(path) .then().assertThat().statusCode(StatusCode.OK.getStatusCode()); get(path).then().assertThat().statusCode(StatusCode.NOT_FOUND.getStatusCode()); // perform PUT without lock (should return ok) given().body(newContent).put(path).then().assertThat().statusCode(StatusCode.OK.getStatusCode()); get(path).then().assertThat().statusCode(StatusCode.NOT_FOUND.getStatusCode()); // perform PUT without lock (wait at most 2 x expire time) await().atMost(expireAfter * 2, SECONDS).until(() -> { given().body(newContent).put(path).then().assertThat().statusCode(StatusCode.OK.getStatusCode()); if ( get(path).getStatusCode() == StatusCode.OK.getStatusCode() ) { return get(path).getBody().jsonPath().getString("content"); } else { return "notfound"; } }, is("new") ); async.complete(); } @Test public void testDeleteCollectionWithLockedResource(TestContext context) { Async async = context.async(); String base = "/lock/test/lockcollection/"; String path1 = base + "res1"; String path2 = base + "res2"; String path3 = base + "res3"; String content1 = "{\"content\":\"res1\"}"; String content2 = "{\"content\":\"locked\"}"; String content3 = "{\"content\":\"res2\"}"; // create collection given().body(content1).put(path1).then().assertThat().statusCode(StatusCode.OK.getStatusCode()); given() .body(content2) .headers(createLockHeaders("test", LockMode.REJECT, 10)) .put(path2) .then() .assertThat().statusCode(StatusCode.OK.getStatusCode()); given().body(content3).put(path3).then().assertThat().statusCode(StatusCode.OK.getStatusCode()); // try to delete resource (should fail) delete(path2).then().statusCode(StatusCode.CONFLICT.getStatusCode()); // try to delete collection (should succeed) delete(base).then().statusCode(StatusCode.OK.getStatusCode()); get(path2).then().assertThat().statusCode(StatusCode.NOT_FOUND.getStatusCode()); async.complete(); } private Map<String, Object> createLockHeaders(String owner, LockMode mode, long expire) { Map<String, Object> lockHeaders = new HashMap<>(); lockHeaders.put(LOCK_HEADER, owner); lockHeaders.put(LOCK_MODE_HEADER, mode.text()); lockHeaders.put(LOCK_EXPIRE_AFTER_HEADER, expire); return lockHeaders; } }