/** * personium.io * Copyright 2014 FUJITSU LIMITED * * Licensed 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. */ package com.fujitsu.dc.test.jersey.box.dav.file; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import org.apache.http.HttpHeaders; import org.apache.http.HttpStatus; import org.junit.Ignore; import org.junit.Test; import org.junit.experimental.categories.Category; import org.junit.runner.RunWith; import org.w3c.dom.Element; import org.w3c.dom.NodeList; import com.fujitsu.dc.common.utils.DcCoreUtils; import com.fujitsu.dc.core.DcCoreConfig; import com.fujitsu.dc.core.DcCoreConfig.BinaryData; import com.fujitsu.dc.test.categories.Integration; import com.fujitsu.dc.test.categories.Regression; import com.fujitsu.dc.test.categories.Unit; import com.fujitsu.dc.test.jersey.AbstractCase; import com.fujitsu.dc.test.jersey.DcRunner; import com.fujitsu.dc.test.setup.Setup; import com.fujitsu.dc.test.utils.Http; import com.fujitsu.dc.test.utils.TResponse; import com.sun.jersey.test.framework.JerseyTest; /** * DAV File related tests. */ @RunWith(DcRunner.class) @Category({Unit.class, Integration.class, Regression.class }) public class DavFileTest extends JerseyTest { private static final String CELL_NAME = "testcell1"; private static final String TEST_BOX1 = "box1"; private static final String FILE_NAME = "file1.txt"; private static final String FILE_BODY = "testFileBody"; private static final String FILE_BODY2 = "testFileBody2"; /** * constructor. */ public DavFileTest() { super("com.fujitsu.dc.core.rs"); } /** * IfNoneMatch指定なしでFileをGetした場合に200が返却されること. */ @Test public final void IfNoneMatch指定なしでFileをGetした場合に200が返却されること() { try { // ファイル新規作成 final Http theReq = this.putFileRequest(FILE_NAME, FILE_BODY, null, Setup.TEST_BOX1); TResponse resp = theReq.returns() .statusCode(HttpStatus.SC_CREATED); // Etag取得 String etag = resp.getHeader(HttpHeaders.ETAG); assertNotNull(etag); // ファイル取得 TResponse getResp = this.getFileRequest(FILE_NAME, TEST_BOX1, null) .returns(); getResp.statusCode(HttpStatus.SC_OK); assertEquals(FILE_BODY, getResp.getBody()); } finally { this.deleteFileRequest(FILE_NAME, null, Setup.TEST_BOX1).returns() .statusCode(HttpStatus.SC_NO_CONTENT); } } /** * IfNoneMatchでに正しい形式の値を指定してFileをGetした場合に指定が有効になること. */ @Test public final void returns_304_on_GET_with_matching_ETag_in_IfNoneMatch_header() { try { // create new DAV file for this test final Http theReq = this.putFileRequest(FILE_NAME, FILE_BODY, null, Setup.TEST_BOX1); TResponse resp = theReq.returns() .statusCode(HttpStatus.SC_CREATED); // Retrieve Etag header String etag1 = resp.getHeader(HttpHeaders.ETAG); assertNotNull(etag1); // Should return 304 on GET request with // matching ETag specified in If-None-Match. TResponse getResp = this.getFileRequest(FILE_NAME, TEST_BOX1, etag1) .returns(); getResp.statusCode(HttpStatus.SC_NOT_MODIFIED); // Body Should be empty. assertEquals("", getResp.getBody()); // Same ETag should be returned again assertEquals(etag1, resp.getHeader(HttpHeaders.ETAG)); // Weak ETag format should also work. String wEtag = "W/" + etag1; getResp = this.getFileRequest(FILE_NAME, TEST_BOX1, wEtag) .returns(); getResp.statusCode(HttpStatus.SC_NOT_MODIFIED); // Body Should be empty. assertEquals("", getResp.getBody()); // Same ETag should be returned again assertEquals(etag1, resp.getHeader(HttpHeaders.ETAG)); // Update the DAV File resp = this.putFileRequest(FILE_NAME, FILE_BODY, null, Setup.TEST_BOX1).returns() .statusCode(HttpStatus.SC_NO_CONTENT); // retrieve new Etag String etag2 = resp.getHeader(HttpHeaders.ETAG); assertNotNull(etag2); // Should return 200 on GET request with // non-matching ETag specified in If-None-Match. getResp = this.getFileRequest(FILE_NAME, TEST_BOX1, etag1) .returns(); getResp.statusCode(HttpStatus.SC_OK); assertEquals(FILE_BODY, getResp.getBody()); assertEquals(etag2, resp.getHeader(HttpHeaders.ETAG)); } finally { // delete the DAV file for this test this.deleteFileRequest(FILE_NAME, null, Setup.TEST_BOX1).returns() .statusCode(HttpStatus.SC_NO_CONTENT); } } /** * IfNoneMatchにアスタを指定してFileをGetした場合に200が返却されること. */ @Test public final void returns_200_on_GET_with_asterisk_in_IfNoneMatch_header() { try { // create new DAV file for this test final Http theReq = this.putFileRequest(FILE_NAME, FILE_BODY, null, Setup.TEST_BOX1); TResponse resp = theReq.returns() .statusCode(HttpStatus.SC_CREATED); // retrieve Etag header String etag = resp.getHeader(HttpHeaders.ETAG); assertNotNull(etag); // ファイル取得 TResponse getResp = this.getFileRequest(FILE_NAME, TEST_BOX1, "*") .returns(); getResp.statusCode(HttpStatus.SC_OK); assertEquals(FILE_BODY, getResp.getBody()); assertEquals(etag, resp.getHeader(HttpHeaders.ETAG)); } finally { this.deleteFileRequest(FILE_NAME, null, Setup.TEST_BOX1).returns() .statusCode(HttpStatus.SC_NO_CONTENT); } } /** * It should return 200 on GET with invalid value in IfNoneMatch header. */ @Test public final void returns_200_on_GET_with_invalid_value_in_IfNoneMatch_header() { try { // create new DAV file for this test final Http theReq = this.putFileRequest(FILE_NAME, FILE_BODY, null, Setup.TEST_BOX1); TResponse resp = theReq.returns() .statusCode(HttpStatus.SC_CREATED); // retrieve Etag header String etag = resp.getHeader(HttpHeaders.ETAG); assertNotNull(etag); // If-None-Matchヘッダに空文字を指定した場合に、ヘッダが無視されてファイルを取得できること TResponse getResp = this.getFileRequest(FILE_NAME, TEST_BOX1, "") .returns(); getResp.statusCode(HttpStatus.SC_OK); assertEquals(FILE_BODY, getResp.getBody()); assertEquals(etag, resp.getHeader(HttpHeaders.ETAG)); // If-None-MatchヘッダにEtag形式でない文字列を指定した場合に、ヘッダが無視されてファイルを取得できること getResp = this.getFileRequest(FILE_NAME, TEST_BOX1, "abc!abc!abc!abc!abc!abc!") .returns(); getResp.statusCode(HttpStatus.SC_OK); assertEquals(FILE_BODY, getResp.getBody()); assertEquals(etag, resp.getHeader(HttpHeaders.ETAG)); } finally { this.deleteFileRequest(FILE_NAME, null, Setup.TEST_BOX1).returns() .statusCode(HttpStatus.SC_NO_CONTENT); } } /** * FileをPUTで新規配置しそれが取得できる. */ @Test public final void FileをPUTで新規配置するテスト() { try { final Http theReq = this.putFileRequest(FILE_NAME, FILE_BODY, null, Setup.TEST_BOX1); TResponse resp = theReq.returns() .statusCode(HttpStatus.SC_CREATED); String etag = resp.getHeader(HttpHeaders.ETAG); assertNotNull(etag); this.getFileRequest(FILE_NAME, TEST_BOX1, null) .returns() .statusCode(HttpStatus.SC_OK); this.getFileRequest(FILE_NAME, TEST_BOX1, etag) .returns() .statusCode(HttpStatus.SC_NOT_MODIFIED); long expectedSize = FILE_BODY.getBytes().length; String fileSize = null; // PROPFINDでDavファイルのContentLengthが正しく取得できるかチェック TResponse res = Http.request("box/propfind-box-allprop.txt") .with("cellPath", Setup.TEST_CELL1) .with("path", Setup.TEST_BOX1) .with("depth", "1") .with("token", AbstractCase.MASTER_TOKEN_NAME) .returns() .debug() .statusCode(HttpStatus.SC_MULTI_STATUS); Element root = res.bodyAsXml().getDocumentElement(); NodeList resElems = root.getElementsByTagName("response"); assertTrue(0 < resElems.getLength()); for (int i = 0; i < resElems.getLength(); i++) { Element elem = (Element) resElems.item(i); String davUrl = elem.getElementsByTagName("href").item(0).getTextContent(); if (!davUrl.endsWith("/" + FILE_NAME)) { continue; } fileSize = elem.getElementsByTagName("getcontentlength").item(0).getTextContent(); } assertNotNull(fileSize); assertEquals(expectedSize, Long.parseLong(fileSize)); } finally { this.deleteFileRequest(FILE_NAME, null, Setup.TEST_BOX1).returns() .statusCode(HttpStatus.SC_NO_CONTENT); } } /** * FileをEtagなしPUTで更新する. */ @Test public final void FileをEtagなしPUTで更新する() { try { TResponse resp = this.putFileRequest(FILE_NAME, FILE_BODY, null, Setup.TEST_BOX1) .returns() .statusCode(HttpStatus.SC_CREATED); String etag = resp.getHeader(HttpHeaders.ETAG); assertNotNull(etag); TResponse resp2 = this.putFileRequest(FILE_NAME, FILE_BODY2, null, Setup.TEST_BOX1) .returns() .statusCode(HttpStatus.SC_NO_CONTENT); String etag2 = resp2.getHeader(HttpHeaders.ETAG); assertNotNull(etag2); assertFalse(etag.equals(etag2)); } finally { this.deleteFileRequest(FILE_NAME, null, Setup.TEST_BOX1).returns() .statusCode(HttpStatus.SC_NO_CONTENT); } } /** * FileをEtagなしPUTで更新し更新後データを正常に取得できる. */ @Test public final void FileをEtagなしPUTで更新し更新後もデータを正常に取得できる() { try { // PUT TResponse resp = this.putFileRequest(FILE_NAME, FILE_BODY, null, Setup.TEST_BOX1) .returns() .statusCode(HttpStatus.SC_CREATED); String etag = resp.getHeader(HttpHeaders.ETAG); assertNotNull(etag); // GET TResponse getRes = this.getFileRequest(FILE_NAME, TEST_BOX1, null) .returns() .statusCode(HttpStatus.SC_OK); int cl1 = Integer.parseInt(getRes.getHeader(HttpHeaders.CONTENT_LENGTH)); assertEquals(FILE_BODY.length(), cl1); // PUT TResponse resp2 = this.putFileRequest(FILE_NAME, FILE_BODY2, null, Setup.TEST_BOX1) .returns() .statusCode(HttpStatus.SC_NO_CONTENT); String etag2 = resp2.getHeader(HttpHeaders.ETAG); assertNotNull(etag2); // GET getRes = this.getFileRequest(FILE_NAME, TEST_BOX1, null) .returns() .statusCode(HttpStatus.SC_OK); int cl2 = Integer.parseInt(getRes.getHeader(HttpHeaders.CONTENT_LENGTH)); assertEquals(FILE_BODY2.length(), cl2); } finally { this.deleteFileRequest(FILE_NAME, null, Setup.TEST_BOX1).returns() .statusCode(HttpStatus.SC_NO_CONTENT); } } /** * FileをETAGつきPUTで更新してDELETEする. */ @Test public final void FileをETAGつきPUTで更新してDELETEする() { try { TResponse resp = this.putFileRequest(FILE_NAME, FILE_BODY, null, Setup.TEST_BOX1) .returns() .statusCode(HttpStatus.SC_CREATED); String etag = resp.getHeader(HttpHeaders.ETAG); assertNotNull(etag); TResponse resp2 = this.putFileRequest(FILE_NAME, FILE_BODY2, etag, Setup.TEST_BOX1) .returns() .statusCode(HttpStatus.SC_NO_CONTENT); String etag2 = resp2.getHeader(HttpHeaders.ETAG); assertNotNull(etag2); assertFalse(etag.equals(etag2)); this.deleteFileRequest(FILE_NAME, etag2, Setup.TEST_BOX1) .returns() .statusCode(HttpStatus.SC_NO_CONTENT); } finally { this.deleteFileRequest(FILE_NAME, null, Setup.TEST_BOX1).returns() .statusCode(HttpStatus.SC_NOT_FOUND); // 既に消えているはずなので404を期待 } } /** * FileをPUTでの格納フォルダ重複エラーを確認する. * テストの実施に時間が掛かり過ぎる(約3分)ため、本テストは無効化する。 * なお、既存ディレクトリの存在チェックテストは、以下ユニットテストで確認する。 * BinaryDataAccessorTest#既存ディレクトリにファイルの登録が可能な事を確認する() */ @Test @Ignore public final void FileをPUTでの格納フォルダ重複エラーを確認する() { String filaName = "test"; String txt = ".txt"; int makeFileNum = 1500; try { // ファイルを1500作成し、"File System Inconsistency Detected."を発生しない事を確認 for (int i = 0; i < makeFileNum; i++) { final Http theReq = this.putFileRequest(filaName + i + txt, FILE_BODY, null, Setup.TEST_BOX1); theReq.returns().statusCode(HttpStatus.SC_CREATED); } // test1499.txtが作成されていることを確認する final Http theReq = this.getFileRequest(filaName + "1499" + txt, Setup.TEST_BOX1, null); theReq.returns().statusCode(HttpStatus.SC_OK); } finally { for (int i = 0; i < makeFileNum; i++) { this.deleteFileRequest(filaName + i + txt, null, Setup.TEST_BOX1).returns(); } } } /** * FileをfsyncでPUTする. */ @Test public final void FileをfsyncでPUTする() { // fsyncを有効にする boolean fsyncEnabled = DcCoreConfig.getFsyncEnabled(); DcCoreConfig.set(BinaryData.FSYNC_ENABLED, "true"); try { final Http theReq = this.putFileRequest(FILE_NAME, FILE_BODY, null, Setup.TEST_BOX1); TResponse resp = theReq.returns() .statusCode(HttpStatus.SC_CREATED); String etag = resp.getHeader(HttpHeaders.ETAG); assertNotNull(etag); this.getFileRequest(FILE_NAME, TEST_BOX1, null) .returns() .statusCode(HttpStatus.SC_OK); this.getFileRequest(FILE_NAME, TEST_BOX1, etag) .returns() .statusCode(HttpStatus.SC_NOT_MODIFIED); long expectedSize = FILE_BODY.getBytes().length; String fileSize = null; // PROPFINDでDavファイルのContentLengthが正しく取得できるかチェック TResponse res = Http.request("box/propfind-box-allprop.txt") .with("cellPath", Setup.TEST_CELL1) .with("path", Setup.TEST_BOX1) .with("depth", "1") .with("token", AbstractCase.MASTER_TOKEN_NAME) .returns() .debug() .statusCode(HttpStatus.SC_MULTI_STATUS); Element root = res.bodyAsXml().getDocumentElement(); NodeList resElems = root.getElementsByTagName("response"); assertTrue(0 < resElems.getLength()); for (int i = 0; i < resElems.getLength(); i++) { Element elem = (Element) resElems.item(i); String davUrl = elem.getElementsByTagName("href").item(0).getTextContent(); if (!davUrl.endsWith("/" + FILE_NAME)) { continue; } fileSize = elem.getElementsByTagName("getcontentlength").item(0).getTextContent(); } assertNotNull(fileSize); assertEquals(expectedSize, Long.parseLong(fileSize)); } finally { this.deleteFileRequest(FILE_NAME, null, Setup.TEST_BOX1).returns() .statusCode(HttpStatus.SC_NO_CONTENT); DcCoreConfig.set(BinaryData.FSYNC_ENABLED, String.valueOf(fsyncEnabled)); } } /** * Rangeヘッダ指定でファイルの部分取得. */ @Test public final void Rangeヘッダ指定でファイルの部分取得() { try { String body = "abcdefghijklmn"; // ファイル新規作成 final Http theReq = this.putFileRequest(FILE_NAME, body, null, Setup.TEST_BOX1); theReq.returns().statusCode(HttpStatus.SC_CREATED); // ファイル取得 int first = 2; int last = 10; String rangeHeader = String.format("bytes=%s-%s", first, last); TResponse getResp = this.getFileRequestAtRange(FILE_NAME, TEST_BOX1, rangeHeader) .returns(); getResp.statusCode(HttpStatus.SC_PARTIAL_CONTENT); assertEquals(String.format("bytes %s-%s/%s", first, last, body.length()), getResp.getHeader(DcCoreUtils.HttpHeaders.CONTENT_RANGE)); assertEquals(body.substring(first, last + 1), getResp.getBody()); } finally { this.deleteFileRequest(FILE_NAME, null, Setup.TEST_BOX1).returns() .statusCode(HttpStatus.SC_NO_CONTENT); } } /** * 無効なRangeヘッダ指定でファイル全体取得になること. */ @Test public final void 無効なRangeヘッダ指定でファイル全体取得になること() { try { String body = "abcdefghijklmn"; // ファイル新規作成 final Http theReq = this.putFileRequest(FILE_NAME, body, null, Setup.TEST_BOX1); theReq.returns().statusCode(HttpStatus.SC_CREATED); // ファイル取得 String rangeHeader = "bytes=a-b,1-2"; TResponse getResp = this.getFileRequestAtRange(FILE_NAME, TEST_BOX1, rangeHeader) .returns(); getResp.statusCode(HttpStatus.SC_OK); assertEquals(body, getResp.getBody()); } finally { this.deleteFileRequest(FILE_NAME, null, Setup.TEST_BOX1).returns() .statusCode(HttpStatus.SC_NO_CONTENT); } } /** * Rangeヘッダ指定範囲誤りで416レスポンスが返却されること. */ @Test public final void Rangeヘッダ指定範囲誤りで416レスポンスが返却されること() { try { String body = "abcdefghijklmn"; // ファイル新規作成 final Http theReq = this.putFileRequest(FILE_NAME, body, null, Setup.TEST_BOX1); theReq.returns().statusCode(HttpStatus.SC_CREATED); // ファイル取得 int first = body.length() + 100; int last = first + 10; String rangeHeader = String.format("bytes=%s-%s", first, last); TResponse getResp = this.getFileRequestAtRange(FILE_NAME, TEST_BOX1, rangeHeader) .returns(); getResp.statusCode(HttpStatus.SC_REQUESTED_RANGE_NOT_SATISFIABLE); } finally { this.deleteFileRequest(FILE_NAME, null, Setup.TEST_BOX1).returns() .statusCode(HttpStatus.SC_NO_CONTENT); } } /** * Rangeヘッダで複数範囲指定すると501レスポンスが返却されること. * MultiPartレスポンスを制限事項とするため501を返却する. */ @Test public final void Rangeヘッダで複数範囲指定すると501レスポンスが返却されること() { try { String body = "abcdefghijklmn"; // ファイル新規作成 final Http theReq = this.putFileRequest(FILE_NAME, body, null, Setup.TEST_BOX1); theReq.returns().statusCode(HttpStatus.SC_CREATED); // ファイル取得 String rangeHeader = "bytes=1-2,3-4"; TResponse getResp = this.getFileRequestAtRange(FILE_NAME, TEST_BOX1, rangeHeader) .returns(); getResp.statusCode(HttpStatus.SC_NOT_IMPLEMENTED); } finally { this.deleteFileRequest(FILE_NAME, null, Setup.TEST_BOX1).returns() .statusCode(HttpStatus.SC_NO_CONTENT); } } /** * File取得リクエストを生成. * @param boxName box名 * @param col ロール名 * @return リクエストオブジェクト */ Http getFileRequest(String fileName, String boxName, String etag) { if (etag == null) { return Http.request("box/dav-get.txt") .with("cellPath", CELL_NAME) .with("token", AbstractCase.MASTER_TOKEN_NAME) .with("box", boxName) .with("path", fileName); } else { return Http.request("box/dav-get-ifnonematch.txt") .with("cellPath", CELL_NAME) .with("token", AbstractCase.MASTER_TOKEN_NAME) .with("etag", etag) .with("box", boxName) .with("path", fileName); } } /** * Rangeヘッダ指定File取得リクエストを生成. * @param fileName リソースファイル名 * @param boxName ボックス名 * @param rangeHeader Rangeヘッダ指定 * @return リクエストオブジェクト */ Http getFileRequestAtRange(String fileName, String boxName, String rangeHeader) { return Http.request("box/dav-get-range.txt") .with("cellPath", CELL_NAME) .with("token", AbstractCase.MASTER_TOKEN_NAME) .with("box", boxName) .with("path", fileName) .with("range-field", rangeHeader); } /** * File作成リクエストを生成. * @param boxName box名 * @param roleName ロール名 * @param boxName Box名 * @return リクエストオブジェクト */ Http putFileRequest(String fileName, String fileBody, String etag, String boxName) { if (etag == null) { return Http.request("box/dav-put.txt") .with("cellPath", CELL_NAME) .with("token", AbstractCase.MASTER_TOKEN_NAME) .with("path", fileName) .with("box", boxName) .with("contentType", javax.ws.rs.core.MediaType.TEXT_PLAIN) .with("source", fileBody); } else { return Http.request("box/dav-put-ifmatch.txt") .with("cellPath", CELL_NAME) .with("token", AbstractCase.MASTER_TOKEN_NAME) .with("path", fileName) .with("box", boxName) .with("contentType", javax.ws.rs.core.MediaType.TEXT_PLAIN) .with("etag", etag) .with("source", fileBody); } } /** * File削除リクエストを生成. * @param boxName box名 * @param col ロール名 * @return リクエストオブジェクト */ Http deleteFileRequest(String fileName, String etag, String boxName) { if (etag == null) { return Http.request("box/dav-delete.txt") .with("cellPath", CELL_NAME) .with("token", AbstractCase.MASTER_TOKEN_NAME) .with("box", boxName) .with("path", fileName); } else { return Http.request("box/dav-delete-ifmatch.txt") .with("cellPath", CELL_NAME) .with("etag", etag) .with("token", AbstractCase.MASTER_TOKEN_NAME) .with("box", boxName) .with("path", fileName); } } }