diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml new file mode 100644 index 0000000..3b74bc0 --- /dev/null +++ b/.github/workflows/e2e.yml @@ -0,0 +1,210 @@ +name: E2E Test + +on: + workflow_dispatch: + inputs: + test_filter: + description: 'Test filter (e.g., --tests SmokeTest)' + required: false + default: '' + push: + branches: + - master + +# Note: E2E tests do NOT run on pull_request events from forks +# because GitHub Secrets are not available for security reasons. +# PRs from forks will only run the regular CI (build + unit tests). + +jobs: + e2e-test: + name: E2E Test + runs-on: ubuntu-latest + # Skip if secrets are not available (e.g., fork PRs) + if: ${{ github.event_name == 'workflow_dispatch' || github.event_name == 'push' }} + env: + KINTONE_BASE_URL: ${{ secrets.KINTONE_BASE_URL }} + KINTONE_DEFAULT_USER: ${{ secrets.KINTONE_DEFAULT_USER }} + KINTONE_DEFAULT_PASSWORD: ${{ secrets.KINTONE_DEFAULT_PASSWORD }} + KINTONE_TEST_USER: ${{ secrets.KINTONE_TEST_USER }} + KINTONE_TEST_PASSWORD: ${{ secrets.KINTONE_TEST_PASSWORD }} + KINTONE_SPACE_ID: ${{ secrets.KINTONE_SPACE_ID }} + KINTONE_MULTI_THREAD_SPACE_ID: ${{ secrets.KINTONE_MULTI_THREAD_SPACE_ID }} + KINTONE_MULTI_THREAD_DEFAULT_THREAD_ID: ${{ secrets.KINTONE_MULTI_THREAD_DEFAULT_THREAD_ID }} + KINTONE_GUEST_SPACE_ID: ${{ secrets.KINTONE_GUEST_SPACE_ID }} + KINTONE_TEMPLATE_ID: ${{ secrets.KINTONE_TEMPLATE_ID }} + KINTONE_TEST_APP_ID: ${{ secrets.KINTONE_TEST_APP_ID }} + KINTONE_TEST_SPACE_APP_ID: ${{ secrets.KINTONE_TEST_SPACE_APP_ID }} + KINTONE_TEST_APP_ID_FOR_GET_PLUGINS_PREVIEW: ${{ secrets.KINTONE_TEST_APP_ID_FOR_GET_PLUGINS_PREVIEW }} + KINTONE_TEST_APP_ID_FOR_GET_FORM_LAYOUT_PREVIEW: ${{ secrets.KINTONE_TEST_APP_ID_FOR_GET_FORM_LAYOUT_PREVIEW }} + KINTONE_TEST_APP_ID_FOR_PROCESS_MANAGEMENT: ${{ secrets.KINTONE_TEST_APP_ID_FOR_PROCESS_MANAGEMENT }} + KINTONE_TEST_APP_ID_FOR_UPDATE_APP_SETTINGS: ${{ secrets.KINTONE_TEST_APP_ID_FOR_UPDATE_APP_SETTINGS }} + KINTONE_TEST_APP_ID_FOR_PLUGIN: ${{ secrets.KINTONE_TEST_APP_ID_FOR_PLUGIN }} + KINTONE_BASIC_USER: ${{ secrets.KINTONE_BASIC_USER }} + KINTONE_BASIC_PASS: ${{ secrets.KINTONE_BASIC_PASS }} + permissions: + checks: write + contents: read + steps: + - uses: actions/checkout@v4 + + - name: Set up JDK 8 + uses: actions/setup-java@v4 + with: + distribution: corretto + java-version: 8 + cache: gradle + + - name: Grant execute permission for gradlew + run: | + chmod +x gradlew + chmod +x e2e-tests/gradlew + chmod +x e2e-tests/bin/*.sh + + - name: Run E2E tests + env: + TEST_FILTER: ${{ github.event.inputs.test_filter }} + run: | + cd e2e-tests + ./bin/run-test.sh ${TEST_FILTER} + + - name: Publish Test Report + uses: mikepenz/action-junit-report@v5 + if: success() || failure() + with: + check_name: E2E Test Report + report_paths: e2e-tests/build/test-results/test/TEST-*.xml + annotations_limit: 50 + + e2e-proxy-test: + name: Proxy E2E Test + runs-on: ubuntu-latest + needs: e2e-test + env: + KINTONE_BASE_URL: ${{ secrets.KINTONE_BASE_URL }} + KINTONE_DEFAULT_USER: ${{ secrets.KINTONE_DEFAULT_USER }} + KINTONE_DEFAULT_PASSWORD: ${{ secrets.KINTONE_DEFAULT_PASSWORD }} + KINTONE_TEST_USER: ${{ secrets.KINTONE_TEST_USER }} + KINTONE_TEST_PASSWORD: ${{ secrets.KINTONE_TEST_PASSWORD }} + KINTONE_SPACE_ID: ${{ secrets.KINTONE_SPACE_ID }} + KINTONE_MULTI_THREAD_SPACE_ID: ${{ secrets.KINTONE_MULTI_THREAD_SPACE_ID }} + KINTONE_MULTI_THREAD_DEFAULT_THREAD_ID: ${{ secrets.KINTONE_MULTI_THREAD_DEFAULT_THREAD_ID }} + KINTONE_GUEST_SPACE_ID: ${{ secrets.KINTONE_GUEST_SPACE_ID }} + KINTONE_TEMPLATE_ID: ${{ secrets.KINTONE_TEMPLATE_ID }} + KINTONE_TEST_APP_ID: ${{ secrets.KINTONE_TEST_APP_ID }} + KINTONE_TEST_SPACE_APP_ID: ${{ secrets.KINTONE_TEST_SPACE_APP_ID }} + KINTONE_TEST_APP_ID_FOR_GET_PLUGINS_PREVIEW: ${{ secrets.KINTONE_TEST_APP_ID_FOR_GET_PLUGINS_PREVIEW }} + KINTONE_TEST_APP_ID_FOR_GET_FORM_LAYOUT_PREVIEW: ${{ secrets.KINTONE_TEST_APP_ID_FOR_GET_FORM_LAYOUT_PREVIEW }} + KINTONE_TEST_APP_ID_FOR_PROCESS_MANAGEMENT: ${{ secrets.KINTONE_TEST_APP_ID_FOR_PROCESS_MANAGEMENT }} + KINTONE_TEST_APP_ID_FOR_UPDATE_APP_SETTINGS: ${{ secrets.KINTONE_TEST_APP_ID_FOR_UPDATE_APP_SETTINGS }} + KINTONE_TEST_APP_ID_FOR_PLUGIN: ${{ secrets.KINTONE_TEST_APP_ID_FOR_PLUGIN }} + KINTONE_BASIC_USER: ${{ secrets.KINTONE_BASIC_USER }} + KINTONE_BASIC_PASS: ${{ secrets.KINTONE_BASIC_PASS }} + steps: + - uses: actions/checkout@v4 + + - name: Set up JDK 8 + uses: actions/setup-java@v4 + with: + distribution: corretto + java-version: 8 + cache: gradle + + - name: Grant execute permission + run: | + chmod +x gradlew + chmod +x e2e-tests/gradlew + chmod +x e2e-tests/bin/*.sh + + - name: Build and start proxy container + run: | + docker build -t java-sdk-test-proxy e2e-tests/docker/proxy + docker run -d --name squid-proxy -p 3128:3128 java-sdk-test-proxy + for i in $(seq 1 30); do + if nc -z localhost 3128; then + echo "Proxy is ready" + break + fi + echo "Waiting for proxy... ($i/30)" + sleep 1 + done + + - name: Run SmokeTest via proxy + run: | + cd e2e-tests + ./bin/run-test.sh --tests SmokeTest + env: + KINTONE_PROXY_URL: http://localhost:3128 + + - name: Stop proxy container + run: docker rm -f squid-proxy || true + if: always() + + e2e-auth-proxy-test: + name: Authenticated Proxy E2E Test + runs-on: ubuntu-latest + needs: e2e-proxy-test + env: + KINTONE_BASE_URL: ${{ secrets.KINTONE_BASE_URL }} + KINTONE_DEFAULT_USER: ${{ secrets.KINTONE_DEFAULT_USER }} + KINTONE_DEFAULT_PASSWORD: ${{ secrets.KINTONE_DEFAULT_PASSWORD }} + KINTONE_TEST_USER: ${{ secrets.KINTONE_TEST_USER }} + KINTONE_TEST_PASSWORD: ${{ secrets.KINTONE_TEST_PASSWORD }} + KINTONE_SPACE_ID: ${{ secrets.KINTONE_SPACE_ID }} + KINTONE_MULTI_THREAD_SPACE_ID: ${{ secrets.KINTONE_MULTI_THREAD_SPACE_ID }} + KINTONE_MULTI_THREAD_DEFAULT_THREAD_ID: ${{ secrets.KINTONE_MULTI_THREAD_DEFAULT_THREAD_ID }} + KINTONE_GUEST_SPACE_ID: ${{ secrets.KINTONE_GUEST_SPACE_ID }} + KINTONE_TEMPLATE_ID: ${{ secrets.KINTONE_TEMPLATE_ID }} + KINTONE_TEST_APP_ID: ${{ secrets.KINTONE_TEST_APP_ID }} + KINTONE_TEST_SPACE_APP_ID: ${{ secrets.KINTONE_TEST_SPACE_APP_ID }} + KINTONE_TEST_APP_ID_FOR_GET_PLUGINS_PREVIEW: ${{ secrets.KINTONE_TEST_APP_ID_FOR_GET_PLUGINS_PREVIEW }} + KINTONE_TEST_APP_ID_FOR_GET_FORM_LAYOUT_PREVIEW: ${{ secrets.KINTONE_TEST_APP_ID_FOR_GET_FORM_LAYOUT_PREVIEW }} + KINTONE_TEST_APP_ID_FOR_PROCESS_MANAGEMENT: ${{ secrets.KINTONE_TEST_APP_ID_FOR_PROCESS_MANAGEMENT }} + KINTONE_TEST_APP_ID_FOR_UPDATE_APP_SETTINGS: ${{ secrets.KINTONE_TEST_APP_ID_FOR_UPDATE_APP_SETTINGS }} + KINTONE_TEST_APP_ID_FOR_PLUGIN: ${{ secrets.KINTONE_TEST_APP_ID_FOR_PLUGIN }} + KINTONE_BASIC_USER: ${{ secrets.KINTONE_BASIC_USER }} + KINTONE_BASIC_PASS: ${{ secrets.KINTONE_BASIC_PASS }} + steps: + - uses: actions/checkout@v4 + + - name: Set up JDK 8 + uses: actions/setup-java@v4 + with: + distribution: corretto + java-version: 8 + cache: gradle + + - name: Grant execute permission + run: | + chmod +x gradlew + chmod +x e2e-tests/gradlew + chmod +x e2e-tests/bin/*.sh + + - name: Build and start proxy container with auth + run: | + docker build -t java-sdk-test-proxy e2e-tests/docker/proxy + docker run -d --name squid-proxy-auth -p 3128:3128 \ + -e proxy_auth=basic \ + -e proxy_user=user1 \ + -e proxy_pass=password1 \ + java-sdk-test-proxy + for i in $(seq 1 30); do + if nc -z localhost 3128; then + echo "Proxy is ready" + break + fi + echo "Waiting for proxy... ($i/30)" + sleep 1 + done + + - name: Run SmokeTest via authenticated proxy + run: | + cd e2e-tests + ./bin/run-test.sh --tests SmokeTest + env: + KINTONE_PROXY_URL: http://localhost:3128 + KINTONE_PROXY_USER: user1 + KINTONE_PROXY_PASS: password1 + + - name: Stop proxy container + run: docker rm -f squid-proxy-auth || true + if: always() diff --git a/.gitignore b/.gitignore index f3814e3..bb40167 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,12 @@ build .idea .mise.toml.local + +e2e-tests/.env +e2e-tests/build/ +e2e-tests/bin/test/ +e2e-tests/*.jar +e2e-tests/.classpath +e2e-tests/.project +e2e-tests/.settings/ + diff --git a/e2e-tests/.env.example b/e2e-tests/.env.example new file mode 100644 index 0000000..3d1e51e --- /dev/null +++ b/e2e-tests/.env.example @@ -0,0 +1,59 @@ +# kintone E2E Test Environment Variables +# Copy this file to .env and fill in the values + +# Java Home (must match java command version, required for Gradle) +# Uncomment and adjust path if you have JAVA_HOME mismatch issues +# export JAVA_HOME=/path/to/java8 + +# kintone Base URL +export KINTONE_BASE_URL=https://example.cybozu.com + +# Default User +export KINTONE_DEFAULT_USER= +export KINTONE_DEFAULT_PASSWORD= + +# Test User +export KINTONE_TEST_USER= +export KINTONE_TEST_PASSWORD= + +# Space IDs +export KINTONE_SPACE_ID= +export KINTONE_MULTI_THREAD_SPACE_ID= +export KINTONE_MULTI_THREAD_DEFAULT_THREAD_ID= +export KINTONE_GUEST_SPACE_ID= +export KINTONE_TEMPLATE_ID= + +# Test App ID +# See README.md for required field configuration +export KINTONE_TEST_APP_ID= + +# Test App in Space (pre-created app inside a space, for getApp/move tests) +export KINTONE_TEST_SPACE_APP_ID= + +# Test App for Plugins Preview +export KINTONE_TEST_APP_ID_FOR_GET_PLUGINS_PREVIEW= + +# Test App for Form Layout Preview +export KINTONE_TEST_APP_ID_FOR_GET_FORM_LAYOUT_PREVIEW= + +# Test App for Process Management (requires NUMBER_FIELD "数値", process management disabled initially) +export KINTONE_TEST_APP_ID_FOR_PROCESS_MANAGEMENT= + +# Test App for updateAppSettings tests (app name/icon will be changed, empty app is fine) +export KINTONE_TEST_APP_ID_FOR_UPDATE_APP_SETTINGS= + +# Test App for Plugin tests (plugins will be added/removed, empty app is fine) +export KINTONE_TEST_APP_ID_FOR_PLUGIN= + +# Optional: Basic Auth (if enabled) +# export KINTONE_BASIC_USER= +# export KINTONE_BASIC_PASS= + +# Optional: Client Certificate (if required) +# export KINTONE_CLIENT_CERT=/path/to/cert.p12 +# export KINTONE_CLIENT_CERT_PASS= + +# Optional: Proxy (if required) +# export KINTONE_PROXY_URL=http://proxy.example.com:8080 +# export KINTONE_PROXY_USER= +# export KINTONE_PROXY_PASS= diff --git a/e2e-tests/.gitignore b/e2e-tests/.gitignore new file mode 100644 index 0000000..b5651fc --- /dev/null +++ b/e2e-tests/.gitignore @@ -0,0 +1,8 @@ +.gradle +.idea +/build/ +/bin/test/ +/client/ +.env +kintone-java-client.jar + diff --git a/e2e-tests/README.md b/e2e-tests/README.md new file mode 100644 index 0000000..df4eabd --- /dev/null +++ b/e2e-tests/README.md @@ -0,0 +1,114 @@ +# kintone-java-client E2E Tests + +End-to-end tests that send actual requests to a kintone environment using kintone-java-client. + +## How to Run + +Tests are JUnit tests and can be run from IntelliJ or via `bin/run-test.sh` (a wrapper for gradlew test). + +### Running from IntelliJ + +- Run test classes or methods as usual +- Set the `KINTONE_BASE_URL` environment variable to specify the target kintone environment + +### Using bin/run-test.sh + +`run-test.sh` automatically builds the root project (kintone-java-client) and runs E2E tests. + +```bash +# Basic usage (using .env file) +$ source .env && ./bin/run-test.sh + +# Run specific tests only +$ source .env && ./bin/run-test.sh --tests SmokeTest + +# Skip client build (when jar already exists) +$ SKIP_CLIENT_BUILD=true ./bin/run-test.sh +``` + +#### Environment Variables + +Copy `.env.example` to `.env` and set the required values. + +| Variable | Description | +|----------|-------------| +| `KINTONE_BASE_URL` | URL of the kintone environment to test against | +| `KINTONE_DEFAULT_USER` | Login name for the default user | +| `KINTONE_DEFAULT_PASSWORD` | Password for the default user | +| `KINTONE_TEST_USER` | Login name for the test user | +| `KINTONE_TEST_PASSWORD` | Password for the test user | +| `KINTONE_SPACE_ID`, etc. | See `.env.example` for details | +| `KINTONE_TEST_APP_ID` | Pre-created app for E2E tests (see below) | + +#### Basic Authentication and Client Certificates + +If the domain has Basic authentication or client certificates enabled, set the following environment variables: + +| Variable | Description | +|----------|-------------| +| `KINTONE_BASIC_USER` | Basic authentication username | +| `KINTONE_BASIC_PASS` | Basic authentication password | +| `KINTONE_CLIENT_CERT` | Path to client certificate file | +| `KINTONE_CLIENT_CERT_PASS` | Client certificate password | + +### Proxy Tests + +Both IntelliJ and `run-test.sh` support proxy configuration via the `KINTONE_PROXY_URL` environment variable. + +A Squid container for testing is available in `docker/proxy/`. + +```bash +# Build and run the proxy server +$ docker build -t test-proxy docker/proxy +$ docker run --rm -p3128:3128 test-proxy + +# (In another terminal) Run tests via proxy +$ KINTONE_PROXY_URL=http://localhost:3128 \ + KINTONE_BASE_URL=https://example.cybozu.com ./bin/run-test.sh +``` + +Stop the proxy server with `Ctrl+C`. + +#### Authenticated Proxy Tests + +To enable Basic authentication on the proxy, pass `-e proxy_auth=basic`, `-e proxy_user=`, and `-e proxy_pass=` to `docker run`. + +```bash +# Start proxy with Basic authentication +$ docker run --rm -p3128:3128 -e proxy_auth=basic \ + -e proxy_user=user1 -e proxy_pass=password1 -it test-proxy + +# Run tests with proxy credentials +$ KINTONE_PROXY_URL=http://localhost:3128 \ + KINTONE_PROXY_USER=user1 \ + KINTONE_PROXY_PASS=password1 \ + KINTONE_BASE_URL=https://example.cybozu.com ./bin/run-test.sh +``` + +Setting `proxy_auth=digest` enables Digest authentication. Since kintone-java-client only supports Basic authentication explicitly, Digest auth is used to verify that it *does not work*. + +## Package Structure + +Tests are organized into the following packages: + +``` +com.kintone.client + + app: Tests for AppClient APIs + + bulk: Tests for bulkRequest API + + file: Tests for FileClient APIs + + record: Tests for RecordClient APIs + + schema: Tests for SchemaClient APIs + + space: Tests for SpaceClient APIs + + plugin: Tests for PluginClient APIs + + scenarios: Scenario tests combining multiple APIs + + helper: Helper classes for test setup (app/space operations) +``` + +### Client-specific Packages (app, record, etc.) + +These packages contain tests for individual API operations. + +Classes like `app.AppApiTest` and `record.RecordApiTest` are provided. As tests grow, they may be split into multiple files within each package. + +Only APIs that take `*Request` objects (e.g., `AddRecordRequest`) and return `*ResponseBody` objects (e.g., `AddRecordResponseBody`) are tested here. Other variations (taking appId or recordId directly) are covered by unit tests for request construction. + diff --git a/e2e-tests/bin/run-test.sh b/e2e-tests/bin/run-test.sh new file mode 100755 index 0000000..c13f772 --- /dev/null +++ b/e2e-tests/bin/run-test.sh @@ -0,0 +1,66 @@ +#!/bin/bash +# +# kintone-java-clientをビルドしてE2Eテストを実行する +# +# * 実行時に指定する環境変数 +# 必須: +# KINTONE_BASE_URL - テストに使うkintoneのURL +# KINTONE_DEFAULT_USER / KINTONE_DEFAULT_PASSWORD - デフォルトユーザー認証情報 +# KINTONE_TEST_USER / KINTONE_TEST_PASSWORD - テストユーザー認証情報 +# KINTONE_SPACE_ID 等 - 詳細は .env.example を参照 +# +# オプション: +# KINTONE_PROXY_URL - プロキシサーバーのURL +# KINTONE_BASIC_USER / KINTONE_BASIC_PASS - Basic認証 +# SKIP_CLIENT_BUILD - "true"を設定するとクライアントのビルドをスキップ +# +# * 実行例 +# - .envファイルを使ってテスト実行 +# $ source .env && ./bin/run-test.sh +# +# - 特定のテストのみ実行 +# $ source .env && ./bin/run-test.sh --tests SmokeTest +# +# - クライアントビルドをスキップ(jarがすでにある場合) +# $ SKIP_CLIENT_BUILD=true ./bin/run-test.sh + +set -e + +KINTONE_BASE_URL=${KINTONE_BASE_URL:-http://localhost} +KINTONE_BASE_URL=$(echo "$KINTONE_BASE_URL" | sed -e 's|/\+$||') +KINTONE_PROXY_URL=${KINTONE_PROXY_URL:-""} +BASE_DIR=$(cd "$(dirname "$0")"/../; pwd) +CLIENT_DIR=$(cd "$BASE_DIR"/.. ; pwd) + +echo "=== E2E Test Configuration ===" +echo "KINTONE_BASE_URL=$KINTONE_BASE_URL" +echo "KINTONE_DEFAULT_USER=$KINTONE_DEFAULT_USER" +echo "KINTONE_TEST_USER=$KINTONE_TEST_USER" +echo "BASE_DIR=$BASE_DIR" +echo "CLIENT_DIR=$CLIENT_DIR" +echo "SKIP_CLIENT_BUILD=${SKIP_CLIENT_BUILD:-false}" +echo + +cd "$BASE_DIR" + +# テスト結果を消す +rm -fr "build/test-results/test" + +# kintone-java-clientをビルド(スキップ指定がない場合) +if [ "${SKIP_CLIENT_BUILD}" != "true" ]; then + echo "=== Building kintone-java-client ===" + cd "$CLIENT_DIR" + ./gradlew ${GRADLE_OPTS} clean jar + echo + + echo "=== Copying built jar ===" + cd "$BASE_DIR" + rm -f kintone-java-client.jar + cp "$CLIENT_DIR"/build/libs/kintone-java-client-*.jar kintone-java-client.jar + ls -la kintone-java-client.jar + echo +fi + +# 環境変数をそのまま継承してgradleを実行 +echo "=== Running E2E tests ===" +./gradlew ${GRADLE_OPTS} clean test "$@" diff --git a/e2e-tests/bin/update-client-jar.sh b/e2e-tests/bin/update-client-jar.sh new file mode 100755 index 0000000..002ff28 --- /dev/null +++ b/e2e-tests/bin/update-client-jar.sh @@ -0,0 +1,16 @@ +#!/bin/bash +# +# ../kintone-java-clientをビルドし、作成されたjarをE2Eテストで使う用にコピーする +# +# * 実行例 +# $ ./bin/update-client-jar.sh + +BASE_DIR=$(cd $(dirname "$0")/../; pwd) + +echo "### ../kintone-java-client をビルド ###" +cd "$BASE_DIR/../kintone-java-client" +./gradlew clean jar + +echo "### ビルド結果をコピー ###" +cd "$BASE_DIR" +cp ../kintone-java-client/build/libs/kintone-java-client-*.jar kintone-java-client.jar diff --git a/e2e-tests/build.gradle b/e2e-tests/build.gradle new file mode 100644 index 0000000..f2b6758 --- /dev/null +++ b/e2e-tests/build.gradle @@ -0,0 +1,52 @@ +plugins { + id 'java' + id 'com.diffplug.spotless' version '6.13.0' // Java 8を使うので6.13.0にしておく + id 'com.github.hierynomus.license' version '0.16.1' +} + +group 'com.kintone' +version '1.0-SNAPSHOT' + +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + +repositories { + mavenCentral() +} + +dependencies { + implementation files('kintone-java-client.jar') + implementation 'org.apache.httpcomponents.client5:httpclient5:5.3.1' + implementation 'com.fasterxml.jackson.core:jackson-databind:2.17.1' + implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.17.1' + + testCompileOnly 'org.projectlombok:lombok:1.18.34' + testAnnotationProcessor 'org.projectlombok:lombok:1.18.34' + + testImplementation 'ch.qos.logback:logback-classic:1.3.14' // Java 8を使うので1.3.xにしておく + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.10.3' + testImplementation 'org.assertj:assertj-core:3.26.0' + + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.10.3' + testRuntimeOnly 'mysql:mysql-connector-java:8.0.33' +} + +spotless { + java { + googleJavaFormat() + indentWithTabs(2) + indentWithSpaces(4) + removeUnusedImports() + trimTrailingWhitespace() + endWithNewline() + } +} + +test { + useJUnitPlatform() + jvmArgs '-Duser.timezone=Asia/Tokyo' +} + +downloadLicenses { + dependencyConfiguration = "testRuntimeClasspath" +} diff --git a/e2e-tests/docker/proxy/Dockerfile b/e2e-tests/docker/proxy/Dockerfile new file mode 100644 index 0000000..82c06f6 --- /dev/null +++ b/e2e-tests/docker/proxy/Dockerfile @@ -0,0 +1,24 @@ +# プロキシ経由の動作を確認するためのプロキシサーバー +# ビルド方法は build-and-push.sh を参照 +FROM ubuntu:18.04 + +ENV DEBIAN_FRONTEND=noninteractive + +RUN apt-get update && \ + apt-get install --no-install-recommends -y squid apache2-utils && \ + apt-get clean && rm -rf /var/lib/apt/lists/* + +COPY squid.conf /etc/squid/squid.conf +COPY squid_basic_auth.conf /etc/squid/squid_basic_auth.conf +COPY squid_digest_auth.conf /etc/squid/squid_digest_auth.conf +COPY start.sh /start.sh + +RUN adduser --quiet squid && \ + mkdir -p /var/squid && \ + chown squid:squid /var/squid + +USER squid + +EXPOSE 3128 + +CMD ["/start.sh"] diff --git a/e2e-tests/docker/proxy/squid.conf b/e2e-tests/docker/proxy/squid.conf new file mode 100644 index 0000000..032ed9b --- /dev/null +++ b/e2e-tests/docker/proxy/squid.conf @@ -0,0 +1,17 @@ +acl SSL_ports port 443 +acl Safe_ports port 80 # http +acl Safe_ports port 443 # https +acl CONNECT method CONNECT +http_access deny !Safe_ports +http_access deny CONNECT !SSL_ports +http_access allow localhost manager +http_access deny manager +http_access allow all # allow access from outside of container +http_port 3128 +coredump_dir /var/spool/squid +refresh_pattern . 0 0% 0 # don't cache +request_entities on # allow GET and HEAD requests with request entities +pid_filename /var/squid/squid.pid +logfile_rotate 0 +cache_log stdio:/dev/stdout +access_log stdio:/dev/stdout diff --git a/e2e-tests/docker/proxy/squid_basic_auth.conf b/e2e-tests/docker/proxy/squid_basic_auth.conf new file mode 100644 index 0000000..17e4707 --- /dev/null +++ b/e2e-tests/docker/proxy/squid_basic_auth.conf @@ -0,0 +1,24 @@ +auth_param basic program /usr/lib/squid/basic_ncsa_auth /var/squid/passwords +auth_param basic children 5 +auth_param basic credentialsttl 1 minute +acl auth proxy_auth REQUIRED + +acl SSL_ports port 443 +acl Safe_ports port 80 # http +acl Safe_ports port 443 # https +acl CONNECT method CONNECT + +http_access deny !Safe_ports +http_access deny CONNECT !SSL_ports +http_access allow localhost manager +http_access deny manager +http_access allow auth # allow access with authentication +http_access deny all +http_port 3128 +coredump_dir /var/spool/squid +refresh_pattern . 0 0% 0 # don't cache +request_entities on # allow GET and HEAD requests with request entities +pid_filename /var/squid/squid.pid +logfile_rotate 0 +cache_log stdio:/dev/stdout +access_log stdio:/dev/stdout diff --git a/e2e-tests/docker/proxy/squid_digest_auth.conf b/e2e-tests/docker/proxy/squid_digest_auth.conf new file mode 100644 index 0000000..68ff9d4 --- /dev/null +++ b/e2e-tests/docker/proxy/squid_digest_auth.conf @@ -0,0 +1,27 @@ +auth_param digest program /usr/lib/squid/digest_file_auth /var/squid/passwords +auth_param digest children 5 +auth_param digest realm Squid proxy-caching web server +auth_param digest nonce_garbage_interval 5 minutes +auth_param digest nonce_max_duration 30 minutes +auth_param digest nonce_max_count 50 +acl auth proxy_auth REQUIRED + +acl SSL_ports port 443 +acl Safe_ports port 80 # http +acl Safe_ports port 443 # https +acl CONNECT method CONNECT + +http_access deny !Safe_ports +http_access deny CONNECT !SSL_ports +http_access allow localhost manager +http_access deny manager +http_access allow auth # allow access with authentication +http_access deny all +http_port 3128 +coredump_dir /var/spool/squid +refresh_pattern . 0 0% 0 # don't cache +request_entities on # allow GET and HEAD requests with request entities +pid_filename /var/squid/squid.pid +logfile_rotate 0 +cache_log stdio:/dev/stdout +access_log stdio:/dev/stdout diff --git a/e2e-tests/docker/proxy/start.sh b/e2e-tests/docker/proxy/start.sh new file mode 100755 index 0000000..adde07a --- /dev/null +++ b/e2e-tests/docker/proxy/start.sh @@ -0,0 +1,14 @@ +#!/bin/sh + +opt= +if [ "${proxy_auth}" = "basic" ]; then + htpasswd -c -b /var/squid/passwords "${proxy_user}" "${proxy_pass}" + opt="-N -f /etc/squid/squid_basic_auth.conf" +elif [ "${proxy_auth}" = "digest" ]; then + echo "${proxy_user}:${proxy_pass}" > /var/squid/passwords + opt="-N -f /etc/squid/squid_digest_auth.conf" +else + opt="-N" +fi + +exec /usr/sbin/squid $opt diff --git a/e2e-tests/gradle/wrapper/gradle-wrapper.jar b/e2e-tests/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..7454180 Binary files /dev/null and b/e2e-tests/gradle/wrapper/gradle-wrapper.jar differ diff --git a/e2e-tests/gradle/wrapper/gradle-wrapper.properties b/e2e-tests/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..2e6e589 --- /dev/null +++ b/e2e-tests/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/e2e-tests/gradlew b/e2e-tests/gradlew new file mode 100755 index 0000000..1b6c787 --- /dev/null +++ b/e2e-tests/gradlew @@ -0,0 +1,234 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# 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 +# +# https://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. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/e2e-tests/gradlew.bat b/e2e-tests/gradlew.bat new file mode 100644 index 0000000..ac1b06f --- /dev/null +++ b/e2e-tests/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/e2e-tests/settings.gradle b/e2e-tests/settings.gradle new file mode 100644 index 0000000..5b84606 --- /dev/null +++ b/e2e-tests/settings.gradle @@ -0,0 +1,2 @@ +rootProject.name = 'e2e-tests' + diff --git a/e2e-tests/src/test/java/com/kintone/client/ApiTestBase.java b/e2e-tests/src/test/java/com/kintone/client/ApiTestBase.java new file mode 100644 index 0000000..98e22fa --- /dev/null +++ b/e2e-tests/src/test/java/com/kintone/client/ApiTestBase.java @@ -0,0 +1,112 @@ +package com.kintone.client; + +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.file.FileSystems; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; + +public class ApiTestBase { + + private static final int TIMEOUT_MS = 60000; + + private final List clients = new ArrayList<>(); + + private CloseableHttpClient httpClient; + + @BeforeEach + public void setup() { + httpClient = null; + } + + @AfterEach + public void cleanup() throws Exception { + for (KintoneClient client : clients) { + client.close(); + } + clients.clear(); + + if (httpClient != null) { + httpClient.close(); + httpClient = null; + } + } + + public KintoneClient setupDefaultClient() { + return setupDefaultClient(null); + } + + public KintoneClient setupDefaultClient(Long guestSpaceId) { + KintoneClientBuilder builder = + KintoneClientBuilder.create(getBaseURL()) + .authByPassword(getDefaultUser(), getDefaultUserPassword()) + .setConnectionRequestTimeout(TIMEOUT_MS) + .setConnectionTimeout(TIMEOUT_MS) + .setSocketTimeout(TIMEOUT_MS); + if (guestSpaceId != null) { + builder.setGuestSpaceId(guestSpaceId); + } + + final TestSettings settings = getSettings(); + if (!settings.getBasicAuthUser().isEmpty()) { + builder.withBasicAuth(settings.getBasicAuthUser(), settings.getBasicAuthPass()); + } + if (!settings.getClientCertPath().isEmpty()) { + Path path = FileSystems.getDefault().getPath(settings.getClientCertPath()); + builder.withClientCertificate(path, settings.getClientCertPass()); + } + getProxyURL() + .ifPresent( + proxy -> { + builder.withProxy(proxy.getScheme(), proxy.getHost(), proxy.getPort()); + if (!settings.getProxyUser().isEmpty()) { + builder.setProxyAuthentication( + settings.getProxyUser(), settings.getProxyPassword()); + } + }); + KintoneClient client = builder.build(); + clients.add(client); + return client; + } + + public CloseableHttpClient getHttpClient() { + if (httpClient == null) { + httpClient = getSettings().createHttpClient(); + } + return httpClient; + } + + public TestSettings getSettings() { + return TestSettings.get(); + } + + public String getBaseURL() { + return getSettings().getBaseUrl(); + } + + public Optional getProxyURL() { + String proxyUrl = getSettings().getProxyUrl(); + if (proxyUrl == null || proxyUrl.isEmpty()) { + return Optional.empty(); + } + try { + URI uri = new URI(proxyUrl); + return Optional.of(uri); + } catch (URISyntaxException e) { + throw new IllegalArgumentException(e); + } + } + + public String getDefaultUser() { + return getSettings().getDefaultUser().getCode(); + } + + public String getDefaultUserPassword() { + return getSettings().getDefaultUser().getPassword(); + } +} diff --git a/e2e-tests/src/test/java/com/kintone/client/GroupSetting.java b/e2e-tests/src/test/java/com/kintone/client/GroupSetting.java new file mode 100644 index 0000000..48745bb --- /dev/null +++ b/e2e-tests/src/test/java/com/kintone/client/GroupSetting.java @@ -0,0 +1,20 @@ +package com.kintone.client; + +import com.kintone.client.model.Entity; +import com.kintone.client.model.EntityType; +import lombok.Getter; + +@Getter +public class GroupSetting { + private final String code; + private final String name; + + public GroupSetting(String code, String name) { + this.code = code; + this.name = name; + } + + public Entity toEntity() { + return new Entity(EntityType.GROUP, this.code); + } +} diff --git a/e2e-tests/src/test/java/com/kintone/client/Groups.java b/e2e-tests/src/test/java/com/kintone/client/Groups.java new file mode 100644 index 0000000..ee1a79f --- /dev/null +++ b/e2e-tests/src/test/java/com/kintone/client/Groups.java @@ -0,0 +1,5 @@ +package com.kintone.client; + +public class Groups { + public static final GroupSetting everyone = new GroupSetting("everyone", "Everyone"); +} diff --git a/e2e-tests/src/test/java/com/kintone/client/OrgSetting.java b/e2e-tests/src/test/java/com/kintone/client/OrgSetting.java new file mode 100644 index 0000000..df5eda7 --- /dev/null +++ b/e2e-tests/src/test/java/com/kintone/client/OrgSetting.java @@ -0,0 +1,22 @@ +package com.kintone.client; + +import com.kintone.client.model.Entity; +import com.kintone.client.model.EntityType; +import lombok.Getter; + +@Getter +public class OrgSetting { + private final String code; + private final String name; + private final String parent; + + public OrgSetting(String code, String name, String parent) { + this.code = code; + this.name = name; + this.parent = parent; + } + + public Entity toEntity() { + return new Entity(EntityType.ORGANIZATION, this.code); + } +} diff --git a/e2e-tests/src/test/java/com/kintone/client/Orgs.java b/e2e-tests/src/test/java/com/kintone/client/Orgs.java new file mode 100644 index 0000000..a6c0522 --- /dev/null +++ b/e2e-tests/src/test/java/com/kintone/client/Orgs.java @@ -0,0 +1,6 @@ +package com.kintone.client; + +public class Orgs { + public static final OrgSetting org1 = new OrgSetting("org1", "組織1", null); + public static final OrgSetting org2 = new OrgSetting("org2", "組織2", null); +} diff --git a/e2e-tests/src/test/java/com/kintone/client/TestSettings.java b/e2e-tests/src/test/java/com/kintone/client/TestSettings.java new file mode 100644 index 0000000..a571ec4 --- /dev/null +++ b/e2e-tests/src/test/java/com/kintone/client/TestSettings.java @@ -0,0 +1,215 @@ +package com.kintone.client; + +import com.kintone.client.exception.KintoneRuntimeException; +import java.io.FileInputStream; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.security.GeneralSecurityException; +import java.security.KeyStore; +import java.util.Collections; +import java.util.Objects; +import java.util.concurrent.TimeUnit; +import javax.net.ssl.SSLContext; +import lombok.Getter; +import org.apache.hc.client5.http.ContextBuilder; +import org.apache.hc.client5.http.auth.AuthScope; +import org.apache.hc.client5.http.auth.UsernamePasswordCredentials; +import org.apache.hc.client5.http.config.ConnectionConfig; +import org.apache.hc.client5.http.config.RequestConfig; +import org.apache.hc.client5.http.impl.auth.BasicCredentialsProvider; +import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; +import org.apache.hc.client5.http.impl.classic.HttpClientBuilder; +import org.apache.hc.client5.http.impl.classic.HttpClients; +import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager; +import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder; +import org.apache.hc.client5.http.protocol.HttpClientContext; +import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactoryBuilder; +import org.apache.hc.core5.http.HttpHost; +import org.apache.hc.core5.http.ssl.TLS; +import org.apache.hc.core5.pool.PoolConcurrencyPolicy; +import org.apache.hc.core5.pool.PoolReusePolicy; +import org.apache.hc.core5.ssl.SSLContexts; +import org.apache.hc.core5.util.Timeout; + +@Getter +public class TestSettings { + private final String baseUrl; + private final UserSetting defaultUser; + private final UserSetting testUser; + private final String basicAuthUser; + private final String basicAuthPass; + private final String clientCertPath; + private final String clientCertPass; + private final HttpHost proxyHost; + private final String proxyUrl; + private final String proxyUser; + private final String proxyPassword; + + private final boolean localMode; + + private final Long singleThreadSpaceId; + private final Long multiThreadSpaceId; + private final Long multiThreadDefaultThreadId; + private final Long guestSpaceId; + private final Long templateId; + private final Long testAppId; + private final Long testSpaceAppId; + private final Long testAppIdForGetPluginsPreview; + private final Long testAppIdForGetFormLayoutPreview; + private final Long testAppIdForProcessManagement; + private final Long testAppIdForUpdateAppSettings; + private final Long testAppIdForPlugin; + + private static final TestSettings INSTANCE = new TestSettings(); + + public static TestSettings get() { + return INSTANCE; + } + + private TestSettings() { + baseUrl = getEnv("KINTONE_BASE_URL", "http://localhost"); + + String defaultUserCode = getEnv("KINTONE_DEFAULT_USER", "cybozu"); + String defaultUserPassword = getEnv("KINTONE_DEFAULT_PASSWORD", "cybozu"); + defaultUser = new UserSetting(defaultUserCode, defaultUserCode, defaultUserPassword); + + String testUserCode = getEnv("KINTONE_TEST_USER", "user1"); + String testUserPassword = getEnv("KINTONE_TEST_PASSWORD", "user1"); + testUser = new UserSetting(testUserCode, testUserCode, testUserPassword); + + basicAuthUser = getEnv("KINTONE_BASIC_USER", ""); + basicAuthPass = getEnv("KINTONE_BASIC_PASS", ""); + clientCertPath = getEnv("KINTONE_CLIENT_CERT", ""); + clientCertPass = getEnv("KINTONE_CLIENT_CERT_PASS", ""); + proxyUrl = getEnv("KINTONE_PROXY_URL", ""); + proxyUser = getEnv("KINTONE_PROXY_USER", ""); + proxyPassword = getEnv("KINTONE_PROXY_PASS", ""); + localMode = initIsLocal(baseUrl); + + singleThreadSpaceId = getLongEnv("KINTONE_SPACE_ID"); + multiThreadSpaceId = getLongEnv("KINTONE_MULTI_THREAD_SPACE_ID"); + multiThreadDefaultThreadId = getLongEnv("KINTONE_MULTI_THREAD_DEFAULT_THREAD_ID"); + guestSpaceId = getLongEnv("KINTONE_GUEST_SPACE_ID"); + templateId = getLongEnv("KINTONE_TEMPLATE_ID"); + testAppId = getLongEnv("KINTONE_TEST_APP_ID"); + testSpaceAppId = getLongEnv("KINTONE_TEST_SPACE_APP_ID"); + testAppIdForGetPluginsPreview = getLongEnv("KINTONE_TEST_APP_ID_FOR_GET_PLUGINS_PREVIEW"); + testAppIdForGetFormLayoutPreview = + getLongEnv("KINTONE_TEST_APP_ID_FOR_GET_FORM_LAYOUT_PREVIEW"); + testAppIdForProcessManagement = getLongEnv("KINTONE_TEST_APP_ID_FOR_PROCESS_MANAGEMENT"); + testAppIdForUpdateAppSettings = getLongEnv("KINTONE_TEST_APP_ID_FOR_UPDATE_APP_SETTINGS"); + testAppIdForPlugin = getLongEnv("KINTONE_TEST_APP_ID_FOR_PLUGIN"); + + proxyHost = Objects.equals(proxyUrl, "") ? null : createProxyHost(URI.create(proxyUrl)); + } + + private static boolean initIsLocal(String baseUrl) { + // 環境変数 KINTONE_IS_LOCALがtrueかbaseUrlがlocalhostならローカル実行とする + String env = System.getenv("KINTONE_IS_LOCAL"); + if (env != null && env.equals("true")) { + return true; + } + return baseUrl.startsWith("http://localhost"); + } + + private static String getEnv(String name, String defaultValue) { + String value = System.getenv(name); + return (value != null && !value.isEmpty()) ? value : defaultValue; + } + + private static Long getLongEnv(String name) { + String value = System.getenv(name); + if (value == null || value.isEmpty()) { + return null; + } + return Long.parseLong(value); + } + + public CloseableHttpClient createHttpClient() { + final Timeout timeout = Timeout.of(60000, TimeUnit.MILLISECONDS); + + ConnectionConfig connectionConfig = + ConnectionConfig.custom().setConnectTimeout(timeout).setSocketTimeout(timeout).build(); + + RequestConfig.Builder configBuilder = RequestConfig.custom(); + configBuilder.setConnectionRequestTimeout(timeout); + if (proxyHost != null) { + configBuilder.setProxyPreferredAuthSchemes(Collections.singleton("basic")); + } + + SSLConnectionSocketFactoryBuilder sslConfigBuilder = SSLConnectionSocketFactoryBuilder.create(); + sslConfigBuilder.setTlsVersions(TLS.V_1_3, TLS.V_1_2); + if (!clientCertPath.isEmpty()) { + SSLContext sslContext = createSSLContext(clientCertPath, clientCertPass); + sslConfigBuilder.setSslContext(sslContext); + } else { + sslConfigBuilder.setSslContext(SSLContexts.createSystemDefault()); + } + + PoolingHttpClientConnectionManager connectionManager = + PoolingHttpClientConnectionManagerBuilder.create() + .setSSLSocketFactory(sslConfigBuilder.build()) + .setDefaultConnectionConfig(connectionConfig) + .setPoolConcurrencyPolicy(PoolConcurrencyPolicy.STRICT) + .setConnPoolPolicy(PoolReusePolicy.LIFO) + .build(); + + HttpClientBuilder clientBuilder = HttpClients.custom(); + if (proxyHost != null) { + clientBuilder.setProxy(proxyHost); + + // proxy認証情報があれば設定 + if (!proxyUser.isEmpty() && !proxyPassword.isEmpty()) { + BasicCredentialsProvider credsProvider = new BasicCredentialsProvider(); + credsProvider.setCredentials( + new AuthScope(proxyHost), + new UsernamePasswordCredentials(proxyUser, proxyPassword.toCharArray())); + clientBuilder.setDefaultCredentialsProvider(credsProvider); + } + } + clientBuilder.setDefaultRequestConfig(configBuilder.build()); + clientBuilder.setConnectionManager(connectionManager); + clientBuilder.disableRedirectHandling(); + return clientBuilder.build(); + } + + private SSLContext createSSLContext(String path, String password) { + char[] pass = password.toCharArray(); + try (FileInputStream stream0 = new FileInputStream(path)) { + KeyStore keyStore = KeyStore.getInstance("PKCS12"); + keyStore.load(stream0, pass); + return SSLContexts.custom().loadKeyMaterial(keyStore, pass).build(); + } catch (IOException | GeneralSecurityException e) { + throw new KintoneRuntimeException("Failed to create ssl context.", e); + } + } + + private static HttpHost createProxyHost(URI proxyHost) { + return proxyHost == null + ? null + : new HttpHost(proxyHost.getScheme(), proxyHost.getHost(), proxyHost.getPort()); + } + + /** + * Basic認証用のヘッダを積んでHTTP リクエストするための{@code HttpClientContext}を生成。
+ * {@code createHttpClient()}で取得した{@code CloseableHttpClient}でリクエストする際にこのコンテキストを設定すること + * + * @return Basic認証用のヘッダを積んでHTTP リクエストするためのコンテキスト + */ + public HttpClientContext createHttpClientContext() { + ContextBuilder builder = ContextBuilder.create(); + if (!basicAuthUser.isEmpty()) { + URI uri = null; + try { + uri = new URI(baseUrl); + } catch (URISyntaxException e) { + throw new RuntimeException(e); + } + HttpHost target = new HttpHost(uri.getScheme(), uri.getHost(), uri.getPort()); + builder.preemptiveBasicAuth( + target, new UsernamePasswordCredentials(basicAuthUser, basicAuthPass.toCharArray())); + } + return builder.build(); + } +} diff --git a/e2e-tests/src/test/java/com/kintone/client/UserSetting.java b/e2e-tests/src/test/java/com/kintone/client/UserSetting.java new file mode 100644 index 0000000..2452ce4 --- /dev/null +++ b/e2e-tests/src/test/java/com/kintone/client/UserSetting.java @@ -0,0 +1,22 @@ +package com.kintone.client; + +import com.kintone.client.model.Entity; +import com.kintone.client.model.EntityType; +import lombok.Getter; + +@Getter +public class UserSetting { + private final String code; + private final String name; + private final String password; + + public UserSetting(String code, String name, String password) { + this.code = code; + this.name = name; + this.password = password; + } + + public Entity toEntity() { + return new Entity(EntityType.USER, this.code); + } +} diff --git a/e2e-tests/src/test/java/com/kintone/client/Users.java b/e2e-tests/src/test/java/com/kintone/client/Users.java new file mode 100644 index 0000000..38a18eb --- /dev/null +++ b/e2e-tests/src/test/java/com/kintone/client/Users.java @@ -0,0 +1,15 @@ +package com.kintone.client; + +public class Users { + public static UserSetting cybozu() { + return TestSettings.get().getDefaultUser(); + } + + public static UserSetting user1() { + return TestSettings.get().getTestUser(); + } + + // 後方互換性のためのフィールド + public static final UserSetting cybozu = cybozu(); + public static final UserSetting user1 = user1(); +} diff --git a/e2e-tests/src/test/java/com/kintone/client/app/ActionsTest.java b/e2e-tests/src/test/java/com/kintone/client/app/ActionsTest.java new file mode 100644 index 0000000..9af112c --- /dev/null +++ b/e2e-tests/src/test/java/com/kintone/client/app/ActionsTest.java @@ -0,0 +1,180 @@ +package com.kintone.client.app; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.kintone.client.*; +import com.kintone.client.api.app.*; +import com.kintone.client.helper.App; +import com.kintone.client.model.Entity; +import com.kintone.client.model.app.*; +import com.kintone.client.model.app.field.FieldProperty; +import com.kintone.client.model.app.field.RelatedApp; +import java.util.*; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** AppClientのactions.jsonのテスト */ +public class ActionsTest extends ApiTestBase { + + private static final String TEXT_FIELD_CODE = "文字列__1行_"; + private static final String LINK_FIELD_CODE = "リンク__URL_"; + + private KintoneClient client; + private App app; + private Map originalActions; + + @BeforeEach + public void setupApp() { + client = setupDefaultClient(); + Long testAppId = TestSettings.get().getTestAppId(); + if (testAppId != null) { + app = App.fromExisting(client, testAppId); + } else { + throw new IllegalStateException( + "KINTONE_TEST_APP_ID is not set. Please create a test app and set the environment variable."); + } + // 元のアクション設定を保存 + originalActions = app.getActions(false); + } + + @AfterEach + public void cleanupActions() { + if (app != null) { + try { + // 元のアクション設定に戻す + client.app().updateAppActions(app.id(), originalActions); + client.app().deployApp(app.id()); + app.waitDeploy(); + } catch (Exception e) { + // ignore cleanup errors + } + } + } + + @Test + public void getAppActions_getAppActionsPreview() { + FieldProperty text = app.field(TEXT_FIELD_CODE); + AppAction action1 = + createAction( + 0L, + "action1", + app.id(), + Collections.singletonList(createFieldMapping(text, text)), + Collections.singletonList(Users.user1.toEntity())); + app.updateActions(action1).deploy(); + long revision = app.getAppRevision(false); + + GetAppActionsRequest req1 = new GetAppActionsRequest(); + req1.setApp(app.id()); + GetAppActionsResponseBody resp1 = client.app().getAppActions(req1); + assertThat(resp1.getRevision()).isEqualTo(revision); + Map actions = resp1.getActions(); + assertThat(actions).containsOnlyKeys("action1"); + assertThat(actions.get("action1")) + .usingRecursiveComparison() + .ignoringFieldsMatchingRegexes("id") + .isEqualTo(action1); + + AppAction action2 = + createAction( + 0L, + "action2", + app.id(), + Collections.singletonList(createFieldMapping(text, text)), + Collections.singletonList(Users.user1.toEntity())); + app.updateActions(Collections.singletonMap("action1", action2)); + + GetAppActionsPreviewRequest req2 = new GetAppActionsPreviewRequest(); + req2.setApp(app.id()); + GetAppActionsPreviewResponseBody resp2 = client.app().getAppActionsPreview(req2); + assertThat(resp2.getRevision()).isEqualTo(revision + 1); + actions = resp2.getActions(); + assertThat(actions).containsOnlyKeys("action2"); + assertThat(actions.get("action2")) + .usingRecursiveComparison() + .ignoringFieldsMatchingRegexes("id") + .isEqualTo(action2); + } + + @Test + public void updateAppActions() { + FieldProperty text = app.field(TEXT_FIELD_CODE); + FieldProperty link = app.field(LINK_FIELD_CODE); + long revision = app.getAppRevision(true); + + AppAction action1 = + createAction( + 0L, + "action1", + app.id(), + Arrays.asList(createRecordURLMapping(link), createFieldMapping(text, text)), + Collections.singletonList(Users.user1.toEntity())); + AppAction action2 = + createAction( + 1L, + "action2", + app.id(), + Collections.singletonList(createFieldMapping(text, text)), + Arrays.asList(Groups.everyone.toEntity(), Orgs.org1.toEntity())); + Map actions = new HashMap<>(); + actions.put("action1", action1); + actions.put("action2", action2); + + UpdateAppActionsRequest req = new UpdateAppActionsRequest(); + req.setApp(app.id()); + req.setActions(actions); + req.setRevision(revision); + UpdateAppActionsResponseBody resp = client.app().updateAppActions(req); + assertThat(resp.getRevision()).isEqualTo(revision + 1); + assertThat(resp.getActions()).hasSize(2); + assertThat(resp.getActions().get("action1").getId()).isGreaterThan(0); + assertThat(resp.getActions().get("action2").getId()).isGreaterThan(0); + updateActionIds(actions, resp.getActions()); + + Map settings = app.getActions(true); + assertThat(settings.get("action1")) + .usingRecursiveComparison() + .isEqualTo(actions.get("action1")); + assertThat(settings.get("action2")) + .usingRecursiveComparison() + .isEqualTo(actions.get("action2")); + } + + private void updateActionIds(Map actions, Map ids) { + for (AppAction action : actions.values()) { + action.setId(ids.get(action.getName()).getId()); + } + } + + private AppAction createAction( + long index, + String name, + long destAppId, + List mappings, + List entities) { + AppAction action = new AppAction(); + action.setName(name); + action.setIndex(index); + action.setDestApp(new RelatedApp().setApp(destAppId).setCode("")); + action.setMappings(mappings); + action.setEntities(entities); + action.setFilterCond(""); + return action; + } + + private AppActionMapping createRecordURLMapping(FieldProperty field) { + AppActionMapping mapping = new AppActionMapping(); + mapping.setSrcType(AppActionSourceType.RECORD_URL); + mapping.setDestField(field.getCode()); + return mapping; + } + + private AppActionMapping createFieldMapping(FieldProperty src, FieldProperty dest) { + AppActionMapping mapping = new AppActionMapping(); + mapping.setSrcType(AppActionSourceType.FIELD); + mapping.setSrcField(src.getCode()); + mapping.setDestField(dest.getCode()); + return mapping; + } +} diff --git a/e2e-tests/src/test/java/com/kintone/client/app/AdminNotesTest.java b/e2e-tests/src/test/java/com/kintone/client/app/AdminNotesTest.java new file mode 100644 index 0000000..728a1aa --- /dev/null +++ b/e2e-tests/src/test/java/com/kintone/client/app/AdminNotesTest.java @@ -0,0 +1,210 @@ +package com.kintone.client.app; + +import static org.assertj.core.api.Assertions.*; + +import com.kintone.client.ApiTestBase; +import com.kintone.client.KintoneClient; +import com.kintone.client.TestSettings; +import com.kintone.client.api.app.GetAdminNotesPreviewRequest; +import com.kintone.client.api.app.GetAdminNotesPreviewResponseBody; +import com.kintone.client.api.app.GetAdminNotesRequest; +import com.kintone.client.api.app.GetAdminNotesResponseBody; +import com.kintone.client.api.app.UpdateAdminNotesRequest; +import com.kintone.client.exception.KintoneApiRuntimeException; +import com.kintone.client.helper.App; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** AppClientの管理者用メモに関するテスト */ +public class AdminNotesTest extends ApiTestBase { + + private KintoneClient client; + private App app; + private String originalContent; + private boolean originalIncludeInTemplateAndDuplicates; + + @BeforeEach + public void setupApp() { + client = setupDefaultClient(); + Long testAppId = TestSettings.get().getTestAppId(); + if (testAppId != null) { + app = App.fromExisting(client, testAppId); + } else { + throw new IllegalStateException( + "KINTONE_TEST_APP_ID is not set. Please create a test app and set the environment variable."); + } + // 元の管理者用メモ設定を保存 + GetAdminNotesRequest req = new GetAdminNotesRequest(); + req.setApp(app.id()); + GetAdminNotesResponseBody resp = client.app().getAdminNotes(req); + originalContent = resp.getContent(); + originalIncludeInTemplateAndDuplicates = resp.isIncludeInTemplateAndDuplicates(); + } + + @AfterEach + public void cleanupAdminNotes() { + if (app != null) { + try { + // 元の管理者用メモ設定に戻す + UpdateAdminNotesRequest req = new UpdateAdminNotesRequest(); + req.setApp(app.id()); + req.setContent(originalContent); + req.setIncludeInTemplateAndDuplicates(originalIncludeInTemplateAndDuplicates); + client.app().updateAdminNotes(req); + client.app().deployApp(app.id()); + app.waitDeploy(); + } catch (Exception e) { + // ignore cleanup errors + } + } + } + + @Test + public void getAdminNotes_getAdminNotesPreview_updateAdminNotes() { + UpdateAdminNotesRequest req = new UpdateAdminNotesRequest(); + String adminNotes1 = "Admin Notes For Get Admin Notes"; + req.setApp(app.id()); + req.setContent(adminNotes1); + req.setIncludeInTemplateAndDuplicates(false); + client.app().updateAdminNotes(req); + + client.app().deployApp(app.id()); + app.waitDeploy(); + long revision = app.getAppRevision(true); + + // previewの管理者用メモを更新 + String adminNotes2 = "Admin Notes For Get Admin Notes Preview"; + req.setApp(app.id()); + req.setContent(adminNotes2); + req.setIncludeInTemplateAndDuplicates(true); + long updatedRevision = client.app().updateAdminNotes(req).getRevision(); + + GetAdminNotesRequest req1 = new GetAdminNotesRequest(); + req1.setApp(app.id()); + GetAdminNotesResponseBody resp1 = client.app().getAdminNotes(req1); + assertThat(resp1.getContent()).isEqualTo(adminNotes1); + assertThat(resp1.isIncludeInTemplateAndDuplicates()).isFalse(); + assertThat(resp1.getRevision()).isEqualTo(revision); + + GetAdminNotesPreviewRequest req2 = new GetAdminNotesPreviewRequest(); + req2.setApp(app.id()); + GetAdminNotesPreviewResponseBody resp2 = client.app().getAdminNotesPreview(req2); + assertThat(resp2.getContent()).isEqualTo(adminNotes2); + assertThat(resp2.isIncludeInTemplateAndDuplicates()).isTrue(); + assertThat(resp2.getRevision()).isEqualTo(updatedRevision); + } + + @Test + public void getAdminNotes_getAdminNotesPreview_1() { + // 管理者用メモが1文字以上あり、アプリテンプレートが含まれる場合 + UpdateAdminNotesRequest updateReq = new UpdateAdminNotesRequest(); + String adminNotesContent = "
アプリの管理者用メモ
"; + updateReq.setApp(app.id()); + updateReq.setContent(adminNotesContent); + updateReq.setIncludeInTemplateAndDuplicates(true); + client.app().updateAdminNotes(updateReq); + client.app().deployApp(app.id()); + app.waitDeploy(); + + GetAdminNotesRequest req = new GetAdminNotesRequest(); + req.setApp(app.id()); + GetAdminNotesResponseBody resp = client.app().getAdminNotes(req); + assertThat(resp.getContent()).isEqualTo(adminNotesContent); + assertThat(resp.isIncludeInTemplateAndDuplicates()).isTrue(); + + GetAdminNotesPreviewRequest previewReq = new GetAdminNotesPreviewRequest(); + previewReq.setApp(app.id()); + GetAdminNotesPreviewResponseBody previewResp = client.app().getAdminNotesPreview(previewReq); + assertThat(previewResp.getContent()).isEqualTo(adminNotesContent); + assertThat(previewResp.isIncludeInTemplateAndDuplicates()).isTrue(); + } + + @Test + public void getAdminNotes_getAdminNotesPreview_2() { + // 管理者用メモが空で、アプリテンプレートに含まれない場合 + UpdateAdminNotesRequest updateReq = new UpdateAdminNotesRequest(); + updateReq.setApp(app.id()); + updateReq.setContent(""); + updateReq.setIncludeInTemplateAndDuplicates(false); + client.app().updateAdminNotes(updateReq); + client.app().deployApp(app.id()); + app.waitDeploy(); + + GetAdminNotesRequest req = new GetAdminNotesRequest(); + req.setApp(app.id()); + GetAdminNotesResponseBody resp = client.app().getAdminNotes(req); + assertThat(resp.getContent()).isEqualTo(""); + assertThat(resp.isIncludeInTemplateAndDuplicates()).isFalse(); + + GetAdminNotesPreviewRequest previewReq = new GetAdminNotesPreviewRequest(); + previewReq.setApp(app.id()); + GetAdminNotesPreviewResponseBody previewResp = client.app().getAdminNotesPreview(previewReq); + assertThat(previewResp.getContent()).isEqualTo(""); + assertThat(previewResp.isIncludeInTemplateAndDuplicates()).isFalse(); + } + + @Test + public void getAdminNotes_getAdminNotesPreview_3() { + // 必須パラメータがない場合エラーとなる + GetAdminNotesRequest req = new GetAdminNotesRequest(); + assertThatThrownBy(() -> client.app().getAdminNotes(req)) + .isInstanceOf(KintoneApiRuntimeException.class); + + GetAdminNotesPreviewRequest previewReq = new GetAdminNotesPreviewRequest(); + assertThatThrownBy(() -> client.app().getAdminNotesPreview(previewReq)) + .isInstanceOf(KintoneApiRuntimeException.class); + } + + @Test + public void updateAdminNotes_1() { + // 必須パラメータのみで管理者用メモが更新できる + UpdateAdminNotesRequest updateReq = new UpdateAdminNotesRequest(); + updateReq.setApp(app.id()); + client.app().updateAdminNotes(updateReq); + + GetAdminNotesPreviewRequest req = new GetAdminNotesPreviewRequest(); + req.setApp(app.id()); + GetAdminNotesPreviewResponseBody resp = client.app().getAdminNotesPreview(req); + assertThat(resp.getContent()).isEqualTo(""); + assertThat(resp.isIncludeInTemplateAndDuplicates()).isFalse(); + } + + @Test + public void updateAdminNotes_2() { + // contentが0文字、revisionが-1で実行できること + // アプリテンプレートに含むかの項目を変更できること + UpdateAdminNotesRequest preUpdateReq = new UpdateAdminNotesRequest(); + preUpdateReq.setApp(app.id()); + preUpdateReq.setContent("updateAdminNotes_2"); + client.app().updateAdminNotes(preUpdateReq); + + // 管理者用メモの事前設定 + GetAdminNotesPreviewRequest preGetReq = new GetAdminNotesPreviewRequest(); + preGetReq.setApp(app.id()); + assertThat(client.app().getAdminNotesPreview(preGetReq).getContent()) + .isEqualTo("updateAdminNotes_2"); + + // 管理者用メモの更新 + UpdateAdminNotesRequest updateReq = new UpdateAdminNotesRequest(); + updateReq.setApp(app.id()); + updateReq.setContent(""); + updateReq.setIncludeInTemplateAndDuplicates(true); + updateReq.setRevision(-1L); + client.app().updateAdminNotes(updateReq); + + GetAdminNotesPreviewRequest getReq = new GetAdminNotesPreviewRequest(); + getReq.setApp(app.id()); + GetAdminNotesPreviewResponseBody resp = client.app().getAdminNotesPreview(getReq); + assertThat(resp.getContent()).isEqualTo(""); + assertThat(resp.isIncludeInTemplateAndDuplicates()).isTrue(); + } + + @Test + public void updateAdminNotes_3() { + // 必須パラメータがない場合エラーとなる + UpdateAdminNotesRequest updateReq = new UpdateAdminNotesRequest(); + assertThatThrownBy(() -> client.app().updateAdminNotes(updateReq)) + .isInstanceOf(KintoneApiRuntimeException.class); + } +} diff --git a/e2e-tests/src/test/java/com/kintone/client/app/AppAclTest.java b/e2e-tests/src/test/java/com/kintone/client/app/AppAclTest.java new file mode 100644 index 0000000..6870f89 --- /dev/null +++ b/e2e-tests/src/test/java/com/kintone/client/app/AppAclTest.java @@ -0,0 +1,142 @@ +package com.kintone.client.app; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.kintone.client.ApiTestBase; +import com.kintone.client.KintoneClient; +import com.kintone.client.TestSettings; +import com.kintone.client.api.app.*; +import com.kintone.client.helper.App; +import com.kintone.client.helper.AppAclBuilder; +import com.kintone.client.model.Entity; +import com.kintone.client.model.EntityType; +import com.kintone.client.model.app.AppRightEntity; +import java.util.Arrays; +import java.util.List; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** AppClientのアプリアクセス権設定に関するテスト */ +public class AppAclTest extends ApiTestBase { + + private KintoneClient client; + private App app; + private List originalAppAcl; + + @BeforeEach + public void setupApp() { + client = setupDefaultClient(); + Long testAppId = TestSettings.get().getTestAppId(); + if (testAppId != null) { + app = App.fromExisting(client, testAppId); + } else { + throw new IllegalStateException( + "KINTONE_TEST_APP_ID is not set. Please create a test app and set the environment variable."); + } + // 元のACL設定を保存 + originalAppAcl = app.getAppAcl(false); + } + + @AfterEach + public void cleanupAcl() { + if (app != null) { + try { + // 元のACL設定に戻す + UpdateAppAclRequest req = new UpdateAppAclRequest(); + req.setApp(app.id()); + req.setRights(originalAppAcl); + client.app().updateAppAcl(req); + client.app().deployApp(app.id()); + app.waitDeploy(); + } catch (Exception e) { + // ignore cleanup errors + } + } + } + + @Test + public void getAppAcl_getAppAclPreview() { + + AppAclBuilder builder = new AppAclBuilder(); + builder.everyone().all(false); + builder.creator().all(false).appEditable(true); + builder.user(getDefaultUser()).all(true); + app.updateAppAcl(builder).deploy(); + long revision = app.getAppRevision(true); + + Entity creator = new Entity(EntityType.CREATOR, null); + Entity user = new Entity(EntityType.USER, getDefaultUser()); + Entity everyone = new Entity(EntityType.GROUP, "everyone"); + + AppRightEntity r1 = right(creator, false, false, false, false, true, false, false); + AppRightEntity r2 = right(user, true, true, true, true, true, true, true); + AppRightEntity r3 = right(everyone, false, false, false, false, false, false, false); + AppRightEntity r4 = right(creator, true, true, true, true, true, false, false); + + GetAppAclRequest req1 = new GetAppAclRequest(); + req1.setApp(app.id()); + GetAppAclResponseBody resp1 = client.app().getAppAcl(req1); + assertThat(resp1.getRevision()).isEqualTo(revision); + assertThat(resp1.getRights()).containsExactly(r1, r2, r3); + + builder = new AppAclBuilder(); + builder.creator().applyDefault().everyone().all(false); + app.updateAppAcl(builder); + + GetAppAclPreviewRequest req2 = new GetAppAclPreviewRequest(); + req2.setApp(app.id()); + GetAppAclPreviewResponseBody resp2 = client.app().getAppAclPreview(req2); + assertThat(resp2.getRevision()).isEqualTo(revision + 1); + assertThat(resp2.getRights()).containsExactly(r4, r3); + } + + @Test + public void updateAppAcl() { + long revision = app.getAppRevision(true); + + Entity everyone = new Entity(EntityType.GROUP, "everyone"); + Entity creator = new Entity(EntityType.CREATOR, null); + Entity user = new Entity(EntityType.USER, getDefaultUser()); + + AppRightEntity r1 = right(everyone, false, false, false, false, false, false, false); + AppRightEntity r2 = right(creator, true, true, true, true, true, true, true); + AppRightEntity r3 = right(user, true, false, false, false, true, false, false); + + UpdateAppAclRequest req = new UpdateAppAclRequest(); + req.setApp(app.id()); + req.setRevision(revision); + req.setRights(Arrays.asList(r1, r2, r3)); + UpdateAppAclResponseBody resp = client.app().updateAppAcl(req); + assertThat(resp.getRevision()).isEqualTo(revision + 1); + + List previewRights = app.getAppAcl(true); + assertThat(previewRights).containsExactly(r2, r3, r1); // everyoneの設定は最後になる + + List deployedRights = app.getAppAcl(false); + AppRightEntity default1 = right(everyone, true, true, true, true, false, false, false); + AppRightEntity default2 = right(creator, true, true, true, true, true, true, true); + assertThat(deployedRights).containsExactly(default2, default1); + } + + private AppRightEntity right( + Entity entity, + boolean recordViewable, + boolean recordAddable, + boolean recordEditable, + boolean recordDeletable, + boolean appEditable, + boolean recordImportable, + boolean recordExportable) { + AppRightEntity r = new AppRightEntity().setEntity(entity); + r.setIncludeSubs(false); + r.setRecordViewable(recordViewable); + r.setRecordAddable(recordAddable); + r.setRecordEditable(recordEditable); + r.setRecordDeletable(recordDeletable); + r.setAppEditable(appEditable); + r.setRecordImportable(recordImportable); + r.setRecordExportable(recordExportable); + return r; + } +} diff --git a/e2e-tests/src/test/java/com/kintone/client/app/AppApiTest.java b/e2e-tests/src/test/java/com/kintone/client/app/AppApiTest.java new file mode 100644 index 0000000..451247a --- /dev/null +++ b/e2e-tests/src/test/java/com/kintone/client/app/AppApiTest.java @@ -0,0 +1,490 @@ +package com.kintone.client.app; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.kintone.client.ApiTestBase; +import com.kintone.client.KintoneClient; +import com.kintone.client.TestSettings; +import com.kintone.client.api.app.*; +import com.kintone.client.api.common.UploadFileRequest; +import com.kintone.client.helper.App; +import com.kintone.client.helper.AppCustomizeBuilder; +import com.kintone.client.helper.AppSettingsBuilder; +import com.kintone.client.model.FileBody; +import com.kintone.client.model.app.*; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import java.util.*; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +/** + * AppClientのテスト + * + *

AppClientのテストは数が多くなると思われるので、fieldsやviewsなど適当な単位でファイル分割している。 + * + *

このファイルには個別に分類する程でもないもの(アプリ全般設定など)を集めている。 + */ +public class AppApiTest extends ApiTestBase { + + private KintoneClient client; + private App app; + private App.AppCustomize originalCustomize; + private App.AppSettings originalSettings; + + @BeforeEach + public void setupApp() { + client = setupDefaultClient(); + Long testAppId = TestSettings.get().getTestAppId(); + if (testAppId != null) { + app = App.fromExisting(client, testAppId); + originalCustomize = app.getAppCustomize(false); + originalSettings = app.getAppSettings(false); + } else { + throw new IllegalStateException( + "KINTONE_TEST_APP_ID is not set. Please create a test app and set the environment variable."); + } + } + + @AfterEach + public void cleanupApp() { + if (app != null) { + try { + restoreAppCustomize(); + restoreAppSettings(); + client.app().deployApp(app.id()); + app.waitDeploy(); + } catch (Exception e) { + // ignore cleanup errors + } + } + } + + private void restoreAppCustomize() { + AppCustomizeBuilder builder = new AppCustomizeBuilder().scope(originalCustomize.getScope()); + addCustomizeResources(builder, originalCustomize.getDesktop(), true); + addCustomizeResources(builder, originalCustomize.getMobile(), false); + app.updateAppCustomize(builder); + } + + private void addCustomizeResources( + AppCustomizeBuilder builder, CustomizeBody body, boolean isDesktop) { + if (body == null) return; + for (CustomizeResource res : body.getJs()) { + if (res.getType() == CustomizeType.URL) { + if (isDesktop) { + builder.desktopJsUrl(((CustomizeUrlResource) res).getUrl()); + } else { + builder.mobileJsUrl(((CustomizeUrlResource) res).getUrl()); + } + } else { + if (isDesktop) { + builder.desktopJsFile(((CustomizeFileResource) res).getFile().getFileKey()); + } else { + builder.mobileJsFile(((CustomizeFileResource) res).getFile().getFileKey()); + } + } + } + for (CustomizeResource res : body.getCss()) { + if (res.getType() == CustomizeType.URL) { + if (isDesktop) { + builder.desktopCssUrl(((CustomizeUrlResource) res).getUrl()); + } else { + builder.mobileCssUrl(((CustomizeUrlResource) res).getUrl()); + } + } else { + if (isDesktop) { + builder.desktopCssFile(((CustomizeFileResource) res).getFile().getFileKey()); + } else { + builder.mobileCssFile(((CustomizeFileResource) res).getFile().getFileKey()); + } + } + } + } + + private void restoreAppSettings() { + AppSettingsBuilder builder = + new AppSettingsBuilder() + .name(originalSettings.getName()) + .description(originalSettings.getDescription()) + .theme(originalSettings.getTheme()); + if (originalSettings.getIcon() instanceof AppPresetIcon) { + builder.presetIcon(((AppPresetIcon) originalSettings.getIcon()).getKey()); + } + app.updateAppSettings(builder); + } + + @Test + public void getApp() { + Long spaceAppId = TestSettings.get().getTestSpaceAppId(); + App spaceApp = App.fromExisting(client, spaceAppId); + + GetAppRequest req = new GetAppRequest(); + req.setId(spaceApp.id()); + GetAppResponseBody resp = client.app().getApp(req); + assertThat(resp.getAppId()).isEqualTo(spaceApp.id()); + assertThat(resp.getSpaceId()).isNotNull(); + assertThat(resp.getThreadId()).isNotNull(); + assertThat(resp.getCreator().getCode()).isNotNull(); + assertThat(resp.getModifier().getCode()).isNotNull(); + assertThat(resp.getCreatedAt()).isNotNull(); + assertThat(resp.getModifiedAt()).isNotNull(); + } + + @Test + public void getAppCustomize_getAppCustomizePreview() { + String key1 = uploadMockFile(client, "desktop.js", "application/javascript", "// desktop.js"); + String key2 = uploadMockFile(client, "desktop.css", "text/css", "// desktop.css"); + String key3 = uploadMockFile(client, "mobile.js", "application/javascript", "// mobile.js"); + String key4 = uploadMockFile(client, "mobile.css", "text/css", "// mobile.css"); + + AppCustomizeBuilder builder = + new AppCustomizeBuilder() + .scope(CustomizeScope.ALL) + .desktopJsUrl("https://localhost/desktop.js") + .desktopJsFile(key1) + .desktopCssUrl("https://localhost/desktop.css") + .desktopCssFile(key2) + .mobileJsUrl("https://localhost/mobile.js") + .mobileJsFile(key3) + .mobileCssUrl("https://localhost/mobile.css") + .mobileCssFile(key4); + app.updateAppCustomize(builder); + client.app().deployApp(app.id()); + app.waitDeploy(); + long revision = app.getAppRevision(false); + + GetAppCustomizeRequest req1 = new GetAppCustomizeRequest(); + req1.setApp(app.id()); + GetAppCustomizeResponseBody resp1 = client.app().getAppCustomize(req1); + assertThat(resp1.getScope()).isEqualTo(CustomizeScope.ALL); + assertThat(resp1.getRevision()).isEqualTo(revision); + CustomizeBody desktop = resp1.getDesktop(); + assertCustomizeResources( + desktop.getJs(), + CustomizeType.URL, + "https://localhost/desktop.js", + CustomizeType.FILE, + "desktop.js"); + assertCustomizeResources( + desktop.getCss(), + CustomizeType.URL, + "https://localhost/desktop.css", + CustomizeType.FILE, + "desktop.css"); + CustomizeBody mobile = resp1.getMobile(); + assertCustomizeResources( + mobile.getJs(), + CustomizeType.URL, + "https://localhost/mobile.js", + CustomizeType.FILE, + "mobile.js"); + assertCustomizeResources( + mobile.getCss(), + CustomizeType.URL, + "https://localhost/mobile.css", + CustomizeType.FILE, + "mobile.css"); + + app.updateAppCustomize(new AppCustomizeBuilder().scope(CustomizeScope.NONE)); + + GetAppCustomizePreviewRequest req2 = new GetAppCustomizePreviewRequest(); + req2.setApp(app.id()); + GetAppCustomizePreviewResponseBody resp2 = client.app().getAppCustomizePreview(req2); + assertThat(resp2.getScope()).isEqualTo(CustomizeScope.NONE); + assertThat(resp2.getRevision()).isEqualTo(revision + 1); + } + + @Test + public void getApps() { + Long spaceAppId = TestSettings.get().getTestSpaceAppId(); + + GetAppsRequest req = new GetAppsRequest(); + req.setIds(Arrays.asList(app.id(), spaceAppId)); + GetAppsResponseBody resp = client.app().getApps(req); + List apps = resp.getApps(); + assertThat(apps).hasSize(2); + + long appId = app.id(); + com.kintone.client.model.app.App testApp = + apps.stream().filter(a -> a.getAppId() == appId).findFirst().get(); + assertThat(testApp.getAppId()).isEqualTo(appId); + assertThat(testApp.getCreator().getCode()).isNotNull(); + assertThat(testApp.getModifier().getCode()).isNotNull(); + assertThat(testApp.getCreatedAt()).isNotNull(); + assertThat(testApp.getModifiedAt()).isNotNull(); + + long spaceAppIdValue = spaceAppId; + com.kintone.client.model.app.App spaceAppModel = + apps.stream().filter(a -> a.getAppId() == spaceAppIdValue).findFirst().get(); + assertThat(spaceAppModel.getAppId()).isEqualTo(spaceAppId); + assertThat(spaceAppModel.getSpaceId()).isNotNull(); + assertThat(spaceAppModel.getThreadId()).isNotNull(); + assertThat(spaceAppModel.getCreator().getCode()).isNotNull(); + assertThat(spaceAppModel.getModifier().getCode()).isNotNull(); + assertThat(spaceAppModel.getCreatedAt()).isNotNull(); + assertThat(spaceAppModel.getModifiedAt()).isNotNull(); + } + + @Test + public void move() { + Long spaceAppId = TestSettings.get().getTestSpaceAppId(); + Long spaceId = TestSettings.get().getSingleThreadSpaceId(); + App spaceApp = App.fromExisting(client, spaceAppId); + Long originalSpaceId = client.app().getApp(spaceApp.id()).getSpaceId(); + + try { + client.app().move(spaceApp.id(), null); + assertThat(client.app().getApp(spaceApp.id()).getSpaceId()).isNull(); + client.app().move(spaceApp.id(), spaceId); + assertThat(client.app().getApp(spaceApp.id()).getSpaceId()).isEqualTo(spaceId); + } finally { + client.app().move(spaceApp.id(), originalSpaceId); + } + } + + @Test + public void getAppSettings_getAppSettingsPreview() { + AppSettingsBuilder builder = + new AppSettingsBuilder().description("description").theme("GREEN").presetIcon("APP60"); + app.updateAppSettings(builder); + client.app().deployApp(app.id()); + app.waitDeploy(); + + GetAppSettingsRequest req1 = new GetAppSettingsRequest(); + req1.setApp(app.id()); + req1.setLang("default"); + GetAppSettingsResponseBody resp1 = client.app().getAppSettings(req1); + assertThat(resp1.getDescription()).isEqualTo("description"); + assertThat(resp1.getTheme()).isEqualTo("GREEN"); + assertThat(resp1.getIcon().getType()).isEqualTo(AppIconType.PRESET); + assertThat(((AppPresetIcon) resp1.getIcon()).getKey()).isEqualTo("APP60"); + + builder = + new AppSettingsBuilder().description("description 2").theme("BLUE").presetIcon("APP59"); + app.updateAppSettings(builder); + + GetAppSettingsPreviewRequest req2 = new GetAppSettingsPreviewRequest(); + req2.setApp(app.id()); + req2.setLang("default"); + GetAppSettingsPreviewResponseBody resp2 = client.app().getAppSettingsPreview(req2); + assertThat(resp2.getDescription()).isEqualTo("description 2"); + assertThat(resp2.getTheme()).isEqualTo("BLUE"); + assertThat(resp2.getIcon().getType()).isEqualTo(AppIconType.PRESET); + assertThat(((AppPresetIcon) resp2.getIcon()).getKey()).isEqualTo("APP59"); + } + + @Test + public void updateAppCustomize() { + long revision = app.getAppRevision(true); + String key1 = uploadMockFile(client, "desktop.js", "application/javascript", "// desktop.js"); + String key2 = uploadMockFile(client, "mobile.js", "application/javascript", "// mobile.js"); + + UpdateAppCustomizeRequest req = new UpdateAppCustomizeRequest(); + req.setApp(app.id()); + req.setScope(CustomizeScope.ADMIN); + req.setDesktop(makeCustomizeBody(key1, "https://localhost/desktop.css")); + req.setMobile(makeCustomizeBody(key2, "https://localhost/mobile.css")); + req.setRevision(revision); + UpdateAppCustomizeResponseBody resp = client.app().updateAppCustomize(req); + assertThat(resp.getRevision()).isEqualTo(revision + 1); + + App.AppCustomize previewSettings = app.getAppCustomize(true); + assertThat(previewSettings.getScope()).isEqualTo(CustomizeScope.ADMIN); + assertThat(previewSettings.getRevision()).isEqualTo(revision + 1); + CustomizeBody desktop = previewSettings.getDesktop(); + assertCustomizeResources(desktop.getJs(), CustomizeType.FILE, "desktop.js"); + assertCustomizeResources(desktop.getCss(), CustomizeType.URL, "https://localhost/desktop.css"); + CustomizeBody mobile = previewSettings.getMobile(); + assertCustomizeResources(mobile.getJs(), CustomizeType.FILE, "mobile.js"); + assertCustomizeResources(mobile.getCss(), CustomizeType.URL, "https://localhost/mobile.css"); + } + + @Test + public void updateAppSettings() { + // アプリ名やアイコンを変更するテストは専用アプリを使用 + Long updateSettingsAppId = TestSettings.get().getTestAppIdForUpdateAppSettings(); + if (updateSettingsAppId == null) { + System.out.println("KINTONE_TEST_APP_ID_FOR_UPDATE_APP_SETTINGS is not set, skipping test"); + return; + } + App settingsApp = App.fromExisting(client, updateSettingsAppId); + + // 前回のテスト実行で残った未デプロイの変更をリバート + try { + client.app().revertApp(settingsApp.id()); + } catch (Exception e) { + // ignore if no changes to revert + } + + try { + long revision = settingsApp.getAppRevision(true); + boolean supportNumberPrecision = + client.app().getAppSettingsPreview(settingsApp.id()).getNumberPrecision() != null; + + UpdateAppSettingsRequest req = new UpdateAppSettingsRequest(); + req.setApp(settingsApp.id()); + req.setName("updateAppSettings_updated"); + req.setDescription("app description"); + req.setTheme("YELLOW"); + req.setIcon(new AppPresetIcon().setKey("APP60")); + req.setRevision(revision); + if (supportNumberPrecision) { + req.setNumberPrecision( + new NumberPrecision() + .setDigits(30) + .setDecimalPlaces(10) + .setRoundingMode(RoundingMode.UP)); + req.setFirstMonthOfFiscalYear(6); + req.setEnableThumbnails(false); + req.setEnableComments(false); + req.setEnableDuplicateRecord(false); + req.setEnableBulkDeletion(true); + } + UpdateAppSettingsResponseBody resp = client.app().updateAppSettings(req); + assertThat(resp.getRevision()).isEqualTo(revision + 1); + + App.AppSettings previewSettings = settingsApp.getAppSettings(true); + assertThat(previewSettings.getName()).isEqualTo("updateAppSettings_updated"); + assertThat(previewSettings.getDescription()).isEqualTo("app description"); + assertThat(previewSettings.getTheme()).isEqualTo("YELLOW"); + assertThat(((AppPresetIcon) previewSettings.getIcon()).getKey()).isEqualTo("APP60"); + + if (supportNumberPrecision) { + GetAppSettingsPreviewResponseBody response = + client.app().getAppSettingsPreview(settingsApp.id()); + assertThat(response.getNumberPrecision().getDigits()).isEqualTo(30); + assertThat(response.getNumberPrecision().getDecimalPlaces()).isEqualTo(10); + assertThat(response.getNumberPrecision().getRoundingMode()).isEqualTo(RoundingMode.UP); + assertThat(response.getFirstMonthOfFiscalYear()).isEqualTo(6); + assertThat(response.isEnableThumbnails()).isFalse(); + assertThat(response.isEnableComments()).isFalse(); + assertThat(response.isEnableDuplicateRecord()).isFalse(); + assertThat(response.isEnableBulkDeletion()).isTrue(); + } + } finally { + // 変更をリバート + try { + client.app().revertApp(settingsApp.id()); + } catch (Exception e) { + // ignore + } + } + } + + @Test + public void updateAppSettingsFileIcon() throws IOException { + // アプリアイコンを変更するテストは専用アプリを使用 + Long updateSettingsAppId = TestSettings.get().getTestAppIdForUpdateAppSettings(); + if (updateSettingsAppId == null) { + System.out.println("KINTONE_TEST_APP_ID_FOR_UPDATE_APP_SETTINGS is not set, skipping test"); + return; + } + App settingsApp = App.fromExisting(client, updateSettingsAppId); + + // 前回のテスト実行で残った未デプロイの変更をリバート + try { + client.app().revertApp(settingsApp.id()); + } catch (Exception e) { + // ignore if no changes to revert + } + + try { + Path path = new File(AppApiTest.class.getResource("fileicon.png").getFile()).toPath(); + String fileKey = client.file().uploadFile(path, "image/png"); + long revision = settingsApp.getAppRevision(true); + + UpdateAppSettingsRequest req = new UpdateAppSettingsRequest(); + req.setApp(settingsApp.id()); + req.setIcon(new AppFileIcon().setFile(new FileBody().setFileKey(fileKey))); + + UpdateAppSettingsResponseBody resp = client.app().updateAppSettings(req); + assertThat(resp.getRevision()).isEqualTo(revision + 1); + + App.AppSettings settings = settingsApp.getAppSettings(true); + assertThat(((AppFileIcon) settings.getIcon()).getFile().getName()).isEqualTo("fileicon.png"); + assertThat(((AppFileIcon) settings.getIcon()).getFile().getContentType()) + .isEqualTo("image/png"); + } finally { + // 変更をリバート + try { + client.app().revertApp(settingsApp.id()); + } catch (Exception e) { + // ignore + } + } + } + + private String uploadMockFile(KintoneClient client, String name, String type, String data) { + try (ByteArrayInputStream in = new ByteArrayInputStream(data.getBytes())) { + UploadFileRequest req1 = new UploadFileRequest(); + req1.setFilename(name); + req1.setContentType(type); + req1.setContent(in); + return client.file().uploadFile(req1).getFileKey(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private CustomizeBody makeCustomizeBody(String fileKey, String url) { + CustomizeResource r1 = new CustomizeFileResource().setFile(new FileBody().setFileKey(fileKey)); + CustomizeResource r2 = new CustomizeUrlResource().setUrl(url); + CustomizeBody body = new CustomizeBody(); + body.setJs(Collections.singletonList(r1)); + body.setCss(Collections.singletonList(r2)); + return body; + } + + private void assertCustomizeResources(List resources, Object... data) { + // dataは CustomizeType, ファイル名かURL の繰り返し + int size = data.length / 2; + assertThat(resources).hasSize(size); + for (int i = 0; i < size; i++) { + CustomizeResource resource = resources.get(i); + CustomizeType type = (CustomizeType) data[i * 2]; + String value = (String) data[i * 2 + 1]; + assertThat(resource.getType()).isEqualTo(type); + if (type == CustomizeType.FILE) { + assertThat(((CustomizeFileResource) resource).getFile().getName()).isEqualTo(value); + } else { + assertThat(((CustomizeUrlResource) resource).getUrl()).isEqualTo(value); + } + } + } + + @Test + @Disabled("Skipped until AppStatistics model is updated for new API fields") + public void getStatistics() { + KintoneClient client = setupDefaultClient(); + + GetAppsStatisticsRequest req = new GetAppsStatisticsRequest(); + req.setLimit(10L); + req.setOffset(0L); + GetAppsStatisticsResponseBody resp = client.app().getStatistics(req); + + List apps = resp.getApps(); + assertThat(apps).isNotNull(); + assertThat(apps).isNotEmpty(); + + AppStatistics stats = apps.get(0); + assertThat(stats.getId()).isNotNull(); + assertThat(stats.getName()).isNotNull(); + // space はスペースに属していない場合はnull + // appGroup はnullの可能性あり + assertThat(stats.getStatus()).isNotNull(); + // recordUpdatedAt はレコードがない場合null + assertThat(stats.getRecordCount()).isNotNull(); + assertThat(stats.getFieldCount()).isNotNull(); + assertThat(stats.getDailyRequestCount()).isNotNull(); + assertThat(stats.getStorageUsage()).isNotNull(); + assertThat(stats.getCustomized()).isNotNull(); + // creator, modifier はシステムアプリの場合nullの可能性あり + assertThat(stats.getCreatedAt()).isNotNull(); + // modifiedAt はnullの可能性あり + } +} diff --git a/e2e-tests/src/test/java/com/kintone/client/app/AppPluginsTest.java b/e2e-tests/src/test/java/com/kintone/client/app/AppPluginsTest.java new file mode 100644 index 0000000..e10695b --- /dev/null +++ b/e2e-tests/src/test/java/com/kintone/client/app/AppPluginsTest.java @@ -0,0 +1,89 @@ +package com.kintone.client.app; + +import static org.assertj.core.api.Assertions.*; + +import com.kintone.client.ApiTestBase; +import com.kintone.client.KintoneClient; +import com.kintone.client.TestSettings; +import com.kintone.client.api.app.AddAppPluginsRequest; +import com.kintone.client.helper.App; +import com.kintone.client.model.app.AppPlugin; +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +public class AppPluginsTest extends ApiTestBase { + + private KintoneClient client; + private App app; + private App appForGetPluginsPreview; + + @BeforeEach + public void setupApp() { + client = setupDefaultClient(); + Long testAppId = TestSettings.get().getTestAppId(); + Long testAppIdForGetPluginsPreview = TestSettings.get().getTestAppIdForGetPluginsPreview(); + if (testAppId != null) { + app = App.fromExisting(client, testAppId); + } else { + throw new IllegalStateException( + "KINTONE_TEST_APP_ID is not set. Please create a test app and set the environment variable."); + } + if (testAppIdForGetPluginsPreview != null) { + appForGetPluginsPreview = App.fromExisting(client, testAppIdForGetPluginsPreview); + } else { + throw new IllegalStateException( + "KINTONE_TEST_APP_ID_FOR_GET_PLUGINS_PREVIEW is not set. Please create a test app and set the environment variable."); + } + } + + @Test + public void getPlugins_getPluginsPreview() throws IOException, InterruptedException { + List pluginsForPreviewApp = + client.app().getPluginsPreview(appForGetPluginsPreview.id(), "ja"); + List pluginsForLiveApp = client.app().getPlugins(appForGetPluginsPreview.id(), "ja"); + + assertThat(pluginsForPreviewApp).hasSize(pluginsForLiveApp.size() + 1); + } + + @Test + @Disabled("Since plugins cannot be deleted from the app, they must be disabled temporarily.") + public void addPlugins() throws IOException, InterruptedException { + String pluginId = installTestPlugin(client, "plugin-c"); + + try { + List initialPlugins = client.app().getPluginsPreview(app.id(), "ja"); + boolean alreadyAdded = initialPlugins.stream().anyMatch(p -> p.getId().equals(pluginId)); + int initialPluginCount = initialPlugins.size(); + + AddAppPluginsRequest request = new AddAppPluginsRequest(); + request.setApp(app.id()); + request.setIds(Arrays.asList(pluginId)); + client.app().addPlugins(request); + + List plugins = client.app().getPluginsPreview(app.id(), "ja"); + int expectedSize = alreadyAdded ? initialPluginCount : initialPluginCount + 1; + assertThat(plugins).hasSize(expectedSize); + + assertThat(plugins.stream().anyMatch(p -> p.getId().equals(pluginId))).isTrue(); + } finally { + client.plugin().uninstallPlugin(pluginId); + } + } + + private String installTestPlugin(KintoneClient client, String pluginName) throws IOException { + Path path = + new File( + AppPluginsTest.class + .getResource("/com/kintone/client/plugin/" + pluginName + ".zip") + .getFile()) + .toPath(); + String fileKey = client.file().uploadFile(path, "multipart/form-data"); + return client.plugin().installPlugin(fileKey).getId(); + } +} diff --git a/e2e-tests/src/test/java/com/kintone/client/app/DeployTest.java b/e2e-tests/src/test/java/com/kintone/client/app/DeployTest.java new file mode 100644 index 0000000..3cdc5ba --- /dev/null +++ b/e2e-tests/src/test/java/com/kintone/client/app/DeployTest.java @@ -0,0 +1,110 @@ +package com.kintone.client.app; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; + +import com.kintone.client.ApiTestBase; +import com.kintone.client.KintoneClient; +import com.kintone.client.api.app.*; +import com.kintone.client.helper.App; +import com.kintone.client.helper.Fields; +import com.kintone.client.helper.Space; +import com.kintone.client.model.app.AppDeployStatus; +import com.kintone.client.model.app.DeployApp; +import com.kintone.client.model.app.DeployStatus; +import com.kintone.client.model.app.field.FieldProperty; +import java.util.Arrays; +import java.util.Collections; +import java.util.Map; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +/** AppClientのアプリ作成、デプロイに関するテスト */ +public class DeployTest extends ApiTestBase { + + private static final int DEPLOY_WAIT_SEC = 300; + + @Test + @Disabled("Since the app cannot be deleted, it has been temporarily disabled.") + public void addApp() { + Space space = Space.guest(this); + long spaceId = space.id(); + long threadId = space.getDefaultThread(); + KintoneClient client = setupDefaultClient(spaceId); + + AddAppRequest req = new AddAppRequest(); + req.setName("addApp_" + System.currentTimeMillis()); + req.setSpace(spaceId); + req.setThread(threadId); + AddAppResponseBody resp = client.app().addApp(req); + assertThat(resp.getApp()).isGreaterThan(0); + assertThat(resp.getRevision()).isGreaterThan(0); + + App app = App.fromExisting(client, resp.getApp()); + assertThat(app.getAppSettings(true).getName()).startsWith("addApp_"); + } + + @Test + @Disabled("Since the app cannot be deleted, it has been temporarily disabled.") + public void deployApp_getDeployStatus() { + KintoneClient client = setupDefaultClient(); + DeployApp app1 = createApp(client, "deployApp 1"); + DeployApp app2 = createApp(client, "deployApp 2"); + + DeployAppRequest req1 = new DeployAppRequest(); + req1.setApps(Arrays.asList(app1, app2)); + req1.setRevert(false); + DeployAppResponseBody resp = client.app().deployApp(req1); + + GetDeployStatusRequest req2 = new GetDeployStatusRequest(); + req2.setApps(Arrays.asList(app1.getApp(), app2.getApp())); + for (int i = 0; i < DEPLOY_WAIT_SEC; i++) { + GetDeployStatusResponseBody resp2 = client.app().getDeployStatus(req2); + assertThat(resp2.getApps()).hasSize(2); + + AppDeployStatus status1 = resp2.getApps().get(0); + assertThat(status1.getApp()).isEqualTo(app1.getApp()); + assertThat(status1.getStatus()).isNotNull(); + + AppDeployStatus status2 = resp2.getApps().get(1); + assertThat(status2.getApp()).isEqualTo(app2.getApp()); + assertThat(status2.getStatus()).isNotNull(); + + if (status1.getStatus() == DeployStatus.SUCCESS + && status2.getStatus() == DeployStatus.SUCCESS) { + return; + } + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + fail("deploy faield"); + } + + @Test + @Disabled("Since the app cannot be deleted, it has been temporarily disabled.") + public void deployApp_revert() { + KintoneClient client = setupDefaultClient(); + App app = App.create(client, "deployApp_revert").deploy(); + FieldProperty text = Fields.text(); + app.addFields(text); + + DeployAppRequest req = new DeployAppRequest(); + req.setApps(Collections.singletonList(new DeployApp().setApp(app.id()))); + req.setRevert(true); + DeployAppResponseBody resp = client.app().deployApp(req); + + app.waitDeploy(); + Map fields = app.getFields(false); + assertThat(fields).doesNotContainKeys(text.getCode()); + } + + private DeployApp createApp(KintoneClient client, String name) { + AddAppRequest req = new AddAppRequest(); + req.setName(name); + AddAppResponseBody resp = client.app().addApp(req); + return new DeployApp().setApp(resp.getApp()).setRevision(resp.getRevision()); + } +} diff --git a/e2e-tests/src/test/java/com/kintone/client/app/FieldAclTest.java b/e2e-tests/src/test/java/com/kintone/client/app/FieldAclTest.java new file mode 100644 index 0000000..ec0af2e --- /dev/null +++ b/e2e-tests/src/test/java/com/kintone/client/app/FieldAclTest.java @@ -0,0 +1,147 @@ +package com.kintone.client.app; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.kintone.client.ApiTestBase; +import com.kintone.client.KintoneClient; +import com.kintone.client.TestSettings; +import com.kintone.client.api.app.*; +import com.kintone.client.helper.App; +import com.kintone.client.helper.FieldAclBuilder; +import com.kintone.client.model.Entity; +import com.kintone.client.model.EntityType; +import com.kintone.client.model.app.FieldAccessibility; +import com.kintone.client.model.app.FieldRight; +import com.kintone.client.model.app.FieldRightEntity; +import com.kintone.client.model.app.field.FieldProperty; +import java.util.Arrays; +import java.util.List; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** AppClientのフィールドアクセス権設定に関するテスト */ +public class FieldAclTest extends ApiTestBase { + + private static final String TEXT_FIELD_CODE = "文字列__1行_"; + private static final String NUMBER_FIELD_CODE = "数値"; + private static final String USER_SELECT_FIELD_CODE = "ユーザー選択"; + + private KintoneClient client; + private App app; + private List originalFieldAcl; + + @BeforeEach + public void setupApp() { + client = setupDefaultClient(); + Long testAppId = TestSettings.get().getTestAppId(); + if (testAppId != null) { + app = App.fromExisting(client, testAppId); + } else { + throw new IllegalStateException( + "KINTONE_TEST_APP_ID is not set. Please create a test app and set the environment variable."); + } + // 元のACL設定を保存 + originalFieldAcl = app.getFieldAcl(false); + } + + @AfterEach + public void cleanupAcl() { + if (app != null) { + try { + // 元のACL設定に戻す + UpdateFieldAclRequest req = new UpdateFieldAclRequest(); + req.setApp(app.id()); + req.setRights(originalFieldAcl); + client.app().updateFieldAcl(req); + client.app().deployApp(app.id()); + app.waitDeploy(); + } catch (Exception e) { + // ignore cleanup errors + } + } + } + + @Test + public void getFieldAcl_getFieldAclPreview() { + FieldProperty text = app.field(TEXT_FIELD_CODE); + FieldProperty number = app.field(NUMBER_FIELD_CODE); + FieldProperty userSelect = app.field(USER_SELECT_FIELD_CODE); + + FieldAclBuilder builder = new FieldAclBuilder(); + builder.target(text).user(getDefaultUser(), true, true).everyone(false, false); + builder.target(number).field(userSelect, true, false).everyone(true, true); + app.updateFieldAcl(builder).deploy(); + long revision = app.getAppRevision(true); + + FieldRight r1 = + right( + text, + entity(EntityType.USER, getDefaultUser(), FieldAccessibility.WRITE), + entity(EntityType.GROUP, "everyone", FieldAccessibility.NONE)); + FieldRight r2 = + right( + number, + entity(EntityType.FIELD_ENTITY, userSelect.getCode(), FieldAccessibility.READ), + entity(EntityType.GROUP, "everyone", FieldAccessibility.WRITE)); + + GetFieldAclRequest req1 = new GetFieldAclRequest(); + req1.setApp(app.id()); + GetFieldAclResponseBody resp1 = client.app().getFieldAcl(req1); + assertThat(resp1.getRevision()).isEqualTo(revision); + assertThat(resp1.getRights()).containsExactly(r1, r2); + + builder = new FieldAclBuilder().target(text).everyone(true, false); + app.updateFieldAcl(builder); + FieldRight r3 = right(text, entity(EntityType.GROUP, "everyone", FieldAccessibility.READ)); + + GetFieldAclPreviewRequest req2 = new GetFieldAclPreviewRequest(); + req2.setApp(app.id()); + GetFieldAclPreviewResponseBody resp2 = client.app().getFieldAclPreview(req2); + assertThat(resp2.getRevision()).isEqualTo(revision + 1); + assertThat(resp2.getRights()).containsExactly(r3); + } + + @Test + public void updateFieldAcl() { + FieldProperty text = app.field(TEXT_FIELD_CODE); + FieldProperty number = app.field(NUMBER_FIELD_CODE); + FieldProperty userSelect = app.field(USER_SELECT_FIELD_CODE); + long revision = app.getAppRevision(true); + + FieldRight r1 = + right( + text, + entity(EntityType.USER, getDefaultUser(), FieldAccessibility.WRITE), + entity(EntityType.GROUP, "everyone", FieldAccessibility.NONE)); + FieldRight r2 = + right( + number, + entity(EntityType.FIELD_ENTITY, userSelect.getCode(), FieldAccessibility.NONE), + entity(EntityType.GROUP, "everyone", FieldAccessibility.READ)); + + UpdateFieldAclRequest req = new UpdateFieldAclRequest(); + req.setApp(app.id()); + req.setRevision(revision); + req.setRights(Arrays.asList(r1, r2)); + UpdateFieldAclResponseBody resp = client.app().updateFieldAcl(req); + assertThat(resp.getRevision()).isEqualTo(revision + 1); + + List previewRights = app.getFieldAcl(true); + assertThat(previewRights).containsExactly(r1, r2); + + List deployedRights = app.getFieldAcl(false); + assertThat(deployedRights).isEmpty(); + } + + private FieldRight right(FieldProperty field, FieldRightEntity... entities) { + return new FieldRight().setCode(field.getCode()).setEntities(Arrays.asList(entities)); + } + + private FieldRightEntity entity(EntityType type, String code, FieldAccessibility acl) { + return new FieldRightEntity() + .setEntity(new Entity(type, code)) + .setAccessibility(acl) + .setIncludeSubs(false); + } +} diff --git a/e2e-tests/src/test/java/com/kintone/client/app/FormFieldsTest.java b/e2e-tests/src/test/java/com/kintone/client/app/FormFieldsTest.java new file mode 100644 index 0000000..6b3082e --- /dev/null +++ b/e2e-tests/src/test/java/com/kintone/client/app/FormFieldsTest.java @@ -0,0 +1,176 @@ +package com.kintone.client.app; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.kintone.client.ApiTestBase; +import com.kintone.client.KintoneClient; +import com.kintone.client.TestSettings; +import com.kintone.client.api.app.*; +import com.kintone.client.helper.App; +import com.kintone.client.helper.Fields; +import com.kintone.client.model.app.field.FieldProperty; +import com.kintone.client.model.app.field.NumberFieldProperty; +import com.kintone.client.model.app.field.SingleLineTextFieldProperty; +import java.math.BigDecimal; +import java.util.*; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** AppClientのfields.jsonのテスト */ +public class FormFieldsTest extends ApiTestBase { + + private KintoneClient client; + private App app; + private Set addedFieldCodes = new HashSet<>(); + + @BeforeEach + public void setupApp() { + client = setupDefaultClient(); + Long testAppId = TestSettings.get().getTestAppId(); + if (testAppId != null) { + app = App.fromExisting(client, testAppId); + } else { + throw new IllegalStateException( + "KINTONE_TEST_APP_ID is not set. Please create a test app and set the environment variable."); + } + addedFieldCodes.clear(); + } + + @AfterEach + public void cleanupFields() { + if (app != null && !addedFieldCodes.isEmpty()) { + try { + client.app().deleteFormFields(app.id(), new ArrayList<>(addedFieldCodes)); + client.app().deployApp(app.id()); + app.waitDeploy(); + } catch (Exception e) { + // ignore cleanup errors + } + } + } + + @Test + public void addFormFields() { + long revision = app.getAppRevision(true); + + Map fields = new HashMap<>(); + String textCode = "test_text_" + System.currentTimeMillis(); + String numberCode = "test_number_" + System.currentTimeMillis(); + fields.put(textCode, Fields.text(textCode)); + fields.put(numberCode, Fields.number(numberCode)); + + AddFormFieldsRequest req = new AddFormFieldsRequest(); + req.setApp(app.id()); + req.setProperties(fields); + req.setRevision(revision); + AddFormFieldsResponseBody resp = client.app().addFormFields(req); + assertThat(resp.getRevision()).isEqualTo(revision + 1); + + addedFieldCodes.add(textCode); + addedFieldCodes.add(numberCode); + + Map updatedFields = app.getFields(true); + assertThat(updatedFields).containsKeys(textCode, numberCode); + } + + @Test + public void deleteFormFields() { + String textCode = "test_text_" + System.currentTimeMillis(); + String numberCode = "test_number_" + System.currentTimeMillis(); + String userSelectCode = "test_user_" + System.currentTimeMillis(); + + FieldProperty text = Fields.text(textCode); + FieldProperty number = Fields.number(numberCode); + FieldProperty userSelect = Fields.userSelect(userSelectCode); + app.addFields(text, number, userSelect); + long revision = app.getAppRevision(true); + + DeleteFormFieldsRequest req = new DeleteFormFieldsRequest(); + req.setApp(app.id()); + req.setFields(Arrays.asList(textCode, userSelectCode)); + req.setRevision(revision); + DeleteFormFieldsResponseBody resp = client.app().deleteFormFields(req); + assertThat(resp.getRevision()).isEqualTo(revision + 1); + + // numberCodeは削除されていないのでクリーンアップ対象 + addedFieldCodes.add(numberCode); + + Map updatedFields = app.getFields(true); + assertThat(updatedFields).containsKeys(numberCode); + assertThat(updatedFields).doesNotContainKeys(textCode, userSelectCode); + } + + @Test + public void getFormFields_getFormFieldsPreview() { + String textCode = "test_text_" + System.currentTimeMillis(); + String numberCode = "test_number_" + System.currentTimeMillis(); + + FieldProperty text = Fields.text(textCode).setExpression("\"ABC\""); + FieldProperty number = Fields.number(numberCode).setDefaultValue(BigDecimal.valueOf(100)); + app.addFields(text, number).deploy(); + addedFieldCodes.add(textCode); + addedFieldCodes.add(numberCode); + long revision = app.getAppRevision(false); + + GetFormFieldsRequest req1 = new GetFormFieldsRequest(); + req1.setApp(app.id()); + GetFormFieldsResponseBody resp1 = client.app().getFormFields(req1); + assertThat(resp1.getRevision()).isEqualTo(revision); + Map fields = resp1.getProperties(); + SingleLineTextFieldProperty p1 = (SingleLineTextFieldProperty) fields.get(textCode); + assertThat(p1.getExpression()).isEqualTo("\"ABC\""); + NumberFieldProperty p2 = (NumberFieldProperty) fields.get(numberCode); + assertThat(p2.getDefaultValue()).isEqualTo(BigDecimal.valueOf(100)); + + app.deleteFields(textCode); + addedFieldCodes.remove(textCode); + GetFormFieldsPreviewRequest req2 = new GetFormFieldsPreviewRequest(); + req2.setApp(app.id()); + GetFormFieldsPreviewResponseBody resp2 = client.app().getFormFieldsPreview(req2); + assertThat(resp2.getRevision()).isEqualTo(revision + 1); + fields = resp2.getProperties(); + assertThat(fields).doesNotContainKey(textCode); + NumberFieldProperty p3 = (NumberFieldProperty) fields.get(numberCode); + assertThat(p3.getDefaultValue()).isEqualTo(BigDecimal.valueOf(100)); + } + + @Test + public void updateFormFields() { + String textCode = "test_text_" + System.currentTimeMillis(); + String numberCode = "test_number_" + System.currentTimeMillis(); + String userSelectCode = "test_user_" + System.currentTimeMillis(); + String newTextCode = "test_A_" + System.currentTimeMillis(); + String newNumberCode = "test_B_" + System.currentTimeMillis(); + + FieldProperty text = Fields.text(textCode); + FieldProperty number = Fields.number(numberCode); + FieldProperty userSelect = Fields.userSelect(userSelectCode); + app.addFields(text, number, userSelect); + long revision = app.getAppRevision(true); + + Map fields = new HashMap<>(); + fields.put(textCode, Fields.text(newTextCode).setUnique(true)); + fields.put(numberCode, Fields.number(newNumberCode).setRequired(true)); + + UpdateFormFieldsRequest req = new UpdateFormFieldsRequest(); + req.setApp(app.id()); + req.setProperties(fields); + req.setRevision(revision); + UpdateFormFieldsResponseBody resp = client.app().updateFormFields(req); + assertThat(resp.getRevision()).isEqualTo(revision + 1); + + // 更新後のフィールドコードをクリーンアップ対象に + addedFieldCodes.add(newTextCode); + addedFieldCodes.add(newNumberCode); + addedFieldCodes.add(userSelectCode); + + Map updatedFields = app.getFields(true); + assertThat(updatedFields).containsKeys(newTextCode, newNumberCode, userSelectCode); + assertThat(updatedFields).doesNotContainKeys(textCode, numberCode); + SingleLineTextFieldProperty p1 = (SingleLineTextFieldProperty) updatedFields.get(newTextCode); + assertThat(p1.getUnique()).isTrue(); + NumberFieldProperty p2 = (NumberFieldProperty) updatedFields.get(newNumberCode); + assertThat(p2.getRequired()).isTrue(); + } +} diff --git a/e2e-tests/src/test/java/com/kintone/client/app/FormLayoutTest.java b/e2e-tests/src/test/java/com/kintone/client/app/FormLayoutTest.java new file mode 100644 index 0000000..4d86b32 --- /dev/null +++ b/e2e-tests/src/test/java/com/kintone/client/app/FormLayoutTest.java @@ -0,0 +1,131 @@ +package com.kintone.client.app; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.kintone.client.ApiTestBase; +import com.kintone.client.KintoneClient; +import com.kintone.client.TestSettings; +import com.kintone.client.api.app.*; +import com.kintone.client.helper.App; +import com.kintone.client.helper.FormLayoutBuilder; +import com.kintone.client.model.app.field.FieldProperty; +import com.kintone.client.model.app.layout.*; +import com.kintone.client.model.record.FieldType; +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** AppClientのlayout.jsonのテスト */ +public class FormLayoutTest extends ApiTestBase { + + private static final String TEXT_FIELD_CODE = "文字列__1行_"; + + private KintoneClient client; + private App app; + private App appForGetFormLayoutPreview; + private List originalLayout; + + @BeforeEach + public void setupApp() { + client = setupDefaultClient(); + Long testAppId = TestSettings.get().getTestAppId(); + Long testAppIdForGetFormLayoutPreview = + TestSettings.get().getTestAppIdForGetFormLayoutPreview(); + if (testAppId != null) { + app = App.fromExisting(client, testAppId); + } else { + throw new IllegalStateException( + "KINTONE_TEST_APP_ID is not set. Please create a test app and set the environment variable."); + } + if (testAppIdForGetFormLayoutPreview != null) { + appForGetFormLayoutPreview = App.fromExisting(client, testAppIdForGetFormLayoutPreview); + } else { + throw new IllegalStateException( + "KINTONE_TEST_APP_ID_FOR_GET_FORM_LAYOUT_PREVIEW is not set. Please create a test app and set the environment variable."); + } + // 元のレイアウト設定を保存 + originalLayout = app.getLayout(false); + } + + @AfterEach + public void cleanupLayout() { + if (app != null) { + try { + // 未デプロイの変更がある場合はリバートしてからデプロイ + client.app().revertApp(app.id()); + } catch (Exception e) { + // ignore cleanup errors (revert fails if no changes) + } + } + } + + @Test + public void getFormLayout_getFormLayoutPreview() { + FieldProperty text = app.field(TEXT_FIELD_CODE); + + FormLayoutBuilder builder = new FormLayoutBuilder(); + builder.row().field(text, 200).hr(100); + builder.row().label("sample label", 200).spacer("spacer", 100, 100); + appForGetFormLayoutPreview.updateLayout(builder).deploy(); + long revision = appForGetFormLayoutPreview.getAppRevision(true); + + GetFormLayoutRequest req1 = new GetFormLayoutRequest(); + req1.setApp(appForGetFormLayoutPreview.id()); + GetFormLayoutResponseBody resp1 = client.app().getFormLayout(req1); + assertThat(resp1.getRevision()).isEqualTo(revision); + assertThat(resp1.getLayout()) + .usingRecursiveFieldByFieldElementComparator() + .isEqualTo(builder.build()); + + builder = new FormLayoutBuilder().row().field(text, 200); + appForGetFormLayoutPreview.updateLayout(builder); + + GetFormLayoutPreviewRequest req2 = new GetFormLayoutPreviewRequest(); + req2.setApp(appForGetFormLayoutPreview.id()); + GetFormLayoutPreviewResponseBody resp2 = client.app().getFormLayoutPreview(req2); + assertThat(resp2.getRevision()).isEqualTo(revision + 1); + assertThat(resp2.getLayout()) + .usingRecursiveFieldByFieldElementComparator() + .isEqualTo(builder.build()); + } + + @Test + public void updateFormLayout() { + FieldProperty text = appForGetFormLayoutPreview.field(TEXT_FIELD_CODE); + long revision = appForGetFormLayoutPreview.getAppRevision(true); + + List row1 = new ArrayList<>(); + row1.add( + new FieldLayout() + .setType(text.getType()) + .setCode(text.getCode()) + .setSize(new FieldSize().setWidth(200))); + row1.add( + new FieldLayout() + .setType(FieldType.SPACER) + .setElementId("spacer") + .setSize(new FieldSize().setWidth(100).setHeight(80))); + List row2 = new ArrayList<>(); + row2.add( + new FieldLayout() + .setType(FieldType.HR) + .setElementId("") + .setSize(new FieldSize().setWidth(300))); + + List layout = new ArrayList<>(); + layout.add(new RowLayout().setFields(row1)); + layout.add(new RowLayout().setFields(row2)); + + UpdateFormLayoutRequest req = new UpdateFormLayoutRequest(); + req.setApp(appForGetFormLayoutPreview.id()); + req.setLayout(layout); + req.setRevision(revision); + UpdateFormLayoutResponseBody resp = client.app().updateFormLayout(req); + assertThat(resp.getRevision()).isEqualTo(revision + 1); + + List updatedLayout = appForGetFormLayoutPreview.getLayout(true); + assertThat(updatedLayout).usingRecursiveFieldByFieldElementComparator().isEqualTo(layout); + } +} diff --git a/e2e-tests/src/test/java/com/kintone/client/app/NotificationTest.java b/e2e-tests/src/test/java/com/kintone/client/app/NotificationTest.java new file mode 100644 index 0000000..6658602 --- /dev/null +++ b/e2e-tests/src/test/java/com/kintone/client/app/NotificationTest.java @@ -0,0 +1,405 @@ +package com.kintone.client.app; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.kintone.client.*; +import com.kintone.client.api.app.*; +import com.kintone.client.helper.*; +import com.kintone.client.helper.App; +import com.kintone.client.model.Entity; +import com.kintone.client.model.EntityType; +import com.kintone.client.model.app.*; +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** AppClientのnotifications/*.jsonのテスト */ +public class NotificationTest extends ApiTestBase { + + private static final String NUMBER_FIELD_CODE = "数値"; + private static final String USER_SELECT_FIELD_CODE = "ユーザー選択"; + private static final String DATE_FIELD_CODE = "日付"; + private static final String DATETIME_FIELD_CODE = "日時"; + + private KintoneClient client; + private App app; + private App.GeneralNotifications originalGeneralNotifications; + private List originalPerRecordNotifications; + private App.ReminderNotifications originalReminderNotifications; + + @BeforeEach + public void setupApp() { + client = setupDefaultClient(); + Long testAppId = TestSettings.get().getTestAppId(); + if (testAppId != null) { + app = App.fromExisting(client, testAppId); + } else { + throw new IllegalStateException( + "KINTONE_TEST_APP_ID is not set. Please create a test app and set the environment variable."); + } + // 元の通知設定を保存 + originalGeneralNotifications = app.getGeneralNotifications(false); + originalPerRecordNotifications = app.getRecordNotifications(false); + originalReminderNotifications = app.getReminderNotifications(false); + } + + @AfterEach + public void cleanupNotifications() { + if (app != null) { + try { + // 元の通知設定に戻す + GeneralNotificationsBuilder genBuilder = new GeneralNotificationsBuilder(); + genBuilder.notifyToCommenter(originalGeneralNotifications.isNotifyToCommenter()); + app.updateGeneralNotifications(genBuilder); + + ReminderNotificationsBuilder remBuilder = new ReminderNotificationsBuilder(); + remBuilder.timezone(originalReminderNotifications.getTimezone()); + app.updateReminderNotifications(remBuilder); + + RecordNotificationsBuilder recBuilder = new RecordNotificationsBuilder(); + app.updateRecordNotifications(recBuilder); + + client.app().deployApp(app.id()); + app.waitDeploy(); + } catch (Exception e) { + // ignore cleanup errors + } + } + } + + @Test + public void getGeneralNotifications_getGeneralNotificationsPreview() { + GeneralNotificationsBuilder builder = new GeneralNotificationsBuilder(); + builder.notifyToCommenter(true); + builder.user(getDefaultUser()).all(true); + builder.everyone().all(false); + builder.org(Orgs.org1.getCode(), true).statusChanged(true); + builder.field(USER_SELECT_FIELD_CODE).recordAdded(true); + app.updateGeneralNotifications(builder).deploy(); + long revision = app.getAppRevision(false); + + List notifications = new ArrayList<>(); + Entity user = new Entity(EntityType.USER, getDefaultUser()); + Entity group = new Entity(EntityType.GROUP, "everyone"); + Entity org = new Entity(EntityType.ORGANIZATION, Orgs.org1.getCode()); + Entity field = new Entity(EntityType.FIELD_ENTITY, USER_SELECT_FIELD_CODE); + notifications.add(generalNotification(user, true, true, true, true, true)); + notifications.add(generalNotification(group, false, false, false, false, false)); + GeneralNotification n = generalNotification(org, false, false, false, true, false); + notifications.add(n.setIncludeSubs(true)); + notifications.add(generalNotification(field, true, false, false, false, false)); + + GetGeneralNotificationsRequest req1 = new GetGeneralNotificationsRequest(); + req1.setApp(app.id()); + GetGeneralNotificationsResponseBody resp1 = client.app().getGeneralNotifications(req1); + assertThat(resp1.getRevision()).isEqualTo(revision); + assertThat(resp1.isNotifyToCommenter()).isTrue(); + assertThat(resp1.getNotifications()) + .usingRecursiveFieldByFieldElementComparator() + .isEqualTo(notifications); + + app.updateGeneralNotifications(new GeneralNotificationsBuilder().notifyToCommenter(false)); + + GetGeneralNotificationsPreviewRequest req2 = new GetGeneralNotificationsPreviewRequest(); + req2.setApp(app.id()); + GetGeneralNotificationsPreviewResponseBody resp2 = + client.app().getGeneralNotificationsPreview(req2); + assertThat(resp2.getRevision()).isEqualTo(revision + 1); + assertThat(resp2.isNotifyToCommenter()).isFalse(); + assertThat(resp2.getNotifications()) + .usingRecursiveFieldByFieldElementComparator() + .isEqualTo(notifications); + } + + @Test + public void getPerRecordNotifications_getPerRecordNotificationsPreview() { + RecordNotificationsBuilder builder = new RecordNotificationsBuilder(); + builder.query(NUMBER_FIELD_CODE + " >= 1").title("n1").user(getDefaultUser()); + builder.query(NUMBER_FIELD_CODE + " >= 2").title("n2").everyone().user(getDefaultUser()); + builder + .query(NUMBER_FIELD_CODE + " >= 3") + .title("n3") + .org(Orgs.org1.getCode(), true) + .org(Orgs.org2.getCode(), false); + builder.query(NUMBER_FIELD_CODE + " >= 4").title("n4").field(USER_SELECT_FIELD_CODE); + app.updateRecordNotifications(builder).deploy(); + long revision = app.getAppRevision(false); + + List notifications = new ArrayList<>(); + List t1 = makeTargets(EntityType.USER, getDefaultUser()); + List t2 = + makeTargets(EntityType.GROUP, "everyone", EntityType.USER, getDefaultUser()); + List t3 = + makeTargets(EntityType.ORGANIZATION, Orgs.org1, EntityType.ORGANIZATION, Orgs.org2); + t3.get(0).setIncludeSubs(true); + List t4 = makeTargets(EntityType.FIELD_ENTITY, USER_SELECT_FIELD_CODE); + notifications.add(perRecordNotification("n1", NUMBER_FIELD_CODE + " >= 1", t1)); + notifications.add(perRecordNotification("n2", NUMBER_FIELD_CODE + " >= 2", t2)); + notifications.add(perRecordNotification("n3", NUMBER_FIELD_CODE + " >= 3", t3)); + notifications.add(perRecordNotification("n4", NUMBER_FIELD_CODE + " >= 4", t4)); + + GetPerRecordNotificationsRequest req1 = new GetPerRecordNotificationsRequest(); + req1.setApp(app.id()); + req1.setLang("default"); + GetPerRecordNotificationsResponseBody resp1 = client.app().getPerRecordNotifications(req1); + assertThat(resp1.getRevision()).isEqualTo(revision); + assertThat(resp1.getNotifications()) + .usingRecursiveFieldByFieldElementComparator() + .isEqualTo(notifications); + + builder.query(NUMBER_FIELD_CODE + " >= 5").title("n5").user(getDefaultUser()); + notifications.add(perRecordNotification("n5", NUMBER_FIELD_CODE + " >= 5", t1)); + app.updateRecordNotifications(builder); + + GetPerRecordNotificationsPreviewRequest req2 = new GetPerRecordNotificationsPreviewRequest(); + req2.setApp(app.id()); + req2.setLang("default"); + GetPerRecordNotificationsPreviewResponseBody resp2 = + client.app().getPerRecordNotificationsPreview(req2); + assertThat(resp2.getRevision()).isEqualTo(revision + 1); + assertThat(resp2.getNotifications()) + .usingRecursiveFieldByFieldElementComparator() + .isEqualTo(notifications); + } + + @Test + public void getReminderNotifications_getReminderNotificationsPreview() { + ReminderNotificationsBuilder builder = new ReminderNotificationsBuilder(); + builder.timezone("Asia/Tokyo"); + builder + .field(app.field(DATETIME_FIELD_CODE), 3, "12:00") + .title("n1") + .query(NUMBER_FIELD_CODE + " >= 1") + .user(getDefaultUser()); + builder + .field(app.field(DATETIME_FIELD_CODE), -2, 1) + .title("n2") + .everyone() + .user(getDefaultUser()); + builder.field(app.field(DATETIME_FIELD_CODE), 1, -3).title("n3").field(USER_SELECT_FIELD_CODE); + builder + .field(app.field(DATE_FIELD_CODE), -5, "23:50") + .title("n4") + .org(Orgs.org1.getCode(), true) + .org(Orgs.org2.getCode(), false); + app.updateReminderNotifications(builder).deploy(); + long revision = app.getAppRevision(false); + + List notifications = new ArrayList<>(); + ReminderTiming r1 = timingAbsolute(DATETIME_FIELD_CODE, 3, "12:00"); + ReminderTiming r2 = timingRelative(DATETIME_FIELD_CODE, -2, 1); + ReminderTiming r3 = timingRelative(DATETIME_FIELD_CODE, 1, -3); + ReminderTiming r4 = timingDate(DATE_FIELD_CODE, -5, "23:50"); + + List t1 = makeTargets(EntityType.USER, getDefaultUser()); + List t2 = + makeTargets(EntityType.GROUP, "everyone", EntityType.USER, getDefaultUser()); + List t3 = makeTargets(EntityType.FIELD_ENTITY, USER_SELECT_FIELD_CODE); + List t4 = + makeTargets(EntityType.ORGANIZATION, Orgs.org1, EntityType.ORGANIZATION, Orgs.org2); + t4.get(0).setIncludeSubs(true); + notifications.add(reminderNotification(r1, "n1", NUMBER_FIELD_CODE + " >= 1", t1)); + notifications.add(reminderNotification(r2, "n2", "", t2)); + notifications.add(reminderNotification(r3, "n3", "", t3)); + notifications.add(reminderNotification(r4, "n4", "", t4)); + + GetReminderNotificationsRequest req1 = new GetReminderNotificationsRequest(); + req1.setApp(app.id()); + req1.setLang("default"); + GetReminderNotificationsResponseBody resp1 = client.app().getReminderNotifications(req1); + assertThat(resp1.getRevision()).isEqualTo(revision); + assertThat(resp1.getTimezone()).isEqualTo("Asia/Tokyo"); + assertThat(resp1.getNotifications()) + .usingRecursiveFieldByFieldElementComparator() + .isEqualTo(notifications); + + app.updateReminderNotifications(new ReminderNotificationsBuilder().timezone("UTC")); + + GetReminderNotificationsPreviewRequest req2 = new GetReminderNotificationsPreviewRequest(); + req2.setApp(app.id()); + req2.setLang("default"); + GetReminderNotificationsPreviewResponseBody resp2 = + client.app().getReminderNotificationsPreview(req2); + assertThat(resp2.getRevision()).isEqualTo(revision + 1); + assertThat(resp2.getTimezone()).isEqualTo("UTC"); + assertThat(resp2.getNotifications()) + .usingRecursiveFieldByFieldElementComparator() + .isEqualTo(notifications); + } + + @Test + public void updateGeneralNotifications() { + long revision = app.getAppRevision(true); + + List notifications = new ArrayList<>(); + Entity user = new Entity(EntityType.USER, getDefaultUser()); + Entity group = new Entity(EntityType.GROUP, "everyone"); + Entity org = new Entity(EntityType.ORGANIZATION, Orgs.org1.getCode()); + Entity field = new Entity(EntityType.FIELD_ENTITY, USER_SELECT_FIELD_CODE); + notifications.add(generalNotification(user, true, true, true, true, true)); + notifications.add(generalNotification(group, false, false, false, false, false)); + notifications.add( + generalNotification(org, false, false, false, true, true).setIncludeSubs(true)); + notifications.add(generalNotification(field, true, true, false, false, false)); + + UpdateGeneralNotificationsRequest req = new UpdateGeneralNotificationsRequest(); + req.setApp(app.id()); + req.setRevision(revision); + req.setNotifyToCommenter(true); + req.setNotifications(notifications); + UpdateGeneralNotificationsResponseBody resp = client.app().updateGeneralNotifications(req); + assertThat(resp.getRevision()).isEqualTo(revision + 1); + + App.GeneralNotifications settings = app.getGeneralNotifications(true); + assertThat(settings.isNotifyToCommenter()).isTrue(); + assertThat(settings.getNotifications()) + .usingRecursiveFieldByFieldElementComparator() + .isEqualTo(notifications); + } + + @Test + public void updatePerRecordNotifications() { + long revision = app.getAppRevision(true); + + List notifications = new ArrayList<>(); + List t1 = makeTargets(EntityType.USER, getDefaultUser()); + List t2 = + makeTargets(EntityType.GROUP, "everyone", EntityType.USER, getDefaultUser()); + List t3 = + makeTargets(EntityType.ORGANIZATION, Orgs.org1, EntityType.ORGANIZATION, Orgs.org2); + t3.get(0).setIncludeSubs(true); + List t4 = makeTargets(EntityType.FIELD_ENTITY, USER_SELECT_FIELD_CODE); + notifications.add(perRecordNotification("n1", NUMBER_FIELD_CODE + " >= 1", t1)); + notifications.add(perRecordNotification("n2", NUMBER_FIELD_CODE + " >= 2", t2)); + notifications.add(perRecordNotification("n3", NUMBER_FIELD_CODE + " >= 3", t3)); + notifications.add(perRecordNotification("n4", NUMBER_FIELD_CODE + " >= 4", t4)); + + UpdatePerRecordNotificationsRequest req = new UpdatePerRecordNotificationsRequest(); + req.setApp(app.id()); + req.setRevision(revision); + req.setNotifications(notifications); + UpdatePerRecordNotificationsResponseBody resp = client.app().updatePerRecordNotifications(req); + assertThat(resp.getRevision()).isEqualTo(revision + 1); + + List settings = app.getRecordNotifications(true); + assertThat(settings).containsExactly(notifications.toArray(new PerRecordNotification[0])); + } + + @Test + public void updateReminderNotifications() { + long revision = app.getAppRevision(true); + + List notifications = new ArrayList<>(); + ReminderTiming r1 = timingAbsolute(DATETIME_FIELD_CODE, 3, "12:00"); + ReminderTiming r2 = timingRelative(DATETIME_FIELD_CODE, -2, 1); + ReminderTiming r3 = timingRelative(DATETIME_FIELD_CODE, 1, -3); + ReminderTiming r4 = timingDate(DATE_FIELD_CODE, -5, "23:50"); + + List t1 = makeTargets(EntityType.USER, getDefaultUser()); + List t2 = + makeTargets(EntityType.GROUP, "everyone", EntityType.USER, getDefaultUser()); + List t3 = makeTargets(EntityType.FIELD_ENTITY, USER_SELECT_FIELD_CODE); + List t4 = + makeTargets(EntityType.ORGANIZATION, Orgs.org1, EntityType.ORGANIZATION, Orgs.org2); + t4.get(0).setIncludeSubs(true); + notifications.add(reminderNotification(r1, "n1", NUMBER_FIELD_CODE + " >= 1", t1)); + notifications.add(reminderNotification(r2, "n2", "", t2)); + notifications.add(reminderNotification(r3, "n3", "", t3)); + notifications.add(reminderNotification(r4, "n4", "", t4)); + + UpdateReminderNotificationsRequest req = new UpdateReminderNotificationsRequest(); + req.setApp(app.id()); + req.setRevision(revision); + req.setNotifications(notifications); + req.setTimezone("Asia/Tokyo"); + UpdateReminderNotificationsResponseBody resp = client.app().updateReminderNotifications(req); + assertThat(resp.getRevision()).isEqualTo(revision + 1); + + App.ReminderNotifications settings = app.getReminderNotifications(true); + assertThat(settings.getNotifications()) + .containsExactly(notifications.toArray(new ReminderNotification[0])); + assertThat(settings.getTimezone()).isEqualTo("Asia/Tokyo"); + } + + private GeneralNotification generalNotification( + Entity entity, + boolean recordAdded, + boolean recordEdited, + boolean commentAdded, + boolean statusChanged, + boolean fileImported) { + GeneralNotification setting = new GeneralNotification(); + setting.setEntity(entity); + setting.setIncludeSubs(false); + setting.setRecordAdded(recordAdded); + setting.setRecordEdited(recordEdited); + setting.setCommentAdded(commentAdded); + setting.setStatusChanged(statusChanged); + setting.setFileImported(fileImported); + return setting; + } + + private PerRecordNotification perRecordNotification( + String title, String query, List targets) { + PerRecordNotification setting = new PerRecordNotification(); + setting.setTitle(title); + setting.setFilterCond(query); + setting.setTargets(targets); + return setting; + } + + private ReminderNotification reminderNotification( + ReminderTiming timing, String title, String query, List targets) { + ReminderNotification setting = new ReminderNotification(); + setting.setTiming(timing); + setting.setTitle(title); + setting.setFilterCond(query); + setting.setTargets(targets); + return setting; + } + + private List makeTargets(Object... args) { + List targets = new ArrayList<>(); + for (int i = 0; i < args.length; i += 2) { + EntityType type = (EntityType) args[i]; + Object obj = args[i + 1]; + String code; + if (obj instanceof UserSetting) { + code = ((UserSetting) obj).getCode(); + } else if (obj instanceof OrgSetting) { + code = ((OrgSetting) obj).getCode(); + } else { + code = (String) obj; + } + Entity entity = new Entity(type, code); + targets.add(new NotificationTarget().setEntity(entity).setIncludeSubs(false)); + } + return targets; + } + + private ReminderTiming timingAbsolute(String code, int relativeDays, String time) { + ReminderTiming t = new ReminderTiming(); + t.setCode(code); + t.setDaysLater(relativeDays); + t.setTime(time); + return t; + } + + private ReminderTiming timingRelative(String code, int relativeDays, int relativeHours) { + ReminderTiming t = new ReminderTiming(); + t.setCode(code); + t.setDaysLater(relativeDays); + t.setHoursLater(relativeHours); + return t; + } + + private ReminderTiming timingDate(String code, int relativeDays, String time) { + ReminderTiming t = new ReminderTiming(); + t.setCode(code); + t.setDaysLater(relativeDays); + t.setTime(time); + return t; + } +} diff --git a/e2e-tests/src/test/java/com/kintone/client/app/ProcessManagementTest.java b/e2e-tests/src/test/java/com/kintone/client/app/ProcessManagementTest.java new file mode 100644 index 0000000..08a04cd --- /dev/null +++ b/e2e-tests/src/test/java/com/kintone/client/app/ProcessManagementTest.java @@ -0,0 +1,183 @@ +package com.kintone.client.app; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.kintone.client.ApiTestBase; +import com.kintone.client.KintoneClient; +import com.kintone.client.TestSettings; +import com.kintone.client.api.app.*; +import com.kintone.client.helper.App; +import com.kintone.client.helper.ProcessManagementBuilder; +import com.kintone.client.model.Entity; +import com.kintone.client.model.EntityType; +import com.kintone.client.model.app.*; +import java.util.*; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** AppClientのstatus.jsonのテスト */ +public class ProcessManagementTest extends ApiTestBase { + + private static final String NUMBER_FIELD_CODE = "数値"; + + private KintoneClient client; + private App app; + + @BeforeEach + public void setupApp() { + client = setupDefaultClient(); + // プロセス管理テストは専用のアプリを使用(プロセス管理の状態が他テストに影響するため) + Long testAppId = TestSettings.get().getTestAppIdForProcessManagement(); + if (testAppId != null) { + app = App.fromExisting(client, testAppId); + } else { + throw new IllegalStateException( + "KINTONE_TEST_APP_ID_FOR_PROCESS_MANAGEMENT is not set. Please create a test app for process management tests."); + } + // テスト開始前にプロセス管理を無効化してクリーンな状態にする + try { + app.updateProcessManagement(new ProcessManagementBuilder().enable(false)).deploy(); + } catch (Exception e) { + // ignore if already disabled + } + } + + @AfterEach + public void cleanupProcessManagement() { + if (app != null) { + try { + // プロセス管理を無効化してデプロイ(次のテストのためにクリーンな状態に戻す) + app.updateProcessManagement(new ProcessManagementBuilder().enable(false)).deploy(); + } catch (Exception e) { + // ignore cleanup errors + } + } + } + + @Test + public void getProcessManagement_getProcessManagementPreview() { + // このテストでは、updateProcessManagementと同じステータス名を使用して + // テスト間の依存関係を避ける(kintoneは既存ステータスの先頭位置変更を許可しない) + ProcessAssignee assignee = assignee(ProcessAssigneeType.ONE, Collections.emptyList()); + Map states = new HashMap<>(); + states.put("S0", new ProcessState().setName("S0").setIndex("0").setAssignee(assignee)); + states.put("S1", new ProcessState().setName("S1").setIndex("1").setAssignee(assignee)); + states.put("S2", new ProcessState().setName("S2").setIndex("2").setAssignee(assignee)); + + List actions = new ArrayList<>(); + actions.add( + new ProcessAction() + .setFrom("S0") + .setTo("S1") + .setName("action 1") + .setFilterCond("") + .setType(ProcessActionType.PRIMARY)); + actions.add( + new ProcessAction() + .setFrom("S1") + .setTo("S2") + .setName("action 2") + .setFilterCond("") + .setType(ProcessActionType.PRIMARY)); + + // プロセス管理を有効化 + UpdateProcessManagementRequest updateReq = new UpdateProcessManagementRequest(); + updateReq.setApp(app.id()); + updateReq.setEnable(true); + updateReq.setStates(states); + updateReq.setActions(actions); + client.app().updateProcessManagement(updateReq); + client.app().deployApp(app.id()); + app.waitDeploy(); + + long revision = app.getAppRevision(false); + + GetProcessManagementRequest req1 = new GetProcessManagementRequest(); + req1.setApp(app.id()); + GetProcessManagementResponseBody resp1 = client.app().getProcessManagement(req1); + assertThat(resp1.isEnable()).isTrue(); + assertThat(resp1.getStates()).usingRecursiveComparison().isEqualTo(states); + assertThat(resp1.getActions()).usingRecursiveComparison().isEqualTo(actions); + assertThat(resp1.getRevision()).isEqualTo(revision); + + app.updateProcessManagement(new ProcessManagementBuilder().enable(false)); + + GetProcessManagementPreviewRequest req2 = new GetProcessManagementPreviewRequest(); + req2.setApp(app.id()); + GetProcessManagementPreviewResponseBody resp2 = client.app().getProcessManagementPreview(req2); + assertThat(resp2.isEnable()).isFalse(); + assertThat(resp2.getStates()).usingRecursiveComparison().isEqualTo(states); + assertThat(resp2.getActions()).usingRecursiveComparison().isEqualTo(actions); + assertThat(resp2.getRevision()).isEqualTo(revision + 1); + } + + @Test + public void updateProcessManagement() { + long revision = app.getAppRevision(true); + + List entities = new ArrayList<>(); + entities.add(entity(EntityType.USER, getDefaultUser(), false)); + entities.add(entity(EntityType.GROUP, "everyone", false)); + ProcessAssignee assignee1 = assignee(ProcessAssigneeType.ONE, entities); + ProcessAssignee assignee2 = assignee(ProcessAssigneeType.ALL, entities); + ProcessAssignee assignee3 = assignee(ProcessAssigneeType.ANY, entities); + Map states = new HashMap<>(); + states.put("S0", new ProcessState().setName("S0").setIndex("0")); + states.put("S1", new ProcessState().setName("S1").setIndex("1").setAssignee(assignee1)); + states.put("S2", new ProcessState().setName("S2").setIndex("2").setAssignee(assignee2)); + states.put("S3", new ProcessState().setName("S3").setIndex("3").setAssignee(assignee3)); + + List actions = new ArrayList<>(); + String query = NUMBER_FIELD_CODE + " >= 10"; + actions.add( + new ProcessAction() + .setFrom("S0") + .setTo("S1") + .setName("0_1") + .setFilterCond("") + .setType(ProcessActionType.PRIMARY)); + actions.add( + new ProcessAction() + .setFrom("S1") + .setTo("S2") + .setName("1_2") + .setFilterCond(query) + .setType(ProcessActionType.PRIMARY)); + actions.add( + new ProcessAction() + .setFrom("S2") + .setTo("S3") + .setName("2_3") + .setFilterCond("") + .setType(ProcessActionType.PRIMARY)); + + UpdateProcessManagementRequest req = new UpdateProcessManagementRequest(); + req.setApp(app.id()); + req.setEnable(true); + req.setStates(states); + req.setActions(actions); + req.setRevision(revision); + UpdateProcessManagementResponseBody resp = client.app().updateProcessManagement(req); + assertThat(resp.getRevision()).isEqualTo(revision + 1); + + App.ProcessManagement settings = app.getProcessManagement(true); + // assignee設定が空の場合の初期値を入れる + ProcessAssignee assignee0 = assignee(ProcessAssigneeType.ONE, Collections.emptyList()); + states.get("S0").setAssignee(assignee0); + assertThat(settings.isEnable()).isTrue(); + assertThat(settings.getStates()).usingRecursiveComparison().isEqualTo(states); + assertThat(settings.getActions()).usingRecursiveComparison().isEqualTo(actions); + assertThat(settings.getRevision()).isEqualTo(revision + 1); + } + + private ProcessEntity entity(EntityType type, String code, boolean includeSubs) { + ProcessEntity e = new ProcessEntity(); + e.setEntity(new Entity(type, code)).setIncludeSubs(includeSubs); + return e; + } + + private ProcessAssignee assignee(ProcessAssigneeType type, List entities) { + return new ProcessAssignee().setType(type).setEntities(entities); + } +} diff --git a/e2e-tests/src/test/java/com/kintone/client/app/RecordAclTest.java b/e2e-tests/src/test/java/com/kintone/client/app/RecordAclTest.java new file mode 100644 index 0000000..7b50e27 --- /dev/null +++ b/e2e-tests/src/test/java/com/kintone/client/app/RecordAclTest.java @@ -0,0 +1,207 @@ +package com.kintone.client.app; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.kintone.client.ApiTestBase; +import com.kintone.client.KintoneClient; +import com.kintone.client.TestSettings; +import com.kintone.client.api.app.*; +import com.kintone.client.helper.App; +import com.kintone.client.helper.FieldAclBuilder; +import com.kintone.client.helper.RecordAclBuilder; +import com.kintone.client.model.Entity; +import com.kintone.client.model.EntityType; +import com.kintone.client.model.app.*; +import com.kintone.client.model.app.field.FieldProperty; +import java.util.Arrays; +import java.util.List; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** AppClientのレコードアクセス権設定に関するテスト */ +public class RecordAclTest extends ApiTestBase { + + private static final String TEXT_FIELD_CODE = "文字列__1行_"; + private static final String NUMBER_FIELD_CODE = "数値"; + private static final String USER_SELECT_FIELD_CODE = "ユーザー選択"; + + private KintoneClient client; + private App app; + private List originalRecordAcl; + private List originalFieldAcl; + + @BeforeEach + public void setupApp() { + client = setupDefaultClient(); + Long testAppId = TestSettings.get().getTestAppId(); + if (testAppId != null) { + app = App.fromExisting(client, testAppId); + } else { + throw new IllegalStateException( + "KINTONE_TEST_APP_ID is not set. Please create a test app and set the environment variable."); + } + // 元のACL設定を保存 + originalRecordAcl = app.getRecordAcl(false); + originalFieldAcl = app.getFieldAcl(false); + } + + @AfterEach + public void cleanupAcl() { + if (app != null) { + try { + // 元のACL設定に戻す + UpdateRecordAclRequest recordReq = new UpdateRecordAclRequest(); + recordReq.setApp(app.id()); + recordReq.setRights(originalRecordAcl); + client.app().updateRecordAcl(recordReq); + + UpdateFieldAclRequest fieldReq = new UpdateFieldAclRequest(); + fieldReq.setApp(app.id()); + fieldReq.setRights(originalFieldAcl); + client.app().updateFieldAcl(fieldReq); + + client.app().deployApp(app.id()); + app.waitDeploy(); + + // レコードを削除 + app.deleteAllRecords(); + } catch (Exception e) { + // ignore cleanup errors + } + } + } + + @Test + public void evaluateRecordAcl() { + FieldProperty text = app.field(TEXT_FIELD_CODE); + FieldProperty number = app.field(NUMBER_FIELD_CODE); + FieldProperty userSelect = app.field(USER_SELECT_FIELD_CODE); + + // 先にレコードを作成して、実際のIDを取得 + long recordId1 = app.addRecord(); + long recordId2 = app.addRecord(); + + // recordId2以上のIDを持つレコードは削除不可にする + RecordAclBuilder recordAcl = new RecordAclBuilder(); + recordAcl + .target("$id >= " + recordId2) + .user(getDefaultUser(), true, true, false) + .everyone(false, false, false); + app.updateRecordAcl(recordAcl); + + FieldAclBuilder fieldAcl = new FieldAclBuilder(); + fieldAcl.target(text).user(getDefaultUser(), true, true).everyone(false, false); + fieldAcl.target(number).field(userSelect, true, true).everyone(true, false); + app.updateFieldAcl(fieldAcl).deploy(); + + EvaluateRecordAclRequest req = new EvaluateRecordAclRequest(); + req.setApp(app.id()); + req.setIds(Arrays.asList(recordId1, recordId2)); + EvaluateRecordAclResponseBody resp = client.app().evaluateRecordAcl(req); + assertThat(resp.getRights()).hasSize(2); + + EvaluatedRecordRight r1 = resp.getRights().get(0); + assertThat(r1.getId()).isEqualTo(recordId1); + assertThat(r1.getRecord()).isEqualTo(new EvaluatedRecordRightEntity(true, true, true)); + // フィールド数はテストアプリの構成に依存するため、ACL設定したフィールドが含まれていることを確認 + assertThat(r1.getFields()) + .containsEntry(text.getCode(), new EvaluatedFieldRightEntity(true, true)); + assertThat(r1.getFields()) + .containsEntry(number.getCode(), new EvaluatedFieldRightEntity(true, false)); + assertThat(r1.getFields()) + .containsEntry(userSelect.getCode(), new EvaluatedFieldRightEntity(true, true)); + + EvaluatedRecordRight r2 = resp.getRights().get(1); + assertThat(r2.getId()).isEqualTo(recordId2); + assertThat(r2.getRecord()).isEqualTo(new EvaluatedRecordRightEntity(true, true, false)); + // フィールド数はテストアプリの構成に依存するため、ACL設定したフィールドが含まれていることを確認 + assertThat(r2.getFields()) + .containsEntry(text.getCode(), new EvaluatedFieldRightEntity(true, true)); + assertThat(r2.getFields()) + .containsEntry(number.getCode(), new EvaluatedFieldRightEntity(true, false)); + assertThat(r2.getFields()) + .containsEntry(userSelect.getCode(), new EvaluatedFieldRightEntity(true, true)); + } + + @Test + public void getRecordAcl_getRecordAclPreview() { + String query = NUMBER_FIELD_CODE + " >= 10"; + RecordAclBuilder builder = new RecordAclBuilder(); + builder.target(query).user(getDefaultUser(), true, false, false).everyone(false, false, false); + builder.any().field(USER_SELECT_FIELD_CODE, true, false, true).everyone(true, true, true); + app.updateRecordAcl(builder).deploy(); + long revision = app.getAppRevision(true); + + RecordRight r1 = + right( + query, + entity(EntityType.USER, getDefaultUser(), true, false, false), + entity(EntityType.GROUP, "everyone", false, false, false)); + RecordRight r2 = + right( + "", + entity(EntityType.FIELD_ENTITY, USER_SELECT_FIELD_CODE, true, false, true), + entity(EntityType.GROUP, "everyone", true, true, true)); + + GetRecordAclRequest req1 = new GetRecordAclRequest(); + req1.setApp(app.id()); + GetRecordAclResponseBody resp1 = client.app().getRecordAcl(req1); + assertThat(resp1.getRevision()).isEqualTo(revision); + assertThat(resp1.getRights()).containsExactly(r1, r2); + + builder = new RecordAclBuilder().any().everyone(true, true, false); + app.updateRecordAcl(builder).deploy(); + RecordRight r3 = right("", entity(EntityType.GROUP, "everyone", true, true, false)); + + GetRecordAclPreviewRequest req2 = new GetRecordAclPreviewRequest(); + req2.setApp(app.id()); + GetRecordAclPreviewResponseBody resp2 = client.app().getRecordAclPreview(req2); + assertThat(resp2.getRevision()).isEqualTo(revision + 1); + assertThat(resp2.getRights()).containsExactly(r3); + } + + @Test + public void updateRecordAcl() { + long revision = app.getAppRevision(true); + + String query = NUMBER_FIELD_CODE + " >= 10"; + RecordRight r1 = + right( + query, + entity(EntityType.USER, getDefaultUser(), true, true, true), + entity(EntityType.GROUP, "everyone", false, false, false)); + RecordRight r2 = + right( + "", + entity(EntityType.FIELD_ENTITY, USER_SELECT_FIELD_CODE, true, false, false), + entity(EntityType.GROUP, "everyone", true, true, true)); + + UpdateRecordAclRequest req = new UpdateRecordAclRequest(); + req.setApp(app.id()); + req.setRevision(revision); + req.setRights(Arrays.asList(r1, r2)); + UpdateRecordAclResponseBody resp = client.app().updateRecordAcl(req); + assertThat(resp.getRevision()).isEqualTo(revision + 1); + + List previewRights = app.getRecordAcl(true); + assertThat(previewRights).containsExactly(r1, r2); + + List deployedRights = app.getRecordAcl(false); + assertThat(deployedRights).isEmpty(); + } + + private RecordRight right(String query, RecordRightEntity... entities) { + return new RecordRight().setFilterCond(query).setEntities(Arrays.asList(entities)); + } + + private RecordRightEntity entity( + EntityType type, String code, boolean viewable, boolean editable, boolean deletable) { + return new RecordRightEntity() + .setEntity(new Entity(type, code)) + .setViewable(viewable) + .setEditable(editable) + .setDeletable(deletable) + .setIncludeSubs(false); + } +} diff --git a/e2e-tests/src/test/java/com/kintone/client/app/ReportsTest.java b/e2e-tests/src/test/java/com/kintone/client/app/ReportsTest.java new file mode 100644 index 0000000..f669030 --- /dev/null +++ b/e2e-tests/src/test/java/com/kintone/client/app/ReportsTest.java @@ -0,0 +1,228 @@ +package com.kintone.client.app; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.kintone.client.ApiTestBase; +import com.kintone.client.KintoneClient; +import com.kintone.client.TestSettings; +import com.kintone.client.api.app.*; +import com.kintone.client.helper.App; +import com.kintone.client.model.Order; +import com.kintone.client.model.app.report.*; +import java.time.DayOfWeek; +import java.time.LocalTime; +import java.util.*; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** AppClientのreports.jsonのテスト */ +public class ReportsTest extends ApiTestBase { + + private static final String NUMBER_FIELD_CODE = "数値"; + + private KintoneClient client; + private App app; + private Map originalReports; + + @BeforeEach + public void setupApp() { + client = setupDefaultClient(); + Long testAppId = TestSettings.get().getTestAppId(); + if (testAppId != null) { + app = App.fromExisting(client, testAppId); + } else { + throw new IllegalStateException( + "KINTONE_TEST_APP_ID is not set. Please create a test app and set the environment variable."); + } + // 元のレポート設定を保存 + originalReports = app.getReports(false); + } + + @AfterEach + public void cleanupReports() { + if (app != null) { + try { + // 元のレポート設定に戻す + client.app().updateReports(app.id(), originalReports); + client.app().deployApp(app.id()); + app.waitDeploy(); + } catch (Exception e) { + // ignore cleanup errors + } + } + } + + @Test + public void getReports_getReportsPreview() { + Report report = barGraph(0, "棒グラフ", NUMBER_FIELD_CODE + " >= 1"); + app.updateReports(report).deploy(); + long revision = app.getAppRevision(false); + + GetReportsRequest req1 = new GetReportsRequest(); + req1.setApp(app.id()); + GetReportsResponseBody resp1 = client.app().getReports(req1); + assertThat(resp1.getRevision()).isEqualTo(revision); + Map reports = resp1.getReports(); + assertThat(reports).containsOnlyKeys("棒グラフ"); + assertThat(reports.get("棒グラフ")) + .usingRecursiveComparison() + .ignoringFieldsMatchingRegexes("id") + .isEqualTo(report); + + Report report2 = + new Report() + .setName("グラフ2") + .setChartType(ChartType.BAR) + .setChartMode(ChartMode.NORMAL) + .setIndex(0L); + app.updateReports(Collections.singletonMap("棒グラフ", report2)); + + GetReportsPreviewRequest req2 = new GetReportsPreviewRequest(); + req2.setApp(app.id()); + GetReportsPreviewResponseBody resp2 = client.app().getReportsPreview(req2); + assertThat(resp2.getRevision()).isEqualTo(revision + 1); + reports = resp2.getReports(); + report.setName("グラフ2"); + assertThat(reports).containsOnlyKeys("グラフ2"); + assertThat(reports.get("グラフ2")) + .usingRecursiveComparison() + .ignoringFieldsMatchingRegexes("id") + .isEqualTo(report); + } + + @Test + public void updateReports() { + long revision = app.getAppRevision(true); + + // グラフ0件 + UpdateReportsRequest req1 = new UpdateReportsRequest(); + req1.setApp(app.id()).setReports(Collections.emptyMap()).setRevision(revision); + UpdateReportsResponseBody resp1 = client.app().updateReports(req1); + assertThat(resp1.getRevision()).isEqualTo(revision + 1); + assertThat(resp1.getReports()).isEmpty(); + + revision += 1; + String query = NUMBER_FIELD_CODE + " >= 1"; + Map reports = new HashMap<>(); + reports.put("棒グラフ", barGraph(0, "棒グラフ", query)); + reports.put("毎年", barGraph(1, "毎年", query).setPeriodicReport(everyYear(12, 31, 23, 59))); + PeriodicReport quarterly = everyQuarter(QuarterlyPattern.FEB_MAY_AUG_NOV, 1, 23); + reports.put("毎四半期", barGraph(2, "毎四半期", query).setPeriodicReport(quarterly)); + reports.put("毎月", barGraph(3, "毎月", query).setPeriodicReport(everyMonth(15, 12, 00))); + PeriodicReport weekly = everyWeek(DayOfWeek.SUNDAY, 9, 30); + reports.put("毎週", barGraph(4, "毎週", query).setPeriodicReport(weekly)); + reports.put("毎日", barGraph(5, "毎日", query).setPeriodicReport(everyDay(15, 45))); + reports.put("毎時", barGraph(6, "毎時", query).setPeriodicReport(everyHour(50))); + + UpdateReportsRequest req2 = new UpdateReportsRequest(); + req2.setApp(app.id()).setReports(reports).setRevision(revision); + UpdateReportsResponseBody resp2 = client.app().updateReports(req2); + assertThat(resp2.getRevision()).isEqualTo(revision + 1); + assertThat(resp2.getReports()).hasSize(7); + for (String name : reports.keySet()) { + assertThat(resp2.getReports().get(name).getId()).isGreaterThan(0); + } + + updateReportIds(reports, resp2.getReports()); + Map settings = app.getReports(true); + for (String name : reports.keySet()) { + assertThat(settings.get(name)).usingRecursiveComparison().isEqualTo(reports.get(name)); + } + } + + private void updateReportIds(Map reports, Map ids) { + for (Report report : reports.values()) { + report.setId(ids.get(report.getName()).getId()); + } + } + + private Report barGraph(long index, String name, String query) { + Report report = new Report(); + report.setName(name); + report.setIndex(index); + report.setChartType(ChartType.BAR); + report.setChartMode(ChartMode.NORMAL); + report.setFilterCond(query); + report.setGroups(Arrays.asList(group("作成者"))); + report.setAggregations(Arrays.asList(aggCount())); + report.setSorts(Arrays.asList(sort(0, Order.ASC), sort(1, Order.DESC))); + return report; + } + + private AggregationGroup group(String code) { + AggregationGroup group = new AggregationGroup(); + group.setCode(code); + return group; + } + + private AggregationSetting aggCount() { + AggregationSetting agg = new AggregationSetting(); + agg.setType(AggregationFunction.COUNT); + return agg; + } + + private AggregationSetting aggAvg(String code) { + AggregationSetting agg = new AggregationSetting(); + agg.setType(AggregationFunction.AVERAGE); + agg.setCode(code); + return agg; + } + + private AggregationSort sort(int i, Order order) { + AggregationSort sort = new AggregationSort(); + if (i == 1) { + sort.setBy(AggregationSortTarget.GROUP1); + } else if (i == 2) { + sort.setBy(AggregationSortTarget.GROUP2); + } else if (i == 3) { + sort.setBy(AggregationSortTarget.GROUP3); + } else { + sort.setBy(AggregationSortTarget.TOTAL); + } + sort.setOrder(order); + return sort; + } + + private PeriodicReport everyYear(int month, int day, int hour, int minute) { + EveryYearPeriod p = new EveryYearPeriod(); + p.setMonth(month); + p.setDayOfMonth(day); + p.setTime(LocalTime.of(hour, minute)); + return new PeriodicReport().setActive(true).setPeriod(p); + } + + private PeriodicReport everyQuarter(QuarterlyPattern pattern, int hour, int minute) { + EveryQuarterPeriod p = new EveryQuarterPeriod(); + p.setPattern(pattern); + p.setEndOfMonth(); + p.setTime(LocalTime.of(hour, minute)); + return new PeriodicReport().setActive(true).setPeriod(p); + } + + private PeriodicReport everyMonth(int day, int hour, int minute) { + EveryMonthPeriod p = new EveryMonthPeriod(); + p.setDayOfMonth(day); + p.setTime(LocalTime.of(hour, minute)); + return new PeriodicReport().setActive(true).setPeriod(p); + } + + private PeriodicReport everyWeek(DayOfWeek dayOfWeek, int hour, int minute) { + EveryWeekPeriod p = new EveryWeekPeriod(); + p.setDayOfWeek(dayOfWeek); + p.setTime(LocalTime.of(hour, minute)); + return new PeriodicReport().setActive(true).setPeriod(p); + } + + private PeriodicReport everyDay(int hour, int minute) { + EveryDayPeriod p = new EveryDayPeriod(); + p.setTime(LocalTime.of(hour, minute)); + return new PeriodicReport().setActive(true).setPeriod(p); + } + + private PeriodicReport everyHour(int minute) { + EveryHourPeriod p = new EveryHourPeriod(); + p.setMinute(minute); + return new PeriodicReport().setActive(true).setPeriod(p); + } +} diff --git a/e2e-tests/src/test/java/com/kintone/client/app/ViewsTest.java b/e2e-tests/src/test/java/com/kintone/client/app/ViewsTest.java new file mode 100644 index 0000000..0d8ed07 --- /dev/null +++ b/e2e-tests/src/test/java/com/kintone/client/app/ViewsTest.java @@ -0,0 +1,206 @@ +package com.kintone.client.app; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.kintone.client.ApiTestBase; +import com.kintone.client.KintoneClient; +import com.kintone.client.TestSettings; +import com.kintone.client.api.app.*; +import com.kintone.client.helper.App; +import com.kintone.client.model.app.Device; +import com.kintone.client.model.app.View; +import com.kintone.client.model.app.ViewId; +import com.kintone.client.model.app.ViewType; +import java.util.*; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** AppClientのviews.jsonのテスト */ +public class ViewsTest extends ApiTestBase { + + private static final String TEXT_FIELD_CODE = "文字列__1行_"; + private static final String NUMBER_FIELD_CODE = "数値"; + private static final String DATE_FIELD_CODE = "日付"; + + private KintoneClient client; + private App app; + private Map originalViews; + + @BeforeEach + public void setupApp() { + client = setupDefaultClient(); + Long testAppId = TestSettings.get().getTestAppId(); + if (testAppId != null) { + app = App.fromExisting(client, testAppId); + } else { + throw new IllegalStateException( + "KINTONE_TEST_APP_ID is not set. Please create a test app and set the environment variable."); + } + // 元のビュー設定を保存 + originalViews = app.getViews(false); + } + + @AfterEach + public void cleanupViews() { + if (app != null) { + try { + // 現在のビュー設定を取得して、自動生成ビューを保持しつつ元に戻す + Map currentViews = app.getViews(true); + Map viewsToRestore = new HashMap<>(originalViews); + // プロセス管理で自動生成されるビュー(「(作業者が自分)」など)は削除不可なので保持 + for (Map.Entry entry : currentViews.entrySet()) { + String name = entry.getKey(); + if (name.startsWith("(") && name.endsWith(")")) { + viewsToRestore.put(name, entry.getValue()); + } + } + client.app().updateViews(app.id(), viewsToRestore); + client.app().deployApp(app.id()); + app.waitDeploy(); + } catch (Exception e) { + // ignore cleanup errors + } + } + } + + @Test + public void getViews_getViewsPreview() { + // 自動生成ビュー(プロセス管理)との重複を避けるため、index=100から開始 + View view = + listView("v1", 100, "", NUMBER_FIELD_CODE + " asc", TEXT_FIELD_CODE, NUMBER_FIELD_CODE); + // 自動生成ビューを保持しながら新しいビューを追加 + Map viewsToUpdate = preserveAutoGeneratedViews(app.getViews(false)); + viewsToUpdate.put("v1", view); + app.updateViews(viewsToUpdate).deploy(); + long revision = app.getAppRevision(false); + + GetViewsRequest req1 = new GetViewsRequest(); + req1.setApp(app.id()); + GetViewsResponseBody resp1 = client.app().getViews(req1); + assertThat(resp1.getRevision()).isEqualTo(revision); + Map views = resp1.getViews(); + assertThat(views).containsKey("v1"); + // indexはkintoneによって正規化されるため、idとindexを無視して比較 + assertThat(views.get("v1")) + .usingRecursiveComparison() + .ignoringFieldsMatchingRegexes("id", "index") + .isEqualTo(view); + + // 自動生成ビューとの重複を避けるため、index=101 + // LIST型のビューにはfieldsが必須 + View view2 = + listView("v2", 101, "", NUMBER_FIELD_CODE + " asc", TEXT_FIELD_CODE, NUMBER_FIELD_CODE); + Map viewsToUpdate2 = preserveAutoGeneratedViews(app.getViews(true)); + viewsToUpdate2.put("v2", view2); + app.updateViews(viewsToUpdate2); + + GetViewsPreviewRequest req2 = new GetViewsPreviewRequest(); + req2.setApp(app.id()); + GetViewsPreviewResponseBody resp2 = client.app().getViewsPreview(req2); + assertThat(resp2.getRevision()).isEqualTo(revision + 1); + views = resp2.getViews(); + assertThat(views).containsKey("v2"); + // indexはkintoneによって正規化されるため、idとindexを無視して比較 + assertThat(views.get("v2")) + .usingRecursiveComparison() + .ignoringFieldsMatchingRegexes("id", "index") + .isEqualTo(view2); + } + + @Test + public void updateViews() { + long revision = app.getAppRevision(true); + + // 自動生成ビューを保持しながら新しいビューを追加 + // 自動生成ビュー(プロセス管理)との重複を避けるため、index=100から開始 + Map views = preserveAutoGeneratedViews(app.getViews(true)); + String sort = DATE_FIELD_CODE + " desc"; + views.put("v1", listView("v1", 100, "", sort, TEXT_FIELD_CODE)); + views.put("v2", calendarView("v2", 101, TEXT_FIELD_CODE, DATE_FIELD_CODE, "", sort)); + views.put("v3", customizeView("v3", 102, "test", "", sort)); + + UpdateViewsRequest req = new UpdateViewsRequest(); + req.setApp(app.id()); + req.setRevision(revision); + req.setViews(views); + UpdateViewsResponseBody resp = client.app().updateViews(req); + assertThat(resp.getRevision()).isEqualTo(revision + 1); + assertThat(resp.getViews().get("v1").getId()).isGreaterThan(0); + assertThat(resp.getViews().get("v2").getId()).isGreaterThan(0); + assertThat(resp.getViews().get("v3").getId()).isGreaterThan(0); + updateViewIds(views, resp.getViews()); + + Map settings = app.getViews(true); + // indexはkintoneによって正規化されるため、indexを無視して比較 + assertThat(settings.get("v1")) + .usingRecursiveComparison() + .ignoringFields("index") + .isEqualTo(views.get("v1")); + assertThat(settings.get("v2")) + .usingRecursiveComparison() + .ignoringFields("index") + .isEqualTo(views.get("v2")); + assertThat(settings.get("v3")) + .usingRecursiveComparison() + .ignoringFields("index") + .isEqualTo(views.get("v3")); + } + + private void updateViewIds(Map views, Map ids) { + for (View view : views.values()) { + ViewId viewId = ids.get(view.getName()); + if (viewId != null) { + view.setId(viewId.getId()); + } + } + } + + private Map preserveAutoGeneratedViews(Map currentViews) { + Map result = new HashMap<>(); + for (Map.Entry entry : currentViews.entrySet()) { + String name = entry.getKey(); + if (name.startsWith("(") && name.endsWith(")")) { + result.put(name, entry.getValue()); + } + } + return result; + } + + private View listView(String name, long index, String query, String sort, String... fields) { + View v = new View(); + v.setName(name); + v.setType(ViewType.LIST); + v.setIndex(index); + v.setFields(Arrays.asList(fields)); + v.setFilterCond(query); + v.setSort(sort); + return v; + } + + private View calendarView( + String name, long index, String titleField, String dateField, String query, String sort) { + View v = new View(); + v.setName(name); + v.setType(ViewType.CALENDAR); + v.setIndex(index); + v.setTitle(titleField); + v.setDate(dateField); + v.setFilterCond(query); + v.setSort(sort); + return v; + } + + private View customizeView(String name, long index, String html, String query, String sort) { + View v = new View(); + v.setName(name); + v.setType(ViewType.CUSTOM); + v.setIndex(index); + v.setHtml(html); + v.setFilterCond(query); + v.setSort(sort); + v.setDevice(Device.ANY); + v.setPager(true); + return v; + } +} diff --git a/e2e-tests/src/test/java/com/kintone/client/bulk/BulkApiTest.java b/e2e-tests/src/test/java/com/kintone/client/bulk/BulkApiTest.java new file mode 100644 index 0000000..7f419ef --- /dev/null +++ b/e2e-tests/src/test/java/com/kintone/client/bulk/BulkApiTest.java @@ -0,0 +1,240 @@ +package com.kintone.client.bulk; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.kintone.client.ApiTestBase; +import com.kintone.client.KintoneClient; +import com.kintone.client.TestSettings; +import com.kintone.client.Users; +import com.kintone.client.api.common.BulkRequestsRequest; +import com.kintone.client.api.common.BulkRequestsResponseBody; +import com.kintone.client.api.record.*; +import com.kintone.client.helper.App; +import com.kintone.client.helper.ProcessManagementBuilder; +import com.kintone.client.model.User; +import com.kintone.client.model.app.field.FieldProperty; +import com.kintone.client.model.record.*; +import com.kintone.client.model.record.Record; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** bulkRequestのテスト */ +public class BulkApiTest extends ApiTestBase { + + private static final String TEXT_FIELD_CODE = "文字列__1行_"; + private static final String KEY_FIELD_CODE = "key"; + + private KintoneClient client; + private App app; + + @BeforeEach + public void setupApp() { + client = setupDefaultClient(); + Long testAppId = TestSettings.get().getTestAppId(); + if (testAppId != null) { + app = App.fromExisting(client, testAppId); + } else { + throw new IllegalStateException( + "KINTONE_TEST_APP_ID is not set. Please create a test app and set the environment variable."); + } + } + + @AfterEach + public void cleanupRecords() { + if (app != null) { + app.deleteAllRecords(); + } + } + + @Test + public void bulkRequests() { + // このテストにはプロセス管理が必要(ステータス変更を含むため) + app.applyExampleProcessManagement().deploy(); + + try { + bulkRequestsImpl(); + } finally { + // プロセス管理を無効化してクリーンな状態に戻す + app.updateProcessManagement(new ProcessManagementBuilder().enable(false)).deploy(); + } + } + + private void bulkRequestsImpl() { + FieldProperty key = app.field(KEY_FIELD_CODE); + FieldProperty field = app.field(TEXT_FIELD_CODE); + + long recordId1 = app.addRecord(key, "aaa", field, "initial value 0"); + long recordId2 = app.addRecord(key, "bbb", field, "initial value 1"); + long recordId3 = app.addRecord(key, "ccc", field, "initial value 2"); + long recordId4 = app.addRecord(key, "ddd", field, "initial value 3"); + long recordId5 = app.addRecord(key, "eee", field, "initial value 4"); + long recordId6 = recordId5 + 1; + long recordId7 = recordId6 + 1; + long recordId8 = recordId7 + 1; + BulkRequestsRequest req = new BulkRequestsRequest(); + + AddRecordRequest req1 = new AddRecordRequest(); + req1.setApp(app.id()); + req1.setRecord(new Record().putField(TEXT_FIELD_CODE, new SingleLineTextFieldValue("add"))); + req.registerAddRecord(req1); + + AddRecordsRequest req2 = new AddRecordsRequest(); + Record r1 = new Record().putField(TEXT_FIELD_CODE, new SingleLineTextFieldValue("adds 1")); + Record r2 = new Record().putField(TEXT_FIELD_CODE, new SingleLineTextFieldValue("adds 2")); + req2.setApp(app.id()); + req2.setRecords(Arrays.asList(r1, r2)); + req.registerAddRecords(req2); + + DeleteRecordsRequest req3 = new DeleteRecordsRequest(); + req3.setApp(app.id()); + req3.setIds(Collections.singletonList(recordId1)); + req3.setRevisions(Collections.singletonList(1L)); + req.registerDeleteRecords(req3); + + UpdateRecordRequest req4 = new UpdateRecordRequest(); + req4.setApp(app.id()); + req4.setId(recordId2); + req4.setRecord(new Record().putField(TEXT_FIELD_CODE, new SingleLineTextFieldValue("update"))); + req4.setRevision(1L); + req.registerUpdateRecord(req4); + + UpdateRecordRequest req5 = new UpdateRecordRequest(); + req5.setApp(app.id()); + req5.setUpdateKey(new UpdateKey(KEY_FIELD_CODE, "ccc")); + req5.setRecord( + new Record().putField(TEXT_FIELD_CODE, new SingleLineTextFieldValue("update by key"))); + req5.setRevision(1L); + req.registerUpdateRecord(req5); + + RecordForUpdate up1 = + new RecordForUpdate( + recordId4, + new Record().putField(TEXT_FIELD_CODE, new SingleLineTextFieldValue("updates 1")), + 1L); + RecordForUpdate up2 = + new RecordForUpdate( + new UpdateKey(KEY_FIELD_CODE, "eee"), + new Record().putField(TEXT_FIELD_CODE, new SingleLineTextFieldValue("updates 2")), + 1L); + UpdateRecordsRequest req6 = new UpdateRecordsRequest(); + req6.setApp(app.id()); + req6.setRecords(Arrays.asList(up1, up2)); + req.registerUpdateRecords(req6); + + UpdateRecordAssigneesRequest req7 = new UpdateRecordAssigneesRequest(); + req7.setApp(app.id()); + req7.setId(recordId2); + req7.setAssignees(Collections.singletonList(Users.cybozu.getCode())); + req7.setRevision(2L); // req4で更新されるため + req.registerUpdateRecordAssignees(req7); + + UpdateRecordStatusRequest req8 = new UpdateRecordStatusRequest(); + req8.setApp(app.id()); + req8.setId(recordId3); + req8.setAction("action 1"); + req8.setRevision(2L); // req5で更新されるため + req.registerUpdateRecordStatus(req8); + + StatusAction a1 = + new StatusAction().setId(recordId4).setAction("action 1").setRevision(2L); // req6で更新されるため + StatusAction a2 = new StatusAction().setId(recordId5).setAction("action 1").setRevision(2L); + UpdateRecordStatusesRequest req9 = new UpdateRecordStatusesRequest(); + req9.setApp(app.id()); + req9.setRecords(Arrays.asList(a1, a2)); + req.registerUpdateRecordStatuses(req9); + + // bulkRequest実行 + BulkRequestsResponseBody resp = client.bulkRequests(req); + assertThat(resp.getResults()).hasSize(9); + + AddRecordResponseBody resp1 = (AddRecordResponseBody) resp.getResults().get(0); + assertThat(resp1.getId()).isEqualTo(recordId6); + assertThat(resp1.getRevision()).isEqualTo(1L); + + AddRecordsResponseBody resp2 = (AddRecordsResponseBody) resp.getResults().get(1); + assertThat(resp2.getIds()).containsExactly(recordId7, recordId8); + assertThat(resp2.getRevisions()).containsExactly(1L, 1L); + + // delete結果は特にないので何も確認しない + DeleteRecordsResponseBody resp3 = (DeleteRecordsResponseBody) resp.getResults().get(2); + + UpdateRecordResponseBody resp4 = (UpdateRecordResponseBody) resp.getResults().get(3); + assertThat(resp4.getRevision()).isEqualTo(2L); + + UpdateRecordResponseBody resp5 = (UpdateRecordResponseBody) resp.getResults().get(4); + assertThat(resp5.getRevision()).isEqualTo(2L); + + UpdateRecordsResponseBody resp6 = (UpdateRecordsResponseBody) resp.getResults().get(5); + assertThat(resp6.getRecords()).hasSize(2); + assertThat(resp6.getRecords().get(0).getId()).isEqualTo(recordId4); + assertThat(resp6.getRecords().get(0).getRevision()).isEqualTo(2L); + assertThat(resp6.getRecords().get(1).getId()).isEqualTo(recordId5); + assertThat(resp6.getRecords().get(1).getRevision()).isEqualTo(2L); + + UpdateRecordAssigneesResponseBody resp7 = + (UpdateRecordAssigneesResponseBody) resp.getResults().get(6); + assertThat(resp7.getRevision()).isEqualTo(3); + + UpdateRecordStatusResponseBody resp8 = + (UpdateRecordStatusResponseBody) resp.getResults().get(7); + assertThat(resp8.getRevision()).isEqualTo(4); // ステータス変更は2進む + + UpdateRecordStatusesResponseBody resp9 = + (UpdateRecordStatusesResponseBody) resp.getResults().get(8); + assertThat(resp9.getRecords()).hasSize(2); + assertThat(resp9.getRecords().get(0).getId()).isEqualTo(recordId4); + assertThat(resp9.getRecords().get(0).getRevision()).isEqualTo(4); // ステータス変更は2進む + assertThat(resp9.getRecords().get(1).getId()).isEqualTo(recordId5); + assertThat(resp9.getRecords().get(1).getRevision()).isEqualTo(4); + + List records = app.getRecords(); + // recordId1が削除され、3レコード増えるので7件 + assertThat(records).hasSize(7); + + // 追加分の確認 + assertThat(records.get(0).getId()).isEqualTo(recordId8); + assertThat(records.get(0).getSingleLineTextFieldValue(TEXT_FIELD_CODE)).isEqualTo("adds 2"); + + assertThat(records.get(1).getId()).isEqualTo(recordId7); + assertThat(records.get(1).getSingleLineTextFieldValue(TEXT_FIELD_CODE)).isEqualTo("adds 1"); + + assertThat(records.get(2).getId()).isEqualTo(recordId6); + assertThat(records.get(2).getSingleLineTextFieldValue(TEXT_FIELD_CODE)).isEqualTo("add"); + + // 更新分の確認 + assertThat(records.get(3).getId()).isEqualTo(recordId5); + assertThat(records.get(3).getSingleLineTextFieldValue(KEY_FIELD_CODE)).isEqualTo("eee"); + assertThat(records.get(3).getSingleLineTextFieldValue(TEXT_FIELD_CODE)).isEqualTo("updates 2"); + assertThat(records.get(3).getStatusFieldValue()).isEqualTo("state B"); + assertThat(getAssigneeCodes(records.get(3))).isEmpty(); + + assertThat(records.get(4).getId()).isEqualTo(recordId4); + assertThat(records.get(4).getSingleLineTextFieldValue(KEY_FIELD_CODE)).isEqualTo("ddd"); + assertThat(records.get(4).getSingleLineTextFieldValue(TEXT_FIELD_CODE)).isEqualTo("updates 1"); + assertThat(records.get(4).getStatusFieldValue()).isEqualTo("state B"); + assertThat(getAssigneeCodes(records.get(4))).isEmpty(); + + assertThat(records.get(5).getId()).isEqualTo(recordId3); + assertThat(records.get(5).getSingleLineTextFieldValue(KEY_FIELD_CODE)).isEqualTo("ccc"); + assertThat(records.get(5).getSingleLineTextFieldValue(TEXT_FIELD_CODE)) + .isEqualTo("update by key"); + assertThat(records.get(5).getStatusFieldValue()).isEqualTo("state B"); + assertThat(getAssigneeCodes(records.get(5))).isEmpty(); + + assertThat(records.get(6).getId()).isEqualTo(recordId2); + assertThat(records.get(6).getSingleLineTextFieldValue(KEY_FIELD_CODE)).isEqualTo("bbb"); + assertThat(records.get(6).getSingleLineTextFieldValue(TEXT_FIELD_CODE)).isEqualTo("update"); + assertThat(records.get(6).getStatusFieldValue()).isEqualTo("state A"); + assertThat(getAssigneeCodes(records.get(6))).containsExactly(Users.cybozu.getCode()); + } + + private List getAssigneeCodes(Record record) { + List assignees = record.getStatusAssigneeFieldValue(); + return assignees.stream().map(User::getCode).collect(Collectors.toList()); + } +} diff --git a/e2e-tests/src/test/java/com/kintone/client/file/FileApiTest.java b/e2e-tests/src/test/java/com/kintone/client/file/FileApiTest.java new file mode 100644 index 0000000..d1ac5c2 --- /dev/null +++ b/e2e-tests/src/test/java/com/kintone/client/file/FileApiTest.java @@ -0,0 +1,85 @@ +package com.kintone.client.file; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.kintone.client.ApiTestBase; +import com.kintone.client.KintoneClient; +import com.kintone.client.TestSettings; +import com.kintone.client.api.common.DownloadFileRequest; +import com.kintone.client.api.common.DownloadFileResponseBody; +import com.kintone.client.api.common.UploadFileRequest; +import com.kintone.client.api.common.UploadFileResponseBody; +import com.kintone.client.helper.App; +import com.kintone.client.model.FileBody; +import com.kintone.client.model.app.field.FieldProperty; +import java.io.*; +import java.nio.charset.StandardCharsets; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** FileClientのテスト */ +public class FileApiTest extends ApiTestBase { + + private static final String FILE_FIELD_CODE = "添付ファイル"; + + private KintoneClient client; + private App app; + + @BeforeEach + public void setupApp() { + client = setupDefaultClient(); + Long testAppId = TestSettings.get().getTestAppId(); + if (testAppId != null) { + app = App.fromExisting(client, testAppId); + } else { + throw new IllegalStateException( + "KINTONE_TEST_APP_ID is not set. Please create a test app and set the environment variable."); + } + } + + @AfterEach + public void cleanupRecords() { + if (app != null) { + app.deleteAllRecords(); + } + } + + @Test + public void uploadFile_downloadFile() { + FieldProperty file = app.field(FILE_FIELD_CODE); + + UploadFileResponseBody resp1; + try (ByteArrayInputStream in = new ByteArrayInputStream("test".getBytes())) { + UploadFileRequest req1 = new UploadFileRequest(); + req1.setFilename("test.txt"); + req1.setContentType("text/plain"); + req1.setContent(in); + resp1 = client.file().uploadFile(req1); + } catch (IOException e) { + throw new RuntimeException(e); + } + + long recordId = app.addRecord(file, resp1.getFileKey()); + FileBody value = app.getRecord(recordId).getFileFieldValue(FILE_FIELD_CODE).get(0); + assertThat(value.getName()).isEqualTo("test.txt"); + assertThat(value.getContentType()).isEqualTo("text/plain"); + assertThat(value.getSize()).isEqualTo(4); + + DownloadFileRequest req2 = new DownloadFileRequest(); + req2.setFileKey(value.getFileKey()); + DownloadFileResponseBody resp2 = client.file().downloadFile(req2); + + assertThat(resp2.getContentType()).startsWith("text/plain"); // charsetが付属するため + assertThat(resp2.getContentLength()).isEqualTo(4); + String body; + try (InputStream in = resp2.getContent()) { + byte[] buffer = new byte[32]; + int size = in.read(buffer); + body = new String(buffer, 0, size, StandardCharsets.UTF_8); + } catch (IOException e) { + throw new RuntimeException(e); + } + assertThat(body).isEqualTo("test"); + } +} diff --git a/e2e-tests/src/test/java/com/kintone/client/helper/App.java b/e2e-tests/src/test/java/com/kintone/client/helper/App.java new file mode 100644 index 0000000..057b2be --- /dev/null +++ b/e2e-tests/src/test/java/com/kintone/client/helper/App.java @@ -0,0 +1,571 @@ +package com.kintone.client.helper; + +import com.kintone.client.KintoneClient; +import com.kintone.client.api.app.*; +import com.kintone.client.model.FileBody; +import com.kintone.client.model.Group; +import com.kintone.client.model.Organization; +import com.kintone.client.model.User; +import com.kintone.client.model.app.*; +import com.kintone.client.model.app.field.FieldProperty; +import com.kintone.client.model.app.layout.Layout; +import com.kintone.client.model.app.report.Report; +import com.kintone.client.model.record.*; +import com.kintone.client.model.record.Record; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalTime; +import java.time.ZonedDateTime; +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import lombok.Value; + +public class App { + private static final int MAX_DEPLOY_WAIT_SEC = 180; + + private final KintoneClient client; + private final long appId; + + // 組み込みフィールド + private final Map builtinFields = new TreeMap<>(); + // 組み込み以外・サブテーブル内フィールド以外の、フィールドコードがあるフィールド + private final Map customFields = new TreeMap<>(); + + public static App create(KintoneClient client, String name) { + long appId = client.app().addApp(name); + return new App(client, appId); + } + + public static App create(KintoneClient client, String name, long spaceId, long threadId) { + long appId = client.app().addApp(name, spaceId, threadId); + return new App(client, appId); + } + + public static App fromExisting(KintoneClient client, long appId) { + return new App(client, appId); + } + + private App(KintoneClient client, long appId) { + this.client = client; + this.appId = appId; + } + + /* キャッシュされたアプリ情報の取得 */ + public long id() { + return appId; + } + + public FieldProperty field(String code) { + FieldProperty field = findFieldByCode(code); + if (field != null) { + return field; + } + refreshFieldCache(); + return findFieldByCode(code); + } + + private FieldProperty findFieldByCode(String code) { + FieldProperty field = customFields.get(code); + if (field != null) { + return field; + } + return builtinFields.values().stream() + .filter(f -> code.equals(f.getCode())) + .findFirst() + .orElse(null); + } + + public FieldProperty field(FieldType type) { + FieldProperty field = findFirstFieldByType(type); + if (field != null) { + return field; + } + refreshFieldCache(); + return findFirstFieldByType(type); + } + + private FieldProperty findFirstFieldByType(FieldType type) { + FieldProperty field = builtinFields.get(type); + if (field != null) { + return field; + } + return customFields.values().stream().filter(f -> f.getType() == type).findFirst().orElse(null); + } + + // フィールド情報のキャッシュをクリアして取得し直す + private void refreshFieldCache() { + builtinFields.clear(); + customFields.clear(); + + Map fields = getFields(true); + updateFieldCache(fields.values()); + } + + // フィールド情報のキャッシュを追加・更新する。クリアはしない + private void updateFieldCache(Collection fields) { + for (FieldProperty field : fields) { + if (field.getType().isBuiltin()) { + builtinFields.put(field.getType(), field); + } else { + customFields.put(field.getCode(), field); + } + } + } + + private void removeFieldCache(List codes) { + for (String code : codes) { + customFields.remove(code); + } + } + + /* deploy */ + public App deploy() { + client.app().deployApp(appId); + waitDeploy(); + return this; + } + + public void waitDeploy() { + for (int i = 0; i < MAX_DEPLOY_WAIT_SEC; i++) { + DeployStatus status = client.app().getDeployStatus(appId); + if (status == DeployStatus.SUCCESS) { + return; + } else if (status != DeployStatus.PROCESSING) { + String msg = String.format("deploy failed or canceled. app:%d, status:%s", appId, status); + throw new RuntimeException(msg); + } + + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + } + + /* general settings */ + public long getAppRevision(boolean preview) { + return getAppSettings(preview).getRevision(); + } + + public AppSettings getAppSettings(boolean preview) { + if (preview) { + GetAppSettingsPreviewResponseBody resp = client.app().getAppSettingsPreview(appId); + return new AppSettings( + resp.getName(), + resp.getDescription(), + resp.getIcon(), + resp.getTheme(), + resp.getRevision()); + } else { + GetAppSettingsResponseBody resp = client.app().getAppSettings(appId); + return new AppSettings( + resp.getName(), + resp.getDescription(), + resp.getIcon(), + resp.getTheme(), + resp.getRevision()); + } + } + + public App updateAppSettings(AppSettingsBuilder builder) { + client.app().updateAppSettings(builder.build(appId)); + return this; + } + + @Value + public static class AppSettings { + private final String name; + private final String description; + private final AppIcon icon; + private final String theme; + private final long revision; + } + + public Map getActions(boolean preview) { + if (preview) { + return client.app().getAppActionsPreview(appId); + } else { + return client.app().getAppActions(appId); + } + } + + public App updateActions(AppAction... actions) { + Map actionMap = + Stream.of(actions).collect(Collectors.toMap(AppAction::getName, Function.identity())); + return updateActions(actionMap); + } + + public App updateActions(Map actions) { + client.app().updateAppActions(appId, actions); + return this; + } + + public AppCustomize getAppCustomize(boolean preview) { + if (preview) { + GetAppCustomizePreviewResponseBody resp = client.app().getAppCustomizePreview(appId); + return new AppCustomize( + resp.getScope(), resp.getDesktop(), resp.getMobile(), resp.getRevision()); + } else { + GetAppCustomizeResponseBody resp = client.app().getAppCustomize(appId); + return new AppCustomize( + resp.getScope(), resp.getDesktop(), resp.getMobile(), resp.getRevision()); + } + } + + public App updateAppCustomize(AppCustomizeBuilder builder) { + client.app().updateAppCustomize(builder.build(appId)); + return this; + } + + @Value + public static class AppCustomize { + private final CustomizeScope scope; + private final CustomizeBody desktop; + private final CustomizeBody mobile; + private final long revision; + } + + /* fields */ + public App addFields(FieldProperty... fields) { + return addFields(Arrays.asList(fields)); + } + + public App addFields(List fields) { + client.app().addFormFields(appId, fields); + updateFieldCache(fields); + return this; + } + + public App deleteFields(String... codes) { + List list = Arrays.asList(codes); + client.app().deleteFormFields(appId, list); + removeFieldCache(list); + return this; + } + + public Map getFields(boolean preview) { + if (preview) { + return client.app().getFormFieldsPreview(appId); + } else { + return client.app().getFormFields(appId); + } + } + + public List getLayout(boolean preview) { + if (preview) { + return client.app().getFormLayoutPreview(appId); + } else { + return client.app().getFormLayout(appId); + } + } + + public App updateLayout(FormLayoutBuilder builder) { + client.app().updateFormLayout(appId, builder.build()); + return this; + } + + /* acl */ + public List getAppAcl(boolean preview) { + if (preview) { + return client.app().getAppAclPreview(appId); + } else { + return client.app().getAppAcl(appId); + } + } + + public App updateAppAcl(AppAclBuilder builder) { + client.app().updateAppAcl(builder.build(appId)); + return this; + } + + public List getRecordAcl(boolean preview) { + if (preview) { + return client.app().getRecordAclPreview(appId); + } else { + return client.app().getRecordAcl(appId); + } + } + + public App updateRecordAcl(RecordAclBuilder builder) { + client.app().updateRecordAcl(builder.build(appId)); + return this; + } + + public List getFieldAcl(boolean preview) { + if (preview) { + return client.app().getFieldAclPreview(appId); + } else { + return client.app().getFieldAcl(appId); + } + } + + public App updateFieldAcl(FieldAclBuilder builder) { + client.app().updateFieldAcl(builder.build(appId)); + return this; + } + + /* notification settings */ + public GeneralNotifications getGeneralNotifications(boolean preview) { + if (preview) { + GetGeneralNotificationsPreviewResponseBody resp = + client.app().getGeneralNotificationsPreview(appId); + return new GeneralNotifications(resp.getNotifications(), resp.isNotifyToCommenter()); + } else { + GetGeneralNotificationsResponseBody resp = client.app().getGeneralNotifications(appId); + return new GeneralNotifications(resp.getNotifications(), resp.isNotifyToCommenter()); + } + } + + @Value + public static class GeneralNotifications { + private final List notifications; + private final boolean notifyToCommenter; + } + + public App updateGeneralNotifications(GeneralNotificationsBuilder builder) { + client.app().updateGeneralNotifications(builder.build(appId)); + return this; + } + + public List getRecordNotifications(boolean preview) { + if (preview) { + return client.app().getPerRecordNotificationsPreview(appId).getNotifications(); + } else { + return client.app().getPerRecordNotifications(appId).getNotifications(); + } + } + + public App updateRecordNotifications(RecordNotificationsBuilder builder) { + client.app().updatePerRecordNotifications(builder.build(appId)); + return this; + } + + public ReminderNotifications getReminderNotifications(boolean preview) { + if (preview) { + GetReminderNotificationsPreviewResponseBody resp = + client.app().getReminderNotificationsPreview(appId); + return new ReminderNotifications(resp.getNotifications(), resp.getTimezone()); + } else { + GetReminderNotificationsResponseBody resp = client.app().getReminderNotifications(appId); + return new ReminderNotifications(resp.getNotifications(), resp.getTimezone()); + } + } + + @Value + public static class ReminderNotifications { + private final List notifications; + private final String timezone; + } + + public App updateReminderNotifications(ReminderNotificationsBuilder builder) { + client.app().updateReminderNotifications(builder.build(appId)); + return this; + } + + /* process management settings */ + public ProcessManagement getProcessManagement(boolean preview) { + if (preview) { + GetProcessManagementPreviewResponseBody resp = + client.app().getProcessManagementPreview(appId); + return new ProcessManagement( + resp.isEnable(), resp.getStates(), resp.getActions(), resp.getRevision()); + } else { + GetProcessManagementResponseBody resp = client.app().getProcessManagement(appId); + return new ProcessManagement( + resp.isEnable(), resp.getStates(), resp.getActions(), resp.getRevision()); + } + } + + @Value + public static class ProcessManagement { + private final boolean enable; + private final Map states; + private final List actions; + private final long revision; + } + + public App updateProcessManagement(ProcessManagementBuilder builder) { + client.app().updateProcessManagement(builder.build(appId)); + return this; + } + + public App applyExampleProcessManagement() { + return updateProcessManagement(ProcessManagementBuilder.example()); + } + + /* reports */ + public Map getReports(boolean preview) { + if (preview) { + return client.app().getReportsPreview(appId); + } else { + return client.app().getReports(appId); + } + } + + public App updateReports(Report... reports) { + Map reportMap = new HashMap<>(); + for (Report report : reports) { + reportMap.put(report.getName(), report); + } + return updateReports(reportMap); + } + + public App updateReports(Map reports) { + client.app().updateReports(appId, reports); + return this; + } + + /* views */ + public Map getViews(boolean preview) { + if (preview) { + return client.app().getViewsPreview(appId); + } else { + return client.app().getViews(appId); + } + } + + public App updateViews(View... views) { + Map viewMap = new HashMap<>(); + for (View view : views) { + viewMap.put(view.getName(), view); + } + return updateViews(viewMap); + } + + public App updateViews(Map views) { + client.app().updateViews(appId, views); + return this; + } + + /* records */ + public Record getRecord(long recordId) { + return client.record().getRecord(appId, recordId); + } + + public List getRecords() { + return client.record().getRecords(appId); + } + + /** + * レコードを追加する + * + * @param fieldAndValues 奇数番目はFieldProperty, 偶数番目はその値の文字列 + * @return 追加したレコード番号 + */ + public long addRecord(Object... fieldAndValues) { + if (fieldAndValues.length % 2 != 0) { + throw new RuntimeException("invalid argument length."); + } + + Record record = new Record(); + for (int i = 0; i < fieldAndValues.length / 2; i++) { + FieldProperty schema = (FieldProperty) fieldAndValues[i * 2]; + String value = (String) fieldAndValues[i * 2 + 1]; + record.putField(schema.getCode(), makeValue(schema.getType(), value)); + } + return client.record().addRecord(appId, record); + } + + private FieldValue makeValue(FieldType type, String value) { + switch (type) { + case CHECK_BOX: + if (value == null || value.isEmpty()) { + return new CheckBoxFieldValue(); + } + return new CheckBoxFieldValue(value.split(",")); + case CREATED_TIME: + return new CreatedTimeFieldValue(ZonedDateTime.parse(value)); + case CREATOR: + return new CreatorFieldValue(new User(value)); + case DATE: + return new DateFieldValue(LocalDate.parse(value)); + case DATETIME: + return new DateTimeFieldValue(ZonedDateTime.parse(value)); + case DROP_DOWN: + return new DropDownFieldValue(value); + case FILE: + if (value == null || value.isEmpty()) { + return new FileFieldValue(); + } + List files = + Arrays.stream(value.split(",")) + .map(v -> new FileBody().setFileKey(v)) + .collect(Collectors.toList()); + return new FileFieldValue(files); + case GROUP_SELECT: + if (value == null || value.isEmpty()) { + return new GroupSelectFieldValue(); + } + List groups = + Arrays.stream(value.split(",")).map(Group::new).collect(Collectors.toList()); + return new GroupSelectFieldValue(groups); + case LINK: + return new LinkFieldValue(value); + case MODIFIER: + return new ModifierFieldValue(new User(value)); + case MULTI_LINE_TEXT: + return new MultiLineTextFieldValue(value); + case MULTI_SELECT: + if (value == null || value.isEmpty()) { + return new MultiSelectFieldValue(); + } + return new MultiSelectFieldValue(value.split(",")); + case NUMBER: + return new NumberFieldValue(new BigDecimal(value)); + case ORGANIZATION_SELECT: + if (value == null || value.isEmpty()) { + return new OrganizationSelectFieldValue(); + } + List orgs = + Arrays.stream(value.split(",")).map(Organization::new).collect(Collectors.toList()); + return new OrganizationSelectFieldValue(orgs); + case RADIO_BUTTON: + return new RadioButtonFieldValue(value); + case RICH_TEXT: + return new RichTextFieldValue(value); + case SINGLE_LINE_TEXT: + return new SingleLineTextFieldValue(value); + case TIME: + return new TimeFieldValue(LocalTime.parse(value)); + case UPDATED_TIME: + return new UpdatedTimeFieldValue(ZonedDateTime.parse(value)); + case USER_SELECT: + if (value == null || value.isEmpty()) { + return new UserSelectFieldValue(); + } + List users = + Arrays.stream(value.split(",")).map(User::new).collect(Collectors.toList()); + return new UserSelectFieldValue(users); + /* 以下はサポートしない */ + case CALC: + case CATEGORY: + case GROUP: + case HR: + case LABEL: + case RECORD_NUMBER: + case REFERENCE_TABLE: + case SPACER: + case STATUS: + case STATUS_ASSIGNEE: + case SUBTABLE: + default: + throw new AssertionError("unsupported type: " + type); + } + } + + public List addRecords(List records) { + return client.record().addRecords(appId, records); + } + + public void deleteAllRecords() { + List records = getRecords(); + if (records.isEmpty()) { + return; + } + List ids = records.stream().map(Record::getId).collect(Collectors.toList()); + client.record().deleteRecords(appId, ids); + } +} diff --git a/e2e-tests/src/test/java/com/kintone/client/helper/AppAclBuilder.java b/e2e-tests/src/test/java/com/kintone/client/helper/AppAclBuilder.java new file mode 100644 index 0000000..7f8865c --- /dev/null +++ b/e2e-tests/src/test/java/com/kintone/client/helper/AppAclBuilder.java @@ -0,0 +1,114 @@ +package com.kintone.client.helper; + +import com.kintone.client.api.app.UpdateAppAclRequest; +import com.kintone.client.model.Entity; +import com.kintone.client.model.EntityType; +import com.kintone.client.model.app.AppRightEntity; +import java.util.ArrayList; +import java.util.List; + +public class AppAclBuilder { + private final List rights = new ArrayList<>(); + + private AppRightEntity getCurrent() { + return rights.get(rights.size() - 1); + } + + public AppAclBuilder user(String code) { + AppRightEntity r = new AppRightEntity().setEntity(new Entity(EntityType.USER, code)); + r.setIncludeSubs(false); + rights.add(r); + return applyDefault(); + } + + public AppAclBuilder group(String code) { + AppRightEntity r = new AppRightEntity().setEntity(new Entity(EntityType.GROUP, code)); + r.setIncludeSubs(false); + rights.add(r); + return applyDefault(); + } + + public AppAclBuilder org(String code, boolean incluseSubs) { + AppRightEntity r = new AppRightEntity().setEntity(new Entity(EntityType.GROUP, code)); + r.setIncludeSubs(incluseSubs); + rights.add(r); + return applyDefault(); + } + + public AppAclBuilder creator() { + AppRightEntity r = new AppRightEntity().setEntity(new Entity(EntityType.CREATOR, null)); + r.setIncludeSubs(false); + rights.add(r); + return all(true); // 作成者はデフォルトで全権限有り + } + + public AppAclBuilder everyone() { + return group("everyone"); + } + + public AppAclBuilder recordViewable(boolean viewable) { + getCurrent().setRecordViewable(viewable); + return this; + } + + public AppAclBuilder recordAddable(boolean addable) { + getCurrent().setRecordAddable(addable); + return this; + } + + public AppAclBuilder recordEditable(boolean editable) { + getCurrent().setRecordEditable(editable); + return this; + } + + public AppAclBuilder recordDeletable(boolean deletable) { + getCurrent().setRecordDeletable(deletable); + return this; + } + + public AppAclBuilder appEditable(boolean editable) { + getCurrent().setAppEditable(editable); + return this; + } + + public AppAclBuilder recordImportable(boolean importable) { + getCurrent().setRecordImportable(importable); + return this; + } + + public AppAclBuilder recordExportable(boolean exportable) { + getCurrent().setRecordExportable(exportable); + return this; + } + + public AppAclBuilder all(boolean enable) { + AppRightEntity current = getCurrent(); + current.setRecordViewable(enable); + current.setRecordAddable(enable); + current.setRecordEditable(enable); + current.setRecordDeletable(enable); + current.setAppEditable(enable); + current.setRecordImportable(enable); + current.setRecordExportable(enable); + return this; + } + + public AppAclBuilder applyDefault() { + AppRightEntity current = getCurrent(); + current.setRecordViewable(true); + current.setRecordAddable(true); + current.setRecordEditable(true); + current.setRecordDeletable(true); + current.setAppEditable(true); + current.setRecordImportable(false); + current.setRecordExportable(false); + return this; + } + + UpdateAppAclRequest build(long appId) { + UpdateAppAclRequest req = new UpdateAppAclRequest(); + req.setApp(appId); + req.setRights(rights); + return req; + } +} diff --git a/e2e-tests/src/test/java/com/kintone/client/helper/AppCustomizeBuilder.java b/e2e-tests/src/test/java/com/kintone/client/helper/AppCustomizeBuilder.java new file mode 100644 index 0000000..39b8bc8 --- /dev/null +++ b/e2e-tests/src/test/java/com/kintone/client/helper/AppCustomizeBuilder.java @@ -0,0 +1,94 @@ +package com.kintone.client.helper; + +import com.kintone.client.api.app.UpdateAppCustomizeRequest; +import com.kintone.client.model.FileBody; +import com.kintone.client.model.app.*; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class AppCustomizeBuilder { + + private enum ResourceType { + DESKTOP_JS, + DESKTOP_CSS, + MOBILE_JS, + MOBILE_CSS + } + + private final Map> resources = new HashMap<>(); + private CustomizeScope scope; + private Long revision; + + private AppCustomizeBuilder addResource(ResourceType type, CustomizeResource resource) { + resources.putIfAbsent(type, new ArrayList<>()); + resources.get(type).add(resource); + return this; + } + + public AppCustomizeBuilder scope(CustomizeScope scope) { + this.scope = scope; + return this; + } + + public AppCustomizeBuilder desktopJsFile(String fileKey) { + FileBody file = new FileBody().setFileKey(fileKey); + return addResource(ResourceType.DESKTOP_JS, new CustomizeFileResource().setFile(file)); + } + + public AppCustomizeBuilder desktopJsUrl(String url) { + return addResource(ResourceType.DESKTOP_JS, new CustomizeUrlResource().setUrl(url)); + } + + public AppCustomizeBuilder desktopCssFile(String fileKey) { + FileBody file = new FileBody().setFileKey(fileKey); + return addResource(ResourceType.DESKTOP_CSS, new CustomizeFileResource().setFile(file)); + } + + public AppCustomizeBuilder desktopCssUrl(String url) { + return addResource(ResourceType.DESKTOP_CSS, new CustomizeUrlResource().setUrl(url)); + } + + public AppCustomizeBuilder mobileJsFile(String fileKey) { + FileBody file = new FileBody().setFileKey(fileKey); + return addResource(ResourceType.MOBILE_JS, new CustomizeFileResource().setFile(file)); + } + + public AppCustomizeBuilder mobileJsUrl(String url) { + return addResource(ResourceType.MOBILE_JS, new CustomizeUrlResource().setUrl(url)); + } + + public AppCustomizeBuilder mobileCssFile(String fileKey) { + FileBody file = new FileBody().setFileKey(fileKey); + return addResource(ResourceType.MOBILE_CSS, new CustomizeFileResource().setFile(file)); + } + + public AppCustomizeBuilder mobileCssUrl(String url) { + return addResource(ResourceType.MOBILE_CSS, new CustomizeUrlResource().setUrl(url)); + } + + public AppCustomizeBuilder revision(Long revision) { + this.revision = revision; + return this; + } + + UpdateAppCustomizeRequest build(long appId) { + List desktopJs = resources.get(ResourceType.DESKTOP_JS); + List desktopCss = resources.get(ResourceType.DESKTOP_CSS); + List mobileJs = resources.get(ResourceType.MOBILE_JS); + List mobileCss = resources.get(ResourceType.MOBILE_CSS); + + UpdateAppCustomizeRequest req = new UpdateAppCustomizeRequest(); + req.setApp(appId); + req.setScope(scope); + if (desktopJs != null || desktopCss != null) { + req.setDesktop(new CustomizeBody().setJs(desktopJs).setCss(desktopCss)); + } + if (mobileJs != null || mobileCss != null) { + req.setMobile(new CustomizeBody().setJs(mobileJs).setCss(mobileCss)); + } + req.setRevision(revision); + return req; + } +} diff --git a/e2e-tests/src/test/java/com/kintone/client/helper/AppSettingsBuilder.java b/e2e-tests/src/test/java/com/kintone/client/helper/AppSettingsBuilder.java new file mode 100644 index 0000000..5d21fe4 --- /dev/null +++ b/e2e-tests/src/test/java/com/kintone/client/helper/AppSettingsBuilder.java @@ -0,0 +1,56 @@ +package com.kintone.client.helper; + +import com.kintone.client.api.app.UpdateAppSettingsRequest; +import com.kintone.client.model.FileBody; +import com.kintone.client.model.app.AppFileIcon; +import com.kintone.client.model.app.AppIcon; +import com.kintone.client.model.app.AppPresetIcon; + +public class AppSettingsBuilder { + private String name; + private String description; + private AppIcon icon; + private String theme; + private Long revision; + + public AppSettingsBuilder name(String name) { + this.name = name; + return this; + } + + public AppSettingsBuilder description(String description) { + this.description = description; + return this; + } + + public AppSettingsBuilder presetIcon(String icon) { + this.icon = new AppPresetIcon().setKey(icon); + return this; + } + + public AppSettingsBuilder fileIcon(String fileKey) { + this.icon = new AppFileIcon().setFile(new FileBody().setFileKey(fileKey)); + return this; + } + + public AppSettingsBuilder theme(String theme) { + this.theme = theme; + return this; + } + + public AppSettingsBuilder revision(Long revision) { + this.revision = revision; + return this; + } + + UpdateAppSettingsRequest build(long appId) { + UpdateAppSettingsRequest req = new UpdateAppSettingsRequest(); + req.setApp(appId); + req.setName(name); + req.setDescription(description); + req.setTheme(theme); + req.setIcon(icon); + req.setRevision(revision); + return req; + } +} diff --git a/e2e-tests/src/test/java/com/kintone/client/helper/FieldAclBuilder.java b/e2e-tests/src/test/java/com/kintone/client/helper/FieldAclBuilder.java new file mode 100644 index 0000000..ab19e34 --- /dev/null +++ b/e2e-tests/src/test/java/com/kintone/client/helper/FieldAclBuilder.java @@ -0,0 +1,92 @@ +package com.kintone.client.helper; + +import com.kintone.client.api.app.UpdateFieldAclRequest; +import com.kintone.client.model.Entity; +import com.kintone.client.model.EntityType; +import com.kintone.client.model.app.FieldAccessibility; +import com.kintone.client.model.app.FieldRight; +import com.kintone.client.model.app.FieldRightEntity; +import com.kintone.client.model.app.field.FieldProperty; +import java.util.ArrayList; +import java.util.List; + +public class FieldAclBuilder { + private final List rights = new ArrayList<>(); + + private FieldAccessibility toAcl(boolean read, boolean write) { + if (read && write) { + return FieldAccessibility.WRITE; + } + if (read && !write) { + return FieldAccessibility.READ; + } + if (!read && !write) { + return FieldAccessibility.NONE; + } + throw new AssertionError("invalid field right."); + } + + private FieldAclBuilder addFieldRightEntity(FieldRightEntity r) { + rights.get(rights.size() - 1).getEntities().add(r); + return this; + } + + public FieldAclBuilder target(String field) { + FieldRight right = new FieldRight(); + right.setCode(field); + right.setEntities(new ArrayList<>()); + rights.add(right); + return this; + } + + public FieldAclBuilder target(FieldProperty field) { + return target(field.getCode()); + } + + public FieldAclBuilder user(String code, boolean read, boolean write) { + FieldRightEntity r = new FieldRightEntity(); + r.setEntity(new Entity(EntityType.USER, code)); + r.setIncludeSubs(false); + r.setAccessibility(toAcl(read, write)); + return addFieldRightEntity(r); + } + + public FieldAclBuilder group(String code, boolean read, boolean write) { + FieldRightEntity r = new FieldRightEntity(); + r.setEntity(new Entity(EntityType.GROUP, code)); + r.setIncludeSubs(false); + r.setAccessibility(toAcl(read, write)); + return addFieldRightEntity(r); + } + + public FieldAclBuilder everyone(boolean read, boolean write) { + return group("everyone", read, write); + } + + public FieldAclBuilder org(String code, boolean includeSubs, boolean read, boolean write) { + FieldRightEntity r = new FieldRightEntity(); + r.setEntity(new Entity(EntityType.ORGANIZATION, code)); + r.setIncludeSubs(includeSubs); + r.setAccessibility(toAcl(read, write)); + return addFieldRightEntity(r); + } + + public FieldAclBuilder field(String code, boolean read, boolean write) { + FieldRightEntity r = new FieldRightEntity(); + r.setEntity(new Entity(EntityType.FIELD_ENTITY, code)); + r.setIncludeSubs(false); + r.setAccessibility(toAcl(read, write)); + return addFieldRightEntity(r); + } + + public FieldAclBuilder field(FieldProperty field, boolean read, boolean write) { + return field(field.getCode(), read, write); + } + + UpdateFieldAclRequest build(long appId) { + UpdateFieldAclRequest req = new UpdateFieldAclRequest(); + req.setApp(appId); + req.setRights(rights); + return req; + } +} diff --git a/e2e-tests/src/test/java/com/kintone/client/helper/Fields.java b/e2e-tests/src/test/java/com/kintone/client/helper/Fields.java new file mode 100644 index 0000000..159c024 --- /dev/null +++ b/e2e-tests/src/test/java/com/kintone/client/helper/Fields.java @@ -0,0 +1,93 @@ +package com.kintone.client.helper; + +import com.kintone.client.model.app.field.*; + +public class Fields { + public static SingleLineTextFieldProperty text() { + return text("文字列__1行_", "文字列 (1行)"); + } + + public static SingleLineTextFieldProperty text(String code) { + return text(code, code); + } + + public static SingleLineTextFieldProperty text(String code, String label) { + return new SingleLineTextFieldProperty().setCode(code).setLabel(label); + } + + public static LinkFieldProperty link() { + return link("リンク__URL_", "リンク (URL)"); + } + + public static LinkFieldProperty link(String code) { + return link(code, code); + } + + public static LinkFieldProperty link(String code, String label) { + return link(code, label, LinkProtocol.WEB); + } + + public static LinkFieldProperty link(String code, String label, LinkProtocol protocol) { + return new LinkFieldProperty().setCode(code).setLabel(label).setProtocol(protocol); + } + + public static FileFieldProperty file() { + return file("添付ファイル"); + } + + public static FileFieldProperty file(String code) { + return file(code, code); + } + + public static FileFieldProperty file(String code, String label) { + return new FileFieldProperty().setCode(code).setLabel(label); + } + + public static NumberFieldProperty number() { + return number("数値", "数値"); + } + + public static NumberFieldProperty number(String code) { + return number(code, code); + } + + public static NumberFieldProperty number(String code, String label) { + return new NumberFieldProperty().setCode(code).setLabel(label); + } + + public static UserSelectFieldProperty userSelect() { + return userSelect("ユーザー選択", "ユーザー選択"); + } + + public static UserSelectFieldProperty userSelect(String code) { + return userSelect(code, code); + } + + public static UserSelectFieldProperty userSelect(String code, String label) { + return new UserSelectFieldProperty().setCode(code).setLabel(label); + } + + public static DateFieldProperty date() { + return date("日付", "日付"); + } + + public static DateFieldProperty date(String code) { + return date(code, code); + } + + public static DateFieldProperty date(String code, String label) { + return new DateFieldProperty().setCode(code).setLabel(label); + } + + public static DateTimeFieldProperty datetime() { + return datetime("日時", "日時"); + } + + public static DateTimeFieldProperty datetime(String code) { + return datetime(code, code); + } + + public static DateTimeFieldProperty datetime(String code, String label) { + return new DateTimeFieldProperty().setCode(code).setLabel(label); + } +} diff --git a/e2e-tests/src/test/java/com/kintone/client/helper/FormLayoutBuilder.java b/e2e-tests/src/test/java/com/kintone/client/helper/FormLayoutBuilder.java new file mode 100644 index 0000000..0ccc7d2 --- /dev/null +++ b/e2e-tests/src/test/java/com/kintone/client/helper/FormLayoutBuilder.java @@ -0,0 +1,85 @@ +package com.kintone.client.helper; + +import com.kintone.client.model.app.field.FieldProperty; +import com.kintone.client.model.app.layout.*; +import com.kintone.client.model.record.FieldType; +import java.util.ArrayList; +import java.util.List; + +public class FormLayoutBuilder { + + private final List layout = new ArrayList<>(); + private List fields; + + public FormLayoutBuilder row() { + if (fields != null) { + layout.add(new RowLayout().setFields(fields)); + fields = null; + } + fields = new ArrayList<>(); + return this; + } + + public List build() { + if (fields != null) { + layout.add(new RowLayout().setFields(fields)); + fields = null; + } + return layout; + } + + private FormLayoutBuilder addFieldLayout(FieldLayout field) { + if (fields == null) { + row(); + } + fields.add(field); + return this; + } + + private FormLayoutBuilder addField(FieldProperty field, Integer w, Integer h) { + FieldLayout settings = new FieldLayout(); + settings.setType(field.getType()); + settings.setCode(field.getCode()); + if (w != null || h != null) { + settings.setSize(new FieldSize().setWidth(w).setInnerHeight(h)); + } + return addFieldLayout(settings); + } + + public FormLayoutBuilder field(FieldProperty field) { + return addField(field, null, null); + } + + public FormLayoutBuilder field(FieldProperty field, int width) { + return addField(field, width, null); + } + + public FormLayoutBuilder field(FieldProperty field, int width, int innerHeight) { + return addField(field, width, innerHeight); + } + + public FormLayoutBuilder spacer(String elementId, int width, int height) { + FieldLayout settings = new FieldLayout(); + settings.setType(FieldType.SPACER); + settings.setElementId(elementId); + settings.setSize(new FieldSize().setWidth(width).setHeight(height)); + return addFieldLayout(settings); + } + + public FormLayoutBuilder hr(int width) { + FieldLayout settings = new FieldLayout(); + settings.setType(FieldType.HR); + settings.setElementId(""); + settings.setSize(new FieldSize().setWidth(width)); + return addFieldLayout(settings); + } + + public FormLayoutBuilder label(String label, int width) { + FieldLayout settings = new FieldLayout(); + settings.setType(FieldType.LABEL); + settings.setLabel(label); + settings.setElementId(""); + settings.setSize(new FieldSize().setWidth(width)); + return addFieldLayout(settings); + } +} diff --git a/e2e-tests/src/test/java/com/kintone/client/helper/GeneralNotificationsBuilder.java b/e2e-tests/src/test/java/com/kintone/client/helper/GeneralNotificationsBuilder.java new file mode 100644 index 0000000..7311a78 --- /dev/null +++ b/e2e-tests/src/test/java/com/kintone/client/helper/GeneralNotificationsBuilder.java @@ -0,0 +1,107 @@ +package com.kintone.client.helper; + +import com.kintone.client.api.app.UpdateGeneralNotificationsRequest; +import com.kintone.client.model.Entity; +import com.kintone.client.model.EntityType; +import com.kintone.client.model.app.GeneralNotification; +import java.util.ArrayList; +import java.util.List; + +public class GeneralNotificationsBuilder { + private List notifications; + private Boolean notifyToCommenter; + + private GeneralNotification getCurrent() { + return notifications.get(notifications.size() - 1); + } + + private void addNotification(GeneralNotification setting) { + if (notifications == null) { + notifications = new ArrayList<>(); + } + notifications.add(setting); + } + + public GeneralNotificationsBuilder user(String code) { + GeneralNotification n = new GeneralNotification(); + n.setEntity(new Entity(EntityType.USER, code)); + n.setIncludeSubs(false); + addNotification(n); + return this; + } + + public GeneralNotificationsBuilder group(String code) { + GeneralNotification n = new GeneralNotification(); + n.setEntity(new Entity(EntityType.GROUP, code)); + n.setIncludeSubs(false); + addNotification(n); + return this; + } + + public GeneralNotificationsBuilder org(String code, boolean includeSubs) { + GeneralNotification n = new GeneralNotification(); + n.setEntity(new Entity(EntityType.ORGANIZATION, code)); + n.setIncludeSubs(includeSubs); + addNotification(n); + return this; + } + + public GeneralNotificationsBuilder field(String code) { + GeneralNotification n = new GeneralNotification(); + n.setEntity(new Entity(EntityType.FIELD_ENTITY, code)); + n.setIncludeSubs(false); + addNotification(n); + return this; + } + + public GeneralNotificationsBuilder everyone() { + return group("everyone"); + } + + public GeneralNotificationsBuilder recordAdded(boolean flag) { + getCurrent().setRecordAdded(flag); + return this; + } + + public GeneralNotificationsBuilder recordEdited(boolean flag) { + getCurrent().setRecordEdited(flag); + return this; + } + + public GeneralNotificationsBuilder commentAdded(boolean flag) { + getCurrent().setCommentAdded(flag); + return this; + } + + public GeneralNotificationsBuilder statusChanged(boolean flag) { + getCurrent().setStatusChanged(flag); + return this; + } + + public GeneralNotificationsBuilder fileImported(boolean flag) { + getCurrent().setFileImported(flag); + return this; + } + + public GeneralNotificationsBuilder all(boolean flag) { + getCurrent().setRecordAdded(flag); + getCurrent().setRecordEdited(flag); + getCurrent().setCommentAdded(flag); + getCurrent().setStatusChanged(flag); + getCurrent().setFileImported(flag); + return this; + } + + public GeneralNotificationsBuilder notifyToCommenter(boolean flag) { + notifyToCommenter = flag; + return this; + } + + UpdateGeneralNotificationsRequest build(long appId) { + UpdateGeneralNotificationsRequest req = new UpdateGeneralNotificationsRequest(); + req.setApp(appId); + req.setNotifications(notifications); + req.setNotifyToCommenter(notifyToCommenter); + return req; + } +} diff --git a/e2e-tests/src/test/java/com/kintone/client/helper/ProcessManagementBuilder.java b/e2e-tests/src/test/java/com/kintone/client/helper/ProcessManagementBuilder.java new file mode 100644 index 0000000..53f55a9 --- /dev/null +++ b/e2e-tests/src/test/java/com/kintone/client/helper/ProcessManagementBuilder.java @@ -0,0 +1,172 @@ +package com.kintone.client.helper; + +import com.kintone.client.api.app.UpdateProcessManagementRequest; +import com.kintone.client.model.Entity; +import com.kintone.client.model.EntityType; +import com.kintone.client.model.app.*; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class ProcessManagementBuilder { + private Boolean enabled; + private final List states = new ArrayList<>(); + private final List actions = new ArrayList<>(); + + public ProcessManagementBuilder enable(boolean enabled) { + this.enabled = enabled; + return this; + } + + public ProcessManagementBuilder state(String name) { + ProcessState state = new ProcessState(); + state.setName(name); + states.add(state); + return this; + } + + public StateBuilder state(String name, ProcessAssigneeType type) { + return new StateBuilder(this, name, type); + } + + public ProcessManagementBuilder action(String name, String from, String to) { + ProcessAction action = new ProcessAction(); + action.setName(name); + action.setFrom(from); + action.setTo(to); + actions.add(action); + return this; + } + + public ProcessManagementBuilder action(String name, String from, String to, String cond) { + ProcessAction action = new ProcessAction(); + action.setName(name); + action.setFrom(from); + action.setTo(to); + action.setFilterCond(cond); + actions.add(action); + return this; + } + + UpdateProcessManagementRequest build(long appId) { + Map stateMap = new HashMap<>(); + for (int i = 0; i < states.size(); i++) { + ProcessState state = states.get(i); + state.setIndex(Integer.toString(i)); + stateMap.put(state.getName(), state); + } + + UpdateProcessManagementRequest req = new UpdateProcessManagementRequest(); + req.setApp(appId); + req.setEnable(enabled); + if (!stateMap.isEmpty()) { + req.setStates(stateMap); + } + if (!actions.isEmpty()) { + req.setActions(actions); + } + return req; + } + + public static class StateBuilder { + private final ProcessManagementBuilder parent; + private final String name; + private final ProcessAssigneeType type; + private final List entities = new ArrayList<>(); + + private StateBuilder(ProcessManagementBuilder parent, String name, ProcessAssigneeType type) { + this.parent = parent; + this.name = name; + this.type = type; + } + + public StateBuilder user(String code) { + ProcessEntity entity = new ProcessEntity(); + entity.setEntity(new Entity(EntityType.USER, code)); + entities.add(entity); + return this; + } + + public StateBuilder group(String code) { + ProcessEntity entity = new ProcessEntity(); + entity.setEntity(new Entity(EntityType.GROUP, code)); + entities.add(entity); + return this; + } + + public StateBuilder org(String code) { + ProcessEntity entity = new ProcessEntity(); + entity.setEntity(new Entity(EntityType.ORGANIZATION, code)); + entities.add(entity); + return this; + } + + public StateBuilder org(String code, boolean includeSubs) { + ProcessEntity entity = new ProcessEntity(); + entity.setEntity(new Entity(EntityType.ORGANIZATION, code)); + entity.setIncludeSubs(includeSubs); + entities.add(entity); + return this; + } + + public StateBuilder field(String code) { + ProcessEntity entity = new ProcessEntity(); + entity.setEntity(new Entity(EntityType.FIELD_ENTITY, code)); + entities.add(entity); + return this; + } + + public StateBuilder creator() { + ProcessEntity entity = new ProcessEntity(); + entity.setEntity(new Entity(EntityType.CREATOR, null)); + entities.add(entity); + return this; + } + + public StateBuilder customField(String code) { + ProcessEntity entity = new ProcessEntity(); + entity.setEntity(new Entity(EntityType.CUSTOM_FIELD, code)); + entities.add(entity); + return this; + } + + ProcessManagementBuilder build() { + ProcessAssignee assignee = new ProcessAssignee(); + assignee.setType(type); + assignee.setEntities(entities); + + ProcessState state = new ProcessState(); + state.setName(name); + state.setAssignee(assignee); + parent.states.add(state); + return parent; + } + + public ProcessManagementBuilder state(String name) { + return build().state(name); + } + + public StateBuilder state(String name, ProcessAssigneeType type) { + return build().state(name, type); + } + + public ProcessManagementBuilder action(String name, String from, String to) { + return build().action(name, from, to); + } + + public ProcessManagementBuilder action(String name, String from, String to, String cond) { + return build().action(name, from, to, cond); + } + } + + public static ProcessManagementBuilder example() { + return new ProcessManagementBuilder() + .enable(true) + .state("state A") + .state("state B") + .state("state C") + .action("action 1", "state A", "state B") + .action("action 2", "state B", "state C"); + } +} diff --git a/e2e-tests/src/test/java/com/kintone/client/helper/RecordAclBuilder.java b/e2e-tests/src/test/java/com/kintone/client/helper/RecordAclBuilder.java new file mode 100644 index 0000000..8cd708c --- /dev/null +++ b/e2e-tests/src/test/java/com/kintone/client/helper/RecordAclBuilder.java @@ -0,0 +1,93 @@ +package com.kintone.client.helper; + +import com.kintone.client.api.app.UpdateRecordAclRequest; +import com.kintone.client.model.Entity; +import com.kintone.client.model.EntityType; +import com.kintone.client.model.app.*; +import com.kintone.client.model.app.field.FieldProperty; +import java.util.ArrayList; +import java.util.List; + +public class RecordAclBuilder { + private final List rights = new ArrayList<>(); + + private RecordAclBuilder addRecordRightEntity(RecordRightEntity r) { + rights.get(rights.size() - 1).getEntities().add(r); + return this; + } + + public RecordAclBuilder target(String query) { + RecordRight right = new RecordRight(); + right.setFilterCond(query); + right.setEntities(new ArrayList<>()); + rights.add(right); + return this; + } + + public RecordAclBuilder any() { + return target(""); + } + + public RecordAclBuilder user(String code, boolean viewable, boolean editable, boolean deletable) { + RecordRightEntity r = + new RecordRightEntity() + .setEntity(new Entity(EntityType.USER, code)) + .setViewable(viewable) + .setEditable(editable) + .setDeletable(deletable) + .setIncludeSubs(false); + return addRecordRightEntity(r); + } + + public RecordAclBuilder group( + String code, boolean viewable, boolean editable, boolean deletable) { + RecordRightEntity r = + new RecordRightEntity() + .setEntity(new Entity(EntityType.GROUP, code)) + .setViewable(viewable) + .setEditable(editable) + .setDeletable(deletable) + .setIncludeSubs(false); + return addRecordRightEntity(r); + } + + public RecordAclBuilder everyone(boolean viewable, boolean editable, boolean deletable) { + return group("everyone", viewable, editable, deletable); + } + + public RecordAclBuilder org( + String code, boolean includeSubs, boolean viewable, boolean editable, boolean deletable) { + RecordRightEntity r = + new RecordRightEntity() + .setEntity(new Entity(EntityType.ORGANIZATION, code)) + .setViewable(viewable) + .setEditable(editable) + .setDeletable(deletable) + .setIncludeSubs(includeSubs); + return addRecordRightEntity(r); + } + + public RecordAclBuilder field( + String code, boolean viewable, boolean editable, boolean deletable) { + RecordRightEntity r = + new RecordRightEntity() + .setEntity(new Entity(EntityType.FIELD_ENTITY, code)) + .setViewable(viewable) + .setEditable(editable) + .setDeletable(deletable) + .setIncludeSubs(false); + return addRecordRightEntity(r); + } + + public RecordAclBuilder field( + FieldProperty field, boolean viewable, boolean editable, boolean deletable) { + return field(field.getCode(), viewable, editable, deletable); + } + + UpdateRecordAclRequest build(long appId) { + UpdateRecordAclRequest req = new UpdateRecordAclRequest(); + req.setApp(appId); + req.setRights(rights); + return req; + } +} diff --git a/e2e-tests/src/test/java/com/kintone/client/helper/RecordNotificationsBuilder.java b/e2e-tests/src/test/java/com/kintone/client/helper/RecordNotificationsBuilder.java new file mode 100644 index 0000000..89fd907 --- /dev/null +++ b/e2e-tests/src/test/java/com/kintone/client/helper/RecordNotificationsBuilder.java @@ -0,0 +1,72 @@ +package com.kintone.client.helper; + +import com.kintone.client.api.app.UpdatePerRecordNotificationsRequest; +import com.kintone.client.model.Entity; +import com.kintone.client.model.EntityType; +import com.kintone.client.model.app.NotificationTarget; +import com.kintone.client.model.app.PerRecordNotification; +import java.util.ArrayList; +import java.util.List; + +public class RecordNotificationsBuilder { + private List notifications; + + private PerRecordNotification getCurrent() { + return notifications.get(notifications.size() - 1); + } + + public RecordNotificationsBuilder query(String query) { + if (notifications == null) { + notifications = new ArrayList<>(); + } + PerRecordNotification n = new PerRecordNotification(); + n.setFilterCond(query); + n.setTargets(new ArrayList<>()); + notifications.add(n); + return this; + } + + public RecordNotificationsBuilder title(String title) { + getCurrent().setTitle(title); + return this; + } + + public RecordNotificationsBuilder user(String code) { + Entity e = new Entity(EntityType.USER, code); + NotificationTarget t = new NotificationTarget().setEntity(e).setIncludeSubs(false); + getCurrent().getTargets().add(t); + return this; + } + + public RecordNotificationsBuilder group(String code) { + Entity e = new Entity(EntityType.GROUP, code); + NotificationTarget t = new NotificationTarget().setEntity(e).setIncludeSubs(false); + getCurrent().getTargets().add(t); + return this; + } + + public RecordNotificationsBuilder org(String code, boolean includeSubs) { + Entity e = new Entity(EntityType.ORGANIZATION, code); + NotificationTarget t = new NotificationTarget().setEntity(e).setIncludeSubs(includeSubs); + getCurrent().getTargets().add(t); + return this; + } + + public RecordNotificationsBuilder field(String code) { + Entity e = new Entity(EntityType.FIELD_ENTITY, code); + NotificationTarget t = new NotificationTarget().setEntity(e).setIncludeSubs(false); + getCurrent().getTargets().add(t); + return this; + } + + public RecordNotificationsBuilder everyone() { + return group("everyone"); + } + + UpdatePerRecordNotificationsRequest build(long appId) { + UpdatePerRecordNotificationsRequest req = new UpdatePerRecordNotificationsRequest(); + req.setApp(appId); + req.setNotifications(notifications); + return req; + } +} diff --git a/e2e-tests/src/test/java/com/kintone/client/helper/ReminderNotificationsBuilder.java b/e2e-tests/src/test/java/com/kintone/client/helper/ReminderNotificationsBuilder.java new file mode 100644 index 0000000..9d0664c --- /dev/null +++ b/e2e-tests/src/test/java/com/kintone/client/helper/ReminderNotificationsBuilder.java @@ -0,0 +1,136 @@ +package com.kintone.client.helper; + +import com.kintone.client.api.app.UpdateReminderNotificationsRequest; +import com.kintone.client.model.Entity; +import com.kintone.client.model.EntityType; +import com.kintone.client.model.app.NotificationTarget; +import com.kintone.client.model.app.ReminderNotification; +import com.kintone.client.model.app.ReminderTiming; +import com.kintone.client.model.app.field.FieldProperty; +import com.kintone.client.model.record.FieldType; +import java.util.ArrayList; +import java.util.List; + +public class ReminderNotificationsBuilder { + private List notifications; + private String timezone; + + private ReminderNotification getCurrent() { + return notifications.get(notifications.size() - 1); + } + + public ReminderNotificationsBuilder datetime(String code, int daysLater, String time) { + if (notifications == null) { + notifications = new ArrayList<>(); + } + ReminderNotification n = new ReminderNotification(); + ReminderTiming timing = + new ReminderTiming().setCode(code).setDaysLater(daysLater).setTime(time); + n.setTiming(timing); + n.setTargets(new ArrayList<>()); + notifications.add(n); + return this; + } + + public ReminderNotificationsBuilder datetime(String code, int daysLater, int hoursLater) { + if (notifications == null) { + notifications = new ArrayList<>(); + } + ReminderNotification n = new ReminderNotification(); + ReminderTiming timing = + new ReminderTiming().setCode(code).setDaysLater(daysLater).setHoursLater(hoursLater); + n.setTiming(timing); + n.setTargets(new ArrayList<>()); + notifications.add(n); + return this; + } + + public ReminderNotificationsBuilder date(String code, int daysLater, String time) { + if (notifications == null) { + notifications = new ArrayList<>(); + } + ReminderNotification n = new ReminderNotification(); + ReminderTiming timing = + new ReminderTiming().setCode(code).setDaysLater(daysLater).setTime(time); + n.setTiming(timing); + n.setTargets(new ArrayList<>()); + notifications.add(n); + return this; + } + + public ReminderNotificationsBuilder field(FieldProperty field, int daysLater, String time) { + if (field.getType() == FieldType.DATETIME + || field.getType() == FieldType.CREATED_TIME + || field.getType() == FieldType.UPDATED_TIME) { + return datetime(field.getCode(), daysLater, time); + } + if (field.getType() == FieldType.DATE) { + return date(field.getCode(), daysLater, time); + } + throw new AssertionError("invalid field type: " + field.getType()); + } + + public ReminderNotificationsBuilder field(FieldProperty field, int daysLater, int hoursLater) { + if (field.getType() == FieldType.DATETIME + || field.getType() == FieldType.CREATED_TIME + || field.getType() == FieldType.UPDATED_TIME) { + return datetime(field.getCode(), daysLater, hoursLater); + } + throw new AssertionError("invalid field type: " + field.getType()); + } + + public ReminderNotificationsBuilder title(String title) { + getCurrent().setTitle(title); + return this; + } + + public ReminderNotificationsBuilder query(String query) { + getCurrent().setFilterCond(query); + return this; + } + + public ReminderNotificationsBuilder user(String code) { + Entity e = new Entity(EntityType.USER, code); + NotificationTarget t = new NotificationTarget().setEntity(e).setIncludeSubs(false); + getCurrent().getTargets().add(t); + return this; + } + + public ReminderNotificationsBuilder group(String code) { + Entity e = new Entity(EntityType.GROUP, code); + NotificationTarget t = new NotificationTarget().setEntity(e).setIncludeSubs(false); + getCurrent().getTargets().add(t); + return this; + } + + public ReminderNotificationsBuilder org(String code, boolean includeSubs) { + Entity e = new Entity(EntityType.ORGANIZATION, code); + NotificationTarget t = new NotificationTarget().setEntity(e).setIncludeSubs(includeSubs); + getCurrent().getTargets().add(t); + return this; + } + + public ReminderNotificationsBuilder field(String code) { + Entity e = new Entity(EntityType.FIELD_ENTITY, code); + NotificationTarget t = new NotificationTarget().setEntity(e).setIncludeSubs(false); + getCurrent().getTargets().add(t); + return this; + } + + public ReminderNotificationsBuilder everyone() { + return group("everyone"); + } + + public ReminderNotificationsBuilder timezone(String timezone) { + this.timezone = timezone; + return this; + } + + UpdateReminderNotificationsRequest build(long appId) { + UpdateReminderNotificationsRequest req = new UpdateReminderNotificationsRequest(); + req.setApp(appId); + req.setNotifications(notifications); + req.setTimezone(timezone); + return req; + } +} diff --git a/e2e-tests/src/test/java/com/kintone/client/helper/Space.java b/e2e-tests/src/test/java/com/kintone/client/helper/Space.java new file mode 100644 index 0000000..8d3d131 --- /dev/null +++ b/e2e-tests/src/test/java/com/kintone/client/helper/Space.java @@ -0,0 +1,79 @@ +package com.kintone.client.helper; + +import com.kintone.client.ApiTestBase; +import com.kintone.client.KintoneClient; +import com.kintone.client.TestSettings; + +public class Space { + private final KintoneClient client; + private final long spaceId; + private final boolean isGuestSpace; + + private Long defaultThreadId; + + private Space(ApiTestBase base, long spaceId, boolean isGuest) { + if (isGuest) { + client = base.setupDefaultClient(spaceId); + } else { + client = base.setupDefaultClient(); + } + this.spaceId = spaceId; + isGuestSpace = isGuest; + } + + public static Space singleThread(ApiTestBase base) { + TestSettings settings = TestSettings.get(); + Long spaceId = settings.getSingleThreadSpaceId(); + if (spaceId == null) { + throw new IllegalStateException("KINTONE_SPACE_ID is not set"); + } + return new Space(base, spaceId, false); + } + + public static Space multiThread(ApiTestBase base) { + TestSettings settings = TestSettings.get(); + Long spaceId = settings.getMultiThreadSpaceId(); + if (spaceId == null) { + throw new IllegalStateException("KINTONE_MULTI_THREAD_SPACE_ID is not set"); + } + Space space = new Space(base, spaceId, false); + space.defaultThreadId = settings.getMultiThreadDefaultThreadId(); + return space; + } + + public static Space guest(ApiTestBase base) { + TestSettings settings = TestSettings.get(); + Long spaceId = settings.getGuestSpaceId(); + if (spaceId == null) { + throw new IllegalStateException("KINTONE_GUEST_SPACE_ID is not set"); + } + return new Space(base, spaceId, true); + } + + public static Space fromExisting(ApiTestBase base, long spaceId, boolean isGuest) { + return new Space(base, spaceId, isGuest); + } + + public long id() { + return spaceId; + } + + public String getName() { + return client.space().getSpace(spaceId).getName(); + } + + public String getBody() { + return client.space().getSpace(spaceId).getBody(); + } + + public Long getDefaultThread() { + if (defaultThreadId == null) { + defaultThreadId = client.space().getSpace(spaceId).getDefaultThread(); + } + return defaultThreadId; + } + + public boolean isGuestSpace() { + return isGuestSpace; + } +} diff --git a/e2e-tests/src/test/java/com/kintone/client/plugin/PluginApiTest.java b/e2e-tests/src/test/java/com/kintone/client/plugin/PluginApiTest.java new file mode 100644 index 0000000..6311407 --- /dev/null +++ b/e2e-tests/src/test/java/com/kintone/client/plugin/PluginApiTest.java @@ -0,0 +1,217 @@ +package com.kintone.client.plugin; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.kintone.client.ApiTestBase; +import com.kintone.client.KintoneClient; +import com.kintone.client.TestSettings; +import com.kintone.client.api.plugin.GetInstalledPluginsRequest; +import com.kintone.client.api.plugin.GetInstalledPluginsResponseBody; +import com.kintone.client.api.plugin.InstallPluginRequest; +import com.kintone.client.api.plugin.InstallPluginResponseBody; +import com.kintone.client.api.plugin.UninstallPluginRequest; +import com.kintone.client.api.plugin.UninstallPluginResponseBody; +import com.kintone.client.api.plugin.UpdatePluginRequest; +import com.kintone.client.api.plugin.UpdatePluginResponseBody; +import com.kintone.client.model.app.DeployStatus; +import com.kintone.client.model.plugin.App; +import com.kintone.client.model.plugin.Plugin; +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class PluginApiTest extends ApiTestBase { + + private KintoneClient client; + + @BeforeEach + public void setup() { + super.setup(); + client = setupDefaultClient(); + uninstallTestPlugins(client); + } + + @Test + public void getInstalledPlugins() throws IOException { + installTestPlugins(client); + + GetInstalledPluginsResponseBody resp = client.plugin().getInstalledPlugins(); + assertThat(resp.getPlugins()).hasSizeGreaterThanOrEqualTo(3); + assertThat(resp.getPlugins().get(0).getName()).isEqualTo("plugin-c"); + assertThat(resp.getPlugins().get(1).getName()).isEqualTo("plugin-b"); + assertThat(resp.getPlugins().get(2).getName()).isEqualTo("plugin-a"); + + Plugin plugin = resp.getPlugins().get(0); + assertThat(plugin.getDescription()).isNotNull(); + + resp = client.plugin().getInstalledPlugins(1L, 2L); + assertThat(resp.getPlugins()).hasSize(2); + assertThat(resp.getPlugins().get(0).getName()).isEqualTo("plugin-b"); + assertThat(resp.getPlugins().get(1).getName()).isEqualTo("plugin-a"); + + uninstallTestPlugins(client); + } + + @Test + public void getInstalledPlugins_withIds() throws IOException { + installTestPlugins(client); + + GetInstalledPluginsResponseBody allPlugins = client.plugin().getInstalledPlugins(); + assertThat(allPlugins.getPlugins()).hasSizeGreaterThanOrEqualTo(3); + + String pluginId1 = allPlugins.getPlugins().get(0).getId(); + String pluginId2 = allPlugins.getPlugins().get(2).getId(); + + GetInstalledPluginsRequest req = new GetInstalledPluginsRequest(); + req.setIds(Arrays.asList(pluginId1, pluginId2)); + req.setLimit(100L); + req.setOffset(0L); + GetInstalledPluginsResponseBody resp = client.plugin().getInstalledPlugins(req); + + assertThat(resp.getPlugins()).hasSize(2); + List returnedIds = + resp.getPlugins().stream().map(Plugin::getId).collect(Collectors.toList()); + assertThat(returnedIds).containsExactlyInAnyOrder(pluginId1, pluginId2); + + assertThat(resp.getPlugins().get(0).getDescription()).isNotNull(); + + uninstallTestPlugins(client); + } + + private void uninstallTestPlugins(KintoneClient client) { + GetInstalledPluginsResponseBody resp = client.plugin().getInstalledPlugins(0L, 10L); + for (Plugin plugin : resp.getPlugins()) { + if (plugin.getName().startsWith("plugin-")) { + client.plugin().uninstallPlugin(plugin.getId()); + } + } + } + + private void installTestPlugins(KintoneClient client) throws IOException { + List plugins = Arrays.asList("plugin-a", "plugin-b", "plugin-c"); + for (String p : plugins) { + Path path = new File(PluginApiTest.class.getResource(p + ".zip").getFile()).toPath(); + String filekey = client.file().uploadFile(path, "multipart/form-data"); + try { + client.plugin().installPlugin(filekey); + Thread.sleep(1000); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + } + + @Test + public void installPlugin() throws IOException { + Path path = new File(PluginApiTest.class.getResource("plugin-a.zip").getFile()).toPath(); + String fileKey = client.file().uploadFile(path, "multipart/form-data"); + InstallPluginRequest req = new InstallPluginRequest(); + req.setFileKey(fileKey); + InstallPluginResponseBody resp = client.plugin().installPlugin(req); + + assertThat(resp.getId()).isNotNull(); + assertThat(resp.getVersion()).isNotNull(); + + client.plugin().uninstallPlugin(resp.getId()); + } + + @Test + public void updatePlugin() throws IOException { + Path path = new File(PluginApiTest.class.getResource("plugin-a.zip").getFile()).toPath(); + String fileKey1 = client.file().uploadFile(path, "multipart/form-data"); + InstallPluginRequest req1 = new InstallPluginRequest(); + req1.setFileKey(fileKey1); + InstallPluginResponseBody resp1 = client.plugin().installPlugin(req1); + + String fileKey2 = client.file().uploadFile(path, "multipart/form-data"); + UpdatePluginRequest req2 = new UpdatePluginRequest(); + req2.setId(resp1.getId()); + req2.setFileKey(fileKey2); + UpdatePluginResponseBody resp2 = client.plugin().updatePlugin(req2); + + assertThat(resp2.getId()).isEqualTo(resp1.getId()); + assertThat(resp2.getVersion()).isNotNull(); + + client.plugin().uninstallPlugin(resp2.getId()); + } + + @Test + public void uninstallPlugin() throws IOException { + Path path = new File(PluginApiTest.class.getResource("plugin-a.zip").getFile()).toPath(); + String fileKey = client.file().uploadFile(path, "multipart/form-data"); + InstallPluginRequest req = new InstallPluginRequest(); + req.setFileKey(fileKey); + InstallPluginResponseBody resp = client.plugin().installPlugin(req); + + assertThat(resp.getId()).isNotNull(); + assertThat(resp.getVersion()).isNotNull(); + + UninstallPluginRequest req2 = new UninstallPluginRequest(); + req2.setId(resp.getId()); + assertThat(client.plugin().uninstallPlugin(req2)).isEqualTo(new UninstallPluginResponseBody()); + } + + @Test + public void getApps() throws IOException { + Long testAppId = TestSettings.get().getTestAppIdForPlugin(); + + Path path = new File(PluginApiTest.class.getResource("plugin-a.zip").getFile()).toPath(); + String fileKey = client.file().uploadFile(path, "multipart/form-data"); + InstallPluginRequest req = new InstallPluginRequest(); + req.setFileKey(fileKey); + InstallPluginResponseBody resp = client.plugin().installPlugin(req); + + String pluginId = resp.getId(); + assertThat(pluginId).isNotNull(); + assertThat(resp.getVersion()).isNotNull(); + + try { + client.app().addPlugins(testAppId, Arrays.asList(pluginId)); + + List apps = client.plugin().getApps(pluginId); + assertThat(apps.stream().map(App::getId)).contains(testAppId); + } finally { + client.plugin().uninstallPlugin(pluginId); + } + } + + @Test + public void getRequiredPlugins() throws InterruptedException, IOException { + Long testAppId = TestSettings.get().getTestAppIdForPlugin(); + + Path path = new File(PluginApiTest.class.getResource("plugin-a.zip").getFile()).toPath(); + String fileKey = client.file().uploadFile(path, "multipart/form-data"); + InstallPluginResponseBody installResp = client.plugin().installPlugin(fileKey); + String pluginId = installResp.getId(); + + client.app().addPlugins(testAppId, Arrays.asList(pluginId)); + waitForDeployApp(client, testAppId); + + client.plugin().uninstallPlugin(pluginId); + + boolean hasPlugin = false; + for (Plugin plugin : client.plugin().getRequiredPlugins()) { + if (plugin.getId().equals(pluginId)) { + hasPlugin = true; + break; + } + } + assertThat(hasPlugin).isTrue(); + } + + private void waitForDeployApp(KintoneClient client, long appId) throws InterruptedException { + client.app().deployApp(appId); + while (true) { + DeployStatus status = client.app().getDeployStatus(appId); + if (status != DeployStatus.PROCESSING) { + break; + } + Thread.sleep(500); + } + } +} diff --git a/e2e-tests/src/test/java/com/kintone/client/record/RecordApiTest.java b/e2e-tests/src/test/java/com/kintone/client/record/RecordApiTest.java new file mode 100644 index 0000000..273f2eb --- /dev/null +++ b/e2e-tests/src/test/java/com/kintone/client/record/RecordApiTest.java @@ -0,0 +1,482 @@ +package com.kintone.client.record; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.kintone.client.ApiTestBase; +import com.kintone.client.KintoneClient; +import com.kintone.client.TestSettings; +import com.kintone.client.Users; +import com.kintone.client.api.record.*; +import com.kintone.client.helper.App; +import com.kintone.client.helper.ProcessManagementBuilder; +import com.kintone.client.model.Entity; +import com.kintone.client.model.EntityType; +import com.kintone.client.model.Order; +import com.kintone.client.model.User; +import com.kintone.client.model.app.field.FieldProperty; +import com.kintone.client.model.record.*; +import com.kintone.client.model.record.Record; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** RecordClientのテスト */ +public class RecordApiTest extends ApiTestBase { + + private static final String TEXT_FIELD_CODE = "文字列__1行_"; + private static final String TEXT2_FIELD_CODE = "text2"; + private static final String KEY_FIELD_CODE = "key"; + + private KintoneClient client; + private App app; + + @BeforeEach + public void setupApp() { + client = setupDefaultClient(); + Long testAppId = TestSettings.get().getTestAppId(); + if (testAppId != null) { + app = App.fromExisting(client, testAppId); + } else { + throw new IllegalStateException( + "KINTONE_TEST_APP_ID is not set. Please create a test app and set the environment variable."); + } + // テスト前に既存レコードをクリアして確実にクリーンな状態から開始 + app.deleteAllRecords(); + } + + @AfterEach + public void cleanupRecords() { + if (app != null) { + app.deleteAllRecords(); + } + } + + private List setupRecords(String textFieldCode, int size) { + return IntStream.range(0, size) + .mapToObj( + i -> new Record().putField(textFieldCode, new SingleLineTextFieldValue("value " + i))) + .collect(Collectors.toList()); + } + + @Test + public void addRecord() { + Record record = setupRecords(TEXT_FIELD_CODE, 1).get(0); + AddRecordRequest req = new AddRecordRequest(); + req.setApp(app.id()); + req.setRecord(record); + AddRecordResponseBody resp = client.record().addRecord(req); + assertThat(resp.getRevision()).isEqualTo(1L); + + List records = app.getRecords(); + assertThat(records).hasSize(1); + assertThat(records.get(0).getId()).isEqualTo(resp.getId()); + assertThat(records.get(0).getSingleLineTextFieldValue(TEXT_FIELD_CODE)).isEqualTo("value 0"); + } + + @Test + public void addRecordComment() { + long recordId = app.addRecord(); + + RecordComment comment = new RecordComment("Record Comment"); + List mentions = new ArrayList<>(); + mentions.add(new Entity(EntityType.USER, Users.user1.getCode())); + comment.setMentions(mentions); + AddRecordCommentRequest req = + new AddRecordCommentRequest().setApp(app.id()).setRecord(recordId).setComment(comment); + long id; + try { + id = client.record().addRecordComment(req).getId(); + } catch (com.kintone.client.exception.KintoneApiRuntimeException e) { + if (e.getMessage().contains("GAIA_RE12")) { + System.out.println("Skipping addRecordComment: Comment feature is disabled on this app"); + return; + } + throw e; + } + assertThat(id).isEqualTo(1L); + + List comments = client.record().getRecordComments(app.id(), recordId); + assertThat(comments).hasSize(1); + assertThat(comments.get(0).getId()).isEqualTo(id); + assertThat(comments.get(0).getCreator().getCode()).isEqualTo(getDefaultUser()); + assertThat(comments.get(0).getText()) + .isEqualTo(Users.user1.getName() + " \n" + "Record Comment "); + assertThat(comments.get(0).getMentions()) + .usingRecursiveFieldByFieldElementComparator() + .containsExactly(new Entity(EntityType.USER, Users.user1.getCode())); + } + + @Test + public void addRecords() { + AddRecordsRequest req = new AddRecordsRequest(); + req.setApp(app.id()); + req.setRecords(setupRecords(TEXT_FIELD_CODE, 2)); + AddRecordsResponseBody resp = client.record().addRecords(req); + assertThat(resp.getIds()).hasSize(2); + assertThat(resp.getRevisions()).containsExactly(1L, 1L); + + List records = app.getRecords(); + assertThat(records).hasSize(2); + assertThat(records.get(0).getSingleLineTextFieldValue(TEXT_FIELD_CODE)).isEqualTo("value 1"); + assertThat(records.get(1).getSingleLineTextFieldValue(TEXT_FIELD_CODE)).isEqualTo("value 0"); + } + + @Test + public void createCursor_getRecordsByCursor_deleteCursor() { + List recordIds = app.addRecords(setupRecords(TEXT_FIELD_CODE, 5)); + long firstRecordId = recordIds.get(0); + + CreateCursorRequest req1 = new CreateCursorRequest(); + req1.setApp(app.id()); + req1.setFields(Arrays.asList("$id", TEXT_FIELD_CODE)); + req1.setQuery("$id > " + firstRecordId); + req1.setSize(3L); + CreateCursorResponseBody resp1 = client.record().createCursor(req1); + assertThat(resp1.getTotalCount()).isEqualTo(4); + String cursorId = resp1.getId(); + + GetRecordsByCursorRequest req2 = new GetRecordsByCursorRequest(); + req2.setId(cursorId); + GetRecordsByCursorResponseBody resp2 = client.record().getRecordsByCursor(req2); + List records = resp2.getRecords(); + assertThat(records).hasSize(3); + assertThat(records.get(0).getSingleLineTextFieldValue(TEXT_FIELD_CODE)).isEqualTo("value 4"); + assertThat(records.get(0).getFieldCodes(false)).containsExactly(TEXT_FIELD_CODE); + assertThat(records.get(1).getSingleLineTextFieldValue(TEXT_FIELD_CODE)).isEqualTo("value 3"); + assertThat(records.get(1).getFieldCodes(false)).containsExactly(TEXT_FIELD_CODE); + assertThat(records.get(2).getSingleLineTextFieldValue(TEXT_FIELD_CODE)).isEqualTo("value 2"); + assertThat(records.get(2).getFieldCodes(false)).containsExactly(TEXT_FIELD_CODE); + + DeleteCursorRequest req3 = new DeleteCursorRequest(); + req3.setId(cursorId); + client.record().deleteCursor(req3); + } + + @Test + public void deleteRecordComment() { + long recordId = app.addRecord(); + long commentId; + try { + commentId = + client.record().addRecordComment(app.id(), recordId, new RecordComment("comment")); + } catch (com.kintone.client.exception.KintoneApiRuntimeException e) { + if (e.getMessage().contains("GAIA_RE12")) { + System.out.println("Skipping deleteRecordComment: Comment feature is disabled on this app"); + return; + } + throw e; + } + + DeleteRecordCommentRequest req = + new DeleteRecordCommentRequest().setApp(app.id()).setRecord(recordId).setComment(commentId); + client.record().deleteRecordComment(req); + + List comments = client.record().getRecordComments(app.id(), recordId); + assertThat(comments).isEmpty(); + } + + @Test + public void deleteRecords() { + List recordIds = app.addRecords(setupRecords(TEXT_FIELD_CODE, 3)); + + DeleteRecordsRequest req = new DeleteRecordsRequest(); + req.setApp(app.id()); + req.setIds(recordIds); + req.setRevisions(Arrays.asList(1L, 1L, 1L)); + client.record().deleteRecords(req); + + assertThat(app.getRecords()).isEmpty(); + } + + @Test + public void getRecord() { + FieldProperty field = app.field(TEXT_FIELD_CODE); + long recordId = app.addRecord(field, "text"); + + GetRecordRequest req = new GetRecordRequest(); + req.setApp(app.id()); + req.setId(recordId); + GetRecordResponseBody resp = client.record().getRecord(req); + assertThat(resp.getRecord().getId()).isEqualTo(recordId); + assertThat(resp.getRecord().getSingleLineTextFieldValue(TEXT_FIELD_CODE)).isEqualTo("text"); + } + + @Test + public void getRecordComments() { + long recordId = app.addRecord(); + try { + for (int i = 0; i < 4; i++) { + RecordComment comment = new RecordComment("comment " + i); + client.record().addRecordComment(app.id(), recordId, comment); + } + } catch (com.kintone.client.exception.KintoneApiRuntimeException e) { + if (e.getMessage().contains("GAIA_RE12")) { + System.out.println("Skipping getRecordComments: Comment feature is disabled on this app"); + return; + } + throw e; + } + + GetRecordCommentsRequest req = new GetRecordCommentsRequest(); + req.setApp(app.id()); + req.setRecord(recordId); + req.setLimit(2L); + req.setOffset(1L); + req.setOrder(Order.DESC); + GetRecordCommentsResponseBody resp = client.record().getRecordComments(req); + List comments = resp.getComments(); + assertThat(comments).hasSize(2); + assertThat(comments.get(0).getId()).isEqualTo(3L); + assertThat(comments.get(0).getText()).startsWith("comment 2"); + assertThat(comments.get(1).getId()).isEqualTo(2L); + assertThat(comments.get(1).getText()).startsWith("comment 1"); + } + + @Test + public void getRecords() { + List recordIds = app.addRecords(setupRecords(TEXT_FIELD_CODE, 5)); + long firstRecordId = recordIds.get(0); + + GetRecordsRequest req = new GetRecordsRequest(); + req.setApp(app.id()); + req.setFields(Arrays.asList("$id", TEXT_FIELD_CODE)); + req.setQuery("$id > " + firstRecordId + " limit 3"); + req.setTotalCount(true); + GetRecordsResponseBody resp = client.record().getRecords(req); + assertThat(resp.getTotalCount()).isEqualTo(4); + List records = resp.getRecords(); + assertThat(records).hasSize(3); + assertThat(records.get(0).getSingleLineTextFieldValue(TEXT_FIELD_CODE)).isEqualTo("value 4"); + assertThat(records.get(0).getFieldCodes(false)).containsExactly(TEXT_FIELD_CODE); + assertThat(records.get(1).getSingleLineTextFieldValue(TEXT_FIELD_CODE)).isEqualTo("value 3"); + assertThat(records.get(1).getFieldCodes(false)).containsExactly(TEXT_FIELD_CODE); + assertThat(records.get(2).getSingleLineTextFieldValue(TEXT_FIELD_CODE)).isEqualTo("value 2"); + assertThat(records.get(2).getFieldCodes(false)).containsExactly(TEXT_FIELD_CODE); + } + + @Test + public void updateRecord() { + FieldProperty field = app.field(TEXT_FIELD_CODE); + long recordId = app.addRecord(field, "initial value"); + + UpdateRecordRequest req = new UpdateRecordRequest(); + req.setApp(app.id()); + req.setId(recordId); + req.setRevision(1L); + req.setRecord(setupRecords(TEXT_FIELD_CODE, 1).get(0)); + UpdateRecordResponseBody resp = client.record().updateRecord(req); + assertThat(resp.getRevision()).isEqualTo(2L); + + List records = app.getRecords(); + assertThat(records).hasSize(1); + assertThat(records.get(0).getId()).isEqualTo(recordId); + assertThat(records.get(0).getSingleLineTextFieldValue(TEXT_FIELD_CODE)).isEqualTo("value 0"); + } + + @Test + public void updateRecord_updateKey() { + FieldProperty key = app.field(KEY_FIELD_CODE); + FieldProperty field = app.field(TEXT_FIELD_CODE); + long recordId1 = app.addRecord(key, "abc", field, "initial value 0"); + long recordId2 = app.addRecord(key, "def", field, "initial value 1"); + + UpdateRecordRequest req = new UpdateRecordRequest(); + req.setApp(app.id()); + req.setUpdateKey(new UpdateKey(KEY_FIELD_CODE, "abc")); + req.setRevision(1L); + req.setRecord(setupRecords(TEXT_FIELD_CODE, 1).get(0)); + UpdateRecordResponseBody resp = client.record().updateRecord(req); + assertThat(resp.getRevision()).isEqualTo(2L); + + List records = app.getRecords(); + assertThat(records).hasSize(2); + assertThat(records.get(0).getId()).isEqualTo(recordId2); + assertThat(records.get(0).getSingleLineTextFieldValue(TEXT_FIELD_CODE)) + .isEqualTo("initial value 1"); + assertThat(records.get(1).getId()).isEqualTo(recordId1); + assertThat(records.get(1).getSingleLineTextFieldValue(TEXT_FIELD_CODE)).isEqualTo("value 0"); + } + + @Test + public void updateRecordAssignees() { + // このテストにはプロセス管理が必要(作業者の更新はプロセス管理が有効な場合のみ使用可能) + app.applyExampleProcessManagement().deploy(); + + try { + long recordId = app.addRecord(); + + UpdateRecordAssigneesRequest req = new UpdateRecordAssigneesRequest(); + req.setApp(app.id()); + req.setId(recordId); + req.setAssignees(Collections.singletonList(Users.cybozu.getCode())); + req.setRevision(1L); + UpdateRecordAssigneesResponseBody resp = client.record().updateRecordAssignees(req); + assertThat(resp.getRevision()).isEqualTo(2L); + + List records = app.getRecords(); + assertThat(records).hasSize(1); + assertThat(records.get(0).getId()).isEqualTo(recordId); + List assignees = records.get(0).getStatusAssigneeFieldValue(); + List codes = assignees.stream().map(User::getCode).collect(Collectors.toList()); + assertThat(codes).containsExactly(Users.cybozu.getCode()); + } finally { + // プロセス管理を無効化してクリーンな状態に戻す + app.updateProcessManagement(new ProcessManagementBuilder().enable(false)).deploy(); + } + } + + @Test + public void updateRecords() { + FieldProperty key = app.field(KEY_FIELD_CODE); + FieldProperty field = app.field(TEXT_FIELD_CODE); + long recordId1 = app.addRecord(key, "abc", field, "initial value 0"); + long recordId2 = app.addRecord(key, "def", field, "initial value 1"); + + RecordForUpdate up1 = + new RecordForUpdate( + recordId1, + new Record().putField(TEXT_FIELD_CODE, new SingleLineTextFieldValue("value 0")), + 1L); + RecordForUpdate up2 = + new RecordForUpdate( + new UpdateKey(KEY_FIELD_CODE, "def"), + new Record().putField(TEXT_FIELD_CODE, new SingleLineTextFieldValue("value 1")), + 1L); + UpdateRecordsRequest req = new UpdateRecordsRequest(); + req.setApp(app.id()); + req.setRecords(Arrays.asList(up1, up2)); + UpdateRecordsResponseBody resp = client.record().updateRecords(req); + assertThat(resp.getRecords()).hasSize(2); + + List records = app.getRecords(); + assertThat(records).hasSize(2); + assertThat(records.get(0).getId()).isEqualTo(recordId2); + assertThat(records.get(0).getSingleLineTextFieldValue(TEXT_FIELD_CODE)).isEqualTo("value 1"); + assertThat(records.get(1).getId()).isEqualTo(recordId1); + assertThat(records.get(1).getSingleLineTextFieldValue(TEXT_FIELD_CODE)).isEqualTo("value 0"); + } + + @Test + public void updateRecordStatus() { + // このテストにはプロセス管理が必要 + app.applyExampleProcessManagement().deploy(); + + try { + long recordId = app.addRecord(); + + UpdateRecordStatusRequest req = new UpdateRecordStatusRequest(); + req.setApp(app.id()); + req.setId(recordId); + req.setAction("action 1"); + req.setRevision(1L); + UpdateRecordStatusResponseBody resp = client.record().updateRecordStatus(req); + assertThat(resp.getRevision()).isEqualTo(3L); + + List records = app.getRecords(); + assertThat(records).hasSize(1); + assertThat(records.get(0).getId()).isEqualTo(recordId); + assertThat(records.get(0).getStatusFieldValue()).isEqualTo("state B"); + } finally { + // プロセス管理を無効化してクリーンな状態に戻す + app.updateProcessManagement(new ProcessManagementBuilder().enable(false)).deploy(); + } + } + + @Test + public void updateRecordStatuses() { + // このテストにはプロセス管理が必要 + app.applyExampleProcessManagement().deploy(); + + try { + long recordId1 = app.addRecord(); + long recordId2 = app.addRecord(); + + StatusAction a1 = new StatusAction().setId(recordId1).setAction("action 1").setRevision(1L); + StatusAction a2 = new StatusAction().setId(recordId2).setAction("action 1").setRevision(1L); + UpdateRecordStatusesRequest req = new UpdateRecordStatusesRequest(); + req.setApp(app.id()); + req.setRecords(Arrays.asList(a1, a2)); + UpdateRecordStatusesResponseBody resp = client.record().updateRecordStatuses(req); + List revisions = resp.getRecords(); + assertThat(revisions).hasSize(2); + assertThat(revisions.get(0).getId()).isEqualTo(recordId1); + assertThat(revisions.get(0).getRevision()).isEqualTo(3L); + assertThat(revisions.get(1).getId()).isEqualTo(recordId2); + assertThat(revisions.get(1).getRevision()).isEqualTo(3L); + + List records = app.getRecords(); + assertThat(records).hasSize(2); + assertThat(records.get(0).getId()).isEqualTo(recordId2); + assertThat(records.get(0).getStatusFieldValue()).isEqualTo("state B"); + assertThat(records.get(1).getId()).isEqualTo(recordId1); + assertThat(records.get(1).getStatusFieldValue()).isEqualTo("state B"); + } finally { + // プロセス管理を無効化してクリーンな状態に戻す + app.updateProcessManagement(new ProcessManagementBuilder().enable(false)).deploy(); + } + } + + @Test + public void upsertRecords() { + FieldProperty key = app.field(KEY_FIELD_CODE); + FieldProperty field = app.field(TEXT_FIELD_CODE); + + // Insert: 新規レコードを作成 + long existingRecordId = app.addRecord(key, "existing_key", field, "initial value"); + + // Upsert: 既存レコード (updateKeyで更新) と新規レコード (INSERT) を同時に処理 + RecordForUpdate updateExisting = + new RecordForUpdate( + new UpdateKey(KEY_FIELD_CODE, "existing_key"), + new Record().putField(TEXT_FIELD_CODE, new SingleLineTextFieldValue("updated value"))); + RecordForUpdate insertNew = + new RecordForUpdate( + new UpdateKey(KEY_FIELD_CODE, "new_key"), + new Record().putField(TEXT_FIELD_CODE, new SingleLineTextFieldValue("new value"))); + + UpsertRecordsRequest req = new UpsertRecordsRequest(); + req.setApp(app.id()); + req.setRecords(Arrays.asList(updateExisting, insertNew)); + UpsertRecordsResponseBody resp = client.record().upsertRecords(req); + + List results = resp.getRecords(); + assertThat(results).hasSize(2); + + // 既存レコードの更新結果 + assertThat(results.get(0).getId()).isEqualTo(existingRecordId); + assertThat(results.get(0).getRevision()).isEqualTo(2L); + assertThat(results.get(0).getOperation()).isEqualTo(RecordOperationType.UPDATE); + + // 新規レコードの作成結果 + assertThat(results.get(1).getId()).isGreaterThan(existingRecordId); + assertThat(results.get(1).getRevision()).isEqualTo(1L); + assertThat(results.get(1).getOperation()).isEqualTo(RecordOperationType.INSERT); + + // レコードの内容を確認 + List records = app.getRecords(); + assertThat(records).hasSize(2); + + Record updatedRecord = + records.stream() + .filter(r -> r.getId() == existingRecordId) + .findFirst() + .orElseThrow(AssertionError::new); + assertThat(updatedRecord.getSingleLineTextFieldValue(KEY_FIELD_CODE)).isEqualTo("existing_key"); + assertThat(updatedRecord.getSingleLineTextFieldValue(TEXT_FIELD_CODE)) + .isEqualTo("updated value"); + + Record newRecord = + records.stream() + .filter(r -> r.getId() == results.get(1).getId()) + .findFirst() + .orElseThrow(AssertionError::new); + assertThat(newRecord.getSingleLineTextFieldValue(KEY_FIELD_CODE)).isEqualTo("new_key"); + assertThat(newRecord.getSingleLineTextFieldValue(TEXT_FIELD_CODE)).isEqualTo("new value"); + } +} diff --git a/e2e-tests/src/test/java/com/kintone/client/schema/SchemaApiTest.java b/e2e-tests/src/test/java/com/kintone/client/schema/SchemaApiTest.java new file mode 100644 index 0000000..3cdb7e7 --- /dev/null +++ b/e2e-tests/src/test/java/com/kintone/client/schema/SchemaApiTest.java @@ -0,0 +1,55 @@ +package com.kintone.client.schema; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.kintone.client.ApiTestBase; +import com.kintone.client.KintoneClient; +import com.kintone.client.api.schema.GetApiListResponseBody; +import com.kintone.client.api.schema.GetApiSchemaResponseBody; +import com.kintone.client.model.schema.ApiSchemaLink; +import com.kintone.client.model.schema.RequestSchema; +import com.kintone.client.model.schema.ResponseSchema; +import java.util.Map; +import org.junit.jupiter.api.Test; + +/** SchemaClientのテスト */ +public class SchemaApiTest extends ApiTestBase { + @Test + public void getApiList() { + KintoneClient client = setupDefaultClient(); + GetApiListResponseBody resp = client.schema().getApiList(); + + String baseUrl = getBaseURL() + "/k/v1/"; + assertThat(resp.getBaseUrl()).isEqualTo(baseUrl); + + Map apis = resp.getApis(); + assertThat(apis).hasSizeGreaterThan(1); + // すべて確認するのは大変なので1件だけ確認 + ApiSchemaLink schema = apis.get("app/get"); + assertThat(schema.getLink()).isEqualTo("apis/app/get.json"); + } + + @Test + public void getApiSchema() { + // すべて確認するのは大変なので比較的簡単なPOST records/cursor.jsonを確認 + KintoneClient client = setupDefaultClient(); + GetApiSchemaResponseBody resp = client.schema().getApiSchema("apis/records/cursor/post.json"); + + String baseUrl = getBaseURL() + "/k/v1/"; + assertThat(resp.getBaseUrl()).isEqualTo(baseUrl); + assertThat(resp.getHttpMethod()).isEqualTo("POST"); + assertThat(resp.getPath()).isEqualTo("records/cursor.json"); + assertThat(resp.getId()).isEqualTo("records/cursor/post"); + + RequestSchema requestSchema = resp.getRequest(); + assertThat(requestSchema.getType()).isEqualTo("object"); + assertThat(requestSchema.getRequired()).containsExactly("app"); + assertThat(requestSchema.getProperties()).containsOnlyKeys("app", "size", "query", "fields"); + + ResponseSchema responseSchema = resp.getResponse(); + assertThat(responseSchema.getType()).isEqualTo("object"); + assertThat(responseSchema.getProperties()).containsOnlyKeys("id", "totalCount"); + + assertThat(resp.getSchemas()).isEmpty(); + } +} diff --git a/e2e-tests/src/test/java/com/kintone/client/space/SpaceApiTest.java b/e2e-tests/src/test/java/com/kintone/client/space/SpaceApiTest.java new file mode 100644 index 0000000..7505151 --- /dev/null +++ b/e2e-tests/src/test/java/com/kintone/client/space/SpaceApiTest.java @@ -0,0 +1,295 @@ +package com.kintone.client.space; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.kintone.client.ApiTestBase; +import com.kintone.client.KintoneClient; +import com.kintone.client.TestSettings; +import com.kintone.client.Users; +import com.kintone.client.api.space.*; +import com.kintone.client.exception.KintoneApiRuntimeException; +import com.kintone.client.helper.Space; +import com.kintone.client.model.Entity; +import com.kintone.client.model.EntityType; +import com.kintone.client.model.space.AddedSpaceMember; +import com.kintone.client.model.space.GuestUser; +import com.kintone.client.model.space.SpaceMember; +import com.kintone.client.model.space.SpaceStatistics; +import com.kintone.client.model.space.ThreadComment; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.TimeZone; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +public class SpaceApiTest extends ApiTestBase { + + @Test + public void addGuests_deleteGuests() { + List codes = Arrays.asList("guest1@localhost", "guest2@localhost"); + KintoneClient client = setupDefaultClient(); + deleteGuestUsers(client, codes); + + List users = + Arrays.asList(createGuest("guest1@localhost"), createGuest("guest2@localhost")); + AddGuestsRequest req1 = new AddGuestsRequest(); + req1.setGuests(users); + AddGuestsResponseBody resp1 = client.space().addGuests(req1); + + DeleteGuestsRequest req2 = new DeleteGuestsRequest(); + req2.setGuests(codes); + DeleteGuestsResponseBody resp2 = client.space().deleteGuests(req2); + } + + @Test + public void addSpaceFromTemplate() { + KintoneClient client = setupDefaultClient(); + + SpaceMember member = new SpaceMember(); + member.setEntity(new Entity(EntityType.USER, getDefaultUser())); + member.setIsAdmin(true); + + Long templateId = TestSettings.get().getTemplateId(); + + if (templateId == null) { + System.out.println("Skipping addSpaceFromTemplate: KINTONE_TEMPLATE_ID is not set"); + return; + } + + AddSpaceFromTemplateRequest req = new AddSpaceFromTemplateRequest(); + req.setId(templateId); + req.setName("addSpaceFromTemplate_copy_" + System.currentTimeMillis()); + req.setMembers(Collections.singletonList(member)); + req.setFixedMember(true); + req.setIsGuest(false); + req.setIsPrivate(true); + AddSpaceFromTemplateResponseBody resp = client.space().addSpaceFromTemplate(req); + assertThat(resp.getId()).isGreaterThan(0); + + client.space().deleteSpace(resp.getId()); + } + + @Test + public void addThread() { + Space space = Space.multiThread(this); + KintoneClient client = setupDefaultClient(); + + AddThreadRequest req = new AddThreadRequest(); + req.setSpace(space.id()); + String newThreadName = "added thread name via java client " + System.currentTimeMillis(); + req.setName(newThreadName); + AddThreadResponseBody resp = client.space().addThread(req); + assertThat(resp.getId()).isGreaterThan(0); + } + + @Test + public void addThreadComment() { + KintoneClient client = setupDefaultClient(); + Space space = Space.singleThread(this); + + AddThreadCommentRequest req = new AddThreadCommentRequest(); + req.setSpace(space.id()); + req.setThread(space.getDefaultThread()); + req.setComment(new ThreadComment().setText("comment! " + System.currentTimeMillis())); + AddThreadCommentResponseBody resp = client.space().addThreadComment(req); + assertThat(resp.getId()).isGreaterThan(0L); + } + + @Test + @Disabled( + "deleteSpace requires space recreation which is not available with pre-created resources") + public void deleteSpace() {} + + @Test + public void getSpace() { + Space space = Space.singleThread(this); + KintoneClient client = setupDefaultClient(); + + GetSpaceRequest req = new GetSpaceRequest(); + req.setId(space.id()); + GetSpaceResponseBody resp = client.space().getSpace(req); + assertThat(resp.getId()).isEqualTo(space.id()); + assertThat(resp.getName()).isNotEmpty(); + } + + @Test + public void getSpaceMembers() { + Space space = Space.singleThread(this); + KintoneClient client = setupDefaultClient(); + + GetSpaceMembersRequest req = new GetSpaceMembersRequest(); + req.setId(space.id()); + GetSpaceMembersResponseBody resp = client.space().getSpaceMembers(req); + List members = resp.getMembers(); + assertThat(members).isNotEmpty(); + } + + @Test + public void updateSpace() { + Space space = Space.singleThread(this); + KintoneClient client = setupDefaultClient(); + + GetSpaceResponseBody resp1 = client.space().getSpace(space.id()); + String originalName = resp1.getName(); + + UpdateSpaceRequest req = new UpdateSpaceRequest(); + req.setId(space.id()); + String newName = "updated_" + System.currentTimeMillis(); + req.setName(newName); + client.space().updateSpace(req); + + GetSpaceResponseBody resp2 = client.space().getSpace(space.id()); + assertThat(resp2.getName()).isEqualTo(newName); + + UpdateSpaceRequest restoreReq = new UpdateSpaceRequest(); + restoreReq.setId(space.id()); + restoreReq.setName(originalName); + client.space().updateSpace(restoreReq); + } + + @Test + public void updateSpaceBody() { + Space space = Space.multiThread(this); + KintoneClient client = setupDefaultClient(); + + UpdateSpaceBodyRequest req = new UpdateSpaceBodyRequest(); + req.setId(space.id()); + String newBody = "Space Body " + System.currentTimeMillis(); + req.setBody(newBody); + UpdateSpaceBodyResponseBody resp = client.space().updateSpaceBody(req); + + assertThat(client.space().getSpace(space.id()).getBody()).isEqualTo(newBody); + + // 元のボディに添付ファイル参照があると復元できないため、シンプルな空文字列で復元 + // テスト用スペースなので問題なし + UpdateSpaceBodyRequest restoreReq = new UpdateSpaceBodyRequest(); + restoreReq.setId(space.id()); + restoreReq.setBody(""); + client.space().updateSpaceBody(restoreReq); + } + + @Test + public void updateSpaceGuests() { + List codes = Arrays.asList("guest1@localhost", "guest2@localhost"); + KintoneClient client = setupDefaultClient(); + deleteGuestUsers(client, codes); + + List users = + Arrays.asList(createGuest("guest1@localhost"), createGuest("guest2@localhost")); + client.space().addGuests(users); + + Space space = Space.guest(this); + + KintoneClient guestSpaceClient = setupDefaultClient(space.id()); + UpdateSpaceGuestsRequest req = new UpdateSpaceGuestsRequest(); + req.setId(space.id()); + req.setGuests(codes); + UpdateSpaceGuestsResponseBody resp1 = guestSpaceClient.space().updateSpaceGuests(req); + + UpdateSpaceGuestsRequest resetReq = new UpdateSpaceGuestsRequest(); + resetReq.setId(space.id()); + resetReq.setGuests(Collections.emptyList()); + guestSpaceClient.space().updateSpaceGuests(resetReq); + + deleteGuestUsers(client, codes); + } + + @Test + public void updateSpaceMembers() { + Space space = Space.singleThread(this); + KintoneClient client = setupDefaultClient(); + + List originalMembers = client.space().getSpaceMembers(space.id()); + + SpaceMember member1 = + new SpaceMember().setEntity(new Entity(EntityType.USER, getDefaultUser())).setIsAdmin(true); + SpaceMember member2 = + new SpaceMember().setEntity(new Entity(EntityType.USER, Users.user1.getCode())); + SpaceMember member3 = + new SpaceMember().setEntity(new Entity(EntityType.GROUP, "everyone")).setIsAdmin(true); + + UpdateSpaceMembersRequest req = new UpdateSpaceMembersRequest(); + req.setId(space.id()); + req.setMembers(Arrays.asList(member1, member2, member3)); + UpdateSpaceMembersResponseBody resp = client.space().updateSpaceMembers(req); + + List updatedMembers = client.space().getSpaceMembers(space.id()); + assertThat(updatedMembers.size()).isGreaterThanOrEqualTo(2); + + List restoreMembers = new java.util.ArrayList<>(); + for (AddedSpaceMember m : originalMembers) { + SpaceMember sm = new SpaceMember(); + sm.setEntity(m.getEntity()); + sm.setIsAdmin(m.isAdmin()); + restoreMembers.add(sm); + } + UpdateSpaceMembersRequest restoreReq = new UpdateSpaceMembersRequest(); + restoreReq.setId(space.id()); + restoreReq.setMembers(restoreMembers); + client.space().updateSpaceMembers(restoreReq); + } + + @Test + public void updateThread() { + Space space = Space.multiThread(this); + KintoneClient client = setupDefaultClient(); + + UpdateThreadRequest req = new UpdateThreadRequest(); + req.setId(space.getDefaultThread()); + String newName = "Thread Name " + System.currentTimeMillis(); + req.setName(newName); + req.setBody("Thread Body"); + UpdateThreadResponseBody resp = client.space().updateThread(req); + } + + private GuestUser createGuest(String code) { + String name = code.split("@")[0]; + GuestUser user = new GuestUser(); + user.setCode(code); + user.setName(name); + user.setPassword("password123"); + user.setTimezone(TimeZone.getTimeZone("Asia/Tokyo")); + return user; + } + + private void deleteGuestUsers(KintoneClient client, List codes) { + for (String code : codes) { + try { + client.space().deleteGuests(Collections.singletonList(code)); + } catch (KintoneApiRuntimeException e) { + System.out.println("ignore deleting guest error: " + e.getMessage()); + } + } + + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + + @Test + @Disabled("Skipped until SpaceStatistics model is updated for new API fields") + public void getStatistics() { + KintoneClient client = setupDefaultClient(); + Space space = Space.singleThread(this); + + GetSpacesStatisticsRequest req = new GetSpacesStatisticsRequest(); + req.setLimit(100L); + req.setOffset(0L); + GetSpacesStatisticsResponseBody resp = client.space().getStatistics(req); + + List spaces = resp.getSpaces(); + assertThat(spaces).isNotEmpty(); + + Optional targetSpace = + spaces.stream().filter(s -> s.getId() == space.id()).findFirst(); + assertThat(targetSpace).isPresent(); + + SpaceStatistics stats = targetSpace.get(); + assertThat(stats.getId()).isEqualTo(space.id()); + assertThat(stats.getName()).isNotEmpty(); + } +} diff --git a/e2e-tests/src/test/resources/com/kintone/client/app/fileicon.png b/e2e-tests/src/test/resources/com/kintone/client/app/fileicon.png new file mode 100644 index 0000000..0abb80f Binary files /dev/null and b/e2e-tests/src/test/resources/com/kintone/client/app/fileicon.png differ diff --git a/e2e-tests/src/test/resources/com/kintone/client/plugin/plugin-a.zip b/e2e-tests/src/test/resources/com/kintone/client/plugin/plugin-a.zip new file mode 100644 index 0000000..0a36b78 Binary files /dev/null and b/e2e-tests/src/test/resources/com/kintone/client/plugin/plugin-a.zip differ diff --git a/e2e-tests/src/test/resources/com/kintone/client/plugin/plugin-b.zip b/e2e-tests/src/test/resources/com/kintone/client/plugin/plugin-b.zip new file mode 100644 index 0000000..168829b Binary files /dev/null and b/e2e-tests/src/test/resources/com/kintone/client/plugin/plugin-b.zip differ diff --git a/e2e-tests/src/test/resources/com/kintone/client/plugin/plugin-c.zip b/e2e-tests/src/test/resources/com/kintone/client/plugin/plugin-c.zip new file mode 100644 index 0000000..e4bd92b Binary files /dev/null and b/e2e-tests/src/test/resources/com/kintone/client/plugin/plugin-c.zip differ diff --git a/e2e-tests/src/test/resources/logback-test.xml b/e2e-tests/src/test/resources/logback-test.xml new file mode 100644 index 0000000..e56fae3 --- /dev/null +++ b/e2e-tests/src/test/resources/logback-test.xml @@ -0,0 +1,14 @@ + + + + %d [%thread] %logger %msg%n + + + + + + + + + +